From 938cbcbc088e1b0546626cc1cf229298fadd2e00 Mon Sep 17 00:00:00 2001 From: Anders Olsson Date: Fri, 7 Aug 2026 16:17:20 +0200 Subject: [PATCH] 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) Claude-Session: https://claude.ai/code/session_01EGntTHCW3sEPy1VBRopNNp --- crates/xy/tests/wait_for.rs | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 crates/xy/tests/wait_for.rs diff --git a/crates/xy/tests/wait_for.rs b/crates/xy/tests/wait_for.rs new file mode 100644 index 0000000..2fcac46 --- /dev/null +++ b/crates/xy/tests/wait_for.rs @@ -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}" + ); +}