a73x

b2deef85

test: the grace table arms its stubborn shells through the fixture doors

a73x   2026-09-04 10:16

Commit message
test: the grace table arms its stubborn shells through the fixture doors

The test wrote its own trap shim and slept 400 ms before shutting the
daemon down. A shell that had not reached its `trap` line yet died to the
first TERM, teardown finished well inside the bound, and `kill(pid, 0)`
found nothing -- so every assertion held while the grace the test is named
for was never paid. That is a vacuous pass on a slow box, not a failure.

`TestDaemon.startStubborn` and `attachStubborn` already block on the
per-session armed marker, and this was the last caller arming by sleep.
Graded by shortening the door's wait budget to 0: this test then fails at
its `startStubborn` line with `error.StubbornShellNeverArmed`.

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
@@ -738,35 +738,22 @@ test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
738 var td = try h.TestDaemon.open(alloc, "stubborn"); 738 var td = try h.TestDaemon.open(alloc, "stubborn");
739 defer td.deinit(); 739 defer td.deinit();
740 740
741 // A real shell ignoring the two signals teardown relies on — HUP from 741 // The stubborn shell is the harness's, and its doors do not return until
742 // the master's close, then TERM — which is what an interactive shell 742 // that session's own traps are armed. The arming is what makes the child
743 // does with TERM anyway. Only these children pay the grace at all. 743 // ignore HUP and TERM, and only a child that ignores them pays the grace
744 const shim = try std.fmt.allocPrintSentinel(alloc, "{s}/stubborn.sh", .{td.tmp.path()}, 0); 744 // at all, so a shell signalled before its `trap` line ran would let every
745 defer alloc.free(shim); 745 // assertion below hold with no stubborn shell in the table.
746 { 746 try td.startStubborn(alloc, .{});
747 var f = try std.fs.cwd().createFile(shim, .{ .mode = 0o755 });
748 defer f.close();
749 try f.writeAll("#!/bin/sh\ntrap '' HUP TERM\nwhile :; do sleep 1; done\n");
750 }
751
752 try td.start(.{ .shell = shim });
753 747
754 // Eight beside the default: enough that serial grace (9 x 500ms) and 748 // Eight beside the default: enough that serial grace (9 x 500ms) and
755 // shared grace (500ms) cannot be confused, cheap enough to spawn. 749 // shared grace (500ms) cannot be confused, cheap enough to spawn.
756 for (1..9) |i| { 750 for (1..9) |i| {
757 var nb: [8]u8 = undefined; 751 var nb: [8]u8 = undefined;
758 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i}); 752 const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i});
759 const c = try dial.dialAttachNamed(td.sock_path, 80, 24, nm); 753 const c = try td.attachStubborn(alloc, nm, 80, 24);
760 defer c.close(); 754 defer c.close();
761 const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
762 return error.NoSnapshotFillingTable;
763 f.deinit(alloc);
764 } 755 }
765 756
766 // Before signalling, not after: a TERM that lands before `trap` runs is
767 // honoured, and the test would pass without a stubborn child in it.
768 std.Thread.sleep(400 * std.time.ns_per_ms);
769
770 var kids: [max_sessions]std.posix.pid_t = undefined; 757 var kids: [max_sessions]std.posix.pid_t = undefined;
771 var n: usize = 0; 758 var n: usize = 0;
772 for (&td.srv.sessions.table) |*slot| { 759 for (&td.srv.sessions.table) |*slot| {