magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
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",
                prompt_origin: crate::output::UserPromptOrigin::User,
                effective_prompt: None,
                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(),
                continuation_auto_compaction_policy: None,
            },
        )
        .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")
    ));
}