magi-code 0.77.1

Repository-aware CLI coding agent for terminal work
Documentation
use super::*;

fn enabled_ttsr_settings() -> crate::config::TtsrSettings {
    crate::config::TtsrSettings {
        enabled: true,
        rules: Vec::new(),
    }
}

#[derive(Default)]
struct SeparatorRejectingSink {
    text: String,
    outputs: Vec<OutputEvent>,
}

impl AgentOutputSink for SeparatorRejectingSink {
    fn assistant_delta(&mut self, text: &str) -> anyhow::Result<()> {
        self.text.push_str(text);
        Ok(())
    }

    fn output_event(&mut self, event: OutputEvent) -> anyhow::Result<()> {
        if matches!(&event, OutputEvent::AssistantDelta { text } if text == "\n\n") {
            anyhow::bail!("separator sink rejected")
        }
        if let OutputEvent::AssistantDelta { text } = &event {
            self.text.push_str(text);
        }
        self.outputs.push(event);
        Ok(())
    }

    fn tool_block(&mut self, _block: &str) -> anyhow::Result<()> {
        Ok(())
    }
}

fn ttsr_run_request<'a, 'sink>(
    prompt: &'a str,
    cwd: &'a std::path::Path,
    session: Option<&'a crate::sessions::Session>,
    output_sink: Option<&'sink mut dyn AgentOutputSink>,
) -> AgentRunRequest<'a, 'sink> {
    AgentRunRequest {
        prompt,
        prompt_origin: crate::output::UserPromptOrigin::User,
        effective_prompt: None,
        tools: None,
        hooks: None,
        session,
        cwd,
        output_sink,
        cancellation: AgentCancellation::default(),
        session_title_job: None,
        semantic_progress_timeout: None,
        invocation_mode: crate::output::InvocationMode::Print,
        agent_id: None,
        initial_instructions: &[],
        ttsr: enabled_ttsr_settings(),
        herdr_reporter: None,
        continuation_auto_compaction_policy: None,
    }
}

#[test]
fn ttsr_stream_match_blocks_delta_before_sink() {
    let temp = tempfile::TempDir::new().unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![vec![text("safe "), text("rm -rf / "), done()]]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
    let mut sink = CapturingOutputSink::default();

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(
            &provider,
            ttsr_run_request("say", temp.path(), Some(&session), Some(&mut sink)),
        )
        .unwrap();

    assert_eq!(output.text, "safe ");
    assert_eq!(sink.text, "safe ");
    assert!(!assistant_chunk_text(&session.read_events().unwrap()).contains("rm -rf"));
    assert!(event_types(&session).contains(&"ttsr_injection".to_string()));
}

#[test]
fn ttsr_match_aborts_and_retries_with_reminder() {
    let temp = tempfile::TempDir::new().unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![text("git reset --hard"), done()],
        text_done(" safe retry"),
    ]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(
            &provider,
            ttsr_run_request("say", temp.path(), Some(&session), None),
        )
        .unwrap();

    assert_eq!(output.text, " safe retry");
    assert_eq!(provider.requests().len(), 2);
    assert!(provider.requests()[1].messages().iter().any(|message| {
        message.role == crate::providers::MessageRole::System
            && message.content.contains("Git safety reminder")
    }));
    let events = session.read_events().unwrap();
    assert!(
        events
            .iter()
            .any(|event| event.event_type == "ttsr_injection")
    );
    assert!(!events.iter().any(|event| {
        event.event_type == "turn_status" && event.payload["status"] == "cancelled"
    }));
}

#[test]
fn ttsr_retry_preserves_pending_separator_after_tool_iteration() {
    let temp = tempfile::TempDir::new().unwrap();
    std::fs::write(temp.path().join("file.txt"), "file contents").unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![text("prior text"), read_call("call_separator_ttsr"), done()],
        vec![text("git reset --hard"), done()],
        text_done("safe text"),
    ]);
    let tools = ToolRuntime::new(temp.path()).unwrap();
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
    let mut sink = CapturingOutputSink::default();
    let mut request =
        ttsr_run_request("use the tool", temp.path(), Some(&session), Some(&mut sink));
    request.tools = Some(&tools);

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(&provider, request)
        .unwrap();

    assert_eq!(output.text, "prior text\n\nsafe text");
    assert_eq!(provider.requests().len(), 3);
    let assistant_deltas = sink
        .outputs
        .iter()
        .filter_map(|event| match event {
            OutputEvent::AssistantDelta { text } => Some(text.as_str()),
            _ => None,
        })
        .collect::<Vec<_>>();
    assert_eq!(assistant_deltas, vec!["prior text", "\n\n", "safe text"]);
    assert_eq!(
        assistant_chunk_text(&session.read_events().unwrap()),
        "prior textsafe text"
    );
}

#[test]
fn separator_sink_rejection_does_not_persist_separator_or_delta() {
    let temp = tempfile::TempDir::new().unwrap();
    std::fs::write(temp.path().join("file.txt"), "file contents").unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![
            text("prior text"),
            read_call("call_separator_reject"),
            done(),
        ],
        text_done("safe text"),
    ]);
    let tools = ToolRuntime::new(temp.path()).unwrap();
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
    let mut sink = SeparatorRejectingSink::default();
    let mut request =
        ttsr_run_request("use the tool", temp.path(), Some(&session), Some(&mut sink));
    request.tools = Some(&tools);

    let error = agent
        .run_print_with_tools_streaming_output_cancellable(&provider, request)
        .unwrap_err()
        .to_string();

    assert!(error.contains("separator sink rejected"), "{error}");
    assert_eq!(provider.requests().len(), 2);
    assert_eq!(sink.text, "prior text");
    assert!(sink.outputs.iter().all(|event| {
        !matches!(event, OutputEvent::AssistantDelta { text } if text == "\n\n" || text == "safe text")
    }));
    let events = session.read_events().unwrap();
    assert_eq!(assistant_chunk_text(&events), "prior text");
    assert!(!events.iter().any(|event| {
        event.event_type == "assistant_chunk"
            && event.payload["text"]
                .as_str()
                .is_some_and(|text| text.contains("safe text"))
    }));
}

#[test]
fn ttsr_retry_blocks_soft_compaction_after_tool_growth() {
    let temp = tempfile::TempDir::new().unwrap();
    std::fs::write(temp.path().join("file.txt"), "large enough").unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![text("git reset --hard"), done()],
        read_done("recovery_read"),
        text_done("safe after tool"),
    ]);
    let tools = ToolRuntime::new(temp.path()).unwrap();
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
    let mut request = ttsr_run_request("say", temp.path(), None, None);
    request.tools = Some(&tools);
    request.continuation_auto_compaction_policy = Some((1, "1 token".to_string()));

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(&provider, request)
        .unwrap();

    assert_eq!(provider.requests().len(), 3);
    assert_eq!(output.text, "safe after tool");
    assert!(output.auto_compaction_blocked_by_recovery);
}

#[test]
fn ttsr_request_cancellation_does_not_poison_outer_user_cancellation() {
    let temp = tempfile::TempDir::new().unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![text("git reset --hard"), done()],
        text_done("safe after retry"),
    ]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(
            &provider,
            ttsr_run_request("say", temp.path(), None, None),
        )
        .unwrap();

    assert_eq!(output.text, "safe after retry");
    assert_eq!(provider.requests().len(), 2);
}

#[test]
fn ttsr_completed_tool_call_arguments_are_checked_before_tool_lifecycle() {
    let temp = tempfile::TempDir::new().unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![bash_call("bad", "rm -rf / "), done()],
        text_done("no tool"),
    ]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());

    let output = agent
        .run_print_with_tools_streaming_output_cancellable(
            &provider,
            ttsr_run_request("use tool", temp.path(), Some(&session), None),
        )
        .unwrap();

    assert_eq!(output.text, "no tool");
    assert!(!event_types(&session).contains(&"tool_call".to_string()));
    assert!(event_types(&session).contains(&"ttsr_injection".to_string()));
}

#[test]
fn ttsr_retry_exhaustion_records_failed_turn() {
    let temp = tempfile::TempDir::new().unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![
        vec![text("git clean -fd"), done()],
        vec![text("git clean -fd"), done()],
        vec![text("git clean -fd"), done()],
        vec![text("git clean -fd"), done()],
    ]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());

    let error = agent
        .run_print_with_tools_streaming_output_cancellable(
            &provider,
            ttsr_run_request("say", temp.path(), Some(&session), None),
        )
        .unwrap_err()
        .to_string();

    assert!(
        error.contains("TTSR") || error.contains("interrupted"),
        "{error}"
    );
    let events = session.read_events().unwrap();
    assert_eq!(
        events
            .iter()
            .filter(|event| event.event_type == "ttsr_injection")
            .count(),
        3
    );
    assert!(
        events.iter().any(|event| {
            event.event_type == "turn_status" && event.payload["status"] == "failed"
        })
    );
}

#[test]
fn invalid_ttsr_config_fails_before_durable_turn_start() {
    let temp = tempfile::TempDir::new().unwrap();
    let session = crate::sessions::SessionManager::new(temp.path().join("sessions"))
        .create()
        .unwrap();
    let provider = ScriptedProvider::new(vec![text_done("unreachable")]);
    let agent = AgentSession::new("model", &[], &SkillDiscovery::default());
    let mut request = ttsr_run_request("say", temp.path(), Some(&session), None);
    request.ttsr.rules.push(crate::config::TtsrRuleSetting {
        pattern: "[".to_string(),
        reminder: "invalid".to_string(),
    });

    let error = agent
        .run_print_with_tools_streaming_output_cancellable(&provider, request)
        .unwrap_err();

    assert!(error.to_string().contains("regex"));
    assert!(provider.requests().is_empty());
    assert!(session.read_events().unwrap().is_empty());
}