a73x

8dc6a014

test: the scrollback fixture types only after both resizes have landed

a73x   2026-09-03 19:25

Commit message
test: the scrollback fixture types only after both resizes have landed

macOS's /bin/sh is bash, and its readline redraws the input line when
SIGWINCH arrives: carriage return, erase to end of line, prompt again. The
test typed immediately after the second attach, so the resize landed mid-echo
and the history row it selects from lost its first characters — the reply
read "q 1 100" where the test wanted "seq 1 100". Waiting for a state frame
on each fd proves seatClient already resized, since it resizes before it
answers. Linux's dash has no readline and redraws nothing.

The unread-observer test now caps the buffer on both ends. Linux holds
AF_UNIX bytes against the sender's send buffer; Darwin hands them to the
peer and charges the receiver's, so capping only the daemon's end left room
for the whole burst and no drop to grade.

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

src/server/server_test_attach.zig
Old New
@@ -539,6 +539,18 @@ test "Server: scrollback fetch is per-client and independent" {
539 const b = try dial.dialAttach(td.sock_path, 100, 30); 539 const b = try dial.dialAttach(td.sock_path, 100, 30);
540 defer b.close(); 540 defer b.close();
541 541
542 // Both attaches must have REACHED the pty before anything is typed.
543 // `seatClient` resizes before it answers, so a state frame on each fd is
544 // proof that the SIGWINCH for that attach is already out. Without the
545 // wait, macOS's /bin/sh is bash and its readline takes the resize MID-ECHO
546 // and redraws the input line over itself — carriage return, erase to end
547 // of line, prompt again — so the history row this test selects from lost
548 // its first characters and every column after them moved. Linux's dash has
549 // no readline and redraws nothing, which is why the race only ever showed
550 // on the Mac.
551 _ = try h.firstStateFrame(alloc, a.handle, 10_000);
552 _ = try h.firstStateFrame(alloc, b.handle, 10_000);
553
542 // Typed by B; both clients see the history it produces. 554 // Typed by B; both clients see the history it produces.
543 try proto.writeFrame(b.handle, .input, "seq 1 100\n"); 555 try proto.writeFrame(b.handle, .input, "seq 1 100\n");
544 556
@@ -1386,12 +1398,16 @@ test "Server: an observer that never reads its replies is dropped, not allowed t
1386 defer peer.close(); 1398 defer peer.close();
1387 try td.srv.pumpOnce(20); 1399 try td.srv.pumpOnce(20);
1388 // Every reply is up to sessions_text_len bytes and the peer never reads 1400 // Every reply is up to sessions_text_len bytes and the peer never reads
1389 // one, so a small send buffer is what an unread socket looks like a few 1401 // one, so a small buffer is what an unread socket looks like a few frames
1390 // frames sooner. Set on the DAEMON's end: for AF_UNIX it is the sender's 1402 // sooner. Set on BOTH ends, because the two kernels put the bytes in
1391 // buffer that fills. 1403 // different places: for AF_UNIX Linux holds them against the SENDER's
1404 // send buffer, while Darwin hands them to the peer and charges the
1405 // RECEIVER's receive buffer, so capping only the daemon's end left the
1406 // Mac with room for the whole burst and no drop to grade.
1392 const fd = td.srv.observers[0].?.fd; 1407 const fd = td.srv.observers[0].?.fd;
1393 const sndbuf: c_int = 4096; 1408 const small: c_int = 4096;
1394 try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, std.mem.asBytes(&sndbuf)); 1409 try std.posix.setsockopt(fd, std.posix.SOL.SOCKET, std.posix.SO.SNDBUF, std.mem.asBytes(&small));
1410 try std.posix.setsockopt(peer.handle, std.posix.SOL.SOCKET, std.posix.SO.RCVBUF, std.mem.asBytes(&small));
1395 1411
1396 // One write, hundreds of frames: they fit in a single 64 KB read, which 1412 // One write, hundreds of frames: they fit in a single 64 KB read, which
1397 // is what makes the whole burst one uninterrupted drain. 1413 // is what makes the whole burst one uninterrupted drain.