codex-mobile-bridge 0.3.6

Remote bridge and service manager for codex-mobile.
Documentation
use std::fs;

use serde_json::json;

use super::super::threads::build_turn_input_items;
use super::support::bootstrap_test_state;
use crate::bridge_protocol::{SendTurnInputItem, SendTurnRequest};

#[test]
fn send_turn_request_compatibility_supports_legacy_text_and_structured_items() {
    let legacy = serde_json::from_value::<SendTurnRequest>(json!({
        "threadId": "thread-1",
        "text": "继续处理"
    }))
    .expect("legacy send_turn 请求应可解析");
    assert_eq!(legacy.thread_id, "thread-1");
    assert_eq!(legacy.text, "继续处理");
    assert!(legacy.input_items.is_none());

    let structured = serde_json::from_value::<SendTurnRequest>(json!({
        "threadId": "thread-1",
        "text": "",
        "inputItems": [
            {
                "type": "text",
                "text": "看这张图"
            },
            {
                "type": "localImage",
                "path": "/tmp/codex-mobile-test.png"
            }
        ]
    }))
    .expect("structured send_turn 请求应可解析");
    assert_eq!(structured.thread_id, "thread-1");
    assert_eq!(structured.input_items.as_ref().map(Vec::len), Some(2));
}

#[tokio::test]
async fn stage_input_image_writes_file_under_staging_root() {
    let state = bootstrap_test_state().await;
    let response = state
        .handle_request(
            "stage_input_image",
            json!({
                "fileName": "diagram.png",
                "mimeType": "image/png",
                "base64Data": "aGVsbG8="
            }),
        )
        .await
        .expect("stage_input_image 请求失败");

    let local_path = response["image"]["localPath"]
        .as_str()
        .expect("应返回 localPath");
    assert!(local_path.starts_with(&state.staging_root().to_string_lossy().to_string()));
    let content = fs::read(local_path).expect("应写入 staging 文件");
    assert_eq!(content, b"hello");
    state
        .cleanup_staged_paths([local_path.into()])
        .expect("清理 staging 文件失败");
}

#[test]
fn build_turn_input_items_accepts_text_and_local_image() {
    let staging_dir =
        std::env::temp_dir().join(format!("codex-mobile-turn-inputs-{}", std::process::id()));
    fs::create_dir_all(&staging_dir).expect("创建 staging 目录失败");
    let image_path = staging_dir.join("image.png");
    fs::write(&image_path, b"png").expect("写入测试图片失败");

    let request = SendTurnRequest {
        thread_id: "thread-1".to_string(),
        text: String::new(),
        input_items: Some(vec![
            SendTurnInputItem::Text {
                text: "请分析这张图".to_string(),
            },
            SendTurnInputItem::LocalImage {
                path: image_path.to_string_lossy().to_string(),
            },
        ]),
    };

    let (input_items, staged_paths) =
        build_turn_input_items(&request, &staging_dir).expect("应接受图文输入");

    assert_eq!(input_items.len(), 2);
    assert_eq!(input_items[0]["type"], "text");
    assert_eq!(input_items[1]["type"], "localImage");
    assert_eq!(staged_paths, vec![image_path.clone()]);

    fs::remove_file(image_path).expect("删除测试图片失败");
}

#[test]
fn build_turn_input_items_rejects_non_staged_image_path() {
    let staging_dir =
        std::env::temp_dir().join(format!("codex-mobile-turn-inputs-{}", std::process::id()));
    fs::create_dir_all(&staging_dir).expect("创建 staging 目录失败");
    let external_path = std::env::temp_dir().join(format!(
        "codex-mobile-turn-inputs-external-{}.png",
        std::process::id()
    ));
    fs::write(&external_path, b"png").expect("写入测试图片失败");

    let request = SendTurnRequest {
        thread_id: "thread-1".to_string(),
        text: String::new(),
        input_items: Some(vec![SendTurnInputItem::LocalImage {
            path: external_path.to_string_lossy().to_string(),
        }]),
    };

    let error = build_turn_input_items(&request, &staging_dir).expect_err("应拒绝 staging 外路径");
    assert!(error.to_string().contains("staging"));

    fs::remove_file(external_path).expect("删除测试图片失败");
}