codex-mobile-bridge 0.2.11

Remote bridge and service manager for codex-mobile.
Documentation
use tokio::time::{Duration, timeout};
use serde_json::json;

use crate::app_server::AppServerInbound;
use crate::storage::PRIMARY_RUNTIME_ID;

use super::super::events::handle_app_server_message;
use super::support::{bootstrap_test_state, primary_runtime};

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

    timeout(
        Duration::from_secs(2),
        handle_app_server_message(
            &state,
            AppServerInbound::Starting {
                runtime_id: PRIMARY_RUNTIME_ID.to_string(),
            },
        ),
    )
    .await
    .expect("处理 Starting 消息超时")
    .expect("处理 Starting 消息失败");

    let runtime = primary_runtime(&state).await;
    let status = runtime.status.read().await.clone();
    assert_eq!(status.status, "starting");
    assert_eq!(status.app_server_handshake.state, "starting");
}

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

    timeout(
        Duration::from_secs(2),
        handle_app_server_message(
            &state,
            AppServerInbound::ServerRequest {
                runtime_id: PRIMARY_RUNTIME_ID.to_string(),
                id: json!("req-permissions-1"),
                method: "item/permissions/requestApproval".to_string(),
                params: json!({
                    "threadId": "thread-1",
                    "turnId": "turn-1",
                    "itemId": "item-1",
                    "reason": "需要额外权限",
                    "permissions": {
                        "network": { "enabled": true },
                        "fileSystem": {
                            "read": ["/srv/workspace/docs"],
                            "write": ["/srv/workspace/tmp"]
                        }
                    }
                }),
            },
        ),
    )
    .await
    .expect("处理 permissions request 超时")
    .expect("处理 permissions request 失败");

    let pending = state
        .storage
        .list_pending_requests()
        .expect("读取 pending requests 失败");
    assert_eq!(1, pending.len());
    assert_eq!("item/permissions/requestApproval", pending[0].request_type);
    assert_eq!(Some("thread-1"), pending[0].thread_id.as_deref());
    assert!(pending[0].permissions.is_some());
}

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

    timeout(
        Duration::from_secs(2),
        handle_app_server_message(
            &state,
            AppServerInbound::ServerRequest {
                runtime_id: PRIMARY_RUNTIME_ID.to_string(),
                id: json!("req-exec-1"),
                method: "execCommandApproval".to_string(),
                params: json!({
                    "conversationId": "thread-legacy",
                    "callId": "call-exec-1",
                    "approvalId": "approval-1",
                    "command": ["git", "status"],
                    "cwd": "/srv/workspace",
                    "reason": "需要执行命令",
                    "parsedCmd": []
                }),
            },
        ),
    )
    .await
    .expect("处理 legacy request 超时")
    .expect("处理 legacy request 失败");

    let pending = state
        .storage
        .list_pending_requests()
        .expect("读取 pending requests 失败");
    assert_eq!(1, pending.len());
    assert_eq!("execCommandApproval", pending[0].request_type);
    assert_eq!(Some("thread-legacy"), pending[0].thread_id.as_deref());
    assert_eq!(Some("git status"), pending[0].command.as_deref());
    assert_eq!(
        vec!["approved", "approved_for_session", "denied", "abort"],
        pending[0].available_decisions,
    );
}