a73x

a689f425

test: server_test_clipboard rides the harness awaits

a73x   2026-08-31 21:58

Commit message
test: server_test_clipboard rides the harness awaits

`awaitSelectionReply` was `h.awaitFrame` written out again with a decode
on the end; it keeps its name (three callers, and `text_len` has to be
read while the payload is still alive) and loses its loop.

`collectGapReplay` keeps its pump-and-poll loop and now says why in
place: both of its numbers are ROUND counts on purpose. It ends a fixed
number of pumps AFTER the content frame rather than when any particular
frame arrives, and a sink that ended the wait early would reintroduce
exactly the blindness the comment above it warns about.

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

src/server/server_test_clipboard.zig
Old New
@@ -13,26 +13,18 @@ const awaitGridText = h.awaitGridText;
13 const connectedPair = h.connectedPair; 13 const connectedPair = h.connectedPair;
14 const writeDyingGapShell = h.writeDyingGapShell; 14 const writeDyingGapShell = h.writeDyingGapShell;
15 15
16 /// Returns the parts of the reply that outlive the frame's payload. 16 /// Returns the parts of the reply that outlive the frame's payload — which
17 /// is the whole reason this is not `awaitFrame` spelled at each call site:
18 /// `text_len` has to be read while the payload is still alive.
17 fn awaitSelectionReply( 19 fn awaitSelectionReply(
18 alloc: std.mem.Allocator, 20 alloc: std.mem.Allocator,
19 srv: *Server, 21 srv: *Server,
20 peer: std.posix.fd_t, 22 peer: std.posix.fd_t,
21 ) !?struct { id: u32, status: proto.SelectionStatus, text_len: usize } { 23 ) !?struct { id: u32, status: proto.SelectionStatus, text_len: usize } {
22 var rounds: usize = 0; 24 const frame = (try awaitFrame(alloc, srv, peer, .selection_reply, 400)) orelse return null;
23 while (rounds < 400) : (rounds += 1) { 25 defer frame.deinit(alloc);
24 try srv.pumpOnce(5); 26 const reply = try proto.decodeSelectionReply(frame.payload);
25 var pfd = [_]std.posix.pollfd{ 27 return .{ .id = reply.id, .status = reply.status, .text_len = reply.text.len };
26 .{ .fd = peer, .events = std.posix.POLL.IN, .revents = 0 },
27 };
28 if (try std.posix.poll(&pfd, 0) == 0) continue;
29 const frame = (try proto.readFrame(alloc, peer)) orelse return null;
30 defer frame.deinit(alloc);
31 if (frame.type != .selection_reply) continue;
32 const reply = try proto.decodeSelectionReply(frame.payload);
33 return .{ .id = reply.id, .status = reply.status, .text_len = reply.text.len };
34 }
35 return null;
36 } 28 }
37 29
38 test "Server: a session-less slot still answers a well-formed selection request" { 30 test "Server: a session-less slot still answers a well-formed selection request" {
@@ -290,6 +282,12 @@ const GapReplay = struct {
290 /// The 400 below is `awaitFrame`'s own budget for an attach reply, ~6ms an 282 /// The 400 below is `awaitFrame`'s own budget for an attach reply, ~6ms an
291 /// iteration; the 60 is the settling window that keeps running past the content 283 /// iteration; the 60 is the settling window that keeps running past the content
292 /// frame, so an event queued after it is caught too. 284 /// frame, so an event queued after it is caught too.
285 ///
286 /// So this stays a pump-and-poll loop rather than becoming a `Link.awaitFrame`
287 /// wait: both numbers are ROUND counts on purpose. The wait ends a fixed
288 /// number of pumps after the content frame, not when a particular frame
289 /// arrives, and a sink that ended the wait early would be the blindness the
290 /// paragraph above describes.
293 fn collectGapReplay(alloc: std.mem.Allocator, srv: *Server, fd: std.posix.fd_t) !GapReplay { 291 fn collectGapReplay(alloc: std.mem.Allocator, srv: *Server, fd: std.posix.fd_t) !GapReplay {
294 var out: GapReplay = .{}; 292 var out: GapReplay = .{};
295 var i: usize = 0; 293 var i: usize = 0;