a73x

src/server/server_test_modes.zig

Ref:   Size: 34.9 KiB   History

const std = @import("std");
const proto = @import("term").protocol;
const h = @import("server_test_harness.zig");
const dial = h.dial;
const srv_mod = @import("server.zig");
const Server = srv_mod.Server;
const awaitFrame = h.awaitFrame;
const connectedPair = h.connectedPair;

fn pumpAndCollectModes(
    alloc: std.mem.Allocator,
    srv: *Server,
    fd: std.posix.fd_t,
    out: *std.ArrayList(proto.PtyModeFlags),
) !void {
    try srv.pumpOnce(20);
    try drainModes(alloc, fd, out);
}

/// Pump until `out` holds `want` mode frames, keeping every one of them in
/// order. A sink rather than a `pumpUntil`, because the condition is
/// counted out of the frames themselves: the predicate would have to read
/// the socket to answer, and a predicate with a side effect is a worse
/// thing to read than this.
fn collectModes(
    alloc: std.mem.Allocator,
    srv: *Server,
    fd: std.posix.fd_t,
    out: *std.ArrayList(proto.PtyModeFlags),
    want: usize,
    iters: usize,
) !bool {
    const Collect = struct {
        alloc: std.mem.Allocator,
        out: *std.ArrayList(proto.PtyModeFlags),
        want: usize,
        fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void {
            if (frame.type != .pty_mode) return;
            const self: *@This() = @ptrCast(@alignCast(ctx.?));
            try self.out.append(self.alloc, try proto.decodePtyMode(frame.payload));
            if (self.out.items.len >= self.want) return error.ModesCollected;
        }
    };
    var collect: Collect = .{ .alloc = alloc, .out = out, .want = want };
    _ = h.awaitFrameSink(alloc, srv, fd, h.never_from_daemon, iters, .{
        .ctx = &collect,
        .on = Collect.on,
    }) catch |err| switch (err) {
        error.ModesCollected => return true,
        else => return err,
    };
    return false;
}

/// No pump: for paths that answer synchronously, where pumping would blur what
/// caused the frame. Its own poll, and not a `Link.awaitFrame`, because it
/// does not WAIT at all — it takes what is already readable and returns.
fn drainModes(
    alloc: std.mem.Allocator,
    fd: std.posix.fd_t,
    out: *std.ArrayList(proto.PtyModeFlags),
) !void {
    while (true) {
        var pfd = [_]std.posix.pollfd{
            .{ .fd = fd, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((try std.posix.poll(&pfd, 0)) == 0) return;
        const frame = (try proto.readFrame(alloc, fd)) orelse return;
        defer frame.deinit(alloc);
        if (frame.type == .pty_mode) {
            try out.append(alloc, try proto.decodePtyMode(frame.payload));
        }
    }
}

test "Server: an attach arriving on an established connection is answered with the mode" {
    const alloc = std.testing.allocator;

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

    // /bin/cat: a session that produces no output of its own, so the only
    // frames on this socket are the ones the attach caused.
    try td.start(.{ .shell = "/bin/cat" });

    const c = try connectedPair();
    defer std.posix.close(c.peer);
    // Already a client before it has said a word — how a QUIC client starts life,
    // since its slot is created at handshake and its FIRST attach reaches
    // `handleFrame` rather than `serviceObserver`. The two attach paths are
    // separate arms, each needing its own send. No session on purpose.
    td.srv.clients[0] = .{ .sink = .{ .socket = c.daemon } };

    var modes: std.ArrayList(proto.PtyModeFlags) = .empty;
    defer modes.deinit(alloc);

    var frame: std.ArrayList(u8) = .empty;
    defer frame.deinit(alloc);
    try proto.appendFrame(&frame, alloc, .attach, &proto.encodeAttach(80, 24, 0, 0));

    // First attach, before any pump has run: this is the branch that reads
    // the pty for itself because nothing has been broadcast yet.
    td.srv.pushInbound(0, frame.items);
    try drainModes(alloc, c.peer, &modes);
    try std.testing.expectEqual(@as(usize, 1), modes.items.len);
    try std.testing.expect(modes.items[0].icanon);
    try std.testing.expect(modes.items[0].echo);

    // And again once a pump has been round, where the daemon already knows
    // the mode and the fallback is not what is being exercised. A reattach
    // over a live connection is a real client's reconnect path.
    try td.srv.pumpOnce(20);
    modes.clearRetainingCapacity();
    td.srv.pushInbound(0, frame.items);
    try drainModes(alloc, c.peer, &modes);
    try std.testing.expectEqual(@as(usize, 1), modes.items.len);
    try std.testing.expect(modes.items[0].icanon);
    try std.testing.expect(modes.items[0].echo);
}

test "Server: the pty's mode bits reach a client on attach, and again only when they change" {
    const alloc = std.testing.allocator;

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

    // A SCRIPT, because readline takes the tty out of canonical mode per line
    // and puts it back, so the bits flap continuously and "changed exactly once"
    // could not be asserted. It ends on a blocking read, so `deinit`'s SIGTERM
    // lands on the shell itself.
    try td.tmp.dir.writeFile(.{
        .sub_path = "session.sh",
        .data =
        \\#!/bin/sh
        \\read -r start
        \\stty -echo
        \\read -r second
        \\stty -icanon
        \\read -r stop
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/session.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();

    var modes: std.ArrayList(proto.PtyModeFlags) = .empty;
    defer modes.deinit(alloc);

    // Attaching is enough on its own: nothing about the session has changed,
    // so a client that only heard about changes would never be told what the
    // terminal is doing.
    try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 1, 250));
    try std.testing.expectEqual(@as(usize, 1), modes.items.len);
    try std.testing.expect(modes.items[0].icanon);
    try std.testing.expect(modes.items[0].echo);

    // Release the script into `stty -echo`. Note what the daemon gets out of
    // this: no output at all. The mode change is silent, which is why the
    // poll cannot be folded into the "the pty printed something" arm.
    try proto.writeFrame(c.handle, .input, "go\n");

    try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 2, 500));
    try std.testing.expectEqual(@as(usize, 2), modes.items.len);
    try std.testing.expect(!modes.items[1].echo);
    // -echo alone: the line discipline is still canonical, and reporting
    // otherwise would tell a client to earn its predictions in the one
    // context where it must make none at all.
    try std.testing.expect(modes.items[1].icanon);

    // The transition that pins where the poll lives: echo is off, so between
    // here and the frame below the pty emits NOT ONE BYTE. A daemon that checked
    // the mode only after reading pty output sits here forever.
    try proto.writeFrame(c.handle, .input, "silent\n");

    try std.testing.expect(try collectModes(alloc, &td.srv, c.handle, &modes, 3, 500));
    try std.testing.expectEqual(@as(usize, 3), modes.items.len);
    try std.testing.expect(!modes.items[2].icanon);
    try std.testing.expect(!modes.items[2].echo);

    // And then nothing: the pty is in a blocking read with its mode unchanged,
    // so a daemon that re-sent what it already said shows up as a fourth frame
    // on the very NEXT pump. 25 rounds, because round one catches it — a
    // fixed count on purpose, since the verdict is that a FOURTH frame never
    // came and there is no arrival to wait for.
    var rounds: usize = 0;
    while (rounds < 25) : (rounds += 1) {
        try pumpAndCollectModes(alloc, &td.srv, c.handle, &modes);
    }
    try std.testing.expectEqual(@as(usize, 3), modes.items.len);
}

test "Server: a BEL from the session reaches its client as a bell term_event" {
    const alloc = std.testing.allocator;

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

    // A sibling of the clipboard test rather than a leg inside it, which is named
    // for clipboard SCOPING. What is untested is the drain's `.bell` arm:
    // `encodeBellEvent` and the client's rendering are each pinned alone, and
    // neither runs this function.
    try td.start(.{ .shell = "/bin/cat" });

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

    // The newline flushes the line to cat in canonical mode. cat writes the
    // raw BEL back whatever ECHOCTL does to the echo alongside it — mangled to
    // `^G` the echo is not a bell at all, unmangled it is a second one — so
    // the first term_event is a bell either way.
    try proto.writeFrame(c.handle, .input, "\x07\n");

    const ev = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
        return error.NoTermEvent;
    defer ev.deinit(alloc);
    switch (try proto.decodeTermEvent(ev.payload)) {
        .bell => {},
        .clipboard => return error.ExpectedBellGotClipboard,
    }
}

test "Server: a burst of bells in one chunk is coalesced into one frame" {
    const alloc = std.testing.allocator;

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

    // A script rather than `cat`, and the COUNT is the reason: typed input is
    // echoed, so five typed BELs can reach the engine twice in two chunks and
    // therefore two honest drains. A file writes them once.
    //
    // The `read` is not decoration: a script that rings at once races the attach,
    // and the drain then has nobody to queue to — the event is recorded pending,
    // but a fresh attach takes the snapshot branch and replays nothing.
    try td.tmp.dir.writeFile(.{
        .sub_path = "bellburst.sh",
        .data =
        \\#!/bin/sh
        \\read -r go
        \\printf '\007\007\007\007\007'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/bellburst.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();
    // Awaited, not assumed: this is what makes the ring below strictly later
    // than the attach, and so what removes the race described above.
    (try awaitFrame(alloc, &td.srv, c.handle, .snapshot, 400) orelse
        return error.NoSnapshot).deinit(alloc);

    // `go`, never a BEL: with a bell typed here the tty's own echo could
    // arrive as a separate event in a separate chunk, and this test would be
    // counting ECHOCTL rather than coalescing.
    try proto.writeFrame(c.handle, .input, "go\n");

    const ev = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
        return error.NoTermEvent;
    defer ev.deinit(alloc);
    switch (try proto.decodeTermEvent(ev.payload)) {
        .bell => {},
        .clipboard => return error.ExpectedBellGotClipboard,
    }

    // The boundary, with the positive already in hand so it cannot pass
    // vacuously: the other four rings must not have become four more frames.
    // The budget keeps pumping long past the drain that produced the first,
    // which is the very pump the rest would have been queued in.
    if (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 60)) |extra| {
        extra.deinit(alloc);
        return error.BellsNotCoalesced;
    }
}

test "Server: a bell in a later chunk is its own frame, not folded into the first" {
    const alloc = std.testing.allocator;

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

    // Coalescing is PER DRAIN, and this is the half the burst test cannot see:
    // hoisting the flag to session state would pass it and silence every ring
    // after the first for the session's life.
    //
    // Each ring is released by its own `read`, which puts the two in separate
    // chunks and both strictly after the attach. What is typed to release them
    // is `go`, never a BEL: an echoed bell could arrive as a second event and
    // pass this test on the leftovers of the first.
    try td.tmp.dir.writeFile(.{
        .sub_path = "belltwice.sh",
        .data =
        \\#!/bin/sh
        \\read -r one
        \\printf '\007'
        \\read -r two
        \\printf '\007'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/belltwice.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

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

    try proto.writeFrame(c.handle, .input, "one\n");
    const first = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
        return error.NoFirstBell;
    first.deinit(alloc);

    try proto.writeFrame(c.handle, .input, "two\n");

    const second = (try awaitFrame(alloc, &td.srv, c.handle, .term_event, 400)) orelse
        return error.SecondBellSwallowed;
    defer second.deinit(alloc);
    switch (try proto.decodeTermEvent(second.payload)) {
        .bell => {},
        .clipboard => return error.ExpectedBellGotClipboard,
    }
}

test "Server: a session enabling bracketed paste tells its clients, and not again per pump" {
    const alloc = std.testing.allocator;

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

    // Two pty chunks with one mode change between them: the DECSET, then —
    // once the test asks for it — output that changes nothing about the
    // modes. The second chunk is what makes "sent on change" falsifiable;
    // without it a per-pump send would pass just as well.
    try td.tmp.dir.writeFile(.{
        .sub_path = "modes.sh",
        .data =
        \\#!/bin/sh
        \\printf '\033[?2004h'
        \\read -r go
        \\printf 'same modes as before'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modes.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();

    // attach answers with the modes as they stand, which is before the shell
    // has run: keep pumping until one says the DECSET landed. Asserting on
    // the FIRST term_modes would be asserting on the attach-time one.
    const on = while (try awaitFrame(alloc, &td.srv, c.handle, .term_modes, 400)) |f| {
        defer f.deinit(alloc);
        if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
    } else false;
    try std.testing.expect(on);

    // Now more pty output with the modes unchanged. A delta proves a chunk
    // was digested — without it the "no second frame" half would pass
    // vacuously on a session that simply never spoke again — and a second
    // term_modes would prove the send is per-pump rather than on change.
    try proto.writeFrame(c.handle, .input, "go\n");
    // A round-counting loop on purpose: the wait ends a fixed number of
    // pumps AFTER the content frame, not when some frame arrives, and the
    // verdict is what did NOT come in that window.
    var saw_content = false;
    var resent: usize = 0;
    var after: usize = 0;
    var i: usize = 0;
    while (i < 400 and after < 60) : (i += 1) {
        if (saw_content) after += 1;
        try td.srv.pumpOnce(5);
        var pfd = [_]std.posix.pollfd{
            .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue;
        const f = (try proto.readFrame(alloc, c.handle)) orelse break;
        defer f.deinit(alloc);
        switch (f.type) {
            .delta, .snapshot => saw_content = true,
            .term_modes => resent += 1,
            else => {},
        }
    }
    try std.testing.expect(saw_content);
    try std.testing.expectEqual(@as(usize, 0), resent);
}

test "Server: a session entering and leaving the alternate screen tells its clients both times" {
    const alloc = std.testing.allocator;

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

    // The pair a pager sets and clears together: the alternate screen and
    // DECCKM. They are asserted as a pair because a client that read one
    // and guessed the other would send `less` arrows it ignores.
    try td.tmp.dir.writeFile(.{
        .sub_path = "altmodes.sh",
        .data =
        \\#!/bin/sh
        \\printf '\033[?1049h\033[?1h'
        \\read -r go
        \\printf '\033[?1l\033[?1049l'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/altmodes.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();

    // The attach-time frame reports the modes as they stood before the
    // shell ran, so the loop pumps until one carries the pager's pair.
    const entered = while (try awaitFrame(alloc, &td.srv, c.handle, .term_modes, 400)) |f| {
        defer f.deinit(alloc);
        const m = try proto.decodeTermModes(f.payload);
        if (m.alt_screen and m.cursor_keys) break true;
    } else false;
    try std.testing.expect(entered);

    // Leaving must be reported too. A client told only about entering
    // would keep turning the wheel into arrows after the pager quit.
    try proto.writeFrame(c.handle, .input, "go\n");
    const left = while (try awaitFrame(alloc, &td.srv, c.handle, .term_modes, 400)) |f| {
        defer f.deinit(alloc);
        const m = try proto.decodeTermModes(f.payload);
        if (!m.alt_screen and !m.cursor_keys) break true;
    } else false;
    try std.testing.expect(left);
}

test "Server: a client attaching to a session already in bracketed paste is told, on either resync branch" {
    const alloc = std.testing.allocator;

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

    try td.tmp.dir.writeFile(.{
        .sub_path = "modesjoin.sh",
        .data =
        \\#!/bin/sh
        \\printf '\033[?2004h'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesjoin.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    // The first client is here to get the change SENT, not to observe it:
    // once term_modes_sent holds true, sampleTermModes will never fire again
    // this session, so everything below can only be the resync's doing.
    const a = try dial.dialAttach(td.sock_path, 80, 24);
    defer a.close();
    const enabled = while (try awaitFrame(alloc, &td.srv, a.handle, .term_modes, 400)) |f| {
        defer f.deinit(alloc);
        if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
    } else false;
    try std.testing.expect(enabled);

    // Snapshot branch: a joiner holding nothing. It must be told what is
    // true now, or it would paint a session whose application wants
    // bracketed paste while its host terminal has never heard of it.
    const b = try dial.dialAttach(td.sock_path, 80, 24);
    defer b.close();
    var snap = try modesWithResync(alloc, &td.srv, b.handle);
    try std.testing.expectEqual(proto.MsgType.snapshot, snap.content orelse
        return error.NoContentFrameOnJoin);
    try std.testing.expect(snap.modes orelse return error.NoTermModesOnSnapshotBranch);

    // Delta branch: the same client re-attaching current, at the same size.
    // Seq and epoch read off the session rather than parsed back out of the
    // snapshot — the branch under test is chosen by exactly these two values,
    // so handing them over verbatim is what makes the branch certain.
    const s = &td.srv.sessions.table[0].?;
    try proto.writeFrame(b.handle, .attach, &proto.encodeAttach(80, 24, s.tracker.seq, s.epoch));
    snap = try modesWithResync(alloc, &td.srv, b.handle);
    try std.testing.expectEqual(proto.MsgType.delta, snap.content orelse
        return error.NoContentFrameOnReattach);
    try std.testing.expect(snap.modes orelse return error.NoTermModesOnDeltaBranch);
}

test "Server: a joiner that resizes the grid is still told the session's modes" {
    const alloc = std.testing.allocator;

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

    try td.tmp.dir.writeFile(.{
        .sub_path = "modesresize.sh",
        .data =
        \\#!/bin/sh
        \\printf '\033[?2004h'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/modesresize.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    // As in the resync-branch test above: the first client is here to latch
    // term_modes_sent, after which sampleTermModes can never fire again this
    // session and everything below is the resync's doing alone.
    const a = try dial.dialAttach(td.sock_path, 80, 24);
    defer a.close();
    const enabled = while (try awaitFrame(alloc, &td.srv, a.handle, .term_modes, 400)) |f| {
        defer f.deinit(alloc);
        if ((try proto.decodeTermModes(f.payload)).bracketed_paste) break true;
    } else false;
    try std.testing.expect(enabled);

    // The third arm of `sendResync`, which the other two tests cannot reach: a
    // joiner at a DIFFERENT size returns early through `resyncSnapshot`. Left
    // bare, bracketed paste stops being mirrored after any window resize, and
    // nothing connects a shell echoing a literal `200~` to the resize.
    const b = try dial.dialAttach(td.sock_path, 100, 30);
    defer b.close();

    // A's re-snapshot at B's size is the witness that this arm ran at all:
    // resyncSnapshot broadcasts, and the other two arms send to the joiner
    // alone. Without it the test could pass down the snapshot branch and
    // pin nothing the resync-branch test does not already cover.
    // Two connections at once, which is why this keeps its own poll:
    // `Link.awaitFrame` waits on ONE link, and the verdict here is a fact
    // about A and a fact about B arriving in the same window.
    var a_resnapshotted = false;
    var b_modes: ?bool = null;
    var i: usize = 0;
    while (i < 400 and !(a_resnapshotted and b_modes != null)) : (i += 1) {
        try td.srv.pumpOnce(5);
        var pfds = [_]std.posix.pollfd{
            .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
            .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((std.posix.poll(&pfds, 1) catch 0) == 0) continue;
        for (pfds) |pfd| {
            if (pfd.revents & std.posix.POLL.IN == 0) continue;
            const f = (try proto.readFrame(alloc, pfd.fd)) orelse continue;
            defer f.deinit(alloc);
            if (pfd.fd == a.handle and f.type == .snapshot) {
                const p = try proto.readSnapshotPrefix(f.payload);
                if (p.cols == 100 and p.rows == 30) a_resnapshotted = true;
            }
            if (pfd.fd == b.handle and f.type == .term_modes) {
                b_modes = (try proto.decodeTermModes(f.payload)).bracketed_paste;
            }
        }
    }
    try std.testing.expect(a_resnapshotted);
    try std.testing.expect(b_modes orelse return error.NoTermModesOnResizingJoin);
}

test "Server: a window title reaches clients on change, and only on change" {
    const alloc = std.testing.allocator;

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

    try td.tmp.dir.writeFile(.{
        .sub_path = "title.sh",
        // The leading `read` is what makes this about the SAMPLER: a shell that
        // printed its title at once would race the attach, and the frame this
        // waits for could be the resync's.
        .data =
        \\#!/bin/sh
        \\read -r go
        \\printf '\033]0;first\007'
        \\read -r go2
        \\printf 'output that sets no title'
        \\read -r go3
        \\printf '\033]0;second\007'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/title.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    const c = try dial.dialAttach(td.sock_path, 80, 24);
    defer c.close();

    // Queued behind the attach on the same socket, so the daemon has taken
    // the attach before it can forward this — see the script's leading read.
    try proto.writeFrame(c.handle, .input, "go\n");
    const first = (try awaitFrame(alloc, &td.srv, c.handle, .term_title, 400)) orelse
        return error.NoTitleFrame;
    defer first.deinit(alloc);
    try std.testing.expectEqualStrings("first", first.payload);

    // More pty output with the title unchanged. The delta proves a chunk was
    // digested — without it the "no second frame" half would pass vacuously
    // on a session that simply never spoke again.
    try proto.writeFrame(c.handle, .input, "go\n");
    // A round-counting loop on purpose: the wait ends a fixed number of
    // pumps AFTER the content frame, not when some frame arrives, and the
    // verdict is what did NOT come in that window.
    var saw_content = false;
    var resent: usize = 0;
    var after: usize = 0;
    var i: usize = 0;
    while (i < 400 and after < 60) : (i += 1) {
        if (saw_content) after += 1;
        try td.srv.pumpOnce(5);
        var pfd = [_]std.posix.pollfd{
            .{ .fd = c.handle, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((std.posix.poll(&pfd, 1) catch 0) == 0) continue;
        const f = (try proto.readFrame(alloc, c.handle)) orelse break;
        defer f.deinit(alloc);
        switch (f.type) {
            .delta, .snapshot => saw_content = true,
            .term_title => resent += 1,
            else => {},
        }
    }
    try std.testing.expect(saw_content);
    try std.testing.expectEqual(@as(usize, 0), resent);

    // A DIFFERENT title does travel: the early return is a change filter,
    // not a one-title-per-session latch.
    try proto.writeFrame(c.handle, .input, "go\n");
    const second = (try awaitFrame(alloc, &td.srv, c.handle, .term_title, 400)) orelse
        return error.NoSecondTitleFrame;
    defer second.deinit(alloc);
    try std.testing.expectEqualStrings("second", second.payload);
}

test "Server: a session that never set a title has none sent for it" {
    const alloc = std.testing.allocator;

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

    try td.tmp.dir.writeFile(.{
        .sub_path = "notitle.sh",
        .data =
        \\#!/bin/sh
        \\printf 'plain output, no OSC 0 anywhere'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/notitle.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    // Two attaches, because the two paths that could send an empty title are
    // different code: the SAMPLER and the RESYNC. An empty title on either makes
    // the client wipe the title bar of a terminal whose session said nothing.
    const a = try dial.dialAttach(td.sock_path, 80, 24);
    defer a.close();

    var saw_content = false;
    var titles: usize = 0;
    var after: usize = 0;
    var joined = false;
    var b: ?std.net.Stream = null;
    defer if (b) |s| s.close();
    // Both reasons at once: two connections, and a round-counting settle
    // whose verdict is `titles == 0`.
    var i: usize = 0;
    while (i < 500 and after < 80) : (i += 1) {
        if (saw_content) {
            // The joiner goes in only once the shell's output has landed, so
            // its resync reads a session that has genuinely run.
            if (!joined) {
                b = try dial.dialAttach(td.sock_path, 80, 24);
                joined = true;
            }
            after += 1;
        }
        try td.srv.pumpOnce(5);
        // Sliced to what exists rather than padded with a placeholder. The
        // padding this replaces put A's fd in slot 1 until B joined and
        // relied on a `continue` to skip it — and in a test whose verdict is
        // `titles == 0`, double-reading A's frames fails toward PASSING.
        var pfds = [_]std.posix.pollfd{
            .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
            .{ .fd = if (b) |s| s.handle else -1, .events = std.posix.POLL.IN, .revents = 0 },
        };
        const live = pfds[0..if (b == null) @as(usize, 1) else 2];
        if ((std.posix.poll(live, 1) catch 0) == 0) continue;
        for (live) |pfd| {
            if (pfd.revents & std.posix.POLL.IN == 0) continue;
            const f = (try proto.readFrame(alloc, pfd.fd)) orelse continue;
            defer f.deinit(alloc);
            switch (f.type) {
                .delta, .snapshot => saw_content = true,
                .term_title => titles += 1,
                else => {},
            }
        }
    }
    try std.testing.expect(saw_content);
    try std.testing.expect(joined);
    try std.testing.expectEqual(@as(usize, 0), titles);
}

test "Server: a joiner that resizes the grid is still told the session's title" {
    const alloc = std.testing.allocator;

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

    try td.tmp.dir.writeFile(.{
        .sub_path = "titleresize.sh",
        .data =
        \\#!/bin/sh
        \\printf '\033]0;vim\007'
        \\exec sleep 30
        \\
        ,
        .flags = .{ .mode = 0o755 },
    });
    const script = try std.fmt.allocPrintSentinel(alloc, "{s}/titleresize.sh", .{td.tmp.path()}, 0);
    defer alloc.free(script);

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

    // The first client latches title_sent, after which sampleTermTitle can
    // never fire again for this title and everything below is the resync's
    // doing alone.
    const a = try dial.dialAttach(td.sock_path, 80, 24);
    defer a.close();
    const latched = (try awaitFrame(alloc, &td.srv, a.handle, .term_title, 400)) orelse
        return error.NoTitleFrame;
    defer latched.deinit(alloc);
    try std.testing.expectEqualStrings("vim", latched.payload);

    // The third arm of sendResync: a joiner at a DIFFERENT size returns early
    // through resyncSnapshot, before the delta/snapshot split. Left bare it
    // is a real regression and a quiet one — the title bar would silently
    // stop matching the session after any window resize.
    const b = try dial.dialAttach(td.sock_path, 100, 30);
    defer b.close();

    // A's re-snapshot at B's size is the witness that this arm ran at all:
    // resyncSnapshot broadcasts, and the other two arms send to the joiner
    // alone.
    // Two connections again; see the bracketed-paste test above for why the
    // poll stays.
    var a_resnapshotted = false;
    var b_title: ?[]const u8 = null;
    defer if (b_title) |t| alloc.free(t);
    var i: usize = 0;
    while (i < 400 and !(a_resnapshotted and b_title != null)) : (i += 1) {
        try td.srv.pumpOnce(5);
        var pfds = [_]std.posix.pollfd{
            .{ .fd = a.handle, .events = std.posix.POLL.IN, .revents = 0 },
            .{ .fd = b.handle, .events = std.posix.POLL.IN, .revents = 0 },
        };
        if ((std.posix.poll(&pfds, 1) catch 0) == 0) continue;
        for (pfds) |pfd| {
            if (pfd.revents & std.posix.POLL.IN == 0) continue;
            const f = (try proto.readFrame(alloc, pfd.fd)) orelse continue;
            defer f.deinit(alloc);
            if (pfd.fd == a.handle and f.type == .snapshot) {
                const p = try proto.readSnapshotPrefix(f.payload);
                if (p.cols == 100 and p.rows == 30) a_resnapshotted = true;
            }
            if (pfd.fd == b.handle and f.type == .term_title and b_title == null) {
                b_title = try alloc.dupe(u8, f.payload);
            }
        }
    }
    try std.testing.expect(a_resnapshotted);
    try std.testing.expectEqualStrings("vim", b_title orelse return error.NoTitleOnResizingJoin);
}

/// Which resync branch answered the join, and what it said about the modes.
/// A sink rather than a bare `awaitFrame`, because the branch is named by
/// the frame BEFORE the answer and awaitFrame drops those.
fn modesWithResync(
    alloc: std.mem.Allocator,
    srv: *Server,
    fd: std.posix.fd_t,
) !struct { content: ?proto.MsgType, modes: ?bool } {
    const Branch = struct {
        content: ?proto.MsgType = null,
        fn on(ctx: ?*anyopaque, frame: proto.Frame) anyerror!void {
            if (frame.type != .snapshot and frame.type != .delta) return;
            const self: *@This() = @ptrCast(@alignCast(ctx.?));
            self.content = frame.type;
        }
    };
    var branch: Branch = .{};
    var modes: ?bool = null;
    if (try h.awaitFrameSink(alloc, srv, fd, .term_modes, 400, .{
        .ctx = &branch,
        .on = Branch.on,
    })) |frame| {
        defer frame.deinit(alloc);
        modes = (try proto.decodeTermModes(frame.payload)).bracketed_paste;
    }
    return .{ .content = branch.content, .modes = modes };
}

test "Server: a resize reaches the pty as an in-band size report when the app asked for one" {
    const alloc = std.testing.allocator;
    var td = try h.TestDaemon.open(alloc, "inband");
    defer td.deinit();
    // `cat`, not a shell: the oracle below is the line discipline's own echo, and
    // a shell's line editor turns ECHO off and reads the report as editing keys —
    // `ESC [` then digits is a numeric argument to readline.
    try td.start(.{ .shell = "/bin/cat" });

    const s = &td.srv.sessions.table[0].?;
    // The app opts in. The engine queues the immediate report; drop it so
    // only the resize's own report is on trial below.
    s.eng.feed("\x1b[?2048h");
    s.eng.clearPtyOutput();
    try std.testing.expect(td.srv.applySize(0, 100, 30));

    // The report is INPUT to the child, so the only place to see it from
    // here is the tty's own echo of it back on the master. Canonical mode
    // may spell the ESC as ^[, so the match starts after it.
    // Raw bytes off the pty master, not frames on a connection, so there is
    // no Link here to await on.
    var got: std.ArrayList(u8) = .empty;
    defer got.deinit(alloc);
    var tries: usize = 0;
    while (tries < 50) : (tries += 1) {
        var pfd = [_]std.posix.pollfd{.{ .fd = s.pty.master, .events = std.posix.POLL.IN, .revents = 0 }};
        if ((try std.posix.poll(&pfd, 100)) > 0) {
            var buf: [512]u8 = undefined;
            const n = std.posix.read(s.pty.master, &buf) catch 0;
            try got.appendSlice(alloc, buf[0..n]);
        }
        if (std.mem.indexOf(u8, got.items, "[48;30;100;0;0t") != null) break;
    }
    try std.testing.expect(std.mem.indexOf(u8, got.items, "[48;30;100;0;0t") != null);
}