a73x

7e9a491b

test: server_test_modes rides the harness awaits

a73x   2026-08-31 21:58

Commit message
test: server_test_modes rides the harness awaits

`modesWithResync` was the only single-connection frame wait in this file
and becomes `awaitFrameSink`: it wants the `term_modes` frame and needs
the state frame BEFORE it to name which resync branch answered, which is
exactly what a sink is for.

The other six loops stay, and each now says why in place rather than
leaving the next reader to work it out:

  - `drainModes` does not wait at all; it takes what is already readable.
  - Two settling loops end a fixed number of pumps AFTER the content
    frame, and their verdict is what did NOT arrive in that window.
  - Three loops poll TWO connections at once, and `Link.awaitFrame`
    waits on one link.
  - The in-band size report is raw bytes off the pty master, where there
    is no Link to await on.

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

src/server/server_test_modes.zig
Old New
@@ -18,7 +18,8 @@ fn pumpAndCollectModes(
18 } 18 }
19 19
20 /// No pump: for paths that answer synchronously, where pumping would blur what 20 /// No pump: for paths that answer synchronously, where pumping would blur what
21 /// caused the frame. 21 /// 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.
22 fn drainModes( 23 fn drainModes(
23 alloc: std.mem.Allocator, 24 alloc: std.mem.Allocator,
24 fd: std.posix.fd_t, 25 fd: std.posix.fd_t,
@@ -354,6 +355,9 @@ test "Server: a session enabling bracketed paste tells its clients, and not agai
354 // vacuously on a session that simply never spoke again — and a second 355 // vacuously on a session that simply never spoke again — and a second
355 // term_modes would prove the send is per-pump rather than on change. 356 // term_modes would prove the send is per-pump rather than on change.
356 try proto.writeFrame(c.handle, .input, "go\n"); 357 try proto.writeFrame(c.handle, .input, "go\n");
358 // A round-counting loop on purpose: the wait ends a fixed number of
359 // pumps AFTER the content frame, not when some frame arrives, and the
360 // verdict is what did NOT come in that window.
357 var saw_content = false; 361 var saw_content = false;
358 var resent: usize = 0; 362 var resent: usize = 0;
359 var after: usize = 0; 363 var after: usize = 0;
@@ -474,6 +478,9 @@ test "Server: a joiner that resizes the grid is still told the session's modes"
474 // resyncSnapshot broadcasts, and the other two arms send to the joiner 478 // resyncSnapshot broadcasts, and the other two arms send to the joiner
475 // alone. Without it the test could pass down the snapshot branch and 479 // alone. Without it the test could pass down the snapshot branch and
476 // pin nothing the resync-branch test does not already cover. 480 // pin nothing the resync-branch test does not already cover.
481 // Two connections at once, which is why this keeps its own poll:
482 // `Link.awaitFrame` waits on ONE link, and the verdict here is a fact
483 // about A and a fact about B arriving in the same window.
477 var a_resnapshotted = false; 484 var a_resnapshotted = false;
478 var b_modes: ?bool = null; 485 var b_modes: ?bool = null;
479 var i: usize = 0; 486 var i: usize = 0;
@@ -545,6 +552,9 @@ test "Server: a window title reaches clients on change, and only on change" {
545 // digested — without it the "no second frame" half would pass vacuously 552 // digested — without it the "no second frame" half would pass vacuously
546 // on a session that simply never spoke again. 553 // on a session that simply never spoke again.
547 try proto.writeFrame(c.handle, .input, "go\n"); 554 try proto.writeFrame(c.handle, .input, "go\n");
555 // A round-counting loop on purpose: the wait ends a fixed number of
556 // pumps AFTER the content frame, not when some frame arrives, and the
557 // verdict is what did NOT come in that window.
548 var saw_content = false; 558 var saw_content = false;
549 var resent: usize = 0; 559 var resent: usize = 0;
550 var after: usize = 0; 560 var after: usize = 0;
@@ -609,6 +619,8 @@ test "Server: a session that never set a title has none sent for it" {
609 var joined = false; 619 var joined = false;
610 var b: ?std.net.Stream = null; 620 var b: ?std.net.Stream = null;
611 defer if (b) |s| s.close(); 621 defer if (b) |s| s.close();
622 // Both reasons at once: two connections, and a round-counting settle
623 // whose verdict is `titles == 0`.
612 var i: usize = 0; 624 var i: usize = 0;
613 while (i < 500 and after < 80) : (i += 1) { 625 while (i < 500 and after < 80) : (i += 1) {
614 if (saw_content) { 626 if (saw_content) {
@@ -688,6 +700,8 @@ test "Server: a joiner that resizes the grid is still told the session's title"
688 // A's re-snapshot at B's size is the witness that this arm ran at all: 700 // A's re-snapshot at B's size is the witness that this arm ran at all:
689 // resyncSnapshot broadcasts, and the other two arms send to the joiner 701 // resyncSnapshot broadcasts, and the other two arms send to the joiner
690 // alone. 702 // alone.
703 // Two connections again; see the bracketed-paste test above for why the
704 // poll stays.
691 var a_resnapshotted = false; 705 var a_resnapshotted = false;
692 var b_title: ?[]const u8 = null; 706 var b_title: ?[]const u8 = null;
693 defer if (b_title) |t| alloc.free(t); 707 defer if (b_title) |t| alloc.free(t);
@@ -716,30 +730,32 @@ test "Server: a joiner that resizes the grid is still told the session's title"
716 try std.testing.expectEqualStrings("vim", b_title orelse return error.NoTitleOnResizingJoin); 730 try std.testing.expectEqualStrings("vim", b_title orelse return error.NoTitleOnResizingJoin);
717 } 731 }
718 732
719 /// awaitFrame drops every frame but the one asked for, including the branch's. 733 /// Which resync branch answered the join, and what it said about the modes.
734 /// A sink rather than a bare `awaitFrame`, because the branch is named by
735 /// the frame BEFORE the answer and awaitFrame drops those.
720 fn modesWithResync( 736 fn modesWithResync(
721 alloc: std.mem.Allocator, 737 alloc: std.mem.Allocator,
722 srv: *Server, 738 srv: *Server,
723 fd: std.posix.fd_t, 739 fd: std.posix.fd_t,
724 ) !struct { content: ?proto.MsgType, modes: ?bool } { 740 ) !struct { content: ?proto.MsgType, modes: ?bool } {
725 var content: ?proto.MsgType = null; 741 const Branch = struct {
726 var modes: ?bool = null; 742 content: ?proto.MsgType = null,
727 var i: usize = 0; 743 fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void {
728 while (i < 400 and modes == null) : (i += 1) { 744 if (frame.type != .snapshot and frame.type != .delta) return;
729 try srv.pumpOnce(5); 745 const self: *@This() = @ptrCast(@alignCast(ctx.?));
730 var pfd = [_]std.posix.pollfd{ 746 self.content = frame.type;
731 .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 },
732 };
733 if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue;
734 const f = (try proto.readFrame(alloc, fd)) orelse break;
735 defer f.deinit(alloc);
736 switch (f.type) {
737 .snapshot, .delta => content = f.type,
738 .term_modes => modes = (try proto.decodeTermModes(f.payload)).bracketed_paste,
739 else => {},
740 } 747 }
748 };
749 var branch: Branch = .{};
750 var modes: ?bool = null;
751 if (try h.awaitFrameSink(alloc, srv, fd, .term_modes, 400, .{
752 .ctx = &branch,
753 .on = Branch.on,
754 })) |frame| {
755 defer frame.deinit(alloc);
756 modes = (try proto.decodeTermModes(frame.payload)).bracketed_paste;
741 } 757 }
742 return .{ .content = content, .modes = modes }; 758 return .{ .content = branch.content, .modes = modes };
743 } 759 }
744 760
745 test "Server: a resize reaches the pty as an in-band size report when the app asked for one" { 761 test "Server: a resize reaches the pty as an in-band size report when the app asked for one" {
@@ -761,6 +777,8 @@ test "Server: a resize reaches the pty as an in-band size report when the app as
761 // The report is INPUT to the child, so the only place to see it from 777 // The report is INPUT to the child, so the only place to see it from
762 // here is the tty's own echo of it back on the master. Canonical mode 778 // here is the tty's own echo of it back on the master. Canonical mode
763 // may spell the ESC as ^[, so the match starts after it. 779 // may spell the ESC as ^[, so the match starts after it.
780 // Raw bytes off the pty master, not frames on a connection, so there is
781 // no Link here to await on.
764 var got: std.ArrayList(u8) = .empty; 782 var got: std.ArrayList(u8) = .empty;
765 defer got.deinit(alloc); 783 defer got.deinit(alloc);
766 var tries: usize = 0; 784 var tries: usize = 0;