<system-reminder>[Truncated: PARTIAL view — showing lines 1-1093 of 2149 total (41777 tokens, cap 25000). Call Read with offset=1094 limit=1093 for the next page, or Grep to find a specific section. Do NOT answer from this page alone if the answer may be further in the file.]</system-reminder>

1	//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY
2	//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local
3	//! IPC (no mocks of the PTY or the transport).
4	//!
5	//! ## The bug this gate locks down (park (a), the output-stall / wedge root)
6	//!
7	//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the
8	//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the
9	//! session's single drain thread, while the `Mutex<OutputLog>` was locked.
10	//! VIEWERS instead get a dedicated writer thread + a bounded, evicting
11	//! `sync_channel` and can never stall the drain. So a slow / non-draining
12	//! controller (here a controller connection that stops reading while the child
13	//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output
14	//! stalled and any path needing that log lock (a fresh attach, or the global
15	//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)
16	//! blocked FOREVER, while the broker process stayed alive. The repro that proved
17	//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching
18	//! `APPEND_CTRL_EXIT`, `broker_alive=true` but `sessions_answered=false`.
19	//!
20	//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated
21	//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:
22	//! the writer advances `delivered_through`). The drain hands each chunk off with a
23	//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached
24	//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed
25	//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test
26	//! asserts that fixed behavior.
27	//!
28	//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded (W1b F1)
29	//!
30	//! These tests share ONE process-global `SPT_HOME` (set once via
31	//! [`std::env::set_var`] in [`init_wedge_home`]) and each spins a REAL broker +
32	//! PTY child. Under the default `cargo test` harness — N tests as parallel
33	//! THREADS in ONE process — the `set_var` mutation races and the concurrent
34	//! brokers contend, which HANGS on Windows (cost a 30-min wedge during W1b).
35	//!
36	//! Run them isolated:
37	//!   - `cargo nextest run --test inject_control_wedge`  (process-per-test — CI's path, always safe), or
38	//!   - `cargo test --test inject_control_wedge -- --test-threads=1`  (single-threaded fallback).
39	//!
40	//! Do NOT run a bare `cargo test --test inject_control_wedge` on Windows.
41
42	use std::sync::Arc;
43	use std::thread;
44	use std::time::{Duration, Instant};
45
46	use interprocess::local_socket::Stream;
47	use spt_daemon::codec::{read_frame, write_frame};
48	use spt_daemon::frame::{Envelope, Role};
49	use spt_daemon::msg::{
50	    encode_bytes, EndpointInputReq, SpawnReq, Spawned, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT,
51	    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
52	    KIND_SPAWNED,
53	};
54	use spt_daemon::transport::{send_hello, LocalSocketTransport};
55	use spt_daemon::Broker;
56
57	// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach
58	//    path, the Brain client API, and a net-backed broker for loopback attach. ──
59	use std::sync::atomic::{AtomicBool, AtomicU64};
60	use std::sync::OnceLock;
61	use spt_daemon::attach::{request_attach, serve_attach};
62	use spt_daemon::brain::{Brain, BrokerEvent};
63	use spt_daemon::msg::decode_bytes;
64	use spt_daemon::nethost::{NetConfig, NetHost};
65	use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
66	use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
67	use spt_proto::identity::Identity;
68
69	use std::sync::atomic::{AtomicU32, Ordering};
70	static SEQ: AtomicU32 = AtomicU32::new(0);
71	fn unique_name() -> String {
72	    let n = SEQ.fetch_add(1, Ordering::Relaxed);
73	    format!("spt-daemon-w1wedge-{}-{}.sock", std::process::id(), n)
74	}
75
76	/// The concurrent-attach gate watchdog (shared by `a_journaled` + `p0_paste`): the
77	/// hard wall-clock ceiling on the attacher's `result_rx.recv_timeout`. DEFAULT 30s —
78	/// tight enough that a REAL paste-wedge (which blocks FOREVER) still trips it, so the
79	/// gate keeps catching regressions locally / by default. CI exports
80	/// `SPT_ATTACH_GATE_WATCHDOG_MS=120000` ONLY on the shared loaded box, where cross-run
81	/// scheduler starvation can push the gate's sole elastic bound (`request_attach`'s
82	/// journaled `net_open_stream` ack) past 30s with NO product wedge — serviced-but-late
83	/// (ack <=3.8s even under pegged CPU; the 30–120s band is pure starvation noise).
84	/// Mirrors the `SPT_REGISTRY_BUSY_TIMEOUT_MS` env-knob precedent. The knob grants ack
85	/// wall-clock ONLY; it never relaxes the `subscribed` / `got_output` assertions.
86	fn attach_gate_watchdog() -> Duration {
87	    match std::env::var("SPT_ATTACH_GATE_WATCHDOG_MS") {
88	        Ok(ms) => ms
89	            .parse::<u64>()
90	            .map(Duration::from_millis)
91	            .unwrap_or(Duration::from_secs(30)),
92	        Err(_) => Duration::from_secs(30),
93	    }
94	}
95
96	fn kill_pid(pid: u32) {
97	    #[cfg(windows)]
98	    let _ = std::process::Command::new("taskkill")
99	        .args(["/PID", &pid.to_string(), "/F", "/T"])
100	        .output();
101	    #[cfg(unix)]
102	    let _ = std::process::Command::new("kill")
103	        .args(["-9", &pid.to_string()])
104	        .output();
105	}
106
107	/// Join a teardown helper thread, but NEVER ride a permanently-parked helper to
108	/// the nextest slow-timeout backstop (240s). On the FAIL/wedge path a helper can
109	/// be parked forever inside a blocking socket op — e.g. the driver's `send_effect`
110	/// on a wedged broker never returns to observe `stop` — so a bare `.join()` would
111	/// hang the whole test until the 240s SIGKILL, MASKING which failure it was. The
112	/// gate result (`subscribed`/`got_output`) is already captured by the 30s watchdog
113	/// BEFORE teardown, so bounding the join lets a real product wedge fail FAST on the
114	/// assertion (~watchdog time) instead of timing out. A timed-out helper is
115	/// abandoned — it dies at process exit, exactly like the KH-7.6 abandoned
116	/// pump-ipc-reader threads this file already relies on. Returns true iff it joined.
117	fn join_bounded(handle: thread::JoinHandle<()>, dur: Duration) -> bool {
118	    let (tx, rx) = std::sync::mpsc::channel();
119	    thread::spawn(move || {
120	        let _ = handle.join();
121	        let _ = tx.send(());
122	    });
123	    rx.recv_timeout(dur).is_ok()
124	}
125
126	/// Read `Broker::session_pid` under a bound. The pid is only used to best-effort
127	/// kill the flood child during teardown, but `session_pid` locks the broker's
128	/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read
129	/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread
130	/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;
131	/// a missed child-kill is harmless, the process-per-test exit reaps it.
132	fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {
133	    let broker = Arc::clone(broker);
134	    let (tx, rx) = std::sync::mpsc::channel();
135	    thread::spawn(move || {
136	        let _ = tx.send(broker.session_pid(sid));
137	    });
138	    rx.recv_timeout(dur).ok().flatten()
139	}
140
141	/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output
142	/// case (a real TUI redraw burst). Cross-OS, same shape as the broker B1 test's
143	/// `cat`/`findstr` echo child.
144	fn flood_spawn_req(endpoint: &str) -> SpawnReq {
145	    #[cfg(unix)]
146	    let (program, args) = ("yes".to_string(), vec!["FLOODFLOODFLOODFLOOD".to_string()]);
147	    #[cfg(windows)]
148	    let (program, args) = (
149	        "cmd".to_string(),
150	        vec![
151	            "/c".to_string(),
152	            "for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD".to_string(),
153	        ],
154	    );
155	    SpawnReq {
156	        program,
157	        args,
158	        rows: 24,
159	        cols: 80,
160	        endpoint: endpoint.to_string(),
161	        cwd: None,
162	        env: Default::default(),
163	        translation_binary: None,
164	    }
165	}
166
167	/// A QUIET, long-lived child for the w5_a2 IDLE characterization (doyle gate
168	/// ruling, Option B): it produces NO output and does NOT exit, so the session is
169	/// GENUINELY idle for the read window. (`flood_spawn_req` floods through the read,
170	/// which makes b4's full-speed drain hit the dead-operator write-fail →
171	/// Disconnected-evict → controller_by clears — the rig bug that made w5_a2 a false
172	/// red. A silent child never feeds `controller_writer`, so the abandoned-controller
173	/// slot is never cleared via the output path, faithfully staging the IDLE case the
174	/// test characterizes. The TRUE idle-evict is deferred to
175	/// REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT — the D4c presence oracle.) Unix `sleep`
176	/// is silent + long-lived; Windows `ping -n` with output redirected to NUL via
177	/// `cmd /c` is the silent long-lived analog (`timeout`/bare `ping` print a
178	/// countdown/replies that WOULD feed the drain).
179	fn quiet_spawn_req(endpoint: &str) -> SpawnReq {
180	    #[cfg(unix)]
181	    let (program, args) = ("sleep".to_string(), vec!["600".to_string()]);
182	    #[cfg(windows)]
183	    let (program, args) = (
184	        "cmd".to_string(),
185	        vec!["/c".to_string(), "ping -n 600 127.0.0.1 >nul".to_string()],
186	    );
187	    SpawnReq {
188	        program,
189	        args,
190	        rows: 24,
191	        cols: 80,
192	        endpoint: endpoint.to_string(),
193	        cwd: None,
194	        env: Default::default(),
195	        translation_binary: None,
196	    }
197	}
198
199	fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {
200	    write_frame(conn, &Envelope::new(kind, payload)).expect("send frame");
201	}
202
203	/// Open a fresh brain connection, send one command, and return whether the
204	/// expected reply kind arrives within `deadline`. Runs the whole exchange on a
205	/// spawned thread so a BLOCKED `read_frame` (the wedge) surfaces as a timeout
206	/// instead of hanging the test. A `false` return = "did not answer in time".
207	fn probe(name: &str, cmd_kind: &'static str, want_kind: &'static str, deadline: Duration) -> bool {
208	    let name = name.to_string();
209	    let (tx, rx) = std::sync::mpsc::channel::<bool>();
210	    thread::spawn(move || {
211	        let mut conn = match LocalSocketTransport::connect(&name) {
212	            Ok(c) => c,
213	            Err(_) => {
214	                let _ = tx.send(false);
215	                return;
216	            }
217	        };
218	        if send_hello(&mut conn, Role::Brain).is_err() {
219	            let _ = tx.send(false);
220	            return;
221	        }
222	        send(&mut conn, cmd_kind, serde_json::json!({}));
223	        loop {
224	            match read_frame(&mut conn) {
225	                Ok(f) if f.kind == want_kind => {
226	                    let _ = tx.send(true);
227	                    return;
228	                }
229	                Ok(_) => continue, // ignore interleaved frames
230	                Err(_) => {
231	                    let _ = tx.send(false);
232	                    return;
233	                }
234	            }
235	        }
236	    });
237	    rx.recv_timeout(deadline).unwrap_or(false)
238	}
239
240	#[test]
241	fn a_backed_up_controller_does_not_wedge_the_session() {
242	    let name = unique_name();
243	    let broker = Broker::bind(&name).expect("bind broker");
244	    let serve_broker = Arc::clone(&broker);
245	    thread::spawn(move || {
246	        let _ = serve_broker.serve(); // accept every connection on its own thread
247	    });
248
249	    // ── Controller connection A: spawn the flooding child. dispatch_spawn
250	    //    pre-attaches A as the CONTROLLER (become_controller, by=None). ──
251	    let mut a = None;
252	    for _ in 0..200 {
253	        match LocalSocketTransport::connect(&name) {
254	            Ok(c) => {
255	                a = Some(c);
256	                break;
257	            }
258	            Err(_) => thread::sleep(Duration::from_millis(10)),
259	        }
260	    }
261	    let mut a = a.expect("connect controller A");
262	    send_hello(&mut a, Role::Brain).expect("hello A");
263	    send(
264	        &mut a,
265	        KIND_SPAWN,
266	        serde_json::to_value(flood_spawn_req("wedge-ep")).unwrap(),
267	    );
268	    let sid = loop {
269	        let f = read_frame(&mut a).expect("frame before spawned");
270	        if f.kind == KIND_SPAWNED {
271	            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
272	        }
273	    };
274
275	    // ── A now STOPS reading. The child floods → A's socket recv buffer fills →
276	    //    the controller writer thread blocks on it → the controller channel fills
277	    //    → the drain's bounded send_timeout starts blocking. PRE-FIX this parked
278	    //    the drain inside append HOLDING the log lock (permanent wedge). POST-FIX
279	    //    the send is OFF the log lock, so the session must stay responsive even
280	    //    while the controller is fully backed up. Probe DURING the stall (before
281	    //    CONTROLLER_WRITE_DEADLINE = 5s elapses). ──
282	    thread::sleep(Duration::from_secs(3));
283
284	    // A fresh connection's KIND_NET_STATUS touches neither the sessions nor the
285	    // log lock (proves the process is alive). KIND_SESSIONS enumerates every
286	    // session's log (`delivered_through`) under each log lock — PRE-FIX it hung on
287	    // the parked session's held lock; POST-FIX it answers because the drain no
288	    // longer holds the lock during the controller send.
289	    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
290	    let sessions_answered =
291	        probe(&name, KIND_SESSIONS, KIND_SESSIONS_REPLY, Duration::from_secs(5));
292
293	    eprintln!(
294	        "=== W1 GATE: broker_alive(net_status)={broker_alive} \
295	         sessions_answered={sessions_answered} (fixed = both true; pre-fix sessions=false) ==="
296	    );
297
298	    // Reap the flood child (scoped) + tear the broker down.
299	    if let Some(pid) = broker.session_pid(sid) {
300	        kill_pid(pid);
301	    }
302	    drop(a);
303
304	    assert!(
305	        broker_alive,
306	        "the broker PROCESS must answer a fresh connection's net-status"
307	    );
308	    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
309	    assert!(
310	        sessions_answered,
311	        "a fully-backed-up controller must NOT wedge the session: KIND_SESSIONS \
312	         (which reads each log's delivered_through under the log lock) must still \
313	         answer because the controller write is delivered OFF the log lock on a \
314	         dedicated writer thread (W1 fix). Pre-fix this hung — the inline \
315	         controller write parked the drain holding the log lock."
316	    );
317	}
318
319	#[test]
320	fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
321	    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
322	    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
323	    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
324	    // small kernel input queue is expected to park (real there).
325	    let name = unique_name();
326	    let broker = Broker::bind(&name).expect("bind broker");
327	    let serve_broker = Arc::clone(&broker);
328	    thread::spawn(move || {
329	        let _ = serve_broker.serve();
330	    });
331
332	    // Controller A spawns the flood child (which IGNORES stdin) and then DRAINS
333	    // its frames forever on a background thread — so the output-side park (a)
334	    // does NOT fire and we isolate the input-side park (b).
335	    let mut a = None;
336	    for _ in 0..200 {
337	        match LocalSocketTransport::connect(&name) {
338	            Ok(c) => {
339	                a = Some(c);
340	                break;
341	            }
342	            Err(_) => thread::sleep(Duration::from_millis(10)),
343	        }
344	    }
345	    let mut a = a.expect("connect controller A");
346	    send_hello(&mut a, Role::Brain).expect("hello A");
347	    send(
348	        &mut a,
349	        KIND_SPAWN,
350	        serde_json::to_value(flood_spawn_req("inj-ep")).unwrap(),
351	    );
352	    let sid = loop {
353	        let f = read_frame(&mut a).expect("frame before spawned");
354	        if f.kind == KIND_SPAWNED {
355	            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
356	        }
357	    };
358	    // Keep the controller socket drained so append never parks.
359	    thread::spawn(move || while read_frame(&mut a).is_ok() {});
360
361	    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
362	    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
363	    // master; the child never reads stdin, so the input buffer fills and the
364	    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
365	    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
366	    let name_inj = name.clone();
367	    let (tx, rx) = std::sync::mpsc::channel::<bool>();
368	    thread::spawn(move || {
369	        let mut b = match LocalSocketTransport::connect(&name_inj) {
370	            Ok(c) => c,
371	            Err(_) => {
372	                let _ = tx.send(false);
373	                return;
374	            }
375	        };
376	        if send_hello(&mut b, Role::Brain).is_err() {
377	            let _ = tx.send(false);
378	            return;
379	        }
380	        send(
381	            &mut b,
382	            KIND_ENDPOINT_INPUT,
383	            serde_json::to_value(EndpointInputReq {
384	                endpoint: "inj-ep".to_string(),
385	                data_b64: encode_bytes(&big),
386	            })
387	            .unwrap(),
388	        );
389	        loop {
390	            match read_frame(&mut b) {
391	                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
392	                    let _ = tx.send(true);
393	                    return;
394	                }
395	                Ok(_) => continue,
396	                Err(_) => {
397	                    let _ = tx.send(false);
398	                    return;
399	                }
400	            }
401	        }
402	    });
403	    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);
404
405	    // Broker process still alive on a fresh connection (the park is the dispatch
406	    // thread serving the inject, not the whole process).
407	    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
408
409	    eprintln!(
410	        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
411	         (expect inject_acked=false = write_input parked on write_all) ==="
412	    );
413
414	    if let Some(pid) = broker.session_pid(sid) {
415	        kill_pid(pid);
416	    }
417
418	    assert!(broker_alive, "broker process must stay alive (per-session park)");
419
420	    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
421	    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
422	    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
423	    //   the operator's platform the input-side write is BENIGN; the operator wedge
424	    //   is park (a) (controller output delivery), reached on injection via the
425	    //   injection-INDUCED output backing up the controller — not the inject write.
426	    // - Unix forkpty has a small kernel input queue, so the same inject is
427	    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
428	    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
429	    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
430	    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
431	    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
432	    //   (and bounds the park) once CI actually shows it. The only hard invariant
433	    //   here is broker liveness (asserted above for both platforms).
434	    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
435	    #[cfg(windows)]
436	    assert!(
437	        inject_acked,
438	        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
439	         if this flips, the input buffer began blocking — re-examine park (b)."
440	    );
441	    #[cfg(unix)]
442	    eprintln!(
443	        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
444	         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
445	    );
446	}
447
448	// ── v0.13.0 W1b — REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, the facet-B real-fixture
449	//    gate W1 LACKED ────────────────────────────────────────────────────────────
450	//
451	// The hazard: `EffectJournal::apply_once` holds the GLOBAL journal lock across the
452	// effect closure. A journaled PtyWrite (every operator keystroke carrying an
453	// op_id) runs `write_input` INSIDE that lock. If a non-draining child's PTY input
454	// buffer is full, `write_input` BLOCKS — holding the journal lock forever. So the
455	// single-threaded dispatch can no longer service ANY journaled effect, and a
456	// concurrent `spt rc` attach dies with "brain IPC read deadline elapsed" (facet
457	// B). W1's gate only checked KIND_SESSIONS liveness; this gate uses a REAL
458	// rc-attach client (request_attach / serve_attach over a real loopback conn + a
459	// real PTY child) and asserts the attach is actually SERVICED and RECEIVES output.
460	//
461	// PLATFORM (mirrors the park-(b) finding above): Windows ConPTY ABSORBS sustained
462	// input without blocking write_input, so the journal lock is not held long → the
463	// wedge is benign here; this test then asserts the POSITIVE W1 lacked (liveness +
464	// real byte receipt + attach serviced). On cfg(unix) (gravity-linux) forkpty's
465	// small input queue parks write_input → the journal lock is held → pre-fix the
466	// concurrent attach wedges; post-fix (effect off the lock) it is serviced. All
467	// waits are behind generous deadlines + watchdog channels so nothing hangs CI.
468
469	/// Hold the temp home for the whole test binary (serve_attach best-effort stamps
470	/// the endpoint perch's driven_by marker — that resolution must never touch a
471	/// real home).
472	static WEDGE_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();
473	fn init_wedge_home() {
474	    WEDGE_HOME.get_or_init(|| {
475	        let dir = tempfile::TempDir::new().expect("temp dir");
476	        std::env::set_var("SPT_HOME", dir.path());
477	        dir
478	    });
479	}
480
481	fn hermetic() -> NetConfig {
482	    NetConfig {
483	        identity: Identity::generate(),
484	        relay: RelayPolicy::Disabled,
485	        discovery: LocalDiscovery::Off,
486	        scope: BindScope::Loopback,
487	        membership: None,
488	    }
489	}
490
491	fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
492	    let host = NetHost::start(hermetic()).expect("net host start");
493	    let broker =
494	        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
495	    let serve = Arc::clone(&broker);
496	    thread::spawn(move || {
497	        let _ = serve.serve();
498	    });
499	    broker
500	}
501
502	fn connect_retry(name: &str) -> Brain {
503	    for _ in 0..300 {
504	        if let Ok(b) = Brain::cold_start(name, 1) {
505	            return b;
506	        }
507	        thread::sleep(Duration::from_millis(10));
508	    }
509	    panic!("brain could not connect");
510	}
511
512	fn count(hay: &[u8], needle: &[u8]) -> usize {
513	    if needle.is_empty() || hay.len() < needle.len() {
514	        return 0;
515	    }
516	    let (mut n, mut i) = (0, 0);
517	    while i + needle.len() <= hay.len() {
518	        if &hay[i..i + needle.len()] == needle {
519	            n += 1;
520	            i += needle.len();
521	        } else {
522	            i += 1;
523	        }
524	    }
525	    n
526	}
527
528	/// Poll a brain's net-streams for a PEER-initiated stream (the inbound attach the
529	/// target serves) — return (stream_id, origin). Watchdog'd by the poll count.
530	fn wait_for_stream(brain: &mut Brain) -> Option<(u64, String)> {
531	    for _ in 0..400 {
532	        let reply = brain.net_streams().expect("net-streams");
533	        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {
534	            return Some((s.stream_id, s.remote_id_hex.clone()));
535	        }
536	        thread::sleep(Duration::from_millis(5));
537	    }
538	    None
539	}
540
541	#[test]
542	fn a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach() {
543	    init_wedge_home();
544	    let dir = tempfile::tempdir().expect("tempdir");
545	    let name = unique_name();
546	    let _broker = net_broker(&name, &dir.path().join("wedge"));
547
548	    // ── A REAL flooding child that IGNORES stdin (its PTY input buffer fills). ──
549	    let mut spawner = connect_retry(&name);
550	    let sid = spawner
551	        .spawn_session(flood_spawn_req("wedge-jrnl-ep"))
552	        .expect("spawn flood child");
553
554	    // ── The WEDGE driver: on its OWN connection, pump journaled input
555	    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once
556	    //    runs write_input INSIDE the global journal lock). On Unix the full input
557	    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.
558	    //    The driver fires a BOUNDED burst of MODEST-sized ops (so a backed-up
559	    //    broker IPC never blocks the driver's own socket write — the burst always
560	    //    completes and the thread exits cleanly, never hanging the test process)
561	    //    and then idles, checking the stop flag. ──
562	    let stop = Arc::new(AtomicBool::new(false));
563	    let pumped = Arc::new(AtomicU64::new(0));
564	    let driver_name = name.clone();
565	    let driver_stop = Arc::clone(&stop);
566	    let driver_pumped = Arc::clone(&pumped);
567	    let driver = thread::spawn(move || {
568	        // PUMP carrier (cold_start_pump): the reader thread drains the driver's
569	        // OWN incoming frames (KIND_APPLIED acks + session output) so the broker's
570	        // reply side never backs up and blocks reading our input — which would
571	        // deadlock send_effect on a plain Whole conn (observed: it stalled after a
572	        // few ops). The pump keeps the driver's socket healthy so it can keep a
573	        // journaled write in-flight without ever wedging the CLIENT.
574	        let mut w = match Brain::cold_start_pump(&driver_name, 1, Duration::from_secs(2)) {
575	            Ok(b) => b,
576	            Err(_) => return,
577	        };
578	        // Bind the driver brain to the same session so send_effect has a target.
579	        if w.attach(sid, 0).is_err() {
580	            return;
581	        }
582	        let chunk = vec![b'W'; 16 * 1024];
583	        let mut op = 1u64;
584	        // Sustain journaled input: keep ops in flight (and, on Unix, parked under
585	        // the journal lock) until teardown. Each send_effect writes a frame; the
586	        // pump reader drains replies concurrently so the write never deadlocks.
587	        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
588	            if w.send_effect(op, &chunk).is_err() {
589	                break;
590	            }
591	            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
592	            op += 1;
593	            // Drain queued replies (acks/output) so the carrier stays healthy, then
594	            // PACE with an explicit floor sleep. The doc above says "sustain a write
595	            // in-flight", but the bare `while !stop` pumped ~6094 ops and pegged the
596	            // core, out-competing the SIBLING attacher thread for scheduling under
597	            // contention so its subscribe was never serviced in 30s (the kitsubito
598	            // reliability victim, subscribed=false). A small sleep keeps a journaled
599	            // write continuously in-flight (facet-B's load is intact) while yielding
600	            // the CPU the attacher needs — sustained, not storming.
601	            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
602	            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
603	            // ABSORBS the journaled write, so there is no storm to pace and the sleep
604	            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
605	            // skip it there; Windows p0 was green pre-pacing.
606	            #[cfg(unix)]
607	            std::thread::sleep(Duration::from_millis(5));
608	        }
609	    });
610
611	    // Give the driver time to get a journaled write in-flight (and, on Unix, to
612	    // park write_input under the journal lock).
613	    thread::sleep(Duration::from_secs(2));
614
615	    // ── The CONCURRENT `spt rc` attach: a SEPARATE operator connection requests a
616	    //    real attach over a loopback conn, subscribes, and (via serve_attach)
617	    //    receives the child's output. Run on its own thread so the OUTER
618	    //    recv_timeout watchdog bounds the whole gate — a wedged subscribe (facet B,
619	    //    the "brain IPC read deadline elapsed" symptom) surfaces as a clean test
620	    //    failure, never a hang. ──
621	    let (result_tx, result_rx) = std::sync::mpsc::channel::<(bool, bool)>();
622	    let attach_name = name.clone();
623	    let attacher = thread::spawn(move || {
624	        // PUMP mode (Split) so the got_output loop's `read_event_until(now+250ms)`
625	        // actually HONORS its slice deadline — a plain `Whole` cold_start IGNORES it
626	        // (read_frame blocks; see output_contains_within). With a Whole conn the wedge
627	        // (no NetStreamData ever lands) PARKS read_frame forever: the 8s got_output
628	        // deadline is never re-evaluated, result_tx.send never runs, and the 30s
629	        // watchdog yields (false,false) — so `subscribed=false` was a MEASUREMENT
630	        // ARTIFACT, not a subscribe failure (the broker registers the subscribe fast,
631	        // lock-free; gdb showed the attacher parked PAST net_stream_subscribe). Pump
632	        // mode makes the slices real, so the gate reports the TRUE signal.
633	        let mut operator = match Brain::cold_start_pump(&attach_name, 1, Duration::from_secs(2)) {
634	            Ok(b) => b,
635	            Err(_) => {
636	                let _ = result_tx.send((false, false));
637	                return;
638	            }
639	        };
640	        let dialed = match operator.net_dial_loopback() {
641	            Ok(d) => d,
642	            Err(_) => {
643	                let _ = result_tx.send((false, false));
644	                return;
645	            }
646	        };
647	        let own_origin = dialed.remote_id_hex.clone();
648	        let stream_b = match request_attach(
649	            &mut operator,
650	            dialed.conn_id,
651	            sid,
652	            0,
653	            2,
654	            AttachIntent::Viewer, // a read-only rc --view: receives output, no journaled input
655	        ) {
656	            Ok(s) => s,
657	            Err(_) => {
658	                let _ = result_tx.send((false, false));
659	                return;
660	            }
661	        };
662	        // The subscribe reply is the first servicing proof: a wedged dispatch
663	        // never answers it; the outer recv_timeout watchdog then fails the gate.
664	        let subscribed = operator.net_stream_subscribe(stream_b, 0).is_ok();
665
666	        // The target serves the inbound attach stream (the real serve_attach pump).
667	        let mut target = connect_retry(&attach_name);
668	        let (stream_a, origin) = match wait_for_stream(&mut target) {
669	            Some(s) => s,
670	            None => {
671	                let _ = result_tx.send((subscribed, false));
672	                return;
673	            }
674	        };
675	        assert_eq!(origin, own_origin, "loopback origin is this node's own id");
676	        let serve_broker = attach_name.clone();
677	        let server = thread::spawn(move || {
678	            let _ = serve_attach(&mut target, &serve_broker, stream_a, "wedge-jrnl-ep", &origin, 0);
679	        });
680
681	        // The positive W1 LACKED: a real rc-client attach actually RECEIVES PTY
682	        // output bytes from the flooding child. Output delivery does NOT go through
683	        // the journal lock, so receiving bytes proves the dispatch serviced this
684	        // attach despite the wedged journaled input.
685	        let mut decoder = AttachDecoder::new();
686	        let mut got_output = false;
687	        let deadline = std::time::Instant::now() + Duration::from_secs(8);
688	        while std::time::Instant::now() < deadline {
689	            match operator.read_event_until(Some(
690	                std::time::Instant::now() + Duration::from_millis(250),
691	            )) {
692	                Ok(BrokerEvent::NetStreamData { stream_id, bytes, .. }) if stream_id == stream_b => {
693	                    for rec in decoder.push(&bytes) {
694	                        if let AttachRecord::Output { data_b64, .. } = rec {
695	                            let chunk = decode_bytes(&data_b64).unwrap_or_default();
696	                            if count(&chunk, b"FLOOD") >= 1 {
697	                                got_output = true;
698	                            }
699	                        }
700	                    }
701	                    if got_output {
702	                        break;
703	                    }
704	                }
705	                Ok(_) => continue,
706	                Err(_) => continue, // slice timeout — keep waiting until the deadline
707	            }
708	        }
709
710	        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),
711	        // THEN best-effort detach + drain. We deliberately do NOT block the result
712	        // on the serve thread: a controller's `serve_attach` forward can block on a
713	        // non-draining operator socket while the flood child keeps producing — the
714	        // main thread kills the child to unblock it. Reporting first then draining
715	        // keeps the watchdog free of that wedge.
716	        let _ = result_tx.send((subscribed, got_output));
717	        let _ = operator.net_stream_send(stream_b, &[], None, true);
718	        // Keep draining the operator so serve's forwards drain and serve_attach can
719	        // observe our detach + the child's eventual death and return cleanly.
720	        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);
721	        while std::time::Instant::now() < drain_deadline {
722	            match operator
723	                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))
724	            {
725	                Ok(BrokerEvent::NetStreamEof { .. }) => break,
726	                Ok(_) => continue,
727	                Err(_) => continue,
728	            }
729	        }
730	        let _ = server.join();
731	    });
732
733	    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a
734	    // hang. The attacher reports its result before any blocking teardown, so this
735	    // recv is the hard ceiling on the whole gate. Deadline is env-tunable
736	    // (attach_gate_watchdog: default 30s tight, CI-generous): a real wedge blocks
737	    // FOREVER so it trips any finite deadline — the knob only grants headroom for a
738	    // serviced-but-late request_attach ack under CI scheduler starvation.
739	    let watchdog = attach_gate_watchdog();
740	    let recv_t0 = Instant::now();
741	    let (subscribed, got_output) = result_rx
742	        .recv_timeout(watchdog)
743	        .unwrap_or((false, false));
744	    let recv_elapsed = recv_t0.elapsed();
745	    // Source-attribution (cheap, load-bearing triage): `channel` = the attacher
746	    // reported; `WATCHDOG-DEFAULT` = the gate's elastic bound (request_attach's
747	    // journaled ack) outran the watchdog. Threshold derived from the CONFIGURED
748	    // watchdog (>= watchdog-1s) so the label stays truthful at 30s OR 120s.
749	    let source = if recv_elapsed >= watchdog.saturating_sub(Duration::from_secs(1)) {
750	        "WATCHDOG-DEFAULT"
751	    } else {
752	        "channel"
753	    };
754	    eprintln!(
755	        "JRNL_FLAKE: MAIN recv -> ({subscribed},{got_output}) after {recv_elapsed:?} (source={source})"
756	    );
757
758	    // Tear down so nothing leaks AND so any backpressured helper thread unblocks:
759	    // kill the flood child FIRST (ends the output backing up serve's forwards),
760	    // then signal the driver to stop. The helper threads' own bounded drains let
761	    // them exit; we never block the GATE/assertions on a wedged thread.
762	    if let Some(pid) = session_pid_bounded(&_broker, sid, Duration::from_secs(5)) {
763	        kill_pid(pid);
764	    }
765	    stop.store(true, std::sync::atomic::Ordering::Relaxed);
766	    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in
767	    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so
768	    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict
769	    // (the symptom doyle's kitsubito reliability run hit). The gate result is already
770	    // captured by the 30s watchdog above, so abandon a parked helper and let the
771	    // assertions fire fast (a true wedge then fails on `subscribed=false` in ~30s).
772	    let _ = join_bounded(driver, Duration::from_secs(8));
773	    let _ = join_bounded(attacher, Duration::from_secs(8));
774	    let pumped_n = pumped.load(std::sync::atomic::Ordering::Relaxed);
775
776	    // On Unix, fold the W1-missing park-(b) liveness probe HERE (before tearing the
777	    // broker down — the probe needs a live broker). On Windows the journaled write
778	    // is absorbed, so there is no park to assert liveness against.
779	    #[cfg(unix)]
780	    let alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
781
782	    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──
783	    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)
784	    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe
785	    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,
786	    // which drops each Brain's SendHalf → the broker's per-conn handler read errors
787	    // → the broker closes its end → the client RecvHalf read finally errors → the
788	    // reader thread exits. But that close CHAIN is asynchronous and the reader's
789	    // JoinHandle is only dropped, never joined (it is abandoned by design — KH 7.6).
790	    // On Windows, if the test returns before that chain completes, a reader thread
791	    // is still parked inside a named-pipe read syscall at process teardown, and a
792	    // thread blocked in that syscall on a half-closed handle aborts the WHOLE
793	    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,
794	    // where the next test / process exit races the chain). FIX: after the Brains
795	    // are dropped (helpers joined), settle briefly so the close chain completes and
796	    // every abandoned reader observes its read error and exits before the harness
797	    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.
798	    // This is purely teardown hygiene; the assertions below are unchanged.
799	    thread::sleep(Duration::from_millis(750));
800
801	    eprintln!(
802	        "=== W1b JOURNAL-WEDGE GATE: journaled_ops_pumped={pumped_n} \
803	         concurrent_attach_subscribed={subscribed} attach_received_pty_output={got_output} \
804	         (fixed = both true; pre-fix on Unix: subscribed=false — attach wedged on the \
805	         held journal lock) ==="
806	    );
807
808	    // [int->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
809	    // FACET B — the gate W1 lacked: a concurrent rc attach must be SERVICED even
810	    // while a journaled input effect is in-flight (and, on Unix, blocked holding
811	    // the journal lock). Pre-fix on a forkpty platform this subscribe wedges
812	    // ("brain IPC read deadline elapsed"); post-fix the effect runs off the lock.
813	    assert!(
814	        subscribed,
815	        "a concurrent `spt rc` attach must be SERVICED while a journaled PtyWrite \
816	         is in-flight: the attach's subscribe reply did not arrive within the \
817	         watchdog deadline — the journal lock is held across write_input (facet B, \
818	         REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE)."
819	    );
820	    // POSITIVE (W1 lacked): a real rc-client attach RECEIVES PTY output bytes —
821	    // the b4 drain-decouple keeps the viewer fed despite controller backpressure,
822	    // and (post-keystone) the viewer SNAPS the pre-eviction ring-roll forward gap
823	    // instead of fataling on it, so the FLOOD bytes actually render.
824	    // [int->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
825	    // [int->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]
826	    assert!(
827	        got_output,
828	        "the concurrent attach must actually RECEIVE PTY output from the child \
829	         (not just liveness): no FLOOD bytes rendered over the real attach pump."
830	    );
831
832	    // On Unix, ALSO fold the W1-missing park-(b) hard assertion: the broker PROCESS
833	    // must stay alive on a fresh connection even with the journaled write parked
834	    // (the park is per-effect under the lock, not a whole-process death). The probe
835	    // ran above (while the broker was still alive); assert its result here.
836	    #[cfg(unix)]
837	    assert!(
838	        alive,
839	        "broker process must stay alive while a journaled input is parked (Unix forkpty)"
840	    );
841	}
842
843	// ── v0.13.0 W2 Layer G — REQ-MSG-IDLE-TRANSLATION-BINARY: the ADR-0022
844	//    translation-binary idle-delivery INTEGRATION gate, on a REAL broker + REAL
845	//    PTY + a REAL spawned translation-binary fixture (NO mocks of the PTY, the
846	//    transport, or the binary). This is a doyle gate point — every assertion is
847	//    STRUCTURAL (a byte-receipt file, a serviceability probe, a stdin-log content
848	//    check, a pid-gone check), never on wall-clock / fsync timing. ──────────────
849	//
850	// The REAL translation binary fixture (auto-discovered `src/bin/*.rs`, referenced
851	// by the cargo `CARGO_BIN_EXE_*` env var):
852	//   - `xlate_choreo_fixture` — a pure stdin→stdout JSON-lines filter. On an
853	//     `{type:event}` it emits the CC stash/restore choreography (`ctrl+s` `delay`
854	//     `text:XLATE_OK` `enter` `delay` `ctrl+s` `commit`); env
855	//     `XLATE_FIXTURE_MODE=nocommit` drops the final `{commit}` to exercise the
856	//     broker's commit-deadline FAULT. It appends every stdin LINE it receives to
857	//     `XLATE_FIXTURE_STDIN_LOG` (the privacy / content-free-ping assertion reads
858	//     that file).
859	//
860	// The REAL PTY CHILD is the proven cross-OS ECHO child the existing attach tests
861	// use (`findstr "."` on Windows / `cat` on Unix): it echoes its stdin (the
862	// injected keystrokes + the operator's flushed input) back to stdout LINE BY LINE.
863	// Byte-receipt is therefore observed STRUCTURALLY through the broker's REAL output
864	// ring over a REAL viewer attach — not a mock, not a side file. To dodge the
865	// `ctrl+s` (0x13 = XOFF) echo-stall concern, every marker the test wants to OBSERVE
866	// is terminated with `\r` (Enter) so the line-buffered echo child flushes that line
867	// regardless of a preceding control byte; the choreography's own `enter` does the
868	// same for `XLATE_OK`.
869
870	const XLATE_BIN: &str = env!("CARGO_BIN_EXE_xlate_choreo_fixture");
871
872	/// The proven cross-OS echo PTY child (stdin → stdout, line-buffered).
873	fn echo_program() -> (String, Vec<String>) {
874	    #[cfg(unix)]
875	    {
876	        ("cat".to_string(), Vec::<String>::new())
877	    }
878	    #[cfg(windows)]
879	    {
880	        ("findstr".to_string(), vec![".".to_string()])
881	    }
882	}
883
884	/// A translation-bearing spawn: the PTY child is the real echo child (so the
885	/// injected/operator bytes echo back through the broker ring), and the
886	/// `translation_binary` is the real `xlate_choreo_fixture`. The fixture env vars
887	/// must already be set in THIS process (set_var before the session spawns the
888	/// child — clean under nextest's process-per-test).
889	fn xlate_spawn_req(endpoint: &str) -> SpawnReq {
890	    let (program, args) = echo_program();
891	    SpawnReq {
892	        program,
893	        args,
894	        rows: 24,
895	        cols: 80,
896	        endpoint: endpoint.to_string(),
897	        cwd: None,
898	        env: Default::default(),
899	        translation_binary: Some(XLATE_BIN.to_string()),
900	    }
901	}
902
903	/// Attach a viewer brain to a session and accumulate its output bytes until either
904	/// `needle` appears (→ true) or `deadline` elapses (→ false). The structural
905	/// byte-receipt seam: bytes that reach the PTY are echoed by the child and arrive
906	/// here over the REAL broker output ring. Drains on its own connection so the
907	/// caller's controller socket is untouched.
908	fn output_contains_within(name: &str, sid: u64, needle: &[u8], deadline: Duration) -> bool {
909	    // PUMP mode (Split) so `read_event_until` actually HONORS the deadline — a
910	    // plain `Whole` cold_start ignores it (read_frame blocks), which would hang
911	    // forever if the PTY produced no output (e.g. an XOFF-stalled terminal).
912	    let mut viewer = match Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
913	        Ok(b) => b,
914	        Err(_) => return false,
915	    };
916	    if viewer.attach(sid, 0).is_err() {
917	        return false;
918	    }
919	    let mut acc: Vec<u8> = Vec::new();
920	    let stop = Instant::now() + deadline;
921	    while Instant::now() < stop {
922	        match viewer.read_event_until(Some(Instant::now() + Duration::from_millis(150))) {
923	            Ok(BrokerEvent::Output { bytes, .. }) => {
924	                acc.extend_from_slice(&bytes);
925	                if count(&acc, needle) >= 1 {
926	                    return true;
927	                }
928	            }
929	            Ok(_) => continue,
930	            Err(_) => continue,
931	        }
932	    }
933	    false
934	}
935
936	/// Observe `needle` in a session's output ring while interleaving OPERATOR XON
937	/// (`ctrl+q` = 0x11) keystrokes to RESUME any XOFF-paused output (the choreography's
938	/// `ctrl+s`/0x13 pauses this ConPTY's output flow, stranding the echoed payload).
939	/// When `type_needle` is set, the helper ALSO types `needle\r` itself (for the
940	/// fresh-attach serviceability marker the operator originates); for the injected
941	/// payload it is `false` (we must NOT type the payload — that would be a false pass;
942	/// we only un-stick the flow so the BINARY-injected payload's echo can flush).
943	/// Pump mode → bounded reads. Returns true once `needle` appears.
944	fn observe_with_xon(
945	    name: &str,
946	    sid: u64,
947	    needle: &[u8],
948	    type_needle: bool,
949	    deadline: Duration,
950	) -> bool {
951	    let mut op = match Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
952	        Ok(b) => b,
953	        Err(_) => return false,
954	    };
955	    if op.attach(sid, 0).is_err() {
956	        return false;
957	    }
958	    let mut acc: Vec<u8> = Vec::new();
959	    let stop = Instant::now() + deadline;
960	    while Instant::now() < stop {
961	        // XON pulse to resume paused output; optionally re-type the operator marker.
962	        let _ = op.send_input(b"\x11");
963	        if type_needle {
964	            let mut line = needle.to_vec();
965	            line.push(b'\r');
966	            let _ = op.send_input(&line);
967	        }
968	        // Drain a slice of output, accumulating, before the next pulse.
969	        let slice = Instant::now() + Duration::from_millis(250);
970	        while Instant::now() < slice {
971	            match op.read_event_until(Some(Instant::now() + Duration::from_millis(120))) {
972	                Ok(BrokerEvent::Output { bytes, .. }) => {
973	                    acc.extend_from_slice(&bytes);
974	                    if count(&acc, needle) >= 1 {
975	                        return true;
976	                    }
977	                }
978	                Ok(_) => continue,
979	                Err(_) => break,
980	            }
981	        }
982	    }
983	    false
984	}
985
986	/// Deliver an inbound `<EVENT>` to a hosted endpoint via a raw `KIND_ENDPOINT_INPUT`
987	/// frame on a fresh brain connection (the Brain client has no endpoint-input
988	/// helper), and return whether the broker acked `delivered=true` within `deadline`.
989	/// Runs the exchange on a thread so a wedged reply surfaces as a timeout, not a hang.
990	fn deliver_endpoint_event(name: &str, endpoint: &str, envelope: &[u8], deadline: Duration) -> bool {
991	    let name = name.to_string();
992	    let endpoint = endpoint.to_string();
993	    let envelope = envelope.to_vec();
994	    let (tx, rx) = std::sync::mpsc::channel::<bool>();
995	    thread::spawn(move || {
996	        let mut conn = match LocalSocketTransport::connect(&name) {
997	            Ok(c) => c,
998	            Err(_) => {
999	                let _ = tx.send(false);
1000	                return;
1001	            }
1002	        };
1003	        if send_hello(&mut conn, Role::Brain).is_err() {
1004	            let _ = tx.send(false);
1005	            return;
1006	        }
1007	        send(
1008	            &mut conn,
1009	            KIND_ENDPOINT_INPUT,
1010	            serde_json::to_value(EndpointInputReq {
1011	                endpoint: endpoint.clone(),
1012	                data_b64: encode_bytes(&envelope),
1013	            })
1014	            .unwrap(),
1015	        );
1016	        loop {
1017	            match read_frame(&mut conn) {
1018	                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
1019	                    // The reply carries `delivered`; surface it.
1020	                    let delivered = f
1021	                        .payload
1022	                        .get("delivered")
1023	                        .and_then(|d| d.as_bool())
1024	                        .unwrap_or(false);
1025	                    let _ = tx.send(delivered);
1026	                    return;
1027	                }
1028	                Ok(_) => continue,
1029	                Err(_) => {
1030	                    let _ = tx.send(false);
1031	                    return;
1032	                }
1033	            }
1034	        }
1035	    });
1036	    rx.recv_timeout(deadline).unwrap_or(false)
1037	}
1038
1039	/// Whether a pid is still a live process (the no-zombie reap check). Best-effort
1040	/// and cross-OS: a `tasklist` query on Windows, `kill -0` on Unix.
1041	#[cfg(windows)]
1042	fn pid_alive(pid: u32) -> bool {
1043	    let out = std::process::Command::new("tasklist")
1044	        .args(["/FI", &format!("PID eq {pid}"), "/NH"])
1045	        .output();
1046	    match out {
1047	        Ok(o) => {
1048	            let s = String::from_utf8_lossy(&o.stdout);
1049	            // tasklist prints the row when the pid is live; "No tasks" otherwise.
1050	            s.contains(&pid.to_string())
1051	        }
1052	        Err(_) => false,
1053	    }
1054	}
1055	#[cfg(unix)]
1056	fn pid_alive(pid: u32) -> bool {
1057	    std::process::Command::new("kill")
1058	        .args(["-0", &pid.to_string()])
1059	        .status()
1060	        .map(|s| s.success())
1061	        .unwrap_or(false)
1062	}
1063
1064	/// Spawn the translation-bearing session on a fresh net-broker and return the
1065	/// (broker, name, sid, controller-brain). The controller brain stays attached as
1066	/// the live operator (the W1 coexistence partner) and is RETURNED so the test
1067	/// keeps it alive + can probe it; the caller drains it as needed.
1068	fn spawn_xlate_session(
1069	    name: &str,
1070	    dir: &std::path::Path,
1071	    endpoint: &str,
1072	) -> (Arc<Broker>, u64, Brain) {
1073	    let broker = net_broker(name, dir);
1074	    // PUMP mode so the spawner-controller's reads are BOUNDED (a Whole conn ignores
1075	    // the read deadline → a drainer loop would block forever and never observe a
1076	    // stop flag, hanging the test on join). The pump reader drains the controller's
1077	    // socket so it stays serviceable without the test blocking.
1078	    let mut spawner = {
1079	        let mut b = None;
1080	        for _ in 0..300 {
1081	            if let Ok(c) = Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
1082	                b = Some(c);
1083	                break;
1084	            }
1085	            thread::sleep(Duration::from_millis(10));
1086	        }
1087	        b.expect("brain could not connect (pump)")
1088	    };
1089	    let sid = spawner
1090	        .spawn_session(xlate_spawn_req(endpoint))
1091	        .expect("spawn translation-bearing session");
1092	    (broker, sid, spawner)
1093	}

//! v0.13.0 W1 (REQ-HAZARD-INJECT-CONTROL-COEXIST) — the CONTROLLER-DELIVERY
//! non-wedge gate, on a REAL broker hosting a REAL PTY child over the real local
//! IPC (no mocks of the PTY or the transport).
//!
//! ## The bug this gate locks down (park (a), the output-stall / wedge root)
//!
//! PRE-FIX (v0.12.x), [`OutputLog::append`] fanned each output chunk to the
//! CONTROLLER via a SYNCHRONOUS, BLOCKING `write_frame` held INLINE on the
//! session's single drain thread, while the `Mutex<OutputLog>` was locked.
//! VIEWERS instead get a dedicated writer thread + a bounded, evicting
//! `sync_channel` and can never stall the drain. So a slow / non-draining
//! controller (here a controller connection that stops reading while the child
//! floods) parked the drain INSIDE `append` STILL HOLDING the log lock — output
//! stalled and any path needing that log lock (a fresh attach, or the global
//! `KIND_SESSIONS` enumeration, which reads each log's `delivered_through`)
//! blocked FOREVER, while the broker process stayed alive. The repro that proved
//! this (captured 2026-06-19): `APPEND_CTRL_ENTER sid=1 seq=2` with no matching
//! `APPEND_CTRL_EXIT`, `broker_alive=true` but `sessions_answered=false`.
//!
//! POST-FIX (W1): controller delivery moved off the drain thread onto a dedicated
//! writer thread + bounded channel (the viewer_writer pattern, but authoritative:
//! the writer advances `delivered_through`). The drain hands each chunk off with a
//! bounded `send_timeout` OFF the log lock; a fully-stalled controller is detached
//! after `CONTROLLER_WRITE_DEADLINE`. So even while the controller is fully backed
//! up, the session does NOT wedge — `KIND_SESSIONS` still answers. THIS test
//! asserts that fixed behavior.
//!
//! ## RUNNER CONTRACT — run process-per-test (nextest) or single-threaded (W1b F1)
//!
//! These tests share ONE process-global `SPT_HOME` (set once via
//! [`std::env::set_var`] in [`init_wedge_home`]) and each spins a REAL broker +
//! PTY child. Under the default `cargo test` harness — N tests as parallel
//! THREADS in ONE process — the `set_var` mutation races and the concurrent
//! brokers contend, which HANGS on Windows (cost a 30-min wedge during W1b).
//!
//! Run them isolated:
//!   - `cargo nextest run --test inject_control_wedge`  (process-per-test — CI's path, always safe), or
//!   - `cargo test --test inject_control_wedge -- --test-threads=1`  (single-threaded fallback).
//!
//! Do NOT run a bare `cargo test --test inject_control_wedge` on Windows.

use std::sync::Arc;
use std::thread;
use std::time::{Duration, Instant};

use interprocess::local_socket::Stream;
use spt_daemon::codec::{read_frame, write_frame};
use spt_daemon::frame::{Envelope, Role};
use spt_daemon::msg::{
    encode_bytes, EndpointInputReq, SpawnReq, Spawned, KIND_ENDPOINT_INJECTED, KIND_ENDPOINT_INPUT,
    KIND_NET_STATUS, KIND_NET_STATUS_REPLY, KIND_SESSIONS, KIND_SESSIONS_REPLY, KIND_SPAWN,
    KIND_SPAWNED,
};
use spt_daemon::transport::{send_hello, LocalSocketTransport};
use spt_daemon::Broker;

// ── W1b additions (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE): the real-fixture attach
//    path, the Brain client API, and a net-backed broker for loopback attach. ──
use std::sync::atomic::{AtomicBool, AtomicU64};
use std::sync::OnceLock;
use spt_daemon::attach::{request_attach, serve_attach};
use spt_daemon::brain::{Brain, BrokerEvent};
use spt_daemon::msg::decode_bytes;
use spt_daemon::nethost::{NetConfig, NetHost};
use spt_net::net::attach::{AttachDecoder, AttachIntent, AttachRecord};
use spt_net::net::endpoint::{BindScope, LocalDiscovery, RelayPolicy};
use spt_proto::identity::Identity;

use std::sync::atomic::{AtomicU32, Ordering};
static SEQ: AtomicU32 = AtomicU32::new(0);
fn unique_name() -> String {
    let n = SEQ.fetch_add(1, Ordering::Relaxed);
    format!("spt-daemon-w1wedge-{}-{}.sock", std::process::id(), n)
}

/// The concurrent-attach gate watchdog (shared by `a_journaled` + `p0_paste`): the
/// hard wall-clock ceiling on the attacher's `result_rx.recv_timeout`. DEFAULT 30s —
/// tight enough that a REAL paste-wedge (which blocks FOREVER) still trips it, so the
/// gate keeps catching regressions locally / by default. CI exports
/// `SPT_ATTACH_GATE_WATCHDOG_MS=120000` ONLY on the shared loaded box, where cross-run
/// scheduler starvation can push the gate's sole elastic bound (`request_attach`'s
/// journaled `net_open_stream` ack) past 30s with NO product wedge — serviced-but-late
/// (ack <=3.8s even under pegged CPU; the 30–120s band is pure starvation noise).
/// Mirrors the `SPT_REGISTRY_BUSY_TIMEOUT_MS` env-knob precedent. The knob grants ack
/// wall-clock ONLY; it never relaxes the `subscribed` / `got_output` assertions.
fn attach_gate_watchdog() -> Duration {
    match std::env::var("SPT_ATTACH_GATE_WATCHDOG_MS") {
        Ok(ms) => ms
            .parse::<u64>()
            .map(Duration::from_millis)
            .unwrap_or(Duration::from_secs(30)),
        Err(_) => Duration::from_secs(30),
    }
}

fn kill_pid(pid: u32) {
    #[cfg(windows)]
    let _ = std::process::Command::new("taskkill")
        .args(["/PID", &pid.to_string(), "/F", "/T"])
        .output();
    #[cfg(unix)]
    let _ = std::process::Command::new("kill")
        .args(["-9", &pid.to_string()])
        .output();
}

/// Join a teardown helper thread, but NEVER ride a permanently-parked helper to
/// the nextest slow-timeout backstop (240s). On the FAIL/wedge path a helper can
/// be parked forever inside a blocking socket op — e.g. the driver's `send_effect`
/// on a wedged broker never returns to observe `stop` — so a bare `.join()` would
/// hang the whole test until the 240s SIGKILL, MASKING which failure it was. The
/// gate result (`subscribed`/`got_output`) is already captured by the 30s watchdog
/// BEFORE teardown, so bounding the join lets a real product wedge fail FAST on the
/// assertion (~watchdog time) instead of timing out. A timed-out helper is
/// abandoned — it dies at process exit, exactly like the KH-7.6 abandoned
/// pump-ipc-reader threads this file already relies on. Returns true iff it joined.
fn join_bounded(handle: thread::JoinHandle<()>, dur: Duration) -> bool {
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let _ = handle.join();
        let _ = tx.send(());
    });
    rx.recv_timeout(dur).is_ok()
}

/// Read `Broker::session_pid` under a bound. The pid is only used to best-effort
/// kill the flood child during teardown, but `session_pid` locks the broker's
/// `sessions` Mutex — if a wedged per-conn handler is holding that lock the read
/// blocks and rides teardown to the 240s backstop. Probe it on a detached thread
/// (the broker is an `Arc`, so the clone keeps it alive) and give up on timeout;
/// a missed child-kill is harmless, the process-per-test exit reaps it.
fn session_pid_bounded(broker: &Arc<Broker>, sid: u64, dur: Duration) -> Option<u32> {
    let broker = Arc::clone(broker);
    let (tx, rx) = std::sync::mpsc::channel();
    thread::spawn(move || {
        let _ = tx.send(broker.session_pid(sid));
    });
    rx.recv_timeout(dur).ok().flatten()
}

/// A child that FLOODS stdout forever (never reads stdin) — the heavy-output
/// case (a real TUI redraw burst). Cross-OS, same shape as the broker B1 test's
/// `cat`/`findstr` echo child.
fn flood_spawn_req(endpoint: &str) -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("yes".to_string(), vec!["FLOODFLOODFLOODFLOOD".to_string()]);
    #[cfg(windows)]
    let (program, args) = (
        "cmd".to_string(),
        vec![
            "/c".to_string(),
            "for /l %x in (0,0,1) do @echo FLOODFLOODFLOODFLOODFLOODFLOODFLOOD".to_string(),
        ],
    );
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: endpoint.to_string(),
        cwd: None,
        env: Default::default(),
        translation_binary: None,
    }
}

/// A QUIET, long-lived child for the w5_a2 IDLE characterization (doyle gate
/// ruling, Option B): it produces NO output and does NOT exit, so the session is
/// GENUINELY idle for the read window. (`flood_spawn_req` floods through the read,
/// which makes b4's full-speed drain hit the dead-operator write-fail →
/// Disconnected-evict → controller_by clears — the rig bug that made w5_a2 a false
/// red. A silent child never feeds `controller_writer`, so the abandoned-controller
/// slot is never cleared via the output path, faithfully staging the IDLE case the
/// test characterizes. The TRUE idle-evict is deferred to
/// REQ-HAZARD-DRIVEN-BY-IDLE-REMOTE-EVICT — the D4c presence oracle.) Unix `sleep`
/// is silent + long-lived; Windows `ping -n` with output redirected to NUL via
/// `cmd /c` is the silent long-lived analog (`timeout`/bare `ping` print a
/// countdown/replies that WOULD feed the drain).
fn quiet_spawn_req(endpoint: &str) -> SpawnReq {
    #[cfg(unix)]
    let (program, args) = ("sleep".to_string(), vec!["600".to_string()]);
    #[cfg(windows)]
    let (program, args) = (
        "cmd".to_string(),
        vec!["/c".to_string(), "ping -n 600 127.0.0.1 >nul".to_string()],
    );
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: endpoint.to_string(),
        cwd: None,
        env: Default::default(),
        translation_binary: None,
    }
}

fn send(conn: &mut Stream, kind: &str, payload: serde_json::Value) {
    write_frame(conn, &Envelope::new(kind, payload)).expect("send frame");
}

/// Open a fresh brain connection, send one command, and return whether the
/// expected reply kind arrives within `deadline`. Runs the whole exchange on a
/// spawned thread so a BLOCKED `read_frame` (the wedge) surfaces as a timeout
/// instead of hanging the test. A `false` return = "did not answer in time".
fn probe(name: &str, cmd_kind: &'static str, want_kind: &'static str, deadline: Duration) -> bool {
    let name = name.to_string();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut conn = match LocalSocketTransport::connect(&name) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut conn, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(&mut conn, cmd_kind, serde_json::json!({}));
        loop {
            match read_frame(&mut conn) {
                Ok(f) if f.kind == want_kind => {
                    let _ = tx.send(true);
                    return;
                }
                Ok(_) => continue, // ignore interleaved frames
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    rx.recv_timeout(deadline).unwrap_or(false)
}

#[test]
fn a_backed_up_controller_does_not_wedge_the_session() {
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");
    let serve_broker = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve_broker.serve(); // accept every connection on its own thread
    });

    // ── Controller connection A: spawn the flooding child. dispatch_spawn
    //    pre-attaches A as the CONTROLLER (become_controller, by=None). ──
    let mut a = None;
    for _ in 0..200 {
        match LocalSocketTransport::connect(&name) {
            Ok(c) => {
                a = Some(c);
                break;
            }
            Err(_) => thread::sleep(Duration::from_millis(10)),
        }
    }
    let mut a = a.expect("connect controller A");
    send_hello(&mut a, Role::Brain).expect("hello A");
    send(
        &mut a,
        KIND_SPAWN,
        serde_json::to_value(flood_spawn_req("wedge-ep")).unwrap(),
    );
    let sid = loop {
        let f = read_frame(&mut a).expect("frame before spawned");
        if f.kind == KIND_SPAWNED {
            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
        }
    };

    // ── A now STOPS reading. The child floods → A's socket recv buffer fills →
    //    the controller writer thread blocks on it → the controller channel fills
    //    → the drain's bounded send_timeout starts blocking. PRE-FIX this parked
    //    the drain inside append HOLDING the log lock (permanent wedge). POST-FIX
    //    the send is OFF the log lock, so the session must stay responsive even
    //    while the controller is fully backed up. Probe DURING the stall (before
    //    CONTROLLER_WRITE_DEADLINE = 5s elapses). ──
    thread::sleep(Duration::from_secs(3));

    // A fresh connection's KIND_NET_STATUS touches neither the sessions nor the
    // log lock (proves the process is alive). KIND_SESSIONS enumerates every
    // session's log (`delivered_through`) under each log lock — PRE-FIX it hung on
    // the parked session's held lock; POST-FIX it answers because the drain no
    // longer holds the lock during the controller send.
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));
    let sessions_answered =
        probe(&name, KIND_SESSIONS, KIND_SESSIONS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== W1 GATE: broker_alive(net_status)={broker_alive} \
         sessions_answered={sessions_answered} (fixed = both true; pre-fix sessions=false) ==="
    );

    // Reap the flood child (scoped) + tear the broker down.
    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }
    drop(a);

    assert!(
        broker_alive,
        "the broker PROCESS must answer a fresh connection's net-status"
    );
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    assert!(
        sessions_answered,
        "a fully-backed-up controller must NOT wedge the session: KIND_SESSIONS \
         (which reads each log's delivered_through under the log lock) must still \
         answer because the controller write is delivered OFF the log lock on a \
         dedicated writer thread (W1 fix). Pre-fix this hung — the inline \
         controller write parked the drain holding the log lock."
    );
}

#[test]
fn injecting_a_large_payload_into_a_stdin_ignoring_child() {
    // Park (b) probe: write_input's blocking write_all on a full PTY input buffer.
    // PLATFORM-DEPENDENT (recorded inline at the assertions below): Windows ConPTY
    // ABSORBS the inject (no park, benign — the operator platform); Unix forkpty's
    // small kernel input queue is expected to park (real there).
    let name = unique_name();
    let broker = Broker::bind(&name).expect("bind broker");
    let serve_broker = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve_broker.serve();
    });

    // Controller A spawns the flood child (which IGNORES stdin) and then DRAINS
    // its frames forever on a background thread — so the output-side park (a)
    // does NOT fire and we isolate the input-side park (b).
    let mut a = None;
    for _ in 0..200 {
        match LocalSocketTransport::connect(&name) {
            Ok(c) => {
                a = Some(c);
                break;
            }
            Err(_) => thread::sleep(Duration::from_millis(10)),
        }
    }
    let mut a = a.expect("connect controller A");
    send_hello(&mut a, Role::Brain).expect("hello A");
    send(
        &mut a,
        KIND_SPAWN,
        serde_json::to_value(flood_spawn_req("inj-ep")).unwrap(),
    );
    let sid = loop {
        let f = read_frame(&mut a).expect("frame before spawned");
        if f.kind == KIND_SPAWNED {
            break serde_json::from_value::<Spawned>(f.payload).unwrap().session_id;
        }
    };
    // Keep the controller socket drained so append never parks.
    thread::spawn(move || while read_frame(&mut a).is_ok() {});

    // Inject a LARGE payload into the stdin-ignoring child via a SECOND brain
    // connection. dispatch_endpoint_input → write_input → write_all on the PTY
    // master; the child never reads stdin, so the input buffer fills and the
    // write parks — the KIND_ENDPOINT_INJECTED reply never comes.
    let big = vec![b'K'; 4 * 1024 * 1024]; // 4 MiB of keystrokes
    let name_inj = name.clone();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut b = match LocalSocketTransport::connect(&name_inj) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut b, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(
            &mut b,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: "inj-ep".to_string(),
                data_b64: encode_bytes(&big),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut b) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    let _ = tx.send(true);
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    let inject_acked = rx.recv_timeout(Duration::from_secs(8)).unwrap_or(false);

    // Broker process still alive on a fresh connection (the park is the dispatch
    // thread serving the inject, not the whole process).
    let broker_alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    eprintln!(
        "=== W1 PARK-B DIAGNOSTIC: inject_acked={inject_acked} broker_alive={broker_alive} \
         (expect inject_acked=false = write_input parked on write_all) ==="
    );

    if let Some(pid) = broker.session_pid(sid) {
        kill_pid(pid);
    }

    assert!(broker_alive, "broker process must stay alive (per-session park)");

    // PLATFORM-DEPENDENT FINDING (W1 repro, 2026-06-19):
    // - Windows ConPTY ABSORBS a large inject into a stdin-ignoring child without
    //   blocking write_all — `inject_acked` is true, park (b) does NOT fire. So on
    //   the operator's platform the input-side write is BENIGN; the operator wedge
    //   is park (a) (controller output delivery), reached on injection via the
    //   injection-INDUCED output backing up the controller — not the inject write.
    // - Unix forkpty has a small kernel input queue, so the same inject is
    //   HYPOTHESIZED to park write_input's write_all (park b). That is UNVERIFIED
    //   (no Linux run yet) and absorbing would be BENIGN, not a regression — so we
    //   do NOT assert it (asserting a maybe-wrong bug whose failure mode is good =
    //   a false-red). cfg(unix) only CAPTURES + prints the observation; gravity-linux
    //   CI is the observation venue. W2 converts it to a real pre-fix repro assert
    //   (and bounds the park) once CI actually shows it. The only hard invariant
    //   here is broker liveness (asserted above for both platforms).
    // [int->REQ-HAZARD-INJECT-CONTROL-COEXIST]
    #[cfg(windows)]
    assert!(
        inject_acked,
        "Windows ConPTY is expected to ABSORB the inject (park b benign here); \
         if this flips, the input buffer began blocking — re-examine park (b)."
    );
    #[cfg(unix)]
    eprintln!(
        "=== W1 PARK-B (Unix forkpty observation, not asserted): inject_acked={inject_acked} \
         (absorbed=true is benign; park=false would be the hypothesized forkpty park) ==="
    );
}

// ── v0.13.0 W1b — REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE, the facet-B real-fixture
//    gate W1 LACKED ────────────────────────────────────────────────────────────
//
// The hazard: `EffectJournal::apply_once` holds the GLOBAL journal lock across the
// effect closure. A journaled PtyWrite (every operator keystroke carrying an
// op_id) runs `write_input` INSIDE that lock. If a non-draining child's PTY input
// buffer is full, `write_input` BLOCKS — holding the journal lock forever. So the
// single-threaded dispatch can no longer service ANY journaled effect, and a
// concurrent `spt rc` attach dies with "brain IPC read deadline elapsed" (facet
// B). W1's gate only checked KIND_SESSIONS liveness; this gate uses a REAL
// rc-attach client (request_attach / serve_attach over a real loopback conn + a
// real PTY child) and asserts the attach is actually SERVICED and RECEIVES output.
//
// PLATFORM (mirrors the park-(b) finding above): Windows ConPTY ABSORBS sustained
// input without blocking write_input, so the journal lock is not held long → the
// wedge is benign here; this test then asserts the POSITIVE W1 lacked (liveness +
// real byte receipt + attach serviced). On cfg(unix) (gravity-linux) forkpty's
// small input queue parks write_input → the journal lock is held → pre-fix the
// concurrent attach wedges; post-fix (effect off the lock) it is serviced. All
// waits are behind generous deadlines + watchdog channels so nothing hangs CI.

/// Hold the temp home for the whole test binary (serve_attach best-effort stamps
/// the endpoint perch's driven_by marker — that resolution must never touch a
/// real home).
static WEDGE_HOME: OnceLock<tempfile::TempDir> = OnceLock::new();
fn init_wedge_home() {
    WEDGE_HOME.get_or_init(|| {
        let dir = tempfile::TempDir::new().expect("temp dir");
        std::env::set_var("SPT_HOME", dir.path());
        dir
    });
}

fn hermetic() -> NetConfig {
    NetConfig {
        identity: Identity::generate(),
        relay: RelayPolicy::Disabled,
        discovery: LocalDiscovery::Off,
        scope: BindScope::Loopback,
        membership: None,
    }
}

fn net_broker(name: &str, dir: &std::path::Path) -> Arc<Broker> {
    let host = NetHost::start(hermetic()).expect("net host start");
    let broker =
        Broker::bind_in_with_net(name, dir.join("effects.log"), Some(host)).expect("bind broker");
    let serve = Arc::clone(&broker);
    thread::spawn(move || {
        let _ = serve.serve();
    });
    broker
}

fn connect_retry(name: &str) -> Brain {
    for _ in 0..300 {
        if let Ok(b) = Brain::cold_start(name, 1) {
            return b;
        }
        thread::sleep(Duration::from_millis(10));
    }
    panic!("brain could not connect");
}

fn count(hay: &[u8], needle: &[u8]) -> usize {
    if needle.is_empty() || hay.len() < needle.len() {
        return 0;
    }
    let (mut n, mut i) = (0, 0);
    while i + needle.len() <= hay.len() {
        if &hay[i..i + needle.len()] == needle {
            n += 1;
            i += needle.len();
        } else {
            i += 1;
        }
    }
    n
}

/// Poll a brain's net-streams for a PEER-initiated stream (the inbound attach the
/// target serves) — return (stream_id, origin). Watchdog'd by the poll count.
fn wait_for_stream(brain: &mut Brain) -> Option<(u64, String)> {
    for _ in 0..400 {
        let reply = brain.net_streams().expect("net-streams");
        if let Some(s) = reply.streams.iter().find(|s| !s.initiated_locally) {
            return Some((s.stream_id, s.remote_id_hex.clone()));
        }
        thread::sleep(Duration::from_millis(5));
    }
    None
}

#[test]
fn a_journaled_input_wedge_does_not_starve_a_concurrent_rc_attach() {
    init_wedge_home();
    let dir = tempfile::tempdir().expect("tempdir");
    let name = unique_name();
    let _broker = net_broker(&name, &dir.path().join("wedge"));

    // ── A REAL flooding child that IGNORES stdin (its PTY input buffer fills). ──
    let mut spawner = connect_retry(&name);
    let sid = spawner
        .spawn_session(flood_spawn_req("wedge-jrnl-ep"))
        .expect("spawn flood child");

    // ── The WEDGE driver: on its OWN connection, pump journaled input
    //    (op_id-carrying send_effect → broker dispatch_input → journal.apply_once
    //    runs write_input INSIDE the global journal lock). On Unix the full input
    //    buffer parks write_input, holding the lock; on Windows ConPTY absorbs it.
    //    The driver fires a BOUNDED burst of MODEST-sized ops (so a backed-up
    //    broker IPC never blocks the driver's own socket write — the burst always
    //    completes and the thread exits cleanly, never hanging the test process)
    //    and then idles, checking the stop flag. ──
    let stop = Arc::new(AtomicBool::new(false));
    let pumped = Arc::new(AtomicU64::new(0));
    let driver_name = name.clone();
    let driver_stop = Arc::clone(&stop);
    let driver_pumped = Arc::clone(&pumped);
    let driver = thread::spawn(move || {
        // PUMP carrier (cold_start_pump): the reader thread drains the driver's
        // OWN incoming frames (KIND_APPLIED acks + session output) so the broker's
        // reply side never backs up and blocks reading our input — which would
        // deadlock send_effect on a plain Whole conn (observed: it stalled after a
        // few ops). The pump keeps the driver's socket healthy so it can keep a
        // journaled write in-flight without ever wedging the CLIENT.
        let mut w = match Brain::cold_start_pump(&driver_name, 1, Duration::from_secs(2)) {
            Ok(b) => b,
            Err(_) => return,
        };
        // Bind the driver brain to the same session so send_effect has a target.
        if w.attach(sid, 0).is_err() {
            return;
        }
        let chunk = vec![b'W'; 16 * 1024];
        let mut op = 1u64;
        // Sustain journaled input: keep ops in flight (and, on Unix, parked under
        // the journal lock) until teardown. Each send_effect writes a frame; the
        // pump reader drains replies concurrently so the write never deadlocks.
        while !driver_stop.load(std::sync::atomic::Ordering::Relaxed) {
            if w.send_effect(op, &chunk).is_err() {
                break;
            }
            driver_pumped.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
            op += 1;
            // Drain queued replies (acks/output) so the carrier stays healthy, then
            // PACE with an explicit floor sleep. The doc above says "sustain a write
            // in-flight", but the bare `while !stop` pumped ~6094 ops and pegged the
            // core, out-competing the SIBLING attacher thread for scheduling under
            // contention so its subscribe was never serviced in 30s (the kitsubito
            // reliability victim, subscribed=false). A small sleep keeps a journaled
            // write continuously in-flight (facet-B's load is intact) while yielding
            // the CPU the attacher needs — sustained, not storming.
            let _ = w.read_event_until(Some(std::time::Instant::now() + Duration::from_millis(20)));
            // Unix-only: the CPU storm is a forkpty problem. On Windows ConPTY
            // ABSORBS the journaled write, so there is no storm to pace and the sleep
            // only adds a timing variable (p0 flaked on the SHARED Windows runner) —
            // skip it there; Windows p0 was green pre-pacing.
            #[cfg(unix)]
            std::thread::sleep(Duration::from_millis(5));
        }
    });

    // Give the driver time to get a journaled write in-flight (and, on Unix, to
    // park write_input under the journal lock).
    thread::sleep(Duration::from_secs(2));

    // ── The CONCURRENT `spt rc` attach: a SEPARATE operator connection requests a
    //    real attach over a loopback conn, subscribes, and (via serve_attach)
    //    receives the child's output. Run on its own thread so the OUTER
    //    recv_timeout watchdog bounds the whole gate — a wedged subscribe (facet B,
    //    the "brain IPC read deadline elapsed" symptom) surfaces as a clean test
    //    failure, never a hang. ──
    let (result_tx, result_rx) = std::sync::mpsc::channel::<(bool, bool)>();
    let attach_name = name.clone();
    let attacher = thread::spawn(move || {
        // PUMP mode (Split) so the got_output loop's `read_event_until(now+250ms)`
        // actually HONORS its slice deadline — a plain `Whole` cold_start IGNORES it
        // (read_frame blocks; see output_contains_within). With a Whole conn the wedge
        // (no NetStreamData ever lands) PARKS read_frame forever: the 8s got_output
        // deadline is never re-evaluated, result_tx.send never runs, and the 30s
        // watchdog yields (false,false) — so `subscribed=false` was a MEASUREMENT
        // ARTIFACT, not a subscribe failure (the broker registers the subscribe fast,
        // lock-free; gdb showed the attacher parked PAST net_stream_subscribe). Pump
        // mode makes the slices real, so the gate reports the TRUE signal.
        let mut operator = match Brain::cold_start_pump(&attach_name, 1, Duration::from_secs(2)) {
            Ok(b) => b,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        let dialed = match operator.net_dial_loopback() {
            Ok(d) => d,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        let own_origin = dialed.remote_id_hex.clone();
        let stream_b = match request_attach(
            &mut operator,
            dialed.conn_id,
            sid,
            0,
            2,
            AttachIntent::Viewer, // a read-only rc --view: receives output, no journaled input
        ) {
            Ok(s) => s,
            Err(_) => {
                let _ = result_tx.send((false, false));
                return;
            }
        };
        // The subscribe reply is the first servicing proof: a wedged dispatch
        // never answers it; the outer recv_timeout watchdog then fails the gate.
        let subscribed = operator.net_stream_subscribe(stream_b, 0).is_ok();

        // The target serves the inbound attach stream (the real serve_attach pump).
        let mut target = connect_retry(&attach_name);
        let (stream_a, origin) = match wait_for_stream(&mut target) {
            Some(s) => s,
            None => {
                let _ = result_tx.send((subscribed, false));
                return;
            }
        };
        assert_eq!(origin, own_origin, "loopback origin is this node's own id");
        let serve_broker = attach_name.clone();
        let server = thread::spawn(move || {
            let _ = serve_attach(&mut target, &serve_broker, stream_a, "wedge-jrnl-ep", &origin, 0);
        });

        // The positive W1 LACKED: a real rc-client attach actually RECEIVES PTY
        // output bytes from the flooding child. Output delivery does NOT go through
        // the journal lock, so receiving bytes proves the dispatch serviced this
        // attach despite the wedged journaled input.
        let mut decoder = AttachDecoder::new();
        let mut got_output = false;
        let deadline = std::time::Instant::now() + Duration::from_secs(8);
        while std::time::Instant::now() < deadline {
            match operator.read_event_until(Some(
                std::time::Instant::now() + Duration::from_millis(250),
            )) {
                Ok(BrokerEvent::NetStreamData { stream_id, bytes, .. }) if stream_id == stream_b => {
                    for rec in decoder.push(&bytes) {
                        if let AttachRecord::Output { data_b64, .. } = rec {
                            let chunk = decode_bytes(&data_b64).unwrap_or_default();
                            if count(&chunk, b"FLOOD") >= 1 {
                                got_output = true;
                            }
                        }
                    }
                    if got_output {
                        break;
                    }
                }
                Ok(_) => continue,
                Err(_) => continue, // slice timeout — keep waiting until the deadline
            }
        }

        // Report the result IMMEDIATELY (the main thread's watchdog can proceed),
        // THEN best-effort detach + drain. We deliberately do NOT block the result
        // on the serve thread: a controller's `serve_attach` forward can block on a
        // non-draining operator socket while the flood child keeps producing — the
        // main thread kills the child to unblock it. Reporting first then draining
        // keeps the watchdog free of that wedge.
        let _ = result_tx.send((subscribed, got_output));
        let _ = operator.net_stream_send(stream_b, &[], None, true);
        // Keep draining the operator so serve's forwards drain and serve_attach can
        // observe our detach + the child's eventual death and return cleanly.
        let drain_deadline = std::time::Instant::now() + Duration::from_secs(12);
        while std::time::Instant::now() < drain_deadline {
            match operator
                .read_event_until(Some(std::time::Instant::now() + Duration::from_millis(200)))
            {
                Ok(BrokerEvent::NetStreamEof { .. }) => break,
                Ok(_) => continue,
                Err(_) => continue,
            }
        }
        let _ = server.join();
    });

    // Watchdog: a hard wedge surfaces as a recv timeout (a test FAILURE), never a
    // hang. The attacher reports its result before any blocking teardown, so this
    // recv is the hard ceiling on the whole gate. Deadline is env-tunable
    // (attach_gate_watchdog: default 30s tight, CI-generous): a real wedge blocks
    // FOREVER so it trips any finite deadline — the knob only grants headroom for a
    // serviced-but-late request_attach ack under CI scheduler starvation.
    let watchdog = attach_gate_watchdog();
    let recv_t0 = Instant::now();
    let (subscribed, got_output) = result_rx
        .recv_timeout(watchdog)
        .unwrap_or((false, false));
    let recv_elapsed = recv_t0.elapsed();
    // Source-attribution (cheap, load-bearing triage): `channel` = the attacher
    // reported; `WATCHDOG-DEFAULT` = the gate's elastic bound (request_attach's
    // journaled ack) outran the watchdog. Threshold derived from the CONFIGURED
    // watchdog (>= watchdog-1s) so the label stays truthful at 30s OR 120s.
    let source = if recv_elapsed >= watchdog.saturating_sub(Duration::from_secs(1)) {
        "WATCHDOG-DEFAULT"
    } else {
        "channel"
    };
    eprintln!(
        "JRNL_FLAKE: MAIN recv -> ({subscribed},{got_output}) after {recv_elapsed:?} (source={source})"
    );

    // Tear down so nothing leaks AND so any backpressured helper thread unblocks:
    // kill the flood child FIRST (ends the output backing up serve's forwards),
    // then signal the driver to stop. The helper threads' own bounded drains let
    // them exit; we never block the GATE/assertions on a wedged thread.
    if let Some(pid) = session_pid_bounded(&_broker, sid, Duration::from_secs(5)) {
        kill_pid(pid);
    }
    stop.store(true, std::sync::atomic::Ordering::Relaxed);
    // Bound the teardown joins: on a REAL facet-B wedge the driver stays parked in
    // `send_effect` and the attacher in its subscribe — neither observes `stop`, so
    // a bare `.join()` would ride to the 240s nextest backstop and MASK the verdict
    // (the symptom doyle's kitsubito reliability run hit). The gate result is already
    // captured by the 30s watchdog above, so abandon a parked helper and let the
    // assertions fire fast (a true wedge then fails on `subscribed=false` in ~30s).
    let _ = join_bounded(driver, Duration::from_secs(8));
    let _ = join_bounded(attacher, Duration::from_secs(8));
    let pumped_n = pumped.load(std::sync::atomic::Ordering::Relaxed);

    // On Unix, fold the W1-missing park-(b) liveness probe HERE (before tearing the
    // broker down — the probe needs a live broker). On Windows the journaled write
    // is absorbed, so there is no park to assert liveness against.
    #[cfg(unix)]
    let alive = probe(&name, KIND_NET_STATUS, KIND_NET_STATUS_REPLY, Duration::from_secs(5));

    // ── PROCESS-CLEAN-EXIT teardown (test-robustness, not the gate). ──
    // Each `cold_start_pump` Brain (the driver's `w`, the attacher's `operator`)
    // spawns a DETACHED `pump-ipc-reader` thread parked in a BLOCKING named-pipe
    // `read_frame` on its RecvHalf. Joining the helper threads drops those Brains,
    // which drops each Brain's SendHalf → the broker's per-conn handler read errors
    // → the broker closes its end → the client RecvHalf read finally errors → the
    // reader thread exits. But that close CHAIN is asynchronous and the reader's
    // JoinHandle is only dropped, never joined (it is abandoned by design — KH 7.6).
    // On Windows, if the test returns before that chain completes, a reader thread
    // is still parked inside a named-pipe read syscall at process teardown, and a
    // thread blocked in that syscall on a half-closed handle aborts the WHOLE
    // process with exit 0xffffffff (the flaky ~50% abort under `--test-threads=1`,
    // where the next test / process exit races the chain). FIX: after the Brains
    // are dropped (helpers joined), settle briefly so the close chain completes and
    // every abandoned reader observes its read error and exits before the harness
    // moves on. Verified by `PUMP_IPC_READER: exited` printing within this window.
    // This is purely teardown hygiene; the assertions below are unchanged.
    thread::sleep(Duration::from_millis(750));

    eprintln!(
        "=== W1b JOURNAL-WEDGE GATE: journaled_ops_pumped={pumped_n} \
         concurrent_attach_subscribed={subscribed} attach_received_pty_output={got_output} \
         (fixed = both true; pre-fix on Unix: subscribed=false — attach wedged on the \
         held journal lock) ==="
    );

    // [int->REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE]
    // FACET B — the gate W1 lacked: a concurrent rc attach must be SERVICED even
    // while a journaled input effect is in-flight (and, on Unix, blocked holding
    // the journal lock). Pre-fix on a forkpty platform this subscribe wedges
    // ("brain IPC read deadline elapsed"); post-fix the effect runs off the lock.
    assert!(
        subscribed,
        "a concurrent `spt rc` attach must be SERVICED while a journaled PtyWrite \
         is in-flight: the attach's subscribe reply did not arrive within the \
         watchdog deadline — the journal lock is held across write_input (facet B, \
         REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE)."
    );
    // POSITIVE (W1 lacked): a real rc-client attach RECEIVES PTY output bytes —
    // the b4 drain-decouple keeps the viewer fed despite controller backpressure,
    // and (post-keystone) the viewer SNAPS the pre-eviction ring-roll forward gap
    // instead of fataling on it, so the FLOOD bytes actually render.
    // [int->REQ-HAZARD-VIEWER-STARVE-UNDER-CONTROLLER-BACKPRESSURE]
    // [int->REQ-HAZARD-VIEWER-RING-ROLL-SNAP]
    assert!(
        got_output,
        "the concurrent attach must actually RECEIVE PTY output from the child \
         (not just liveness): no FLOOD bytes rendered over the real attach pump."
    );

    // On Unix, ALSO fold the W1-missing park-(b) hard assertion: the broker PROCESS
    // must stay alive on a fresh connection even with the journaled write parked
    // (the park is per-effect under the lock, not a whole-process death). The probe
    // ran above (while the broker was still alive); assert its result here.
    #[cfg(unix)]
    assert!(
        alive,
        "broker process must stay alive while a journaled input is parked (Unix forkpty)"
    );
}

// ── v0.13.0 W2 Layer G — REQ-MSG-IDLE-TRANSLATION-BINARY: the ADR-0022
//    translation-binary idle-delivery INTEGRATION gate, on a REAL broker + REAL
//    PTY + a REAL spawned translation-binary fixture (NO mocks of the PTY, the
//    transport, or the binary). This is a doyle gate point — every assertion is
//    STRUCTURAL (a byte-receipt file, a serviceability probe, a stdin-log content
//    check, a pid-gone check), never on wall-clock / fsync timing. ──────────────
//
// The REAL translation binary fixture (auto-discovered `src/bin/*.rs`, referenced
// by the cargo `CARGO_BIN_EXE_*` env var):
//   - `xlate_choreo_fixture` — a pure stdin→stdout JSON-lines filter. On an
//     `{type:event}` it emits the CC stash/restore choreography (`ctrl+s` `delay`
//     `text:XLATE_OK` `enter` `delay` `ctrl+s` `commit`); env
//     `XLATE_FIXTURE_MODE=nocommit` drops the final `{commit}` to exercise the
//     broker's commit-deadline FAULT. It appends every stdin LINE it receives to
//     `XLATE_FIXTURE_STDIN_LOG` (the privacy / content-free-ping assertion reads
//     that file).
//
// The REAL PTY CHILD is the proven cross-OS ECHO child the existing attach tests
// use (`findstr "."` on Windows / `cat` on Unix): it echoes its stdin (the
// injected keystrokes + the operator's flushed input) back to stdout LINE BY LINE.
// Byte-receipt is therefore observed STRUCTURALLY through the broker's REAL output
// ring over a REAL viewer attach — not a mock, not a side file. To dodge the
// `ctrl+s` (0x13 = XOFF) echo-stall concern, every marker the test wants to OBSERVE
// is terminated with `\r` (Enter) so the line-buffered echo child flushes that line
// regardless of a preceding control byte; the choreography's own `enter` does the
// same for `XLATE_OK`.

const XLATE_BIN: &str = env!("CARGO_BIN_EXE_xlate_choreo_fixture");

/// The proven cross-OS echo PTY child (stdin → stdout, line-buffered).
fn echo_program() -> (String, Vec<String>) {
    #[cfg(unix)]
    {
        ("cat".to_string(), Vec::<String>::new())
    }
    #[cfg(windows)]
    {
        ("findstr".to_string(), vec![".".to_string()])
    }
}

/// A translation-bearing spawn: the PTY child is the real echo child (so the
/// injected/operator bytes echo back through the broker ring), and the
/// `translation_binary` is the real `xlate_choreo_fixture`. The fixture env vars
/// must already be set in THIS process (set_var before the session spawns the
/// child — clean under nextest's process-per-test).
fn xlate_spawn_req(endpoint: &str) -> SpawnReq {
    let (program, args) = echo_program();
    SpawnReq {
        program,
        args,
        rows: 24,
        cols: 80,
        endpoint: endpoint.to_string(),
        cwd: None,
        env: Default::default(),
        translation_binary: Some(XLATE_BIN.to_string()),
    }
}

/// Attach a viewer brain to a session and accumulate its output bytes until either
/// `needle` appears (→ true) or `deadline` elapses (→ false). The structural
/// byte-receipt seam: bytes that reach the PTY are echoed by the child and arrive
/// here over the REAL broker output ring. Drains on its own connection so the
/// caller's controller socket is untouched.
fn output_contains_within(name: &str, sid: u64, needle: &[u8], deadline: Duration) -> bool {
    // PUMP mode (Split) so `read_event_until` actually HONORS the deadline — a
    // plain `Whole` cold_start ignores it (read_frame blocks), which would hang
    // forever if the PTY produced no output (e.g. an XOFF-stalled terminal).
    let mut viewer = match Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
        Ok(b) => b,
        Err(_) => return false,
    };
    if viewer.attach(sid, 0).is_err() {
        return false;
    }
    let mut acc: Vec<u8> = Vec::new();
    let stop = Instant::now() + deadline;
    while Instant::now() < stop {
        match viewer.read_event_until(Some(Instant::now() + Duration::from_millis(150))) {
            Ok(BrokerEvent::Output { bytes, .. }) => {
                acc.extend_from_slice(&bytes);
                if count(&acc, needle) >= 1 {
                    return true;
                }
            }
            Ok(_) => continue,
            Err(_) => continue,
        }
    }
    false
}

/// Observe `needle` in a session's output ring while interleaving OPERATOR XON
/// (`ctrl+q` = 0x11) keystrokes to RESUME any XOFF-paused output (the choreography's
/// `ctrl+s`/0x13 pauses this ConPTY's output flow, stranding the echoed payload).
/// When `type_needle` is set, the helper ALSO types `needle\r` itself (for the
/// fresh-attach serviceability marker the operator originates); for the injected
/// payload it is `false` (we must NOT type the payload — that would be a false pass;
/// we only un-stick the flow so the BINARY-injected payload's echo can flush).
/// Pump mode → bounded reads. Returns true once `needle` appears.
fn observe_with_xon(
    name: &str,
    sid: u64,
    needle: &[u8],
    type_needle: bool,
    deadline: Duration,
) -> bool {
    let mut op = match Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
        Ok(b) => b,
        Err(_) => return false,
    };
    if op.attach(sid, 0).is_err() {
        return false;
    }
    let mut acc: Vec<u8> = Vec::new();
    let stop = Instant::now() + deadline;
    while Instant::now() < stop {
        // XON pulse to resume paused output; optionally re-type the operator marker.
        let _ = op.send_input(b"\x11");
        if type_needle {
            let mut line = needle.to_vec();
            line.push(b'\r');
            let _ = op.send_input(&line);
        }
        // Drain a slice of output, accumulating, before the next pulse.
        let slice = Instant::now() + Duration::from_millis(250);
        while Instant::now() < slice {
            match op.read_event_until(Some(Instant::now() + Duration::from_millis(120))) {
                Ok(BrokerEvent::Output { bytes, .. }) => {
                    acc.extend_from_slice(&bytes);
                    if count(&acc, needle) >= 1 {
                        return true;
                    }
                }
                Ok(_) => continue,
                Err(_) => break,
            }
        }
    }
    false
}

/// Deliver an inbound `<EVENT>` to a hosted endpoint via a raw `KIND_ENDPOINT_INPUT`
/// frame on a fresh brain connection (the Brain client has no endpoint-input
/// helper), and return whether the broker acked `delivered=true` within `deadline`.
/// Runs the exchange on a thread so a wedged reply surfaces as a timeout, not a hang.
fn deliver_endpoint_event(name: &str, endpoint: &str, envelope: &[u8], deadline: Duration) -> bool {
    let name = name.to_string();
    let endpoint = endpoint.to_string();
    let envelope = envelope.to_vec();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    thread::spawn(move || {
        let mut conn = match LocalSocketTransport::connect(&name) {
            Ok(c) => c,
            Err(_) => {
                let _ = tx.send(false);
                return;
            }
        };
        if send_hello(&mut conn, Role::Brain).is_err() {
            let _ = tx.send(false);
            return;
        }
        send(
            &mut conn,
            KIND_ENDPOINT_INPUT,
            serde_json::to_value(EndpointInputReq {
                endpoint: endpoint.clone(),
                data_b64: encode_bytes(&envelope),
            })
            .unwrap(),
        );
        loop {
            match read_frame(&mut conn) {
                Ok(f) if f.kind == KIND_ENDPOINT_INJECTED => {
                    // The reply carries `delivered`; surface it.
                    let delivered = f
                        .payload
                        .get("delivered")
                        .and_then(|d| d.as_bool())
                        .unwrap_or(false);
                    let _ = tx.send(delivered);
                    return;
                }
                Ok(_) => continue,
                Err(_) => {
                    let _ = tx.send(false);
                    return;
                }
            }
        }
    });
    rx.recv_timeout(deadline).unwrap_or(false)
}

/// Whether a pid is still a live process (the no-zombie reap check). Best-effort
/// and cross-OS: a `tasklist` query on Windows, `kill -0` on Unix.
#[cfg(windows)]
fn pid_alive(pid: u32) -> bool {
    let out = std::process::Command::new("tasklist")
        .args(["/FI", &format!("PID eq {pid}"), "/NH"])
        .output();
    match out {
        Ok(o) => {
            let s = String::from_utf8_lossy(&o.stdout);
            // tasklist prints the row when the pid is live; "No tasks" otherwise.
            s.contains(&pid.to_string())
        }
        Err(_) => false,
    }
}
#[cfg(unix)]
fn pid_alive(pid: u32) -> bool {
    std::process::Command::new("kill")
        .args(["-0", &pid.to_string()])
        .status()
        .map(|s| s.success())
        .unwrap_or(false)
}

/// Spawn the translation-bearing session on a fresh net-broker and return the
/// (broker, name, sid, controller-brain). The controller brain stays attached as
/// the live operator (the W1 coexistence partner) and is RETURNED so the test
/// keeps it alive + can probe it; the caller drains it as needed.
fn spawn_xlate_session(
    name: &str,
    dir: &std::path::Path,
    endpoint: &str,
) -> (Arc<Broker>, u64, Brain) {
    let broker = net_broker(name, dir);
    // PUMP mode so the spawner-controller's reads are BOUNDED (a Whole conn ignores
    // the read deadline → a drainer loop would block forever and never observe a
    // stop flag, hanging the test on join). The pump reader drains the controller's
    // socket so it stays serviceable without the test blocking.
    let mut spawner = {
        let mut b = None;
        for _ in 0..300 {
            if let Ok(c) = Brain::cold_start_pump(name, 1, Duration::from_secs(2)) {
                b = Some(c);
                break;
            }
            thread::sleep(Duration::from_millis(10));
        }
        b.expect("brain could not connect (pump)")
    };
    let sid = spawner
        .spawn_session(xlate_spawn_req(endpoint))
        .expect("spawn translation-bearing session");
    (broker, sid, spawner)
}