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()
}