mobius 0.15.6

A small, modular Rust framework for building coding agents
Documentation
use base64::Engine as _;

use super::*;

const PNG: &str =
    "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNk+A8AAQUBAScY42YAAAAASUVORK5CYII=";

fn view_image_call() -> ToolCall {
    ToolCall {
        call_id: "view".into(),
        name: "view_image".into(),
        arguments: serde_json::json!({"path": "page.png"}),
    }
}

async fn execute_view_image(bytes: &[u8]) -> ToolResult {
    execute_view_images(bytes, &[view_image_call()])
        .await
        .pop()
        .expect("tool result")
}

async fn execute_view_images(bytes: &[u8], calls: &[ToolCall]) -> Vec<ToolResult> {
    let workspace = tempfile::tempdir().expect("workspace");
    std::fs::write(workspace.path().join("page.png"), bytes).expect("write image");
    let sandbox = Arc::new(Sandbox::new(
        Arc::new(
            crate::backend::sandbox::local::LocalSandbox::new(workspace.path())
                .expect("local sandbox"),
        ),
        crate::backend::sandbox::ApprovalPolicy::Ask,
    ));
    let mut catalog = Catalog::default();
    catalog
        .register(Arc::new(ViewImage))
        .expect("register view image");
    let calls = finalize_and_bind(&mut catalog, calls);

    execute_batch(&catalog, &calls, sandbox, &test_permissions(&[]), "turn").await
}

#[tokio::test]
async fn view_image_adds_workspace_image_to_model_input() {
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(PNG)
        .expect("decode PNG");

    let result = execute_view_image(&bytes).await;

    assert_eq!(
        result,
        ToolResult {
            call_id: "view".into(),
            name: "view_image".into(),
            output: "viewed image page.png".into(),
            is_error: false,
            handler_executed: true,
            additional_input: vec![serde_json::json!({
                "role": "user",
                "content": [{
                    "type": "input_image",
                    "media_type": "image/png",
                    "data": PNG
                }],
                (crate::protocol::INTERNAL_MESSAGE_FIELD): "view_image"
            })],
            events: Vec::new(),
        }
    );
}

#[tokio::test]
async fn view_image_rejects_non_images() {
    let result = execute_view_image(b"not an image").await;

    assert!(
        result.is_error
            && result.additional_input.is_empty()
            && result.output.contains("supported PNG, JPEG, WebP, or GIF")
    );
}

#[tokio::test]
async fn view_image_limits_each_tool_batch_to_one_image() {
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(PNG)
        .expect("decode PNG");
    let mut second = view_image_call();
    second.call_id = "view-2".into();

    let results = execute_view_images(&bytes, &[view_image_call(), second]).await;

    assert_eq!(results.len(), 2);
    assert!(!results[0].is_error);
    assert_eq!(results[0].additional_input.len(), 1);
    assert!(results[1].is_error);
    assert!(results[1].additional_input.is_empty());
    assert!(results[1].output.contains("only one image"));
}