mobius 0.15.16

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

use super::*;
use crate::backend::session_files::SessionFileStore;
use crate::protocol::ContentPart;

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

#[tokio::test]
async fn ordered_images_are_durable_and_reinspectable_after_source_deletion() {
    let workspace = tempfile::tempdir().expect("workspace");
    let state = tempfile::tempdir().expect("state");
    let store = SessionFileStore::new(state.path());
    let bytes = base64::engine::general_purpose::STANDARD
        .decode(PNG)
        .expect("PNG");
    let path = workspace.path().join("page.png");
    std::fs::write(&path, &bytes).expect("image");
    let sandbox = Arc::new(Sandbox::new(
        Arc::new(
            crate::backend::sandbox::local::LocalSandbox::new(workspace.path()).expect("sandbox"),
        ),
        crate::backend::sandbox::ApprovalPolicy::Ask,
    ));
    let mut catalog = Catalog::default();
    catalog
        .register(Arc::new(ViewImage {
            store: store.clone(),
        }))
        .expect("register");
    let calls = finalize_and_bind(
        &mut catalog,
        &[ToolCall {
            call_id: "view".into(),
            name: "view_image".into(),
            arguments: serde_json::json!({"images": [{"path": "page.png", "detail": "high"}, {"path": "page.png", "detail": "low"}]}),
        }],
    );
    let result = execute_batch(
        &catalog,
        &calls,
        sandbox.clone(),
        &test_permissions(&[]),
        "turn",
    )
    .await
    .remove(0);
    assert!(!result.is_error, "{}", result.output.text());
    assert!(result.additional_input.is_empty());
    let [
        ContentPart::Text { .. },
        ContentPart::Image { image: first },
        ContentPart::Text { .. },
        ContentPart::Image { image: second },
    ] = result.output.0.as_slice()
    else {
        panic!("ordered native images");
    };
    assert_eq!(first.detail, crate::protocol::ImageDetail::High);
    assert_eq!(second.detail, crate::protocol::ImageDetail::Low);
    std::fs::remove_file(path).expect("remove source");
    assert_eq!(
        store
            .read_file("session", &first.file)
            .await
            .expect("durable image"),
        bytes
    );
    assert!(
        store
            .read_file("another-session", &first.file)
            .await
            .is_err()
    );
    let call = catalog
        .bind_call(
            ToolCall {
                call_id: "again".into(),
                name: "view_image".into(),
                arguments: serde_json::json!({"images": [{"file_id": first.file.id}]}),
            },
            &BTreeSet::new(),
            &BTreeSet::new(),
        )
        .expect("bind");
    let result = execute_batch(
        &catalog,
        std::slice::from_ref(&call),
        sandbox.clone(),
        &test_permissions(&[]),
        "turn",
    )
    .await
    .remove(0);
    assert!(!result.is_error, "{}", result.output.text());
    assert_eq!(result.output.files().count(), 1);
    let other_session = SandboxPermissions::restore(
        "another-session",
        crate::backend::sandbox::SandboxMode::WorkspaceWrite,
        crate::backend::sandbox::NetworkAccess::Denied,
        Vec::new(),
    );
    let result = execute_batch(&catalog, &[call], sandbox, &other_session, "turn")
        .await
        .remove(0);
    assert!(
        result.is_error,
        "a shared handler must use the calling session's authority"
    );
    assert!(
        store
            .list_files("session")
            .await
            .expect("published files")
            .is_empty()
    );
}

#[tokio::test]
async fn view_image_rejects_malformed_bytes_and_ambiguous_sources() {
    let state = tempfile::tempdir().expect("state");
    let workspace = tempfile::tempdir().expect("workspace");
    std::fs::write(workspace.path().join("bad.png"), b"not an image").expect("file");
    let tool = ViewImage {
        store: SessionFileStore::new(state.path()),
    };
    for arguments in [
        serde_json::json!({"images": [{"path": "bad.png"}]}),
        serde_json::json!({"images": [{"path": "bad.png", "file_id": "other"}]}),
        serde_json::json!({"images": []}),
    ] {
        let sandbox = Arc::new(Sandbox::new(
            Arc::new(
                crate::backend::sandbox::local::LocalSandbox::new(workspace.path())
                    .expect("sandbox"),
            ),
            crate::backend::sandbox::ApprovalPolicy::Ask,
        ));
        assert!(
            tool.call(
                ToolContext::new(sandbox, test_permissions(&[]).for_call("view"), "turn"),
                arguments
            )
            .await
            .is_err()
        );
    }
}