use crate::harness::*;
#[test]
fn agents_classifies_teammate_and_id_round_trips() {
let enc = "-Users-testuser-Projects-team";
let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
let tid = "aVSRepro-68a2a1661c9390c1";
let h = Home::new();
h.write(
&format!("{enc}/{sess}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"fix the slider"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"toolu_team","name":"Agent","input":{"description":"repro the bug","subagent_type":"oh-my-claudecode:qa-tester","name":"VSRepro"}}]}}"#, "\n",
),
);
h.write(
&format!("{enc}/{sess}/subagents/agent-{tid}.jsonl"),
concat!(
r#"{"type":"user","isSidechain":true,"agentId":"aVSRepro-68a2a1661c9390c1","timestamp":"2026-06-07T05:00:01.500Z","message":{"role":"user","content":"<teammate-message teammate_id=\"team-lead\">repro the speed slider</teammate-message>"}}"#, "\n",
r#"{"type":"assistant","timestamp":"2026-06-07T05:20:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"the multi-region matrix result"}]}}"#, "\n",
),
);
h.write(
&format!("{enc}/{sess}/subagents/agent-{tid}.meta.json"),
r#"{"agentType":"VSRepro","description":"repro the bug","name":"VSRepro","taskKind":"in_process_teammate","teamName":"session-25f56dee","color":"purple"}"#,
);
let out = h.run(&["agents", &format!("@{sess}"), "--format", "json"]);
assert!(out.success, "stderr: {}", out.stderr);
let node = json_rows(&out.stdout, "agent")
.into_iter()
.find(|n| n.get("shape").and_then(|k| k.as_str()) == Some("teammate"))
.expect("a teammate row in agents JSON");
assert_eq!(node["agent_id"], tid);
assert_eq!(node["agent_type"], "oh-my-claudecode:qa-tester"); assert_eq!(node["name"], "VSRepro");
assert_eq!(node["team_name"], "session-25f56dee");
assert_eq!(node["spawn_tool"], "Agent");
assert_eq!(node["spawn_tool_use_id"], "toolu_team"); assert_eq!(node["trigger_utc"], "2026-06-07T05:00:01.000Z"); let chint = node["control_hint"].as_str().unwrap_or("");
assert!(
chint.contains("SendMessage") && chint.contains("shutdown_request"),
"teammate node missing control_hint: {node}"
);
let kind = h.run(&["agents", &format!("@{sess}"), "--shape", "teammate"]);
assert!(kind.success, "stderr: {}", kind.stderr);
assert!(
kind.stdout.contains(tid),
"kind filter dropped it: {}",
kind.stdout
);
assert!(kind.stdout.contains("teammate"));
assert!(
kind.stdout.contains("session-25f56dee"),
"no team line: {}",
kind.stdout
);
assert!(
kind.stdout.contains("SendMessage")
&& kind.stdout.contains("shutdown_request")
&& kind.stdout.contains("TaskStop"),
"no teammate control hint: {}",
kind.stdout
);
let bt = h.run(&["agents", &format!("@{sess}"), "--shape", "builtin-task"]);
assert!(
!bt.stdout.contains("shutdown_request"),
"control hint must not appear without a teammate in scope: {}",
bt.stdout
);
let refed = h.run(&["search", "matrix result", &format!("@{tid}"), "-t", "agent"]);
assert!(refed.success, "re-feed failed: {}", refed.stderr);
assert!(
refed.stdout.contains("multi-region matrix result"),
"re-fed teammate search found nothing: {}",
refed.stdout
);
}
#[test]
fn agents_nested_subagent_topology_links_parent_depth_and_tree() {
let enc = "-Users-testuser-Projects-nested";
let sess = "0a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d";
let h = Home::new();
h.write(
&format!("{enc}/{sess}.jsonl"),
concat!(
r#"{"type":"user","uuid":"u0","timestamp":"2026-06-07T05:00:00.000Z","message":{"role":"user","content":"go"}}"#, "\n",
r#"{"type":"assistant","uuid":"a0","timestamp":"2026-06-07T05:00:01.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"call_parent","name":"Agent","input":{"description":"parent agent","subagent_type":"general-purpose"}}]}}"#, "\n",
r#"{"type":"user","uuid":"c0","timestamp":"2026-06-07T05:09:00.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"call_parent","content":"parent done"}]}}"#, "\n",
),
);
h.write(
&format!("{enc}/{sess}/subagents/agent-parentaaa.jsonl"),
concat!(
r#"{"type":"user","isSidechain":true,"agentId":"parentaaa","timestamp":"2026-06-07T05:00:02.000Z","message":{"role":"user","content":"parent: do work"}}"#, "\n",
r#"{"type":"assistant","timestamp":"2026-06-07T05:01:00.000Z","message":{"role":"assistant","content":[{"type":"tool_use","id":"call_child","name":"Agent","input":{"description":"child agent","subagent_type":"Explore"}}]}}"#, "\n",
r#"{"type":"user","timestamp":"2026-06-07T05:08:00.000Z","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"call_child","content":"child done"}]}}"#, "\n",
),
);
h.write(
&format!("{enc}/{sess}/subagents/agent-parentaaa.meta.json"),
r#"{"agentType":"general-purpose","description":"parent agent","toolUseId":"call_parent"}"#,
);
h.write(
&format!("{enc}/{sess}/subagents/agent-childbbb.jsonl"),
concat!(
r#"{"type":"user","isSidechain":true,"agentId":"childbbb","timestamp":"2026-06-07T05:01:30.000Z","message":{"role":"user","content":"child: explore"}}"#, "\n",
r#"{"type":"assistant","timestamp":"2026-06-07T05:07:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"child result"}]}}"#, "\n",
),
);
h.write(
&format!("{enc}/{sess}/subagents/agent-childbbb.meta.json"),
r#"{"agentType":"Explore","description":"child agent","toolUseId":"call_child"}"#,
);
let j = h.run(&["agents", at(sess).as_str(), "--format", "json"]);
assert!(j.success, "stderr: {}", j.stderr);
let agents = json_rows(&j.stdout, "agent");
let parent = agents
.iter()
.find(|o| o["agent_id"] == "parentaaa")
.expect("parent row");
let child = agents
.iter()
.find(|o| o["agent_id"] == "childbbb")
.expect("child row");
assert_eq!(
child["parent_agent_id"], "parentaaa",
"child links to parent: {child}"
);
assert_eq!(
child["depth"],
serde_json::json!(1),
"child depth 1: {child}"
);
assert_eq!(
parent["parent_agent_id"],
serde_json::Value::Null,
"parent is a root: {parent}"
);
assert_eq!(
parent["depth"],
serde_json::json!(0),
"parent depth 0: {parent}"
);
let pi = agents
.iter()
.position(|o| o["agent_id"] == "parentaaa")
.unwrap();
let ci = agents
.iter()
.position(|o| o["agent_id"] == "childbbb")
.unwrap();
assert!(pi < ci, "pre-order: parent before child");
assert!(
agents.iter().all(|o| o.get("children").is_none()),
"flat rows carry no children[]: {agents:?}"
);
let sess_row = json_rows(&j.stdout, "session").remove(0);
assert_eq!(sess_row["agents"], serde_json::json!(2), "{sess_row}");
let tt = h.run(&["agents", at(sess).as_str()]);
assert!(tt.success, "stderr: {}", tt.stderr);
let pidx = tt.stdout.find("parentaaa").expect("parent in tree text");
let cidx = tt.stdout.find("childbbb").expect("child in tree text");
assert!(cidx > pidx, "child printed after parent: {}", tt.stdout);
let line_indent = |needle: &str| -> usize {
let li = tt.stdout[..tt.stdout.find(needle).unwrap()]
.rfind('\n')
.map_or(0, |p| p + 1);
tt.stdout[li..].chars().take_while(|c| *c == ' ').count()
};
assert!(
line_indent("childbbb") > line_indent("parentaaa"),
"child is indented deeper than parent: {}",
tt.stdout
);
}
#[test]
fn agents_tree_renders_workflow_run_as_parent_of_its_agents() {
let h = topology_home();
let out = h.run(&["agents", at(SESS).as_str(), "--format", "json"]);
assert!(out.success, "stderr: {}", out.stderr);
let runs = json_rows(&out.stdout, "run");
assert_eq!(runs.len(), 1, "one workflow run");
let run = &runs[0];
assert_eq!(run["run_id"], "wf_topo");
assert_eq!(run["workflow_name"], "carry-wf");
assert_eq!(run["agent_count"], 1);
let agents = json_rows(&out.stdout, "agent");
let wf_member = agents
.iter()
.find(|a| a["agent_id"] == "topo22")
.expect("workflow member row");
assert_eq!(wf_member["workflow_id"], "wf_topo", "join key: {wf_member}");
let builtin = agents
.iter()
.find(|a| a["agent_id"] == "topo11")
.expect("builtin row");
assert_eq!(builtin["workflow_id"], serde_json::Value::Null);
let run_pos = out.stdout.find(r#""kind":"run""#).unwrap();
let member_pos = out.stdout.find("topo22").unwrap();
assert!(run_pos < member_pos, "run row precedes its members");
let text = h.run(&["agents", at(SESS).as_str()]);
assert!(
text.stdout.contains("WORKFLOW wf_topo"),
"got: {}",
text.stdout
);
assert!(text.stdout.contains("[carry-wf]"));
assert!(text.stdout.contains("topo22"));
}
#[test]
fn agents_tree_keeps_workflow_agents_without_a_run_manifest() {
let h = topology_home();
h.write(
&format!("{ENC}/{SESS}/subagents/workflows/wf_orphan/agent-topo33.jsonl"),
concat!(
r#"{"type":"user","isSidechain":true,"agentId":"topo33","timestamp":"2026-06-07T07:00:00.000Z","message":{"role":"user","content":"orphan wf"}}"#, "\n",
r#"{"type":"assistant","timestamp":"2026-06-07T07:01:00.000Z","message":{"role":"assistant","content":[{"type":"text","text":"orphan done"}]}}"#, "\n",
),
);
h.write(
&format!("{ENC}/{SESS}/subagents/workflows/wf_orphan/agent-topo33.meta.json"),
r#"{"agentType":"workflow-subagent"}"#,
);
h.write(
&format!("{ENC}/{SESS}/subagents/workflows/wf_orphan/journal.jsonl"),
concat!(
r#"{"type":"started","agentId":"topo33","key":"v2:orphan"}"#, "\n",
r#"{"type":"result","agentId":"topo33","key":"v2:orphan","result":"ORPHAN-RETURN: in-flight payload"}"#, "\n",
),
);
let expected_ids = ["topo11", "topo22", "topo33"];
let tree = h.run(&["agents", at(SESS).as_str(), "--format", "json"]);
assert!(tree.success, "stderr: {}", tree.stderr);
let runs = json_rows(&tree.stdout, "run");
let orphan_run = runs
.iter()
.find(|r| r["run_id"] == "wf_orphan")
.expect("a synthesized run row for the manifest-less wf_orphan");
assert!(orphan_run["status"].is_null(), "no manifest → null status");
assert!(
orphan_run["agent_count"].is_null(),
"no manifest → null agent_count"
);
let tree_ids: std::collections::BTreeSet<String> = json_rows(&tree.stdout, "agent")
.iter()
.filter_map(|a| a["agent_id"].as_str().map(String::from))
.collect();
for id in expected_ids {
assert!(
tree_ids.contains(id),
"flat rows dropped agent {id} (rows={tree_ids:?})"
);
}
let orphan_member = json_rows(&tree.stdout, "agent")
.into_iter()
.find(|a| a["agent_id"] == "topo33")
.expect("topo33 row");
assert_eq!(orphan_member["workflow_id"], "wf_orphan", "join key");
let text = h.run(&["agents", at(SESS).as_str()]);
assert!(
text.stdout.contains("WORKFLOW wf_orphan"),
"text tree must show the stand-in run header: {}",
text.stdout
);
assert!(text.stdout.contains("topo33"), "got: {}", text.stdout);
}
#[test]
fn agents_id_form_is_bare_hex_joinable_across_files_and_recover() {
let h = topology_home();
let agents_json = h.run(&["agents", at(SESS).as_str(), "--format", "json"]);
assert!(agents_json.stdout.contains(r#""agent_id":"topo11""#));
let files_json = h.run(&[
"files",
at(SESS).as_str(),
"--format",
"json",
"--by",
"file",
]);
assert!(files_json.success, "stderr: {}", files_json.stderr);
assert!(
files_json.stdout.contains(r#""session_id":"topo11""#)
|| files_json.stdout.contains("topo11"),
"files must carry the bare-hex subagent id (joinable to agents): {}",
files_json.stdout
);
assert!(
!files_json.stdout.contains("agent-topo11"),
"the un-stripped agent- stem must NOT appear: {}",
files_json.stdout
);
}