use std::sync::Arc;
use serde_json::json;
use super::{Decided, action_instructions, execute_verb, parse_action};
use crate::core::sm::agent::delegate::mock_control::MockSessionControl;
use crate::core::sm::control::SessionControl;
#[test]
fn parse_verb_call() {
let d = parse_action(r#"{"action":"sessions.list","args":{}}"#);
match d {
Decided::Verb { verb, .. } => assert_eq!(verb, "sessions.list"),
other => panic!("expected verb, got {other:?}"),
}
}
#[test]
fn parse_final_answer() {
let d = parse_action(r#"{"action":"final","message":"all good"}"#);
assert_eq!(
d,
Decided::Final {
message: "all good".to_string()
}
);
}
#[test]
fn parse_prose_is_final() {
let d = parse_action("I checked and everything looks fine.");
assert_eq!(
d,
Decided::Final {
message: "I checked and everything looks fine.".to_string()
}
);
}
#[test]
fn parse_handles_stray_closing_brace_without_panic() {
let d = parse_action("} stray");
assert_eq!(
d,
Decided::Final {
message: "} stray".to_string()
}
);
let d = parse_action(r#"{"action":"final","message":"ok"} junk }"#);
assert_eq!(
d,
Decided::Final {
message: "ok".to_string()
}
);
}
#[test]
fn parse_fenced_verb() {
let reply = "Let me look.\n```json\n{\"action\":\"sessions.get\",\"args\":{\"session_id\":\"s1\"}}\n```";
match parse_action(reply) {
Decided::Verb { verb, args } => {
assert_eq!(verb, "sessions.get");
assert_eq!(args.get("session_id").unwrap(), "s1");
}
other => panic!("expected verb, got {other:?}"),
}
}
#[test]
fn prompt_lists_every_catalog_verb() {
let prompt = action_instructions();
for verb in [
"sessions.launch",
"sessions.list",
"sessions.get",
"sessions.send",
"sessions.stop",
"sessions.resume",
"sessions.kill",
] {
assert!(prompt.contains(verb), "prompt missing `{verb}`");
}
assert!(
prompt.contains("\"action\":\"final\""),
"missing final shape"
);
assert!(prompt.contains("INLINE"), "must state verbs run inline");
}
#[test]
fn prompt_lists_health_ops_verb() {
let prompt = action_instructions();
assert!(
prompt.contains("sessions.health"),
"action prompt must advertise the executable `sessions.health` verb"
);
}
#[tokio::test]
async fn execute_health_reports_fleet() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let out = execute_verb(&control, "sessions.health", &json!({}))
.await
.expect("health dispatches");
assert_eq!(out.get("reachable").and_then(|v| v.as_bool()), Some(true));
assert_eq!(out.get("status").and_then(|v| v.as_str()), Some("ok"));
assert_eq!(out.get("managed_total").and_then(|v| v.as_u64()), Some(0));
assert_eq!(
out.get("managed_pending_decisions")
.and_then(|v| v.as_u64()),
Some(0)
);
}
#[tokio::test]
async fn unknown_verb_error_lists_health() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.bogus", &json!({}))
.await
.expect_err("unknown verb errors");
assert!(err.to_string().contains("sessions.health"));
}
#[test]
fn prompt_lists_decommission_ops_verb() {
let prompt = action_instructions();
assert!(
prompt.contains("sessions.decommission"),
"action prompt must advertise the executable `sessions.decommission` verb"
);
}
#[test]
fn prompt_lists_inject_ops_verb() {
let prompt = action_instructions();
assert!(
prompt.contains("sessions.inject"),
"action prompt must advertise the executable `sessions.inject` verb"
);
assert!(
prompt.contains("args.submit"),
"action prompt must document the inject submit arg"
);
}
#[tokio::test]
async fn execute_decommission_dispatches() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let out = execute_verb(
&control,
"sessions.decommission",
&json!({ "session_id": "abc-123" }),
)
.await
.expect("decommission dispatches");
assert_eq!(
out.get("ok").and_then(|v| v.as_bool()),
Some(true),
"decommission must return {{ok: true}}"
);
}
#[tokio::test]
async fn execute_decommission_requires_session_id() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.decommission", &json!({}))
.await
.expect_err("missing session_id errors");
assert!(err.to_string().contains("session_id"));
}
#[tokio::test]
async fn execute_inject_dispatches_with_default_submit() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let out = execute_verb(
&control,
"sessions.inject",
&json!({ "session_id": "sid-1", "text": "cargo test" }),
)
.await
.expect("inject dispatches");
assert_eq!(
out.get("ok").and_then(|v| v.as_bool()),
Some(true),
"inject must return {{ok: true}}"
);
let sends = mock.sends();
assert_eq!(sends.len(), 1, "exactly one inject recorded");
assert_eq!(sends[0].0, "sid-1");
assert_eq!(sends[0].1, "cargo test");
}
#[tokio::test]
async fn execute_inject_accepts_no_submit_variant() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
execute_verb(
&control,
"sessions.inject",
&json!({ "session_id": "sid-2", "text": "partial cmd", "submit": "no_submit" }),
)
.await
.expect("inject no_submit dispatches");
let sends = mock.sends();
assert_eq!(sends.len(), 1);
assert_eq!(sends[0].1, "partial cmd");
}
#[tokio::test]
async fn execute_inject_requires_session_id_and_text() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.inject", &json!({ "text": "hi" }))
.await
.expect_err("missing session_id errors");
assert!(err.to_string().contains("session_id"));
let err = execute_verb(
&control,
"sessions.inject",
&json!({ "session_id": "sid-3" }),
)
.await
.expect_err("missing text errors");
assert!(err.to_string().contains("text"));
}
#[tokio::test]
async fn unknown_verb_error_lists_new_ops_verbs() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.bogus", &json!({}))
.await
.expect_err("unknown verb errors");
let msg = err.to_string();
assert!(
msg.contains("sessions.decommission"),
"error must list decommission"
);
assert!(msg.contains("sessions.inject"), "error must list inject");
}
#[test]
fn prompt_lists_adopt_ops_verb() {
let prompt = action_instructions();
assert!(
prompt.contains("sessions.adopt"),
"action prompt must advertise the executable `sessions.adopt` verb"
);
assert!(
prompt.contains("args.tmux_name"),
"action prompt must document the adopt args"
);
}
#[tokio::test]
async fn execute_adopt_reads_args() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let out = execute_verb(
&control,
"sessions.adopt",
&json!({
"tmux_name": "tmpm-hand-started",
"cwd": "/Users/op/work/proj",
"task": "drive it",
"runtime": "claude-code"
}),
)
.await
.expect("adopt dispatches");
assert!(
out.get("session_id").and_then(|v| v.as_str()).is_some(),
"adopt must return a session_id"
);
let adopts = mock.adopts();
assert_eq!(adopts.len(), 1, "exactly one adopt recorded");
assert_eq!(adopts[0].0, "tmpm-hand-started");
assert_eq!(adopts[0].1, "/Users/op/work/proj");
assert_eq!(adopts[0].2.as_deref(), Some("drive it"));
assert_eq!(adopts[0].3.as_deref(), Some("claude-code"));
}
#[tokio::test]
async fn execute_adopt_requires_tmux_name_and_cwd() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.adopt", &json!({ "cwd": "/x" }))
.await
.expect_err("missing tmux_name errors");
assert!(err.to_string().contains("tmux_name"));
let err = execute_verb(
&control,
"sessions.adopt",
&json!({ "tmux_name": "tmpm-x" }),
)
.await
.expect_err("missing cwd errors");
assert!(err.to_string().contains("cwd"));
}
#[tokio::test]
async fn unknown_verb_error_lists_adopt() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.bogus", &json!({}))
.await
.expect_err("unknown verb errors");
assert!(err.to_string().contains("sessions.adopt"));
}
#[tokio::test]
async fn execute_dispatches_list() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let out = execute_verb(&control, "sessions.list", &json!({}))
.await
.expect("list dispatches");
assert!(out.get("sessions").is_some());
}
#[tokio::test]
async fn execute_send_reads_args() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
execute_verb(
&control,
"sessions.send",
&json!({"session_id":"abc","text":"hello"}),
)
.await
.expect("send dispatches");
let sends = mock.sends();
assert_eq!(sends, vec![("abc".to_string(), "hello".to_string())]);
}
#[tokio::test]
async fn execute_get_requires_id() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.get", &json!({}))
.await
.expect_err("missing id errors");
assert!(err.to_string().contains("session_id"));
}
#[tokio::test]
async fn execute_unknown_verb_errors() {
let control: Arc<dyn SessionControl> = Arc::new(MockSessionControl::default());
let err = execute_verb(&control, "sessions.frobnicate", &json!({}))
.await
.expect_err("unknown verb errors");
assert!(err.to_string().contains("unknown verb"));
assert!(err.to_string().contains("sessions.list"));
}
#[tokio::test]
async fn execute_launch_threads_repo_url_and_ref_() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
let out = execute_verb(
&control,
"sessions.launch",
&json!({
"workdir": "/local/fallback",
"repo_url": "https://github.com/example/repo",
"ref_": "feat/my-branch",
"prompt": "run tests",
}),
)
.await
.expect("launch with repo_url+ref_ dispatches");
assert!(
out.get("session_id").and_then(|v| v.as_str()).is_some(),
"launch must return a session_id"
);
let launches = mock.launches();
assert_eq!(launches.len(), 1, "exactly one launch recorded");
let (_, params) = &launches[0];
assert_eq!(
params.repo_url.as_deref(),
Some("https://github.com/example/repo"),
"repo_url must be threaded through LaunchParams"
);
assert_eq!(
params.ref_.as_deref(),
Some("feat/my-branch"),
"ref_ must be threaded through LaunchParams"
);
assert_eq!(params.workdir, "/local/fallback");
assert_eq!(params.prompt.as_deref(), Some("run tests"));
}
#[tokio::test]
async fn execute_launch_repo_url_and_ref_default_to_none() {
let mock = Arc::new(MockSessionControl::default());
let control: Arc<dyn SessionControl> = mock.clone();
execute_verb(
&control,
"sessions.launch",
&json!({ "workdir": "/some/dir", "prompt": "do work" }),
)
.await
.expect("launch without repo_url dispatches");
let launches = mock.launches();
assert_eq!(launches.len(), 1);
let (_, params) = &launches[0];
assert!(
params.repo_url.is_none(),
"repo_url must be None when not supplied"
);
assert!(params.ref_.is_none(), "ref_ must be None when not supplied");
}