use super::*;
#[test]
fn base_url_is_stored() {
let client = DaemonClient::new("http://127.0.0.1:7880");
assert_eq!(client.base_url(), "http://127.0.0.1:7880");
}
#[test]
fn with_client_reuses_passed_client() {
let configured = reqwest::Client::new();
let client = DaemonClient::with_client(configured, "http://127.0.0.1:7880");
assert_eq!(client.base_url(), "http://127.0.0.1:7880");
}
#[test]
fn set_base_url_repoints_client() {
let mut client = DaemonClient::new("http://127.0.0.1:7880");
client.set_base_url("http://127.0.0.1:54321");
assert_eq!(client.base_url(), "http://127.0.0.1:54321");
}
#[tokio::test]
async fn launch_session_errors_when_daemon_unreachable() {
let client = DaemonClient::new("http://127.0.0.1:0");
let result = client.launch_session("/tmp/no-such-project").await;
assert!(result.is_err(), "expected launch to fail with no daemon");
}
#[tokio::test]
async fn connect_session_errors_when_daemon_unreachable() {
let client = DaemonClient::new("http://127.0.0.1:0");
let result = client.connect_session("/tmp/no-such-project").await;
assert!(result.is_err(), "expected connect to fail with no daemon");
}
#[test]
fn session_row_deserializes_tmux_name() {
let json = serde_json::json!({
"id": "abcd1234-5678-90ab-cdef-1234567890ab",
"workdir": "/tmp/proj",
"status": "Active",
"active_delegations": 1,
"tmux_name": "tmpm-quiet-falcon"
});
let row: SessionRow = serde_json::from_value(json).unwrap();
assert_eq!(row.tmux_name, "tmpm-quiet-falcon");
}
#[test]
fn session_row_defaults_tmux_name_when_absent() {
let json = serde_json::json!({
"id": "abcd1234-5678-90ab-cdef-1234567890ab",
"workdir": "/tmp/proj",
"status": "Active"
});
let row: SessionRow = serde_json::from_value(json).unwrap();
assert_eq!(row.tmux_name, "");
assert_eq!(row.last_seen.secs_since_epoch, 0);
}
#[test]
fn events_deserialize_from_record_shape() {
let json = serde_json::json!({
"session": "abcd1234-5678-90ab-cdef-1234567890ab",
"event": "PreToolUse",
"at": "2024-01-01T00:00:00Z",
"payload": {}
});
let row: EventRow = serde_json::from_value(json).unwrap();
assert_eq!(row.event, crate::core::hook::HookEvent::PreToolUse);
assert_eq!(row.at, "2024-01-01T00:00:00Z");
}
#[test]
fn events_default_payload_when_absent() {
let json = serde_json::json!({
"session": "abcd1234-5678-90ab-cdef-1234567890ab",
"event": "Stop",
"at": "2024-01-01T00:00:00Z"
});
let row: EventRow = serde_json::from_value(json).unwrap();
assert!(row.payload.is_null());
}
#[test]
fn breakers_deserialize_from_api_shape() {
let json = serde_json::json!({
"agent": "research",
"breaker": { "state": "closed", "consecutive_failures": 0 }
});
#[derive(serde::Deserialize)]
struct WireBreaker {
state: String,
consecutive_failures: u32,
}
#[derive(serde::Deserialize)]
struct WireRow {
agent: String,
breaker: WireBreaker,
}
let row: WireRow = serde_json::from_value(json).unwrap();
assert_eq!(row.agent, "research");
assert_eq!(row.breaker.state, "closed");
assert_eq!(row.breaker.consecutive_failures, 0);
}
#[test]
fn tmux_session_row_accepts_name() {
let obj = serde_json::json!({"name": "tmpm-quiet-falcon"});
assert_eq!(
obj.get("name").and_then(|v| v.as_str()),
Some("tmpm-quiet-falcon")
);
let plain = serde_json::json!("external-shell");
assert_eq!(plain.as_str(), Some("external-shell"));
}
#[test]
fn snapshot_text_handles_each_shape() {
assert_eq!(snapshot_text(&serde_json::json!("plain")), "plain");
assert_eq!(
snapshot_text(&serde_json::json!({"content": "from content"})),
"from content"
);
assert_eq!(
snapshot_text(&serde_json::json!({"lines": ["a", "b"]})),
"a\nb"
);
}
#[test]
fn pair_request_deserializes() {
let json = serde_json::json!({"code": "A4X9KZ", "expires_in_seconds": 300});
let req: PairRequest = serde_json::from_value(json).unwrap();
assert_eq!(req.code, "A4X9KZ");
assert_eq!(req.expires_in_seconds, 300);
}
#[test]
fn pair_confirm_deserializes_failure() {
let json = serde_json::json!({"success": false, "error": "invalid or expired code"});
let confirm: PairConfirm = serde_json::from_value(json).unwrap();
assert!(!confirm.success);
assert_eq!(confirm.error.as_deref(), Some("invalid or expired code"));
assert_eq!(confirm.chat_id, None);
}
#[test]
fn llm_chat_message_round_trips() {
let msg = ChatMessage {
role: "user".into(),
content: "hello".into(),
};
let json = serde_json::to_value(&msg).unwrap();
assert_eq!(json["role"], "user");
assert_eq!(json["content"], "hello");
let back: ChatMessage = serde_json::from_value(json).unwrap();
assert_eq!(back, msg);
}
#[test]
fn chat_message_constructors_set_role() {
assert_eq!(ChatMessage::user("x").role, "user");
assert_eq!(ChatMessage::assistant("y").role, "assistant");
assert_eq!(ChatMessage::user("x").content, "x");
}
#[test]
fn llm_chat_response_deserializes() {
let json = serde_json::json!({
"reply": "hi there",
"history": [
{ "role": "user", "content": "hello" },
{ "role": "assistant", "content": "hi there" },
],
});
let outcome: LlmChatOutcome = serde_json::from_value(json).unwrap();
assert_eq!(outcome.reply, "hi there");
assert_eq!(outcome.history.len(), 2);
assert_eq!(outcome.history[1].role, "assistant");
}
#[test]
fn coordinator_context_deserializes() {
let json = serde_json::json!({
"sessions": [{
"id": "00000000-0000-0000-0000-000000000000",
"name": "tmpm-aipowerranking",
"prefix": "aipowerranking",
"workdir": "/tmp/proj",
"status": "Active",
"active_delegations": 3,
"recent_output": ["building…"],
}],
"recent_events": [],
"generated_at": "2026-05-19T00:00:00Z",
});
let context: CoordinatorContext = serde_json::from_value(json).unwrap();
assert_eq!(context.sessions.len(), 1);
assert_eq!(context.sessions[0].prefix, "aipowerranking");
assert_eq!(context.sessions[0].active_delegations, 3);
assert_eq!(context.sessions[0].last_summary, None);
assert!(!context.sessions[0].summarizing);
}
#[test]
fn coordinator_session_carries_summary_fields() {
let json = serde_json::json!({
"sessions": [{
"id": "00000000-0000-0000-0000-000000000000",
"name": "tmpm-aipowerranking",
"prefix": "aipowerranking",
"workdir": "/tmp/proj",
"status": "Active",
"active_delegations": 0,
"recent_output": [],
"last_summary": "Writing the parser tests",
"summarizing": true,
}],
});
let context: CoordinatorContext = serde_json::from_value(json).unwrap();
assert_eq!(
context.sessions[0].last_summary.as_deref(),
Some("Writing the parser tests")
);
assert!(context.sessions[0].summarizing);
}
#[test]
fn coordinator_chat_outcome_deserializes() {
let json = serde_json::json!({
"reply": "Sent to tmpm-foo: run tests",
"routed_to_session": "tmpm-foo",
"command_output": "tests passed",
});
let outcome: CoordinatorChatOutcome = serde_json::from_value(json).unwrap();
assert_eq!(outcome.routed_to_session.as_deref(), Some("tmpm-foo"));
assert_eq!(outcome.command_output.as_deref(), Some("tests passed"));
let json = serde_json::json!({ "reply": "two sessions are active" });
let outcome: CoordinatorChatOutcome = serde_json::from_value(json).unwrap();
assert_eq!(outcome.reply, "two sessions are active");
assert!(outcome.routed_to_session.is_none());
assert!(outcome.actions_taken.is_none());
assert!(outcome.conv_id.is_none());
}
#[test]
fn coordinator_chat_outcome_deserializes_actions() {
let json = serde_json::json!({
"reply": "ran a health check and listed the fleet",
"actions_taken": ["sessions.health", "sessions.list"],
"conv_id": "conv-42",
});
let outcome: CoordinatorChatOutcome = serde_json::from_value(json).unwrap();
assert_eq!(
outcome.actions_taken.as_deref(),
Some(["sessions.health".to_string(), "sessions.list".to_string()].as_slice())
);
assert_eq!(outcome.conv_id.as_deref(), Some("conv-42"));
}
#[test]
fn coordinator_chat_serializes_actions_flag() {
let history = [ChatMessage::user("hi"), ChatMessage::assistant("hello")];
let body = coordinator_chat_body("spin up a session", &history, true);
assert_eq!(body["message"], "spin up a session");
assert_eq!(body["actions"], true);
assert_eq!(body["history"][0]["role"], "user");
assert_eq!(body["history"][1]["content"], "hello");
let passive = coordinator_chat_body("just chatting", &[], false);
assert_eq!(passive["actions"], false);
}
#[test]
fn pair_status_deserializes() {
let json = serde_json::json!({"paired": true, "chat_id": 12345678});
let status: PairStatus = serde_json::from_value(json).unwrap();
assert!(status.paired);
assert_eq!(status.chat_id, Some(12345678));
}
#[test]
fn catalog_stale_health_body_wire_shape() {
#[derive(serde::Deserialize)]
struct HealthBody {
#[serde(default)]
catalog_stale: bool,
}
let stale: HealthBody =
serde_json::from_value(serde_json::json!({"status":"ok","catalog_stale":true})).unwrap();
assert!(stale.catalog_stale);
let fresh: HealthBody =
serde_json::from_value(serde_json::json!({"status":"ok","catalog_stale":false})).unwrap();
assert!(!fresh.catalog_stale);
let legacy: HealthBody = serde_json::from_value(serde_json::json!({"status":"ok"})).unwrap();
assert!(!legacy.catalog_stale, "missing field defaults to false");
}
#[test]
fn managed_session_summary_deserializes() {
let json = serde_json::json!({
"id": "00000000-0000-0000-0000-000000000001",
"name": "tmpm-brave-otter",
"state": "running",
"workspace_path": "/tmp/ws",
"repo_url": "https://example.com/r.git",
"branch": "main",
"created_at": "2026-06-19T00:00:00Z",
"last_activity_at": "2026-06-19T01:00:00Z",
"pending_decision": "overwrite?",
"proposed_default": "yes",
"source_id": "owner/repo",
});
let s: ManagedSessionSummary = serde_json::from_value(json).unwrap();
assert_eq!(s.id, "00000000-0000-0000-0000-000000000001");
assert_eq!(s.name, "tmpm-brave-otter");
assert_eq!(s.state, "running");
assert_eq!(s.created_at.as_deref(), Some("2026-06-19T00:00:00Z"));
assert_eq!(s.pending_decision.as_deref(), Some("overwrite?"));
assert_eq!(s.source_id.as_deref(), Some("owner/repo"));
let lean = serde_json::json!({"id": "x", "name": "n", "state": "stopped"});
let s: ManagedSessionSummary = serde_json::from_value(lean).unwrap();
assert_eq!(s.id, "x");
assert!(s.workspace_path.is_none());
assert!(s.created_at.is_none());
assert!(s.pending_decision.is_none());
assert!(s.source_id.is_none());
}
#[test]
fn managed_list_response_deserializes() {
let json = serde_json::json!({
"sessions": [
{"id": "a", "name": "tmpm-a", "state": "running"},
{"id": "b", "name": "tmpm-b", "state": "stopped"},
],
});
let body: ManagedListResponse = serde_json::from_value(json).unwrap();
assert_eq!(body.sessions.len(), 2);
assert_eq!(body.sessions[1].name, "tmpm-b");
let empty: ManagedListResponse = serde_json::from_value(serde_json::json!({})).unwrap();
assert!(empty.sessions.is_empty());
}
#[test]
fn managed_spawn_request_serializes() {
let req = ManagedSpawnRequest {
repo_url: "https://example.com/r.git".to_string(),
git_ref: "main".to_string(),
task: "do the thing".to_string(),
name_hint: Some("tmpm-custom".to_string()),
runtime: Some("tcode".to_string()),
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["ref"], "main");
assert_eq!(v["repo_url"], "https://example.com/r.git");
assert_eq!(v["name_hint"], "tmpm-custom");
assert_eq!(v["runtime"], "tcode");
assert!(v.get("git_ref").is_none(), "must use wire key `ref`");
let bare = ManagedSpawnRequest {
repo_url: "r".to_string(),
git_ref: "r".to_string(),
task: "t".to_string(),
name_hint: None,
runtime: None,
};
let v = serde_json::to_value(&bare).unwrap();
assert!(v.get("name_hint").is_none(), "None name_hint is omitted");
assert!(v.get("runtime").is_none(), "None runtime is omitted");
}
#[test]
fn managed_spawn_response_deserializes() {
let json = serde_json::json!({
"id": "id-1",
"name": "tmpm-x",
"state": "running",
"created_at": "2026-06-19T00:00:00Z",
"attach_cmd": "tmux attach-session -t tmpm-x",
"runtime": "claude-code",
});
let r: ManagedSpawnResponse = serde_json::from_value(json).unwrap();
assert_eq!(r.id, "id-1");
assert_eq!(r.created_at.as_deref(), Some("2026-06-19T00:00:00Z"));
assert_eq!(r.attach_cmd, "tmux attach-session -t tmpm-x");
assert_eq!(r.runtime, "claude-code");
let lean = serde_json::json!({"id": "id-2", "name": "tmpm-y", "state": "running"});
let r: ManagedSpawnResponse = serde_json::from_value(lean).unwrap();
assert!(r.created_at.is_none());
assert_eq!(r.attach_cmd, "");
assert_eq!(r.runtime, "");
}
#[test]
fn managed_adopt_request_serializes() {
let req = ManagedAdoptRequest {
tmux_name: "tmpm-hand-started".to_string(),
cwd: "/Users/op/work/proj".to_string(),
task: Some("drive it".to_string()),
runtime: Some("claude-code".to_string()),
};
let v = serde_json::to_value(&req).unwrap();
assert_eq!(v["tmux_name"], "tmpm-hand-started");
assert_eq!(v["cwd"], "/Users/op/work/proj");
assert_eq!(v["task"], "drive it");
assert_eq!(v["runtime"], "claude-code");
let bare = ManagedAdoptRequest {
tmux_name: "my-cli-session".to_string(),
cwd: "/x".to_string(),
task: None,
runtime: None,
};
let v = serde_json::to_value(&bare).unwrap();
assert_eq!(v["tmux_name"], "my-cli-session");
assert!(v.get("task").is_none(), "None task is omitted");
assert!(v.get("runtime").is_none(), "None runtime is omitted");
}
#[test]
fn managed_adopt_response_deserializes() {
let json = serde_json::json!({
"id": "id-9",
"name": "tmpm-hand-started",
"state": "active",
"cwd": "/Users/op/work/proj",
"runtime": "claude-code",
"attach_cmd": "tmux attach-session -t tmpm-hand-started",
});
let r: ManagedAdoptResponse = serde_json::from_value(json).unwrap();
assert_eq!(r.id, "id-9");
assert_eq!(r.name, "tmpm-hand-started");
assert_eq!(r.state, "active");
assert_eq!(r.cwd, "/Users/op/work/proj");
assert_eq!(r.runtime, "claude-code");
assert_eq!(r.attach_cmd, "tmux attach-session -t tmpm-hand-started");
let lean = serde_json::json!({"id": "id-10", "name": "x", "state": "active"});
let r: ManagedAdoptResponse = serde_json::from_value(lean).unwrap();
assert_eq!(r.cwd, "");
assert_eq!(r.runtime, "");
assert_eq!(r.attach_cmd, "");
}
#[test]
fn managed_send_and_answer_round_trip() {
let v = serde_json::to_value(ManagedSendInputRequest {
text: "hello".to_string(),
})
.unwrap();
assert_eq!(v["text"], "hello");
let sent: ManagedSendInputResponse =
serde_json::from_value(serde_json::json!({"sent": true, "tmux_name": "tmpm-x"})).unwrap();
assert!(sent.sent);
assert_eq!(sent.tmux_name, "tmpm-x");
let v = serde_json::to_value(ManagedAnswerRequest {
answer: "yes".to_string(),
})
.unwrap();
assert_eq!(v["answer"], "yes");
let answered: ManagedAnswerResponse =
serde_json::from_value(serde_json::json!({"injected": true, "tmux_name": "tmpm-x"}))
.unwrap();
assert!(answered.injected);
}
#[test]
fn managed_attach_cmd_response_deserializes() {
let r: ManagedAttachCmdResponse =
serde_json::from_value(serde_json::json!({"attach_cmd": "tmux attach-session -t tmpm-x"}))
.unwrap();
assert_eq!(r.attach_cmd, "tmux attach-session -t tmpm-x");
}
#[test]
fn managed_activity_response_deserializes() {
let json = serde_json::json!({
"raw_pane": "line1\nline2",
"runtime_active": true,
"state": "working",
"summary": "running tests",
"confidence": 0.8_f32,
"cache_hit": false,
"input_tokens": 10,
"output_tokens": 5,
"latency_ms": 42,
"total_input_tokens": 100,
"total_output_tokens": 50,
"classification": "working",
"pending_decision": "overwrite?",
"proposed_default": "yes",
});
let a: ManagedActivityResponse = serde_json::from_value(json).unwrap();
assert_eq!(a.state, "working");
assert!(a.runtime_active);
assert_eq!(a.classification.as_deref(), Some("working"));
assert_eq!(a.pending_decision.as_deref(), Some("overwrite?"));
let json = serde_json::json!({
"raw_pane": "pane",
"runtime_active": false,
"state": "unknown",
"summary": "",
"confidence": 0.0_f32,
"cache_hit": false,
"input_tokens": 0,
"output_tokens": 0,
"latency_ms": 0,
"total_input_tokens": 0,
"total_output_tokens": 0,
"classification": null,
});
let a: ManagedActivityResponse = serde_json::from_value(json).unwrap();
assert_eq!(a.raw_pane, "pane");
assert!(a.classification.is_none());
assert!(a.pending_decision.is_none());
}
#[test]
fn managed_session_urls_use_managed_route_family() {
let client = DaemonClient::new("http://127.0.0.1:7880");
assert_eq!(client.base_url(), "http://127.0.0.1:7880");
let base = client.base_url();
assert_eq!(
format!("{base}/api/v1/sessions/managed/{id}/resume", id = "abc"),
"http://127.0.0.1:7880/api/v1/sessions/managed/abc/resume"
);
assert_eq!(
format!("{base}/sessions/{id}/resume", id = "abc"),
"http://127.0.0.1:7880/sessions/abc/resume"
);
}
#[test]
fn health_snapshot_deserializes() {
let full: HealthSnapshot = serde_json::from_value(serde_json::json!({
"status": "ok",
"catalog_stale": true,
"catalog_unknown": false,
"catalog_changes": ["agents/foo"],
}))
.expect("full health body parses");
assert_eq!(full.status, "ok");
assert!(full.catalog_stale);
assert!(!full.catalog_unknown);
let minimal: HealthSnapshot =
serde_json::from_value(serde_json::json!({ "status": "ok" })).expect("minimal body parses");
assert_eq!(minimal.status, "ok");
assert!(!minimal.catalog_stale);
assert!(!minimal.catalog_unknown);
}