---
name: effect-journal-wedge-tests
description: How to repro-test the EffectJournal lock-across-effect wedge (REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE) structurally + via real-fixture attach
metadata:
  type: project
---

REQ-HAZARD-EFFECT-JOURNAL-PTY-WEDGE (v0.13.0 W1b). `EffectJournal::apply_once`
(crates/spt-daemon/src/effect.rs) holds the GLOBAL `inner` mutex ACROSS the user
`effect()` closure + two fsync (`write_line` does flush+sync_all). Broker
`dispatch_input` (broker.rs ~1252) runs `session.write_input` INSIDE that lock for
every op_id-carrying keystroke. A blocked write_input (non-draining child, full PTY
input buffer) holds the lock forever → facet A (per-keystroke stutter), facet B
(single-threaded dispatch can't service `spt rc` attaches → "brain IPC read deadline
elapsed").

Planned fix (NOT mine): PtyWrite becomes EPHEMERAL (in-memory dedup only, no fsync);
apply_once runs effect() OFF the lock (reserve key under lock → release → run → re-
acquire to finalize); durable kinds (NetSend/NetDial/Registry/Spool) keep fsync.
Fix adds `EffectKind::is_durable()`.

REPRO-FIRST test patterns that produced clean RED on current code:

1. STRUCTURAL UNIT "lock not held across effect" (effect.rs mod tests):
   Arc<EffectJournal>; thread 1's PtyWrite closure signals "entered" via mpsc then
   BLOCKS on a release channel; after recv("entered") with 2s timeout, thread 2 does
   apply_once for a DIFFERENT key with ||Ok(()); a done-channel recv_timeout(2s) is
   the watchdog — pre-fix it times out (lock held) = RED, never a hang. ALWAYS
   release+join both threads before asserting so no thread leaks.

2. STRUCTURAL UNIT "PtyWrite skips durable write": apply_once PtyWrite (1,1) +
   NetSend (1,2); assert FILE CONTENT (std::fs::read_to_string(j.path())) — durable
   "1 2" token present on a PENDING/DONE line, ephemeral "1 1" ABSENT. Use file
   content (not is_durable(), which doesn't exist yet) so it COMPILES on current
   code + fails on the assertion. Pre-fix file = "PENDING 1 1 pty\nDONE 1 1\nPENDING
   1 2 net-send\nDONE 1 2\n" → "PtyWrite absent" assert fails = RED. Also assert both
   still dedup in-memory (second apply_once → Outcome::Deduped, effect not re-run).

3. INT (tests/inject_control_wedge.rs): net_broker (NetHost::start + Broker::
   bind_in_with_net) — loopback attach NEEDS a net host (plain Broker::bind won't do).
   Spawn flood_spawn_req (stdin-ignoring); a DRIVER thread pumps sustained
   send_effect(op,256KiB) on its own conn (binds via attach(sid,0) first — send_effect
   needs require_session). A CONCURRENT operator on a PUMP conn (cold_start_pump, so
   the deadline surfaces as "brain IPC read deadline elapsed") does net_dial_loopback
   + request_attach(Viewer) + net_stream_subscribe; assert subscribe serviced AND it
   RECEIVES FLOOD output bytes (output path bypasses the journal lock, so byte receipt
   proves dispatch serviced the attach despite the wedge). Use Viewer not Control so
   the attach itself sends NO journaled input (would self-block on the same lock).
   Watchdog every wait; tear down (stop flag, join, kill_pid) BEFORE asserting.

PLATFORM: Windows ConPTY ABSORBS sustained input (write_input doesn't block) → wedge
benign locally → the int test PASSES on Windows (asserts the POSITIVE W1 lacked:
liveness + real byte receipt). The facet-B RED is forkpty-only: gravity-linux
(cfg(unix)) is the venue where write_input parks under the lock. Mirror the existing
park-(b) cfg handling. The two STRUCTURAL UNITS are cross-platform RED (they don't
depend on a real PTY blocking — thread 1's closure blocks deterministically).
