9ea510f6
test: kill-reattach e2e; docs: M3 decisions and usage
a73x 2026-08-08 14:08
Commit message
README.md
| Old | New | ||
|---|---|---|---|
| @@ -5,7 +5,7 @@ authoritatively in a daemon and replicated in the client — state sync | |||
| 5 | instead of escape-sequence replay. See `docs/handoff.md` for the design | 5 | instead of escape-sequence replay. See `docs/handoff.md` for the design |
| 6 | and `docs/decisions.md` for decisions made. | 6 | and `docs/decisions.md` for decisions made. |
| 7 | 7 | ||
| 8 | Status: **M2 — the loop.** | 8 | Status: **M3 — the promise.** |
| 9 | 9 | ||
| 10 | Requires Zig 0.15.x (ghostty pin); the Makefile points at the pinned | 10 | Requires Zig 0.15.x (ghostty pin); the Makefile points at the pinned |
| 11 | toolchain, override with `make ZIG=...`. | 11 | toolchain, override with `make ZIG=...`. |
| @@ -13,9 +13,11 @@ toolchain, override with `make ZIG=...`. | |||
| 13 | make test && make e2e # verify | 13 | make test && make e2e # verify |
| 14 | make build | 14 | make build |
| 15 | ./zig-out/bin/muxd run & # daemon | 15 | ./zig-out/bin/muxd run & # daemon |
| 16 | ./zig-out/bin/mux # attach a client (Ctrl-\ detaches) | 16 | ./zig-out/bin/mux # attach (Ctrl-\ detach, Shift+PgUp scroll) |
| 17 | ./zig-out/bin/muxd dump [--vt] # debug: print the authoritative grid | 17 | ./zig-out/bin/muxd dump [--vt] # debug: print the authoritative grid |
| 18 | 18 | ||
| 19 | Detaching (`Ctrl-\`) leaves the session running; re-running `mux` resumes | 19 | Detach, or kill the client outright — the session survives and `mux` |
| 20 | it from a state snapshot. systemd user units (socket activation) live in | 20 | resumes it from a state snapshot, including TUI screens and the primary |
| 21 | `contrib/`. | 21 | screen behind them. Scrollback is fetched lazily (Shift+PageUp/PageDown |
| 22 | pages; any other key returns to live). systemd user units (socket | ||
| 23 | activation) live in `contrib/`. | ||
docs/decisions.md
| Old | New | ||
|---|---|---|---|
| @@ -73,6 +73,27 @@ | |||
| 73 | - **systemd socket activation supported** via LISTEN_PID/LISTEN_FDS (fd 3); | 73 | - **systemd socket activation supported** via LISTEN_PID/LISTEN_FDS (fd 3); |
| 74 | units in contrib/. Verified with systemd-socket-activate. | 74 | units in contrib/. Verified with systemd-socket-activate. |
| 75 | 75 | ||
| 76 | ## 2026-08-07 (M3) | ||
| 77 | |||
| 78 | - **Scrollback addressing: screen-space row index** (0 = oldest retained | ||
| 79 | row). Positions drift as history evicts at max_scrollback; scroll | ||
| 80 | positions are ephemeral, so drift is acceptable. Fetch is pull-only: | ||
| 81 | snapshots carry a u32 history_rows count, never history content. | ||
| 82 | - **Scroll UX: Shift+PageUp/PageDown** (`\x1b[5;2~`/`\x1b[6;2~`), page at a | ||
| 83 | time, remote-paged with no client cache (a page fetch is ~2KB over a | ||
| 84 | local socket). Any other key snaps back to live and is swallowed. Live | ||
| 85 | snapshots keep applying while scrolled but paint only on return. | ||
| 86 | - **Dual-screen snapshots.** When the alt screen is active, dumpState | ||
| 87 | prepends the primary screen's visible content before the full-state dump | ||
| 88 | (whose mode section performs the alt switch). Primary saved-cursor lands | ||
| 89 | at end-of-content, not the exact pre-TUI position — accepted. | ||
| 90 | - **SIGPIPE ignored in both processes.** A kill -9'd client previously | ||
| 91 | killed the daemon via SIGPIPE on the next snapshot write — found by the | ||
| 92 | M3 kill-reattach scenario, now covered by a server test and e2e. | ||
| 93 | - **SGR canonicalization note:** the formatter emits palette colors in | ||
| 94 | indexed form (31 -> 38;5;1). Semantically identical; tests assert the | ||
| 95 | canonical form. | ||
| 96 | |||
| 76 | ## Open (owed by later milestones) | 97 | ## Open (owed by later milestones) |
| 77 | 98 | ||
| 78 | - Resize policy under multiple clients (M5) | 99 | - Resize policy under multiple clients (M5) |
docs/superpowers/plans/2026-08-07-m3-the-promise.md
| Old | New | ||
|---|---|---|---|
| @@ -42,7 +42,7 @@ Decisions locked in (recorded in decisions.md, Task 6): scrollback addressing is | |||
| 42 | **Files:** | 42 | **Files:** |
| 43 | - Modify: `src/protocol.zig` | 43 | - Modify: `src/protocol.zig` |
| 44 | 44 | ||
| 45 | - [ ] **Step 1: Add failing tests (append to `src/protocol.zig`)** | 45 | - [x] **Step 1: Add failing tests (append to `src/protocol.zig`)** |
| 46 | 46 | ||
| 47 | ```zig | 47 | ```zig |
| 48 | test "u32 encode/decode round trip" { | 48 | test "u32 encode/decode round trip" { |
| @@ -58,12 +58,12 @@ test "scrollback request encode/decode round trip" { | |||
| 58 | } | 58 | } |
| 59 | ``` | 59 | ``` |
| 60 | 60 | ||
| 61 | - [ ] **Step 2: Run to verify failure** | 61 | - [x] **Step 2: Run to verify failure** |
| 62 | 62 | ||
| 63 | Run: `make test` | 63 | Run: `make test` |
| 64 | Expected: compile error — `putU32` etc. undefined. | 64 | Expected: compile error — `putU32` etc. undefined. |
| 65 | 65 | ||
| 66 | - [ ] **Step 3: Implement (add to `src/protocol.zig`), and update the MsgType docs** | 66 | - [x] **Step 3: Implement (add to `src/protocol.zig`), and update the MsgType docs** |
| 67 | 67 | ||
| 68 | Replace the `MsgType` payload comments for `snapshot` and add the two new types: | 68 | Replace the `MsgType` payload comments for `snapshot` and add the two new types: |
| 69 | 69 | ||
| @@ -108,9 +108,9 @@ pub fn decodeScrollbackReq(payload: []const u8) !ScrollbackReq { | |||
| 108 | } | 108 | } |
| 109 | ``` | 109 | ``` |
| 110 | 110 | ||
| 111 | - [ ] **Step 4: Run tests** — `make test`, expected exit 0. | 111 | - [x] **Step 4: Run tests** — `make test`, expected exit 0. |
| 112 | 112 | ||
| 113 | - [ ] **Step 5: Commit** | 113 | - [x] **Step 5: Commit** |
| 114 | 114 | ||
| 115 | ```bash | 115 | ```bash |
| 116 | git add src/protocol.zig | 116 | git add src/protocol.zig |
| @@ -124,7 +124,7 @@ git commit -m "feat: scrollback protocol messages and integer helpers" | |||
| 124 | **Files:** | 124 | **Files:** |
| 125 | - Modify: `src/engine.zig` | 125 | - Modify: `src/engine.zig` |
| 126 | 126 | ||
| 127 | - [ ] **Step 1: Add failing tests (append to `src/engine.zig`)** | 127 | - [x] **Step 1: Add failing tests (append to `src/engine.zig`)** |
| 128 | 128 | ||
| 129 | ```zig | 129 | ```zig |
| 130 | test "Engine: historyRows counts scrolled-off lines, zero on alt screen" { | 130 | test "Engine: historyRows counts scrolled-off lines, zero on alt screen" { |
| @@ -175,9 +175,9 @@ test "Engine: dumpScrollback serves styled history rows by screen-space range" { | |||
| 175 | } | 175 | } |
| 176 | ``` | 176 | ``` |
| 177 | 177 | ||
| 178 | - [ ] **Step 2: Run to verify failure** — `make test`, expected: compile error. | 178 | - [x] **Step 2: Run to verify failure** — `make test`, expected: compile error. |
| 179 | 179 | ||
| 180 | - [ ] **Step 3: Implement (add to `Engine`)** | 180 | - [x] **Step 3: Implement (add to `Engine`)** |
| 181 | 181 | ||
| 182 | ```zig | 182 | ```zig |
| 183 | /// Number of history (scrolled-off) rows above the viewport on the | 183 | /// Number of history (scrolled-off) rows above the viewport on the |
| @@ -218,9 +218,9 @@ test "Engine: dumpScrollback serves styled history rows by screen-space range" { | |||
| 218 | 218 | ||
| 219 | Type note: `pt.screen.y` is a coordinate integer; `total -| 1` guards the empty-terminal case. `self.term.rows` is `size.CellCountInt` — widen with `@as(u32, self.term.rows)` if the compiler complains about the addition. | 219 | Type note: `pt.screen.y` is a coordinate integer; `total -| 1` guards the empty-terminal case. `self.term.rows` is `size.CellCountInt` — widen with `@as(u32, self.term.rows)` if the compiler complains about the addition. |
| 220 | 220 | ||
| 221 | - [ ] **Step 4: Run tests** — `make test`, expected exit 0. If the `historyRows` expectation of 77 is off by one (prompt-row accounting), print the actual value, verify it equals `lines_fed + 1 - 24` reasoning, and adjust the *comment and constant together*. | 221 | - [x] **Step 4: Run tests** — `make test`, expected exit 0. If the `historyRows` expectation of 77 is off by one (prompt-row accounting), print the actual value, verify it equals `lines_fed + 1 - 24` reasoning, and adjust the *comment and constant together*. |
| 222 | 222 | ||
| 223 | - [ ] **Step 5: Commit** | 223 | - [x] **Step 5: Commit** |
| 224 | 224 | ||
| 225 | ```bash | 225 | ```bash |
| 226 | git add src/engine.zig | 226 | git add src/engine.zig |
| @@ -234,7 +234,7 @@ git commit -m "feat: engine history introspection and styled scrollback dumps" | |||
| 234 | **Files:** | 234 | **Files:** |
| 235 | - Modify: `src/engine.zig` (dumpState + the alt-screen test) | 235 | - Modify: `src/engine.zig` (dumpState + the alt-screen test) |
| 236 | 236 | ||
| 237 | - [ ] **Step 1: Un-weaken the alt-screen test** | 237 | - [x] **Step 1: Un-weaken the alt-screen test** |
| 238 | 238 | ||
| 239 | In `test "Engine: alt-screen state survives snapshot into fresh engine"`, replace the final block: | 239 | In `test "Engine: alt-screen state survives snapshot into fresh engine"`, replace the final block: |
| 240 | 240 | ||
| @@ -248,9 +248,9 @@ In `test "Engine: alt-screen state survives snapshot into fresh engine"`, replac | |||
| 248 | } | 248 | } |
| 249 | ``` | 249 | ``` |
| 250 | 250 | ||
| 251 | - [ ] **Step 2: Run to verify failure** — `make test`, expected: that assertion fails. | 251 | - [x] **Step 2: Run to verify failure** — `make test`, expected: that assertion fails. |
| 252 | 252 | ||
| 253 | - [ ] **Step 3: Implement dual-screen `dumpState`** | 253 | - [x] **Step 3: Implement dual-screen `dumpState`** |
| 254 | 254 | ||
| 255 | Replace `dumpState` with: | 255 | Replace `dumpState` with: |
| 256 | 256 | ||
| @@ -303,9 +303,9 @@ API note: `ScreenFormatter.init(screen: *Screen, opts)` and its `extra: ScreenFo | |||
| 303 | 303 | ||
| 304 | Ordering subtlety that makes this correct: the TerminalFormatter's *modes* section (which contains the switch-to-alt-screen DECSET) is emitted **before** its screen-content section, so the replica is still on the primary screen while the prepended primary content applies, and already on the alt screen when the alt content applies. | 304 | Ordering subtlety that makes this correct: the TerminalFormatter's *modes* section (which contains the switch-to-alt-screen DECSET) is emitted **before** its screen-content section, so the replica is still on the primary screen while the prepended primary content applies, and already on the alt screen when the alt content applies. |
| 305 | 305 | ||
| 306 | - [ ] **Step 4: Run tests** — `make test`, expected exit 0, including the strengthened alt test and the M2 fidelity test (which round-trips dumpState through a live daemon). | 306 | - [x] **Step 4: Run tests** — `make test`, expected exit 0, including the strengthened alt test and the M2 fidelity test (which round-trips dumpState through a live daemon). |
| 307 | 307 | ||
| 308 | - [ ] **Step 5: Commit** | 308 | - [x] **Step 5: Commit** |
| 309 | 309 | ||
| 310 | ```bash | 310 | ```bash |
| 311 | git add src/engine.zig | 311 | git add src/engine.zig |
| @@ -319,7 +319,7 @@ git commit -m "feat: dual-screen snapshots — primary content survives alt-scre | |||
| 319 | **Files:** | 319 | **Files:** |
| 320 | - Modify: `src/server.zig` | 320 | - Modify: `src/server.zig` |
| 321 | 321 | ||
| 322 | - [ ] **Step 1: Add failing tests (append to `src/server.zig`)** | 322 | - [x] **Step 1: Add failing tests (append to `src/server.zig`)** |
| 323 | 323 | ||
| 324 | ```zig | 324 | ```zig |
| 325 | test "Server: survives a client that dies without detaching; next attach works" { | 325 | test "Server: survives a client that dies without detaching; next attach works" { |
| @@ -437,9 +437,9 @@ test "Server: serves scrollback chunks on request" { | |||
| 437 | } | 437 | } |
| 438 | ``` | 438 | ``` |
| 439 | 439 | ||
| 440 | - [ ] **Step 2: Run to verify failure** — `make test`. Expected: first test may PASS already on platforms where the write happens after poll reveals POLLHUP (drop-on-read-EOF path) — that's fine, it pins the behavior; the scrollback test must FAIL (no fetch handler). | 440 | - [x] **Step 2: Run to verify failure** — `make test`. Expected: first test may PASS already on platforms where the write happens after poll reveals POLLHUP (drop-on-read-EOF path) — that's fine, it pins the behavior; the scrollback test must FAIL (no fetch handler). |
| 441 | 441 | ||
| 442 | - [ ] **Step 3: Implement** | 442 | - [x] **Step 3: Implement** |
| 443 | 443 | ||
| 444 | In `installSignalHandlers`, add SIGPIPE ignore: | 444 | In `installSignalHandlers`, add SIGPIPE ignore: |
| 445 | 445 | ||
| @@ -482,9 +482,9 @@ In `serviceClient`'s switch, add: | |||
| 482 | }, | 482 | }, |
| 483 | ``` | 483 | ``` |
| 484 | 484 | ||
| 485 | - [ ] **Step 4: Run tests** — `make test`, expected exit 0. The M2 fidelity test still passes because the replica-feed in that test must be updated to skip the 4-byte prefix — do that now: in `test "Server: replica rebuilt from snapshots matches the authoritative grid"`, change both `replica.feed(frame.payload);` occurrences to `replica.feed(frame.payload[4..]);`. | 485 | - [x] **Step 4: Run tests** — `make test`, expected exit 0. The M2 fidelity test still passes because the replica-feed in that test must be updated to skip the 4-byte prefix — do that now: in `test "Server: replica rebuilt from snapshots matches the authoritative grid"`, change both `replica.feed(frame.payload);` occurrences to `replica.feed(frame.payload[4..]);`. |
| 486 | 486 | ||
| 487 | - [ ] **Step 5: Commit** | 487 | - [x] **Step 5: Commit** |
| 488 | 488 | ||
| 489 | ```bash | 489 | ```bash |
| 490 | git add src/server.zig | 490 | git add src/server.zig |
| @@ -498,7 +498,7 @@ git commit -m "feat: daemon serves scrollback; snapshot carries history count; k | |||
| 498 | **Files:** | 498 | **Files:** |
| 499 | - Modify: `src/client.zig` | 499 | - Modify: `src/client.zig` |
| 500 | 500 | ||
| 501 | - [ ] **Step 1: Implement (no isolated unit test — the moving parts are fd-loop glue; coverage comes from Task 4's protocol tests plus Task 6's e2e; the marker rendering gets a small test below)** | 501 | - [x] **Step 1: Implement (no isolated unit test — the moving parts are fd-loop glue; coverage comes from Task 4's protocol tests plus Task 6's e2e; the marker rendering gets a small test below)** |
| 502 | 502 | ||
| 503 | At the top of `attach`, after the socket connects, ignore SIGPIPE (a daemon that dies mid-write must surface as an error return, not kill us): | 503 | At the top of `attach`, after the socket connects, ignore SIGPIPE (a daemon that dies mid-write must surface as an error return, not kill us): |
| 504 | 504 | ||
| @@ -635,9 +635,9 @@ test "renderScrollback paints rows with an inverse scroll marker" { | |||
| 635 | 635 | ||
| 636 | Note the `.snapshot` arm no longer calls `replica.reset()+feed` unconditionally — while scrolled, the latest state is stored and painted on scroll-exit, so the user never loses live output, it's just deferred. | 636 | Note the `.snapshot` arm no longer calls `replica.reset()+feed` unconditionally — while scrolled, the latest state is stored and painted on scroll-exit, so the user never loses live output, it's just deferred. |
| 637 | 637 | ||
| 638 | - [ ] **Step 2: Run tests + build** — `make test && make build`, expected exit 0. | 638 | - [x] **Step 2: Run tests + build** — `make test && make build`, expected exit 0. |
| 639 | 639 | ||
| 640 | - [ ] **Step 3: Commit** | 640 | - [x] **Step 3: Commit** |
| 641 | 641 | ||
| 642 | ```bash | 642 | ```bash |
| 643 | git add src/client.zig | 643 | git add src/client.zig |
| @@ -651,7 +651,7 @@ git commit -m "feat: client scroll mode with lazy history paging; SIGPIPE-safe" | |||
| 651 | **Files:** | 651 | **Files:** |
| 652 | - Modify: `test/e2e.sh`, `docs/decisions.md`, `README.md` | 652 | - Modify: `test/e2e.sh`, `docs/decisions.md`, `README.md` |
| 653 | 653 | ||
| 654 | - [ ] **Step 1: Append the kill-reattach scenario to `test/e2e.sh`** (before the final `echo "e2e OK"`) | 654 | - [x] **Step 1: Append the kill-reattach scenario to `test/e2e.sh`** (before the final `echo "e2e OK"`) |
| 655 | 655 | ||
| 656 | ```sh | 656 | ```sh |
| 657 | # --- M3: kill a client mid-run; daemon survives; reattach lands correctly. | 657 | # --- M3: kill a client mid-run; daemon survives; reattach lands correctly. |
| @@ -671,16 +671,16 @@ rm -f "$OUT.kill" "$OUT.re" | |||
| 671 | 671 | ||
| 672 | Also add `"$OUT.kill" "$OUT.re"` to the `cleanup()` rm line. | 672 | Also add `"$OUT.kill" "$OUT.re"` to the `cleanup()` rm line. |
| 673 | 673 | ||
| 674 | - [ ] **Step 2: Run** — `make e2e`, expected `e2e OK`. | 674 | - [x] **Step 2: Run** — `make e2e`, expected `e2e OK`. |
| 675 | 675 | ||
| 676 | - [ ] **Step 3: Manual demo (M3 acceptance)** — scripted equivalents acceptable, real-terminal run preferred: | 676 | - [x] **Step 3: Manual demo (M3 acceptance)** — scripted equivalents acceptable, real-terminal run preferred: |
| 677 | 677 | ||
| 678 | 1. `muxd run &`, `mux`, run `seq 1 200`, **Shift+PageUp** — oldest lines appear with an inverse `[scroll]` marker; Shift+PageUp/PageDown page through; any key returns live. | 678 | 1. `muxd run &`, `mux`, run `seq 1 200`, **Shift+PageUp** — oldest lines appear with an inverse `[scroll]` marker; Shift+PageUp/PageDown page through; any key returns live. |
| 679 | 2. Run `while true; do date; sleep 1; done`, `kill -9` the mux process from another terminal, wait a few seconds, re-run `mux`: the counter continued (timestamps advanced while detached) and the screen is current. | 679 | 2. Run `while true; do date; sleep 1; done`, `kill -9` the mux process from another terminal, wait a few seconds, re-run `mux`: the counter continued (timestamps advanced while detached) and the screen is current. |
| 680 | 3. Open `nvim`, kill the client, reattach: nvim's screen is correct. Quit nvim (`:q`): the shell history is there, not a blank screen. | 680 | 3. Open `nvim`, kill the client, reattach: nvim's screen is correct. Quit nvim (`:q`): the shell history is there, not a blank screen. |
| 681 | 4. Scrollback after reattach: the history from before the detach pages correctly. | 681 | 4. Scrollback after reattach: the history from before the detach pages correctly. |
| 682 | 682 | ||
| 683 | - [ ] **Step 4: Update `docs/decisions.md`** — append: | 683 | - [x] **Step 4: Update `docs/decisions.md`** — append: |
| 684 | 684 | ||
| 685 | ```markdown | 685 | ```markdown |
| 686 | ## 2026-08-07 (M3) | 686 | ## 2026-08-07 (M3) |
| @@ -702,7 +702,7 @@ Also add `"$OUT.kill" "$OUT.re"` to the `cleanup()` rm line. | |||
| 702 | M3 kill-reattach scenario, now covered by a server test and e2e. | 702 | M3 kill-reattach scenario, now covered by a server test and e2e. |
| 703 | ``` | 703 | ``` |
| 704 | 704 | ||
| 705 | - [ ] **Step 5: Update `README.md`** — replace the status line and usage block: | 705 | - [x] **Step 5: Update `README.md`** — replace the status line and usage block: |
| 706 | 706 | ||
| 707 | ```markdown | 707 | ```markdown |
| 708 | Status: **M3 — the promise.** | 708 | Status: **M3 — the promise.** |
| @@ -718,7 +718,7 @@ resumes it from a state snapshot, including scrollback (fetched lazily) | |||
| 718 | and TUI screens. | 718 | and TUI screens. |
| 719 | ``` | 719 | ``` |
| 720 | 720 | ||
| 721 | - [ ] **Step 6: Commit** | 721 | - [x] **Step 6: Commit** |
| 722 | 722 | ||
| 723 | ```bash | 723 | ```bash |
| 724 | git add test/e2e.sh docs/decisions.md README.md | 724 | git add test/e2e.sh docs/decisions.md README.md |
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -7,7 +7,7 @@ MUX="$2" | |||
| 7 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 7 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 8 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 8 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| 9 | 9 | ||
| 10 | cleanup() { kill "$DPID" 2>/dev/null || true; rm -f "$SOCK" "$OUT"; } | 10 | cleanup() { kill "$DPID" 2>/dev/null || true; rm -f "$SOCK" "$OUT" "$OUT.kill" "$OUT.re"; } |
| 11 | trap cleanup EXIT INT TERM | 11 | trap cleanup EXIT INT TERM |
| 12 | 12 | ||
| 13 | "$MUXD" run --sock "$SOCK" --shell /bin/sh & | 13 | "$MUXD" run --sock "$SOCK" --shell /bin/sh & |
| @@ -34,4 +34,18 @@ grep -q "e2e-works" "$OUT" || { | |||
| 34 | # 3. Detach left the daemon running. | 34 | # 3. Detach left the daemon running. |
| 35 | kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; } | 35 | kill -0 "$DPID" || { echo "e2e FAIL: daemon died on detach"; exit 1; } |
| 36 | 36 | ||
| 37 | # --- M3: kill a client mid-run; daemon survives; reattach lands correctly. | ||
| 38 | { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" & | ||
| 39 | CPID=$! | ||
| 40 | sleep 1 | ||
| 41 | kill -9 "$CPID" 2>/dev/null || true | ||
| 42 | sleep 1 | ||
| 43 | kill -0 "$DPID" || { echo "e2e FAIL: daemon died after client kill -9"; exit 1; } | ||
| 44 | |||
| 45 | { sleep 1; printf '\034'; } | "$MUX" --sock "$SOCK" > "$OUT.re" | ||
| 46 | grep -q "60" "$OUT.re" || { | ||
| 47 | echo "e2e FAIL: reattach after kill missing state"; cat "$OUT.re"; exit 1; | ||
| 48 | } | ||
| 49 | rm -f "$OUT.kill" "$OUT.re" | ||
| 50 | |||
| 37 | echo "e2e OK" | 51 | echo "e2e OK" |