use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
struct Client {
child: Child,
stdin: ChildStdin,
stdout: BufReader<ChildStdout>,
}
impl Client {
fn spawn(child_ledger: &str, fence_ledger: &str) -> Self {
let exe = env!("CARGO_BIN_EXE_effectfence");
let fixture = concat!(env!("CARGO_MANIFEST_DIR"), "/tests/fixtures/slow_child.py");
let mut child = Command::new(exe)
.args(["wrap", "--", "python3", fixture])
.env("EFFECTFENCE_LEDGER", fence_ledger)
.env("SLOW_CHILD_LEDGER", child_ledger)
.env("SLOW_CHILD_DELAY", "0")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.expect("spawn wrap");
let stdin = child.stdin.take().unwrap();
let stdout = BufReader::new(child.stdout.take().unwrap());
let mut c = Self {
child,
stdin,
stdout,
};
c.send(
&serde_json::json!({"jsonrpc":"2.0","id":1,"method":"initialize","params":{
"protocolVersion":"2026-07-28","capabilities":{},
"clientInfo":{"name":"restart-test","version":"0"}}}),
);
c.read_id(1);
c.send(&serde_json::json!({"jsonrpc":"2.0","method":"notifications/initialized"}));
c
}
fn send(&mut self, v: &serde_json::Value) {
writeln!(self.stdin, "{v}").unwrap();
self.stdin.flush().unwrap();
}
fn call(&mut self, id: i64, order: &str) -> serde_json::Value {
self.send(
&serde_json::json!({"jsonrpc":"2.0","id":id,"method":"tools/call",
"params":{"name":"slow_charge","arguments":{"order":order}}}),
);
self.read_id(id)
}
fn read_id(&mut self, id: i64) -> serde_json::Value {
loop {
let mut line = String::new();
let n = self.stdout.read_line(&mut line).expect("read");
assert!(n > 0, "wrap closed stdout while awaiting id {id}");
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&line)
&& v.get("id").and_then(|x| x.as_i64()) == Some(id)
{
return v;
}
}
}
}
impl Drop for Client {
fn drop(&mut self) {
let _ = self.child.kill();
let _ = self.child.wait();
}
}
#[test]
fn an_identical_call_after_a_proxy_restart_is_replayed_not_rerun() {
let pid = std::process::id();
let child_ledger = std::env::temp_dir().join(format!("ef-restart-child-{pid}.ndjson"));
let fence_ledger = std::env::temp_dir().join(format!("ef-restart-fence-{pid}.jsonl"));
let _ = std::fs::remove_file(&child_ledger);
let _ = std::fs::remove_file(&fence_ledger);
let child_s = child_ledger.to_string_lossy().to_string();
let fence_s = fence_ledger.to_string_lossy().to_string();
let first = {
let mut c = Client::spawn(&child_s, &fence_s);
c.call(10, "order-restart")
};
let second = {
let mut c = Client::spawn(&child_s, &fence_s);
c.call(20, "order-restart")
};
let runs = std::fs::read_to_string(&child_ledger).unwrap_or_default();
let executions = runs.lines().filter(|l| !l.trim().is_empty()).count();
let _ = std::fs::remove_file(&child_ledger);
let _ = std::fs::remove_file(&fence_ledger);
assert_eq!(
executions, 1,
"the child must charge order-restart exactly once across a proxy restart; \
ledger:\n{runs}\nfirst={first}\nsecond={second}"
);
assert_eq!(
second["result"]["content"][0]["text"], first["result"]["content"][0]["text"],
"the second proxy must hand back the first proxy's recorded result"
);
}