use super::*;
#[test]
fn classify_sendmessage_message_is_tool_use_plus_sent() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"ab9018739543b1df0","type":"message","message":"do the thing"}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSent]
);
}
#[test]
fn classify_sendmessage_direct_is_sent_not_signal() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"x","type":"direct","message":"hi"}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSent]
);
}
#[test]
fn classify_sendmessage_no_type_defaults_to_sent() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"x","message":"hi"}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSent]
);
}
#[test]
fn classify_sendmessage_shutdown_request_is_signal() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"S-Sync","type":"shutdown_request","recipient":"S-Sync","reason":"done"}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSignal]
);
}
#[test]
fn classify_sendmessage_nested_shutdown_payload_is_signal() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"x","message":{"type":"shutdown_request","reason":"done"}}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSignal]
);
}
#[test]
fn classify_spawn_tool_use_is_tool_use_plus_sent() {
for tool in ["Task", "Agent", "Workflow"] {
let r = parse(&format!(
r#"{{"type":"assistant","message":{{"role":"assistant","content":[{{"type":"tool_use","id":"t","name":"{tool}","input":{{"subagent_type":"executor","description":"go"}}}}]}}}}"#
));
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSent],
"spawn tool {tool}"
);
}
}
#[test]
fn classify_teammate_prose_is_inbox() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":"Another Claude session sent a message:\n<teammate-message teammate_id=\"reviewer\">verdict: LGTM</teammate-message>"}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommInbox]
);
}
#[test]
fn classify_teammate_signal_is_signal() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":"Another Claude session sent a message:\n<teammate-message teammate_id=\"peer\">\n{\"type\":\"idle_notification\",\"from\":\"peer\"}\n</teammate-message>"}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommSignal]
);
}
#[test]
fn classify_subagent_return_is_tool_result_plus_inbox() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_spawn","content":"subagent done: 3 files changed"}]}}"#,
);
let ctx = ClassifyCtx {
spawn: Some(&FakeSpawn),
..ClassifyCtx::top_level()
};
assert_eq!(
r.classify(&ctx),
vec![Class::AgentToolResult, Class::CommInbox]
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolResult]
);
}
#[test]
fn classify_teammate_spawn_ack_is_tool_result_only_not_inbox() {
let r = parse(
r#"{"type":"user","toolUseResult":{"status":"teammate_spawned","name":"P1-engine","agent_id":"aP1-engine-9cf2","agent_type":"executor"},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_spawn","content":"teammate spawned"}]}}"#,
);
let ctx = ClassifyCtx {
owner_id: Some("parent"),
spawn: Some(&FakeSpawn),
..ClassifyCtx::top_level()
};
assert!(r.is_teammate_spawn_ack());
assert_eq!(
r.classify(&ctx),
vec![Class::AgentToolResult],
"the ACK must NOT carry agent.communication.inbox"
);
assert!(r.direction(&ctx).is_none());
}
#[test]
fn classify_async_launch_ack_is_tool_result_only_not_inbox() {
let r = parse(
r#"{"type":"user","toolUseResult":{"isAsync":true,"status":"async_launched","agentId":"ad8012462a52f5c25","description":"draft the fold"},"message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_spawn","content":[{"type":"text","text":"Async agent launched successfully.\nagentId: ad8012462a52f5c25 (internal ID - do not mention to user.)"}]}]}}"#,
);
let ctx = ClassifyCtx {
owner_id: Some("parent"),
spawn: Some(&FakeSpawn),
..ClassifyCtx::top_level()
};
assert!(r.is_async_launch_ack());
assert!(r.is_spawn_launch_ack());
assert_eq!(
r.classify(&ctx),
vec![Class::AgentToolResult],
"the async-launch ACK must NOT carry agent.communication.inbox"
);
assert!(
r.direction(&ctx).is_none(),
"a launch ack carries no child ⇨ self direction"
);
}
#[test]
fn classify_async_launch_ack_detected_by_content_prefix_fallback() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_spawn","content":"Async agent launched successfully.\nagentId: ad80 (internal ID)"}]}}"#,
);
let ctx = ClassifyCtx {
spawn: Some(&FakeSpawn),
..ClassifyCtx::top_level()
};
assert!(r.is_async_launch_ack());
assert_eq!(r.classify(&ctx), vec![Class::AgentToolResult]);
assert!(r.direction(&ctx).is_none());
}
#[test]
fn classify_sync_task_return_still_tool_result_plus_inbox_vs_async_ack() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":[{"type":"tool_result","tool_use_id":"toolu_spawn","content":"subagent done: 3 files changed"}]}}"#,
);
let ctx = ClassifyCtx {
owner_id: Some("parent"),
spawn: Some(&FakeSpawn),
..ClassifyCtx::top_level()
};
assert!(!r.is_spawn_launch_ack());
assert_eq!(
r.classify(&ctx),
vec![Class::AgentToolResult, Class::CommInbox]
);
assert_eq!(
r.direction(&ctx),
Some(("child-abc".to_string(), "parent".to_string()))
);
}
#[test]
fn classify_teammate_terminated_signal_id_is_system() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":"<teammate-message teammate_id=\"system\">\n{\"type\":\"teammate_terminated\",\"message\":\"B38F1Check has shut down.\"}\n</teammate-message>"}}"#,
);
let tm = r.teammate_message().expect("teammate message");
assert_eq!(tm.teammate_id.as_deref(), Some("system"));
assert_eq!(tm.signal_type.as_deref(), Some("teammate_terminated"));
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommSignal]
);
let ctx = ClassifyCtx {
owner_id: Some("me"),
..ClassifyCtx::top_level()
};
assert_eq!(
r.direction(&ctx),
Some(("system".to_string(), "me".to_string())),
"FROM is system, NOT the dead agent named in the body"
);
}
#[test]
fn classify_shutdown_approved_signal() {
let r = parse(
r#"{"type":"user","message":{"role":"user","content":"<teammate-message teammate_id=\"B38F1Check\" color=\"yellow\">\n{\"type\":\"shutdown_approved\",\"requestId\":\"shutdown-1@B38F1Check\",\"from\":\"B38F1Check\",\"backendType\":\"in-process\"}\n</teammate-message>"}}"#,
);
let tm = r.teammate_message().unwrap();
assert_eq!(tm.signal_type.as_deref(), Some("shutdown_approved"));
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommSignal]
);
}
#[test]
fn classify_sendmessage_real_shutdown_request_dict_message_shape() {
let r = parse(
r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"tool_use","id":"t","name":"SendMessage","input":{"to":"GraftBoard","message":{"type":"shutdown_request","reason":"done"},"summary":"shut down","type":"shutdown_request","recipient":"GraftBoard","content":"done"}}]}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::AgentToolUse, Class::CommSignal]
);
assert_eq!(
r.direction(&ClassifyCtx::top_level()),
Some(("self".to_string(), "GraftBoard".to_string()))
);
}
#[test]
fn classify_subagent_opener_is_inbox_via_ctx() {
let r = parse(
r#"{"type":"user","isSidechain":true,"message":{"role":"user","content":"go repro the bug"}}"#,
);
let ctx = ClassifyCtx {
is_subagent: true,
is_transcript_opener: true,
..ClassifyCtx::top_level()
};
assert_eq!(r.classify(&ctx), vec![Class::CommInbox]);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::UserMessage]
);
}
#[test]
fn classify_agent_message_peer_form_is_inbox() {
let r = parse(
r#"{"type":"user","isMeta":true,"message":{"role":"user","content":"<agent-message from=\"oh-my-claudecode:architect\">\n[Reply intended for the executor peer]\nuse the shared resolver.\n</agent-message>"}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommInbox]
);
let ctx = ClassifyCtx {
owner_id: Some("me"),
..ClassifyCtx::top_level()
};
assert_eq!(
r.direction(&ctx),
Some(("oh-my-claudecode:architect".to_string(), "me".to_string()))
);
}
#[test]
fn classify_agent_message_no_from_degrades_to_peer_direction() {
let r = parse(
r#"{"type":"user","isMeta":true,"message":{"role":"user","content":"<agent-message>no sender attr</agent-message>"}}"#,
);
assert_eq!(
r.classify(&ClassifyCtx::top_level()),
vec![Class::CommInbox]
);
assert_eq!(
r.direction(&ClassifyCtx::top_level()),
Some(("peer".to_string(), "self".to_string()))
);
}