capo-cli 0.7.0

Capo — a Rust-native coding agent CLI.
//! End-to-end: --image with -p attaches a base64'd PNG to the user message
//! reaching the LLM. See spec §7.3.

#![cfg_attr(test, allow(clippy::expect_used, clippy::unwrap_used))]

use std::io::Write;
use std::path::PathBuf;
use std::sync::Arc;

use capo_agent::{test_support::ScriptedLlm, AppBuilder, Attachment, Config, UserMessage};
use futures::StreamExt;
use motosan_agent_loop::{ContentPart, Message};

fn png_fixture() -> PathBuf {
    let mut path = std::env::temp_dir();
    path.push(format!("capo-v06-e2e-print-{}.png", std::process::id()));
    let mut bytes = vec![0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A];
    bytes.extend_from_slice(&[0u8; 64]);
    std::fs::File::create(&path)
        .expect("create fixture")
        .write_all(&bytes)
        .expect("write fixture");
    path
}

#[tokio::test]
async fn print_mode_with_image_forwards_multipart_message_to_llm() {
    let path = png_fixture();
    let dir = tempfile::tempdir().expect("tempdir");

    let llm = Arc::new(ScriptedLlm::builder().then_message("acknowledged").build());

    let mut cfg = Config::default();
    cfg.anthropic.api_key = Some("sk-unused".into());
    let app = AppBuilder::new()
        .with_config(cfg)
        .with_cwd(dir.path())
        .with_builtin_tools()
        .with_llm(llm.clone())
        .build()
        .await
        .expect("build app");

    let msg = UserMessage {
        text: "describe".into(),
        attachments: vec![Attachment::Image { path }],
    };
    let _events: Vec<_> = app.send_user_message(msg).collect().await;

    let captured = llm.captured_inputs();
    assert!(!captured.is_empty(), "ScriptedLlm.chat never called");

    let last_call = captured.last().expect("captured input");
    let user_msg = last_call
        .iter()
        .rev()
        .find(|m| matches!(m, Message::User { .. }))
        .expect("user message in chat input");
    let parts = match user_msg {
        Message::User { content, .. } => content.as_slice(),
        _ => unreachable!(),
    };

    assert_eq!(parts.len(), 2, "expected [text, image]; got: {parts:?}");
    assert!(matches!(&parts[0], ContentPart::Text { text, .. } if text == "describe"));
    assert!(matches!(&parts[1], ContentPart::Image { .. }));
}