use std::path::PathBuf;
use std::process::Stdio;
use std::time::Duration;
use serde_json::{json, Value};
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::process::{Child, ChildStdin, ChildStdout};
use tokio::time::timeout;
const RESPONSE_DEADLINE: Duration = Duration::from_secs(60);
const EXIT_DEADLINE: Duration = Duration::from_secs(15);
const POLL_INTERVAL: Duration = Duration::from_millis(50);
const DAEMON_BOOT_TIMEOUT: Duration = Duration::from_secs(30);
fn binary() -> PathBuf {
PathBuf::from(env!("CARGO_BIN_EXE_trusty-memory"))
}
struct RawChild {
child: Child,
stdin: ChildStdin,
reader: BufReader<ChildStdout>,
}
impl RawChild {
async fn close(mut self) {
drop(self.stdin);
let _ = timeout(EXIT_DEADLINE, self.child.wait()).await;
}
}
async fn spawn_raw_bridge(data_path: &std::path::Path) -> RawChild {
let mut cmd = tokio::process::Command::new(binary());
cmd.arg("serve")
.arg("--stdio")
.env("TRUSTY_DATA_DIR_OVERRIDE", data_path)
.env("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1")
.env("RUST_LOG", "warn")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit());
let mut child = cmd.spawn().expect("spawn bridge child");
let stdin = child.stdin.take().expect("stdin");
let stdout = child.stdout.take().expect("stdout");
RawChild {
child,
stdin,
reader: BufReader::new(stdout),
}
}
fn spawn_daemon(data_path: &std::path::Path) -> std::process::Child {
let child = std::process::Command::new(binary())
.arg("serve")
.arg("--foreground")
.arg("--http")
.arg("127.0.0.1:0")
.env("TRUSTY_DATA_DIR_OVERRIDE", data_path)
.env("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1")
.env("RUST_LOG", "warn")
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::inherit())
.spawn()
.expect("spawn daemon");
let readiness_file = data_path.join("trusty-memory").join("http_addr");
let deadline = std::time::Instant::now() + DAEMON_BOOT_TIMEOUT;
loop {
if readiness_file.exists() {
break;
}
assert!(
std::time::Instant::now() < deadline,
"daemon did not write http_addr within {:?}; expected at {}",
DAEMON_BOOT_TIMEOUT,
readiness_file.display()
);
std::thread::sleep(POLL_INTERVAL);
}
child
}
async fn send_raw(stdin: &mut ChildStdin, req: Value) {
let line = serde_json::to_string(&req).expect("serialise");
stdin.write_all(line.as_bytes()).await.expect("write");
stdin.write_all(b"\n").await.expect("newline");
stdin.flush().await.expect("flush");
}
async fn recv_raw(reader: &mut BufReader<ChildStdout>) -> Value {
let read_fut = async {
loop {
let mut line = String::new();
let n = reader
.read_line(&mut line)
.await
.expect("read_line I/O error");
if n == 0 {
panic!("child exited without sending a response (EOF on stdout)");
}
let trimmed = line.trim().to_string();
if !trimmed.is_empty() {
return trimmed;
}
}
};
let raw = timeout(RESPONSE_DEADLINE, read_fut)
.await
.expect("response deadline exceeded — server hung?");
serde_json::from_str::<Value>(&raw).expect("valid JSON")
}
#[tokio::test]
async fn stdio_serve_concurrent_two_bridges_both_work() {
let data_dir = tempfile::tempdir().expect("tempdir");
let mut daemon = spawn_daemon(data_dir.path());
let mut child1 = spawn_raw_bridge(data_dir.path()).await;
let mut child2 = spawn_raw_bridge(data_dir.path()).await;
send_raw(
&mut child1.stdin,
json!({
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}
}),
)
.await;
send_raw(
&mut child2.stdin,
json!({
"jsonrpc":"2.0","id":1,"method":"initialize",
"params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"t","version":"0"}}
}),
)
.await;
let init1 = recv_raw(&mut child1.reader).await;
let init2 = recv_raw(&mut child2.reader).await;
assert_eq!(init1["id"], 1, "child1 initialize id mismatch");
assert_eq!(init2["id"], 1, "child2 initialize id mismatch");
assert!(
init1["error"].is_null(),
"child1 initialize must succeed; got: {init1}"
);
assert!(
init2["error"].is_null(),
"child2 initialize must succeed; got: {init2}"
);
send_raw(
&mut child1.stdin,
json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}),
)
.await;
send_raw(
&mut child2.stdin,
json!({"jsonrpc":"2.0","id":2,"method":"tools/list"}),
)
.await;
let list1 = recv_raw(&mut child1.reader).await;
let list2 = recv_raw(&mut child2.reader).await;
assert_eq!(list1["id"], 2, "child1 tools/list id mismatch");
assert_eq!(list2["id"], 2, "child2 tools/list id mismatch");
assert!(
list1["error"].is_null(),
"child1 tools/list must succeed; got: {list1}"
);
assert!(
list2["error"].is_null(),
"child2 tools/list must succeed; got: {list2}"
);
let tools1 = list1["result"]["tools"]
.as_array()
.expect("child1 tools/list must return an array");
let tools2 = list2["result"]["tools"]
.as_array()
.expect("child2 tools/list must return an array");
assert!(
!tools1.is_empty(),
"child1 tools/list must return at least one tool"
);
assert!(
!tools2.is_empty(),
"child2 tools/list must return at least one tool"
);
send_raw(
&mut child1.stdin,
json!({"jsonrpc":"2.0","id":3,"method":"palace_list"}),
)
.await;
send_raw(
&mut child2.stdin,
json!({"jsonrpc":"2.0","id":3,"method":"palace_list"}),
)
.await;
let plist1 = recv_raw(&mut child1.reader).await;
let plist2 = recv_raw(&mut child2.reader).await;
assert_eq!(plist1["id"], 3, "child1 palace_list id mismatch");
assert_eq!(plist2["id"], 3, "child2 palace_list id mismatch");
assert!(
plist1["error"].is_null(),
"child1 palace_list must succeed; got: {plist1}"
);
assert!(
plist2["error"].is_null(),
"child2 palace_list must succeed; got: {plist2}"
);
child1.close().await;
child2.close().await;
let _ = daemon.kill();
let _ = daemon.wait();
}
#[tokio::test]
async fn stdio_bridge_exits_when_no_daemon() {
let data_dir = tempfile::tempdir().expect("tempdir");
let mut cmd = tokio::process::Command::new(binary());
cmd.arg("serve")
.arg("--stdio")
.env("TRUSTY_DATA_DIR_OVERRIDE", data_dir.path())
.env("TRUSTY_SKIP_PALACE_ENFORCEMENT", "1")
.env("RUST_LOG", "warn")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null());
let mut child = cmd.spawn().expect("spawn bridge");
drop(child.stdin.take());
let exit_result = timeout(EXIT_DEADLINE, child.wait())
.await
.expect("bridge must exit within EXIT_DEADLINE when no daemon is reachable");
let _ = exit_result; }