diff --git a/crates/spt-daemon/src/bin/xlate_choreo_fixture.rs b/crates/spt-daemon/src/bin/xlate_choreo_fixture.rs
index 64411a37..dbad14a7 100644
--- a/crates/spt-daemon/src/bin/xlate_choreo_fixture.rs
+++ b/crates/spt-daemon/src/bin/xlate_choreo_fixture.rs
@@ -43,6 +43,7 @@ const PAYLOAD: &str = "XLATE_OK";
fn main() {
let mode = std::env::var("XLATE_FIXTURE_MODE").unwrap_or_else(|_| "choreo".to_string());
let stdin_log = std::env::var("XLATE_FIXTURE_STDIN_LOG").ok();
+ let generation_log = std::env::args_os().nth(1);
let stdin = io::stdin();
let mut out = io::stdout();
@@ -71,6 +72,20 @@ fn main() {
Ok(v) => v,
Err(_) => continue,
};
+ // [int->REQ-HAZARD-TRANSLATE-FAULT-PERMANENT-DEATH]
+ // Optional per-test argv path: record this generation's identity while alive.
+ if v.get("type").and_then(|t| t.as_str()) == Some("init") {
+ if let Some(path) = &generation_log {
+ let pid = std::process::id();
+ let identity = serde_json::json!([pid, spt_store::proc::process_started_at(pid)]);
+ let mut log = OpenOptions::new()
+ .create(true)
+ .append(true)
+ .open(path)
+ .expect("open generation identity log");
+ writeln!(log, "{identity}").expect("record generation identity");
+ }
+ }
if v.get("type").and_then(|t| t.as_str()) != Some("event") {
continue;
}
diff --git a/crates/spt-daemon/tests/inject_control_wedge.rs b/crates/spt-daemon/tests/inject_control_wedge.rs
index e545bd80..0913e2d5 100644
--- a/crates/spt-daemon/tests/inject_control_wedge.rs
+++ b/crates/spt-daemon/tests/inject_control_wedge.rs
@@ -2030,6 +2030,7 @@ fn c1_strike_fault_stamps_perch_and_bounded_respawns_then_gives_up() {
let _home = init_wedge_home();
let dir = tempfile::tempdir().expect("tempdir");
let stdin_log = dir.path().join("c1b-stdin.log");
+ let generation_log = dir.path().join("c1b-generations.jsonl");
// Every event misses; strike budget 1 → each miss faults; respawn budget 2.
std::env::set_var("XLATE_FIXTURE_MODE", "nocommit");
std::env::set_var("SPT_INJECT_COMMIT_DEADLINE_MS", "300");
@@ -2040,26 +2041,61 @@ fn c1_strike_fault_stamps_perch_and_bounded_respawns_then_gives_up() {
let name = unique_name();
let endpoint = "xlate-c1b-ep";
establish_endpoint_perch(endpoint);
- let (broker, sid, _controller) = spawn_xlate_session(&name, &dir.path().join("c1b"), endpoint);
+ let mut req = xlate_spawn_req(endpoint);
+ req.translation_binary
+ .as_mut()
+ .expect("translation fixture")
+ .push(generation_log.to_str().expect("generation log path").to_string());
+ let (broker, sid, mut controller) =
+ spawn_session_with(&name, &dir.path().join("c1b"), req);
set_endpoint_idle(endpoint);
- // Deliver events with > deadline spacing so each fresh binary faults BEFORE the
- // next delivery hits it (so the next delivery observes `faulted` → respawn). Loop
- // until a spool (give-up) is observed or a bounded number of attempts.
- let mut saw_spool = false;
- let mut attempts = 0u32;
- for i in 0..8 {
- attempts += 1;
- let env = format!(r#"c1b-miss-{i}"#);
- let delivered =
- deliver_endpoint_event(&name, endpoint, env.as_bytes(), Duration::from_secs(3));
- if !delivered {
- saw_spool = true; // give-up reached → spool (delivered=false)
- break;
+ // A queue acknowledgment is not a completed fault. The fixture stamps each
+ // generation while alive; its native death follows the broker's faulted store.
+ // One shared watchdog bounds all three generations without spacing guesses.
+ let fault_deadline = Instant::now() + attach_gate_watchdog();
+ for generation in 0..3 {
+ let event = format!(r#"c1b-miss-{generation}"#);
+ let (delivered, _) = controller
+ .inject_endpoint(endpoint, event.as_bytes(), false)
+ .expect("generation dispatch must return a real broker reply");
+ assert!(delivered, "generation {generation} must accept its faulting event");
+ let pin = loop {
+ let log = match std::fs::read_to_string(&generation_log) {
+ Ok(log) => log,
+ Err(err) if err.kind() == std::io::ErrorKind::NotFound => String::new(),
+ Err(err) => panic!("read generation identities: {err}"),
+ };
+ let rows: Vec<_> = log
+ .split_inclusive('\n')
+ .filter(|line| line.ends_with('\n'))
+ .collect();
+ assert!(rows.len() <= generation + 1, "unexpected extra translation generation");
+ if let Some(row) = rows.get(generation) {
+ let (pid, born): (u32, Option) =
+ serde_json::from_str(row).expect("complete generation identity");
+ #[cfg(any(windows, target_os = "linux"))]
+ assert!(born.is_some(), "fixture must stamp its live process");
+ break spt_store::proc::PinnedProc::from_stamp(pid, born);
+ }
+ assert!(Instant::now() < fault_deadline, "generation {generation} never initialized");
+ thread::sleep(Duration::from_millis(10));
+ };
+ while !pin.provably_gone() {
+ assert!(
+ Instant::now() < fault_deadline,
+ "generation {generation} pid {} did not complete its fault",
+ pin.pid()
+ );
+ thread::sleep(Duration::from_millis(10));
}
- // Let this binary's 300ms commit deadline fire (fault) before the next event.
- thread::sleep(Duration::from_millis(600));
}
+ // All initial+two respawn generations are now dead. Transport errors, malformed
+ // replies and IPC timeouts must fail, never masquerade as the give-up response.
+ let (delivered, _) = controller
+ .inject_endpoint(endpoint, br#"c1b-after-budget"#, false)
+ .expect("exhausted-budget dispatch must return a real broker reply");
+ let saw_spool = !delivered;
// The perch fault surface must be stamped (poll briefly — the last fault after
// give-up leaves it set).
@@ -2080,8 +2116,8 @@ fn c1_strike_fault_stamps_perch_and_bounded_respawns_then_gives_up() {
eprintln!(
"=== C-1 B FAULT/RESPAWN/GIVEUP GATE: stamped={stamped:?} inits_logged={inits_logged} \
- saw_spool={saw_spool} attempts={attempts} \
- (fixed = stamped Some, inits in 2..=3 (initial + bounded respawns), give-up spool; \
+ saw_spool={saw_spool} fault_generations=3 \
+ (fixed = stamped Some, exactly three generations, give-up spool; \
pre-C-1 = no stamp + never respawns + permanent death) ==="
);
@@ -2091,15 +2127,9 @@ fn c1_strike_fault_stamps_perch_and_bounded_respawns_then_gives_up() {
"a REAL fault must STAMP the harness-reachable perch translation_fault surface \
(not only the F-019 daemon-stderr channel)"
);
- assert!(
- inits_logged >= 2,
- "a faulted binary must be BOUNDED-EAGER-RESPAWNED (>= 1 extra spawn), not left \
- permanently dead (ADR-0022's original never-respawn premise) — inits_logged={inits_logged}"
- );
- assert!(
- inits_logged <= 3,
- "respawn must be BOUNDED by the give-up budget (initial + 2 respawns = 3 spawns \
- max) — inits_logged={inits_logged} exceeds the budget (respawn storm)"
+ assert_eq!(
+ inits_logged, 3,
+ "initial + two bounded respawns must fault; neither permanent death nor a respawn storm"
);
assert!(
saw_spool,