a73x

src/tui/wall_test_picker.zig

Ref:   Size: 62.9 KiB   History

//! The host picker popup (wall_picker.zig).
const std = @import("std");
const proto = @import("term").protocol;
const client = @import("client");
const interact = @import("interact.zig");
const layout = @import("client").layout;
const fixture = @import("wall_test_harness.zig");
const wall_host = @import("wall_host.zig");
const wall_picker = @import("wall_picker.zig");
const wv = @import("wallview.zig");
const Host = wall_host.Host;
const PickerAuto = wall_picker.PickerAuto;
const PickerBody = wall_picker.PickerBody;
const Shared = wv.Shared;
const Tile = wv.Tile;
const TmpDir = @import("testtmp").TmpDir;
const WallScreen = fixture.WallScreen;

test "paintPicker: the box is centred on the terminal, not pinned to the origin" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    // Off-origin is the BASELINE here: a fixture on a terminal the box
    // happens to fill is blind to every centring arithmetic mistake.
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 100, .rows = 40 }, .is_tty = true };
    var table = [_]Host{
        fixture.testHost(&shared, "a", "/tmp/a.sock"),
        fixture.testHost(&shared, "b", "/tmp/b.sock"),
        fixture.testHost(&shared, "c", "/tmp/c.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    var buf: [8192]u8 = undefined;
    const frame = fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf);
    // Five rows (header + three hosts + footer) on forty: top = (40-5)/2.
    // The box is the terminal's width here, so left is 0 and the columns
    // are 1-based CUP.
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[18;1H") != null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[19;1H") != null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[22;1H") != null);
    // ...and nothing above or below the box.
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[17;1H") == null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[23;1H") == null);
    // The cursor goes with the popup: a caret blinking in a tile says the
    // keys are going there.
    try std.testing.expect(std.mem.startsWith(u8, frame, "\x1b[?25l"));
}

test "paintPicker: a box wider than its cap is centred in the columns" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    // Wider than `picker_row_max`, so the box is capped and the leftover
    // columns are split: (200 - 128) / 2.
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 200, .rows = 10 }, .is_tty = true };
    var table = [_]Host{fixture.testHost(&shared, "a", "/tmp/a.sock")};
    fixture.setList(&table[0], "");
    table[0].applied = true;
    var buf: [8192]u8 = undefined;
    const frame = fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf);
    try std.testing.expect(std.mem.indexOf(u8, frame, "\x1b[4;37H") != null);
}

test "paintPicker: a terminal too small for a box still says which popup has the keys" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 20, .rows = 3 }, .is_tty = true };
    var table = [_]Host{
        fixture.testHost(&shared, "a", "/tmp/a.sock"),
        fixture.testHost(&shared, "b", "/tmp/b.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    var buf: [8192]u8 = undefined;
    const frame = fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf);
    try std.testing.expect(std.mem.indexOf(u8, frame, "hosts") != null);
    // Under the minimum the header is the WHOLE popup: no rows, no legend.
    // A box that does not fit is worse than a line saying which one it is.
    try std.testing.expect(std.mem.indexOf(u8, frame, "Enter/c new session") == null);
    try std.testing.expect(std.mem.indexOf(u8, frame, " 1  a") == null);
}

test "paintPicker: the window scrolls so the selected row is always on screen" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    // Six rows for the box: header, footer, and four hosts of the eight.
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 60, .rows = 6 }, .is_tty = true };
    const names = [_][]const u8{ "h0", "h1", "h2", "h3", "h4", "h5", "h6", "h7" };
    var table: [8]Host = undefined;
    for (&table, names) |*h, name| {
        h.* = fixture.testHost(&shared, name, "/tmp/x.sock");
        fixture.setList(h, "");
        h.applied = true;
    }
    var buf: [8192]u8 = undefined;
    const top = fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf);
    try std.testing.expect(std.mem.indexOf(u8, top, " 1> h0") != null);
    try std.testing.expect(std.mem.indexOf(u8, top, "h7") == null);
    // The last row: the window has to follow the selection, or `j` walks
    // off the bottom of a box that never moves.
    var buf2: [8192]u8 = undefined;
    const bottom = fixture.pickerFrame(&shared, pipe[0], &table, 7, &buf2);
    try std.testing.expect(std.mem.indexOf(u8, bottom, " 8> h7") != null);
    try std.testing.expect(std.mem.indexOf(u8, bottom, "h0") == null);
}

test "paintPicker: a frame the stamp refuses to write does not eat the notice with it" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 100, .rows = 40 }, .is_tty = true };
    var table = [_]Host{fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock")};
    fixture.setList(&table[0], "");
    table[0].applied = true;
    wall_picker.paintPicker(fixture.hostWall(&shared, &table), 0, .{}, null);

    // A notice whose footer is byte-for-byte the legend already on the
    // screen: the stamp refuses the write, and taking the notice ahead of
    // that check is a sentence consumed by a frame nobody was sent.
    wv.setNotice(&shared, " Enter sessions   c new session   x forget   a add   Esc");
    wall_picker.paintPicker(fixture.hostWall(&shared, &table), 0, .{}, null);
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        " Enter sessions   c new session   x forget   a add   Esc",
        wv.takeNotice(&shared, &buf),
    );
}

test "paintPicker: replayed into an engine, the popup covers its box and NOT one cell more" {
    const alloc = std.testing.allocator;
    const cols: u16 = 160;
    const rows: u16 = 20;
    var screen = try WallScreen.init(alloc, cols, rows);
    defer screen.deinit();
    // Off-origin in BOTH axes: `picker_row_max` caps the width at 128, so
    // left = (160-128)/2, and three hosts make a five-row box at
    // top = (20-5)/2. A grep over the frame's CUP sequences cannot see a
    // paint that reaches a cell by another route — a different CUP form, a
    // wider pad — so this one judges the GRID.
    const w: u16 = @intCast(wall_picker.picker_row_max);
    const left: u16 = (cols - w) / 2;
    const height: u16 = 5;
    const top: u16 = (rows - height) / 2;

    // A background every untouched cell can be recognised by.
    var bg: [cols]u8 = undefined;
    @memset(&bg, '.');
    var r: u16 = 0;
    while (r < rows) : (r += 1) {
        var cup: [16]u8 = undefined;
        screen.eng.feed(std.fmt.bufPrint(&cup, "\x1b[{d};1H", .{r + 1}) catch unreachable);
        screen.eng.feed(&bg);
    }
    screen.eng.feed("\x1b[H");

    var shared = Shared{ .out_fd = screen.w, .size = .{ .cols = cols, .rows = rows }, .is_tty = true };
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock"),
        fixture.testHost(&shared, "--sock /tmp/b.sock", "/tmp/b.sock"),
        fixture.testHost(&shared, "--sock /tmp/c.sock", "/tmp/c.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    wall_picker.paintPicker(fixture.hostWall(&shared, &table), 1, .{}, null);
    screen.drain();
    const dump = try screen.eng.dumpPlain(alloc);
    defer alloc.free(dump);

    var i: u16 = 0;
    while (i < rows) : (i += 1) {
        const l = WallScreen.line(dump, i) orelse return error.NoSuchRow;
        try std.testing.expectEqual(@as(usize, cols), l.len);
        if (i >= top and i < top + height) {
            // The rails and every tile beside the box are outside it.
            try std.testing.expectEqualStrings(bg[0..left], l[0..left]);
            try std.testing.expectEqualStrings(bg[0..left], l[left + w ..]);
            try std.testing.expect(!std.mem.eql(u8, bg[0..w], l[left..][0..w]));
        } else {
            try std.testing.expectEqualStrings(&bg, l);
        }
    }
    // The selected row is the one the popup says it is, in the grid.
    const sel_row = WallScreen.line(dump, top + 2) orelse return error.NoSuchRow;
    try std.testing.expect(std.mem.indexOf(u8, sel_row, " 2> ") != null);
}

test "paintPicker: replayed into an engine, a shorter box erases the rows the taller one left" {
    const alloc = std.testing.allocator;
    const cols: u16 = 160;
    const rows: u16 = 20;
    var screen = try WallScreen.init(alloc, cols, rows);
    defer screen.deinit();
    // Off-origin in both axes, the sibling test's reason. FOUR hosts and
    // then ONE session: the box goes from six rows to three, so the outer
    // rows of the tall box are the ones nothing would repaint — tiles do
    // not draw while the popup is up, and the close is what used to be the
    // first thing to repair them.
    const w: u16 = @intCast(wall_picker.picker_row_max);
    const left: u16 = (cols - w) / 2;
    const tall: u16 = 6;
    const short: u16 = 3;
    const tall_top: u16 = (rows - tall) / 2;
    const short_top: u16 = (rows - short) / 2;

    var bg: [cols]u8 = undefined;
    @memset(&bg, '.');
    var r: u16 = 0;
    while (r < rows) : (r += 1) {
        var cup: [16]u8 = undefined;
        screen.eng.feed(std.fmt.bufPrint(&cup, "\x1b[{d};1H", .{r + 1}) catch unreachable);
        screen.eng.feed(&bg);
    }
    screen.eng.feed("\x1b[H");

    var shared = Shared{ .out_fd = screen.w, .size = .{ .cols = cols, .rows = rows }, .is_tty = true };
    defer shared.tree.deinit();
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock"),
        fixture.testHost(&shared, "--sock /tmp/b.sock", "/tmp/b.sock"),
        fixture.testHost(&shared, "--sock /tmp/c.sock", "/tmp/c.sock"),
        fixture.testHost(&shared, "--sock /tmp/d.sock", "/tmp/d.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    // The selected host's own list, so the descent is a real one: host 1,
    // not host 0, and one session on it.
    fixture.setList(&table[1], "only\n");

    wall_picker.paintPicker(fixture.hostWall(&shared, &table), 1, .{}, null);
    screen.drain();
    wall_picker.paintPicker(fixture.hostWall(&shared, &table), 1, .{ .level = .sessions, .row = 0 }, null);
    screen.drain();

    const dump = try screen.eng.dumpPlain(alloc);
    defer alloc.free(dump);
    var blank: [w]u8 = undefined;
    @memset(&blank, ' ');
    var i: u16 = 0;
    while (i < rows) : (i += 1) {
        const l = WallScreen.line(dump, i) orelse return error.NoSuchRow;
        try std.testing.expectEqual(@as(usize, cols), l.len);
        // Whatever happened inside the box, the wall either side of it is
        // untouched: the clear is span-bounded ECH over the OLD box's own
        // columns, never a line-wide erase that would reach a neighbouring
        // tile's cells or a rail.
        try std.testing.expectEqualStrings(bg[0..left], l[0..left]);
        try std.testing.expectEqualStrings(bg[0..left], l[left + w ..]);
        if (i >= short_top and i < short_top + short) {
            try std.testing.expect(!std.mem.eql(u8, &blank, l[left..][0..w]));
        } else if (i >= tall_top and i < tall_top + tall) {
            try std.testing.expectEqualStrings(&blank, l[left..][0..w]);
        } else {
            try std.testing.expectEqualStrings(&bg, l);
        }
    }
}

test "paintPicker: a box that grows clears nothing, and a shrinking one clears at its own columns" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 160, .rows = 20 }, .is_tty = true };
    defer shared.tree.deinit();
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock"),
        fixture.testHost(&shared, "--sock /tmp/b.sock", "/tmp/b.sock"),
        fixture.testHost(&shared, "--sock /tmp/c.sock", "/tmp/c.sock"),
        fixture.testHost(&shared, "--sock /tmp/d.sock", "/tmp/d.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    fixture.setList(&table[1], "only\n");
    var buf: [8192]u8 = undefined;
    // The three-row session box first, then the six-row host box over it.
    _ = fixture.pickerFrameAt(&shared, pipe[0], &table, 1, .{ .level = .sessions, .row = 0 }, &buf);
    const grown = fixture.pickerFrameAt(&shared, pipe[0], &table, 1, .{}, &buf);
    try std.testing.expect(grown.len > 0);
    // A box that covers every row of the one before it owes no clear, and
    // an ECH here would erase a row this same frame is painting.
    try std.testing.expect(std.mem.indexOf(u8, grown, "X") == null);

    // ...and back down. Six rows at top (20-6)/2 = 7 and three at (20-3)/2
    // = 8, so rows 7, 11 and 12 (CUP rows 8, 12, 13) are the uncovered
    // ones, and the box's left edge is (160-128)/2 = 16 (CUP column 17).
    const shrunk = fixture.pickerFrameAt(&shared, pipe[0], &table, 1, .{ .level = .sessions, .row = 0 }, &buf);
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[8;17H\x1b[128X") != null);
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[12;17H\x1b[128X") != null);
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[13;17H\x1b[128X") != null);
    // The rows the new box writes itself are not cleared first.
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[9;17H\x1b[128X") == null);
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[10;17H\x1b[128X") == null);
    try std.testing.expect(std.mem.indexOf(u8, shrunk, "\x1b[11;17H\x1b[128X") == null);
}

test "paintPicker: an unchanged frame is not written again" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 60, .rows = 20 }, .is_tty = true };
    var table = [_]Host{
        fixture.testHost(&shared, "a", "/tmp/a.sock"),
        fixture.testHost(&shared, "b", "/tmp/b.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    var buf: [8192]u8 = undefined;
    try std.testing.expect(fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf).len > 0);
    // The pollers repaint this box once a second PER HOST. Rewriting an
    // identical screen at that rate is a terminal that never goes quiet,
    // which is also every `settle` in the e2e suite.
    try std.testing.expectEqual(@as(usize, 0), fixture.pickerFrame(&shared, pipe[0], &table, 0, &buf).len);
    // A moved selection is a different frame, and is written.
    try std.testing.expect(fixture.pickerFrame(&shared, pipe[0], &table, 1, &buf).len > 0);
    // ...and a cleared stamp is what a relayout leaves behind, so the box
    // goes back onto a screen that was wiped under it.
    shared.picker_frame = .{};
    try std.testing.expect(fixture.pickerFrame(&shared, pipe[0], &table, 1, &buf).len > 0);
}

test "PickerAuto: an empty wall opens the picker once, and a tile takes it back" {
    var a: PickerAuto = .{};
    // Empty and nothing open: the wall opens it itself, because a blank
    // screen is no place to act from.
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
    // ...ONCE. The Esc that closed it has to leave the one-line text
    // standing rather than being reopened over.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, true, false, false));
    // A tile arrived: the wall has something to show and gets the screen.
    try std.testing.expectEqual(PickerAuto.Step.close, a.step(true, false, true, false));
    // ...and only once; the popup is already gone.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, false, false, false));
}

test "PickerAuto: a spelling in progress keeps the popup a tile would have closed" {
    var a: PickerAuto = .{};
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
    // `prompting` layers OVER `picking`: closing on `picking` alone would
    // take the editor off the screen and leave it eating every key, and its
    // Enter would reach the main switch's dead `.add_tile` arm — no line,
    // no notice, and no host added.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, false, true, true));
    // The moment the line is submitted or cancelled, the close is owed again.
    try std.testing.expectEqual(PickerAuto.Step.close, a.step(true, false, true, false));
}

test "PickerAuto: a picker the user opened is not closed by an arriving tile" {
    var a: PickerAuto = .{};
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
    // Esc, and the user opens it again by hand. Without `taken` the flag
    // outlives the close and the next birth force-closes a popup they are
    // choosing from.
    a.taken();
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, false, true, false));
}

test "PickerAuto: the Esc that closes the wall's own popup is not undone by the wall" {
    var a: PickerAuto = .{};
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
    // The close runs `taken` on EVERY exit, the wall's popup included, and
    // the wall is still empty afterwards. One flag for both facts reopened
    // the box over the one-line text the Esc had just asked for — and on
    // that wall `Ctrl-\ d` never reached the keyboard again.
    a.taken();
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, true, false, false));
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, true, false, false));
    // A tile arrives and leaves again: THAT emptiness earns its own open.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, false, false, false));
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
}

test "PickerAuto: one Esc leaves a user's picker that outlived the wall's last tile" {
    var a: PickerAuto = .{};
    // The user opens the popup on a wall that still has tiles, and the last
    // of them ends underneath it. The emptiness the wall then sees is one
    // whose popup is already up, so it is spent without ever opening.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, false, true, false));
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, true, true, false));
    a.taken();
    // The one Esc that closed it is the whole of this: unspent, the wall
    // answers it by opening the box straight back over the empty line.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(true, true, false, false));
}

test "PickerAuto: a wall still dialling spends nothing, so its first known emptiness still opens" {
    var a: PickerAuto = .{};
    // `is_tty` carries `restore_tried`: the user can open the picker by hand
    // while the hosts are still answering, and a wall not yet KNOWN to be
    // empty must not spend the open it has not made.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(false, true, true, false));
    a.taken();
    try std.testing.expectEqual(PickerAuto.Step.open, a.step(true, true, false, false));
}

test "PickerAuto: a wall still dialling, and a wall with no terminal, open nothing" {
    var a: PickerAuto = .{};
    // The caller folds `restore_tried` into this argument: a wall that has
    // not heard from its hosts yet is not KNOWN to be empty, and a popup
    // flashed over tiles that are about to arrive is one nobody asked for.
    try std.testing.expectEqual(PickerAuto.Step.leave, a.step(false, true, false, false));
    // A piped mux has no screen to put a popup on.
    var b: PickerAuto = .{};
    try std.testing.expectEqual(PickerAuto.Step.leave, b.step(false, true, false, false));
}

test "pickerRow: a table with a 10 in it pads every number, so no spelling shifts a column" {
    var nine_buf: [96]u8 = undefined;
    var ten_buf: [96]u8 = undefined;
    const nine = wall_picker.pickerRow(&nine_buf, 9, true, "--sock /a", "1 session", false, 40);
    const ten = wall_picker.pickerRow(&ten_buf, 10, true, "--sock /a", "1 session", false, 40);
    try std.testing.expectEqual(
        std.mem.indexOf(u8, nine, "--sock").?,
        std.mem.indexOf(u8, ten, "--sock").?,
    );

    // A table that never reaches 10 spends no column on a digit it has not
    // got: the marker is already two wide for the same reason.
    var narrow_buf: [96]u8 = undefined;
    const narrow = wall_picker.pickerRow(&narrow_buf, 9, false, "--sock /a", "1 session", false, 40);
    try std.testing.expectEqualStrings(" 9  --sock", narrow[0..10]);
}

test "pickerRows: every host says what its poller last answered" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    // Four hosts, one per state the column can report. A fixture of one
    // reachable host is blind to three of them.
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock"),
        fixture.testHost(&shared, "box", "/tmp/b.sock"),
        fixture.testHost(&shared, "quic://gate:4433", "/tmp/c.sock"),
        fixture.testHost(&shared, "slow", "/tmp/d.sock"),
    };
    fixture.setList(&table[0], "0\nwork\ndev\n");
    table[0].applied = true;
    fixture.setList(&table[1], "");
    table[1].poll.reachable.store(false, .release);
    table[1].applied = true;
    fixture.setList(&table[2], "");
    table[2].applied = true;
    // Never answered: `applied` is what a first list sets, so a host that
    // has not reported once is `connecting` and not `no sessions` — the
    // two send the user to opposite places.

    var body: PickerBody = .{};
    wall_picker.pickerRows(&body, &table, 2, 48);

    try std.testing.expectEqual(@as(usize, 4), body.n);
    try std.testing.expectEqualStrings(" 1  --sock /tmp/a.sock               3 sessions ", body.row(0));
    try std.testing.expectEqualStrings(" 2  box                             unreachable ", body.row(1));
    // The selected row wears the same marker a focused tile's bar does.
    try std.testing.expectEqualStrings(" 3> quic://gate:4433                no sessions ", body.row(2));
    try std.testing.expectEqualStrings(" 4  slow                             connecting ", body.row(3));
}

test "pickerRows: one session is not 1 sessions" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/b.sock")};
    fixture.setList(&table[0], "0\n");
    table[0].applied = true;
    var body: PickerBody = .{};
    wall_picker.pickerRows(&body, &table, 0, 32);
    try std.testing.expectEqualStrings(" 1> box               1 session ", body.row(0));
}

test "pickerRows: a reason never costs the row the host it names" {
    // The regression a long state introduces. `pickerRow` cuts the
    // SPELLING and never the state, so a state that grew from a word to a
    // whole ssh sentence would take the host name's columns: at 80 with
    // this 17-byte spelling and the 71-byte sentence below, the row had
    // three characters of `noroute@127.0.0.1` left — and the spelling is
    // the one column that says which machine Enter would act on.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var table = [_]Host{fixture.testHost(&shared, "noroute@127.0.0.1", "/tmp/b.sock")};
    table[0].applied = true;
    table[0].poll.reachable.store(false, .release);
    table[0].poll.reason.feed("ssh: connect to host 10.255.255.1 port 22: No route to host\n");

    var body: PickerBody = .{};
    wall_picker.pickerRows(&body, &table, 0, 80);
    const row = body.row(0);
    try std.testing.expectEqual(@as(usize, 80), row.len);
    // Whole, and where a row with a one-word state would have put it.
    try std.testing.expectEqualStrings(" 1> noroute@127.0.0.1", row[0..21]);
    // ...and the reason took the rest, cut rather than dropped: the head
    // of an ssh diagnostic is the part that names the cause.
    try std.testing.expect(std.mem.indexOf(u8, row, "unreachable: ssh: connect to host") != null);
    try std.testing.expect(std.mem.indexOf(u8, row, "No route to host") == null);

    // A row with no room for a reason says what it always said, in the
    // same width, and cuts the spelling by exactly as much as it always
    // did: the floor is the longest word the state could carry before
    // reasons existed, so this row is byte-identical to the one this
    // fixture drew at ad3765ca.
    wall_picker.pickerRows(&body, &table, 0, 32);
    try std.testing.expectEqualStrings(" 1> route@127.0.0.1 unreachable ", body.row(0));
}

test "pickerRows: a spelling wider than the box keeps its tail" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var table = [_]Host{fixture.testHost(
        &shared,
        "--sock /run/user/1000/mux/very/long/path/to/muxd.sock",
        "/tmp/b.sock",
    )};
    fixture.setList(&table[0], "0\n");
    table[0].applied = true;
    var body: PickerBody = .{};
    wall_picker.pickerRows(&body, &table, 0, 32);
    // Cut from the LEFT: the head of a socket path is what every host on
    // one machine has in common, and the tail is what tells them apart.
    try std.testing.expectEqualStrings(" 1> path/to/muxd.sock 1 session ", body.row(0));
}

test "pickBirth: Enter on an emptied popup births nothing, not a session on a host just forgotten" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    // TWO hosts, both forgotten: the wall the last `x` empties is the wall
    // whose popup has no row left to move the selection to, so `sel` still
    // indexes a slot — a one-host fixture proves nothing about the index.
    var table = [_]Host{
        fixture.testHost(&shared, "a", "/tmp/a.sock"),
        fixture.testHost(&shared, "b", "/tmp/b.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
        h.forgotten.store(true, .release);
    }

    const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);

    if (at != null) return error.BornOnAForgottenHost;
    if (live != 0) return error.AForgottenHostTookASlot;
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        "[no hosts to start a session on - a adds one]",
        wv.takeNotice(&shared, &buf),
    );
}

test "pickBirth: Enter is an ask — the tile it births may start a daemon, the row it came from may not" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    // TWO hosts, and the birth on the second: a one-host fixture cannot
    // see a birth that reaches for the wrong row.
    var table = [_]Host{
        .{
            .spec = try client.resolveHost(alloc, "alpha", null, client.quic_idle_ms_default),
            .shared = &shared,
        },
        .{
            .spec = try client.resolveHost(alloc, "beta", null, client.quic_idle_ms_default),
            .shared = &shared,
        },
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }

    const at = wall_picker.pickBirth(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1) orelse
        return error.EnterBornNothing;

    // The row Enter lands on is often exactly the one the poller calls
    // unreachable, and starting that machine's daemon is what choosing it
    // means. Nothing else on the wall may: the spec below it is what the
    // poller re-dials every second.
    try std.testing.expect(tiles[at].r.target.hand.asked);
    try std.testing.expect(!table[1].spec.target.hand.asked);
    try std.testing.expect(!table[1].spec.poll_target.hand.asked);
}

test "pickerRows: a forgotten host is off the list, and the numbers close up" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var table = [_]Host{
        fixture.testHost(&shared, "a", "/tmp/a.sock"),
        fixture.testHost(&shared, "b", "/tmp/b.sock"),
        fixture.testHost(&shared, "c", "/tmp/c.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    // The slot STAYS — a poller thread holds the pointer and `Tile.host`
    // indexes it — so only the list closes up.
    table[1].forgotten.store(true, .release);
    var body: PickerBody = .{};
    wall_picker.pickerRows(&body, &table, 2, 32);
    try std.testing.expectEqual(@as(usize, 2), body.n);
    try std.testing.expectEqualStrings(" 1  a               no sessions ", body.row(0));
    try std.testing.expectEqualStrings(" 2> c               no sessions ", body.row(1));
}

test "pickerRepaint: a screen cleared under the spelling editor still owes a paint, carrying the half-typed line" {
    var f = interact.PrefixFilter{};
    var buf: [interact.PrefixFilter.prompt_max + 4]u8 = undefined;

    // Nothing is owed while the popup is not up, whatever else happened.
    try std.testing.expect(!wall_picker.pickerRepaint(&buf, &f, true).due);

    f.picking = true;
    const closed = wall_picker.pickerRepaint(&buf, &f, false);
    try std.testing.expect(!closed.due);
    try std.testing.expectEqual(@as(?[]const u8, null), closed.line);

    // The editor is open and a poll on some OTHER host vanishes a pane:
    // `relayout` clears the whole screen and zeroes the stamp, which
    // is the trigger. Skipping the paint here leaves the user typing into an
    // editor that is not on the screen, and every key still goes to it.
    f.prompting = true;
    var typed = [_]u8{ 'b', 'o', 'x' };
    for (&typed) |*c| _ = f.feed(c[0..1]);
    const repair = wall_picker.pickerRepaint(&buf, &f, true);
    try std.testing.expect(repair.due);
    try std.testing.expectEqualStrings(": box_", repair.line orelse "");
}

test "hostState: the row counts the sessions the wall could show, not the runs in the reply" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var h = Host{
        .spec = .{ .spelling = "--sock /a", .target = .{ .sock = "/a" }, .poll_target = .{ .sock = "/a" } },
        .shared = &shared,
    };
    var buf: [32]u8 = undefined;

    // Never polled, so nothing is known: `no sessions` here would send the
    // user to birth on a machine that is not answering.
    try std.testing.expectEqualStrings("connecting", wall_picker.hostState(&buf, &h));

    h.applied = true;
    const listed = "a\nb\nc\n";
    @memcpy(h.poll.list[0..listed.len], listed);
    h.poll.list_len = listed.len;
    try std.testing.expectEqualStrings("3 sessions", wall_picker.hostState(&buf, &h));

    // A row saying `4 sessions` beside three tiles is the row lying about
    // the wall: `planHostDiff` grades through `validSessionName`, so the
    // count reads the reply through the same filter.
    const with_junk = "a\nb\n" ++ ("x" ** (proto.session_name_max + 1)) ++ "\nc\n";
    @memcpy(h.poll.list[0..with_junk.len], with_junk);
    h.poll.list_len = with_junk.len;
    try std.testing.expectEqualStrings("3 sessions", wall_picker.hostState(&buf, &h));

    h.poll.reachable.store(false, .release);
    try std.testing.expectEqualStrings("unreachable", wall_picker.hostState(&buf, &h));
}

test "hostState: a drifted daemon says so beside its count, within the row's buffer" {
    var shared: Shared = undefined;
    fixture.stoppedWall(std.testing.allocator, &shared);
    defer shared.tree.deinit();
    var h = Host{
        .spec = .{ .spelling = "--sock /a", .target = .{ .sock = "/a" }, .poll_target = .{ .sock = "/a" } },
        .shared = &shared,
    };
    h.applied = true;
    const listed = "a\n";
    @memcpy(h.poll.list[0..listed.len], listed);
    h.poll.list_len = listed.len;
    // The keyboard's applied word, the same one the tiles wear: the row
    // reads it rather than re-judging the payload, so popup and bar can
    // never disagree about one host.
    const word = "daemon 0.9 stale";
    @memcpy(h.drift[0..word.len], word);
    h.drift_len = word.len;

    var buf: [32]u8 = undefined;
    try std.testing.expectEqualStrings("1 session, daemon 0.9 stale", wall_picker.hostState(&buf, &h));

    // The row's buffer is the bound, and truncation is the contract —
    // never an overflow, never stack garbage.
    var narrow: [12]u8 = undefined;
    try std.testing.expectEqualStrings("1 session, d", wall_picker.hostState(&narrow, &h));

    // A dark box keeps its reason: drift is news about a daemon that IS
    // answering, and `unreachable` outranks it.
    h.poll.reachable.store(false, .release);
    try std.testing.expectEqualStrings("unreachable", wall_picker.hostState(&buf, &h));
}

test "hostState: an unreachable host says what ssh said, cut to the row's buffer" {
    // `unreachable` alone is a row the user cannot act on: a box that is
    // off, a key that was refused and a host name that does not resolve
    // all read the same. ssh said which, and the poll kept the line.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var h = Host{
        .spec = .{ .spelling = "box", .target = .{ .sock = "/a" }, .poll_target = .{ .sock = "/a" } },
        .shared = &shared,
    };
    h.applied = true;
    h.poll.reachable.store(false, .release);
    h.poll.reason.feed("ssh: connect to host box port 22: No route to host\n");

    var wide: [wall_picker.picker_row_max]u8 = undefined;
    try std.testing.expectEqualStrings(
        "unreachable: ssh: connect to host box port 22: No route to host",
        wall_picker.hostState(&wide, &h),
    );

    // The buffer is the ROW's, and a row is as wide as the terminal is.
    // Truncation is the contract, not an error: a narrow buffer shortens
    // the sentence rather than losing it or painting past its rect.
    var narrow: [20]u8 = undefined;
    try std.testing.expectEqualStrings("unreachable: ssh: co", wall_picker.hostState(&narrow, &h));

    // No room for even the prefix: the old word, never a clipped one.
    var tiny: [8]u8 = undefined;
    try std.testing.expectEqualStrings("unreachable", wall_picker.hostState(&tiny, &h));

    // A host that came back has nothing to explain.
    h.poll.reachable.store(true, .release);
    try std.testing.expectEqualStrings("no sessions", wall_picker.hostState(&wide, &h));
}

test "askRows: a masked answer shows one star per byte and never the bytes" {
    var body: wall_picker.AskBody = .{};
    wall_picker.askRows(&body, "e2e@box's password: ", "hunter2", .secret, 60);
    try std.testing.expectEqual(@as(usize, 2), body.n);
    try std.testing.expectEqualStrings(" e2e@box's password: ", body.row(0));
    // The count is the feedback: ssh will not echo, and a box that showed
    // nothing at all reads as one that is not listening.
    try std.testing.expectEqualStrings("> *******_", body.row(1));
    // The claim this whole box exists to keep.
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "hunter2") == null);
}

test "askRows: a host-key answer is shown, because the user is comparing it" {
    var body: wall_picker.AskBody = .{};
    wall_picker.askRows(&body, "Are you sure you want to continue connecting (yes/no)? ", "yes", .confirm, 60);
    try std.testing.expectEqualStrings("> yes_", body.row(body.n - 1));
}

test "askRows: a NOTICE has no input line, because ssh is not asking anything" {
    // "Confirm user presence for key ..." is ssh telling the user to touch
    // a key, and it takes no answer: ssh kills the helper when the touch
    // lands. An input line under it would invite one nothing would read.
    var body: wall_picker.AskBody = .{};
    wall_picker.askRows(&body, "Confirm user presence for key ED25519-SK SHA256:xyz", "", .notice, 60);
    try std.testing.expectEqual(@as(usize, 1), body.n);
    try std.testing.expectEqualStrings(" Confirm user presence for key ED25519-SK SHA256:xyz", body.row(0));
}

test "askRows: a long question wraps to the width and stops at the cap" {
    var body: wall_picker.AskBody = .{};
    var long: [600]u8 = @splat('q');
    wall_picker.askRows(&body, &long, "", .confirm, 42);
    // Four rows of question plus the answer row: a prompt that filled the
    // terminal would leave nowhere to type.
    try std.testing.expectEqual(wall_picker.ask_rows_max + 1, body.n);
    for (0..wall_picker.ask_rows_max) |i| {
        // One space of margin, 40 of question: the row never exceeds the
        // box, and `pickerLine` pads the rest.
        try std.testing.expectEqual(@as(usize, 41), body.row(i).len);
    }
    try std.testing.expectEqualStrings("> _", body.row(wall_picker.ask_rows_max));
}

test "paintAsk: replayed into an engine, the box is centred and the answer is stars" {
    const alloc = std.testing.allocator;
    const cols: u16 = 160;
    const rows: u16 = 20;
    var screen = try WallScreen.init(alloc, cols, rows);
    defer screen.deinit();
    // Off-origin in both axes, `paintPicker`'s reason: a fixture on a
    // terminal the box happens to fill is blind to every centring mistake.
    var bg: [cols]u8 = undefined;
    @memset(&bg, '.');
    var r: u16 = 0;
    while (r < rows) : (r += 1) {
        var cup: [16]u8 = undefined;
        screen.eng.feed(std.fmt.bufPrint(&cup, "\x1b[{d};1H", .{r + 1}) catch unreachable);
        screen.eng.feed(&bg);
    }
    var shared = Shared{ .out_fd = screen.w, .size = .{ .cols = cols, .rows = rows }, .is_tty = true };
    var prefix: interact.PrefixFilter = .{};
    prefix.askOpen(.secret);
    var typed = "hunter2".*;
    _ = prefix.feed(&typed);
    wall_picker.paintAsk(&shared, "e2e@box's password: ", &prefix);
    screen.drain();
    const dump = try screen.eng.dumpPlain(alloc);
    defer alloc.free(dump);

    // The GRID, not the frame: a paint that reached a cell by another CUP
    // form or a wider pad is invisible to a grep over the bytes.
    try std.testing.expect(std.mem.indexOf(u8, dump, "e2e@box's password:") != null);
    try std.testing.expect(std.mem.indexOf(u8, dump, "*******_") != null);
    // The one thing that must never be on a screen anyone can scroll back.
    try std.testing.expect(std.mem.indexOf(u8, dump, "hunter2") == null);
    // ...and every tile is held off the terminal while it is up.
    try std.testing.expect(wv.popupOpen(&shared));
    const w: u16 = @intCast(wall_picker.picker_row_max);
    const left: u16 = (cols - w) / 2;
    const height: u16 = 3;
    const top: u16 = (rows - height) / 2;
    var i: u16 = 0;
    while (i < rows) : (i += 1) {
        const l = WallScreen.line(dump, i) orelse return error.NoSuchRow;
        if (i >= top and i < top + height) {
            try std.testing.expectEqualStrings(bg[0..left], l[0..left]);
            try std.testing.expectEqualStrings(bg[0..left], l[left + w ..]);
        } else {
            try std.testing.expectEqualStrings(&bg, l);
        }
    }
}

test "closeAsk: the picker comes back when the prompt box leaves, whichever end ended it" {
    // The claim n1 found missing on one of the two ends. `relayout` CLEARS
    // the screen, so a picker still open under the box owes a repaint —
    // and a prompt CAN arrive over an open picker, which is why the answer
    // has a buffer of its own. Without the repaint, `picking` is true on a
    // blank screen until the next key.
    const alloc = std.testing.allocator;
    const cols: u16 = 100;
    const rows: u16 = 30;
    var screen = try WallScreen.init(alloc, cols, rows);
    defer screen.deinit();
    var shared = Shared{ .out_fd = screen.w, .size = .{ .cols = cols, .rows = rows }, .is_tty = true };
    shared.tree = layout.Tree.init(alloc);
    shared.flat_alloc = alloc;
    defer shared.tree.deinit();
    defer if (shared.last_flat) |*f| f.deinit(alloc);
    defer if (shared.base_flat) |*f| f.deinit(alloc);

    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/a.sock", "/tmp/a.sock"),
        fixture.testHost(&shared, "--sock /tmp/b.sock", "/tmp/b.sock"),
    };
    for (&table) |*h| {
        fixture.setList(h, "");
        h.applied = true;
    }
    var tiles: [1]Tile = undefined;
    var present = [_]bool{false};
    var live: usize = 0;
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);

    // A notice over an open picker, which is exactly the pair that hangs
    // up on its own: ssh kills the notifier helper when the touch lands.
    var prefix: interact.PrefixFilter = .{};
    prefix.picking = true;
    prefix.askOpen(.notice);
    shared.ask_open.store(true, .release);
    screen.drain();

    wv.closeAsk(w, &shared, &prefix, 0, 0);
    screen.drain();
    const dump = try screen.eng.dumpPlain(alloc);
    defer alloc.free(dump);
    try std.testing.expect(!prefix.asking);
    try std.testing.expect(!shared.ask_open.load(.acquire));
    // The GRID, not the frame: the box is gone and the rows the user was
    // choosing from are back on it.
    try std.testing.expect(std.mem.indexOf(u8, dump, "hosts") != null);
    try std.testing.expect(std.mem.indexOf(u8, dump, "/tmp/b.sock") != null);

    // ...and with no picker under it, nothing is painted in its place: the
    // wall is what the screen goes back to. `closeAsk` leaves `picking`
    // alone — the picker is not the box's to close — so the test says
    // which wall this second half is.
    prefix.picking = false;
    prefix.askOpen(.secret);
    shared.ask_open.store(true, .release);
    wall_picker.paintAsk(&shared, "box's password: ", &prefix);
    screen.drain();
    wv.closeAsk(w, &shared, &prefix, 0, 0);
    screen.drain();
    const after = try screen.eng.dumpPlain(alloc);
    defer alloc.free(after);
    try std.testing.expect(std.mem.indexOf(u8, after, "/tmp/b.sock") == null);
    try std.testing.expect(std.mem.indexOf(u8, after, "password") == null);
}

test "sessionRows: a host's sessions, marked when already on this wall, with the holder count when the daemon says" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var hosts_table = [_]Host{fixture.testHost(&shared, "box", "/b")};
    fixture.setList(&hosts_table[0], "0\nwork\n# holds 0 2\n# holds work 1\n# mux 0.0.1-18");
    // Two panes, because a fixture with one is blind to a lookup that
    // aliased every session onto the first tile it walked.
    var tiles = [_]Tile{ fixture.claimBench(&shared, 0), fixture.claimBench(&shared, 1) };
    tiles[0].host = 0;
    tiles[0].r.session = "work";
    tiles[1].host = 0;
    tiles[1].r.session = "elsewhere";
    var present = [_]bool{ true, true };
    var live: usize = 2;
    const w = fixture.wallOf(std.testing.allocator, &tiles, &present, &live, &shared, &hosts_table);

    var body = wall_picker.PickerBody{};
    wall_picker.sessionRows(&body, w, 0, 1, 80);
    try std.testing.expectEqual(@as(usize, 2), body.n);
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), " 0 ") != null);
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "2 clients") != null);
    // Session 0 has no pane, so it is not marked; `work` does.
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "on this wall") == null);
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "work") != null);
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "on this wall") != null);
    // One holder is singular: the count is read as a number of people.
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "1 client,") == null);
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "1 client") != null);
    // `sel_row` marks the row, not the host: the second one is selected.
    try std.testing.expect(std.mem.indexOf(u8, body.row(1), "> ") != null);
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "> ") == null);
}

test "sessionRows: a pane on ANOTHER host does not mark this host's same-named session" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var hosts_table = [_]Host{
        fixture.testHost(&shared, "box", "/b"),
        fixture.testHost(&shared, "other", "/o"),
    };
    fixture.setList(&hosts_table[0], "work\n");
    fixture.setList(&hosts_table[1], "work\n");
    var tiles = [_]Tile{fixture.claimBench(&shared, 0)};
    tiles[0].host = 1;
    tiles[0].r.session = "work";
    var present = [_]bool{true};
    var live: usize = 1;
    const w = fixture.wallOf(std.testing.allocator, &tiles, &present, &live, &shared, &hosts_table);

    var body = wall_picker.PickerBody{};
    wall_picker.sessionRows(&body, w, 0, 0, 80);
    try std.testing.expectEqual(@as(usize, 1), body.n);
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "on this wall") == null);
    wall_picker.sessionRows(&body, w, 1, 0, 80);
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "on this wall") != null);
}

test "sessionRows: an old daemon's list shows no count rather than zero" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var hosts_table = [_]Host{fixture.testHost(&shared, "box", "/b")};
    fixture.setList(&hosts_table[0], "0\n");
    const w = fixture.hostWall(&shared, &hosts_table);
    var body = wall_picker.PickerBody{};
    wall_picker.sessionRows(&body, w, 0, 0, 80);
    try std.testing.expectEqual(@as(usize, 1), body.n);
    // Never "0 clients": a daemon that cannot count said nothing, and a
    // zero would read as a session nobody is in.
    try std.testing.expect(std.mem.indexOf(u8, body.row(0), "client") == null);
}

test "sessionCount and rowStep: the row walks the host's own list and wraps" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var hosts_table = [_]Host{fixture.testHost(&shared, "box", "/b")};
    fixture.setList(&hosts_table[0], "0\na\nb\n# holds 0 1");
    try std.testing.expectEqual(@as(usize, 3), wall_picker.sessionCount(&hosts_table, 0));
    // A selection with no host under it — an empty hosts file leaves one —
    // counts zero rather than faulting.
    try std.testing.expectEqual(@as(usize, 0), wall_picker.sessionCount(&hosts_table, 7));
    try std.testing.expectEqual(@as(usize, 1), wall_picker.rowStep(0, 1, 3));
    try std.testing.expectEqual(@as(usize, 0), wall_picker.rowStep(2, 1, 3));
    try std.testing.expectEqual(@as(usize, 2), wall_picker.rowStep(0, -1, 3));
    // An empty list has no row to rest on.
    try std.testing.expectEqual(@as(usize, 0), wall_picker.rowStep(4, 1, 0));
}

test "pickAdd: a listed session becomes a pane once; a second add zooms to it" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    shared.size = .{ .cols = 120, .rows = 40 };
    var hosts_table = [_]Host{fixture.testHost(&shared, "--sock /a", "/a")};
    fixture.setList(&hosts_table[0], "0\nwork\n");
    var tiles: [wv.max_tiles]Tile = undefined;
    var present = [_]bool{false} ** wv.max_tiles;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &hosts_table);

    const first = wall_picker.pickAdd(w, 0, 1).?;
    try std.testing.expectEqualStrings("work", tiles[first].r.session);
    // JOINED, never created: the session is already on that daemon, and a
    // create would take a name the daemon has.
    try std.testing.expect(!tiles[first].creates);
    try std.testing.expectEqual(@as(?usize, 0), tiles[first].host);
    try std.testing.expectEqual(@as(usize, 1), wv.presentCount(w.livePresent()));

    const again = wall_picker.pickAdd(w, 0, 1).?;
    try std.testing.expectEqual(first, again);
    try std.testing.expectEqual(@as(usize, 1), wv.presentCount(w.livePresent()));
    try std.testing.expectEqual(first, shared.sel);

    // A row the list does not have adds nothing, and says so.
    try std.testing.expectEqual(@as(?usize, null), wall_picker.pickAdd(w, 0, 9));
    var buf: [96]u8 = undefined;
    try std.testing.expect(std.mem.indexOf(u8, wv.takeNotice(&shared, &buf), "no session") != null);
}

test "pickBirth: the name a restarted daemon calls free is already a pane, and the birth is refused to it" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    shared.size = .{ .cols = 120, .rows = 40 };
    // TWO hosts and the pane on the SECOND: a one-host fixture cannot see a
    // birth that matched a pane on the wrong machine, and two daemons may
    // each call their own next name `0`.
    var hosts_table = [_]Host{
        fixture.testHost(&shared, "--sock /a", "/a"),
        fixture.testHost(&shared, "--sock /b", "/b"),
    };
    fixture.setList(&hosts_table[0], "0\n");
    fixture.setList(&hosts_table[1], "0\n");
    var tiles: [wv.max_tiles]Tile = undefined;
    var present = [_]bool{false} ** wv.max_tiles;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &hosts_table);

    const pane = wall_picker.pickAdd(w, 1, 0).?;
    try std.testing.expectEqualStrings("0", tiles[pane].r.session);

    // The daemon restarts: it answers an empty list, the pane it used to
    // serve stands there wearing `gone`, and `nextFreeName` off that list
    // hands back `0` — the name the pane already spells.
    fixture.setList(&hosts_table[1], "");
    var buf: [96]u8 = undefined;
    _ = wv.takeNotice(&shared, &buf);
    shared.sel = 0;

    const at = wall_picker.pickBirth(w, 1);
    // The pane the user meant, not a second tile onto one session: two
    // panes spelling one leaf is a repeat, and `seedLayout` refuses a file
    // with one WHOLE — the wall would be gone at the next start.
    try std.testing.expectEqual(@as(?usize, pane), at);
    try std.testing.expectEqual(@as(usize, 1), wv.presentCount(w.livePresent()));
    try std.testing.expectEqual(pane, shared.sel);
    const said = wv.takeNotice(&shared, &buf);
    try std.testing.expect(std.mem.indexOf(u8, said, "already a pane") != null);
    try std.testing.expect(std.mem.indexOf(u8, said, "0") != null);
}

test "paintPicker: the session level names the host and lists its sessions" {
    const pipe = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(pipe[0]);
    defer std.posix.close(pipe[1]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 100, .rows = 40 }, .is_tty = true };
    defer shared.tree.deinit();
    var table = [_]Host{
        fixture.testHost(&shared, "box", "/tmp/a.sock"),
        fixture.testHost(&shared, "other", "/tmp/b.sock"),
    };
    for (&table) |*h| h.applied = true;
    fixture.setList(&table[1], "0\nwork\n# holds 0 1");
    fixture.setList(&table[0], "only\n");
    var buf: [8192]u8 = undefined;
    // Host 1, not host 0: an off-origin selection is the baseline, so a
    // paint that read the FIRST host's list would be caught.
    const frame = fixture.pickerFrameAt(&shared, pipe[0], &table, 1, .{ .level = .sessions, .row = 1 }, &buf);
    try std.testing.expect(std.mem.indexOf(u8, frame, "sessions on other") != null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "work") != null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "1 client") != null);
    // The other host's session is not in this box.
    try std.testing.expect(std.mem.indexOf(u8, frame, "only") == null);
    // The footer is the session level's keys, not the host level's.
    try std.testing.expect(std.mem.indexOf(u8, frame, "x end") != null);
    try std.testing.expect(std.mem.indexOf(u8, frame, "x forget") == null);
}

test "Shared.PickEnd: the second x forces only on the same host and row, inside the window" {
    var end: wv.PickEnd = .{};
    try std.testing.expect(!end.armedFor(0, "work", 1000));
    end.arm(0, "work", 1000 + wv.end_arm_ms);
    try std.testing.expect(end.armedFor(0, "work", 1500));
    // A different row is a FIRST press: the count the user read was about
    // another session.
    try std.testing.expect(!end.armedFor(0, "other", 1500));
    // ...and so is the same name on another daemon.
    try std.testing.expect(!end.armedFor(1, "work", 1500));
    // The window closes on the clock.
    try std.testing.expect(!end.armedFor(0, "work", 1000 + wv.end_arm_ms + 1));
    // An accepted end disarms: the window must not outlive its session.
    end.arm(0, "work", 1000 + wv.end_arm_ms);
    end.clear();
    try std.testing.expect(!end.armedFor(0, "work", 1500));
}

/// A daemon stand-in for `pickEnd`: one connection per press, each answering
/// one `end_req` with a scripted verdict and recording the `force` bit that
/// arrived. A REAL socket, because `pickEnd`'s whole job is the side
/// connection — the verdicts are the daemon's to make and are pinned against
/// a real one in `server_test_session.zig`.
const EndFake = struct {
    listener: std.net.Server,
    replies: []const proto.EndReply,
    force: [4]bool = @splat(false),
    n: usize = 0,

    fn serve(self: *EndFake) void {
        const alloc = std.testing.allocator;
        while (self.n < self.replies.len) {
            const conn = self.listener.accept() catch return;
            defer conn.stream.close();
            const f = (proto.readFrame(alloc, conn.stream.handle) catch return) orelse return;
            defer f.deinit(alloc);
            if (f.type != .end_req or f.payload.len < proto.end_req_len) return;
            const at = self.n;
            self.force[at] = f.payload[0] != 0;
            self.n += 1;
            const r = self.replies[at];
            var buf: [proto.end_reply_max_len]u8 = undefined;
            proto.writeFrame(
                conn.stream.handle,
                .end_reply,
                proto.encodeEndReply(&buf, r.accepted, r.others, r.reason),
            ) catch return;
        }
    }
};

test "pickEnd: the first x arms with the count, the second forces, and a refusal that is not others-attached arms nothing" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    const sp = try std.fmt.allocPrint(alloc, "{s}/end.sock", .{tmp.path()});
    defer alloc.free(sp);
    const addr = try std.net.Address.initUnix(sp);
    var fake = EndFake{
        .listener = try addr.listen(.{}),
        .replies = &.{
            .{ .accepted = false, .others = 2, .reason = proto.end_reason.others_attached },
            .{ .accepted = true, .others = 0, .reason = proto.end_reason.accepted },
            .{ .accepted = false, .others = 0, .reason = proto.end_reason.no_session },
        },
    };
    defer fake.listener.deinit();
    const th = try std.Thread.spawn(.{}, EndFake.serve, .{&fake});

    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    // TWO hosts, and the daemon on the SECOND: a one-host fixture cannot
    // see a press that reached for the wrong row's machine. Host 0's socket
    // has nothing on it, so a misdirected press fails loudly.
    var table = [_]Host{
        fixture.testHost(&shared, "nowhere", "/tmp/mux-no-such-socket"),
        fixture.testHost(&shared, "box", sp),
    };
    // The two recipes deliberately DIFFER, and only the poller's reaches the
    // fake: an end must ride the batch recipe (`BatchMode`, a connect
    // timeout, `asked` false) and never the interactive one that would
    // prompt on /dev/tty under the popup and start a daemon.
    table[1].spec.target = .{ .sock = "/tmp/mux-no-such-socket-entry" };
    // The holds lines are what say this daemon is new enough to be asked.
    fixture.setList(&table[1], "0\nwork\n# holds 0 1\n# holds work 3");
    const w = fixture.hostWall(&shared, &table);
    var buf: [96]u8 = undefined;

    // First press: refused with the count, and the window is armed on THIS
    // host and THIS name.
    wall_picker.pickEnd(w, 1, 1, 1000);
    try std.testing.expect(shared.pick_end.armedFor(1, "work", 1500));
    try std.testing.expect(!shared.pick_end.armedFor(1, "0", 1500));
    try std.testing.expect(!shared.pick_end.armedFor(0, "work", 1500));
    const first = wv.takeNotice(&shared, &buf);
    try std.testing.expect(std.mem.indexOf(u8, first, "work") != null);
    // The DAEMON's verdict (2 others), not the row's holder count (3): the
    // `# holds` line counts every holder including the asker, and the two
    // numbers are deliberately different in this fixture so a notice built
    // from the wrong one would be caught.
    try std.testing.expect(std.mem.indexOf(u8, first, "2 others attached") != null);
    try std.testing.expect(std.mem.indexOf(u8, first, "3 other") == null);
    try std.testing.expect(std.mem.indexOf(u8, first, "x again to end") != null);

    // Second press inside the window: forced, accepted, and disarmed.
    wall_picker.pickEnd(w, 1, 1, 1500);
    try std.testing.expect(!shared.pick_end.armedFor(1, "work", 1600));
    const second = wv.takeNotice(&shared, &buf);
    try std.testing.expect(std.mem.indexOf(u8, second, "ending work on box") != null);

    // A refusal that is NOT others-attached: the daemon's reason, in this
    // client's words, and no arm — a next `x` must not force an end on a
    // session the daemon just said it has not got.
    wall_picker.pickEnd(w, 1, 1, 5000);
    try std.testing.expect(!shared.pick_end.armedFor(1, "work", 5100));
    const third = wv.takeNotice(&shared, &buf);
    try std.testing.expect(std.mem.indexOf(u8, third, "work") != null);
    try std.testing.expect(std.mem.indexOf(u8, third, "no such session") != null);
    try std.testing.expect(std.mem.indexOf(u8, third, "x again") == null);
    th.join();

    // Three presses, three connections — and the force bit is the SECOND
    // press and nothing else.
    try std.testing.expectEqual(@as(usize, 3), fake.n);
    try std.testing.expectEqual([3]bool{ false, true, false }, fake.force[0..3].*);
}

/// A daemon that accepts the connection and answers NOTHING: what a box
/// with no `end_req` arm does with a frame type it does not know. The
/// connection has to stay OPEN — a close would be a transport error, and
/// the fact under test is the reply budget running out.
const SilentFake = struct {
    listener: std.net.Server,
    stop: std.atomic.Value(bool) = .init(false),

    fn serve(self: *SilentFake) void {
        const conn = self.listener.accept() catch return;
        defer conn.stream.close();
        while (!self.stop.load(.acquire)) std.Thread.sleep(20 * std.time.ns_per_ms);
    }
};

test "pickEnd: a daemon that answers nothing is the too-old sentence, and the holds line is not the gate" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    const sp = try std.fmt.allocPrint(alloc, "{s}/silent.sock", .{tmp.path()});
    defer alloc.free(sp);
    const addr = try std.net.Address.initUnix(sp);
    var fake = SilentFake{ .listener = try addr.listen(.{}) };
    defer fake.listener.deinit();
    const th = try std.Thread.spawn(.{}, SilentFake.serve, .{&fake});

    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var table = [_]Host{
        fixture.testHost(&shared, "new", "/tmp/mux-no-such-socket-a"),
        fixture.testHost(&shared, "old", sp),
    };
    fixture.setList(&table[0], "0\n# holds 0 1");
    // No `# holds` line: the absence used to refuse the press up front, and
    // that was wrong about a real box — the released v0.0.1-16 daemon
    // answers `end_req` and sends no holds line. The verdict comes off the
    // WIRE now, so this list says nothing about whether the end is asked.
    fixture.setList(&table[1], "0\nwork\n");
    const w = fixture.hostWall(&shared, &table);
    var buf: [96]u8 = undefined;
    wall_picker.pickEnd(w, 1, 0, 1000);
    fake.stop.store(true, .release);
    th.join();
    // The pump's own wording for the same fact about the box, and no arm:
    // nothing was answered, so there is nothing to press again for.
    try std.testing.expectEqualStrings("[daemon too old to end a session]", wv.takeNotice(&shared, &buf));
    try std.testing.expect(!shared.pick_end.armedFor(1, "0", 1500));
}

test "pickEnd: a daemon with no holds line that ANSWERS ends the session" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    const sp = try std.fmt.allocPrint(alloc, "{s}/end16.sock", .{tmp.path()});
    defer alloc.free(sp);
    const addr = try std.net.Address.initUnix(sp);
    var fake = EndFake{
        .listener = try addr.listen(.{}),
        .replies = &.{.{ .accepted = true, .others = 0, .reason = proto.end_reason.accepted }},
    };
    defer fake.listener.deinit();
    const th = try std.Thread.spawn(.{}, EndFake.serve, .{&fake});

    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var table = [_]Host{
        fixture.testHost(&shared, "nowhere", "/tmp/mux-no-such-socket-d"),
        fixture.testHost(&shared, "box", sp),
    };
    // A v0.0.1-16 daemon's list, verbatim in shape: names and the meta
    // line, no holds. It has the `end_req` arm all the same, and refusing
    // it for the missing count refused an end that works.
    fixture.setList(&table[1], "0\nwork\n# mux 0.0.1-16");
    const w = fixture.hostWall(&shared, &table);
    var buf: [96]u8 = undefined;
    wall_picker.pickEnd(w, 1, 1, 1000);
    th.join();
    try std.testing.expectEqual(@as(usize, 1), fake.n);
    try std.testing.expect(!fake.force[0]);
    try std.testing.expectEqualStrings("[ending work on box]", wv.takeNotice(&shared, &buf));
}

test "pickEnd: a row the list does not have asks nothing" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/mux-no-such-socket-c")};
    fixture.setList(&table[0], "0\n# holds 0 1");
    const w = fixture.hostWall(&shared, &table);
    var buf: [96]u8 = undefined;
    wall_picker.pickEnd(w, 0, 4, 1000);
    try std.testing.expectEqualStrings("[no session on that row]", wv.takeNotice(&shared, &buf));
}