use super::*;
use crate::core::session::{ControlModel, Session, SessionStatus};
use crate::daemon::idle_nudge::has_live_children;
use crate::daemon::state::sessions::{
DECLARED_STALE_AFTER_SECS, DELEGATION_RETENTION_SECS, RUNNING_STALE_AFTER_SECS,
STALE_RETENTION_SECS,
};
use std::sync::Arc;
fn state_with_session() -> (Arc<DaemonState>, SessionId) {
let state = Arc::new(DaemonState::new());
let id = SessionId::new();
let mut s = Session::new(id, "/tmp/p", ControlModel::Tmux, None);
s.status = SessionStatus::Active;
state.register_session(s);
(state, id)
}
fn pre(tool: &str, agent: &str, desc: &str, tool_use_id: &str) -> Value {
serde_json::json!({
"tool": tool,
"cwd": "/tmp/p",
"transcript_path": "/tmp/p/parent.jsonl",
"tool_use_id": tool_use_id,
"input": { "subagent_type": agent, "description": desc, "prompt": "do it" }
})
}
fn post_async(tool: &str, tool_use_id: &str, agent_id: &str) -> Value {
serde_json::json!({
"tool": tool,
"tool_use_id": tool_use_id,
"tool_response": {
"isAsync": true,
"status": "async_launched",
"agentId": agent_id,
"resolvedModel": "claude-haiku-4-5-20251001"
}
})
}
fn stop(agent_id: &str) -> Value {
serde_json::json!({ "agent_id": agent_id, "agent_type": "general-purpose" })
}
fn only(state: &DaemonState, session: SessionId) -> Delegation {
let all = state.delegations_for(session);
assert_eq!(all.len(), 1, "expected exactly one delegation, got {all:?}");
all.into_iter().next().expect("one")
}
#[test]
fn ignores_unrelated_tools() {
let (state, sid) = state_with_session();
let payload = serde_json::json!({ "tool": "Bash", "input": { "command": "ls" } });
observe(&state, sid, HookEvent::PreToolUse, &payload);
assert!(state.delegations_for(sid).is_empty());
}
#[test]
fn pre_tool_use_creates_running_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "build the thing", "toolu_1"),
);
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Running);
assert_eq!(d.source, DelegationSource::HookObserved);
assert_eq!(d.agent, "engineer");
assert_eq!(d.task, "build the thing");
assert_eq!(d.tool_use_id.as_deref(), Some("toolu_1"));
assert_eq!(d.cwd, Some(std::path::PathBuf::from("/tmp/p")));
assert!(d.started_at.is_some());
}
#[test]
fn legacy_task_tool_name_is_tracked() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Task", "qa", "verify", "toolu_t"),
);
assert_eq!(only(&state, sid).status, DelegationStatus::Running);
}
#[test]
fn duplicate_pre_tool_use_is_idempotent() {
let (state, sid) = state_with_session();
let p = pre("Agent", "engineer", "task", "toolu_1");
observe(&state, sid, HookEvent::PreToolUse, &p);
observe(&state, sid, HookEvent::PreToolUse, &p);
assert_eq!(state.delegations_for(sid).len(), 1);
}
#[test]
fn async_launch_keeps_delegation_running() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "a403cdbc"),
);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Running,
"an async_launched dispatch must stay Running — terminalizing here \
reports 'no agents in flight' while they are all still running"
);
assert_eq!(d.agent_id.as_deref(), Some("a403cdbc"));
assert_eq!(d.tier, ModelTier::Haiku, "tier refined from resolvedModel");
assert!(has_live_children(&state.delegations_for(sid)));
}
#[test]
fn synchronous_post_tool_use_completes_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Task", "qa", "t", "toolu_s"),
);
let payload = serde_json::json!({
"tool": "Task",
"tool_use_id": "toolu_s",
"tool_response": { "isAsync": false, "is_error": false }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Completed);
assert!(d.ended_at.is_some());
}
#[test]
fn liveness_silent_response_is_read_as_a_return_known_gap() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "resolvedModel": "claude-haiku-4-5-20251001" }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Completed,
"documented residual — change this assertion only together with the \
stop-side recovery path"
);
assert_eq!(d.tier, ModelTier::Haiku, "tier is still refined");
}
#[test]
fn post_tool_use_failure_marks_failed() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "qa", "t", "toolu_f"),
);
let payload = serde_json::json!({ "tool": "Agent", "tool_use_id": "toolu_f" });
observe(&state, sid, HookEvent::PostToolUseFailure, &payload);
assert_eq!(only(&state, sid).status, DelegationStatus::Failed);
}
#[test]
fn subagent_stop_completes_matching_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "a403cdbc"),
);
observe(&state, sid, HookEvent::SubagentStop, &stop("a403cdbc"));
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Completed);
assert!(d.ended_at.is_some());
assert!(!has_live_children(&state.delegations_for(sid)));
}
#[test]
fn subagent_stop_failure_marks_failed() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "e", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
observe(&state, sid, HookEvent::SubagentStopFailure, &stop("aid1"));
assert_eq!(only(&state, sid).status, DelegationStatus::Failed);
}
#[test]
fn subagent_stop_without_agent_id_terminalizes_nothing() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "a403cdbc"),
);
observe(
&state,
sid,
HookEvent::SubagentStop,
&serde_json::json!({ "transcript_path": "/tmp/p/parent.jsonl" }),
);
assert_eq!(only(&state, sid).status, DelegationStatus::Running);
assert!(has_live_children(&state.delegations_for(sid)));
}
#[test]
fn subagent_stop_with_unknown_agent_id_is_a_noop() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "e", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "known"),
);
observe(&state, sid, HookEvent::SubagentStop, &stop("stranger"));
assert_eq!(only(&state, sid).status, DelegationStatus::Running);
}
#[test]
fn subagent_stop_is_idempotent() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "e", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
let first = only(&state, sid).ended_at;
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert_eq!(only(&state, sid).ended_at, first, "must not re-terminalize");
}
#[test]
fn concurrent_delegations_terminalize_independently() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "alpha", "a", "toolu_a"),
);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "beta", "b", "toolu_b"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_a", "aid_alpha"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_b", "aid_beta"),
);
assert_eq!(state.delegations_for(sid).len(), 2);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid_alpha"));
let all = state.delegations_for(sid);
let alpha = all.iter().find(|d| d.agent == "alpha").expect("alpha");
let beta = all.iter().find(|d| d.agent == "beta").expect("beta");
assert_eq!(alpha.status, DelegationStatus::Completed);
assert_eq!(
beta.status,
DelegationStatus::Running,
"terminalizing alpha must NOT close the concurrently-running beta"
);
assert!(
has_live_children(&all),
"beta is still live, so the session must not look idle"
);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid_beta"));
assert!(!has_live_children(&state.delegations_for(sid)));
}
#[test]
fn dedups_declaration_and_observation() {
let (state, sid) = state_with_session();
let declared = Delegation::new(sid, None, "engineer", ModelTier::Opus, "declared task");
let declared_id = declared.id;
state.upsert_delegation(declared);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "declared task", "toolu_1"),
);
let d = only(&state, sid);
assert_eq!(d.id, declared_id, "must reuse the declared record");
assert_eq!(d.status, DelegationStatus::Running);
assert_eq!(d.tool_use_id.as_deref(), Some("toolu_1"));
assert_eq!(d.source, DelegationSource::McpDeclared);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert_eq!(only(&state, sid).status, DelegationStatus::Completed);
}
#[test]
fn dedup_ignores_different_agent() {
let (state, sid) = state_with_session();
state.upsert_delegation(Delegation::new(
sid,
None,
"research",
ModelTier::Sonnet,
"investigate",
));
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "build", "toolu_1"),
);
assert_eq!(
state.delegations_for(sid).len(),
2,
"a different agent is a different delegation"
);
}
#[test]
fn dedup_window_expires() {
let (state, sid) = state_with_session();
let mut old = Delegation::new(sid, None, "engineer", ModelTier::Sonnet, "same task");
old.created_at = Utc::now() - chrono::Duration::seconds(DEDUP_WINDOW_SECS + 60);
state.upsert_delegation(old);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "same task", "toolu_1"),
);
assert_eq!(state.delegations_for(sid).len(), 2);
}
#[test]
fn dedup_does_not_steal_an_already_bound_record() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "first", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "second", "toolu_2"),
);
assert_eq!(state.delegations_for(sid).len(), 2);
}
#[test]
fn delegations_are_scoped_per_session() {
let (state, sid_a) = state_with_session();
let sid_b = SessionId::new();
let mut s = Session::new(sid_b, "/tmp/q", ControlModel::Tmux, None);
s.status = SessionStatus::Active;
state.register_session(s);
for sid in [sid_a, sid_b] {
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_shared"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_shared", "aid_shared"),
);
}
observe(&state, sid_a, HookEvent::SubagentStop, &stop("aid_shared"));
assert_eq!(only(&state, sid_a).status, DelegationStatus::Completed);
assert_eq!(
only(&state, sid_b).status,
DelegationStatus::Running,
"a stop in session A must not touch session B"
);
}
#[test]
fn post_tool_use_without_tool_response_stays_running() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({ "tool": "Agent", "tool_use_id": "toolu_1" });
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Running,
"no response is 'unknown', not 'finished' — terminalizing here reports \
'no agents in flight' while the subagent is still running"
);
assert!(d.ended_at.is_none());
assert!(has_live_children(&state.delegations_for(sid)));
}
#[test]
fn post_tool_use_with_unrecognized_response_stays_running() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "content": [{ "type": "text", "text": "…" }] }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
assert_eq!(only(&state, sid).status, DelegationStatus::Running);
assert!(has_live_children(&state.delegations_for(sid)));
}
#[test]
fn post_tool_use_with_only_an_agent_id_stays_running() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "agentId": "aid_p1" }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Running);
assert_eq!(
d.agent_id.as_deref(),
Some("aid_p1"),
"the join key is still learned — it is just not read as completion"
);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid_p1"));
assert_eq!(only(&state, sid).status, DelegationStatus::Completed);
}
#[test]
fn changed_async_status_value_with_an_agent_id_stays_running() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "status": "launched", "agentId": "aid_p3" }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
assert_eq!(only(&state, sid).status, DelegationStatus::Running);
}
#[test]
fn null_agent_id_does_not_launch_a_phantom_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "agentId": null }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Completed,
"a null agentId is not a usable handle and carries no other launch \
marker, so it must not pin the delegation Running with nothing left \
to resolve it"
);
assert_eq!(d.agent_id, None);
}
#[test]
fn empty_string_agent_id_does_not_launch_a_phantom_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "agentId": "" }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Completed,
"an empty-string agentId is not a usable handle and carries no other \
launch marker, so it must not pin the delegation Running with \
nothing left to resolve it"
);
assert_eq!(d.agent_id, None);
}
#[test]
fn renamed_async_marker_does_not_terminalize() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"tool_response": { "agent_id": "a403", "is_async": true, "state": "async_launched" }
});
observe(&state, sid, HookEvent::PostToolUse, &payload);
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Running);
assert!(
d.agent_id.is_none(),
"no key we recognise, so nothing learned"
);
}
#[test]
fn stale_running_delegation_stops_suppressing_the_nudge() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
assert!(has_live_children(&state.delegations_for(sid)));
let sweep = state.sweep_delegations_at(Utc::now() + chrono::Duration::hours(3));
assert_eq!(sweep.staled, 0, "a 3h-old agent is still plausibly running");
assert!(has_live_children(&state.delegations_for(sid)));
let past = Utc::now() + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + 60);
let sweep = state.sweep_delegations_at(past);
assert_eq!(sweep.staled, 1);
let d = only(&state, sid);
assert_eq!(
d.status,
DelegationStatus::Stale,
"tracking gave up — but it must NOT claim the agent completed"
);
assert_ne!(d.status, DelegationStatus::Completed);
assert!(!has_live_children(&state.delegations_for(sid)));
}
#[test]
fn late_subagent_stop_still_resolves_a_stale_delegation() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
state
.sweep_delegations_at(Utc::now() + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + 1));
assert_eq!(only(&state, sid).status, DelegationStatus::Stale);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert_eq!(
only(&state, sid).status,
DelegationStatus::Completed,
"a late stop replaces 'we lost track' with the truth"
);
}
#[test]
fn declared_but_never_dispatched_goes_stale_quickly() {
let (state, sid) = state_with_session();
state.upsert_delegation(Delegation::new(
sid,
None,
"engineer",
ModelTier::Sonnet,
"declared",
));
let sweep = state.sweep_delegations_at(
Utc::now() + chrono::Duration::seconds(DECLARED_STALE_AFTER_SECS + 1),
);
assert_eq!(sweep.staled, 1);
assert_eq!(only(&state, sid).status, DelegationStatus::Stale);
assert!(!has_live_children(&state.delegations_for(sid)));
}
#[test]
fn terminal_delegations_are_evicted_after_retention() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert_eq!(state.delegations_for(sid).len(), 1);
let sweep = state.sweep_delegations_at(
Utc::now() + chrono::Duration::seconds(DELEGATION_RETENTION_SECS + 1),
);
assert_eq!(sweep.evicted, 1);
assert!(
state.delegations_for(sid).is_empty(),
"the map must not grow monotonically for the daemon's lifetime"
);
}
#[test]
fn live_delegations_are_never_evicted() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
let sweep = state.sweep_delegations_at(Utc::now() + chrono::Duration::days(30));
assert_eq!(sweep.evicted, 0, "a live record is never evicted");
assert_eq!(state.delegations_for(sid).len(), 1);
}
#[test]
fn a_stale_delegation_has_no_ended_at() {
let (state, sid) = state_with_session();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
state
.sweep_delegations_at(Utc::now() + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + 1));
let d = only(&state, sid);
assert_eq!(d.status, DelegationStatus::Stale);
assert!(
d.ended_at.is_none(),
"the sweep must not claim the delegation ended — it did not end, we \
stopped trusting the record"
);
}
#[test]
fn stale_delegation_stays_resolvable_far_past_the_terminal_window() {
let (state, sid) = state_with_session();
let t0 = Utc::now();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
state.sweep_delegations_at(t0 + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + 1));
assert_eq!(only(&state, sid).status, DelegationStatus::Stale);
state.sweep_delegations_at(
t0 + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + DELEGATION_RETENTION_SECS + 60),
);
assert_eq!(
state.delegations_for(sid).len(),
1,
"the record that used to vanish at ~7 h must still be here"
);
let sweep =
state.sweep_delegations_at(t0 + chrono::Duration::seconds(STALE_RETENTION_SECS - 60));
assert_eq!(sweep.evicted, 0);
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert_eq!(
only(&state, sid).status,
DelegationStatus::Completed,
"a stop arriving ~24 h later still replaces 'we lost track' with the truth"
);
}
#[test]
fn a_stale_delegation_is_eventually_evicted() {
let (state, sid) = state_with_session();
let t0 = Utc::now();
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "t", "toolu_1"),
);
observe(
&state,
sid,
HookEvent::PostToolUse,
&post_async("Agent", "toolu_1", "aid1"),
);
state.sweep_delegations_at(t0 + chrono::Duration::seconds(RUNNING_STALE_AFTER_SECS + 1));
let sweep =
state.sweep_delegations_at(t0 + chrono::Duration::seconds(STALE_RETENTION_SECS + 60));
assert_eq!(sweep.evicted, 1);
assert!(state.delegations_for(sid).is_empty());
observe(&state, sid, HookEvent::SubagentStop, &stop("aid1"));
assert!(state.delegations_for(sid).is_empty());
}
#[test]
fn dedup_does_not_merge_a_different_task() {
let (state, sid) = state_with_session();
state.upsert_delegation(Delegation::new(
sid,
None,
"rust-engineer",
ModelTier::Sonnet,
"fix the delegation tracker",
));
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre(
"Agent",
"rust-engineer",
"rebase the release branch",
"toolu_2",
),
);
let all = state.delegations_for(sid);
assert_eq!(all.len(), 2, "different work is a different delegation");
let observed = all
.iter()
.find(|d| d.source == DelegationSource::HookObserved)
.expect("observed record");
assert_eq!(
observed.task, "rebase the release branch",
"the dispatched task text must survive"
);
}
#[test]
fn dedup_declines_a_description_less_dispatch() {
let (state, sid) = state_with_session();
state.upsert_delegation(Delegation::new(
sid,
None,
"engineer",
ModelTier::Sonnet,
"fix the delegation tracker",
));
let payload = serde_json::json!({
"tool": "Agent",
"tool_use_id": "toolu_1",
"input": { "subagent_type": "engineer" }
});
observe(&state, sid, HookEvent::PreToolUse, &payload);
assert_eq!(state.delegations_for(sid).len(), 2);
}
#[test]
fn dedup_merges_an_unlabelled_declaration() {
let (state, sid) = state_with_session();
let declared = Delegation::new(sid, None, "engineer", ModelTier::Opus, "");
let declared_id = declared.id;
state.upsert_delegation(declared);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre("Agent", "engineer", "rebase the release branch", "toolu_1"),
);
let d = only(&state, sid);
assert_eq!(d.id, declared_id);
assert_eq!(d.task, "rebase the release branch");
}
#[test]
fn dedup_merges_a_summarised_task_description() {
let (state, sid) = state_with_session();
let declared = Delegation::new(
sid,
None,
"engineer",
ModelTier::Opus,
"Fix the delegation tracker fail-open bug",
);
let declared_id = declared.id;
state.upsert_delegation(declared);
observe(
&state,
sid,
HookEvent::PreToolUse,
&pre(
"Agent",
"engineer",
"fix the delegation TRACKER",
"toolu_1",
),
);
let d = only(&state, sid);
assert_eq!(d.id, declared_id);
assert_eq!(d.status, DelegationStatus::Running);
}
#[test]
fn missing_subagent_type_still_tracks_as_live() {
let (state, sid) = state_with_session();
let payload = serde_json::json!({ "tool": "Agent", "tool_use_id": "toolu_x" });
observe(&state, sid, HookEvent::PreToolUse, &payload);
let d = only(&state, sid);
assert_eq!(d.agent, "unknown");
assert_eq!(d.status, DelegationStatus::Running);
}