diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-28 12:36:58 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-28 12:37:12 -0300 |
| commit | b3595e04e67708c3b060a15cbbbf2478d9e6241e (patch) | |
| tree | 25752214b022ce03e2916eec7aea95d5ddfec5a7 | |
| parent | 7df45a1586568476fb7f3fc5ffe8d644bbe13c93 (diff) | |
| download | pardes-b3595e04e67708c3b060a15cbbbf2478d9e6241e.tar.gz pardes-b3595e04e67708c3b060a15cbbbf2478d9e6241e.zip | |
detached: a frontend that has not finished reading is not a screen that differs
`detached session: input from a frontend reaches the core and comes back as a
diff` failed about three runs in five, always the same way: cell 122, the core
holding `g` and the frontend a space. Cell 122 on a 60-wide grid is row 2
column 2, and row 2 is the pane shell's first line — the `g` is the front of
`goblin@pardes ...`, the prompt bash printed into the pty at whatever moment it
felt like. Nothing in the transport was wrong. The frame carrying that prompt
was sent, arrived, and was sitting unread in the client's socket: instrumenting
the mismatch printed `drained 1 more queued messages` and then
`same after drain: true`.
`pumpUntil` returns the instant it decodes the message it was asked for and
abandons the rest of the burst, while every `pump` presents one frame. One
`c.wait(5)` that times out — and under a loaded test binary one does — buys a
second pump before the first frame is read, and from there the client is one
frame behind for the rest of the test. Harmless while the only thing in that
frame is nothing; a cell that differs the moment a forked shell writes its
prompt.
So the assertion was comparing the core's NOW against the frontend's THEN, and
the fix is the one this file already made for two frontends: converge.
`pumpUntilShowsCore` is `pumpUntilSameScreen` with one client instead of two —
pump, wait, drain EVERYTHING, compare — checked before the first pump so a test
already in sync spends nothing, and handing its last comparison to
`expectSameScreen` so a transport that genuinely drops a cell still fails by
naming it rather than by timing out.
Both single-frontend screen assertions take it; the two-frontend one already
had its own. 8/8 clean gui runs against 3-failures-in-5 before, and the tty
suite's 457 unchanged.
| -rw-r--r-- | src/detached/client.zig | 37 |
1 files changed, 31 insertions, 6 deletions
diff --git a/src/detached/client.zig b/src/detached/client.zig index 07c85945..4769cf25 100644 --- a/src/detached/client.zig +++ b/src/detached/client.zig @@ -717,6 +717,33 @@ const Harness = struct { return error.NeverConverged; } + /// Pump until THIS frontend is showing the core's own screen, draining + /// everything that arrives on every pass. `pumpUntilSameScreen`'s reason, + /// with one frontend instead of two, and a second reason of its own: + /// `pumpUntil` returns the instant it decodes the message it was waiting + /// for and leaves the rest of that burst in the socket, so a client can sit + /// one whole frame behind for the rest of a test. Comparing once against + /// that grid compares the core's NOW with the frontend's THEN — which is + /// nothing at all until the pane shell's prompt lands in the frame nobody + /// read, and then it is a cell that differs. + /// + /// Checked BEFORE the first pump, so a test already in sync spends nothing + /// and no extra frame is manufactured to make one appear. Out of rounds it + /// hands the last comparison to `expectSameScreen`, which names the cell: + /// a transport that really does drop one must fail as a wrong screen, not + /// as a timeout. + fn pumpUntilShowsCore(h: *Harness, c: *Client) !void { + for (0..64) |_| { + _ = h.arena.reset(.retain_capacity); + if (sameScreen((try h.core.render(h.arena.allocator())).cells, c.grid.items)) return; + try h.pump(); + try c.wait(5); + while (try c.next()) |_| {} + } + _ = h.arena.reset(.retain_capacity); + return expectSameScreen((try h.core.render(h.arena.allocator())).cells, c.grid.items); + } + /// Attach a frontend and get it greeted: `open` writes the hello into the /// listener's backlog, one pump accepts and answers it. Nothing blocks, /// which is the whole reason the handshake is not a blocking call. @@ -745,10 +772,9 @@ test "detached session: a frontend attaches, is greeted, and is sent the screen" try testing.expectEqual(wire.FrameKind.full, frame.kind); try testing.expectEqual(@as(usize, 60 * 16), c.grid.items.len); // ...and it must be the core's own frame, cell for cell. This is the whole - // claim of the transport. - _ = h.arena.reset(.retain_capacity); - const surface = try h.core.render(h.arena.allocator()); - try expectSameScreen(surface.cells, c.grid.items); + // claim of the transport — asserted once the frontend has read everything + // the session sent, which `pumpUntil` above deliberately did not do. + try h.pumpUntilShowsCore(&c); } test "detached session: input from a frontend reaches the core and comes back as a diff" { @@ -766,8 +792,7 @@ test "detached session: input from a frontend reaches the core and comes back as // The change arrived as a DIFF: this frontend was already in sync, so // nothing it already had was re-sent. try testing.expectEqual(wire.FrameKind.diff, frame.kind); - _ = h.arena.reset(.retain_capacity); - try expectSameScreen((try h.core.render(h.arena.allocator())).cells, c.grid.items); + try h.pumpUntilShowsCore(&c); } test "detached session: two frontends share one screen at the smallest common grid" { |
