use super::support::*;
#[test]
fn inherited_subagent_hook_payload_uses_child_metadata() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(WritingProvider::new());
let mut cfg = config(provider, temp.path());
cfg.sessions_root = Some(temp.path().join("sessions"));
cfg.inherited_hooks = Some(
HookRuntime::new(
temp.path(),
HookSettings {
enabled: true,
before_tool: vec![HookDefinition {
label: Some("capture-child".into()),
command: "cat > child-hook.json".into(),
include_tools: vec!["write".into()],
..HookDefinition::default()
}],
..HookSettings::default()
},
true,
)
.unwrap(),
);
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "write from child".into(),
agent: Some("reviewer".into()),
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
assert_eq!(output.summary.completed, 1);
let result = &output.results[0];
let session_path = result.session_path.as_ref().expect("child session path");
let payload: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(temp.path().join("child-hook.json")).unwrap(),
)
.unwrap();
assert_eq!(payload["context"]["invocation_mode"], "subagent");
assert_eq!(payload["context"]["subagent"], true);
assert_eq!(payload["context"]["agent_id"], "reviewer");
assert_eq!(
payload["context"]["session_id"],
result.session_id.as_deref().unwrap()
);
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")
);
assert_eq!(payload["affected_paths"][0]["path"], "child.txt");
}
#[test]
fn inherited_hooks_run_child_write_with_child_cwd_and_session_only() {
let temp = tempfile::TempDir::new().unwrap();
let child_dir = temp.path().join("child");
std::fs::create_dir(&child_dir).unwrap();
let provider = Arc::new(WritingProvider::new());
let runtime = HookRuntime::new(
temp.path(),
HookSettings {
enabled: true,
before_tool: vec![HookDefinition {
label: Some("child-cwd-capture".into()),
command: "cat > hook-payload.json".into(),
include_tools: vec!["write".into()],
..HookDefinition::default()
}],
..HookSettings::default()
},
true,
)
.unwrap();
let mut cfg = config(provider, temp.path());
cfg.sessions_root = Some(temp.path().join("sessions"));
cfg.inherited_hooks = Some(runtime);
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "write child file".into(),
agent: None,
identity: None,
context: None,
cwd: Some(PathBuf::from("child")),
}],
},
cfg,
)
.unwrap();
assert_eq!(output.summary.failed, 0);
assert!(!temp.path().join("hook-payload.json").exists());
let payload_text = std::fs::read_to_string(child_dir.join("hook-payload.json")).unwrap();
let payload: Value = serde_json::from_str(&payload_text).unwrap();
let expected_child_cwd = child_dir.canonicalize().unwrap().display().to_string();
assert_eq!(payload["cwd"].as_str(), Some(expected_child_cwd.as_str()));
assert_eq!(
payload["context"]["cwd"].as_str(),
Some(expected_child_cwd.as_str())
);
assert_eq!(payload["context"]["invocation_mode"], "subagent");
assert_eq!(payload["context"]["subagent"], true);
assert!(payload["context"]["session_id"].as_str().is_some());
let session_path = payload["context"]["session_path"].as_str().unwrap();
assert!(
session_path.contains("/sessions/subagents/"),
"{session_path}"
);
assert!(session_path.ends_with(".jsonl"), "{session_path}");
let session_jsonl =
std::fs::read_to_string(output.results[0].session_path.as_ref().unwrap()).unwrap();
assert!(session_jsonl.contains("hook_lifecycle"), "{session_jsonl}");
assert!(
session_jsonl.contains("child-cwd-capture"),
"{session_jsonl}"
);
let parent_result_content = serde_json::to_string(&output).unwrap();
assert!(!parent_result_content.contains("hook_lifecycle"));
assert!(!parent_result_content.contains("child-cwd-capture"));
}
#[test]
fn absent_inherited_hooks_leave_child_session_without_hook_records() {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(WritingProvider::new());
let mut cfg = config(provider, temp.path());
cfg.sessions_root = Some(temp.path().join("sessions"));
cfg.inherited_hooks = None;
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "write child file".into(),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let session_jsonl =
std::fs::read_to_string(output.results[0].session_path.as_ref().unwrap()).unwrap();
assert!(!session_jsonl.contains("hook_lifecycle"), "{session_jsonl}");
assert!(
!session_jsonl.contains("hook_diagnostic"),
"{session_jsonl}"
);
}
#[test]
fn inherited_hook_failure_policies_preserve_local_records_and_parent_safety() {
for (policy, target_runs, failed) in [
(HookFailurePolicy::Ignore, true, false),
(HookFailurePolicy::Warn, true, false),
(HookFailurePolicy::Block, false, false),
(HookFailurePolicy::Fail, false, true),
] {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(PolicyToolProvider::new());
let runtime = HookRuntime::new(
temp.path(),
HookSettings {
enabled: true,
before_tool: vec![HookDefinition {
label: Some("CHILD_HOOK_LABEL_LEAK_MARKER".into()),
command: "printf CHILD_STDOUT_POISON; printf CHILD_STDERR_POISON >&2; exit 7"
.into(),
failure_policy: Some(policy),
include_tools: vec!["write".into()],
..HookDefinition::default()
}],
..HookSettings::default()
},
true,
)
.unwrap();
let mut cfg = config(provider.clone(), temp.path());
cfg.sessions_root = Some(temp.path().join("sessions"));
cfg.inherited_hooks = Some(runtime);
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: format!("policy {}", policy.as_str()),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
assert_eq!(
output.results[0].status == SubagentStatus::Failed,
failed,
"{policy:?}"
);
assert_eq!(
temp.path().join("policy.txt").exists(),
target_runs,
"{policy:?}"
);
let session_jsonl =
std::fs::read_to_string(output.results[0].session_path.as_ref().unwrap()).unwrap();
assert!(
session_jsonl.contains("hook_lifecycle"),
"{policy:?}: {session_jsonl}"
);
assert!(
session_jsonl.contains("CHILD_HOOK_LABEL_LEAK_MARKER"),
"{policy:?}: {session_jsonl}"
);
if matches!(
policy,
HookFailurePolicy::Warn | HookFailurePolicy::Block | HookFailurePolicy::Fail
) {
assert!(
session_jsonl.contains("hook_diagnostic"),
"{policy:?}: {session_jsonl}"
);
}
let parent_result_content = serde_json::to_string(&output).unwrap();
for marker in [
"CHILD_HOOK_LABEL_LEAK_MARKER",
"CHILD_STDOUT_POISON",
"CHILD_STDERR_POISON",
"hook_lifecycle",
"hook_diagnostic",
] {
assert!(
!parent_result_content.contains(marker),
"{policy:?}: marker {marker} leaked in {parent_result_content}"
);
}
if policy == HookFailurePolicy::Fail {
assert_eq!(
output.results[0].error.as_deref(),
Some(
"child subagent stopped by local hook policy; inspect child session JSONL for local hook details"
)
);
}
}
}
#[test]
fn inherited_hook_activity_is_local_only_and_respects_visibility_setting() {
for show_in_tui in [true, false] {
let temp = tempfile::TempDir::new().unwrap();
let provider = Arc::new(WritingProvider::new());
let events: Arc<Mutex<Vec<ActivityEvent>>> = Arc::new(Mutex::new(Vec::new()));
let captured = Arc::clone(&events);
let runtime = HookRuntime::new(
temp.path(),
HookSettings {
enabled: true,
show_in_tui,
before_tool: vec![HookDefinition {
label: Some("CHILD_TUI_HOOK_MARKER".into()),
command: "true".into(),
include_tools: vec!["write".into()],
..HookDefinition::default()
}],
..HookSettings::default()
},
true,
)
.unwrap();
let mut cfg = config(provider, temp.path());
cfg.sessions_root = Some(temp.path().join("sessions"));
cfg.inherited_hooks = Some(runtime);
cfg.activity_sender = Some(Arc::new(move |event| captured.lock().unwrap().push(event)));
let output = run_subagents(
SubagentsArgs {
concurrency: Some(1),
tasks: vec![SubagentTask {
intent: "visible hook activity".into(),
agent: None,
identity: None,
context: None,
cwd: None,
}],
},
cfg,
)
.unwrap();
let session_jsonl =
std::fs::read_to_string(output.results[0].session_path.as_ref().unwrap()).unwrap();
assert!(session_jsonl.contains("hook_lifecycle"));
let has_hook_activity = events.lock().unwrap().iter().any(|event| {
matches!(
event,
ActivityEvent::Started {
kind: ActivityKind::Hook,
..
}
)
});
assert_eq!(has_hook_activity, show_in_tui);
let parent_result_content = serde_json::to_string(&output).unwrap();
assert!(!parent_result_content.contains("CHILD_TUI_HOOK_MARKER"));
assert!(!parent_result_content.contains("hook_lifecycle"));
}
}