use super::*;
#[test]
fn lifecycle_builtin_from_transcript_terminal_message() {
let fx = Fixture::new();
let session = layout(&fx);
let subs = discover_subagents(&session).unwrap();
let builtin = subs
.iter()
.find(|s| s.kind == SubagentKind::BuiltinTask)
.unwrap();
let lc = lifecycle(builtin, &JournalCache::build(std::slice::from_ref(builtin))).unwrap();
assert_eq!(lc.started_utc.as_deref(), Some("2026-06-07T05:00:00.000Z"));
assert_eq!(
lc.completed_utc.as_deref(),
Some("2026-06-07T05:03:20.000Z")
);
assert_eq!(lc.status, SubagentStatus::Completed);
assert_eq!(lc.agent_type.as_deref(), Some("oh-my-claudecode:executor"));
assert_eq!(lc.description.as_deref(), Some("run it"));
}
#[test]
fn lifecycle_workflow_completion_from_journal_result() {
let fx = Fixture::new();
let session = layout(&fx);
let subs = discover_subagents(&session).unwrap();
let wf = subs
.iter()
.find(|s| s.kind == SubagentKind::Workflow)
.unwrap();
let lc = lifecycle(wf, &JournalCache::build(std::slice::from_ref(wf))).unwrap();
assert_eq!(lc.status, SubagentStatus::Completed);
assert_eq!(lc.agent_type.as_deref(), Some("workflow-subagent"));
assert_eq!(lc.started_utc.as_deref(), Some("2026-06-07T06:00:00.000Z"));
assert_eq!(
lc.completed_utc.as_deref(),
Some("2026-06-07T06:01:00.000Z")
);
}
#[test]
fn journal_cache_first_result_event_wins_and_renders_nonstring() {
let fx = Fixture::new();
let enc = "-Users-testuser-Projects-jc";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
let wf_dir = format!("{enc}/{SESS}/subagents/workflows/wf_jc1");
for agent in ["aaa111", "bbb222"] {
fx.write(
&format!("{wf_dir}/agent-{agent}.jsonl"),
&format!("{{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"{agent}\",\"timestamp\":\"2026-06-07T07:00:00.000Z\",\"message\":{{\"role\":\"user\",\"content\":\"s\"}}}}\n"),
);
}
fx.write(
&format!("{wf_dir}/journal.jsonl"),
concat!(
"{\"type\":\"result\",\"agentId\":\"aaa111\"}\n",
"{\"type\":\"result\",\"agentId\":\"aaa111\",\"result\":\"late\"}\n",
"not json - skipped exactly as the direct scans skipped it\n",
"{\"type\":\"result\",\"agentId\":\"bbb222\",\"result\":{\"k\":1}}\n",
),
);
let subs = discover_subagents(&session).unwrap();
let cache = JournalCache::build(&subs);
let a = subs.iter().find(|s| s.agent_id == "aaa111").unwrap();
let b = subs.iter().find(|s| s.agent_id == "bbb222").unwrap();
assert!(journal_reports_completion(a, &cache));
assert!(journal_reports_completion(b, &cache));
assert_eq!(journal_result(a, &cache), None);
assert_eq!(journal_result(b, &cache).as_deref(), Some("{\"k\":1}"));
}
#[test]
fn running_when_no_completion_signal() {
let fx = Fixture::new();
let enc = "-Users-testuser-Projects-bar";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-ccc333.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"ccc333\",\"timestamp\":\"2026-06-07T07:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"start\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T07:00:10.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t\",\"name\":\"Bash\",\"input\":{}}]}}\n"
),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert_eq!(lc.status, SubagentStatus::Running);
}
#[test]
fn duration_label_formats() {
assert_eq!(
duration_label(
Some("2026-06-07T05:00:00.000Z"),
Some("2026-06-07T05:03:20.000Z")
)
.as_deref(),
Some("3m20s")
);
assert_eq!(
duration_label(Some("2026-06-07T05:00:00Z"), Some("2026-06-07T07:05:00Z")).as_deref(),
Some("2h05m")
);
assert_eq!(
duration_label(Some("2026-06-07T05:00:00Z"), Some("2026-06-07T05:00:03Z")).as_deref(),
Some("3s")
);
assert!(duration_label(None, Some("2026-06-07T05:00:00Z")).is_none());
}
#[test]
fn kind_and_status_labels() {
assert_eq!(SubagentKind::BuiltinTask.label(), "builtin-task");
assert_eq!(SubagentKind::Workflow.label(), "workflow");
assert_eq!(SubagentStatus::Completed.label(), "completed");
assert_eq!(SubagentStatus::Running.label(), "running");
assert_eq!(SubagentStatus::Unknown.label(), "unknown");
}
#[test]
fn journal_completion_false_for_builtin_no_workflow() {
let fx = Fixture::new();
let enc = "-Users-nojournal";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-fff666.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"fff666\",\"timestamp\":\"2026-06-07T05:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"x\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:00:10.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t\",\"name\":\"Bash\",\"input\":{}}]}}\n"
),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert_eq!(lc.status, SubagentStatus::Running);
}
#[test]
fn journal_completion_handles_missing_malformed_and_nonmatching() {
let fx = Fixture::new();
let enc = "-Users-wfjournal";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/workflows/wf_z/agent-ggg777.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"ggg777\",\"timestamp\":\"2026-06-07T06:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"x\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T06:00:30.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t\",\"name\":\"Bash\",\"input\":{}}]}}\n"
),
);
fx.write(
&format!("{enc}/{SESS}/subagents/workflows/wf_z/journal.jsonl"),
concat!(
"\n", "{ this is not valid json }\n", "{\"type\":\"started\",\"agentId\":\"ggg777\"}\n", "{\"type\":\"result\",\"agentId\":\"someone-else\"}\n" ),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert_eq!(
lc.status,
SubagentStatus::Running,
"no matching result → not completed"
);
}
#[test]
fn journal_completion_false_when_journal_absent() {
let fx = Fixture::new();
let enc = "-Users-wfnojournal";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/workflows/wf_n/agent-hhh888.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"hhh888\",\"timestamp\":\"2026-06-07T06:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"x\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T06:00:30.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"done cleanly\"}]}}\n"
),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert_eq!(lc.status, SubagentStatus::Completed);
}
#[test]
fn lifecycle_unknown_when_no_timestamps() {
let fx = Fixture::new();
let enc = "-Users-nots";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-iii999.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"iii999\",\"message\":{\"role\":\"user\",\"content\":\"no ts\"}}\n",
"{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"t\",\"name\":\"Bash\",\"input\":{}}]}}\n"
),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert!(lc.started_utc.is_none());
assert_eq!(lc.status, SubagentStatus::Unknown);
}
#[test]
fn lifecycle_tail_terminal_flag_short_circuits_on_later_record() {
let fx = Fixture::new();
let enc = "-Users-shortcirc";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"hi\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-jjj000.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"jjj000\",\"timestamp\":\"2026-06-07T05:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"start\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:00:10.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"older text\"}]}}\n",
"{\"type\":\"assistant\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"newest no-ts text\"}]}}\n"
),
);
let subs = discover_subagents(&session).unwrap();
let lc = lifecycle(
&subs[0],
&JournalCache::build(std::slice::from_ref(&subs[0])),
)
.unwrap();
assert_eq!(lc.status, SubagentStatus::Completed);
assert_eq!(
lc.completed_utc.as_deref(),
Some("2026-06-07T05:00:10.000Z")
);
}
#[test]
fn resolve_status_all_arms() {
assert_eq!(
resolve_status(true, true, false, true),
SubagentStatus::Completed
); assert_eq!(
resolve_status(true, false, true, true),
SubagentStatus::Completed
); assert_eq!(
resolve_status(true, false, false, true),
SubagentStatus::Running
); assert_eq!(
resolve_status(false, false, false, false),
SubagentStatus::Unknown
); assert_eq!(
resolve_status(true, false, false, false),
SubagentStatus::Unknown
); }
#[test]
fn duration_label_none_for_unparseable_bounds() {
assert!(duration_label(Some("garbage"), Some("2026-06-07T05:00:00Z")).is_none());
assert!(duration_label(Some("2026-06-07T05:00:00Z"), Some("not-a-time")).is_none());
assert_eq!(
duration_label(Some("2026-06-07T05:00:10Z"), Some("2026-06-07T05:00:00Z")).as_deref(),
Some("0s")
);
}
#[test]
fn frozen_lane_classifies_escalation_blocked_vs_awaiting_execution() {
let fx = Fixture::new();
let enc = "-Users-frozen";
let session = fx.write(
&format!("{enc}/{SESS}.jsonl"),
"{\"type\":\"user\",\"message\":{\"role\":\"user\",\"content\":\"go\"}}\n",
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-aesc111.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"aesc111\",\"timestamp\":\"2026-06-07T05:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"teardown\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:01:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Now removing the scratch files.\"}]}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:02:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_rm\",\"name\":\"Bash\",\"input\":{\"command\":\"for f in a b; do rm -rf \\\"$SCRATCH/$f\\\"; done\"}}]}}\n"
),
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-await22.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"await22\",\"timestamp\":\"2026-06-07T05:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"read\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:02:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_read\",\"name\":\"Read\",\"input\":{\"file_path\":\"/x/big.log\"}}]}}\n"
),
);
fx.write(
&format!("{enc}/{SESS}/subagents/agent-done333.jsonl"),
concat!(
"{\"type\":\"user\",\"isSidechain\":true,\"agentId\":\"done333\",\"timestamp\":\"2026-06-07T05:00:00.000Z\",\"message\":{\"role\":\"user\",\"content\":\"teardown\"}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:02:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"tool_use\",\"id\":\"toolu_rm2\",\"name\":\"Bash\",\"input\":{\"command\":\"rm -rf $SCRATCH/*\"}}]}}\n",
"{\"type\":\"user\",\"timestamp\":\"2026-06-07T05:40:00.000Z\",\"message\":{\"role\":\"user\",\"content\":[{\"type\":\"tool_result\",\"tool_use_id\":\"toolu_rm2\",\"content\":\"done\"}]}}\n",
"{\"type\":\"assistant\",\"timestamp\":\"2026-06-07T05:41:00.000Z\",\"message\":{\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"Teardown complete.\"}]}}\n"
),
);
let nodes = build_topology(&session, false).unwrap();
let esc = nodes.iter().find(|n| n.agent_id == "aesc111").unwrap();
assert_eq!(esc.status, SubagentStatus::Running);
assert_eq!(
esc.pending_classification,
Some(PendingClassification::EscalationBlocked)
);
assert_eq!(esc.pending_tool_name.as_deref(), Some("Bash"));
assert_eq!(esc.pending_tool_use_id.as_deref(), Some("toolu_rm"));
assert_eq!(
esc.pending_since_utc.as_deref(),
Some("2026-06-07T05:02:00.000Z")
);
let awa = nodes.iter().find(|n| n.agent_id == "await22").unwrap();
assert_eq!(awa.status, SubagentStatus::Running);
assert_eq!(
awa.pending_classification,
Some(PendingClassification::AwaitingExecution)
);
assert_eq!(awa.pending_tool_name.as_deref(), Some("Read"));
let done = nodes.iter().find(|n| n.agent_id == "done333").unwrap();
assert_eq!(done.status, SubagentStatus::Completed);
assert!(done.pending_classification.is_none());
}