a73x

src/server/server_test_session.zig

Ref:   Size: 82.5 KiB   History

const std = @import("std");
const builtin = @import("builtin");
const Grid = @import("term").grid.Grid;
const proto = @import("term").protocol;
const quic = @import("quic");
const quic_server = @import("quic_server.zig");
const xdg = @import("xdg");
const TmpDir = @import("testtmp").TmpDir;
const h = @import("server_test_harness.zig");
const dial = h.dial;
const Pty = h.Pty;
const srv_mod = @import("server.zig");
const SessionTable = @import("server_sessions.zig").SessionTable;
const Server = srv_mod.Server;
const boundUdpPort = srv_mod.boundUdpPort;
const max_sessions = srv_mod.max_sessions;
const shutdown_flag = &srv_mod.shutdown_flag;
const awaitFrame = h.awaitFrame;
const awaitFrameOn = h.awaitFrameOn;
const connectedPair = h.connectedPair;
const firstStateFrame = h.firstStateFrame;

// Rationale: a test's daemon needs a shell to spawn, and this
// file's fixture helpers sit outside the `test` blocks the rule skips.

/// The teardown on the failing branch is not tidiness: a Server that got built
/// owns a live shell on a pty, and discarding it leaves that shell holding the
/// test runner's stdout — the build never sees EOF and hangs instead of
/// printing a failure.
fn expectInitRefused(alloc: std.mem.Allocator, path: []const u8, want: anyerror) !void {
    // folder rule 5 exemption: This fixture invokes a shell to exercise session behavior.
    if (Server.init(alloc, .{ .sock_path = path, .shell = "/bin/sh" })) |built| {
        var stolen = built;
        stolen.deinit();
        return error.InitShouldHaveRefused;
    } else |err| {
        try std.testing.expectEqual(want, err);
    }
}

test "Server: a second daemon refuses a live socket instead of stealing it" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "live", .{ .shell = "/bin/sh" });
    defer td.deinit();

    try td.threaded();

    // The incident: this used to unlink the path and bind over it, leaving
    // the first daemon running and unreachable.
    try expectInitRefused(alloc, td.sock_path, error.DaemonAlreadyRunning);

    // The loser touched nothing: the socket file is still there...
    const st = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
    try std.testing.expect(std.posix.S.ISSOCK(st.mode));

    // ...and it still reaches the daemon that was already there. The epoch
    // is what makes that precise: it names one daemon *instance*, so
    // matching it rules out having been handed a replacement.
    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    const first = try firstStateFrame(alloc, c.handle, 10_000);
    try std.testing.expect(first != null);
    try std.testing.expectEqual(proto.MsgType.snapshot, first.?.type);
    try std.testing.expectEqual(td.srv.sessions.table[0].?.epoch, first.?.epoch);
}

test "Server: a dead daemon's leftover socket file is cleared and rebound" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "stale");
    defer td.deinit();

    // What a daemon killed with SIGKILL leaves behind: a bound socket file
    // whose listener is gone, since deinit's unlink never ran.
    {
        const addr = try std.net.Address.initUnix(td.sock_path);
        var dead = try addr.listen(.{});
        dead.deinit(); // closes the fd; the file stays on disk
    }
    const before = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
    try std.testing.expect(std.posix.S.ISSOCK(before.mode));

    // Nobody answers there, so the path is ours: recovery, not refusal.
    try td.start(.{ .shell = "/bin/sh" });

    try td.threaded();

    // And the rebind is real, not just a file that reappeared: it serves.
    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    const first = try firstStateFrame(alloc, c.handle, 10_000);
    try std.testing.expect(first != null);
    try std.testing.expectEqual(td.srv.sessions.table[0].?.epoch, first.?.epoch);
}

test "Server: a path that cannot be bound fails as AddressInUse" {
    const alloc = std.testing.allocator;

    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    const dir_path = tmp.path();
    const sock_path = try std.fmt.allocPrint(alloc, "{s}/dangling.sock", .{dir_path});
    defer alloc.free(sock_path);

    // A dangling symlink reaches `bind()` the way a lost start-up race does, but
    // deterministically: the stat gets ENOENT so the probe reads the path as
    // free, while the bind gets EADDRINUSE off the symlink's own entry.
    try tmp.dir.symLink("no-such-target", "dangling.sock", .{});

    // Linux only, and not because of a spelling: Darwin's bind FOLLOWS a
    // dangling symlink and creates the socket at the name it points to, so
    // there is no unbindable path to grade there. Measured 2026-09-03 on
    // macOS 26 with a plain C bind, which returned 0.
    if (builtin.os.tag != .linux) return error.SkipZigTest;

    try expectInitRefused(alloc, sock_path, error.AddressInUse);

    // And the path is left alone: we could not identify it as a dead
    // daemon's socket, so it was never ours to delete.
    const st = try std.posix.fstatat(
        std.posix.AT.FDCWD,
        sock_path,
        std.posix.AT.SYMLINK_NOFOLLOW,
    );
    try std.testing.expect(std.posix.S.ISLNK(st.mode));
}

test "Server: a non-socket at the path is refused, not deleted" {
    const alloc = std.testing.allocator;

    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    const dir_path = tmp.path();
    const file_path = try std.fmt.allocPrint(alloc, "{s}/notes.txt", .{dir_path});
    defer alloc.free(file_path);

    const contents = "mux must not eat this";
    try tmp.dir.writeFile(.{ .sub_path = "notes.txt", .data = contents });

    // Nothing but the stat separates this file from a dead daemon's socket:
    // Linux answers a connect to a regular file with ECONNREFUSED, the very
    // same errno a dead socket gives, so identifying stale sockets by the
    // connect result alone would delete this file. `claim` stats first and
    // never connects to a non-socket at all, and this test is what pins that.
    try expectInitRefused(alloc, file_path, error.SockPathNotASocket);

    const after = try tmp.dir.readFileAlloc(alloc, "notes.txt", 1024);
    defer alloc.free(after);
    try std.testing.expectEqualStrings(contents, after);
}

// BEFORE the QUIC integration tests, and not cosmetically: widening `deinit`'s
// `.owned` arm to free a `.borrowed` listener makes the daemon free one it does
// not own, and every test down there hands one in and frees it itself — so the
// regression double-frees and WEDGES in wolfSSL teardown, printing nothing at
// all. Here it fails first and says what broke.
//
// Worth knowing before moving it: a FAILURE here returns before its own
// `l.deinit()`, so the process-global latch stays held and every QUIC test
// below refuses its bind rather than reaching the free.
test "Server: a listener attached by the caller survives srv.deinit — ownership stays with whoever bound it" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "ep3");
    defer td.deinit();
    var kbuf: [128]u8 = undefined;
    const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
    try xdg.writeNewKey(key_path);
    const key = try quic.Key.load(key_path);

    // Bound out here, as main.zig's explicit `--quic` path does. Deliberately NOT
    // deferred: which side frees this IS the claim, so the frees are written in
    // the order the claim is about — a deferred free would be a double free the
    // moment the guard stopped guarding.
    const addr = try std.net.Address.parseIp("0.0.0.0", 0);
    const l = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
    const port = boundUdpPort(l);
    try std.testing.expect(port != 0);

    {
        try td.start(.{ .shell = "/bin/sh" });
        defer td.shutdown();

        // The two lines main.zig's run() uses, in its order.
        l.setHandler(td.srv.quicHandler());
        td.srv.attachQuic(l);
        // attachQuic adopts the reference and nothing else: the tag that
        // decides who frees says borrowed, which is what the switch in
        // deinit reads.
        try std.testing.expect(td.srv.quic == .borrowed);

        // The reporting path answers from the listener it was handed, with
        // no env and no key resolution — the same short circuit the lazy
        // path takes once it has bound.
        try std.testing.expectEqual(port, td.srv.endpointPortFrom(null, null, null));
    }

    // The daemon's `deinit` has run and the listener must have survived it: the
    // process-global latch is still held, so a fresh bind is refused. Without
    // this, a deinit that freed somebody else's listener shows up only as a hang.
    try std.testing.expectError(
        error.ListenerAlreadyRunning,
        quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms),
    );

    // And it is still ours to free, which is the other half of "survived".
    l.deinit();
    const l2 = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
    l2.deinit();
}

test "Server: a clean exit takes its socket file with it" {
    const alloc = std.testing.allocator;
    var td = try h.TestDaemon.init(alloc, "unlink", .{ .shell = "/bin/sh" });
    defer td.deinit();
    try std.fs.cwd().access(td.sock_path, .{});
    td.shutdown();

    // The EFFECT, not the reasoning: a guard comparing a sockfs inode against a
    // filesystem one is false every time, and the leftover socket is
    // indistinguishable from a correct refusal to delete someone else's.
    try std.testing.expectError(
        error.FileNotFound,
        std.fs.cwd().access(td.sock_path, .{}),
    );
}

test "Server: a socket that replaced ours is not ours to delete" {
    const alloc = std.testing.allocator;
    var td = try h.TestDaemon.init(alloc, "replaced", .{ .shell = "/bin/sh" });
    defer td.deinit();

    // A second daemon takes the path over while the first is still up —
    // the shape of the field incident behind the socket-steal fix, and the
    // whole reason the unlink is guarded rather than unconditional.
    try std.fs.cwd().deleteFile(td.sock_path);
    const addr = try std.net.Address.initUnix(td.sock_path);
    var survivor = try addr.listen(.{});
    defer survivor.deinit();

    td.shutdown();

    // The survivor's socket is still there. Deleting it would leave the
    // daemon that owns it listening on a path nothing can reach.
    try std.fs.cwd().access(td.sock_path, .{});
    const st = try std.posix.fstatat(std.posix.AT.FDCWD, td.sock_path, 0);
    try std.testing.expect(std.posix.S.ISSOCK(st.mode));
    std.fs.cwd().deleteFile(td.sock_path) catch {};
}

test "Server: stats reports live client slots, and the number comes down again" {
    const alloc = std.testing.allocator;
    var td = try h.TestDaemon.init(alloc, "gauge", .{ .shell = "/bin/sh" });
    defer td.deinit();

    var buf: [Server.stats_text_len]u8 = undefined;
    // indexOf rather than endsWith: the text no longer ends on the
    // global gauge — a per-session tail follows it — so the assertion has
    // to name the field it means rather than lean on it being last.
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=0") != null);

    // Slots filled directly: what is under test is the gauge, not the
    // machinery that fills them. Emptied by defer so that a failed
    // assertion below still leaves teardown a valid Server — otherwise the
    // real failure is buried under an abort from closing fd -1.
    defer {
        td.srv.clients[0] = null;
        td.srv.clients[3] = null;
    }
    td.srv.clients[0] = .{ .sink = .{ .socket = -1 } };
    td.srv.clients[3] = .{ .sink = .{ .socket = -1 } };
    // Filled with no session (session: null, the promoted-but-unattached
    // shape), so this raises the daemon-wide gauge on the main line
    // without moving the default session's own per-session count — that
    // is the boundary the next assertion pins.
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=2") != null);

    // A gauge, not a counter: the whole reason for adding it is watching
    // occupancy clear, so it has to be able to go down.
    td.srv.clients[0] = null;
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=1") != null);
    td.srv.clients[3] = null;
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "clients=0") != null);

    // ...and `attaches=` is the COUNTER beside it, which is why both exist:
    // occupancy went 0 → 2 → 0 above without an attach frame ever arriving, so
    // this is still 0. It answers "did anyone attach since I last looked".
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "attaches=0") != null);
    td.srv.stats.attaches += 1;
    try std.testing.expect(std.mem.indexOf(u8, try td.srv.statsText(&buf), "attaches=1") != null);
    td.srv.clients[0] = .{ .sink = .{ .socket = -1 } };
    // A second client seating itself does not move it either — only the
    // attach handler does, and a gauge moving is not an attach.
    const both = try td.srv.statsText(&buf);
    try std.testing.expect(std.mem.indexOf(u8, both, "clients=1") != null);
    try std.testing.expect(std.mem.indexOf(u8, both, "attaches=1") != null);
    td.srv.clients[0] = null;

    // The fields the harnesses parse are still where they were: appended,
    // never reordered. The old leading `seq=` was one session's tracker
    // and moved into the per-session tail; the main line now starts
    // with the counters that were always daemon-global.
    const text = try td.srv.statsText(&buf);
    try std.testing.expect(std.mem.startsWith(u8, text, "snapshots="));
    try std.testing.expect(std.mem.indexOf(u8, text, " snapshot_equiv_bytes=") != null);
    try std.testing.expect(std.mem.indexOf(u8, text, " sessions=1") != null);
    try std.testing.expect(std.mem.indexOf(u8, text, " session 0 clients=0 seq=0") != null);
}

// A hand-counted bound is the failure this pins: `statsText` writes into a fixed
// buffer, so an under-count is `error.WriteFailed` and the `.stats_req` arm
// drops the client — `mux d stats` reports the daemon gone rather than a short
// line. A new counter is what makes these values reachable.
test "Server: the stats buffer holds the widest reply its format can print" {
    var buf: [Server.stats_text_len]u8 = undefined;
    var w: std.Io.Writer = .fixed(&buf);
    const widest = std.math.maxInt(u64);
    // A wrong count here is a compile error, not a silent pass: `print`
    // checks the tuple against the format string.
    try w.print(Server.stats_main_fmt, .{widest} ** 11);
    const name = "n" ** proto.session_name_max;
    for (0..max_sessions) |_| {
        try w.print(Server.stats_session_fmt, .{ name, widest, widest });
    }
}

test "Server: stop_req from a bare connection requests shutdown; run returns 0" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "stop");
    defer td.deinit();

    // The flag is global by necessity (a signal handler shares it); reset
    // so this test neither inherits a stale request nor leaves one behind.
    shutdown_flag.store(false, .release);
    defer shutdown_flag.store(false, .release);

    try td.start(.{ .shell = "/bin/sh" });

    const c = try dial.dial(td.sock_path);
    defer c.close();
    // No attach first: the frame must be honored from the OBSERVER
    // dispatch, which is where a bare `mux d stop` connection lives.
    try proto.writeFrame(c.handle, .stop_req, "");

    // Bounded, so the mutation run FAILS here instead of hanging the
    // suite: 100 iterations x 50ms is the deadline, the flag is the exit.
    var i: usize = 0;
    while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) {
        try td.srv.pumpOnce(50);
    }
    try std.testing.expect(shutdown_flag.load(.acquire));

    // `run()` re-checks the flag before its first poll, so this returns without
    // pumping. It is the one unbounded call here, safe only because the expect
    // above guarantees the flag. 0: a daemon asked to stop did its job, and a
    // supervisor reads nonzero as a crash-loop.
    try std.testing.expectEqual(@as(u8, 0), try td.srv.run());
}

test "Server: stop_req from an attached client is honored too" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "stop2");
    defer td.deinit();

    shutdown_flag.store(false, .release);
    defer shutdown_flag.store(false, .release);

    try td.start(.{ .shell = "/bin/sh" });

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    // Both frames go out before any pump and the stop still lands in
    // `handleFrame`: one observer read takes both into the buffer, the attach
    // promotes the connection with that buffer, and the stop drains as the
    // client's through `pushInbound`.
    try proto.writeFrame(c.handle, .stop_req, "");

    var i: usize = 0;
    while (i < 100 and !shutdown_flag.load(.acquire)) : (i += 1) {
        try td.srv.pumpOnce(50);
    }
    try std.testing.expect(shutdown_flag.load(.acquire));
}

test "Server: endpoint_req binds a listener lazily, answers the same port on both dispatches, and deinit hands it back" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "ep");
    defer td.deinit();
    var kbuf: [128]u8 = undefined;
    const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
    try xdg.writeNewKey(key_path);
    const key = try quic.Key.load(key_path);

    // Scoped so the daemon's deinit runs before the release check below, and still
    // runs if an assertion inside fails.
    var first_port: u16 = 0;
    {
        try td.start(.{ .shell = "/bin/sh" });
        defer td.shutdown();

        // Key resolution first, through the env seam: MUX_KEY_FILE's value
        // handed in rather than set, since tests cannot setenv.
        first_port = td.srv.endpointPortFrom(key_path, null, null);
        try std.testing.expect(first_port != 0);
        try std.testing.expect(td.srv.quicListener() != null);

        // Now the wire, on a listener that already exists — which is what
        // keeps this half from depending on the test machine having a real
        // ~/.config/mux/key. The observer arm is the load-bearing one:
        // `mux d endpoint` never attaches.
        const obs = try dial.dial(td.sock_path);
        defer obs.close();
        try proto.writeFrame(obs.handle, .endpoint_req, "");
        const reply = (try awaitFrame(alloc, &td.srv, obs.handle, .endpoint_reply, 200)) orelse
            return error.NoEndpointReply;
        defer reply.deinit(alloc);
        try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply.payload));

        // Asked twice, answered the same: a second bind attempt would be
        // refused by the process-global latch and surface as port 0, so
        // equality is the stronger claim than "nonzero again".
        try proto.writeFrame(obs.handle, .endpoint_req, "");
        const reply2 = (try awaitFrame(alloc, &td.srv, obs.handle, .endpoint_reply, 200)) orelse
            return error.NoSecondEndpointReply;
        defer reply2.deinit(alloc);
        try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply2.payload));

        // The attached-client arm answers the same verb the same way, on a
        // separate connection that attaches first.
        const cl = try dial.dialAttach(td.sock_path, 80, 24);
        defer cl.close();
        try proto.writeFrame(cl.handle, .endpoint_req, "");
        const reply3 = (try awaitFrame(alloc, &td.srv, cl.handle, .endpoint_reply, 400)) orelse
            return error.NoAttachedEndpointReply;
        defer reply3.deinit(alloc);
        try std.testing.expectEqual(first_port, try proto.decodeEndpointReply(reply3.payload));
    }

    // A lazily bound listener is the server's to free, and this is the
    // observable for that: the latch is only released by a deinit that
    // happened, so a fresh bind succeeding proves the old one is gone.
    const addr = try std.net.Address.parseIp("0.0.0.0", 0);
    const l2 = try quic_server.Listener.bind(alloc, addr, key, quic.default_idle_ms);
    l2.deinit();
}

test "Server: endpointPortFrom refuses without a key, survives it, and prefers a listener already bound" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "ep2");
    defer td.deinit();
    var kbuf: [128]u8 = undefined;
    const key_path = try std.fmt.bufPrint(&kbuf, "{s}/key", .{td.tmp.path()});
    try xdg.writeNewKey(key_path);
    const key = try quic.Key.load(key_path);

    try td.start(.{ .shell = "/bin/sh" });

    // No env at all: keyPathFrom has nothing to build a default from, so
    // there is no key and the answer is 0 rather than a crash.
    try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(null, null, null));
    // Named but absent: Key.load fails, same answer.
    var mbuf: [128]u8 = undefined;
    const missing = try std.fmt.bufPrint(&mbuf, "{s}/absent-key", .{td.tmp.path()});
    try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(missing, null, null));
    // A default path pointing at a directory with no key in it: resolvable,
    // not present, so the access check refuses before Key.load is asked.
    try std.testing.expectEqual(@as(u16, 0), td.srv.endpointPortFrom(null, td.tmp.path(), null));

    // Refusing left no half-state behind: nothing was bound, and the daemon
    // is still able to bind when a real key does turn up.
    try std.testing.expect(td.srv.quic == .none);
    const port = try td.srv.lazyBindQuic(key);
    try std.testing.expect(port != 0);

    // With a listener in hand the answer is the bound port, and key
    // resolution is never reached — all-null env would otherwise return 0.
    try std.testing.expectEqual(port, td.srv.endpointPortFrom(null, null, null));
}

test "Server: status_req is answered on an attached client and on a bare observer" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "status");
    defer td.deinit();

    // /bin/cat: a session that produces no output of its own, so nothing
    // moves the state these assertions describe.
    try td.start(.{ .shell = "/bin/cat" });

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    try proto.writeFrame(c.handle, .status_req, "");
    const f = (try awaitFrame(alloc, &td.srv, c.handle, .status_reply, 400)) orelse
        return error.NoAttachedStatusReply;
    defer f.deinit(alloc);
    const st = try proto.decodeStatusReply(f.payload);
    try std.testing.expectEqual(@as(u16, 80), st.cols);
    try std.testing.expectEqual(@as(u16, 24), st.rows);
    try std.testing.expect(!st.alt_screen);
    try std.testing.expectEqual(proto.CmdPhase.at_prompt, st.cmd.phase);
    // No C has ever been seen on this session, so the reply reports the
    // regime it is actually in — pgid, not marks. That field is a report of
    // which mechanism would decide, not a claim that one just did.
    try std.testing.expectEqual(proto.Mechanism.pgid, st.cmd.mechanism);

    // `mux a status` never attaches, so the observer arm is the load-bearing
    // one — same reasoning as endpoint_req's, and the same failure if it is
    // missing: the frame falls into `else => {}` and the caller hangs.
    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    try proto.writeFrame(obs.handle, .status_req, "");
    const f2 = (try awaitFrame(alloc, &td.srv, obs.handle, .status_reply, 400)) orelse
        return error.NoObserverStatusReply;
    defer f2.deinit(alloc);
    const st2 = try proto.decodeStatusReply(f2.payload);
    try std.testing.expectEqual(@as(u16, 80), st2.cols);
    try std.testing.expectEqual(@as(u16, 24), st2.rows);
    try std.testing.expectEqual(proto.CmdPhase.at_prompt, st2.cmd.phase);
}

/// Doubles as an absence probe: with the positives asserted, a bounded `false`
/// means they never crossed onto this connection.
fn pumpUntilReplicaSees(
    alloc: std.mem.Allocator,
    srv: *Server,
    fd: std.posix.fd_t,
    rep: *Grid,
    needle: []const u8,
    iters: usize,
) !bool {
    // The pumping twin of `h.awaitReplicaText`: same sink, and a wait that
    // drives the daemon because this test's daemon has no thread.
    var feed: h.ReplicaFeed = .{ .alloc = alloc, .w = .{ .replica = rep, .needle = needle } };
    _ = h.awaitFrameSink(alloc, srv, fd, h.never_from_daemon, iters, feed.sink()) catch |err| switch (err) {
        error.TextArrived => return true,
        else => return err,
    };
    return false;
}

test "Server: two named sessions hold two shells with independent content" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "twonames");
    defer td.deinit();

    // /bin/cat sessions: what is typed comes straight back and nothing else
    // ever prints, so each grid holds exactly its own client's marker.
    try td.start(.{ .shell = "/bin/cat" });

    const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();

    try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n");
    try proto.writeFrame(cb.handle, .input, "MARKER-BETA\n");

    var rep_a = try Grid.init(alloc, 80, 24);
    defer rep_a.deinit();
    var rep_b = try Grid.init(alloc, 80, 24);
    defer rep_b.deinit();

    // Liveness first: each client converges on its own marker.
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-ALPHA", 400),
    );
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-BETA", 400),
    );

    // The boundary: keep pumping so any misdirected broadcast has every
    // chance to arrive, then assert the other session's marker never does.
    try std.testing.expect(
        !try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-BETA", 30),
    );
    try std.testing.expect(
        !try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-ALPHA", 30),
    );

    // Two real sessions stood up beside the default, each under its name.
    try std.testing.expect(td.srv.sessions.table[1] != null);
    try std.testing.expect(td.srv.sessions.table[2] != null);
    const si_a = td.srv.sessions.find("a") orelse return error.SessionAMissing;
    const si_b = td.srv.sessions.find("b") orelse return error.SessionBMissing;
    try std.testing.expect(si_a != si_b);
    try std.testing.expect(si_a != 0 and si_b != 0);
    try std.testing.expectEqualStrings("a", td.srv.sessions.table[si_a].?.name());
    try std.testing.expectEqualStrings("b", td.srv.sessions.table[si_b].?.name());
}

test "SessionTable: a name read from the table is that session's own, after the table changes again" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "ownname", .{ .shell = "/bin/sh" });
    defer td.deinit();

    // Four slots, no two names the same length. The escape this pins gave
    // every session the LAST one's bytes truncated to its own length, so a
    // table of equal-length names sees nothing, and a table of one sees less.
    td.srv.sessions.table[1] = try SessionTable.create(alloc, td.srv.spawn_plan, "aaa", 80, 24, null);
    td.srv.sessions.table[2] = try SessionTable.create(alloc, td.srv.spawn_plan, "bb", 80, 24, null);

    // Read out and HELD, which is the shape that broke: a name is a slice,
    // and the question is whose bytes it points at once the walk that
    // produced it is over. Inline storage per slot is what makes this safe;
    // one buffer shared by the table would not be.
    const held = [_][]const u8{ td.srv.ses(0).name(), td.srv.ses(1).name(), td.srv.ses(2).name() };

    // The table changes under the slices, and the newest name is READ —
    // the read is the half that matters, because that is what would write
    // over a buffer the table shared.
    td.srv.sessions.table[3] = try SessionTable.create(alloc, td.srv.spawn_plan, "zzzzz", 80, 24, null);
    try std.testing.expectEqualStrings("zzzzz", td.srv.ses(3).name());

    try std.testing.expectEqualStrings(proto.default_session, held[0]);
    try std.testing.expectEqualStrings("aaa", held[1]);
    try std.testing.expectEqualStrings("bb", held[2]);
}

test "Server: every session shell is told the socket it lives on and its own name" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "selfenv", .{ .shell = "/bin/sh" });
    defer td.deinit();

    // The shell does the comparing, so a pass means the CHILD saw the
    // daemon's own path — not that this test can rebuild the string. The
    // marker is assembled by printf from pieces, never typed whole, so the
    // grid's echo of the command line can never satisfy the grep.
    const probe = try std.fmt.allocPrint(
        alloc,
        "[ \"$MUX_SOCK\" = \"{s}\" ] && printf 'SOCK%s-%s\\n' OK \"$MUX_SESSION\"\n",
        .{td.sock_path},
    );
    defer alloc.free(probe);

    const c0 = try dial.dialAttachNamed(td.sock_path, 80, 24, "");
    defer c0.close();
    const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer ca.close();

    try proto.writeFrame(c0.handle, .input, probe);
    try proto.writeFrame(ca.handle, .input, probe);

    var rep_0 = try Grid.init(alloc, 80, 24);
    defer rep_0.deinit();
    var rep_a = try Grid.init(alloc, 80, 24);
    defer rep_a.deinit();

    // The default session's name on the wire is empty; what its shell is
    // told is the RESOLVED name, because that is what a client comparing
    // `--session` against it will have resolved too.
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, c0.handle, rep_0, "SOCKOK-0", 600),
    );
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "SOCKOK-a", 600),
    );
}

test "Server: a bare 20-byte attach lands in the default session" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "bare", .{ .shell = "/bin/cat" });
    defer td.deinit();

    // The wire spelling from before named sessions, byte for byte: no name
    // tail at all. An
    // old client must land in the default session, not create a nameless
    // one beside it.
    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
        return error.NoSnapshotOnBareAttach;
    f.deinit(alloc);

    const si = td.srv.clients[0].?.session orelse return error.SlotHoldsNoSession;
    try std.testing.expectEqualStrings(proto.default_session, td.srv.sessions.table[si].?.name());
    // And nothing was created for it: the default was already there.
    for (td.srv.sessions.table[1..]) |slot| try std.testing.expect(slot == null);
}

test "Server: an attach past max_sessions is refused with exit_status, sessions intact" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "tablefull", .{ .shell = "/bin/cat" });
    defer td.deinit();

    // The default session holds slot 0, so `max_sessions - 1` more fill the
    // table. Each attach is confirmed before the next, so the refusal below is
    // unambiguously "no session slot" — and each connection CLOSES, because
    // the two tables are the same size: holding all of them open would leave
    // the client table one attach from full, and the refusal under test could
    // then be either table's. The session outlives its client, which makes
    // closing safe.
    for (1..max_sessions) |i| {
        var nb: [8]u8 = undefined;
        const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i});
        const c = try dial.dialAttachNamed(td.sock_path, 80, 24, nm);
        defer c.close();
        const f = (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400)) orelse
            return error.NoSnapshotFillingTable;
        f.deinit(alloc);
    }
    // The closes above are only visible to the daemon once it polls, and a
    // client slot it still believes is live would refuse the probe below
    // before the session table ever got asked.
    for (0..8) |_| try td.srv.pumpOnce(1);

    // One name past the table gets the same honest no a full client table
    // gives.
    const extra = try dial.dialAttachNamed(td.sock_path, 80, 24, "extra");
    defer extra.close();
    const f = (try awaitFrame(alloc, &td.srv, extra.handle, .exit_status, 400)) orelse
        return error.NoRefusal;
    defer f.deinit(alloc);
    try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1);

    // And it cost nobody anything: every slot's shell is live, none exited.
    for (0..max_sessions) |si| {
        try std.testing.expect(td.srv.sessions.table[si] != null);
        try std.testing.expect(td.srv.sessions.table[si].?.pty.checkExited() == null);
    }
}

// PLURAL on purpose: the grace is a per-child number, and one session cannot
// tell "one grace for the table" from "one grace each". A supervisor's stop
// timeout kills a daemon that spends the second shape.
test "Server: a table of TERM-ignoring shells costs one grace, not one each" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "stubborn");
    defer td.deinit();

    // The stubborn shell is the harness's, and its doors do not return until
    // that session's own traps are armed. The arming is what makes the child
    // ignore HUP and TERM, and only a child that ignores them pays the grace
    // at all, so a shell signalled before its `trap` line ran would let every
    // assertion below hold with no stubborn shell in the table.
    try td.startStubborn(alloc, .{});

    // Eight beside the default: enough that serial grace (9 x 500ms) and
    // shared grace (500ms) cannot be confused, cheap enough to spawn.
    for (1..9) |i| {
        var nb: [8]u8 = undefined;
        const nm = try std.fmt.bufPrint(&nb, "s{d}", .{i});
        const c = try td.attachStubborn(alloc, nm, 80, 24);
        defer c.close();
    }

    var kids: [max_sessions]std.posix.pid_t = undefined;
    var n: usize = 0;
    for (&td.srv.sessions.table) |*slot| {
        if (slot.* == null) continue;
        const s = &slot.*.?;
        try std.testing.expect(s.pty.checkExited() == null);
        kids[n] = s.pty.child;
        n += 1;
    }
    try std.testing.expectEqual(@as(usize, 9), n);

    var t = try std.time.Timer.start();
    td.shutdown();
    const elapsed_ms = t.read() / std.time.ns_per_ms;
    if (elapsed_ms >= 1500) {
        std.debug.print(
            "teardown of {d} stubborn shells took {d}ms: the grace is being spent per child, not shared\n",
            .{ n, elapsed_ms },
        );
        return error.TeardownGraceNotShared;
    }

    // Ask the OS, not the daemon: a pid that was reaped names no process at
    // all, so `kill(pid, 0)` must fail. That catches a survivor, and it
    // catches a zombie the daemon claimed to have waited for too — a zombie
    // is still a process table entry and `kill(pid, 0)` on one SUCCEEDS,
    // which is the same verdict the /proc read this replaces gave, since a
    // zombie keeps its /proc directory. Spelled through `kill` rather than
    // /proc because every OS has it.
    for (kids[0..n]) |pid| {
        try std.testing.expectError(error.ProcessNotFound, std.posix.kill(pid, 0));
    }
}

test "Server: an invalid name on the wire is refused, not created" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "badname", .{ .shell = "/bin/cat" });
    defer td.deinit();

    // A raw-wire client can spell any bytes it likes; a name no tool could
    // ever address must die here, not become a session.
    const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "has space");
    defer c.close();
    const f = (try awaitFrame(alloc, &td.srv, c.handle, .exit_status, 400)) orelse
        return error.NoRefusal;
    defer f.deinit(alloc);
    try std.testing.expect(f.payload.len == 1 and f.payload[0] == 1);
    try std.testing.expect(td.srv.sessions.table[1] == null);
}

test "Server: safeName refuses to hand the console a peer's bytes" {
    // status_req's payload is the whole frame, capped only by max_payload,
    // and the decoders deliberately do not validate names. These strings
    // are what a peer can put in one; none of them may reach stderr.
    try std.testing.expectEqualStrings("<invalid>", SessionTable.safeName("\x1b]0;pwned\x07"));
    try std.testing.expectEqualStrings("<invalid>", SessionTable.safeName("\x1b[2J"));
    try std.testing.expectEqualStrings("<invalid>", SessionTable.safeName("has space"));
    try std.testing.expectEqualStrings("<invalid>", SessionTable.safeName("a" ** (proto.session_name_max + 1)));

    // A legal name still prints as itself — the point is to filter, not to
    // stop naming the session an operator asked about.
    try std.testing.expectEqualStrings("b", SessionTable.safeName("b"));
    // ...and an empty tail is the default session, named, not "<invalid>".
    try std.testing.expectEqualStrings(proto.default_session, SessionTable.safeName(""));
}

/// One script serves every session the daemon spawns (there is one
/// spawn_plan), so "session a dies while b lives" is spelled by what each
/// client TYPES, not by what each shell is.
fn writeMortalScript(tmp: *TmpDir) !void {
    try tmp.dir.writeFile(.{
        .sub_path = "mortal.sh",
        .data =
        \\#!/bin/sh
        \\while read -r line; do
        \\  case "$line" in
        \\    die*) exit "${line#die }" ;;
        \\  esac
        \\  echo "echo:$line"
        \\done
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
}

test "Server: one session's shell exiting drops only its clients; the daemon carries on" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "onedies");
    defer td.deinit();

    try writeMortalScript(&td.tmp);
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

    try td.start(.{ .shell = script });

    const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer ca.close();
    const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const si_a = td.srv.sessions.find("a") orelse return error.SessionAMissing;

    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();

    // b's liveness established BEFORE a dies, so its survival below is a
    // comparison and not a hope.
    var rep_b = try Grid.init(alloc, 80, 24);
    defer rep_b.deinit();
    try proto.writeFrame(cb.handle, .input, "pre\n");
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "echo:pre", 400),
    );

    // Kill a's shell and pump until its client hears so. b is what makes
    // this a comparison: a's death must reach a's client and nobody else's.
    try proto.writeFrame(ca.handle, .input, "die 0\n");
    var status: ?u8 = null;
    if (try awaitFrame(alloc, &td.srv, ca.handle, .exit_status, 500)) |frame| {
        defer frame.deinit(alloc);
        try std.testing.expectEqual(@as(usize, 1), frame.payload.len);
        status = frame.payload[0];
    }
    try std.testing.expectEqual(@as(?u8, 0), status);

    // The session is gone — slot null, name free — and no client slot still
    // points where it was.
    try std.testing.expect(td.srv.sessions.table[si_a] == null);
    try std.testing.expect(td.srv.sessions.find("a") == null);
    for (td.srv.clients) |slot| {
        const cs = slot orelse continue;
        if (cs.session) |si| try std.testing.expect(si != si_a);
    }
    // exit_status was the last frame queued before the drop, so what a's
    // connection reads next is EOF — the daemon-side close, not a reset.
    const Stray = struct {
        fn on(_: ?*anyopaque, _: proto.Frame) anyerror!void {
            return error.FramesAfterExitStatus;
        }
    };
    try std.testing.expect(try h.awaitClosed(alloc, ca.handle, 1000, .{ .on = Stray.on }));

    // And b never noticed: still attached, still echoing.
    try proto.writeFrame(cb.handle, .input, "post\n");
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "echo:post", 400),
    );
}

/// `run` on a thread; the code it answers is a fact only `run` holds.
fn runThread(srv: *Server, out: *?u8) void {
    out.* = srv.run() catch |err| {
        std.debug.print("run() failed: {t}\n", .{err});
        return;
    };
}

/// Ask the daemon for its stats until `want` appears. A connection of its OWN
/// per call, which is the assertion's teeth: a reply that misses its 200 ms
/// stays buffered, so a shared socket lets a later question be answered by an
/// earlier one's frame — generated before the very event being waited on.
fn waitStats(alloc: std.mem.Allocator, sock_path: []const u8, want: []const u8) !void {
    const obs = try dial.dial(sock_path);
    defer obs.close();
    var tries: usize = 0;
    while (tries < 40) : (tries += 1) {
        try proto.writeFrame(obs.handle, .stats_req, "");
        const frame = (try awaitFrameOn(alloc, obs.handle, .stats_reply, 200)) orelse continue;
        defer frame.deinit(alloc);
        if (std.mem.indexOf(u8, frame.payload, want) != null) return;
    }
    std.debug.print("stats never said {s}\n", .{want});
    return error.StatsNeverSaid;
}

/// The socket half of the claim: every wait here runs while `run` pumps on
/// its own thread, so a daemon that RETURNED from `run` answers none of it
/// and each one times out.
fn probeEmptiedDaemon(alloc: std.mem.Allocator, sock_path: []const u8) !void {
    // Attach the default session, then hang up its shell — the daemon's
    // only one, so the table empties behind it. 7 rather than 0 so the
    // caller's exit-code pin can tell the two contracts apart: under the
    // retired one this shell's code WAS the daemon's.
    const c1 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session);
    defer c1.close();
    const snap = (try awaitFrameOn(alloc, c1.handle, .snapshot, 4000)) orelse
        return error.NoFirstSnapshot;
    snap.deinit(alloc);

    // The liveness half, and not a nicety: the snapshot proves the DAEMON
    // seated this client, never that the script behind the pty has reached
    // its first `read`. A line written before it does sits in the tty's input
    // queue, and a shell that touches the terminal on startup discards it —
    // so `die` was echoed, never read, and the session outlived a test whose
    // whole subject is the session ending. Seen on macOS, where /bin/sh is
    // bash and the script's startup is longer; the same race is open on
    // Linux and merely lost less often. One round trip through the script
    // closes it.
    var rep = try Grid.init(alloc, 80, 24);
    defer rep.deinit();
    try proto.writeFrame(c1.handle, .input, "alive\n");
    if (!try h.awaitReplicaText(alloc, c1.handle, 8000, .{
        .replica = rep,
        .needle = "echo:alive",
    })) return error.ShellNeverRead;

    try proto.writeFrame(c1.handle, .input, "die 7\n");

    try waitStats(alloc, sock_path, "sessions=0");

    // Still serving with nothing to serve: a birth on the emptied daemon
    // takes the default name back, which is the attach `mux --sock PATH`
    // sends.
    const c2 = try dial.dialAttachNamed(sock_path, 80, 24, proto.default_session);
    defer c2.close();
    const reborn = (try awaitFrameOn(alloc, c2.handle, .snapshot, 4000)) orelse
        return error.EmptyDaemonRefusedABirth;
    reborn.deinit(alloc);
    try waitStats(alloc, sock_path, "sessions=1");
}

test "Server: a daemon outlives its last session and ends only on stop" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "livesempty");
    defer td.deinit();

    try writeMortalScript(&td.tmp);
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

    // The flag is global by necessity (a signal handler shares it); reset
    // so this test neither inherits a stale request nor leaves one behind.
    shutdown_flag.store(false, .release);
    defer shutdown_flag.store(false, .release);

    try td.start(.{ .shell = script });

    var code: ?u8 = null;
    const th = try std.Thread.spawn(.{}, runThread, .{ &td.srv, &code });
    // Captured, not propagated: nothing may return between the spawn and
    // the join, because `deinit` is a demolition list and must never run
    // under a live pump thread.
    const probed = probeEmptiedDaemon(alloc, td.sock_path);
    shutdown_flag.store(true, .release);
    th.join();
    try probed;

    // 0 and not 7. The shell this daemon was born with exited 7, and under
    // the retired contract that WAS the daemon's exit code — so this line
    // fails on a reap that still answers, not merely on a shutdown that
    // returns the wrong thing.
    try std.testing.expectEqual(@as(?u8, 0), code);
}

test "Server: a dead name re-attaches as a fresh session with a new epoch" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "reborn");
    defer td.deinit();

    try writeMortalScript(&td.tmp);
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/mortal.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

    try td.start(.{ .shell = script });

    // First instance of "a": hold on to what its attach answered — the
    // epoch its snapshots were stamped with, and the newest seq counted
    // under it. That pair is exactly what a surviving client would quote.
    const c1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer c1.close();
    const f1 = (try awaitFrame(alloc, &td.srv, c1.handle, .snapshot, 400)) orelse
        return error.NoFirstSnapshot;
    defer f1.deinit(alloc);
    const p1 = try proto.readSnapshotPrefix(f1.payload);
    try std.testing.expect(p1.epoch != 0);

    // Kill it, and pump until the name is free.
    try proto.writeFrame(c1.handle, .input, "die 0\n");
    const Freed = struct {
        srv: *Server,
        name: []const u8,
        fn yes(self: @This()) bool {
            return self.srv.sessions.find(self.name) == null;
        }
    };
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Freed{ .srv = &td.srv, .name = "a" }, Freed.yes));

    // Re-attach the dead name quoting the DEAD instance's seq and epoch — the
    // reconnect a client that missed the death sends. A fresh session cannot
    // delta-serve seqs counted by a shell it never was.
    const c2 = try dial.dial(td.sock_path);
    defer c2.close();
    var abuf: [proto.attach_max_len]u8 = undefined;
    try proto.writeFrame(
        c2.handle,
        .attach,
        proto.encodeAttachNamed(&abuf, 80, 24, p1.seq, p1.epoch, "a"),
    );
    // A delta here would mean the reborn session kept the old instance's
    // seq space, which is the bug this whole test exists for.
    const NoDelta = struct {
        fn on(_: ?*anyopaque, frame: proto.Frame) anyerror!void {
            if (frame.type == .delta) return error.DeltaAcrossInstances;
        }
    };
    // A refused attach closes the connection and a daemon that never
    // answered leaves it open; both reach here as "no snapshot arrived",
    // which is the fact the assertion is about either way.
    const f2 = (try h.awaitFrameSink(alloc, &td.srv, c2.handle, .snapshot, 400, .{ .on = NoDelta.on })) orelse
        return error.NoRebornSnapshot;
    defer f2.deinit(alloc);
    const p2 = try proto.readSnapshotPrefix(f2.payload);
    try std.testing.expect(p2.epoch != 0);
    try std.testing.expect(p2.epoch != p1.epoch);
    try std.testing.expect(td.srv.sessions.find("a") != null);
}

// ---------------------------------------------------------------------------
// Every out-of-band instrument learns a session name: dump, the observer's
// `status_req`, stats, and the attached-client tail-match rule.
// ---------------------------------------------------------------------------

test "Server: dump names a session; an unknown name answers in words" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "dumpnames", .{ .shell = "/bin/cat" });
    defer td.deinit();

    const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();

    try proto.writeFrame(ca.handle, .input, "MARKER-ALPHA\n");
    try proto.writeFrame(cb.handle, .input, "MARKER-BETA\n");

    var rep_a = try Grid.init(alloc, 80, 24);
    defer rep_a.deinit();
    var rep_b = try Grid.init(alloc, 80, 24);
    defer rep_b.deinit();
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, ca.handle, rep_a, "MARKER-ALPHA", 400),
    );
    try std.testing.expect(
        try pumpUntilReplicaSees(alloc, &td.srv, cb.handle, rep_b, "MARKER-BETA", 400),
    );

    // Asked from a's own connection, for b: dump answers the payload's
    // tail, never the asker's own session.
    try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'b' });
    const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .dump_reply, 400)) orelse
        return error.NoDumpReply;
    defer reply.deinit(alloc);
    try std.testing.expect(std.mem.indexOf(u8, reply.payload, "MARKER-BETA") != null);
    try std.testing.expect(std.mem.indexOf(u8, reply.payload, "MARKER-ALPHA") == null);

    // An unknown name is a dump_reply in words, not a fake connection loss
    // — the only channel this verb has for "no" is the reply itself.
    try proto.writeFrame(ca.handle, .debug_dump, &[_]u8{ 1, 'z' });
    const bad = (try awaitFrame(alloc, &td.srv, ca.handle, .dump_reply, 400)) orelse
        return error.NoDumpReplyForBadName;
    defer bad.deinit(alloc);
    try std.testing.expectEqualStrings("mux d: no such session: z\n", bad.payload);
}

test "Server: an observer's status_req names a session by tail" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "obsstatus");
    defer td.deinit();

    // The one-mark-on-command scripted shell, same shape as the OSC 133
    // test: it answers to being told, not on its own, so a running push
    // against b is unambiguously b's and never a's idle default.
    try td.tmp.dir.writeFile(.{
        .sub_path = "marks.sh",
        .data =
        \\#!/bin/sh
        \\read -r start
        \\printf '\033]133;C\007'
        \\read -r stop
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/marks.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

    try td.start(.{ .shell = script });

    const a = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer a.close();
    const b = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer b.close();
    const fa = (try awaitFrame(alloc, &td.srv, a.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const fb = (try awaitFrame(alloc, &td.srv, b.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    // Drive b into its C — a's shell is never told to run anything, so a
    // stays at its idle default and the two sessions read differently.
    try proto.writeFrame(b.handle, .input, "go\n");
    const push = (try awaitFrame(alloc, &td.srv, b.handle, .cmd_state, 500)) orelse
        return error.NoRunningPush;
    push.deinit(alloc);

    // A bare connection — an observer, never attached — asks about b by
    // name alone.
    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    try proto.writeFrame(obs.handle, .status_req, "b");
    const reply = (try awaitFrame(alloc, &td.srv, obs.handle, .status_reply, 400)) orelse
        return error.NoStatusReply;
    defer reply.deinit(alloc);
    const st = try proto.decodeStatusReply(reply.payload);
    try std.testing.expectEqual(proto.CmdPhase.running, st.cmd.phase);
    try std.testing.expectEqual(proto.Mechanism.marks, st.cmd.mechanism);

    // An unknown name still ends the connection, since `status_reply` has no room
    // for words — but it says `exit_status` 1 FIRST. Silence made
    // `mux a status --session nosuch` report a dead daemon.
    try proto.writeFrame(obs.handle, .status_req, "z");
    const refusal = (try awaitFrame(alloc, &td.srv, obs.handle, .exit_status, 400)) orelse
        return error.NoRefusalForUnknownSession;
    defer refusal.deinit(alloc);
    try std.testing.expectEqualSlices(u8, &.{1}, refusal.payload);

    try std.testing.expect(try h.pumpUntilClosed(alloc, &td.srv, obs.handle, 2400, .{}));
}

test "Server: stats names every live session" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "statsnames", .{ .shell = "/bin/cat" });
    defer td.deinit();

    // The default session (bare attach) plus one named session: two live
    // sessions total, both of which must show up by name.
    const ca = try dial.dialAttach(td.sock_path, 80, 24);
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();
    const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    var buf: [Server.stats_text_len]u8 = undefined;
    const text = try td.srv.statsText(&buf);
    try std.testing.expect(std.mem.indexOf(u8, text, "sessions=2") != null);
    try std.testing.expect(std.mem.indexOf(u8, text, "session " ++ proto.default_session ++ " ") != null);
    try std.testing.expect(std.mem.indexOf(u8, text, "session b ") != null);
}

test "Server: sessions_req answers every live name, whoever asks" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "sessreq", .{ .shell = "/bin/cat" });
    defer td.deinit();

    const ca = try dial.dialAttach(td.sock_path, 80, 24);
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "work");
    defer cb.close();
    const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    // Asked from the DEFAULT session's connection, and the answer still
    // names both: the reply is about the daemon, not about the asker.
    try proto.writeFrame(ca.handle, .sessions_req, "");
    const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .sessions_reply, 400)) orelse
        return error.NoSessionsReply;
    defer reply.deinit(alloc);
    try std.testing.expectEqualStrings(proto.default_session ++ "\nwork", reply.payload);
}

test "Server: sessions_reply carries the daemon's own version behind the names" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "sessmeta", .{
        .shell = "/bin/cat",
        .version = "9.9.9-test",
    });
    defer td.deinit();

    const ca = try dial.dialAttach(td.sock_path, 80, 24);
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "work");
    defer cb.close();
    const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    try proto.writeFrame(ca.handle, .sessions_req, "");
    const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .sessions_reply, 400)) orelse
        return error.NoSessionsReply;
    defer reply.deinit(alloc);

    // The names still read as exactly the names — the sibling test above
    // pins a versionless daemon's payload byte-for-byte, so this one only
    // has to show the line rides BEHIND them, invisibly to the iterator.
    var it = proto.sessionsIter(reply.payload);
    try std.testing.expectEqualStrings(proto.default_session, it.next() orelse return error.NameLost);
    try std.testing.expectEqualStrings("work", it.next() orelse return error.NameLost);
    try std.testing.expect(it.next() == null);

    const meta = proto.parseSessionsMeta(reply.payload) orelse return error.MetaAbsent;
    try std.testing.expectEqualStrings("9.9.9-test", meta.version);
    // This test binary is still the file it was exec'd from, and the server
    // must say so: stale=true here would mean the bit is invented rather
    // than read off /proc. The true side needs a replaced binary under a
    // running daemon, which is the e2e journey's to stage.
    try std.testing.expect(!meta.stale);
}

/// "Every client of this session has gone" as a `pumpUntil` context. The
/// close of a socket is the peer's act; the daemon only learns about it on a
/// pump, so a test that asked for the count straight after `close()` would be
/// racing the reap.
const SessionEmptied = struct {
    srv: *Server,
    si: usize,

    fn reached(s: SessionEmptied) bool {
        return !s.srv.hasClientsIn(s.si);
    }
};

test "Server: sessions_reply states how many clients hold each session" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "sesshold", .{
        .shell = "/bin/cat",
        .version = "0.0.1-test",
    });
    defer td.deinit();

    // One held session and one nobody holds: a fixture where every session
    // had the same number of clients could not tell a real count from a
    // constant.
    const ca = try dial.dialAttach(td.sock_path, 80, 24);
    defer ca.close();
    (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400) orelse
        return error.NoSnapshotA).deinit(alloc);
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "wk");
    (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400) orelse
        return error.NoSnapshotB).deinit(alloc);
    cb.close();
    const wk = td.srv.sessions.find(proto.wireName("wk")) orelse return error.NoSession;
    try std.testing.expect(try h.pumpUntil(&td.srv, 2000, SessionEmptied{
        .srv = &td.srv,
        .si = wk,
    }, SessionEmptied.reached));

    try proto.writeFrame(ca.handle, .sessions_req, "");
    const reply = (try awaitFrame(alloc, &td.srv, ca.handle, .sessions_reply, 400)) orelse
        return error.NoSessionsReply;
    defer reply.deinit(alloc);

    try std.testing.expectEqual(@as(?u8, 1), proto.parseSessionsHolds(reply.payload, proto.default_session));
    try std.testing.expectEqual(@as(?u8, 0), proto.parseSessionsHolds(reply.payload, "wk"));

    // The holds lines ride behind the names invisibly, exactly as the meta
    // line does, and the meta line still parses out from beside them.
    var names = proto.sessionsIter(reply.payload);
    try std.testing.expectEqualStrings(proto.default_session, names.next() orelse return error.NameLost);
    try std.testing.expectEqualStrings("wk", names.next() orelse return error.NameLost);
    try std.testing.expect(names.next() == null);
    const meta = proto.parseSessionsMeta(reply.payload) orelse return error.MetaAbsent;
    try std.testing.expectEqualStrings("0.0.1-test", meta.version);
}

test "Server: an attached client's mismatched status tail is ignored" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "mismatch", .{ .shell = "/bin/cat" });
    defer td.deinit();

    const ca = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer ca.close();
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();
    const fa = (try awaitFrame(alloc, &td.srv, ca.handle, .snapshot, 400)) orelse
        return error.NoSnapshotA;
    fa.deinit(alloc);
    const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    // A tail naming a DIFFERENT session than the one this connection
    // attached to gets silence, never an answer about the tail instead:
    // bounded negative probe, since the absence is what is under test.
    try proto.writeFrame(ca.handle, .status_req, "b");
    if (try awaitFrame(alloc, &td.srv, ca.handle, .status_reply, 30)) |leak| {
        leak.deinit(alloc);
        return error.MismatchedTailAnswered;
    }

    // The empty tail is the "no opinion" spelling and always matches: the
    // reply arrives, and it is a's own state, not b's.
    try proto.writeFrame(ca.handle, .status_req, "");
    const ok = (try awaitFrame(alloc, &td.srv, ca.handle, .status_reply, 400)) orelse
        return error.NoReplyForEmptyTail;
    ok.deinit(alloc);
}

test "Server: a session-less slot's status_req resolves the tail like an observer's" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "quicstatus", .{ .shell = "/bin/cat" });
    defer td.deinit();

    // Stand up session "b" for real, over an ordinary attach.
    const cb = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer cb.close();
    const fb = (try awaitFrame(alloc, &td.srv, cb.handle, .snapshot, 400)) orelse
        return error.NoSnapshotB;
    fb.deinit(alloc);

    // The shape a QUIC connection has between handshake and its first attach: a
    // live client slot with NO session, reached through `pushInbound` exactly as
    // a datagram would be. A `status_req` arm that `orelse return`s such a slot
    // makes `mux a status --quic` time out. Slot 1, because slot 0 is claimed
    // and stomping it would leak its queued snapshot.
    const c = try connectedPair();
    defer std.posix.close(c.peer);
    td.srv.clients[1] = .{ .sink = .{ .socket = c.daemon } };

    var frame: std.ArrayList(u8) = .empty;
    defer frame.deinit(alloc);
    try proto.appendFrame(&frame, alloc, .status_req, "b");
    td.srv.pushInbound(1, frame.items);

    // queueFrame flushes synchronously (see its doc comment), so the reply
    // is already on the wire — no pump needed, same as the other
    // pushInbound-driven tests above.
    var got: ?proto.Frame = null;
    {
        var pfd = [_]std.posix.pollfd{
            .{ .fd = c.peer, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((try std.posix.poll(&pfd, 50)) != 0) got = try proto.readFrame(alloc, c.peer);
    }
    const reply = got orelse return error.NoStatusReplyForSessionlessSlot;
    defer reply.deinit(alloc);
    try std.testing.expectEqual(proto.MsgType.status_reply, reply.type);
    _ = try proto.decodeStatusReply(reply.payload);

    // A one-shot ask, not an attach: the slot still holds no session.
    try std.testing.expect(td.srv.clients[1] != null);
    try std.testing.expect(td.srv.clients[1].?.session == null);

    // An unknown name refuses with `exit_status` 1 and then drops the CLIENT. The
    // frame has to PRECEDE the close, or `mux a status --quic --session nosuch`
    // reads silence as a dead daemon. The SOCKET sink only, which flushes as it
    // queues; a quic sink reaches the peer through `reapClosing`'s last drain,
    // which a unit test cannot stand up.
    frame.clearRetainingCapacity();
    try proto.appendFrame(&frame, alloc, .status_req, "zz");
    td.srv.pushInbound(1, frame.items);
    try std.testing.expect(td.srv.clients[1] == null);

    const refusal = (try proto.readFrame(alloc, c.peer)) orelse
        return error.NoRefusalForUnknownSession;
    defer refusal.deinit(alloc);
    try std.testing.expectEqual(proto.MsgType.exit_status, refusal.type);
    try std.testing.expectEqualSlices(u8, &.{1}, refusal.payload);

    var drain: [8]u8 = undefined;
    try std.testing.expectEqual(@as(usize, 0), try std.posix.read(c.peer, &drain));
}

// ---------------------------------------------------------------------------
// Ending a session on request. Each test builds only the clients and sessions
// its own claim needs: the count in a refusal is per session, and a client is
// not its own "other".
// ---------------------------------------------------------------------------

fn shellPidOf(srv: *Server, name: []const u8) std.posix.pid_t {
    return srv.ses(srv.sessions.find(proto.wireName(name)).?).pty.child;
}

fn alive(pid: std.posix.pid_t) bool {
    std.posix.kill(pid, 0) catch return false;
    return true;
}

/// Asked of the OS, not of the daemon: a session table that has forgotten a
/// shell says nothing about whether the process is gone.
const Dead = struct {
    pid: std.posix.pid_t,
    fn yes(self: Dead) bool {
        return !alive(self.pid);
    }
};

test "Server: end_req with another client attached is refused with the count; forced, both see the exit" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "endreq", .{ .shell = "/bin/sh" });
    defer td.deinit();

    const a1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer a1.close();
    (try awaitFrame(alloc, &td.srv, a1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
    const a2 = try dial.dialAttachNamed(td.sock_path, 80, 24, "a");
    defer a2.close();
    (try awaitFrame(alloc, &td.srv, a2.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
    const b1 = try dial.dialAttachNamed(td.sock_path, 80, 24, "b");
    defer b1.close();
    (try awaitFrame(alloc, &td.srv, b1.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
    const pid_a = shellPidOf(&td.srv, "a");

    // an observer asks: every attached client is "other"
    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    var rq: [proto.end_req_max_len]u8 = undefined;
    try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, false, "a"));
    const r1 = (try awaitFrame(alloc, &td.srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r1.deinit(alloc);
    const v1 = proto.parseEndReply(r1.payload) orelse return error.BadEndReply;
    try std.testing.expect(!v1.accepted);
    try std.testing.expectEqual(@as(u8, 2), v1.others);
    try std.testing.expect(alive(pid_a));

    // a1 asks over its own link: it is not its own "other"
    try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, false, "a"));
    const r2 = (try awaitFrame(alloc, &td.srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r2.deinit(alloc);
    try std.testing.expectEqual(@as(u8, 1), (proto.parseEndReply(r2.payload) orelse return error.BadEndReply).others);
    try std.testing.expect(alive(pid_a));

    // forced: accepted, the shell dies, BOTH clients on "a" get exit_status, "b" is untouched
    try proto.writeFrame(a1.handle, .end_req, proto.encodeEndReq(&rq, true, "a"));
    const r3 = (try awaitFrame(alloc, &td.srv, a1.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r3.deinit(alloc);
    try std.testing.expect((proto.parseEndReply(r3.payload) orelse return error.BadEndReply).accepted);
    const x2 = (try awaitFrame(alloc, &td.srv, a2.handle, .exit_status, 400)) orelse return error.NoExitOnSibling;
    x2.deinit(alloc);
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Dead{ .pid = pid_a }, Dead.yes));
    try std.testing.expect(alive(shellPidOf(&td.srv, "b")));
}

test "Server: end_req alone on a session ends it at once, and an unknown name is refused" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "endsolo", .{ .shell = "/bin/sh" });
    defer td.deinit();

    const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "solo");
    defer c.close();
    (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);

    const pid = shellPidOf(&td.srv, "solo");
    var rq: [proto.end_req_max_len]u8 = undefined;
    try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "solo"));
    const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r.deinit(alloc);
    try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Dead{ .pid = pid }, Dead.yes));

    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    try proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nosuch"));
    const r2 = (try awaitFrame(alloc, &td.srv, obs.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r2.deinit(alloc);
    const v = proto.parseEndReply(r2.payload) orelse return error.BadEndReply;
    try std.testing.expect(!v.accepted);
    try std.testing.expectEqualStrings("no such session", v.reason);
}

test "Server: competing explicit creators get one shell and never attach or resize an existing session" {
    const alloc = std.testing.allocator;
    var td = try h.TestDaemon.init(alloc, "createonly", .{ .shell = "/bin/sh" });
    defer td.deinit();
    const a = try dial.dial(td.sock_path);
    defer a.close();
    const b = try dial.dial(td.sock_path);
    defer b.close();
    var req: [proto.create_req_max_len]u8 = undefined;
    const bytes = proto.encodeCreateReq(&req, 80, 24, "fresh");
    try proto.writeFrame(a.handle, .create_req, bytes);
    try proto.writeFrame(b.handle, .create_req, bytes);
    const ra = (try awaitFrame(alloc, &td.srv, a.handle, .create_reply, 400)) orelse return error.NoCreateReply;
    defer ra.deinit(alloc);
    const rb = (try awaitFrame(alloc, &td.srv, b.handle, .create_reply, 400)) orelse return error.NoCreateReply;
    defer rb.deinit(alloc);
    const sa = (try proto.parseCreateReply(ra.payload)).status;
    const sb = (try proto.parseCreateReply(rb.payload)).status;
    try std.testing.expect((sa == .created and sb == .exists) or (sa == .exists and sb == .created));
    const si = td.srv.sessions.find("fresh") orelse return error.MissingSession;
    const epoch = td.srv.ses(si).epoch;
    for (td.srv.clients) |slot| if (slot) |live| try std.testing.expect(live.session != si);

    // The common daemon-verb path also handles an attached client (including
    // QUIC). A repeated create cannot alter the session it happens to hold.
    const attached = try dial.dialAttachNamed(td.sock_path, 80, 24, "fresh");
    defer attached.close();
    (try awaitFrame(alloc, &td.srv, attached.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
    try proto.writeFrame(attached.handle, .create_req, proto.encodeCreateReq(&req, 120, 40, "fresh"));
    const exists = (try awaitFrame(alloc, &td.srv, attached.handle, .create_reply, 400)) orelse return error.NoCreateReply;
    defer exists.deinit(alloc);
    try std.testing.expectEqual(proto.CreateStatus.exists, (try proto.parseCreateReply(exists.payload)).status);
    try std.testing.expectEqual(epoch, td.srv.ses(si).epoch);
    try std.testing.expectEqual(@as(u16, 80), td.srv.colsNow(si));
    try std.testing.expectEqual(@as(u16, 24), td.srv.rowsNow(si));

    for ([_][]const u8{ &.{}, &.{ 80, 0, 24, 0 }, &.{ 80, 0, 24, 0, '#' }, &.{ 0, 0, 0, 0, 'z' } }) |bad| {
        try proto.writeFrame(a.handle, .create_req, bad);
        const refused = (try awaitFrame(alloc, &td.srv, a.handle, .create_reply, 400)) orelse return error.NoCreateReply;
        defer refused.deinit(alloc);
        try std.testing.expectEqual(proto.CreateStatus.refused, (try proto.parseCreateReply(refused.payload)).status);
    }
    try std.testing.expect(td.srv.sessions.find("z") == null);
}

test "Server: an observer's sessions_req is answered with every live name" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "obssess", .{ .shell = "/bin/sh" });
    defer td.deinit();

    const cx = try dial.dialAttachNamed(td.sock_path, 80, 24, "x");
    defer cx.close();
    (try awaitFrame(alloc, &td.srv, cx.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);
    const cy = try dial.dialAttachNamed(td.sock_path, 80, 24, "y");
    defer cy.close();
    (try awaitFrame(alloc, &td.srv, cy.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);

    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    try proto.writeFrame(obs.handle, .sessions_req, "");
    const r = (try awaitFrame(alloc, &td.srv, obs.handle, .sessions_reply, 200)) orelse return error.NoSessionsReply;
    defer r.deinit(alloc);
    // The whole payload, not two substrings: `"xy"`, a truncation after two
    // names, and a name that aliased its neighbour's buffer all satisfy an
    // indexOf. The last of those is one of the three escapes CLAUDE.md
    // records passing a green gate.
    try std.testing.expectEqualStrings("0\nx\ny", r.payload);
}

/// The grid of a NAMED session — `awaitGridText` only ever looks at slot 0.
fn awaitGridIn(
    alloc: std.mem.Allocator,
    srv: *Server,
    name: []const u8,
    needle: []const u8,
    budget_ms: i64,
) !bool {
    const deadline = std.time.milliTimestamp() + budget_ms;
    while (std.time.milliTimestamp() < deadline) {
        try srv.pumpOnce(5);
        const si = srv.sessions.find(proto.wireName(name)) orelse return false;
        const grid = try srv.ses(si).eng.dumpPlain(alloc);
        defer alloc.free(grid);
        if (std.mem.indexOf(u8, grid, needle) != null) return true;
    }
    return false;
}

test "Server: an accepted end kills a shell that ignores TERM and HUP" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.init(alloc, "endkill", .{ .shell = "/bin/sh" });
    defer td.deinit();

    const c = try dial.dialAttachNamed(td.sock_path, 80, 24, "stubborn");
    defer c.close();
    (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse return error.NoState).deinit(alloc);

    // The shell ignores both signals `requestExit` has, and never reads stdin
    // again — so closing the master cannot end it either. TRAP"PED" is typed
    // and TRAPPED is printed, so the needle can only be the shell's output
    // and never the echo of the line that asked for it.
    try proto.writeFrame(c.handle, .input, "trap '' TERM HUP; echo TRAP\"PED\"; while :; do sleep 1; done\n");
    try std.testing.expect(try awaitGridIn(alloc, &td.srv, "stubborn", "TRAPPED", 5000));

    const pid = shellPidOf(&td.srv, "stubborn");
    var rq: [proto.end_req_max_len]u8 = undefined;
    try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "stubborn"));
    const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r.deinit(alloc);
    try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);

    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, Dead{ .pid = pid }, Dead.yes));
}

test "Server: a keystroke into an ending session does not cost that client its exit_status" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "endtype");
    defer td.deinit();
    try td.startStubborn(alloc, .{});

    const c = try td.attachStubborn(alloc, "typing", 80, 24);
    defer c.close();

    var rq: [proto.end_req_max_len]u8 = undefined;
    try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "typing"));
    const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r.deinit(alloc);
    try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);

    // The master is closed but the shell is still alive, so this keystroke
    // reaches a session with nowhere to put it. Dropping the client for it
    // would trade the exit code for "connection to the daemon lost".
    try proto.writeFrame(c.handle, .input, "x");
    const x = (try awaitFrame(alloc, &td.srv, c.handle, .exit_status, 600)) orelse return error.NoExitStatus;
    x.deinit(alloc);
}

test "Server: a repeated end_req does not push the SIGKILL deadline out" {
    const alloc = std.testing.allocator;

    var td = try h.TestDaemon.open(alloc, "endagain");
    defer td.deinit();
    try td.startStubborn(alloc, .{});

    const c = try td.attachStubborn(alloc, "nag", 80, 24);
    defer c.close();

    const pid = shellPidOf(&td.srv, "nag");
    var rq: [proto.end_req_max_len]u8 = undefined;
    try proto.writeFrame(c.handle, .end_req, proto.encodeEndReq(&rq, false, "nag"));
    const r = (try awaitFrame(alloc, &td.srv, c.handle, .end_reply, 200)) orelse return error.NoEndReply;
    defer r.deinit(alloc);
    try std.testing.expect((proto.parseEndReply(r.payload) orelse return error.BadEndReply).accepted);
    // The clock the assertions below read, started where the daemon starts
    // its own: `endSession` arms the deadline when it accepts. MONOTONIC, and
    // deliberately so — 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.
    var since_end = try std.time.Timer.start();

    // An observer nagging faster than the grace: if each accept restarted the
    // clock, the shell would outlive every deadline it was ever given.
    const obs = try dial.dial(td.sock_path);
    defer obs.close();
    // Its own loop rather than `h.pumpUntil`: the nag is WORK done every
    // round, and a predicate that wrote to a socket to answer "is it dead"
    // would be a worse thing to read than this.
    var waited: u32 = 0;
    while (alive(pid) and waited < 3000) : (waited += 80) {
        proto.writeFrame(obs.handle, .end_req, proto.encodeEndReq(&rq, true, "nag")) catch {};
        try td.srv.pumpOnce(20);
        std.Thread.sleep(60 * std.time.ns_per_ms);
    }
    const outlived_ms = since_end.read() / std.time.ns_per_ms;
    try std.testing.expect(!alive(pid));

    // A dead shell is only half the claim, and it was the only half asserted:
    // a shell that died to the FIRST SIGTERM is dead too, and a fixture that
    // let that happen passed this test while the deadline it is named for was
    // never reached. That is not hypothetical — it is what a wait on the wrong
    // session did here, twice, and the difference was visible only as the
    // suite running faster. So the duration is the pin now. A wall clock and
    // not the loop counter above, which counts nominal turns rather than time.
    //
    // The LOWER bound is the property, and the only one of the two that is:
    // living past the grace is what proves the shell ignored every TERM and
    // left to the SIGKILL.
    //
    // The UPPER bound is a rail on the number, not a second pin, and it is
    // worth saying which regression it does NOT catch. The one this test is
    // named for is the `== null` guard dropped at the arm in `endSession`,
    // and that guard's absence RESETS `end_by_ms` to now plus a grace on
    // every accepted nag rather than adding one — so a deadline re-armed
    // every 80 ms never fires at all, the shell outlives the loop's whole
    // 3 s budget, and the `!alive` above is what fails. No multiplier here
    // reaches that. What this bound does catch is a deadline that drifted a
    // few turns of the loop, which is why it exists and why it is loose.
    //
    // 3x because the measurement it must clear is 661 ms on Linux and 668 to
    // 682 ms on macOS under `make check` (2026-09-04) against a 500 ms grace,
    // leaving about 2.2x. Not tighter: what varies is how long one turn of
    // the loop takes to notice the kill, and a loaded box stretches both its
    // sleep and its pump.
    if (outlived_ms < Pty.term_grace_ms) {
        std.debug.print(
            "the shell died {d} ms after the accepted end, INSIDE the {d} ms grace: " ++
                "it did not ignore the TERM, so the SIGKILL deadline was never reached\n",
            .{ outlived_ms, Pty.term_grace_ms },
        );
        return error.ShellDiedInsideTheGrace;
    }
    if (outlived_ms >= 3 * Pty.term_grace_ms) {
        std.debug.print(
            "the shell lived {d} ms after the accepted end, past 3x the {d} ms grace: " ++
                "the nagging looks to have pushed the deadline out\n",
            .{ outlived_ms, Pty.term_grace_ms },
        );
        return error.GraceDeadlineMoved;
    }
}

fn sockLost(s: *Server) bool {
    return s.sock_watch.lost;
}

fn sockRefused(s: *Server) bool {
    return s.sock_watch.refused != null;
}

test "Server: a socket path deleted under a running daemon is taken back within a tick, and never from a successor" {
    const alloc = std.testing.allocator;
    const sockpath = @import("sockpath");

    var td = try h.TestDaemon.init(alloc, "lost", .{ .shell = "/bin/sh" });
    defer td.deinit();
    // Tick the watch on every pump: the second it waits in production is
    // the wall's auto-start budget, not anything this test is grading.
    td.srv.sock_watch_interval_ms = 0;
    const path: []const u8 = td.sock_path;
    const first_id = td.srv.bound.path_id;

    // The 2026-09-04 shape: the FILE goes and the listener stays. Nothing
    // can reach the daemon by name, and until the watch existed nothing
    // ever would again.
    try std.fs.cwd().deleteFile(path);
    try std.testing.expect(!sockpath.answers(path));

    // Within a tick the daemon has re-bound the path, and the file there is
    // the one it now holds — a new inode, ours.
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, path, sockpath.answers));
    try std.testing.expect(!std.meta.eql(first_id, td.srv.bound.path_id));
    try std.testing.expect(td.srv.bound.path_id.stillAt(path));
    try std.testing.expect(!td.srv.sock_watch.lost);

    // And it SERVES there: this is not a file that reappeared.
    {
        const c = try dial.dial(path);
        defer c.close();
        try proto.writeFrame(c.handle, .sessions_req, "");
        const reply = (try awaitFrame(alloc, &td.srv, c.handle, .sessions_reply, 400)) orelse
            return error.NoReplyOnReboundPath;
        reply.deinit(alloc);
    }

    // The successor case: the path goes again, and before the next tick a
    // second listener has bound it — the second daemon a wall's auto-start
    // made in the incident. "No socket stealing" holds for the re-bind as
    // it does for a start: the successor's file is refused, not replaced.
    const ours_before = td.srv.bound.path_id;
    try std.fs.cwd().deleteFile(path);
    const addr = try std.net.Address.initUnix(path);
    var succ = try addr.listen(.{});
    const succ_id = try sockpath.PathId.of(path);
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, &td.srv, sockLost));
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, &td.srv, sockRefused));
    try std.testing.expectEqual(@as(?anyerror, error.DaemonAlreadyRunning), td.srv.sock_watch.refused);
    try std.testing.expect(std.meta.eql(succ_id, try sockpath.PathId.of(path)));
    try std.testing.expect(std.meta.eql(ours_before, td.srv.bound.path_id));

    // The successor stops without unlinking (a SIGKILL's leftover). The
    // next tick finds a dead socket file, which a claim may clear, and the
    // path is ours again.
    succ.deinit();
    try std.testing.expect(try h.pumpUntil(&td.srv, 3000, path, sockpath.answers));
    try std.testing.expect(td.srv.bound.path_id.stillAt(path));
    try std.testing.expect(!std.meta.eql(ours_before, td.srv.bound.path_id));
    try std.testing.expect(!td.srv.sock_watch.lost);
    try std.testing.expectEqual(@as(?anyerror, null), td.srv.sock_watch.refused);
}