#![allow(
clippy::expect_used,
clippy::panic,
clippy::unwrap_used,
clippy::uninlined_format_args
)]
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc;
use std::thread;
use std::time::{Duration, Instant};
use serde_json::{Value, json};
const MOB_CONFIG: &str = r#"
[mob]
id = "gateway-concurrent-dispatch-test"
[profiles.default]
model = "gpt-5.5"
external_addressable = true
[profiles.default.tools]
comms = true
"#;
struct Gateway {
child: Child,
stdin: ChildStdin,
lines: mpsc::Receiver<String>,
}
impl Gateway {
fn start() -> Self {
let mut child = Command::new(env!("CARGO_BIN_EXE_rpc_gateway"))
.arg("--persistent")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn rpc_gateway");
let stdin = child.stdin.take().expect("gateway stdin");
let stdout = child.stdout.take().expect("gateway stdout");
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for line in BufReader::new(stdout).lines() {
let Ok(line) = line else { break };
if tx.send(line).is_err() {
break;
}
}
});
Self {
child,
stdin,
lines: rx,
}
}
fn send(&mut self, value: Value) {
writeln!(
self.stdin,
"{}",
serde_json::to_string(&value).expect("request json")
)
.expect("write request");
self.stdin.flush().expect("flush request");
}
fn wait_for(
&mut self,
deadline: Duration,
mut on_other: impl FnMut(&mut Self, &Value),
predicate: impl Fn(&Value) -> bool,
) -> Option<Value> {
let start = Instant::now();
while start.elapsed() < deadline {
let remaining = deadline.saturating_sub(start.elapsed());
let Ok(line) = self.lines.recv_timeout(remaining) else {
return None;
};
let Ok(message) = serde_json::from_str::<Value>(line.trim()) else {
continue;
};
if predicate(&message) {
return Some(message);
}
on_other(self, &message);
}
None
}
}
impl Drop for Gateway {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
fn is_response_with_id(message: &Value, id: &str) -> bool {
message.get("method").is_none() && message.get("id").and_then(Value::as_str) == Some(id)
}
fn is_callback_request(message: &Value, method: &str) -> bool {
message.get("method").and_then(Value::as_str) == Some(method) && message.get("id").is_some()
}
#[test]
fn rpc_dispatch_serves_requests_while_a_callback_is_pending() {
let state_dir = tempfile::tempdir().expect("state dir");
let mut gateway = Gateway::start();
gateway.send(json!({
"jsonrpc": "2.0",
"id": "init",
"method": "mobkit/init",
"params": {
"persistent_state": state_dir.path(),
"mob_config": MOB_CONFIG,
"has_session_builder": true
}
}));
let init = gateway
.wait_for(
Duration::from_mins(1),
|_, _| {},
|m| is_response_with_id(m, "init"),
)
.expect("init response");
assert!(
init["result"]["contract_version"].is_string(),
"init failed: {init}"
);
gateway.send(json!({
"jsonrpc": "2.0",
"id": "spawn",
"method": "mobkit/spawn_member",
"params": { "profile": "default", "meerkat_id": "worker-1" }
}));
let build_callback = gateway
.wait_for(
Duration::from_mins(1),
|_, _| {},
|m| is_callback_request(m, "callback/build_agent"),
)
.expect("callback/build_agent request");
let callback_id = build_callback["id"].clone();
gateway.send(json!({
"jsonrpc": "2.0",
"id": "status",
"method": "mobkit/status",
"params": {}
}));
let status = gateway
.wait_for(
Duration::from_secs(15),
|_, _| {},
|m| is_response_with_id(m, "status"),
)
.expect("mobkit/status must answer while callback/build_agent is pending");
assert!(
status["result"]["contract_version"].is_string(),
"status failed: {status}"
);
gateway.send(json!({
"jsonrpc": "2.0",
"id": callback_id,
"result": {}
}));
let spawn = gateway
.wait_for(
Duration::from_mins(1),
|gateway, message| {
if message.get("method").is_some()
&& let Some(id) = message.get("id").cloned()
{
gateway.send(json!({ "jsonrpc": "2.0", "id": id, "result": {} }));
}
},
|m| is_response_with_id(m, "spawn"),
)
.expect("spawn response after callback reply");
assert!(
spawn.get("result").is_some() || spawn.get("error").is_some(),
"spawn response malformed: {spawn}"
);
}