use std::sync::Arc;
use axum::extract::State;
use axum::http::StatusCode;
use axum::{Json, body::Body, http::Request};
use serde_json::Value;
use tempfile::TempDir;
use tower::ServiceExt;
use super::{CoordinatorChatRequest, coordinator_chat, router};
use crate::core::sm::SessionManagerAgent;
use crate::core::sm::agent::mock::{MockChatProvider, MockResolver};
use crate::core::sm::config::SessionManagerConfig;
use crate::daemon::state::DaemonState;
fn enabled_sm() -> SessionManagerConfig {
SessionManagerConfig {
enabled: true,
..SessionManagerConfig::default()
}
}
fn state_with_sm(resolver: Arc<MockResolver>, tmp: &TempDir) -> Arc<DaemonState> {
let agent = Arc::new(SessionManagerAgent::for_test(
enabled_sm(),
resolver,
tmp.path().to_path_buf(),
));
Arc::new(DaemonState::with_session_manager_agent(agent))
}
#[tokio::test]
async fn sm_chat_returns_reply_and_cost() {
let tmp = TempDir::new().unwrap();
let provider = MockChatProvider::new("SM plan ready", 0.0123);
let state = state_with_sm(Arc::new(MockResolver::with_provider(provider)), &tmp);
let resp = coordinator_chat(
State(state),
Json(CoordinatorChatRequest {
message: "plan the migration".into(),
history: Vec::new(),
conv_id: Some("endpoint-conv".into()),
}),
)
.await
.expect("SM chat succeeds with a provider");
assert_eq!(resp.reply, "SM plan ready");
assert_eq!(resp.cost, Some(0.0123));
assert_eq!(resp.conv_id.as_deref(), Some("endpoint-conv"));
assert!(resp.routed_to_session.is_none());
assert!(resp.command_output.is_none());
}
#[tokio::test]
async fn sm_chat_degraded_is_503() {
let tmp = TempDir::new().unwrap();
let state = state_with_sm(Arc::new(MockResolver::degraded()), &tmp);
let err = coordinator_chat(
State(state),
Json(CoordinatorChatRequest {
message: "anything".into(),
history: Vec::new(),
conv_id: None,
}),
)
.await
.unwrap_err();
assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn disabled_sm_falls_back_to_legacy_503() {
let tmp = TempDir::new().unwrap();
let disabled_agent = Arc::new(SessionManagerAgent::for_test(
SessionManagerConfig::default(), Arc::new(MockResolver::with_provider(MockChatProvider::new(
"unused", 0.0,
))),
tmp.path().to_path_buf(),
));
let state = Arc::new(DaemonState::with_session_manager_agent(disabled_agent));
assert!(!state.session_manager_agent().is_enabled());
let err = coordinator_chat(
State(state),
Json(CoordinatorChatRequest {
message: "what is happening?".into(),
history: Vec::new(),
conv_id: None,
}),
)
.await
.unwrap_err();
assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE);
}
#[tokio::test]
async fn session_manager_chat_alias_matches_coordinator() {
let tmp = TempDir::new().unwrap();
let provider = MockChatProvider::new("aliased reply", 0.005);
let provider_handle = provider.clone();
let state = state_with_sm(Arc::new(MockResolver::with_provider(provider)), &tmp);
let body = serde_json::json!({ "message": "hi", "conv_id": "alias-conv" });
let post = |path: &str| {
let app = router(Arc::clone(&state));
let body = body.clone();
let path = path.to_string();
async move {
let resp = app
.oneshot(
Request::builder()
.method("POST")
.uri(path)
.header("content-type", "application/json")
.body(Body::from(body.to_string()))
.unwrap(),
)
.await
.unwrap();
let status = resp.status();
let bytes = axum::body::to_bytes(resp.into_body(), 1 << 20)
.await
.unwrap();
let json: Value = serde_json::from_slice(&bytes).unwrap();
(status, json)
}
};
let (coord_status, coord_json) = post("/api/v1/coordinator/chat").await;
let (sm_status, sm_json) = post("/api/v1/session-manager/chat").await;
assert_eq!(coord_status, StatusCode::OK);
assert_eq!(sm_status, StatusCode::OK);
assert_eq!(coord_json["reply"], sm_json["reply"]);
assert_eq!(coord_json["reply"], "aliased reply");
assert_eq!(coord_json["cost"], sm_json["cost"]);
assert_eq!(coord_json["conv_id"], sm_json["conv_id"]);
assert_eq!(
provider_handle.request_count(),
2,
"the shared provider must be invoked once per router call"
);
}
#[test]
fn doc13_tui_contract_is_preserved() {
let legacy: CoordinatorChatRequest =
serde_json::from_str(r#"{ "message": "hello", "history": [] }"#)
.expect("legacy request shape must still deserialize");
assert_eq!(legacy.message, "hello");
assert!(legacy.conv_id.is_none());
let resp = super::CoordinatorChatResponse {
reply: "ok".into(),
routed_to_session: None,
command_output: None,
cost: None,
conv_id: None,
};
let json: Value = serde_json::to_value(&resp).unwrap();
assert_eq!(json["reply"], "ok");
assert!(json.get("cost").is_none(), "cost must be absent when None");
assert!(
json.get("conv_id").is_none(),
"conv_id must be absent when None"
);
assert!(json.get("routed_to_session").is_none());
}