#![allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::uninlined_format_args
)]
use std::io::{BufRead, BufReader, Write};
use std::process::{Command, Stdio};
use std::sync::mpsc;
use std::time::Duration;
use serde_json::{Value, json};
use tempfile::TempDir;
fn assert_bootstraps(persistent_sessions: bool) {
let bin = env!("CARGO_BIN_EXE_mobkit_gateway");
let workspace = TempDir::new().expect("workspace tempdir");
let store = TempDir::new().expect("store tempdir");
let mut params = serde_json::Map::new();
params.insert(
"workspace_root".into(),
json!(workspace.path().to_string_lossy()),
);
params.insert(
"store_path".into(),
json!(store.path().join("store").to_string_lossy()),
);
if persistent_sessions {
params.insert("persistent_sessions".into(), json!(true));
}
let init = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "mobkit/init",
"params": Value::Object(params),
});
let mut child = Command::new(bin)
.current_dir(workspace.path())
.env("ANTHROPIC_API_KEY", "sk-ant-regression-test")
.env("OPENAI_API_KEY", "sk-regression-test")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn mobkit_gateway");
let mut stdin = child.stdin.take().expect("stdin");
writeln!(stdin, "{}", serde_json::to_string(&init).unwrap()).expect("write init");
stdin.flush().expect("flush");
let stdout = child.stdout.take().expect("stdout");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
let _ = reader.read_line(&mut line);
let _ = tx.send(line);
});
let line = rx
.recv_timeout(Duration::from_secs(45))
.expect("mobkit_gateway did not answer mobkit/init within 45s (bootstrap hung?)");
drop(stdin);
let _ = child.kill();
let _ = child.wait();
let resp: Value = serde_json::from_str(line.trim())
.unwrap_or_else(|e| panic!("non-JSON init response {:?}: {}", line, e));
let err_msg = resp
.get("error")
.and_then(|e| e.get("message"))
.and_then(|m| m.as_str())
.unwrap_or("");
assert!(
resp.get("result").is_some() || !err_msg.contains("failed to bootstrap local runtime"),
"mobkit_gateway failed to bootstrap a local runtime (persistent_sessions={}): {}",
persistent_sessions,
resp
);
}
#[test]
fn mobkit_gateway_bootstraps_ephemeral_runtime() {
assert_bootstraps(false);
}
#[test]
fn mobkit_gateway_bootstraps_persistent_runtime() {
assert_bootstraps(true);
}
#[test]
fn gateways_report_name_and_version() {
for (bin, needle) in [
(env!("CARGO_BIN_EXE_mobkit_gateway"), "mobkit_gateway"),
(env!("CARGO_BIN_EXE_rpc_gateway"), "rpc_gateway"),
] {
let out = Command::new(bin)
.arg("--version")
.output()
.expect("run --version");
assert!(out.status.success(), "{needle} --version exited non-zero");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(needle) && stdout.contains(env!("CARGO_PKG_VERSION")),
"{needle} --version did not print name + version {}: {stdout:?}",
env!("CARGO_PKG_VERSION")
);
}
}
#[test]
fn mobkit_gateway_rejects_post_init_stdin_rpc_loudly() {
let bin = env!("CARGO_BIN_EXE_mobkit_gateway");
let workspace = TempDir::new().expect("workspace tempdir");
let store = TempDir::new().expect("store tempdir");
let init = json!({
"jsonrpc": "2.0", "id": 1, "method": "mobkit/init",
"params": {
"workspace_root": workspace.path().to_string_lossy(),
"store_path": store.path().join("store").to_string_lossy(),
}
});
let reconcile =
json!({ "jsonrpc": "2.0", "id": 2, "method": "mobkit/reconcile_identity", "params": {} });
let mut child = Command::new(bin)
.current_dir(workspace.path())
.env("ANTHROPIC_API_KEY", "sk-ant-regression-test")
.env("OPENAI_API_KEY", "sk-regression-test")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn mobkit_gateway");
let mut stdin = child.stdin.take().expect("stdin");
let stdout = child.stdout.take().expect("stdout");
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let reader = BufReader::new(stdout);
for line in reader.lines() {
match line {
Ok(l) => {
if tx.send(l).is_err() {
break;
}
}
Err(_) => break,
}
}
});
writeln!(stdin, "{}", serde_json::to_string(&init).unwrap()).expect("write init");
stdin.flush().expect("flush init");
let init_resp = rx
.recv_timeout(Duration::from_secs(45))
.expect("no init response within 45s");
let init_v: Value = serde_json::from_str(init_resp.trim())
.unwrap_or_else(|e| panic!("non-JSON init response {init_resp:?}: {e}"));
assert!(
init_v.get("result").is_some(),
"init did not return a result (cannot exercise the post-init guard): {init_resp}"
);
writeln!(stdin, "{}", serde_json::to_string(&reconcile).unwrap()).expect("write reconcile");
stdin.flush().expect("flush reconcile");
let reconcile_resp = rx.recv_timeout(Duration::from_secs(20)).expect(
"mobkit_gateway did not answer a post-init stdin RPC within 20s — the silent-hang regressed",
);
drop(stdin);
let _ = child.kill();
let _ = child.wait();
let v: Value = serde_json::from_str(reconcile_resp.trim())
.unwrap_or_else(|e| panic!("non-JSON reconcile response {reconcile_resp:?}: {e}"));
let msg = v
.get("error")
.and_then(|e| e.get("message"))
.and_then(|m| m.as_str())
.unwrap_or("");
assert!(
msg.contains("rpc_gateway"),
"post-init stdin RPC must fail loudly and point at rpc_gateway, got: {reconcile_resp}"
);
}