codex-mobile-bridge 0.2.6

Remote bridge and service manager for codex-mobile.
Documentation
use std::env;
use std::fs;
use std::path::PathBuf;
use std::sync::Arc;

use axum::extract::State;
use serde_json::{Value, json};
use tokio::time::{Duration, timeout};
use uuid::Uuid;

use super::health::{build_health_payload, health_handler};
use super::websocket::parse_client_envelope;
use crate::bridge_protocol::{
    AppServerHandshakeSummary, ClientEnvelope, RuntimeRecord, RuntimeStatusSnapshot,
    RuntimeSummary,
};
use crate::config::Config;
use crate::state::BridgeState;

#[test]
fn build_health_payload_contains_bridge_metadata_and_primary_runtime() {
    let runtime = RuntimeStatusSnapshot {
        runtime_id: "primary".to_string(),
        status: "running".to_string(),
        codex_home: Some("/srv/codex-home".to_string()),
        user_agent: Some("codex-mobile".to_string()),
        platform_family: Some("linux".to_string()),
        platform_os: Some("ubuntu".to_string()),
        last_error: None,
        pid: Some(4242),
        app_server_handshake: AppServerHandshakeSummary::new(
            "ready",
            true,
            vec!["fs/changed".to_string()],
            Some("握手完成,initialized 已发送".to_string()),
        ),
        updated_at_ms: 1234,
    };
    let runtime_record = RuntimeRecord {
        runtime_id: "primary".to_string(),
        display_name: "Primary".to_string(),
        codex_home: Some("/srv/codex-home".to_string()),
        codex_binary: "codex".to_string(),
        is_primary: true,
        auto_start: true,
        created_at_ms: 1000,
        updated_at_ms: 1000,
    };
    let runtimes = vec![RuntimeSummary::from_parts(&runtime_record, runtime.clone())];

    let payload = build_health_payload(&runtime, &runtimes);

    assert_eq!(payload["ok"], Value::Bool(true));
    assert_eq!(
        payload["bridgeVersion"],
        Value::String(crate::BRIDGE_VERSION.to_string())
    );
    assert_eq!(
        payload["buildHash"],
        Value::String(crate::BRIDGE_BUILD_HASH.to_string())
    );
    assert_eq!(
        payload["protocolVersion"],
        Value::Number(crate::BRIDGE_PROTOCOL_VERSION.into())
    );
    assert_eq!(payload["runtimeCount"], Value::Number(1.into()));
    assert_eq!(
        payload["primaryRuntimeId"],
        Value::String("primary".to_string())
    );
    assert_eq!(
        payload["runtime"]["runtimeId"],
        Value::String("primary".to_string())
    );
    assert_eq!(
        payload["runtime"]["status"],
        Value::String("running".to_string())
    );
}

#[test]
fn parse_client_envelope_accepts_plain_hello_payload() {
    let envelope =
        parse_client_envelope(r#"{"kind":"hello","device_id":"device-alpha","last_ack_seq":7}"#)
            .expect("hello payload 应可解析");

    match envelope {
        ClientEnvelope::Hello {
            device_id,
            last_ack_seq,
        } => {
            assert_eq!(device_id, "device-alpha");
            assert_eq!(last_ack_seq, Some(7));
        }
        _ => panic!("应解析为 hello"),
    }
}

#[test]
fn parse_client_envelope_accepts_double_encoded_hello_payload() {
    let envelope =
        parse_client_envelope(r#""{\"kind\":\"hello\",\"device_id\":\"device-beta\",\"last_ack_seq\":9}""#)
            .expect("双重编码 hello payload 应可解析");

    match envelope {
        ClientEnvelope::Hello {
            device_id,
            last_ack_seq,
        } => {
            assert_eq!(device_id, "device-beta");
            assert_eq!(last_ack_seq, Some(9));
        }
        _ => panic!("应解析为 hello"),
    }
}

#[tokio::test]
async fn runtime_snapshot_returns_without_hanging() {
    let state = bootstrap_test_state().await;

    let snapshot = timeout(Duration::from_secs(2), state.runtime_snapshot())
        .await
        .expect("runtime_snapshot 超时");

    assert_eq!(snapshot.runtime_id, "primary");
}

#[tokio::test]
async fn runtime_summaries_return_without_hanging() {
    let state = bootstrap_test_state().await;

    let summaries = timeout(Duration::from_secs(2), state.runtime_summaries())
        .await
        .expect("runtime_summaries 超时");

    assert!(!summaries.is_empty());
    assert_eq!(summaries[0].runtime_id, "primary");
}

#[tokio::test]
async fn health_handler_returns_without_hanging() {
    let state = bootstrap_test_state().await;

    let _ = timeout(
        Duration::from_secs(2),
        health_handler(State(Arc::clone(&state))),
    )
    .await
    .expect("/health handler 超时");
}

#[tokio::test]
async fn hello_payload_returns_without_hanging() {
    let state = bootstrap_test_state().await;

    let (runtime, runtimes, ..) = timeout(
        Duration::from_secs(2),
        state.hello_payload("device-test", None),
    )
    .await
    .expect("hello_payload 超时")
    .expect("hello_payload 返回错误");

    assert_eq!(runtime.runtime_id, "primary");
    assert!(!runtimes.is_empty());
    assert_eq!(runtimes[0].runtime_id, "primary");
}

#[tokio::test]
async fn list_runtimes_request_returns_without_hanging() {
    let state = bootstrap_test_state().await;

    let response = timeout(
        Duration::from_secs(2),
        state.handle_request("list_runtimes", json!({})),
    )
    .await
    .expect("list_runtimes 超时")
    .expect("list_runtimes 返回错误");

    let runtimes = response["runtimes"].as_array().expect("runtimes 应为数组");
    assert!(!runtimes.is_empty());
    assert_eq!(
        runtimes[0]["runtimeId"],
        Value::String("primary".to_string())
    );
}

#[tokio::test]
async fn get_runtime_status_request_returns_without_hanging() {
    let state = bootstrap_test_state().await;

    let response = timeout(
        Duration::from_secs(2),
        state.handle_request("get_runtime_status", json!({ "runtimeId": "primary" })),
    )
    .await
    .expect("get_runtime_status 超时")
    .expect("get_runtime_status 返回错误");

    assert_eq!(
        response["runtime"]["runtimeId"],
        Value::String("primary".to_string())
    );
}

async fn bootstrap_test_state() -> Arc<BridgeState> {
    let base_dir = env::temp_dir().join(format!("codex-mobile-bridge-test-{}", Uuid::new_v4()));
    fs::create_dir_all(&base_dir).expect("创建测试目录失败");
    let db_path = base_dir.join("bridge.db");

    let config = Config {
        listen_addr: "127.0.0.1:0".to_string(),
        token: "test-token".to_string(),
        runtime_limit: 4,
        db_path,
        codex_home: None,
        codex_binary: resolve_true_binary(),
        directory_bookmarks: Vec::new(),
    };

    BridgeState::bootstrap(config)
        .await
        .expect("bootstrap 测试 BridgeState 失败")
}

fn resolve_true_binary() -> String {
    for candidate in ["/usr/bin/true", "/bin/true"] {
        if PathBuf::from(candidate).exists() {
            return candidate.to_string();
        }
    }
    "true".to_string()
}