use super::*;
#[test]
fn agent_run_request_invocation_mode_reaches_hook_payload() {
for (mode, expected) in [
(InvocationMode::Print, "print"),
(InvocationMode::Shell, "shell"),
(InvocationMode::MissionControl, "mission_control"),
] {
let temp = tempfile::TempDir::new().unwrap();
let provider = ScriptedProvider::new(vec![
vec![write_call("write_1", "file.txt", "content"), done()],
text_done("done"),
]);
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
let tools = ToolRuntime::new(temp.path()).unwrap();
let hooks = HookRuntime::new(
temp.path(),
crate::config::HookSettings {
enabled: true,
before_tool: vec![crate::config::HookDefinition {
label: Some("capture".into()),
command: "cat > hook.json".into(),
include_tools: vec!["write".into()],
..crate::config::HookDefinition::default()
}],
..crate::config::HookSettings::default()
},
true,
)
.unwrap();
let manager = crate::sessions::SessionManager::new(temp.path().join("sessions"));
let session = manager.create().unwrap();
let mut sink = CapturingOutputSink::default();
agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
AgentRunRequest {
tools: Some(&tools),
hooks: Some(&hooks),
session: Some(&session),
output_sink: Some(&mut sink),
invocation_mode: mode,
agent_id: Some("agent-main".to_string()),
..run_request("write file", temp.path())
},
)
.unwrap();
let payload: serde_json::Value =
serde_json::from_str(&std::fs::read_to_string(temp.path().join("hook.json")).unwrap())
.unwrap();
assert_eq!(payload["context"]["invocation_mode"], expected);
assert_eq!(payload["context"]["subagent"], false);
assert_eq!(payload["context"]["agent_id"], "agent-main");
assert_eq!(payload["context"]["session_id"], session.id());
assert_eq!(
payload["context"]["session_path"],
session.path().display().to_string()
);
assert_eq!(payload["context"]["turn_id"], "turn-0");
assert!(
payload["context"]["message_id"]
.as_str()
.unwrap()
.contains("write_1")
);
}
}
#[test]
fn visible_hook_lifecycle_activity_orders_around_target_dispatch() {
let temp = tempfile::TempDir::new().unwrap();
let before_marker = temp.path().join("before_hook_started");
let after_observed = temp.path().join("after_observed_tool_done");
let before_marker_for_probe = before_marker.clone();
let after_observed_for_probe = after_observed.clone();
let temp_for_probe = temp.path().to_path_buf();
let mut sink = ProbeActivitySink::new(move |event| {
if let ActivityEvent::Started {
kind: ActivityKind::Hook,
metadata,
..
} = event
{
let phase = metadata
.fields
.iter()
.find_map(|(key, value)| (key == "phase").then_some(value.as_str()));
if phase == Some("before_tool") {
std::fs::write(&before_marker_for_probe, "started").unwrap();
}
if phase == Some("after_tool") && temp_for_probe.join("tool_done").exists() {
std::fs::write(&after_observed_for_probe, "started after tool").unwrap();
}
}
});
let provider = ScriptedProvider::new(vec![
vec![
bash_call(
"call_bash",
"cat before_hook_started > dispatch_saw_before_event; printf done > tool_done",
),
done(),
],
vec![done()],
]);
let tools = ToolRuntime::new(temp.path()).unwrap();
let hooks = HookRuntime::new(
temp.path(),
crate::config::HookSettings {
enabled: true,
show_in_tui: true,
before_tool: vec![crate::config::HookDefinition {
label: Some("before-visible".into()),
command: "true".into(),
..crate::config::HookDefinition::default()
}],
after_tool: vec![crate::config::HookDefinition {
label: Some("after-visible".into()),
command: "true".into(),
..crate::config::HookDefinition::default()
}],
..crate::config::HookSettings::default()
},
true,
)
.unwrap();
let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
agent
.run_print_with_tools_streaming_output_cancellable(
&provider,
AgentRunRequest {
initial_instructions: &[],
prompt: "run bash",
tools: Some(&tools),
hooks: Some(&hooks),
session: None,
cwd: temp.path(),
output_sink: Some(&mut sink),
cancellation: AgentCancellation::default(),
session_title_job: None,
semantic_progress_timeout: None,
invocation_mode: crate::output::InvocationMode::Print,
agent_id: None,
herdr_reporter: None,
ttsr: crate::config::TtsrSettings::default(),
},
)
.unwrap();
assert_eq!(
std::fs::read_to_string(temp.path().join("dispatch_saw_before_event")).unwrap(),
"started"
);
assert!(after_observed.exists());
let hook_events = sink.sender_events.lock().unwrap();
assert_eq!(hook_events.len(), 4);
assert!(matches!(
&hook_events[0],
ActivityEvent::Started { metadata, .. }
if metadata.fields.iter().any(|(key, value)| key == "phase" && value == "before_tool")
));
assert!(matches!(
&hook_events[2],
ActivityEvent::Started { metadata, .. }
if metadata.fields.iter().any(|(key, value)| key == "phase" && value == "after_tool")
));
}