ai-dispatch 8.91.1

Multi-AI CLI team orchestrator
// Tests for the Claude CLI adapter covering command construction and stream parsing.
// Exports: module-scoped tests only.
// Deps: super::ClaudeAgent, crate::agent::Agent, tempfile.

use super::ClaudeAgent;
use crate::agent::{Agent, RunOpts, embed_context_in_prompt};
use crate::types::{EventKind, TaskId};
use tempfile::tempdir;

#[test]
fn build_command_uses_stream_json_and_verbose() {
    let dir = tempdir().unwrap();
    let opts = RunOpts {
        dir: Some(dir.path().to_string_lossy().to_string()),
        output: None,
        result_file: None,
        model: Some("sonnet".to_string()),
        budget: false,
        read_only: false,
        context_files: vec![],
        session_id: None,
        env: None,
        env_forward: None,
    };
    let cmd = ClaudeAgent.build_command("test prompt", &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(), "claude");
    assert!(args.windows(2).any(|pair| pair == ["--output-format", "stream-json"]));
    assert!(args.iter().any(|arg| arg == "--verbose"));
    assert!(args.windows(2).any(|pair| pair == ["--model", "sonnet"]));
    assert!(args.windows(2).any(|pair| pair[0] == "--add-dir"));
}

#[test]
fn build_command_read_only_restricts_tools() {
    let opts = RunOpts {
        dir: None,
        output: None,
        result_file: Some("result.md".to_string()),
        model: None,
        budget: false,
        read_only: true,
        context_files: vec![],
        session_id: None,
        env: None,
        env_forward: None,
    };
    let cmd = ClaudeAgent.build_command("inspect", &opts).unwrap();
    let args: Vec<String> = cmd.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
    assert!(args.windows(2).any(|pair| pair == ["--allowedTools", "Read,Glob,Grep,LS,Write"]));
}

#[test]
fn build_command_read_only_without_result_file_keeps_strict_tools() {
    let opts = RunOpts {
        dir: None,
        output: None,
        result_file: None,
        model: None,
        budget: false,
        read_only: true,
        context_files: vec![],
        session_id: None,
        env: None,
        env_forward: None,
    };
    let cmd = ClaudeAgent.build_command("inspect", &opts).unwrap();
    let args: Vec<String> = cmd.get_args().map(|arg| arg.to_string_lossy().into_owned()).collect();
    assert!(args.windows(2).any(|pair| pair == ["--allowedTools", "Read,Glob,Grep,LS"]));
}

#[test]
fn embed_context_in_prompt_appends_context_file_contents() {
    let dir = tempdir().unwrap();
    let context_path = dir.path().join("context.txt");
    std::fs::write(&context_path, "shared context").unwrap();

    let prompt = embed_context_in_prompt(
        "inspect",
        &[context_path.to_string_lossy().to_string()],
    )
    .unwrap();

    assert_eq!(
        prompt,
        format!(
            "inspect\n\n[Context File: {}]\nshared context",
            context_path.to_string_lossy()
        )
    );
}

#[test]
fn parses_assistant_reasoning_event() {
    let line = r#"{"type":"assistant","message":{"model":"claude-sonnet-4-5","content":[{"type":"text","text":"Thinking through the change."}]},"session_id":"session-1"}"#;
    let event = ClaudeAgent
        .parse_event(&TaskId("t-claude".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::Reasoning);
    assert_eq!(event.detail, "Thinking through the change.");
    assert_eq!(
        event.metadata.as_ref().and_then(|value| value.get("model")).and_then(|value| value.as_str()),
        Some("claude-sonnet-4-5")
    );
}

#[test]
fn parses_tool_use_event() {
    let line = r#"{"type":"assistant","message":{"content":[{"type":"tool_use","id":"toolu_1","name":"Bash","input":{"command":"pwd","description":"Print working directory"}}]}}"#;
    let event = ClaudeAgent
        .parse_event(&TaskId("t-claude".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::ToolCall);
    assert_eq!(event.detail, "Bash: pwd");
}

#[test]
fn parses_completion_event() {
    let line = r#"{"type":"result","subtype":"success","result":"Hello!","total_cost_usd":0.14359275,"session_id":"session-1","usage":{"input_tokens":4,"cache_creation_input_tokens":18821,"cache_read_input_tokens":44733,"output_tokens":143},"modelUsage":{"claude-opus-4-6[1m]":{"inputTokens":4}}}"#;
    let event = ClaudeAgent
        .parse_event(&TaskId("t-claude".to_string()), line)
        .unwrap();
    assert_eq!(event.event_kind, EventKind::Completion);
    assert_eq!(
        event.metadata.as_ref().and_then(|value| value.get("tokens")).and_then(|value| value.as_i64()),
        Some(63_701)
    );
    assert_eq!(
        event.metadata.as_ref().and_then(|value| value.get("model")).and_then(|value| value.as_str()),
        Some("claude-opus-4-6")
    );
    assert_eq!(
        event.metadata.as_ref().and_then(|value| value.get("cost_usd")).and_then(|value| value.as_f64()),
        Some(0.14359275)
    );
}