magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
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"));
    }
}