a73x

src/tui/wall_test_host.zig

Ref:   Size: 54.6 KiB   History

//! The hosts file, the host table and the poller (wall_host.zig).
const std = @import("std");
const client = @import("client");
const hosts = @import("client").hosts;
const TmpDir = @import("testtmp").TmpDir;
const fixture = @import("wall_test_harness.zig");
const wall_host = @import("wall_host.zig");
const wall_layout = @import("wall_layout.zig");
const wall_pump = @import("wall_pump.zig");
const wv = @import("wallview.zig");
const AddHost = wall_host.AddHost;
const Host = wall_host.Host;
const HostSpec = client.HostSpec;
const Shared = wv.Shared;
const Tile = wv.Tile;
const TileIdxs = wall_host.TileIdxs;

/// Panes the layout seated and a poll already bound: live tiles, in the
/// tree, whose pump never ran — so `alive` is whatever the test states and
/// there is no thread to join. The wall's tiles come from the layout now,
/// so a poll test that wants tiles has to seat them itself.
fn seatBound(
    tiles: []Tile,
    present: []bool,
    live: *usize,
    shared: *Shared,
    rs: []const wall_host.Resolved,
    hostof: []const usize,
) !void {
    for (rs, hostof, 0..) |r, h, i| {
        tiles[i] = .{
            .r = r,
            .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
            .shared = shared,
            .idx = i,
            .wake_r = -1,
            .wake_w = -1,
            .host = h,
            .alive = std.atomic.Value(bool).init(false),
        };
        present[i] = true;
        if (i == 0) {
            try shared.tree.addFirst(0);
        } else {
            try shared.tree.insert(@intCast(i - 1), @intCast(i));
        }
    }
    live.* = rs.len;
}

test "planHostDiff: tiles the daemon dropped vanish, and another host's same name is untouched" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, true, true };

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    // The vanish waits for a second list to agree (see the grace test
    // below), so this list is asked twice. "c" is a session of host 0's
    // that no pane spells, and it stays nothing to this wall.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nc\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), vanish.len);
    try std.testing.expectEqual(@as(usize, 1), vanish.get(0));

    // Host 1's "a" survives host 0's list saying nothing about it.
    vanish.len = 0;
    wall_host.planHostDiff(&tiles, &present, 3, 1, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);

    // An empty list vanishes everything the host had, once every tile has
    // spent the grace.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
    vanish.len = 0;
    wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 2), vanish.len);
}

test "planHostDiff: a live pump's tile is not vanished by ONE list that lacks it" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, true, true };

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    // A poll can overtake a session's exit: the daemon has already dropped
    // the name while the pump has not yet read its `exit_status`. Vanishing
    // on that list loses the exit code — on a wall of one, `endedTile` skips
    // a tile that is no longer present and mux never leaves.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    try std.testing.expect(tiles[1].missed_once);

    // A list that names it again forgives it: a tile does not accumulate
    // misses across the seconds it is legitimately live.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    try std.testing.expect(!tiles[1].missed_once);

    // Two consecutive lists without it, and it goes.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), vanish.len);
    try std.testing.expectEqual(@as(usize, 1), vanish.get(0));

    // A pump that has ENDED has no exit code left to lose, so its tile
    // needs no grace at all.
    tiles[0].alive.store(false, .release);
    vanish.len = 0;
    const only0 = [_]bool{ true, false, false };
    wall_host.planHostDiff(&tiles, &only0, 3, 0, "", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), vanish.len);
    try std.testing.expectEqual(@as(usize, 0), vanish.get(0));
}

test "planHostDiff: a pending pane the reachable list disowns is gone, not vanished — and a later list still binds it" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, true, true };
    // Panes 0 and 1 are sidecar restores: no pump ever ran, nothing dialed.
    for (tiles[0..2]) |*t| {
        t.pending = true;
        t.state = .waiting;
        t.alive.store(false, .release);
    }

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    // The reachable host answers with NEITHER saved session: both panes are
    // gone-fodder, neither is vanish-fodder, and host 1's tile is untouched.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    try std.testing.expectEqual(@as(usize, 2), gones.len);
    try std.testing.expectEqual(@as(usize, 0), gones.get(0));
    try std.testing.expectEqual(@as(usize, 1), gones.get(1));

    // Once dressed, a pane is not re-listed every second: the poll comes
    // back every 1s and a repaint per poll would flicker the bar.
    tiles[0].state = .gone;
    tiles[1].state = .gone;
    gones.len = 0;
    wall_host.planHostDiff(&tiles, &present, 3, 0, "", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), gones.len);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);

    // The dressing is reversible: a list that names session "a" binds pane 0
    // (it stayed pending), in the rect it never left.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), binds.len);
    try std.testing.expectEqual(@as(usize, 0), binds.get(0));
}

test "planHostDiff: a vanished tile is not present, so the next list does not vanish it twice" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, false, true };

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
}

test "planHostDiff: the session this shell is inside never wakes a pane" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, true, true };
    // Two pending panes, one of them spelling the session mux is running
    // inside. `seedLayout` refuses such a leaf, so this is the poll's own
    // half of the same refusal: a pane it woke would attach a replica to
    // the grid it is painting into.
    for (tiles[0..2]) |*t| {
        t.pending = true;
        t.state = .waiting;
        t.alive.store(false, .release);
    }

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", "a", &binds, &vanish, &gones);
    // Only "b" is woken; the pane named for this shell stays as it was,
    // and is not dressed gone either — the daemon does have that session.
    try std.testing.expectEqual(@as(usize, 1), binds.len);
    try std.testing.expectEqual(@as(usize, 1), binds.get(0));
    try std.testing.expect(tiles[0].pending);
    try std.testing.expectEqual(@as(usize, 0), gones.len);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
}

test "applyReadyLists: only the host that reported is marked, and its sessions are not tiles" {
    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: [8]Tile = undefined;
    var present = [_]bool{false} ** 8;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    // TWO hosts open the wall and a THIRD arrives from the picker. A fixture
    // whose table never grows cannot see a per-host flag sized at the count
    // the wall opened with — which is the whole of this.
    var table = [_]Host{
        fixture.testHost(&shared, "box", "/tmp/box.sock"),
        fixture.testHost(&shared, "vm", "/tmp/vm.sock"),
        fixture.testHost(&shared, "--sock /tmp/added.sock", "/tmp/added.sock"),
    };
    fixture.setList(&table[2], "late\n");
    table[2].poll.list_ready.store(true, .release);

    _ = wall_host.applyReadyLists(fixture.wallOf(alloc, &tiles, &present, &live, &shared, table[0..3]));

    // Only the host that reported is marked: the two that opened the wall
    // are still owed a first list.
    try std.testing.expect(table[2].applied);
    try std.testing.expect(!table[0].applied);
    try std.testing.expect(!table[1].applied);
    // ...and the session it reported is on nobody's wall: this one was
    // authored with no panes, and a poll does not author one.
    try std.testing.expectEqual(@as(usize, 0), live);
    try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
}

test "recordHost: a file it cannot write comes BACK — the wall may be on the alternate screen" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    var bad_buf: [256]u8 = undefined;
    var ok_buf: [256]u8 = undefined;
    var blocker_buf: [256]u8 = undefined;
    // A path whose parent is a FILE. `saveBytes` makes missing directories,
    // so an absent one is not a failure — this is. The caller has to say so
    // out loud, and WHERE it may say it depends on which caller it is:
    // stderr before the screen is taken, a notice after.
    const blocker = try std.fmt.bufPrint(&blocker_buf, "{s}/notadir", .{tmp.path()});
    try hosts.saveBytes(blocker, "x");
    const bad = try std.fmt.bufPrint(&bad_buf, "{s}/notadir/hosts", .{tmp.path()});
    const ok = try std.fmt.bufPrint(&ok_buf, "{s}/hosts", .{tmp.path()});
    try std.testing.expect(wall_host.recordHost(alloc, .{ .sock = "/a" }, "--sock /a", bad) != null);
    try std.testing.expect(wall_host.recordHost(alloc, .{ .sock = "/a" }, "--sock /a", ok) == null);
    // `--via` has no form in the host grammar, and writing nothing down for
    // it is not a failure to report.
    try std.testing.expect(wall_host.recordHost(alloc, .{ .via = "ssh h mux d proxy" }, "x", bad) == null);
}

test "applyHostList: drift dresses every tile of its host, and repaints only on change" {
    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.own_version = "1.0";
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    defer fixture.endPumps(tiles[0..live]);
    // Two drifted hosts side by side — one behind us, one stale on its own
    // box with TWO sessions, so "every tile of the host" is plural where it
    // can be.
    var table = [_]Host{
        fixture.testHost(&shared, "behind", "/tmp/behind.sock"),
        fixture.testHost(&shared, "stale", "/tmp/stale.sock"),
    };
    fixture.setList(&table[0], "a\n# mux 0.9");
    fixture.setList(&table[1], "b\nc\n# mux 1.0 stale");
    try seatBound(&tiles, &present, &live, &shared, &.{
        .{ .target = .{ .sock = "/tmp/behind.sock" }, .label = "behind#a", .session = "a" },
        .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#b", .session = "b" },
        .{ .target = .{ .sock = "/tmp/stale.sock" }, .label = "stale#c", .session = "c" },
    }, &.{ 0, 1, 1 });

    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
    try std.testing.expectEqual(@as(usize, 3), wv.presentCount(present[0..live]));
    for (tiles[0..live], present[0..live]) |*t, p| {
        if (!p) continue;
        const want: []const u8 = if (t.host == @as(?usize, 0)) "daemon 0.9" else "daemon stale";
        try std.testing.expectEqualStrings(want, t.drift[0..t.drift_len]);
    }

    // The same lists again: same words, and NO repaint asked for — a wall
    // repainted once a second per host would flicker for no news.
    const gen = shared.repaint_gen.load(.acquire);
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
    try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire));

    // The behind daemon upgrades under us: its word leaves, and that IS news.
    fixture.setList(&table[0], "a\n# mux 1.0");
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    for (tiles[0..live], present[0..live]) |*t, p| {
        if (!p or t.host != @as(?usize, 0)) continue;
        try std.testing.expectEqualStrings("", t.drift[0..t.drift_len]);
    }
    try std.testing.expect(shared.repaint_gen.load(.acquire) != gen);
}

test "applyHostList: a host makes no tile — not when it is silent, not when it lists a session" {
    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]);
    // The two ways a host has nothing to show, side by side: one that never
    // answered and one that answered with an empty list.
    var table = [_]Host{
        fixture.testHost(&shared, "down", "/tmp/nobody.sock"),
        fixture.testHost(&shared, "empty", "/tmp/empty.sock"),
    };
    table[0].poll.reachable.store(false, .release);
    fixture.setList(&table[1], "");

    // Twice, because a placeholder that is born once is still a placeholder.
    for (0..2) |_| {
        wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
        wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);
    }

    try std.testing.expectEqual(@as(usize, 0), live);
    try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));

    // The third way, and the one the layout changed: the host answers with
    // a session, and this wall — which names no pane — still shows nothing.
    // A session somebody else started is not this user's authored wall.
    fixture.setList(&table[1], "a\n");
    for (0..2) |_|
        wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 1);

    try std.testing.expectEqual(@as(usize, 0), live);
    try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
}

test "applyHostList: a live tile survives one list that lost its session, and goes on the next" {
    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;
    // Two panes, because the tile that keeps its name is what shows the
    // grace is per tile and not a wall-wide pause. No pump ever runs here,
    // so `endPumps` has nothing to wait for and the liveness the rule turns
    // on is stated rather than raced for.
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
    try seatBound(&tiles, &present, &live, &shared, &.{
        .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" },
        .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#b", .session = "b" },
    }, &.{ 0, 0 });
    tiles[0].alive.store(true, .release);
    tiles[1].alive.store(true, .release);

    fixture.setList(&table[0], "a\n");
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    // Still there: this list may have overtaken an `exit_status` the pump
    // has not read yet, and that code is the run's own on a wall of one.
    try std.testing.expect(present[1]);
    try std.testing.expect(present[0]);

    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    try std.testing.expect(!present[1]);
    try std.testing.expect(present[0]);
}

test "applyHostList: a busy daemon leaves an empty wall empty — a list is not a source of tiles" {
    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]);
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
    // THREE sessions, none of them a pane: a wall the user authored empty
    // stays empty however busy the box is, and the picker is the door onto
    // it. Twice, because a tile born once is still a tile.
    fixture.setList(&table[0], "a\nb\nc\n");

    for (0..2) |_|
        wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);

    try std.testing.expectEqual(@as(usize, 0), live);
    try std.testing.expectEqual(@as(usize, 0), shared.tree.count());
    try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
    // Not even a word about them: a wall that shows the panes it was given
    // is not a wall hiding the rest of the daemon.
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings("", wv.takeNotice(&shared, &buf));
}

test "applyHostList: when a host's last session goes the wall empties, and nothing stands in for the host" {
    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 = 1;
    defer fixture.endPumps(tiles[0..live]);
    tiles[0] = .{
        .r = .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "--sock /tmp/box.sock#a", .session = "a" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
        .host = 0,
        .alive = std.atomic.Value(bool).init(false),
    };
    present[0] = true;
    try shared.tree.addFirst(0);
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};
    // The daemon has nothing left: a restart, or another client ended it.
    fixture.setList(&table[0], "");

    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);

    // The host is up with nothing on it, which the wall says by showing
    // nothing: no placeholder tile takes the gone session's place.
    try std.testing.expect(!present[0]);
    try std.testing.expectEqual(@as(usize, 0), wv.presentCount(present[0..]));
}

test "planHostDiff: a creating tile whose attach has not landed yet is not vanished by the list that raced it" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    // `Ctrl-\ c` picked "b" off the daemon's free list and the tile is
    // dialling; `pokeHost` asks for the list at exactly this moment, and
    // the daemon answers before the attach that makes "b" lands.
    tiles[1].creates = true;
    var present = [_]bool{ true, true, true };

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);

    // ...and once its session exists, the same list does drop it — after
    // the one list of grace a live pump gets: a session the daemon HAD and
    // no longer lists is gone.
    tiles[1].ever_up.store(true, .release);
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), vanish.len);
    try std.testing.expectEqual(@as(usize, 1), vanish.get(0));
}

test "hostsOverCapacity: a hosts file past the wall's room is counted out loud, not cut in silence" {
    var buf: [64]u8 = undefined;
    // Exactly full is not over: the boundary is where a silent cut starts.
    try std.testing.expectEqual(@as(?[]const u8, null), wall_host.hostsOverCapacity(&buf, wv.max_tiles));
    try std.testing.expectEqual(@as(?[]const u8, null), wall_host.hostsOverCapacity(&buf, 0));
    try std.testing.expectEqualStrings(
        "[+1 host in the file not shown]",
        wall_host.hostsOverCapacity(&buf, wv.max_tiles + 1) orelse "",
    );
    try std.testing.expectEqualStrings(
        "[+9 hosts in the file not shown]",
        wall_host.hostsOverCapacity(&buf, wv.max_tiles + 9) orelse "",
    );
}

test "addHost: a forgotten host's slot comes back, so a and x cannot fill the table between them" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    var path_buf: [256]u8 = undefined;
    const path = try std.fmt.bufPrint(&path_buf, "{s}/hosts", .{tmp.path()});
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();

    // A FULL table is the only one that can see a forgotten slot refused:
    // `hosts_live` only ever grew, so 32 adds spent the wall for good even
    // with two rows left in the picker.
    var table: [2]Host = undefined;
    table[0] = .{ .spec = .{ .spelling = "--sock /a", .target = .{ .sock = "/a" }, .poll_target = .{ .sock = "/a" } }, .shared = &shared };
    table[1] = .{ .spec = .{ .spelling = "--sock /b", .target = .{ .sock = "/b" }, .poll_target = .{ .sock = "/b" } }, .shared = &shared };
    var n: usize = 2;
    var buf: [96]u8 = undefined;
    try std.testing.expect(wall_host.addHost(alloc, &shared, &table, &n, "--sock /c", null, 30_000, path) == .refused);
    try std.testing.expectEqualStrings("[no room on the wall for another host]", wv.takeNotice(&shared, &buf));

    // `x` asks the poller to leave; until it has, the slot is still its own.
    // A spec overwritten under a live poller is a thread dialling a target
    // that has been replaced.
    table[0].forgotten.store(true, .release);
    try std.testing.expect(wall_host.addHost(alloc, &shared, &table, &n, "--sock /c", null, 30_000, path) == .refused);
    try std.testing.expectEqualStrings("[no room on the wall for another host]", wv.takeNotice(&shared, &buf));

    // The poller's last act, and the slot is the wall's again.
    table[0].poller_done.store(true, .release);
    const at = switch (wall_host.addHost(alloc, &shared, &table, &n, "--sock /c", null, 30_000, path)) {
        .added => |i| i,
        else => return error.HostWasNotAdded,
    };
    // `run`'s allocator is an ARENA that never returns, so a reused slot's
    // old spec is deliberately not freed there; a test outlives its own.
    // The target owns its own path: `Target.fromSpec` copies every slice,
    // because the spelling a caller parsed may be a scratch buffer.
    defer alloc.free(table[at].spec.spelling);
    defer alloc.free(table[at].spec.target.sock);
    try std.testing.expectEqual(@as(usize, 0), at);
    try std.testing.expectEqual(@as(usize, 2), n);
    try std.testing.expectEqualStrings("/c", table[0].spec.target.sock);
    try std.testing.expect(!table[0].forgotten.load(.acquire));
    try std.testing.expect(!table[0].poller_done.load(.acquire));
}

test "addHost: the prompt adds a DAEMON — a flag, a session and a host already listed are all refused" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    var path_buf: [256]u8 = undefined;
    const path = try std.fmt.bufPrint(&path_buf, "{s}/hosts", .{tmp.path()});
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    // Two hosts on the wall before the prompt is ever opened: a table that
    // only ever held one cannot see an append landing on the wrong row.
    var table: [4]Host = undefined;
    table[0] = .{ .spec = .{ .spelling = "--sock /a", .target = .{ .sock = "/a" }, .poll_target = .{ .sock = "/a" } }, .shared = &shared };
    table[1] = .{ .spec = .{ .spelling = "--sock /b", .target = .{ .sock = "/b" }, .poll_target = .{ .sock = "/b" } }, .shared = &shared };
    var n: usize = 2;
    var buf: [96]u8 = undefined;

    // The typo `mux hosts add -A host` dies at, typed at the prompt
    // instead: a host named `-A` is nobody's intent at either mouth.
    try std.testing.expect(wall_host.addHost(alloc, &shared, &table, &n, "-A nosuchhost.invalid", null, 30_000, path) == .refused);
    try std.testing.expectEqual(@as(usize, 2), n);
    try std.testing.expectEqualStrings("[bad host: FlagLikeTarget]", wv.takeNotice(&shared, &buf));

    // `#` is the old wall grammar's session split. The prompt names a
    // daemon now, and the refusal says which half was one too many.
    try std.testing.expect(wall_host.addHost(alloc, &shared, &table, &n, "--sock /c#work", null, 30_000, path) == .refused);
    try std.testing.expectEqual(@as(usize, 2), n);
    try std.testing.expect(std.mem.indexOf(u8, wv.takeNotice(&shared, &buf), "names a daemon") != null);

    // A real one: it lands in the table AND in the file, because the wall
    // the user is looking at and the wall they get back are the same wall.
    const at = switch (wall_host.addHost(alloc, &shared, &table, &n, "--sock /c", null, 30_000, path)) {
        .added => |i| i,
        else => return error.HostWasNotAdded,
    };
    // `run`'s allocator never returns, so the table's copy is deliberately
    // never freed there; a test outlives its allocator's bookkeeping.
    defer alloc.free(table[at].spec.spelling);
    defer alloc.free(table[at].spec.target.sock);
    try std.testing.expectEqual(@as(usize, 2), at);
    try std.testing.expectEqual(@as(usize, 3), n);
    try std.testing.expectEqualStrings("/c", table[2].spec.target.sock);
    var h = try hosts.load(alloc, path);
    defer h.deinit(alloc);
    try std.testing.expect(h.has("--sock /c"));

    // Twice is once: the poller is already asking that daemon for its
    // sessions, and a second one would tile every one of them again. It
    // says so and names the row, because an unchanged popup and an unmoved
    // selection is the prompt answering a real host with nothing at all.
    try std.testing.expectEqual(
        AddHost{ .listed = 2 },
        wall_host.addHost(alloc, &shared, &table, &n, "--sock /c", null, 30_000, path),
    );
    try std.testing.expectEqual(@as(usize, 3), n);
    try std.testing.expectEqualStrings("[that host is already on the wall]", wv.takeNotice(&shared, &buf));

    // Full is said out loud, not swallowed.
    n = table.len;
    try std.testing.expect(wall_host.addHost(alloc, &shared, &table, &n, "--sock /d", null, 30_000, path) == .refused);
    try std.testing.expectEqualStrings("[no room on the wall for another host]", wv.takeNotice(&shared, &buf));
}

test "resolveHost: an ssh host is polled by a recipe that cannot prompt and cannot narrate" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();

    // Two spellings, because the poll target is built per host and a shared
    // one would point every stripe at the first host's ssh argv.
    for ([_][]const u8{ "box", "user@gate" }) |spelling| {
        const spec = try client.resolveHost(alloc, spelling, null, 30_000);
        try std.testing.expect(hasWord(spec.poll_target.hand.ssh_argv, "BatchMode=yes"));
        try std.testing.expectEqualStrings(spelling, spec.poll_target.hand.host);
        // The attach the user sees must still be able to ask for a password.
        try std.testing.expect(!hasWord(spec.target.hand.ssh_argv, "BatchMode=yes"));
        // Neither door narrates a fallback: both are dialled under a wall
        // that owns the alternate screen.
        try std.testing.expect(!spec.target.hand.asked);
        try std.testing.expect(!spec.poll_target.hand.asked);
    }

    // A transport that cannot prompt has one target, not a copy of one.
    const s = try client.resolveHost(alloc, "--sock /tmp/a.sock", null, 30_000);
    try std.testing.expectEqualStrings(s.target.sock, s.poll_target.sock);
}

test "otherHosts: the dialled host is not tiled twice, and the rest follow in file order" {
    const alloc = std.testing.allocator;
    var tmp = try TmpDir.make();
    defer tmp.cleanup();
    var buf: [256]u8 = undefined;
    const path = try std.fmt.bufPrint(&buf, "{s}/hosts", .{tmp.path()});
    // Off-origin: the host the user dialled is the file's SECOND line, so a
    // skip that only ever worked on line 0 is caught here.
    try hosts.saveBytes(path, "--sock /a\n--sock /b\n");

    var arena_state = std.heap.ArenaAllocator.init(alloc);
    defer arena_state.deinit();
    const arena = arena_state.allocator();
    var specs: std.ArrayList(HostSpec) = .empty;
    try specs.append(arena, .{ .spelling = "--sock /b", .target = .{ .sock = "/b" }, .poll_target = .{ .sock = "/b" } });
    wall_host.otherHosts(arena, &specs, "--sock /b", path, null, 0);

    try std.testing.expectEqual(@as(usize, 2), specs.items.len);
    try std.testing.expectEqualStrings("--sock /b", specs.items[0].spelling);
    try std.testing.expectEqualStrings("--sock /a", specs.items[1].spelling);

    // A file the grammar refuses costs the user the WALL, never the session
    // they asked for: the entry host stands alone and the attach goes on.
    try hosts.saveBytes(path, "--sock /a\nbox#old\n");
    var one: std.ArrayList(HostSpec) = .empty;
    try one.append(arena, .{ .spelling = "--sock /b", .target = .{ .sock = "/b" }, .poll_target = .{ .sock = "/b" } });
    wall_host.otherHosts(arena, &one, "--sock /b", path, null, 0);
    try std.testing.expectEqual(@as(usize, 1), one.items.len);
}

fn hasWord(argv: []const []const u8, want: []const u8) bool {
    for (argv) |w| if (std.mem.eql(u8, w, want)) return true;
    return false;
}

/// Three pending panes off one sidecar: "a" and "b" on host 0 and an
/// aliasing "a" on host 1, so a bind keyed on name alone is caught.
/// Dupes the names: every wall tile owns its copies, because a birth that
/// takes a freed digit frees the departed tile's with the wall's allocator.
fn seedBench(alloc: std.mem.Allocator, tiles: []Tile, present: []bool, live: *usize, shared: *Shared) !void {
    try shared.tree.addFirst(0);
    try shared.tree.insert(0, 1);
    try shared.tree.insert(1, 2);
    const rs = [_]wall_host.Resolved{
        .{ .target = .{ .sock = "/tmp/h0.sock" }, .label = "--sock /tmp/h0.sock#a", .session = "a" },
        .{ .target = .{ .sock = "/tmp/h0.sock" }, .label = "--sock /tmp/h0.sock#b", .session = "b" },
        .{ .target = .{ .sock = "/tmp/h1.sock" }, .label = "--sock /tmp/h1.sock#a", .session = "a" },
    };
    const hostof = [_]usize{ 0, 0, 1 };
    for (rs, hostof, 0..) |r, h, i| {
        const owned: wall_host.Resolved = .{
            .target = r.target,
            .label = try alloc.dupe(u8, r.label),
            .session = try alloc.dupe(u8, r.session),
        };
        try wv.seedTile(&tiles[i], owned, .{ .top = 0, .left = 0, .rows = 0, .cols = 0 }, shared, i, h);
        present[i] = true;
    }
    live.* = 3;
}

test "planHostDiff: a pending pane the list names is a bind, and only its own host's list may wake 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);
    defer shared.tree.deinit();
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);

    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    // Host 0's whole list is pending panes: two binds, and no pane of
    // host 1's is touched by a name host 0 spelled.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    try std.testing.expectEqual(@as(usize, 2), binds.len);
    try std.testing.expectEqual(@as(usize, 0), binds.get(0));
    try std.testing.expectEqual(@as(usize, 1), binds.get(1));
    // Host 1's "a" is its own pane, not host 0's; its "x" is a session no
    // pane spells, and the wall has nothing to say about it.
    binds.len = 0;
    wall_host.planHostDiff(&tiles, &present, 3, 1, "a\nx\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 1), binds.len);
    try std.testing.expectEqual(@as(usize, 2), binds.get(0));
    try std.testing.expectEqual(@as(usize, 3), live);
}

test "applyHostList: binding a saved pane moves no rect and re-cuts nothing" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    defer shared.tree.deinit();
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"),
        fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"),
    };
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
    // The seed's own cut, then quiet: what the list must not disturb.
    wall_layout.relayout(w, 0);
    for (tiles[0..3]) |*t| t.resize_pending = false;
    const gen = shared.repaint_gen.load(.acquire);
    var rects: [3]@TypeOf(tiles[0].rect) = undefined;
    for (tiles[0..3], 0..) |t, i| rects[i] = t.rect;

    fixture.setList(&table[0], "a\nb\n");
    wall_host.applyHostList(w, 0);

    try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire));
    for (tiles[0..3], 0..) |t, i| {
        try std.testing.expectEqual(rects[i], t.rect);
        try std.testing.expect(!t.resize_pending);
    }
    // Bound panes joined - the daemon already has these sessions, and a
    // sized attach on a live one would resize somebody.
    try std.testing.expect(!tiles[0].pending and !tiles[1].pending);
    try std.testing.expect(!tiles[0].creates and !tiles[1].creates);
    // Host 1's aliasing "a" is still waiting for ITS host.
    try std.testing.expect(tiles[2].pending);
}

test "a silent host's saved panes survive its failed poll and wear unreachable" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    defer shared.tree.deinit();
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"),
        fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"),
    };
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
    // Off-origin rects, so a pane that IS taken shows up as a re-cut.
    wall_layout.relayout(w, 0);
    var rects: [3]@TypeOf(tiles[0].rect) = undefined;
    for (tiles[0..3], 0..) |t, i| rects[i] = t.rect;

    // Host 0 is dark; twice, because a pane that rides out one poll and
    // goes on the next has not ridden anything out.
    table[0].poll.reachable.store(false, .release);
    for (0..2) |_| wall_host.applyHostList(w, 0);

    for (tiles[0..3], present[0..3], 0..) |t, p, i| {
        if (!p) return error.SilentHostTookThePanesTheUserSaved;
        if (!t.pending) return error.SilenceBoundAPaneNoHostNamed;
        if (!std.meta.eql(rects[i], t.rect)) return error.SilenceRecutTheWall;
    }
    // Only host 0's panes are dressed: a quiet box says nothing about
    // anyone else's, and the word is the picker row's own.
    if (tiles[0].state != .@"unreachable" or tiles[1].state != .@"unreachable")
        return error.DarkHostsPanesDoNotSayWhichSetupIsDark;
    if (tiles[2].state != .waiting) return error.OneHostsSilenceSpreadToAnother;
    try std.testing.expectEqualStrings("unreachable", tiles[0].state.word());
}

test "bindTile: the pane under the saved focus claims the terminal when it binds, its neighbour does 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);
    defer shared.tree.deinit();
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"),
        fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"),
    };
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
    // The saved focus record put the user on pane 1 before any host spoke.
    shared.sel = 1;
    fixture.setList(&table[0], "a\nb\n");
    wall_host.applyHostList(w, 0);
    try std.testing.expect(tiles[1].claim_pending.load(.acquire));
    try std.testing.expect(!tiles[0].claim_pending.load(.acquire));
}

test "applyHostList: one list binds a pane, dresses the pane it disowns gone, and adds nothing for the name it never saw" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    defer shared.tree.deinit();
    var tiles: [8]Tile = undefined;
    var present = [_]bool{false} ** 8;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/h0.sock", "/tmp/h0.sock"),
        fixture.testHost(&shared, "--sock /tmp/h1.sock", "/tmp/h1.sock"),
    };
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
    // The seed's own cut, then quiet: what this list must not disturb.
    wall_layout.relayout(w, 0);
    const gen = shared.repaint_gen.load(.acquire);
    var rects: [3]@TypeOf(tiles[0].rect) = undefined;
    for (tiles[0..3], 0..) |t, i| rects[i] = t.rect;

    // One list, all three answers in it: "a" binds, "b" is disowned, and
    // "c" is a session of this daemon's that no pane spells.
    fixture.setList(&table[0], "a\nc\n");
    wall_host.applyHostList(w, 0);

    try std.testing.expect(!tiles[0].pending);
    // The disowned pane keeps its digit and its rect: the host said no, and
    // a pane the user authored is not the daemon's to collapse.
    try std.testing.expect(present[1] and tiles[1].pending);
    try std.testing.expectEqual(wv.State.gone, tiles[1].state);
    // "c" is nobody's business: the wall the user wrote down is three panes
    // wide before this list and three after it.
    try std.testing.expectEqual(@as(usize, 3), live);
    try std.testing.expectEqual(@as(usize, 3), wv.presentCount(present[0..live]));
    // Nothing was re-cut: a bind wakes a pane in place and a gone pane is
    // dressed where it stands, so no rect moved and no repaint was asked for.
    try std.testing.expectEqual(gen, shared.repaint_gen.load(.acquire));
    for (tiles[0..3], 0..) |t, i| try std.testing.expectEqual(rects[i], t.rect);
}

test "sendKeys: the mailbox of a pending pane holds what was typed for the pump the bind will start" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);
    defer shared.tree.deinit();
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    try seedBench(alloc, &tiles, &present, &live, &shared);
    defer fixture.endPumps(tiles[0..live]);

    wv.sendKeys(&tiles[1], "typed-early");
    var buf: [64]u8 = undefined;
    try std.testing.expectEqualStrings("typed-early", wall_pump.takeKeys(&tiles[1], &buf));
}

test "applyHostList: a reachable host that lost its sessions dresses seeded panes gone and keeps them standing" {
    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]);
    var table = [_]Host{fixture.testHost(&shared, "box", "/tmp/box.sock")};

    // Two seeded panes, off-origin, distinct rects: the saved cut of a
    // two-pane wall whose daemon rebooted overnight.
    try wv.seedTile(&tiles[0], .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 0, 0);
    try wv.seedTile(&tiles[1], .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#b", .session = "b" }, .{ .top = 1, .left = 41, .rows = 23, .cols = 39 }, &shared, 1, 0);
    present[0] = true;
    present[1] = true;
    live = 2;

    // The host is REACHABLE and answers empty: both panes dress gone, both
    // stay present — the wall re-cuts nothing.
    fixture.setList(&table[0], "");
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    try std.testing.expectEqual(wv.State.gone, tiles[0].state);
    try std.testing.expectEqual(wv.State.gone, tiles[1].state);
    try std.testing.expectEqual(@as(usize, 2), wv.presentCount(present[0..live]));

    // The poll FAILS next: gone yields to unreachable (the host cannot
    // answer, which is a different sentence than "the host said no")...
    table[0].poll.reachable.store(false, .release);
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    try std.testing.expectEqual(wv.State.@"unreachable", tiles[0].state);

    // ...and recovery without the session dresses gone again. Flap over,
    // both panes still standing.
    fixture.setList(&table[0], "");
    wall_host.applyHostList(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table), 0);
    try std.testing.expectEqual(wv.State.gone, tiles[0].state);
    try std.testing.expectEqual(wv.State.gone, tiles[1].state);
    try std.testing.expectEqual(@as(usize, 2), wv.presentCount(present[0..live]));
}

test "setFocus: landing on a gone pane says what the two keys are" {
    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]);
    try wv.seedTile(&tiles[0], .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 0, 0);
    try wv.seedTile(&tiles[1], .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#b", .session = "b" }, .{ .top = 1, .left = 41, .rows = 23, .cols = 39 }, &shared, 1, 0);
    present[0] = true;
    present[1] = true;
    live = 2;
    tiles[1].state = .gone;

    wv.setFocus(tiles[0..live], &shared, 1);
    try std.testing.expectEqualStrings(wv.gone_notice, shared.notice[0..shared.notice_len]);
}

test "setFocus: landing on an unreachable pane says it is retrying and Enter retries" {
    // The pane the wall used to be SILENT about: no notice on focus, so the
    // user pressed Enter (the gone reflex) into nothing. It now says the
    // host is retrying and that Enter hurries it, the mirror of the gone
    // notice one state over.
    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]);
    try wv.seedTile(&tiles[0], .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 0, 0);
    try wv.seedTile(&tiles[1], .{ .target = .{ .sock = "/tmp/dark.sock" }, .label = "dark#b", .session = "b" }, .{ .top = 1, .left = 41, .rows = 23, .cols = 39 }, &shared, 1, 1);
    present[0] = true;
    present[1] = true;
    live = 2;
    tiles[1].state = .@"unreachable";

    wv.setFocus(tiles[0..live], &shared, 1);
    try std.testing.expectEqualStrings(wv.unreachable_notice, shared.notice[0..shared.notice_len]);
}

test "armPane: the three wakes differ only in create and start" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    const alloc = arena.allocator();
    var shared: Shared = undefined;
    fixture.stoppedWall(alloc, &shared);

    // A gone pane on a HAND host, so `asked` has somewhere to land. Gone
    // means the host is up, so Enter re-creates ({create, no start}) and
    // does so exactly where the pane stood.
    var gone: Tile = undefined;
    const hand = client.Target{ .hand = .{ .host = "box", .ssh_argv = &.{"ssh"}, .cache_path = null } };
    try wv.seedTile(&gone, .{ .target = hand, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 41, .rows = 23, .cols = 39 }, &shared, 0, 0);
    gone.state = .gone;
    wv.armPane(&gone, .{ .creates = true, .asked = false });
    try std.testing.expect(!gone.pending);
    try std.testing.expect(gone.creates);
    try std.testing.expect(!gone.r.target.hand.asked);
    try std.testing.expectEqual(wv.State.connecting, gone.state);
    try std.testing.expect(gone.alive.load(.acquire));
    try std.testing.expect(!gone.pump_done.load(.acquire));
    try std.testing.expectEqual(@as(u16, 41), gone.rect.left);
    try std.testing.expectEqualStrings("a", gone.r.session);

    // An unreachable pane on the same kind of host: its daemon may be dark,
    // so Enter STARTS it first ({create, start}) — the one field that
    // differs from the gone wake.
    var unreach: Tile = undefined;
    const hand2 = client.Target{ .hand = .{ .host = "box", .ssh_argv = &.{"ssh"}, .cache_path = null } };
    try wv.seedTile(&unreach, .{ .target = hand2, .label = "box#b", .session = "b" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 1, 0);
    unreach.state = .@"unreachable";
    wv.armPane(&unreach, .{ .creates = true, .asked = true });
    try std.testing.expect(unreach.creates);
    try std.testing.expect(unreach.r.target.hand.asked);

    // A poll bind: attach only, neither create nor start.
    var bind: Tile = undefined;
    const hand3 = client.Target{ .hand = .{ .host = "box", .ssh_argv = &.{"ssh"}, .cache_path = null } };
    try wv.seedTile(&bind, .{ .target = hand3, .label = "box#c", .session = "c" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 2, 0);
    bind.state = .waiting;
    wv.armPane(&bind, .{ .creates = false, .asked = false });
    try std.testing.expect(!bind.creates);
    try std.testing.expect(!bind.r.target.hand.asked);
}

test "keysToFocused: a gone pane eats every byte except Enter, which revives without starting" {
    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: [2]Tile = undefined;
    // The pump the Enter below spawns holds `*Tile` into this frame, and
    // `shared` is this frame too: the test may not return until that thread
    // has stored `alive = false`, or the next test's stack is what it reads.
    defer fixture.endPumps(tiles[0..1]);
    const hand = client.Target{ .hand = .{ .host = "box", .ssh_argv = &.{"ssh"}, .cache_path = null } };
    try wv.seedTile(&tiles[0], .{ .target = hand, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 0, 0);
    tiles[0].state = .gone;

    // Ordinary typing reaches no session and queues nowhere.
    wv.keysToFocused(&tiles[0], "ls -la");
    try std.testing.expectEqual(@as(usize, 0), tiles[0].in_len);
    try std.testing.expect(tiles[0].pending);

    // Enter revives, and does NOT start the daemon — a gone host is up.
    // (`running` is false in this fixture, so the spawned pump exits at its
    // gate; the transitions are armPane's, pinned above.)
    wv.keysToFocused(&tiles[0], "\r");
    try std.testing.expect(!tiles[0].pending);
    try std.testing.expect(!tiles[0].r.target.hand.asked);
    tiles[0].pump_done.store(true, .release);
}

test "keysToFocused: Enter on an unreachable pane revives it AND starts the daemon" {
    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: [2]Tile = undefined;
    defer fixture.endPumps(tiles[0..1]);
    // A dark host's saved pane. The old behaviour re-polled and could never
    // start a rebooted box's daemon; Enter now wakes a pump that dials
    // `--start`, the one thing that boots it.
    const hand = client.Target{ .hand = .{ .host = "box", .ssh_argv = &.{"ssh"}, .cache_path = null } };
    try wv.seedTile(&tiles[0], .{ .target = hand, .label = "box#a", .session = "a" }, .{ .top = 1, .left = 0, .rows = 23, .cols = 40 }, &shared, 0, 0);
    tiles[0].state = .@"unreachable";

    // Ordinary typing is not the verb: the pane stays a pending drawing.
    wv.keysToFocused(&tiles[0], "ls -la");
    try std.testing.expect(tiles[0].pending);
    try std.testing.expect(!tiles[0].r.target.hand.asked);

    // Enter wakes it with the start armed.
    wv.keysToFocused(&tiles[0], "\r");
    try std.testing.expect(!tiles[0].pending);
    try std.testing.expect(tiles[0].creates);
    try std.testing.expect(tiles[0].r.target.hand.asked);
    tiles[0].pump_done.store(true, .release);
}

test "planHostDiff: a session the daemon has and the wall does not is nobody's business: no birth, no tile" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    var present = [_]bool{ true, true, true };
    var binds = TileIdxs{};
    var vanish = TileIdxs{};
    var gones = TileIdxs{};
    // Host 0 answers with its two panes' sessions and three the wall never
    // asked for. Twice, so the grace has been spent and a vanish would show.
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones);
    wall_host.planHostDiff(&tiles, &present, 3, 0, "a\nb\nx\ny\nz\n", null, &binds, &vanish, &gones);
    try std.testing.expectEqual(@as(usize, 0), vanish.len);
    try std.testing.expectEqual(@as(usize, 0), gones.len);
    try std.testing.expectEqual(@as(usize, 3), wv.presentCount(&present));
}

test "applyHostList: a pane the daemon stopped listing leaves the layout file too" {
    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 tmp = try TmpDir.make();
    defer tmp.cleanup();
    var path_buf: [std.fs.max_path_bytes]u8 = undefined;
    const path = try std.fmt.bufPrint(&path_buf, "{s}/layout", .{tmp.path()});
    shared.layout_path = path;
    var tiles: [4]Tile = undefined;
    var present = [_]bool{false} ** 4;
    var live: usize = 0;
    // Two panes on two HOSTS: the survivor has to be a pane the vanishing
    // one's host does not own, or a save that wrote the wrong slice would
    // still look right.
    var table = [_]Host{
        fixture.testHost(&shared, "--sock /tmp/box.sock", "/tmp/box.sock"),
        fixture.testHost(&shared, "--sock /tmp/vm.sock", "/tmp/vm.sock"),
    };
    try seatBound(&tiles, &present, &live, &shared, &.{
        .{ .target = .{ .sock = "/tmp/box.sock" }, .label = "--sock /tmp/box.sock#a", .session = "a" },
        .{ .target = .{ .sock = "/tmp/vm.sock" }, .label = "--sock /tmp/vm.sock#b", .session = "b" },
    }, &.{ 0, 1 });
    tiles[0].alive.store(true, .release);
    tiles[1].alive.store(true, .release);
    const w = fixture.wallOf(alloc, &tiles, &present, &live, &shared, &table);
    // The wall as seated, on disk: what the poll is about to change.
    wall_layout.persist(w);
    const seated = try std.fs.cwd().readFileAlloc(alloc, path, 4096);
    try std.testing.expect(std.mem.indexOf(u8, seated, "--sock /tmp/box.sock#a") != null);

    // Twice: the first list that loses a session is a grace, the second
    // vanishes the pane.
    fixture.setList(&table[0], "");
    wall_host.applyHostList(w, 0);
    wall_host.applyHostList(w, 0);

    try std.testing.expect(!present[0]);
    const after = try std.fs.cwd().readFileAlloc(alloc, path, 4096);
    try std.testing.expect(std.mem.indexOf(u8, after, "--sock /tmp/box.sock#a") == null);
    // The other host's pane is still the wall's, and still in the file.
    try std.testing.expect(std.mem.indexOf(u8, after, "--sock /tmp/vm.sock#b") != null);
}