1	//! `mock-session` — the generic mock adapter's "agent session". A trivial,
2	//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
3	//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
4	//! `[session.self]` template, it does what a real session's boot hook would do:
5	//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
6	//!
7	//! It shells out to the `spt` binary exactly as a real adapter would — resolved
8	//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
9	//! the inbound contract is harness-agnostic: a dumb helper drives it with only
10	//! `spt api` calls.
11
12	use std::process::Command;
13
14	fn arg(args: &[String], flag: &str) -> Option<String> {
15	    args.iter()
16	        .position(|a| a == flag)
17	        .and_then(|i| args.get(i + 1).cloned())
18	}
19
20	fn main() {
21	    let args: Vec<String> = std::env::args().skip(1).collect();
22
23	    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
24	    // binds its nested perch; the child just needs to launch). `echo` is the
25	    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
26	    match arg(&args, "--mode").as_deref() {
27	        Some("psyche") => {
28	            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
29	            println!("MOCK_PSYCHE_UP:{id}");
30	            return;
31	        }
32	        Some("echo") => {
33	            use std::io::Read;
34	            let mut history = String::new();
35	            let _ = std::io::stdin().read_to_string(&mut history);
36	            // A deterministic brief; a real echo summarizes the history it read.
37	            println!("mock echo brief ({} history bytes)", history.len());
38	            return;
39	        }
40	        Some("dummy") => {
41	            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
42	            // stand-in for a real harness session. Binds its perch (the harness
43	            // contract) exactly like the default stand-in, then prints a stdout
44	            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
45	            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
46	            // attach path can be exercised end-to-end (the default mode exits
47	            // immediately, which is why the v0.12.0 mock tests never caught the
48	            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
49	            // endpoint died" from "a real harness failed to launch."
50	            use std::io::Write;
51	            let id = arg(&args, "--id").unwrap_or_else(|| {
52	                eprintln!("mock-session --mode dummy: --id required");
53	                std::process::exit(2);
54	            });
55	            let session_id =
56	                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
57	            let spt = arg(&args, "--spt")
58	                .or_else(|| std::env::var("SPT_BIN").ok())
59	                .unwrap_or_else(|| "spt".to_string());
60	            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
61	            let endpoint_type = arg(&args, "--type");
62	            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
63	                .and_then(|s| s.parse().ok())
64	                .unwrap_or(500);
65
66	            // Bind the perch live (spt-hosted direct bind, mirroring the default
67	            // stand-in below). A failure is fatal — the fixture must come up.
68	            let mut bind_args = vec![
69	                "api".to_string(),
70	                "--adapter".to_string(),
71	                adapter,
72	                "bind".to_string(),
73	                id.clone(),
74	                "--set-session-id".to_string(),
75	                session_id,
76	            ];
77	            if let Some(ty) = &endpoint_type {
78	                bind_args.push("--type".to_string());
79	                bind_args.push(ty.clone());
80	            }
81	            let bind = Command::new(&spt).args(&bind_args).status();
82	            if !matches!(bind, Ok(s) if s.success()) {
83	                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
84	                std::process::exit(1);
85	            }
86
87	            println!("DUMMY_HARNESS_READY:{id}");
88	            let _ = std::io::stdout().flush();
89	            // Heartbeat forever — only an external kill (tab-close job reap, daemon
90	            // stop, or an explicit kill) ends it.
91	            let mut n: u64 = 0;
92	            loop {
93	                println!("DUMMY_HARNESS_TICK:{id}:{n}");
94	                let _ = std::io::stdout().flush();
95	                n += 1;
96	                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
97	            }
98	        }
99	        _ => {} // default: the agent-session stand-in below.
100	    }
101
102	    let id = arg(&args, "--id").unwrap_or_else(|| {
103	        eprintln!("mock-session: --id required");
104	        std::process::exit(2);
105	    });
106	    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
107	    let spt = arg(&args, "--spt")
108	        .or_else(|| std::env::var("SPT_BIN").ok())
109	        .unwrap_or_else(|| "spt".to_string());
110	    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
111	    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
112	    let endpoint_type = arg(&args, "--type");
113	    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
114
115	    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
116	    let mut bind_args = vec![
117	        "api".to_string(),
118	        "--adapter".to_string(),
119	        adapter.clone(),
120	        "bind".to_string(),
121	        id.clone(),
122	        "--set-session-id".to_string(),
123	        session_id.clone(),
124	    ];
125	    if let Some(ty) = &endpoint_type {
126	        bind_args.push("--type".to_string());
127	        bind_args.push(ty.clone());
128	    }
129	    let bind = Command::new(&spt).args(&bind_args).status();
130	    if !matches!(bind, Ok(s) if s.success()) {
131	        eprintln!("mock-session: api bind failed: {bind:?}");
132	        std::process::exit(1);
133	    }
134
135	    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
136	    let state = Command::new(&spt)
137	        .args([
138	            "api",
139	            "--adapter",
140	            &adapter,
141	            "state",
142	            "idle",
143	            &id,
144	            "--session-id",
145	            &session_id,
146	        ])
147	        .status();
148	    if !matches!(state, Ok(s) if s.success()) {
149	        eprintln!("mock-session: api state idle failed: {state:?}");
150	        std::process::exit(1);
151	    }
152
153	    println!("MOCK_SESSION_READY:{id}");
154	}
155

//! `mock-session` — the generic mock adapter's "agent session". A trivial,
//! cross-platform stand-in for a real harness session (no `claude`, no `.claude`,
//! no CC hooks). When `ManifestRuntime` spawns it from the mock manifest's
//! `[session.self]` template, it does what a real session's boot hook would do:
//! fire `spt api bind` to flip its perch live, then `spt api state idle`.
//!
//! It shells out to the `spt` binary exactly as a real adapter would — resolved
//! from `--spt <path>`, else `$SPT_BIN`, else `spt` on PATH. This is what proves
//! the inbound contract is harness-agnostic: a dumb helper drives it with only
//! `spt api` calls.

use std::process::Command;

fn arg(args: &[String], flag: &str) -> Option<String> {
    args.iter()
        .position(|a| a == flag)
        .and_then(|i| args.get(i + 1).cloned())
}

fn main() {
    let args: Vec<String> = std::env::args().skip(1).collect();

    // M2b lifecycle stand-in modes. `psyche` is the Psyche companion (spt-core
    // binds its nested perch; the child just needs to launch). `echo` is the
    // cheap-model echo-commune: read history on stdin, emit a brief on stdout.
    match arg(&args, "--mode").as_deref() {
        Some("psyche") => {
            let id = arg(&args, "--id").unwrap_or_else(|| "mock-psyche".to_string());
            println!("MOCK_PSYCHE_UP:{id}");
            return;
        }
        Some("echo") => {
            use std::io::Read;
            let mut history = String::new();
            let _ = std::io::stdin().read_to_string(&mut history);
            // A deterministic brief; a real echo summarizes the history it read.
            println!("mock echo brief ({} history bytes)", history.len());
            return;
        }
        Some("dummy") => {
            // The DUMMY-HARNESS fixture (v0.12.1 real-harness gating): a long-lived
            // stand-in for a real harness session. Binds its perch (the harness
            // contract) exactly like the default stand-in, then prints a stdout
            // heartbeat on an interval and STAYS ALIVE until killed — so the REAL
            // `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `rc`
            // attach path can be exercised end-to-end (the default mode exits
            // immediately, which is why the v0.12.0 mock tests never caught the
            // daemon+broker+PTY lifecycle defects). Isolates "broker wedged /
            // endpoint died" from "a real harness failed to launch."
            use std::io::Write;
            let id = arg(&args, "--id").unwrap_or_else(|| {
                eprintln!("mock-session --mode dummy: --id required");
                std::process::exit(2);
            });
            let session_id =
                arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
            let spt = arg(&args, "--spt")
                .or_else(|| std::env::var("SPT_BIN").ok())
                .unwrap_or_else(|| "spt".to_string());
            let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());
            let endpoint_type = arg(&args, "--type");
            let heartbeat_ms: u64 = arg(&args, "--heartbeat-ms")
                .and_then(|s| s.parse().ok())
                .unwrap_or(500);

            // Bind the perch live (spt-hosted direct bind, mirroring the default
            // stand-in below). A failure is fatal — the fixture must come up.
            let mut bind_args = vec![
                "api".to_string(),
                "--adapter".to_string(),
                adapter,
                "bind".to_string(),
                id.clone(),
                "--set-session-id".to_string(),
                session_id,
            ];
            if let Some(ty) = &endpoint_type {
                bind_args.push("--type".to_string());
                bind_args.push(ty.clone());
            }
            let bind = Command::new(&spt).args(&bind_args).status();
            if !matches!(bind, Ok(s) if s.success()) {
                eprintln!("mock-session --mode dummy: api bind failed: {bind:?}");
                std::process::exit(1);
            }

            println!("DUMMY_HARNESS_READY:{id}");
            let _ = std::io::stdout().flush();
            // Heartbeat forever — only an external kill (tab-close job reap, daemon
            // stop, or an explicit kill) ends it.
            let mut n: u64 = 0;
            loop {
                println!("DUMMY_HARNESS_TICK:{id}:{n}");
                let _ = std::io::stdout().flush();
                n += 1;
                std::thread::sleep(std::time::Duration::from_millis(heartbeat_ms));
            }
        }
        _ => {} // default: the agent-session stand-in below.
    }

    let id = arg(&args, "--id").unwrap_or_else(|| {
        eprintln!("mock-session: --id required");
        std::process::exit(2);
    });
    let session_id = arg(&args, "--session-id").unwrap_or_else(|| "mock-session-1".to_string());
    let spt = arg(&args, "--spt")
        .or_else(|| std::env::var("SPT_BIN").ok())
        .unwrap_or_else(|| "spt".to_string());
    // Optional endpoint type (the open type system): a non-agent endpoint — e.g.
    // a Gateway — binds with `--type gateway`. Absent ⇒ the default live_agent.
    let endpoint_type = arg(&args, "--type");
    let adapter = arg(&args, "--mock-adapter").unwrap_or_else(|| "mock".to_string());

    // 1. Post-spawn bind: flip the perch skeleton→live under this session id.
    let mut bind_args = vec![
        "api".to_string(),
        "--adapter".to_string(),
        adapter.clone(),
        "bind".to_string(),
        id.clone(),
        "--set-session-id".to_string(),
        session_id.clone(),
    ];
    if let Some(ty) = &endpoint_type {
        bind_args.push("--type".to_string());
        bind_args.push(ty.clone());
    }
    let bind = Command::new(&spt).args(&bind_args).status();
    if !matches!(bind, Ok(s) if s.success()) {
        eprintln!("mock-session: api bind failed: {bind:?}");
        std::process::exit(1);
    }

    // 2. Report idle (activity sentinel + echo-gate), authenticating by session.
    let state = Command::new(&spt)
        .args([
            "api",
            "--adapter",
            &adapter,
            "state",
            "idle",
            &id,
            "--session-id",
            &session_id,
        ])
        .status();
    if !matches!(state, Ok(s) if s.success()) {
        eprintln!("mock-session: api state idle failed: {state:?}");
        std::process::exit(1);
    }

    println!("MOCK_SESSION_READY:{id}");
}


1	//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent
2	//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the
3	//! REAL spt-hosted bringup path end-to-end —
4	//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach
5	//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and
6	//! NOT a mock that exits immediately.
7	//!
8	//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the
9	//! harness contract), prints a stdout heartbeat on an interval, and stays alive
10	//! until killed — so "broker wedged / endpoint died" is cleanly separable from "a
11	//! real harness failed to launch." Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,
12	//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.
13	//!
14	//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the
15	//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the
16	//! live heartbeat output. Bounded; all spawned pids reaped scoped (never
17	//! machine-wide — shared runner).
18
19	use std::io::Read;
20	use std::path::{Path, PathBuf};
21	use std::process::{Child, Command, Output, Stdio};
22	use std::time::{Duration, Instant};
23
24	mod common;
25	use common::CommandNoWindowExt;
26
27	use spt_store::perch::{self, ParentHint};
28
29	fn kill_pid(pid: u32) {
30	    #[cfg(windows)]
31	    let _ = Command::new("taskkill")
32	        .no_window()
33	        .args(["/PID", &pid.to_string(), "/F", "/T"])
34	        .output();
35	    #[cfg(unix)]
36	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
37	}
38
39	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
40	    let (tx, rx) = std::sync::mpsc::channel();
41	    std::thread::spawn(move || {
42	        let _ = tx.send(cmd.output());
43	    });
44	    rx.recv_timeout(deadline)
45	        .expect("captured spt call must complete within its deadline")
46	        .expect("run spt")
47	}
48
49	fn ready_pid(path: &Path) -> Option<u32> {
50	    let s = std::fs::read_to_string(path).ok()?;
51	    serde_json::from_str::<serde_json::Value>(&s)
52	        .ok()?
53	        .get("pid")?
54	        .as_u64()
55	        .map(|p| p as u32)
56	}
57
58	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
59	    let deadline = Instant::now() + budget;
60	    while Instant::now() < deadline {
61	        if let Some(p) = ready_pid(path) {
62	            return Some(p);
63	        }
64	        std::thread::sleep(Duration::from_millis(50));
65	    }
66	    None
67	}
68
69	fn sibling_bin(name: &str) -> PathBuf {
70	    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
71	        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
72	}
73
74	#[test]
75	fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {
76	    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node
77	    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces
78	    //    as net-stream events, so it requires a net-bound broker — a net-less broker
79	    //    refuses the loopback dial ("net disabled"). Unlike the livehost/reconcile
80	    //    tests (which only inspect perch state), the rc-attach path needs net up. ──
81	    let home = tempfile::tempdir().unwrap();
82	    std::env::set_var("SPT_HOME", home.path());
83
84	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
85	    let exe_suffix = spt_bin
86	        .extension()
87	        .map(|e| format!(".{}", e.to_string_lossy()))
88	        .unwrap_or_default();
89	    let mock_session = sibling_bin("mock-session");
90	    assert!(
91	        mock_session.exists(),
92	        "the dummy-harness program must be built: {} \
93	         (pre-build: cargo build -p mock-adapter --bin mock-session)",
94	        mock_session.display()
95	    );
96
97	    // ── (2) Register the DUMMY-HARNESS adapter: a kind=harness adapter whose
98	    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};
99	    //    the program token is the absolute mock-session path. The dummy shells the
100	    //    same `spt` to bind (via --spt). ──
101	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
102	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
103	    let self_cmd = format!(
104	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
105	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
106	    );
107	    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
108	    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
109	    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
110	    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
111	    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
112	    // VIEWER-CLOSE / psyche-reap gates need.
113	    let src = perch::spt_home().join("srcs").join("dummyharness");
114	    std::fs::create_dir_all(&src).unwrap();
115	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
116	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
117	    let drops = home.path().join("drops");
118	    std::fs::create_dir_all(&drops).unwrap();
119	    let dir = drops.to_string_lossy().replace('\\', "/");
120	    let manifest_toml = format!(
121	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
122	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
123	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
124	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
125	    );
126	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
127	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
128
129	    // ── (3) The REAL daemon broker process. ──
130	    let brain_log = home.path().join("brain.stderr.log");
131	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
132	    let mut broker: Child = Command::new(&spt_bin)
133	        .no_window()
134	        .args(["daemon", "run"])
135	        .env("SPT_HOME", home.path())
136	        .stdout(Stdio::null())
137	        .stderr(Stdio::from(brain_log_file))
138	        .spawn()
139	        .expect("spawn spt daemon run (broker process)");
140	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
141	    {
142	        Some(p) => p,
143	        None => {
144	            let _ = broker.kill();
145	            let _ = broker.wait();
146	            panic!(
147	                "PRECONDITION: brain never came up.\n{}",
148	                std::fs::read_to_string(&brain_log).unwrap_or_default()
149	            );
150	        }
151	    };
152
153	    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
154	    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
155	    let id = "dummy7";
156	    let run = {
157	        let mut cmd = Command::new(&spt_bin);
158	        cmd.no_window()
159	            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
160	            .env("SPT_HOME", home.path());
161	        output_bounded(cmd, Duration::from_secs(45))
162	    };
163
164	    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
165	    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);
166
167	    // The dummy binds its perch live within a few seconds of the broker PTY spawn.
168	    let mut online = false;
169	    let deadline = Instant::now() + Duration::from_secs(20);
170	    while Instant::now() < deadline {
171	        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
172	            online = true;
173	            break;
174	        }
175	        std::thread::sleep(Duration::from_millis(150));
176	    }
177
178	    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
179	    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that
180	    // stamps info.json's pid. It must stay alive (heartbeating).
181	    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
182	    let harness_pid: Option<u32> = run_stderr
183	        .split("pid=Some(")
184	        .nth(1)
185	        .and_then(|s| s.split(')').next())
186	        .and_then(|s| s.trim().parse().ok());
187	    let alive_after = {
188	        std::thread::sleep(Duration::from_millis(700));
189	        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)
190	    };
191
192	    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
193	    let rc_err = home.path().join("rc.stderr.log");
194	    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");
195	    let mut rc: Child = Command::new(&spt_bin)
196	        .no_window()
197	        .args(["rc", id])
198	        .env("SPT_HOME", home.path())
199	        .stdout(Stdio::piped())
200	        .stderr(Stdio::from(rc_err_file))
201	        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming
202	        .spawn()
203	        .expect("spawn spt rc <id>");
204	    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window
205	    let mut rc_out = rc.stdout.take().unwrap();
206	    let (tx, rx) = std::sync::mpsc::channel::<String>();
207	    std::thread::spawn(move || {
208	        let mut buf = [0u8; 4096];
209	        let mut acc = String::new();
210	        loop {
211	            match rc_out.read(&mut buf) {
212	                Ok(0) => break,
213	                Ok(n) => {
214	                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
215	                    if acc.contains("DUMMY_HARNESS_TICK") {
216	                        let _ = tx.send(acc.clone());
217	                        break;
218	                    }
219	                }
220	                Err(_) => break,
221	            }
222	        }
223	        let _ = tx.send(acc);
224	    });
225	    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();
226	    let rc_saw_tick = rc_captured.contains("DUMMY_HARNESS_TICK");
227
228	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
229	    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();
230	    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did
231	    // NOT RC_FAIL (no "no live session" / loopback-dial refusal).
232	    let rc_connected = rc_stderr.contains("PUMP_IPC_READER") && !rc_stderr.contains("RC_FAIL");
233	    eprintln!(
234	        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
235	         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \
236	         rc_stdout_bytes={} ===\n=== endpoint run stderr ===\n{}\n=== rc stderr ===\n{rc_stderr}\
237	         \n=== rc stdout (first 400) ===\n{}",
238	        run.status.success(),
239	        rc_captured.len(),
240	        String::from_utf8_lossy(&run.stderr),
241	        rc_captured.chars().take(400).collect::<String>(),
242	    );
243
244	    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──
245	    kill_pid(rc.id());
246	    let _ = rc.kill();
247	    let _ = rc.wait();
248	    if let Some(p) = harness_pid {
249	        kill_pid(p);
250	    }
251	    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).
252	    let psyche_perch =
253	        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
254	    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
255	        kill_pid(p);
256	    }
257	    let _ = {
258	        let mut cmd = Command::new(&spt_bin);
259	        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
260	        output_bounded(cmd, Duration::from_secs(20))
261	    };
262	    kill_pid(brain_pid);
263	    let _ = broker.kill();
264	    let _ = broker.wait();
265
266	    assert!(
267	        run.status.success(),
268	        "endpoint run --start must succeed: {}",
269	        String::from_utf8_lossy(&run.stderr)
270	    );
271	    assert!(
272	        online,
273	        "the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\n\
274	         === brain stderr ===\n{brain_stderr}"
275	    );
276	    assert!(
277	        alive_after,
278	        "the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \
279	         default mock — harness_pid={harness_pid:?}"
280	    );
281	    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for
282	    // "the attach wired up." It is now SUBSUMED by the `rc_saw_tick` keystone gate
283	    // below: receiving the harness's live output is a strictly stronger proof of a
284	    // wired attach than the setup log line (and the log line races the now-fast
285	    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept
286	    // informational in the DIAGNOSTIC above; the hard proof is output delivery.
287	    let _ = rc_connected;
288	    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,
289	    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its
290	    // `[session.self]` stdout — the operator's "attach shows no output" is fixed. This
291	    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only
292	    // ever attach to an idle/just-spawned child, so `become_controller` replays an
293	    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix
294	    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the
295	    // replay on) is what makes this pass.
296	    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
297	    assert!(
298	        rc_saw_tick,
299	        "`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \
300	         output (got {} bytes) — the broker must forward an already-producing harness's \
301	         PTY to the attach without the replay-vs-forward deadlock.\n=== rc stderr ===\n{rc_stderr}\
302	         \n=== brain stderr ===\n{brain_stderr}",
303	        rc_captured.len()
304	    );
305	}
306

//! Wave 1 (v0.12.1) — the DUMMY-HARNESS fixture smoke test. The permanent
//! real-harness regression fixture the v0.12.0 mock tests never had: it drives the
//! REAL spt-hosted bringup path end-to-end —
//! `spt endpoint run` → `launch_harness_brokered_in` → broker PTY → `spt rc` attach
//! — against a REAL detached `spt daemon run`, NOT an in-proc `reconcile_once` and
//! NOT a mock that exits immediately.
//!
//! The dummy harness (`mock-session --mode dummy`) binds its perch on startup (the
//! harness contract), prints a stdout heartbeat on an interval, and stays alive
//! until killed — so "broker wedged / endpoint died" is cleanly separable from "a
//! real harness failed to launch." Wave 2's lifecycle gates (VIEWER-CLOSE-DETACH,
//! ATTACH-WEDGE, daemon-stop) build on exactly this bring-up.
//!
//! Asserts: `endpoint run --start` brings the dummy up, its perch goes ONLINE, the
//! PTY child stays alive (heartbeating), and `spt rc <id>` attaches and receives the
//! live heartbeat output. Bounded; all spawned pids reaped scoped (never
//! machine-wide — shared runner).

use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::perch::{self, ParentHint};

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

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete within its deadline")
        .expect("run spt")
}

fn ready_pid(path: &Path) -> Option<u32> {
    let s = std::fs::read_to_string(path).ok()?;
    serde_json::from_str::<serde_json::Value>(&s)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}

fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Some(p) = ready_pid(path) {
            return Some(p);
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

fn sibling_bin(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

#[test]
fn endpoint_run_brings_up_a_long_lived_dummy_harness_and_rc_attaches() {
    // ── (1) Isolated SPT_HOME. NET-ENABLED (the daemon self-initializes a real node
    //    identity): `spt rc` attaches over a broker-minted LOOPBACK conn that surfaces
    //    as net-stream events, so it requires a net-bound broker — a net-less broker
    //    refuses the loopback dial ("net disabled"). Unlike the livehost/reconcile
    //    tests (which only inspect perch state), the rc-attach path needs net up. ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();
    let mock_session = sibling_bin("mock-session");
    assert!(
        mock_session.exists(),
        "the dummy-harness program must be built: {} \
         (pre-build: cargo build -p mock-adapter --bin mock-session)",
        mock_session.display()
    );

    // ── (2) Register the DUMMY-HARNESS adapter: a kind=harness adapter whose
    //    [session.self] runs the long-lived dummy. The broker fills {id}/{session_id};
    //    the program token is the absolute mock-session path. The dummy shells the
    //    same `spt` to bind (via --spt). ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
    );
    // A faithful live_agent fixture DECLARES `[session.psyche_init]`: cmd_bind only
    // marks status=online for `live_agent && psyche_init.is_some()` (startup.rs:464 —
    // the online signal is gated to the reconcile host predicate). A LONG-LIVED psyche
    // (a copy of `spt` run as `ready {id}`, mirroring brain_restart_psyche_dup) so the
    // brain's reconcile hosts a resident `{id}-psyche` — the same shape the Wave 2
    // VIEWER-CLOSE / psyche-reap gates need.
    let src = perch::spt_home().join("srcs").join("dummyharness");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The REAL daemon broker process. ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");
    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
    {
        Some(p) => p,
        None => {
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION: brain never came up.\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (4) The REAL spt-hosted bringup: endpoint run --start (broker spawns the
    //    dummy into a PTY; the dummy binds its perch + heartbeats). ──
    let id = "dummy7";
    let run = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window()
            .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
            .env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(45))
    };

    let self_perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let status = || spt_store::info::read_info(&self_perch).and_then(|i| i.status);

    // The dummy binds its perch live within a few seconds of the broker PTY spawn.
    let mut online = false;
    let deadline = Instant::now() + Duration::from_secs(20);
    while Instant::now() < deadline {
        if status().as_deref() == Some(spt_store::liveness::STATUS_ONLINE) {
            online = true;
            break;
        }
        std::thread::sleep(Duration::from_millis(150));
    }

    // The broker-spawned harness pid (from `ENDPOINT_RUN:… pid=Some(NNN)`) — the
    // long-lived PTY child itself, NOT the transient `spt api bind` subprocess that
    // stamps info.json's pid. It must stay alive (heartbeating).
    let run_stderr = String::from_utf8_lossy(&run.stderr).to_string();
    let harness_pid: Option<u32> = run_stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());
    let alive_after = {
        std::thread::sleep(Duration::from_millis(700));
        harness_pid.map(spt_store::proc::is_process_alive).unwrap_or(false)
    };

    // ── (5) rc ATTACH: stream the live PTY output; assert the heartbeat arrives. ──
    let rc_err = home.path().join("rc.stderr.log");
    let rc_err_file = std::fs::File::create(&rc_err).expect("create rc stderr log");
    let mut rc: Child = Command::new(&spt_bin)
        .no_window()
        .args(["rc", id])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::piped())
        .stderr(Stdio::from(rc_err_file))
        .stdin(Stdio::piped()) // keep stdin OPEN — a closed stdin makes the rc pump EOF-detach before streaming
        .spawn()
        .expect("spawn spt rc <id>");
    let _rc_stdin = rc.stdin.take(); // hold the write end open for the attach window
    let mut rc_out = rc.stdout.take().unwrap();
    let (tx, rx) = std::sync::mpsc::channel::<String>();
    std::thread::spawn(move || {
        let mut buf = [0u8; 4096];
        let mut acc = String::new();
        loop {
            match rc_out.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
                    if acc.contains("DUMMY_HARNESS_TICK") {
                        let _ = tx.send(acc.clone());
                        break;
                    }
                }
                Err(_) => break,
            }
        }
        let _ = tx.send(acc);
    });
    let rc_captured = rx.recv_timeout(Duration::from_secs(10)).unwrap_or_default();
    let rc_saw_tick = rc_captured.contains("DUMMY_HARNESS_TICK");

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    let rc_stderr = std::fs::read_to_string(&rc_err).unwrap_or_default();
    // attach-CONNECTS proof (Wave 1 scope): the rc pump spawned its reader and did
    // NOT RC_FAIL (no "no live session" / loopback-dial refusal).
    let rc_connected = rc_stderr.contains("PUMP_IPC_READER") && !rc_stderr.contains("RC_FAIL");
    eprintln!(
        "=== DIAGNOSTIC: run_ok={} online={online} harness_pid={harness_pid:?} \
         alive_after={alive_after} rc_connected={rc_connected} rc_saw_tick={rc_saw_tick} \
         rc_stdout_bytes={} ===\n=== endpoint run stderr ===\n{}\n=== rc stderr ===\n{rc_stderr}\
         \n=== rc stdout (first 400) ===\n{}",
        run.status.success(),
        rc_captured.len(),
        String::from_utf8_lossy(&run.stderr),
        rc_captured.chars().take(400).collect::<String>(),
    );

    // ── (6) Reap scoped: kill rc, the harness, the brain, daemon-stop, broker. ──
    kill_pid(rc.id());
    let _ = rc.kill();
    let _ = rc.wait();
    if let Some(p) = harness_pid {
        kill_pid(p);
    }
    // The reconcile may have hosted a `{id}-psyche` — reap it too (scoped).
    let psyche_perch =
        perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
    if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
        kill_pid(p);
    }
    let _ = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(20))
    };
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    assert!(
        run.status.success(),
        "endpoint run --start must succeed: {}",
        String::from_utf8_lossy(&run.stderr)
    );
    assert!(
        online,
        "the dummy harness must bind its perch ONLINE via the real broker PTY bringup.\n\
         === brain stderr ===\n{brain_stderr}"
    );
    assert!(
        alive_after,
        "the dummy harness PTY child must STAY ALIVE (heartbeating), not exit like the \
         default mock — harness_pid={harness_pid:?}"
    );
    // `rc_connected` (PUMP_IPC_READER present, no RC_FAIL) was the Wave-1 proxy for
    // "the attach wired up." It is now SUBSUMED by the `rc_saw_tick` keystone gate
    // below: receiving the harness's live output is a strictly stronger proof of a
    // wired attach than the setup log line (and the log line races the now-fast
    // teardown — rc delivers a tick and exits before its stderr file flushes). Kept
    // informational in the DIAGNOSTIC above; the hard proof is output delivery.
    let _ = rc_connected;
    // KEYSTONE GATE (v0.12.1 Wave 2 L0): a clean `spt rc` attach to this LIVE,
    // ALREADY-PRODUCING (non-empty OutputLog ring) endpoint-run harness DELIVERS its
    // `[session.self]` stdout — the operator's "attach shows no output" is fixed. This
    // is the exact case the v0.12.0 mock suite + the loopback E2Es lacked (they only
    // ever attach to an idle/just-spawned child, so `become_controller` replays an
    // empty ring and never triggers the replay-vs-forward IPC deadlock). The fix
    // (serve_attach forwards on a SEPARATE broker conn from the one it receives the
    // replay on) is what makes this pass.
    // [int->REQ-HAZARD-ENDPOINT-RUN-ATTACH-OUTPUT]
    assert!(
        rc_saw_tick,
        "`spt rc <id>` must RECEIVE the live endpoint-run harness's DUMMY_HARNESS_TICK \
         output (got {} bytes) — the broker must forward an already-producing harness's \
         PTY to the attach without the replay-vs-forward deadlock.\n=== rc stderr ===\n{rc_stderr}\
         \n=== brain stderr ===\n{brain_stderr}",
        rc_captured.len()
    );
}


1	//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL
2	//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc
3	//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).
4	//!
5	//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real
6	//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed
7	//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`
8	//! must still be served, the dead endpoint must be offlined within a reconcile tick,
9	//! and `daemon stop` must complete bounded.
10	//!
11	//! ## What this proves (prove-don't-change, like the M11-W4 capstone)
12	//!
13	//! The original B-side root hypothesized the broker's loopback forward (`serve_attach`
14	//! → `send_stream` → a bounded 64 KB `tokio::io::duplex`) blocks forever on a dead
15	//! operator and parks the 2-worker net runtime. On the post-L0 code that mechanism does
16	//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own
17	//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream
18	//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a
19	//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The
20	//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any
21	//! residual loopback write. This test is the faithful real-daemon proof that the wedge
22	//! does not occur — and folds in L3 (dead endpoint offlined within one
23	//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).
24	//!
25	//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).
26
27	use std::io::Read;
28	use std::path::{Path, PathBuf};
29	use std::process::{Child, Command, Output, Stdio};
30	use std::time::{Duration, Instant};
31
32	mod common;
33	use common::CommandNoWindowExt;
34
35	use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
36	use spt_store::perch::{self, ParentHint};
37
38	fn kill_pid(pid: u32) {
39	    #[cfg(windows)]
40	    let _ = Command::new("taskkill")
41	        .no_window()
42	        .args(["/PID", &pid.to_string(), "/F", "/T"])
43	        .output();
44	    #[cfg(unix)]
45	    let _ = Command::new("kill").args(["-9", &pid.to_string()]).output();
46	}
47
48	fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
49	    let (tx, rx) = std::sync::mpsc::channel();
50	    std::thread::spawn(move || {
51	        let _ = tx.send(cmd.output());
52	    });
53	    rx.recv_timeout(deadline)
54	        .expect("captured spt call must complete within its deadline")
55	        .expect("run spt")
56	}
57
58	fn ready_pid(path: &Path) -> Option<u32> {
59	    let s = std::fs::read_to_string(path).ok()?;
60	    serde_json::from_str::<serde_json::Value>(&s)
61	        .ok()?
62	        .get("pid")?
63	        .as_u64()
64	        .map(|p| p as u32)
65	}
66
67	fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
68	    let deadline = Instant::now() + budget;
69	    while Instant::now() < deadline {
70	        if let Some(p) = ready_pid(path) {
71	            return Some(p);
72	        }
73	        std::thread::sleep(Duration::from_millis(50));
74	    }
75	    None
76	}
77
78	fn sibling_bin(name: &str) -> PathBuf {
79	    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
80	        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
81	}
82
83	/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
84	/// the captured output and the broker-spawned harness pid (parsed from the
85	/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
86	fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
87	    let mut cmd = Command::new(spt_bin);
88	    cmd.no_window()
89	        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
90	        .env("SPT_HOME", home);
91	    let out = output_bounded(cmd, Duration::from_secs(45));
92	    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
93	    let pid = stderr
94	        .split("pid=Some(")
95	        .nth(1)
96	        .and_then(|s| s.split(')').next())
97	        .and_then(|s| s.trim().parse().ok());
98	    (out, pid)
99	}
100
101	/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.
102	fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {
103	    let perch = perch::resolve_perch_path(id, ParentHint::Infer);
104	    let deadline = Instant::now() + budget;
105	    while Instant::now() < deadline {
106	        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);
107	        if now.as_deref() == Some(want) {
108	            return true;
109	        }
110	        std::thread::sleep(Duration::from_millis(150));
111	    }
112	    false
113	}
114
115	/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within
116	/// the window — the strong "this endpoint is being SERVED" proof. The rc child is then
117	/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the
118	/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the
119	/// L2 wedge trigger when called on the victim. The reader thread observes the killed
120	/// pipe (read → 0/Err) and exits.
121	fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {
122	    let mut rc: Child = Command::new(spt_bin)
123	        .no_window()
124	        .args(["rc", id])
125	        .env("SPT_HOME", home)
126	        .stdout(Stdio::piped())
127	        .stderr(Stdio::null())
128	        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early
129	        .spawn()
130	        .expect("spawn spt rc <id>");
131	    let _rc_stdin = rc.stdin.take();
132	    let mut rc_out = rc.stdout.take().unwrap();
133	    let (tx, rx) = std::sync::mpsc::channel::<bool>();
134	    let reader = std::thread::spawn(move || {
135	        let mut buf = [0u8; 4096];
136	        let mut acc = String::new();
137	        loop {
138	            match rc_out.read(&mut buf) {
139	                Ok(0) => break,
140	                Ok(n) => {
141	                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
142	                    if acc.contains("DUMMY_HARNESS_TICK") {
143	                        let _ = tx.send(true);
144	                        break;
145	                    }
146	                }
147	                Err(_) => break,
148	            }
149	        }
150	        let _ = tx.send(false);
151	    });
152	    let saw = rx.recv_timeout(window).unwrap_or(false);
153	    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is
154	    // idempotent on an already-exited child; `wait()` reaps the handle (clippy
155	    // zombie_processes).
156	    let _ = rc.kill();
157	    let _ = rc.wait();
158	    let _ = reader.join();
159	    saw
160	}
161
162	#[test]
163	fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {
164	    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn
165	    //    that surfaces as net-stream events — needs a net-bound broker). ──
166	    let home = tempfile::tempdir().unwrap();
167	    std::env::set_var("SPT_HOME", home.path());
168
169	    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
170	    let exe_suffix = spt_bin
171	        .extension()
172	        .map(|e| format!(".{}", e.to_string_lossy()))
173	        .unwrap_or_default();
174	    let mock_session = sibling_bin("mock-session");
175	    assert!(
176	        mock_session.exists(),
177	        "the dummy-harness program must be built: {} \
178	         (pre-build: cargo build -p mock-adapter --bin mock-session)",
179	        mock_session.display()
180	    );
181
182	    // ── (2) Register the DUMMY-HARNESS adapter (same shape as dummy_harness_e2e). ──
183	    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
184	    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
185	    let self_cmd = format!(
186	        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
187	         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
188	    );
189	    let src = perch::spt_home().join("srcs").join("dummyharness");
190	    std::fs::create_dir_all(&src).unwrap();
191	    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
192	    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
193	    let drops = home.path().join("drops");
194	    std::fs::create_dir_all(&drops).unwrap();
195	    let dir = drops.to_string_lossy().replace('\\', "/");
196	    let manifest_toml = format!(
197	        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
198	         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
199	         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
200	         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
201	    );
202	    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
203	    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();
204
205	    // ── (3) The REAL daemon broker process. ──
206	    let brain_log = home.path().join("brain.stderr.log");
207	    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
208	    let mut broker: Child = Command::new(&spt_bin)
209	        .no_window()
210	        .args(["daemon", "run"])
211	        .env("SPT_HOME", home.path())
212	        .stdout(Stdio::null())
213	        .stderr(Stdio::from(brain_log_file))
214	        .spawn()
215	        .expect("spawn spt daemon run (broker process)");
216	    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
217	    {
218	        Some(p) => p,
219	        None => {
220	            let _ = broker.kill();
221	            let _ = broker.wait();
222	            panic!(
223	                "PRECONDITION: brain never came up.\n{}",
224	                std::fs::read_to_string(&brain_log).unwrap_or_default()
225	            );
226	        }
227	    };
228
229	    // ── (4) Bring up the VICTIM endpoint and prove it is SERVED (rc sees the tick). ──
230	    let victim = "wedge1";
231	    let (run1, harness_pid1) = endpoint_run(&spt_bin, home.path(), victim);
232	    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));
233	    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of
234	    // the wedge trigger (no clean detach record).
235	    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));
236
237	    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a
238	    //    legitimately dead child). Together with the just-dropped operator pump this is
239	    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──
240	    if let Some(p) = harness_pid1 {
241	        kill_pid(p);
242	    }
243
244	    // ── (6) L2 — the broker is NOT wedged: a brand-NEW endpoint still comes up online
245	    //    AND is served (rc sees its tick) within bounded time. If the dead-child +
246	    //    dropped-pump had parked the net runtime, this bring-up would time out. ──
247	    let fresh = "wedge2";
248	    let (run2, harness_pid2) = endpoint_run(&spt_bin, home.path(), fresh);
249	    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));
250	    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));
251
252	    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick
253	    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's
254	    //    exit-waiter reaps the dead session from its table, and the brain's
255	    //    reconcile_hosted_liveness clears the stale status=online latch. ──
256	    let victim_offlined = wait_for_status(victim, STATUS_OFFLINE, Duration::from_secs(20));
257
258	    // ── (8) L4 — daemon stop completes BOUNDED. ──
259	    let stop = {
260	        let mut cmd = Command::new(&spt_bin);
261	        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
262	        output_bounded(cmd, Duration::from_secs(20))
263	    };
264
265	    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
266	    eprintln!(
267	        "=== DIAGNOSTIC: run1_ok={} victim_online={victim_online} victim_served={victim_served} \
268	         harness_pid1={harness_pid1:?} | run2_ok={} fresh_online={fresh_online} \
269	         fresh_served={fresh_served} | victim_offlined={victim_offlined} stop_ok={} ===\n\
270	         === brain stderr ===\n{brain_stderr}",
271	        run1.status.success(),
272	        run2.status.success(),
273	        stop.status.success(),
274	    );
275
276	    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker
277	    //    (both rc operators were already reaped inside rc_sees_tick). Never
278	    //    machine-wide. ──
279	    if let Some(p) = harness_pid2 {
280	        kill_pid(p);
281	    }
282	    for id in [victim, fresh] {
283	        let psyche_perch =
284	            perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
285	        if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
286	            kill_pid(p);
287	        }
288	    }
289	    kill_pid(brain_pid);
290	    let _ = broker.kill();
291	    let _ = broker.wait();
292
293	    // ── Assertions. ──
294	    assert!(victim_online, "PRECONDITION: the victim endpoint must come ONLINE");
295	    assert!(
296	        victim_served,
297	        "PRECONDITION: the victim must be SERVED (rc saw its tick) before we wedge it"
298	    );
299	    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.
300	    // [int->REQ-HAZARD-ATTACH-WEDGE]
301	    assert!(
302	        fresh_online,
303	        "after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \
304	         still come ONLINE — the broker must not be wedged.\n=== brain stderr ===\n{brain_stderr}"
305	    );
306	    assert!(
307	        fresh_served,
308	        "after the wedge trigger, a NEW `rc` attach must be SERVED (saw its tick) — proof \
309	         the net runtime was not parked by the dead loopback peer.\n=== brain stderr ===\n{brain_stderr}"
310	    );
311	    // L3 (folded): the dead endpoint is offlined within a reconcile tick.
312	    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
313	    assert!(
314	        victim_offlined,
315	        "the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \
316	         session is reaped on child death; reconcile_hosted_liveness clears the latch).\n\
317	         === brain stderr ===\n{brain_stderr}"
318	    );
319	    // L4 (folded): daemon stop is bounded (the output_bounded above would have panicked
320	    // on timeout); assert it also succeeded cleanly.
321	    assert!(
322	        stop.status.success(),
323	        "`daemon stop` must complete bounded + clean after the wedge trigger: {}",
324	        String::from_utf8_lossy(&stop.stderr)
325	    );
326	}
327

//! Wave 2 (v0.12.1) **L2** — REQ-HAZARD-ATTACH-WEDGE robustness gate, on the REAL
//! dummy-harness fixture + a REAL detached `spt daemon run` (no mocks, no in-proc
//! `reconcile_once`). Companion to `dummy_harness_e2e.rs` (L0 keystone bring-up).
//!
//! The hazard (v0.12.0 real-harness finding): a *legitimately* dead PTY child (real
//! crash/kill) plus an operator pump that was DROPPED without a clean detach (closed
//! tab / `taskkill /F`) must NOT wedge the broker — every NEW attach / `endpoint run`
//! must still be served, the dead endpoint must be offlined within a reconcile tick,
//! and `daemon stop` must complete bounded.
//!
//! ## What this proves (prove-don't-change, like the M11-W4 capstone)
//!
//! The original B-side root hypothesized the broker's loopback forward (`serve_attach`
//! → `send_stream` → a bounded 64 KB `tokio::io::duplex`) blocks forever on a dead
//! operator and parks the 2-worker net runtime. On the post-L0 code that mechanism does
//! NOT hold: the loopback duplex is drained broker-INTERNALLY by the operator row's own
//! read pump (`nethost.rs` `RecvHalf::Loopback`), which for an ordinary attach stream
//! (`retentive_cap == 0`) never parks — so `peer_w` never backs up on a dead `rc`; a
//! dead `rc` is merely a dropped IPC subscriber against a bounded (evicting) ring. The
//! `BROKER-QUIC-DEADLINE` bound (`bounded_block_on`, 10 s) is a further backstop on any
//! residual loopback write. This test is the faithful real-daemon proof that the wedge
//! does not occur — and folds in L3 (dead endpoint offlined within one
//! `LIVE_RECONCILE_INTERVAL_MS` tick) and L4 (`daemon stop` bounded).
//!
//! All spawned pids are reaped SCOPED (never machine-wide — shared CI runner).

use std::io::Read;
use std::path::{Path, PathBuf};
use std::process::{Child, Command, Output, Stdio};
use std::time::{Duration, Instant};

mod common;
use common::CommandNoWindowExt;

use spt_store::liveness::{STATUS_OFFLINE, STATUS_ONLINE};
use spt_store::perch::{self, ParentHint};

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

fn output_bounded(mut cmd: Command, deadline: Duration) -> Output {
    let (tx, rx) = std::sync::mpsc::channel();
    std::thread::spawn(move || {
        let _ = tx.send(cmd.output());
    });
    rx.recv_timeout(deadline)
        .expect("captured spt call must complete within its deadline")
        .expect("run spt")
}

fn ready_pid(path: &Path) -> Option<u32> {
    let s = std::fs::read_to_string(path).ok()?;
    serde_json::from_str::<serde_json::Value>(&s)
        .ok()?
        .get("pid")?
        .as_u64()
        .map(|p| p as u32)
}

fn wait_for_ready_pid(path: &Path, budget: Duration) -> Option<u32> {
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        if let Some(p) = ready_pid(path) {
            return Some(p);
        }
        std::thread::sleep(Duration::from_millis(50));
    }
    None
}

fn sibling_bin(name: &str) -> PathBuf {
    PathBuf::from(env!("CARGO_BIN_EXE_spt"))
        .with_file_name(format!("{name}{}", std::env::consts::EXE_SUFFIX))
}

/// `endpoint run --start <id>` against the registered dummyharness adapter; returns
/// the captured output and the broker-spawned harness pid (parsed from the
/// `ENDPOINT_RUN:… pid=Some(NNN)` stderr line — the long-lived PTY child).
fn endpoint_run(spt_bin: &Path, home: &Path, id: &str) -> (Output, Option<u32>) {
    let mut cmd = Command::new(spt_bin);
    cmd.no_window()
        .args(["endpoint", "run", "--adapter", "dummyharness", "--id", id, "--start"])
        .env("SPT_HOME", home);
    let out = output_bounded(cmd, Duration::from_secs(45));
    let stderr = String::from_utf8_lossy(&out.stderr).to_string();
    let pid = stderr
        .split("pid=Some(")
        .nth(1)
        .and_then(|s| s.split(')').next())
        .and_then(|s| s.trim().parse().ok());
    (out, pid)
}

/// Poll `<id>`'s self-perch status until it equals `want` or the budget elapses.
fn wait_for_status(id: &str, want: &str, budget: Duration) -> bool {
    let perch = perch::resolve_perch_path(id, ParentHint::Infer);
    let deadline = Instant::now() + budget;
    while Instant::now() < deadline {
        let now = spt_store::info::read_info(&perch).and_then(|i| i.status);
        if now.as_deref() == Some(want) {
            return true;
        }
        std::thread::sleep(Duration::from_millis(150));
    }
    false
}

/// Attach `rc <id>` and return whether its `DUMMY_HARNESS_TICK` output arrived within
/// the window — the strong "this endpoint is being SERVED" proof. The rc child is then
/// reaped ABRUPTLY (`kill()` = TerminateProcess, no clean detach record) — exactly the
/// closed-tab / crashed-operator case (a DROPPED pump), which is what makes this the
/// L2 wedge trigger when called on the victim. The reader thread observes the killed
/// pipe (read → 0/Err) and exits.
fn rc_sees_tick(spt_bin: &Path, home: &Path, id: &str, window: Duration) -> bool {
    let mut rc: Child = Command::new(spt_bin)
        .no_window()
        .args(["rc", id])
        .env("SPT_HOME", home)
        .stdout(Stdio::piped())
        .stderr(Stdio::null())
        .stdin(Stdio::piped()) // hold stdin open so the pump does not EOF-detach early
        .spawn()
        .expect("spawn spt rc <id>");
    let _rc_stdin = rc.stdin.take();
    let mut rc_out = rc.stdout.take().unwrap();
    let (tx, rx) = std::sync::mpsc::channel::<bool>();
    let reader = std::thread::spawn(move || {
        let mut buf = [0u8; 4096];
        let mut acc = String::new();
        loop {
            match rc_out.read(&mut buf) {
                Ok(0) => break,
                Ok(n) => {
                    acc.push_str(&String::from_utf8_lossy(&buf[..n]));
                    if acc.contains("DUMMY_HARNESS_TICK") {
                        let _ = tx.send(true);
                        break;
                    }
                }
                Err(_) => break,
            }
        }
        let _ = tx.send(false);
    });
    let saw = rx.recv_timeout(window).unwrap_or(false);
    // Abrupt reap = the dropped-pump wedge trigger (no clean detach). `kill()` is
    // idempotent on an already-exited child; `wait()` reaps the handle (clippy
    // zombie_processes).
    let _ = rc.kill();
    let _ = rc.wait();
    let _ = reader.join();
    saw
}

#[test]
fn attach_wedge_dead_child_plus_dropped_pump_does_not_wedge_the_broker() {
    // ── (1) Isolated, NET-ENABLED SPT_HOME (rc attaches over a broker loopback conn
    //    that surfaces as net-stream events — needs a net-bound broker). ──
    let home = tempfile::tempdir().unwrap();
    std::env::set_var("SPT_HOME", home.path());

    let spt_bin = PathBuf::from(env!("CARGO_BIN_EXE_spt"));
    let exe_suffix = spt_bin
        .extension()
        .map(|e| format!(".{}", e.to_string_lossy()))
        .unwrap_or_default();
    let mock_session = sibling_bin("mock-session");
    assert!(
        mock_session.exists(),
        "the dummy-harness program must be built: {} \
         (pre-build: cargo build -p mock-adapter --bin mock-session)",
        mock_session.display()
    );

    // ── (2) Register the DUMMY-HARNESS adapter (same shape as dummy_harness_e2e). ──
    let spt_for_cmd = spt_bin.to_string_lossy().replace('\\', "/");
    let mock_for_cmd = mock_session.to_string_lossy().replace('\\', "/");
    let self_cmd = format!(
        "{mock_for_cmd} --mode dummy --id {{id}} --session-id {{session_id}} \
         --spt {spt_for_cmd} --mock-adapter dummyharness --type live_agent --heartbeat-ms 200"
    );
    let src = perch::spt_home().join("srcs").join("dummyharness");
    std::fs::create_dir_all(&src).unwrap();
    let psyche_bin = src.join(format!("psychebin{exe_suffix}"));
    std::fs::copy(&spt_bin, &psyche_bin).expect("stage the psyche binary fixture");
    let drops = home.path().join("drops");
    std::fs::create_dir_all(&drops).unwrap();
    let dir = drops.to_string_lossy().replace('\\', "/");
    let manifest_toml = format!(
        "[adapter]\nname=\"dummyharness\"\nkind=\"harness\"\nversion=\"1\"\n\
         min_spt_core_version=\"0\"\n\n[session.self]\ncommand='{self_cmd}'\n\n\
         [session]\ncommune_dir=\"{dir}\"\nsignoff_dir=\"{dir}\"\n\n\
         [session.psyche_init]\ncommand='psychebin ready {{id}}'\ncwd=\"{{psyche_dir}}\"\nkeys=[]\n"
    );
    std::fs::write(src.join("manifest.toml"), &manifest_toml).unwrap();
    spt_runtime::registry::register(&perch::adapters_dir(), &src, 1000).unwrap();

    // ── (3) The REAL daemon broker process. ──
    let brain_log = home.path().join("brain.stderr.log");
    let brain_log_file = std::fs::File::create(&brain_log).expect("create brain stderr log");
    let mut broker: Child = Command::new(&spt_bin)
        .no_window()
        .args(["daemon", "run"])
        .env("SPT_HOME", home.path())
        .stdout(Stdio::null())
        .stderr(Stdio::from(brain_log_file))
        .spawn()
        .expect("spawn spt daemon run (broker process)");
    let brain_pid = match wait_for_ready_pid(&home.path().join("brain.ready"), Duration::from_secs(30))
    {
        Some(p) => p,
        None => {
            let _ = broker.kill();
            let _ = broker.wait();
            panic!(
                "PRECONDITION: brain never came up.\n{}",
                std::fs::read_to_string(&brain_log).unwrap_or_default()
            );
        }
    };

    // ── (4) Bring up the VICTIM endpoint and prove it is SERVED (rc sees the tick). ──
    let victim = "wedge1";
    let (run1, harness_pid1) = endpoint_run(&spt_bin, home.path(), victim);
    let victim_online = wait_for_status(victim, STATUS_ONLINE, Duration::from_secs(20));
    // rc_sees_tick reaps its rc ABRUPTLY before returning = the dropped-pump half of
    // the wedge trigger (no clean detach record).
    let victim_served = rc_sees_tick(&spt_bin, home.path(), victim, Duration::from_secs(10));

    // ── (5) THE WEDGE TRIGGER (second half): kill the harness PTY child abruptly (a
    //    legitimately dead child). Together with the just-dropped operator pump this is
    //    exactly the closed-tab + crash case — no clean teardown, no detach record. ──
    if let Some(p) = harness_pid1 {
        kill_pid(p);
    }

    // ── (6) L2 — the broker is NOT wedged: a brand-NEW endpoint still comes up online
    //    AND is served (rc sees its tick) within bounded time. If the dead-child +
    //    dropped-pump had parked the net runtime, this bring-up would time out. ──
    let fresh = "wedge2";
    let (run2, harness_pid2) = endpoint_run(&spt_bin, home.path(), fresh);
    let fresh_online = wait_for_status(fresh, STATUS_ONLINE, Duration::from_secs(20));
    let fresh_served = rc_sees_tick(&spt_bin, home.path(), fresh, Duration::from_secs(10));

    // ── (7) L3 — the dead victim endpoint is OFFLINED within one reconcile tick
    //    (LIVE_RECONCILE_INTERVAL_MS = 5 s; allow a generous window). The broker's
    //    exit-waiter reaps the dead session from its table, and the brain's
    //    reconcile_hosted_liveness clears the stale status=online latch. ──
    let victim_offlined = wait_for_status(victim, STATUS_OFFLINE, Duration::from_secs(20));

    // ── (8) L4 — daemon stop completes BOUNDED. ──
    let stop = {
        let mut cmd = Command::new(&spt_bin);
        cmd.no_window().args(["daemon", "stop"]).env("SPT_HOME", home.path());
        output_bounded(cmd, Duration::from_secs(20))
    };

    let brain_stderr = std::fs::read_to_string(&brain_log).unwrap_or_default();
    eprintln!(
        "=== DIAGNOSTIC: run1_ok={} victim_online={victim_online} victim_served={victim_served} \
         harness_pid1={harness_pid1:?} | run2_ok={} fresh_online={fresh_online} \
         fresh_served={fresh_served} | victim_offlined={victim_offlined} stop_ok={} ===\n\
         === brain stderr ===\n{brain_stderr}",
        run1.status.success(),
        run2.status.success(),
        stop.status.success(),
    );

    // ── (9) Reap scoped: both harness children + any hosted psyches + brain + broker
    //    (both rc operators were already reaped inside rc_sees_tick). Never
    //    machine-wide. ──
    if let Some(p) = harness_pid2 {
        kill_pid(p);
    }
    for id in [victim, fresh] {
        let psyche_perch =
            perch::resolve_perch_path(&format!("{id}-psyche"), ParentHint::Explicit(id));
        if let Some(p) = spt_store::info::read_pid(&psyche_perch) {
            kill_pid(p);
        }
    }
    kill_pid(brain_pid);
    let _ = broker.kill();
    let _ = broker.wait();

    // ── Assertions. ──
    assert!(victim_online, "PRECONDITION: the victim endpoint must come ONLINE");
    assert!(
        victim_served,
        "PRECONDITION: the victim must be SERVED (rc saw its tick) before we wedge it"
    );
    // L2 keystone: the broker survived the dead-child + dropped-pump and still serves.
    // [int->REQ-HAZARD-ATTACH-WEDGE]
    assert!(
        fresh_online,
        "after a dead PTY child + a dropped operator pump, a NEW `endpoint run` must \
         still come ONLINE — the broker must not be wedged.\n=== brain stderr ===\n{brain_stderr}"
    );
    assert!(
        fresh_served,
        "after the wedge trigger, a NEW `rc` attach must be SERVED (saw its tick) — proof \
         the net runtime was not parked by the dead loopback peer.\n=== brain stderr ===\n{brain_stderr}"
    );
    // L3 (folded): the dead endpoint is offlined within a reconcile tick.
    // [int->REQ-HAZARD-HOSTED-LIVENESS-RECONCILE]
    assert!(
        victim_offlined,
        "the dead victim endpoint must be OFFLINED within a reconcile tick (its broker \
         session is reaped on child death; reconcile_hosted_liveness clears the latch).\n\
         === brain stderr ===\n{brain_stderr}"
    );
    // L4 (folded): daemon stop is bounded (the output_bounded above would have panicked
    // on timeout); assert it also succeeded cleanly.
    assert!(
        stop.status.success(),
        "`daemon stop` must complete bounded + clean after the wedge trigger: {}",
        String::from_utf8_lossy(&stop.stderr)
    );
}


Critical finding: the **dummy harness never reads its stdin** — it only heartbeats. So injected bytes accumulate in the PTY master→slave buffer with no drain. That's the repro lever.

Now I need the broker threading model (does a blocked `write_input` wedge the whole broker or just one session?) and how injection is triggered. Read broker.rs dispatch + I/O loop.