a73x

670a80f5

test: the last counted waits — modes, await, deliver

a73x   2026-08-31 21:58

Commit message
test: the last counted waits — modes, await, deliver

A second pass over the counted loops the first sweep's grep missed.

server_test_modes: the three mode-collecting loops become `collectModes`,
a sink that keeps every `pty_mode` frame in order and ends the wait when
it has the number asked for. A sink and not a `pumpUntil`, because the
condition is counted out of the frames themselves — the predicate would
have had to read the socket to answer, and a predicate with a side effect
reads worse than the loop it replaces.

server_test_await: five waits become `pumpUntil` on named conditions —
`Phase`, `Named`, `Awaiting`, `Returned`. Each old loop ended either
because the daemon got there or because the rounds ran out, and the
`expect` after it could not say which.

server_test_deliver keeps all five of its counted loops and says why once,
at the first: every round DOES work — feed the engine, resnapshot — so
they apply load until the daemon reacts rather than waiting on something
already in flight. The cap loop reads `rounds` afterwards, which makes
the count part of that test's verdict rather than a budget.

The settle loop in server_test_modes stays for the mirror reason: its
verdict is that a FOURTH frame never came, and there is no arrival to
wait for.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TWxBL1HBULH1ZwTNzzKTja

src/server/server_test_await.zig
Old New
@@ -83,6 +83,26 @@ test "Server: OSC 133 marks reach attached clients as cmd_state pushes" {
83 // pty → engine → tracker → wire, and no part of it is stubbed. 83 // pty → engine → tracker → wire, and no part of it is stubbed.
84 // --------------------------------------------------------------------------- 84 // ---------------------------------------------------------------------------
85 85
86 /// A session's command phase, as a `pumpUntil` context.
87 const Phase = struct {
88 srv: *Server,
89 si: usize,
90 want: proto.CmdPhase,
91 fn reached(self: Phase) bool {
92 return self.srv.sessions.table[self.si].?.cmd.phase == self.want;
93 }
94 };
95
96 /// A session by name, for the waits that open with "the daemon has seated
97 /// this one".
98 const Named = struct {
99 srv: *Server,
100 name: []const u8,
101 fn exists(self: Named) bool {
102 return self.srv.sessions.find(self.name) != null;
103 }
104 };
105
86 /// The absence half of the phantom-mark assertions: run nothing, claim 106 /// The absence half of the phantom-mark assertions: run nothing, claim
87 /// nothing. 107 /// nothing.
88 fn anyReturnWithin( 108 fn anyReturnWithin(
@@ -570,11 +590,7 @@ test "Server: a return is still answerable once the next command is running" {
570 // until the daemon has actually seen it open. 590 // until the daemon has actually seen it open.
571 try proto.writeFrame(c.handle, .input, "go\n"); 591 try proto.writeFrame(c.handle, .input, "go\n");
572 try proto.writeFrame(c.handle, .input, "go\n"); 592 try proto.writeFrame(c.handle, .input, "go\n");
573 var spun: usize = 0; 593 try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Phase{ .srv = &td.srv, .si = 0, .want = .running }, Phase.reached));
574 while (spun < 500 and td.srv.sessions.table[0].?.cmd.phase != .running) : (spun += 1) {
575 try td.srv.pumpOnce(5);
576 }
577 try std.testing.expectEqual(proto.CmdPhase.running, td.srv.sessions.table[0].?.cmd.phase);
578 594
579 // Now ask about everything since the beginning of time. The honest answer 595 // Now ask about everything since the beginning of time. The honest answer
580 // is the FIRST command's return — the client asked what had returned 596 // is the FIRST command's return — the client asked what had returned
@@ -780,8 +796,7 @@ test "Server: re-attaching to another session drops the await it left behind" {
780 796
781 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "a"); 797 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
782 defer c.close(); 798 defer c.close();
783 var spun: usize = 0; 799 try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Named{ .srv = &td.srv, .name = "a" }, Named.exists));
784 while (spun < 200 and td.srv.sessions.find("a") == null) : (spun += 1) try td.srv.pumpOnce(5);
785 const si_a = td.srv.sessions.find("a") orelse return error.NoSessionA; 800 const si_a = td.srv.sessions.find("a") orelse return error.NoSessionA;
786 801
787 // An await is a question about ONE session's seq series: since_seq is a 802 // An await is a question about ONE session's seq series: since_seq is a
@@ -791,19 +806,20 @@ test "Server: re-attaching to another session drops the await it left behind" {
791 .settle_ms = 0, 806 .settle_ms = 0,
792 .timeout_ms = 60_000, 807 .timeout_ms = 60_000,
793 })); 808 }));
794 spun = 0; 809 const Awaiting = struct {
795 while (spun < 200) : (spun += 1) { 810 srv: *Server,
796 try td.srv.pumpOnce(5); 811 si: usize,
797 if (slotAwaiting(&td.srv, si_a)) break; 812 fn yes(self: @This()) bool {
798 } 813 return slotAwaiting(self.srv, self.si);
799 try std.testing.expect(slotAwaiting(&td.srv, si_a)); 814 }
815 };
816 try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Awaiting{ .srv = &td.srv, .si = si_a }, Awaiting.yes));
800 817
801 // Re-attach the SAME connection to a different session. Carrying the 818 // Re-attach the SAME connection to a different session. Carrying the
802 // watermark across would compare session a's seq against session b's 819 // watermark across would compare session a's seq against session b's
803 // last_return — an await that answers instantly or never, arbitrarily. 820 // last_return — an await that answers instantly or never, arbitrarily.
804 try attachNamed(c.handle, 80, 24, "b"); 821 try attachNamed(c.handle, 80, 24, "b");
805 spun = 0; 822 try std.testing.expect(try h.pumpUntil(&td.srv, 2000, Named{ .srv = &td.srv, .name = "b" }, Named.exists));
806 while (spun < 200 and td.srv.sessions.find("b") == null) : (spun += 1) try td.srv.pumpOnce(5);
807 const si_b = td.srv.sessions.find("b") orelse return error.NoSessionB; 823 const si_b = td.srv.sessions.find("b") orelse return error.NoSessionB;
808 try std.testing.expect(si_a != si_b); 824 try std.testing.expect(si_a != si_b);
809 825
@@ -929,11 +945,14 @@ test "Server: an await resolves against the awaiting client's session" {
929 // b's shell returns a command, and the daemon has demonstrably seen it. 945 // b's shell returns a command, and the daemon has demonstrably seen it.
930 const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing; 946 const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing;
931 try proto.writeFrame(b.handle, .input, "go\n"); 947 try proto.writeFrame(b.handle, .input, "go\n");
932 var spun: usize = 0; 948 const Returned = struct {
933 while (spun < 500 and td.srv.sessions.table[si_b].?.last_return == null) : (spun += 1) { 949 srv: *Server,
934 try td.srv.pumpOnce(5); 950 si: usize,
935 } 951 fn yes(self: @This()) bool {
936 try std.testing.expect(td.srv.sessions.table[si_b].?.last_return != null); 952 return self.srv.sessions.table[self.si].?.last_return != null;
953 }
954 };
955 try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Returned{ .srv = &td.srv, .si = si_b }, Returned.yes));
937 956
938 // That return is another session's and must not resolve a's await. 957 // That return is another session's and must not resolve a's await.
939 if (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 40)) |early| { 958 if (try awaitFrame(alloc, &td.srv, a.handle, .await_reply, 40)) |early| {
src/server/server_test_deliver.zig
Old New
@@ -36,6 +36,12 @@ test "Server: a stalled client does not block delivery to others" {
36 // Snapshot everyone until A backs up. A blocking write into A's full 36 // Snapshot everyone until A backs up. A blocking write into A's full
37 // buffer would hang here rather than fail, so reaching the assertions 37 // buffer would hang here rather than fail, so reaching the assertions
38 // at all is itself the "never stalls" half of this test. 38 // at all is itself the "never stalls" half of this test.
39 //
40 // The five counted loops in this file are all this shape and none is a
41 // `pumpUntil`: each round DOES work — it feeds the engine and
42 // resnapshots — so the loop applies load until the daemon reacts rather
43 // than waiting for something already in flight. The cap loop below goes
44 // further and asserts on the count itself.
39 var rounds: usize = 0; 45 var rounds: usize = 0;
40 while (rounds < 64) : (rounds += 1) { 46 while (rounds < 64) : (rounds += 1) {
41 if (td.srv.clients[0]) |slot| { 47 if (td.srv.clients[0]) |slot| {
@@ -93,7 +99,8 @@ test "Server: a client exceeding the pending cap is dropped" {
93 } 99 }
94 100
95 // Past the cap the daemon drops the client rather than buffering for it 101 // Past the cap the daemon drops the client rather than buffering for it
96 // without bound. 102 // without bound. `rounds` is read after the loop, so the count is part
103 // of the verdict here and not a budget.
97 try std.testing.expect(td.srv.clients[0] == null); 104 try std.testing.expect(td.srv.clients[0] == null);
98 // The cap is what ended it, not the loop bound: a four-frame cap behind 105 // The cap is what ended it, not the loop bound: a four-frame cap behind
99 // a socket buffer of about one frame cannot absorb many more than five. 106 // a socket buffer of about one frame cannot absorb many more than five.
src/server/server_test_modes.zig
Old New
@@ -17,6 +17,41 @@ fn pumpAndCollectModes(
17 try drainModes(alloc, fd, out); 17 try drainModes(alloc, fd, out);
18 } 18 }
19 19
20 /// Pump until `out` holds `want` mode frames, keeping every one of them in
21 /// order. A sink rather than a `pumpUntil`, because the condition is
22 /// counted out of the frames themselves: the predicate would have to read
23 /// the socket to answer, and a predicate with a side effect is a worse
24 /// thing to read than this.
25 fn collectModes(
26 alloc: std.mem.Allocator,
27 srv: *Server,
28 fd: std.posix.fd_t,
29 out: *std.ArrayList(proto.PtyModeFlags),
30 want: usize,
31 iters: usize,
32 ) !bool {
33 const Collect = struct {
34 alloc: std.mem.Allocator,
35 out: *std.ArrayList(proto.PtyModeFlags),
36 want: usize,
37 fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void {
38 if (frame.type != .pty_mode) return;
39 const self: *@This() = @ptrCast(@alignCast(ctx.?));
40 try self.out.append(self.alloc, try proto.decodePtyMode(frame.payload));
41 if (self.out.items.len >= self.want) return error.ModesCollected;
42 }
43 };
44 var collect: Collect = .{ .alloc = alloc, .out = out, .want = want };
45 _ = h.awaitFrameSink(alloc, srv, fd, h.never_from_daemon, iters, .{
46 .ctx = &collect,
47 .on = Collect.on,
48 }) catch |err| switch (err) {
49 error.ModesCollected => return true,
50 else => return err,
51 };
52 return false;
53 }
54
20 /// No pump: for paths that answer synchronously, where pumping would blur what 55 /// No pump: for paths that answer synchronously, where pumping would blur what
21 /// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it 56 /// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it
22 /// does not WAIT at all — it takes what is already readable and returns. 57 /// does not WAIT at all — it takes what is already readable and returns.
@@ -120,10 +155,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
120 // Attaching is enough on its own: nothing about the session has changed, 155 // Attaching is enough on its own: nothing about the session has changed,
121 // so a client that only heard about changes would never be told what the 156 // so a client that only heard about changes would never be told what the
122 // terminal is doing. 157 // terminal is doing.
123 var rounds: usize = 0; 158 try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 1, 250));
124 while (rounds < 250 and modes.items.len == 0) : (rounds += 1) {
125 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
126 }
127 try std.testing.expectEqual(@as(usize, 1), modes.items.len); 159 try std.testing.expectEqual(@as(usize, 1), modes.items.len);
128 try std.testing.expect(modes.items[0].icanon); 160 try std.testing.expect(modes.items[0].icanon);
129 try std.testing.expect(modes.items[0].echo); 161 try std.testing.expect(modes.items[0].echo);
@@ -133,10 +165,7 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
133 // poll cannot be folded into the "the pty printed something" arm. 165 // poll cannot be folded into the "the pty printed something" arm.
134 try proto.writeFrame(c.handle, .input, "go\n"); 166 try proto.writeFrame(c.handle, .input, "go\n");
135 167
136 rounds = 0; 168 try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 2, 500));
137 while (rounds < 500 and modes.items.len < 2) : (rounds += 1) {
138 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
139 }
140 try std.testing.expectEqual(@as(usize, 2), modes.items.len); 169 try std.testing.expectEqual(@as(usize, 2), modes.items.len);
141 try std.testing.expect(!modes.items[1].echo); 170 try std.testing.expect(!modes.items[1].echo);
142 // -echo alone: the line discipline is still canonical, and reporting 171 // -echo alone: the line discipline is still canonical, and reporting
@@ -149,18 +178,17 @@ test "Server: the pty's mode bits reach a client on attach, and again only when
149 // the mode only after reading pty output sits here forever. 178 // the mode only after reading pty output sits here forever.
150 try proto.writeFrame(c.handle, .input, "silent\n"); 179 try proto.writeFrame(c.handle, .input, "silent\n");
151 180
152 rounds = 0; 181 try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 3, 500));
153 while (rounds < 500 and modes.items.len < 3) : (rounds += 1) {
154 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
155 }
156 try std.testing.expectEqual(@as(usize, 3), modes.items.len); 182 try std.testing.expectEqual(@as(usize, 3), modes.items.len);
157 try std.testing.expect(!modes.items[2].icanon); 183 try std.testing.expect(!modes.items[2].icanon);
158 try std.testing.expect(!modes.items[2].echo); 184 try std.testing.expect(!modes.items[2].echo);
159 185
160 // And then nothing: the pty is in a blocking read with its mode unchanged, 186 // And then nothing: the pty is in a blocking read with its mode unchanged,
161 // so a daemon that re-sent what it already said shows up as a fourth frame 187 // so a daemon that re-sent what it already said shows up as a fourth frame
162 // on the very NEXT pump. 25 rounds, because round one catches it. 188 // on the very NEXT pump. 25 rounds, because round one catches it — a
163 rounds = 0; 189 // fixed count on purpose, since the verdict is that a FOURTH frame never
190 // came and there is no arrival to wait for.
191 var rounds: usize = 0;
164 while (rounds < 25) : (rounds += 1) { 192 while (rounds < 25) : (rounds += 1) {
165 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes); 193 try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
166 } 194 }