ai-dispatch 10.38.0

Multi-AI CLI team orchestrator
// Tests for the Copilot CLI adapter covering command construction and JSON event parsing.
// Exports: module-scoped tests only.
// Deps: super::CopilotAgent, crate::agent::Agent, crate::types.

use super::CopilotAgent;
use crate::agent::{Agent, RunOpts};
use crate::types::{AgentKind, EventKind, TaskId};
use crate::{paths, rate_limit};

fn opts() -> RunOpts {
    RunOpts {
        dir: Some("/tmp/project".to_string()),
        output: None,
        result_file: None,
        model: Some("gpt-5.2".to_string()),
        budget: false,
        read_only: false,
        sandbox: false,
        context_files: vec!["/tmp/project/docs/spec.md".to_string()],
        session_id: None,
        env: None,
        env_forward: None,
    }
}

#[test]
fn build_command_uses_copilot_prompt_mode() {
    let cmd = CopilotAgent.build_command("ship it", &opts()).unwrap();
    let args: Vec<String> = cmd
        .get_args()
        .map(|arg| arg.to_string_lossy().into_owned())
        .collect();
    assert_eq!(cmd.get_program().to_string_lossy(), "copilot");
    assert!(args.windows(2).any(|pair| pair == ["-p", "ship it"]));
    assert!(
        args.windows(2)
            .any(|pair| pair == ["--output-format", "json"])
    );
    assert!(args.windows(2).any(|pair| pair == ["--stream", "on"]));
    assert!(args.windows(2).any(|pair| pair == ["--model", "gpt-5.2"]));
    assert!(
        args.windows(2)
            .any(|pair| pair == ["--add-dir", "/tmp/project"])
    );
    assert!(
        args.windows(2)
            .any(|pair| pair == ["--add-dir", "/tmp/project/docs"])
    );
}

#[test]
fn parse_event_reads_final_assistant_message() {
    let line = r#"{"type":"assistant.message","data":{"content":"hello","outputTokens":3}}"#;
    let event = CopilotAgent
        .parse_event(&TaskId("t-copilot".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::Reasoning);
    assert_eq!(event.detail, "hello");
}

#[test]
fn parse_event_reads_tool_execution_start() {
    let line = r#"{"type":"tool.execution_start","data":{"toolName":"view","arguments":{"path":"Cargo.toml"}}}"#;
    let event = CopilotAgent
        .parse_event(&TaskId("t-copilot".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::FileRead);
    assert_eq!(event.detail, "view: Cargo.toml");
}

#[test]
fn parse_event_ignores_persistence_session_errors() {
    let line =
        r#"{"type":"session.error","data":{"errorType":"persistence","message":"mkdir failed"}}"#;
    assert!(
        CopilotAgent
            .parse_event(&TaskId("t-copilot".to_string()), line)
            .is_none()
    );
}

/// Live t-03a68876: `session.error` + `data.errorCode=quota_exceeded`.
#[test]
fn parse_event_marks_live_session_error_quota() {
    let temp = tempfile::tempdir().unwrap();
    let _aid_home = paths::AidHomeGuard::set(temp.path());
    rate_limit::clear_rate_limit(&AgentKind::Copilot, None);
    let line = r#"{"type":"session.error","data":{"errorType":"quota","message":"You have exceeded your monthly quota (Request ID: EC26:161163:156CE45:19CFB24:6A75A3A0)","statusCode":402,"errorCode":"quota_exceeded"}}"#;
    let event = CopilotAgent
        .parse_event(&TaskId("t-copilot".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::Error);
    assert!(rate_limit::is_rate_limited(&AgentKind::Copilot, None));
    rate_limit::clear_rate_limit(&AgentKind::Copilot, None);
}