test(wait-for): pin the gate end to end
The spec asked for an integration test that a server whose condition can never hold reaches failed without ever spawning; every gate test so far drove SupervisorTask in isolation, which is exactly why the daemon boot loop's ack await went unnoticed. This asserts both halves: `xy list` answers well inside the gate, and the gated server fails on timeout without the sleep server ever reaching running. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp
This commit is contained in:
@@ -0,0 +1,81 @@
|
|||||||
|
mod common;
|
||||||
|
use common::*;
|
||||||
|
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
/// The shared `write_server` helper emits no `wait-for` block, and six other
|
||||||
|
/// test files depend on its exact output, so this test writes its own config.
|
||||||
|
fn write_gated_server(h: &Harness, name: &str, command: &str, port: u16) {
|
||||||
|
// The keys inside a block need their own lines: KDL reads a run of values
|
||||||
|
// on one line as arguments to the first node, which silently leaves
|
||||||
|
// `timeout` and `interval` at their two-minute defaults.
|
||||||
|
let body = format!(
|
||||||
|
"command \"{command}\"\n\
|
||||||
|
port {port}\n\
|
||||||
|
restart {{\n policy \"never\"\n backoff-initial \"10ms\"\n backoff-max \"50ms\"\n max-retries-per-minute 3\n}}\n\
|
||||||
|
stop {{\n grace \"500ms\"\n}}\n\
|
||||||
|
wait-for {{\n path \"/definitely/not/here.sock\"\n timeout \"5s\"\n interval \"200ms\"\n}}\n"
|
||||||
|
);
|
||||||
|
|
||||||
|
std::fs::write(h.config_dir.join(format!("{name}.kdl")), body).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn a_gated_server_keeps_the_daemon_responsive_then_fails_without_spawning() {
|
||||||
|
let xy = xy_bin();
|
||||||
|
let sleeper = sleep_server_bin();
|
||||||
|
|
||||||
|
let mut h = Harness::new();
|
||||||
|
|
||||||
|
write_gated_server(&h, "gated", sleeper.to_str().unwrap(), 19_201);
|
||||||
|
|
||||||
|
h.start_daemon(&xy).await;
|
||||||
|
|
||||||
|
// The socket is bound before the autostart loop, so `start_daemon`
|
||||||
|
// returning proves nothing; only a completed round trip does.
|
||||||
|
let waiting = tokio::time::timeout(Duration::from_millis(2_000), async {
|
||||||
|
loop {
|
||||||
|
let (code, out, _err) = h.run_cli(&xy, &["list"]).await;
|
||||||
|
|
||||||
|
assert_eq!(code, 0, "stdout: {out}");
|
||||||
|
assert!(
|
||||||
|
!out.contains("running"),
|
||||||
|
"a gated server must not spawn; stdout: {out}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if out.contains("waiting") {
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(50)).await;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.expect("`xy list` must answer well inside the 5s gate");
|
||||||
|
|
||||||
|
assert!(waiting.contains("gated"), "stdout: {waiting}");
|
||||||
|
|
||||||
|
let mut last = String::new();
|
||||||
|
|
||||||
|
for _ in 0..80 {
|
||||||
|
let (_code, out, _err) = h.run_cli(&xy, &["list"]).await;
|
||||||
|
|
||||||
|
last = out;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!last.contains("running"),
|
||||||
|
"a gated server must reach failed without ever spawning; stdout: {last}"
|
||||||
|
);
|
||||||
|
|
||||||
|
if last.contains("failed") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::time::sleep(Duration::from_millis(100)).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
last.contains("failed"),
|
||||||
|
"the gate must give up after its timeout; stdout: {last}"
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user