a73x

c51815d8

test: the repeated-end test pins that the shell outlives the grace

a73x   2026-09-04 10:16

Commit message
test: the repeated-end test pins that the shell outlives the grace

The upper bound moves from 4x the grace to 3x. My reason for it was wrong
and this message carried the error, which is why this is an amend and not a
plain fixup: I claimed a re-armed deadline adds a grace per nag, so that 3x
would fail one that moved twice. It does not. Dropping the `== null` guard
at the arm in endSession RESETS end_by_ms to now plus a grace on every
accepted nag, so a deadline re-armed every 80 ms never fires at all, the
shell outlives the loop's whole 3 s budget, and it is the !alive above that
fails. Measured by dropping that guard: the test fails there, not on any
bound.

So the lower bound is the property and the only pin of the two — living past
the grace proves the shell ignored every TERM and left to the SIGKILL. The
upper bound is a rail on the number that catches a deadline drifting a few
turns of the loop, and no multiplier reaches the regression this test is
named for. 3x rather than 4x costs nothing and reads tighter against a
measurement of 661 ms on Linux and 668 to 682 ms on macOS.

The clock is std.time.Timer now, not milliTimestamp. The deadline being
graded is monoMs() + grace, so a calendar step landing between the two reads
would fail a daemon that did exactly the right thing.

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

src/server/server_test_session.zig
Old New
@@ -1666,9 +1666,12 @@ test "Server: a repeated end_req does not push the SIGKILL deadline out" {
1666 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply; 1666 const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
1667 defer r.deinit(alloc); 1667 defer r.deinit(alloc);
1668 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted); 1668 try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
1669 // The clock the assertions below read, started where the daemon started 1669 // The clock the assertions below read, started where the daemon starts
1670 // its own: `endSession` arms the deadline when it accepts. 1670 // its own: `endSession` arms the deadline when it accepts. MONOTONIC, and
1671 const t_end = std.time.milliTimestamp(); 1671 // deliberately so — the deadline being graded is `monoMs() + grace`, so a
1672 // calendar step landing between the two reads would fail a daemon that
1673 // did exactly the right thing.
1674 var since_end = try std.time.Timer.start();
1672 1675
1673 // An observer nagging faster than the grace: if each accept restarted the 1676 // An observer nagging faster than the grace: if each accept restarted the
1674 // clock, the shell would outlive every deadline it was ever given. 1677 // clock, the shell would outlive every deadline it was ever given.
@@ -1683,7 +1686,7 @@ test "Server: a repeated end_req does not push the SIGKILL deadline out" {
1683 try td.srv.pumpOnce(20); 1686 try td.srv.pumpOnce(20);
1684 std.Thread.sleep(60 * std.time.ns_per_ms); 1687 std.Thread.sleep(60 * std.time.ns_per_ms);
1685 } 1688 }
1686 const outlived_ms = std.time.milliTimestamp() - t_end; 1689 const outlived_ms = since_end.read() / std.time.ns_per_ms;
1687 try std.testing.expect(!alive(pid)); 1690 try std.testing.expect(!alive(pid));
1688 1691
1689 // A dead shell is only half the claim, and it was the only half asserted: 1692 // A dead shell is only half the claim, and it was the only half asserted:
@@ -1694,14 +1697,25 @@ test "Server: a repeated end_req does not push the SIGKILL deadline out" {
1694 // suite running faster. So the duration is the pin now. A wall clock and 1697 // suite running faster. So the duration is the pin now. A wall clock and
1695 // not the loop counter above, which counts nominal turns rather than time. 1698 // not the loop counter above, which counts nominal turns rather than time.
1696 // 1699 //
1697 // The LOWER bound is the real one: living past the grace is what proves 1700 // The LOWER bound is the property, and the only one of the two that is:
1698 // the shell ignored every TERM and left to the SIGKILL. The upper bound 1701 // living past the grace is what proves the shell ignored every TERM and
1699 // catches a deadline that moved a few turns; a deadline that moved 1702 // left to the SIGKILL.
1700 // FOREVER is already caught by the loop's own budget and the `!alive` 1703 //
1701 // above, so this one is deliberately loose rather than a timing gate. 1704 // The UPPER bound is a rail on the number, not a second pin, and it is
1702 // Measured 2026-09-04 against a 500 ms grace: 661 ms on Linux and 668 ms 1705 // worth saying which regression it does NOT catch. The one this test is
1703 // on macOS under `make check`. A bound at 2x would sit 330 ms above that, 1706 // named for is the `== null` guard dropped at the arm in `endSession`,
1704 // which is a flake waiting for a slower box. 1707 // and that guard's absence RESETS `end_by_ms` to now plus a grace on
1708 // every accepted nag rather than adding one — so a deadline re-armed
1709 // every 80 ms never fires at all, the shell outlives the loop's whole
1710 // 3 s budget, and the `!alive` above is what fails. No multiplier here
1711 // reaches that. What this bound does catch is a deadline that drifted a
1712 // few turns of the loop, which is why it exists and why it is loose.
1713 //
1714 // 3x because the measurement it must clear is 661 ms on Linux and 668 to
1715 // 682 ms on macOS under `make check` (2026-09-04) against a 500 ms grace,
1716 // leaving about 2.2x. Not tighter: what varies is how long one turn of
1717 // the loop takes to notice the kill, and a loaded box stretches both its
1718 // sleep and its pump.
1705 if (outlived_ms < Pty.term_grace_ms) { 1719 if (outlived_ms < Pty.term_grace_ms) {
1706 std.debug.print( 1720 std.debug.print(
1707 "the shell died {d} ms after the accepted end, INSIDE the {d} ms grace: " ++ 1721 "the shell died {d} ms after the accepted end, INSIDE the {d} ms grace: " ++
@@ -1710,9 +1724,9 @@ test "Server: a repeated end_req does not push the SIGKILL deadline out" {
1710 ); 1724 );
1711 return error.ShellDiedInsideTheGrace; 1725 return error.ShellDiedInsideTheGrace;
1712 } 1726 }
1713 if (outlived_ms >= 4 * Pty.term_grace_ms) { 1727 if (outlived_ms >= 3 * Pty.term_grace_ms) {
1714 std.debug.print( 1728 std.debug.print(
1715 "the shell lived {d} ms after the accepted end, past 4x the {d} ms grace: " ++ 1729 "the shell lived {d} ms after the accepted end, past 3x the {d} ms grace: " ++
1716 "the nagging looks to have pushed the deadline out\n", 1730 "the nagging looks to have pushed the deadline out\n",
1717 .{ outlived_ms, Pty.term_grace_ms }, 1731 .{ outlived_ms, Pty.term_grace_ms },
1718 ); 1732 );