a73x

src/tui/wall_test_wall.zig

Ref:   Size: 60.7 KiB   History

//! The wall itself: tiles, focus, births, endings and label bars.
const std = @import("std");
const proto = @import("term").protocol;
const client = @import("client");
const interact = @import("interact.zig");
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 EndAction = wv.EndAction;
const EndReason = wv.EndReason;
const Resolved = wall_host.Resolved;
const Shared = wv.Shared;
const State = wv.State;
const Tile = wv.Tile;

test "birthTile: a chord-born tile creates and offers no agent" {
    const alloc = std.testing.allocator;
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    defer shared.tree.deinit();
    try shared.tree.addFirst(0);
    var tiles: [2]Tile = undefined;
    tiles[0] = .{
        .r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    var present = [_]bool{ true, false };
    var live: usize = 1;
    const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
    const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
        .r = r,
        .from = 0,
        .place = .beside_focus,
        .creates = true,
        .born_from = 0,
    }) orelse return error.TestUnexpectedResult;
    defer std.posix.close(tiles[at].wake_r);
    defer std.posix.close(tiles[at].wake_w);
    try std.testing.expectEqual(@as(usize, 1), at);
    try std.testing.expect(tiles[at].creates);
    try std.testing.expect(!tiles[at].r.agent);
    try std.testing.expectEqual(@as(?usize, 0), tiles[at].born_from);
    try std.testing.expect(present[1]);
    try std.testing.expectEqual(@as(usize, 2), live);
    // A tty wall draws a bar — derived from `is_tty`, so a birth cannot
    // forget it and viewRows is right from the first attach.
    try std.testing.expectEqual(@as(u16, 1), shared.labelRows());
}

test "the empty wall advertises the key the picker really answers to" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    const hint = wv.emptyWallHint(&shared);
    // The sentence names a chord; this is what stops it outliving the
    // binding. `Ctrl-\ s` in the text, `\x1c s` through the filter.
    try std.testing.expect(std.mem.indexOf(u8, hint, "Ctrl-\\ s") != null);
    var f: interact.PrefixFilter = .{};
    var keys = "\x1cs".*;
    try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
}

test "the empty-wall notice names the picker key" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    // The refusal a birth chord earns when the focused tile has no pump
    // left. A SENTENCE, not code: a mechanical rename over `live` rewrote
    // this line and three like it, and every gate stayed green because
    // nothing read a notice back.
    wv.setNotice(&shared, wv.no_live_here);
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        "[no live session here - Ctrl-\\ s picks a host]",
        wv.takeNotice(&shared, &buf),
    );
    // And the way out it names is a chord the filter really answers to.
    var f: interact.PrefixFilter = .{};
    var keys = "\x1cs".*;
    try std.testing.expectEqual(interact.PrefixFilter.Action.pick_open, f.feed(&keys).action);
}

test "emptyWallHint: a chord an empty wall refuses is said there, and said once" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    const way_out = "Ctrl-\\ s to pick a host - Ctrl-\\ d to leave";
    try std.testing.expectEqualStrings(way_out, wv.emptyWallHint(&shared));

    wv.setNotice(&shared, "[no session to birth beside - Ctrl-\\ s picks a host]");
    shared.paint_mu.lock();
    defer shared.paint_mu.unlock();
    // An empty wall has no pump, and a notice is painted by the pump that
    // claims the terminal — so this line is the only place it can be said.
    try std.testing.expectEqualStrings(
        "[no session to birth beside - Ctrl-\\ s picks a host]",
        wv.emptyWallHint(&shared),
    );
    // Once: every relayout repaints the empty line, and a sentence that
    // outlived the keystroke that earned it would never leave the screen.
    try std.testing.expectEqualStrings(way_out, wv.emptyWallHint(&shared));
}

test "awaitDetach: a pump that has already returned is not waited on" {
    var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
    defer arena.deinit();
    var shared: Shared = undefined;
    fixture.stoppedWall(arena.allocator(), &shared);
    var t: Tile = .{
        .r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "0" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
        .alive = std.atomic.Value(bool).init(false),
    };
    // The pump is the only thread that sets `detach_ack`, and this one has
    // returned: the wait can only ever run out its 400ms bound.
    const began = std.time.milliTimestamp();
    wv.awaitDetach(&t, &shared);
    try std.testing.expect(std.time.milliTimestamp() - began < 100);
}

test "setNoticeIdle: a standing condition waits — a refusal the user just earned is not overwritten" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var buf: [96]u8 = undefined;

    // The poll re-derives `[+N not shown]` every second per host, and the
    // slot is one: the earned sentence has to survive to a paint.
    wv.setNotice(&shared, "[bad host: FlagLikeTarget]");
    wv.setNoticeIdle(&shared, "[+2 not shown]");
    try std.testing.expectEqualStrings("[bad host: FlagLikeTarget]", wv.takeNotice(&shared, &buf));

    // Read, and the standing condition takes the slot it was waiting for.
    wv.setNoticeIdle(&shared, "[+2 not shown]");
    try std.testing.expectEqualStrings("[+2 not shown]", wv.takeNotice(&shared, &buf));
}

test "X on a tile whose daemon refused arms a second press, and the window closes after 3 s" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    var t: Tile = .{
        .r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "work" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    // A first `X` is never a force: the daemon is the one that knows who
    // else is attached, so the client asks before it insists.
    try std.testing.expectEqual(client.SwitchIntent.end, wv.intentForEnd(&t, 1000));
    wv.onEndReply(&t, .{ .accepted = false, .others = 2, .reason = "others attached" }, 1000);
    try std.testing.expectEqual(@as(i64, 1000 + wv.end_arm_ms), t.end_armed_until.load(.acquire));
    try std.testing.expectEqual(client.SwitchIntent.end_force, wv.intentForEnd(&t, 2500));

    // The window SHUTS. Without this an `X` typed minutes later, on a
    // session that has picked up watchers since, kills them without asking.
    try std.testing.expectEqual(client.SwitchIntent.end, wv.intentForEnd(&t, 4001));

    // An accepted end disarms rather than leaving the window standing: the
    // reply for the tile that is going must not force the next one.
    t.end_armed_until.store(9000, .release);
    wv.onEndReply(&t, .{ .accepted = true, .others = 0, .reason = "" }, 5000);
    try std.testing.expectEqual(@as(i64, 0), t.end_armed_until.load(.acquire));
}

test "n/p walk the wall's tiles: a hole is stepped over, and a wall of one has nowhere to go" {
    // Three slots with the middle one vanished, because the walk `n` used
    // to do was one daemon's session ring and the wall is every host's
    // sessions — a fixture of two adjacent tiles cannot see the hole.
    const three = [_]bool{ true, false, true };
    try std.testing.expectEqual(@as(?usize, 2), wv.walkTiles(&three, 0, true));
    try std.testing.expectEqual(@as(?usize, 0), wv.walkTiles(&three, 2, true));
    try std.testing.expectEqual(@as(?usize, 2), wv.walkTiles(&three, 0, false));
    try std.testing.expectEqual(@as(?usize, 0), wv.walkTiles(&three, 2, false));

    // One tile is its own neighbour, which is not a move: a focus that
    // "moves" to itself releases the terminal and re-claims it for nothing.
    const one = [_]bool{ false, true, false };
    try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&one, 1, true));
    try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&one, 1, false));

    const none = [_]bool{ false, false };
    try std.testing.expectEqual(@as(?usize, null), wv.walkTiles(&none, 0, true));
}

test "birthTile: no room is null and the tree is left as it was" {
    const alloc = std.testing.allocator;
    // Four rows cannot hold two stacked tiles under a bar: `wallFloors`
    // asks min_session_rows + 1 of each on a tty. The leaf count pins the
    // undo — a birth that refused and kept its insert would leave the tree
    // one leaf wider than the wall.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 4 }, .is_tty = true };
    defer shared.tree.deinit();
    try shared.tree.addFirst(0);
    var tiles: [2]Tile = undefined;
    tiles[0] = .{
        .r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
        .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    var present = [_]bool{ true, false };
    var live: usize = 1;
    const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
    try std.testing.expectEqual(@as(?usize, null), wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
        .r = r,
        .from = 0,
        .place = .beside_focus,
        .creates = true,
        .born_from = 0,
    }));
    try std.testing.expectEqual(@as(usize, 1), live);
    try std.testing.expect(!present[1]);
    try std.testing.expectEqual(@as(usize, 1), shared.tree.count());
}

test "birthTile: a beside wall admits more panes than rows/3" {
    const alloc = std.testing.allocator;
    // Nine panes side by side on 24x200: every one keeps all 24 rows and
    // 8 rails plus 9 columns of 21 fit inside 200. A rows/n admission test
    // refuses this at the 9th (24/9 = 2), capping ANY terminal at rows/3
    // panes however wide it is. The tree owns the answer.
    const n = 9;
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    try shared.tree.addFirst(0);
    var tiles: [n]Tile = undefined;
    var present = [_]bool{false} ** n;
    var live: usize = 0;
    var i: u8 = 0;
    while (i < n - 1) : (i += 1) {
        if (i > 0) {
            try shared.tree.insert(i - 1, i);
            // `insert` wraps a bare root leaf in `.stacked`; a wide
            // terminal's wall is cut the other way (`rootOrient`).
            if (i == 1) shared.tree.setRootOrient(.beside);
        }
        tiles[i] = .{
            .r = .{ .target = .{ .sock = "/tmp/a" }, .label = "--sock /tmp/a", .session = "" },
            .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 21 },
            .shared = &shared,
            .idx = i,
            .wake_r = -1,
            .wake_w = -1,
        };
        present[i] = true;
        live += 1;
    }
    const r: Resolved = .{ .target = .{ .sock = "/tmp/b" }, .label = "--sock /tmp/b#b", .session = "b" };
    const at = wv.birthTile(fixture.wallOf(alloc, &tiles, &present, &live, &shared, &.{}), .{
        .r = r,
        .from = n - 2,
        .place = .beside_focus,
        .creates = true,
        .born_from = n - 2,
        // Borrowed, so the tile gets copies of its own: these are literals,
        // and `Birth.borrowed = false` promises the slot's next reuse two
        // slices it may free.
        .borrowed = true,
    }) orelse return error.TestUnexpectedResult;
    defer std.posix.close(tiles[at].wake_r);
    defer std.posix.close(tiles[at].wake_w);
    defer alloc.free(tiles[at].r.session);
    defer alloc.free(tiles[at].r.label);
    try std.testing.expectEqual(@as(usize, n), shared.tree.count());
    // Admitted AND habitable: every pane keeps the full height and clears
    // the column floor, which is what makes the refusal wrong.
    const flat = try shared.tree.flatten(alloc, 24, 200, wall_layout.wallFloors(true), null);
    defer flat.deinit(alloc);
    try std.testing.expectEqual(@as(usize, n), flat.placed.len);
    for (flat.placed) |p| {
        try std.testing.expectEqual(@as(u16, 24), p.rect.rows);
        try std.testing.expect(p.rect.cols >= proto.min_session_cols);
    }
}

test "birthTile: a vanished digit is taken back, and not before its pump returned" {
    const alloc = std.testing.allocator;
    // Five slots on a wall wide enough for all of them: the claim is which
    // digit a birth lands on, so no refusal may decide it instead.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    try shared.tree.addFirst(0);
    var tiles: [5]Tile = undefined;
    var present = [_]bool{ true, false, false, false, false };
    var live: usize = 1;
    const target: client.Target = .{ .sock = "/tmp/a" };
    tiles[0] = .{
        .r = .{ .target = target, .label = "--sock /tmp/a#0", .session = "0" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 200 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    // Every other tile is born the way every live birth road spells it —
    // `Birth.borrowed`, so the tile takes its own copies — and those are the
    // copies a reuse has to free.
    defer for (tiles[1..], present[1..]) |*t, p| {
        if (!p) continue;
        alloc.free(t.r.session);
        alloc.free(t.r.label);
        std.posix.close(t.wake_r);
        std.posix.close(t.wake_w);
    };
    const born = struct {
        fn at(a: std.mem.Allocator, ts: []Tile, ps: []bool, lv: *usize, sh: *Shared, tg: client.Target, name: []const u8, host: ?usize) ?usize {
            return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
                .r = .{ .target = tg, .label = "", .session = name },
                .from = 0,
                .place = .beside_focus,
                .creates = true,
                .born_from = 0,
                .host = host,
                .borrowed = true,
            });
        }
    }.at;

    try std.testing.expectEqual(@as(?usize, 1), born(alloc, &tiles, &present, &live, &shared, target, "1", 3));
    try std.testing.expectEqual(@as(?usize, 2), born(alloc, &tiles, &present, &live, &shared, target, "2", 3));

    // The middle tile leaves, carrying a full set of dirt: every one of
    // these is a field whose stale value would be a lie about the NEXT
    // tile in the slot — a session that was never up, a list that never
    // missed it, an `X` nobody pressed, a host that does not own it.
    tiles[1].ever_up.store(true, .release);
    tiles[1].missed_once = true;
    tiles[1].end_armed_until.store(9000, .release);
    const doorbell = tiles[1].wake_r;
    wv.vanishTile(&tiles, &present, &shared, 1, null);

    // Its pump has not returned, so the digit is not free yet: the birth
    // appends rather than hand a live thread's `*Tile` to a new tile.
    try std.testing.expectEqual(@as(?usize, 3), born(alloc, &tiles, &present, &live, &shared, target, "3", 3));

    tiles[1].pump_done.store(true, .release);
    try std.testing.expectEqual(@as(?usize, 1), born(alloc, &tiles, &present, &live, &shared, target, "1", null));
    // Reuse, not growth: the wall is four tiles wide, not five.
    try std.testing.expectEqual(@as(usize, 4), live);
    try std.testing.expectEqual(@as(usize, 1), tiles[1].idx);
    try std.testing.expect(!tiles[1].ever_up.load(.acquire));
    try std.testing.expect(!tiles[1].missed_once);
    try std.testing.expectEqual(@as(i64, 0), tiles[1].end_armed_until.load(.acquire));
    try std.testing.expectEqual(@as(?usize, null), tiles[1].host);
    try std.testing.expect(!tiles[1].pump_done.load(.acquire));
    // The doorbell is the slot's, not the tile's: a fresh pipe per reuse
    // would leak the fd pair the old one is never closed on.
    try std.testing.expectEqual(doorbell, tiles[1].wake_r);
}

test "birthTile: the digit's next tile frees the copies an unborrowed birth handed over" {
    const alloc = std.testing.allocator;
    // `Birth.borrowed = false` hands the tile two slices outright, and the
    // birth that takes its digit back is what frees them. The testing
    // allocator is the whole oracle: a slice it never handed out aborts the
    // free, and one nobody frees is reported leaked — which is what a
    // caller passing a literal and a reuse that kept the old copies each
    // look like.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 200, .rows = 24 }, .is_tty = false };
    defer shared.tree.deinit();
    try shared.tree.addFirst(0);
    var tiles: [3]Tile = undefined;
    var present = [_]bool{ true, false, false };
    var live: usize = 1;
    const target: client.Target = .{ .sock = "/tmp/a" };
    tiles[0] = .{
        .r = .{ .target = target, .label = "--sock /tmp/a#0", .session = "0" },
        .rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 200 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    // The chord's road, verbatim: the caller allocates, `birthTile` takes
    // the slices as they are.
    const handed = struct {
        fn birth(
            a: std.mem.Allocator,
            ts: []Tile,
            ps: []bool,
            lv: *usize,
            sh: *Shared,
            tg: client.Target,
            name: []const u8,
        ) ?usize {
            const session = a.dupe(u8, name) catch return null;
            const label = wv.tileLabel(a, tg, session) catch {
                a.free(session);
                return null;
            };
            return wv.birthTile(fixture.wallOf(a, ts, ps, lv, sh, &.{}), .{
                .r = .{ .target = tg, .label = label, .session = session },
                .from = 0,
                .place = .beside_focus,
                .creates = true,
                .born_from = 0,
            });
        }
    }.birth;

    const first = handed(alloc, &tiles, &present, &live, &shared, target, "1") orelse
        return error.TestUnexpectedResult;
    try std.testing.expectEqual(@as(usize, 1), first);
    defer if (present[1]) {
        alloc.free(tiles[1].r.session);
        alloc.free(tiles[1].r.label);
        std.posix.close(tiles[1].wake_r);
        std.posix.close(tiles[1].wake_w);
    };

    wv.vanishTile(&tiles, &present, &shared, 1, null);
    tiles[1].pump_done.store(true, .release);
    // The same digit, so the free under test is one this birth really did.
    try std.testing.expectEqual(
        @as(?usize, 1),
        handed(alloc, &tiles, &present, &live, &shared, target, "2"),
    );
}

test "labelText: the state word survives truncation at every width" {
    var buf: [256]u8 = undefined;
    // A label longer than any bar, so truncation is what is under test and
    // not the label's own length.
    const long = "x" ** 400;

    // The markers are the ones a bar actually carries: three bytes with the
    // chord digit, four for tiles 10 and up. Every one of them comes out of
    // the label's cut, so the marker width is part of what this pins — a
    // two-byte marker exercises no bar that ships.

    // Narrow: the label is cut, the bar fits the terminal, the state stays.
    const narrow = wv.labelText(&buf, 30, "1> ", long, "up");
    if (!std.mem.endsWith(u8, narrow, " [up]")) return error.OneDigitNarrowBarLostTheStateWord;
    if (narrow.len > 30) return error.OneDigitNarrowBarOverranTheTerminal;

    // The same width with the widest marker: the two extra bytes come off
    // the label, never off the state.
    const narrow2 = wv.labelText(&buf, 30, "12  ", long, "up");
    if (!std.mem.endsWith(u8, narrow2, " [up]")) return error.TwoDigitNarrowBarLostTheStateWord;
    if (narrow2.len > 30) return error.TwoDigitNarrowBarOverranTheTerminal;

    // Wide: `cols` alone would have asked for more than `buf` holds, and the
    // failed bufPrint used to hand back the buffer's undefined bytes — with
    // the state word among what was lost.
    const wide = wv.labelText(&buf, 300, "12> ", long, "reconnecting");
    if (!std.mem.endsWith(u8, wide, " [reconnecting]")) return error.TwoDigitWideBarLostTheStateWord;
    if (wide.len > buf.len) return error.TwoDigitWideBarOverranItsBuffer;

    // Wider than `buf` with the longest status word a bar can carry: the
    // bound that matters is `buf`'s, and it is not `cols`'.
    const widest = wv.labelText(&buf, 400, "12  ", long, State.reconnecting.word());
    if (!std.mem.endsWith(u8, widest, " [reconnecting]")) return error.TwoDigitWidestBarLostTheStateWord;
    if (widest.len > buf.len) return error.TwoDigitWidestBarOverranItsBuffer;
}

test "showsSelf: neither the wall nor the CLI attach opens the session the shell is standing in" {
    const sock = "/run/user/1000/muxd.sock";
    const target: client.Target = .{ .sock = sock };

    // The case: `mux --session 1` typed in session 0's shell. Session 0 is
    // on the host's own list, and tiling it would paint that session's
    // stripe into the grid it is reading.
    try std.testing.expect(wv.showsSelf(target, "0", sock, "0"));
    // The empty name is the wire's spelling of the default, and the daemon
    // plants the resolved one.
    try std.testing.expect(wv.showsSelf(target, "", sock, "0"));
    try std.testing.expect(wv.showsSelf(target, "work", sock, "work"));

    // Every other session of the same daemon is exactly what the wall is
    // for — it must not drop those.
    try std.testing.expect(!wv.showsSelf(target, "1", sock, "0"));
    try std.testing.expect(!wv.showsSelf(target, "", sock, "1"));
    // A different daemon's socket is a different session whatever it is
    // called.
    try std.testing.expect(!wv.showsSelf(target, "0", "/run/user/1000/other.sock", "0"));
    // Not a unix socket: a quic:// or --via target cannot be the daemon
    // whose shell planted these.
    try std.testing.expect(!wv.showsSelf(.{ .via = "ssh host mux d proxy" }, "0", sock, "0"));

    // Unset, and the emptied spelling of unset that the self-attach
    // refusal names as the way to override it.
    try std.testing.expect(!wv.showsSelf(target, "0", null, "0"));
    try std.testing.expect(!wv.showsSelf(target, "0", sock, null));
    try std.testing.expect(!wv.showsSelf(target, "0", "", "0"));
    try std.testing.expect(!wv.showsSelf(target, "0", sock, ""));
}

test "sendKeys: a chunk that does not fit is dropped whole, and says so" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var t = Tile{
        .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
        .rect = .{ .top = 0, .left = 0, .rows = 4, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        // -1 both ends: `ring` writes to the doorbell and ignores the
        // failure, which is exactly what it promises to do.
        .wake_r = -1,
        .wake_w = -1,
    };

    const head = "abc";
    wv.sendKeys(&t, head);
    try std.testing.expectEqual(@as(usize, 3), t.in_len);
    try std.testing.expect(!t.in_dropped.load(.acquire));

    // One byte more than the room left. The old code copied what fit.
    const too_big = [_]u8{'z'} ** (wv.mailbox_max - 2);
    wv.sendKeys(&t, &too_big);
    try std.testing.expectEqual(@as(usize, 3), t.in_len);
    try std.testing.expect(t.in_dropped.load(.acquire));

    // ...and the mailbox still holds exactly what was typed before it, so
    // the next chunk cannot splice onto a half-delivered one.
    wv.sendKeys(&t, "def");
    var out: [wv.mailbox_max]u8 = undefined;
    try std.testing.expectEqualStrings("abcdef", wall_pump.takeKeys(&t, &out));

    // Exactly filling it is not overflow — the bound is `>`, not `>=`.
    const exact = [_]u8{'q'} ** wv.mailbox_max;
    t.in_dropped.store(false, .release);
    wv.sendKeys(&t, &exact);
    try std.testing.expectEqual(@as(usize, wv.mailbox_max), t.in_len);
    try std.testing.expect(!t.in_dropped.load(.acquire));
}

test "driftWord: says only what differs — version, staleness, both, or nothing" {
    var buf: [wv.drift_max]u8 = undefined;
    const same: proto.SessionsMeta = .{ .version = "1.0", .stale = false };
    const old: proto.SessionsMeta = .{ .version = "0.9", .stale = false };
    const same_stale: proto.SessionsMeta = .{ .version = "1.0", .stale = true };
    const old_stale: proto.SessionsMeta = .{ .version = "0.9", .stale = true };

    // The common case is silence: a daemon of our own version on its own
    // living image, and a daemon too old to say anything at all.
    try std.testing.expectEqualStrings("", wv.driftWord(&buf, "1.0", same));
    try std.testing.expectEqualStrings("", wv.driftWord(&buf, "1.0", null));

    try std.testing.expectEqualStrings("daemon 0.9", wv.driftWord(&buf, "1.0", old));
    try std.testing.expectEqualStrings("daemon stale", wv.driftWord(&buf, "1.0", same_stale));
    try std.testing.expectEqualStrings("daemon 0.9 stale", wv.driftWord(&buf, "1.0", old_stale));

    // A client that does not know its own version cannot judge anyone
    // else's, but a replaced image is the daemon's own report and stands.
    try std.testing.expectEqualStrings("", wv.driftWord(&buf, "", old));
    try std.testing.expectEqualStrings("daemon stale", wv.driftWord(&buf, "", old_stale));
}

test "labelText: a dropped chunk is narrated beside the state that caused it" {
    // The bar's own format, asserted through the same truncation path the
    // states go through, because "reconnecting, input dropped" is now the
    // longest thing a bar can say and `buf`'s bound is what it tests.
    var buf: [256]u8 = undefined;
    const long = "x" ** 400;
    const said = wv.labelText(&buf, 300, "> ", long, "reconnecting, input dropped");
    try std.testing.expect(std.mem.endsWith(u8, said, " [reconnecting, input dropped]"));
    try std.testing.expect(said.len <= buf.len);
}

test "noticeText: the banner says what the exit line says, whichever sentence it is" {
    var buf: [128]u8 = undefined;
    // Both sentences a vanish can carry. A notice that names a cause the
    // stderr line contradicts is worse than either alone.
    try std.testing.expectEqualStrings(
        "[cannot create a new session (daemon full?)]",
        wv.noticeText(&buf, "mux: cannot create a new session (daemon full?)"),
    );
    try std.testing.expectEqualStrings(
        "[cannot create a new session (daemon unreachable)]",
        wv.noticeText(&buf, "mux: cannot create a new session (daemon unreachable)"),
    );
    // A sentence that never wore the prefix keeps all of its words.
    try std.testing.expectEqualStrings("[detached]", wv.noticeText(&buf, "detached"));
}

test "endAction: on a terminal the last tile's exit leaves an empty wall, not an exit" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [3]Tile = undefined;
    const one = [_]bool{ true, false, false };

    // `x` ends a session, never the program: the wall stays up with the
    // picker over it, and the machines are still there to birth on. There
    // is no tile left to hand the focus to, which is what `back` says.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = null, .msg = null } },
        wv.endAction(&tiles, &one, 3, 0, true, true),
    );

    // The piped half is unchanged and load-bearing: a wall of one with no
    // keyboard is what a script runs, and it exits with the shell's code.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 7, .msg = null } },
        wv.endAction(&tiles, &one, 3, 0, true, false),
    );
}

test "endAction: a birth the picker made into a standing wall is refused ON it, not out of mux" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [3]Tile = undefined;
    const one = [_]bool{ true, false, false };

    // The headline path: the wall's last session ends, the picker opens
    // itself, Enter lands on a daemon that is full. There was no anchor to
    // hand the focus back to, so `born_from` is null — and the ENTRY tile
    // carries null for the opposite reason, which is why the flag exists.
    fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
    tiles[0].keeps_wall = true;
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = null, .msg = "mux: cannot create a new session (daemon full?)" } },
        wv.endAction(&tiles, &one, 3, 0, true, true),
    );

    // A box that never answered: the picker advertises `unreachable` rows
    // as a place to press Enter, so this is a designed path, not a fault.
    fixture.endBench(&tiles, &shared, 0, .lost, 0, null);
    tiles[0].keeps_wall = true;
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = null, .msg = "mux: cannot create a new session (daemon unreachable)" } },
        wv.endAction(&tiles, &one, 3, 0, true, true),
    );

    // Nothing refused anything here, and the wall still stands.
    fixture.endBench(&tiles, &shared, 0, .no_thread, 0, null);
    tiles[0].keeps_wall = true;
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = null, .msg = "mux: could not start a thread for this session" } },
        wv.endAction(&tiles, &one, 3, 0, true, true),
    );

    // The entry tile of `mux TARGET` IS this mux, so its refusal is the
    // program's: the sentence and the code. Byte for byte the inputs of the
    // first block above — same reason, same terminal, same lone tile, same
    // null `born_from` — and `keeps_wall` is what separates the two answers,
    // which is the whole argument for keeping it. (The entry tile is also
    // the one tile with `retry_cold` false, so a reader hunting for another
    // distinguishing field will find that one. It is not the field to read:
    // it answers whether a cold-lost link is worth redialling, and
    // `endAction` must not change meaning when that rule does.)
    fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } },
        wv.endAction(&tiles, &one, 3, 0, true, true),
    );

    // ...and a piped `mux` has no wall to leave standing, flag or no flag.
    fixture.endBench(&tiles, &shared, 0, .refused, 0, null);
    tiles[0].keeps_wall = true;
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 1, .msg = "mux: attach refused or no state received (session full?)" } },
        wv.endAction(&tiles, &one, 3, 0, true, false),
    );
}

test "endAction: the only tile's ending is mux's, and so is any ending nobody can leave" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles: [3]Tile = undefined;
    const one = [_]bool{ true, false, false };
    const two = [_]bool{ true, true, false };

    // One tile, shell exited: the code is mux's, which is what keeps `mux`
    // something you can put in a script.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 7, .msg = null } },
        wv.endAction(&tiles, &one, 3, 0, true, false),
    );

    // Two tiles: the exit vanishes — a dead stripe for a cleanly exited
    // shell is noise — and the wall re-cuts onto the survivor.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = 1, .msg = null } },
        wv.endAction(&tiles, &two, 3, 0, true, false),
    );

    // ...but not once stdin has closed. A piped `mux` with no keyboard
    // waits in poll forever with the code in hand — reproduced at RC=124
    // before this argument existed, which is the whole reason the rule is
    // stated in one pure function and not at the read that happens to
    // notice EOF.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 7, .msg = null } },
        wv.endAction(&tiles, &two, 3, 0, false, false),
    );

    // A tile the user already forgot is not company: `present`, never the
    // wall's length, is what "somewhere to go" counts.
    const one_left = [_]bool{ true, false, false };
    fixture.endBench(&tiles, &shared, 0, .exited, 3, null);
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 3, .msg = null } },
        wv.endAction(&tiles, &one_left, 3, 0, true, false),
    );
}

test "endAction: a refusal a chord earned goes back where the chord was typed" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles: [3]Tile = undefined;
    const two = [_]bool{ true, true, false };

    // The plain client's `.refused` recovery: the daemon would not create
    // the session, and the chord-born tile leaves with no trace — the
    // sentence is the record a script reads, the survivor the screen.
    fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon full?)" } },
        wv.endAction(&tiles, &two, 3, 1, true, false),
    );

    // It outranks the closed stdin, because vanishing lands on a live
    // SESSION rather than on nothing: whatever ends that one ends the run.
    fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon full?)" } },
        wv.endAction(&tiles, &two, 3, 1, false, false),
    );

    // A daemon that never answered is not a daemon that answered and said
    // no. `x` on the last LOCAL session ends the local daemon, so a wall
    // whose own machine has gone is exactly how this arrives — and "full?"
    // would send the user hunting for sessions that are not there.
    fixture.endBench(&tiles, &shared, 1, .lost, 1, 0);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = 0, .msg = "mux: cannot create a new session (daemon unreachable)" } },
        wv.endAction(&tiles, &two, 3, 1, true, false),
    );

    // ...but a chord-born tile that HAD its session and lost the link later
    // is not a failed creation at all: it keeps its rect and narrates, like
    // any other lost tile.
    fixture.endBench(&tiles, &shared, 1, .lost, 1, 0);
    tiles[1].ever_up.store(true, .release);
    try std.testing.expectEqual(
        EndAction{ .refocus = 0 },
        wv.endAction(&tiles, &two, 3, 1, true, false),
    );

    // A refusal with nowhere to fall back to is an ending like any other,
    // and says the thing the plain client said.
    fixture.endBench(&tiles, &shared, 0, .refused, 1, null);
    const one = [_]bool{ true, false, false };
    const out = wv.endAction(&tiles, &one, 3, 0, true, false);
    try std.testing.expectEqual(@as(u8, 1), out.finish.code);
    try std.testing.expectEqualStrings(
        "mux: attach refused or no state received (session full?)",
        out.finish.msg.?,
    );

    // ...and so is one whose fall-back tile the user forgot meanwhile.
    fixture.endBench(&tiles, &shared, 1, .refused, 1, 0);
    const gone_back = [_]bool{ false, true, false };
    try std.testing.expectEqual(
        @as(u8, 1),
        wv.endAction(&tiles, &gone_back, 3, 1, true, false).finish.code,
    );
}

test "endAction: an exited session's tile vanishes; a lost one narrates" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles: [3]Tile = undefined;
    const two = [_]bool{ true, true, false };

    // A clean exit is noise once it is over: the tile leaves, the wall
    // re-cuts, and nothing is said.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    try std.testing.expectEqual(
        EndAction{ .vanish = .{ .back = 1, .msg = null } },
        wv.endAction(&tiles, &two, 3, 0, true, false),
    );

    // A lost link can heal: the tile keeps its rect and its bar, and the
    // focus steps to the next present tile rather than the tile leaving.
    fixture.endBench(&tiles, &shared, 0, .lost, 1, null);
    try std.testing.expectEqual(
        EndAction{ .refocus = 1 },
        wv.endAction(&tiles, &two, 3, 0, true, false),
    );

    // The last present tile exiting is still mux's own ending, with the
    // shell's code — `mux` in a script is the thing that ran the shell.
    fixture.endBench(&tiles, &shared, 0, .exited, 3, null);
    const one = [_]bool{ true, false, false };
    try std.testing.expectEqual(
        EndAction{ .finish = .{ .code = 3, .msg = null } },
        wv.endAction(&tiles, &one, 3, 0, true, false),
    );
}

test "endAction: a vanishing tile hands focus to a live neighbour, not a corpse" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles: [3]Tile = undefined;
    const two = [_]bool{ true, true, false };

    // Tile 0 (focused) has just exited — its pump is dead. Tile 1 is
    // present but dead (pump ended, narrating a lost link). With no live
    // neighbour, the back-target falls back to the dead tile (the wall
    // still shows it), but with a live third tile, focus lands there.
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    tiles[0].alive.store(false, .release);
    tiles[1].alive.store(false, .release);
    tiles[1].end.store(@intFromEnum(EndReason.lost), .release);
    tiles[1].end_seen = true;
    // Two tiles, both dead: fallback to the present-only step.
    const out_dead = wv.endAction(&tiles, &two, 3, 0, true, false);
    try std.testing.expect(out_dead == .vanish);
    try std.testing.expectEqual(@as(usize, 1), out_dead.vanish.back);

    // Three tiles: 0 dead ending, 1 dead narrating, 2 alive. Focus must
    // land on tile 2, the live neighbour — never the dead tile 1.
    const three = [_]bool{ true, true, true };
    fixture.endBench(&tiles, &shared, 0, .exited, 7, null);
    tiles[0].alive.store(false, .release);
    tiles[1].alive.store(false, .release);
    tiles[1].end.store(@intFromEnum(EndReason.lost), .release);
    tiles[1].end_seen = true;
    const out_live = wv.endAction(&tiles, &three, 3, 0, true, false);
    try std.testing.expect(out_live == .vanish);
    try std.testing.expectEqual(@as(usize, 2), out_live.vanish.back);
}

test "closeNotice: a sentence the popup left behind is given a tile to land on" {
    const alloc = std.testing.allocator;
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    const tiles = try alloc.alloc(Tile, 2);
    defer alloc.free(tiles);
    fixture.noticeBench(&shared, tiles);
    const present = [_]bool{ true, true };
    // Focus on tile 1, and the forgotten host owned NEITHER tile: the focus
    // does not move on its own here, which is the whole of the finding.
    shared.sel = 1;
    wv.setNotice(&shared, "[hosts file not updated: read-only file system]");

    wv.closeNotice(tiles, &present, &shared);

    // Armed, not merely rung: a pump paints the notice on a CLAIM, and a
    // doorbell alone gives it nothing to claim for.
    if (!tiles[1].claim_pending.load(.acquire)) return error.NoticeHasNoClaimToRideOn;
    // Still there for the pump to take on that claim, whole: a keyboard
    // that consumed it here would paint nothing and lose the sentence.
    var buf: [96]u8 = undefined;
    try std.testing.expectEqualStrings(
        "[hosts file not updated: read-only file system]",
        wv.takeNotice(&shared, &buf),
    );

    // The control: with nothing to say, the close is the bare retry it was,
    // and no tile is handed a claim it did not earn.
    fixture.noticeBench(&shared, tiles);
    wv.closeNotice(tiles, &present, &shared);
    try std.testing.expect(!tiles[1].claim_pending.load(.acquire));
}

test "bare keys reach the focused tile; the prefix does not" {
    var f: interact.PrefixFilter = .{};
    // Bare keys are forwarded to the focused tile.
    var keys: [3]u8 = .{ 'a', 'b', 'c' };
    const a = f.feed(&keys);
    try std.testing.expectEqualStrings("abc", a.forward);
    try std.testing.expectEqual(interact.PrefixFilter.Action.none, a.action);
    // A chord consumes its key and produces no forward bytes.
    var chord: [2]u8 = .{ 0x1c, 'n' };
    const b = f.feed(&chord);
    try std.testing.expectEqual(@as(usize, 0), b.forward.len);
    try std.testing.expectEqual(interact.PrefixFilter.Action.next_session, b.action);
}

test "focus follows a click: rectHit picks the tile under a row and column" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [2]Tile = undefined;
    tiles[0] = Tile{
        .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    tiles[1] = Tile{
        .r = .{ .target = .{ .sock = "/tmp/y" }, .label = "y", .session = "" },
        .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 1,
        .wake_r = -1,
        .wake_w = -1,
    };
    const present = [_]bool{ true, true };
    // A click in row 3 hits tile 0.
    try std.testing.expectEqual(@as(?usize, 0), wv.rectHit(&tiles, &present, &shared, 3, 0));
    // A click in row 15 hits tile 1.
    try std.testing.expectEqual(@as(?usize, 1), wv.rectHit(&tiles, &present, &shared, 15, 0));

    // A beside pair: same rows, different columns. A click in the left
    // half hits tile 0, the right half hits tile 1, and a rail column
    // between them hits neither.
    tiles[0].rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 39 };
    tiles[1].rect = .{ .top = 0, .left = 41, .rows = 24, .cols = 39 };
    try std.testing.expectEqual(@as(?usize, 0), wv.rectHit(&tiles, &present, &shared, 5, 10));
    try std.testing.expectEqual(@as(?usize, 1), wv.rectHit(&tiles, &present, &shared, 5, 50));
    try std.testing.expectEqual(@as(?usize, null), wv.rectHit(&tiles, &present, &shared, 5, 40));
}

test "setFocus writes the outgoing tile's release before the store that lets the next pump claim" {
    // The keyboard thread's half of the terminal handover, and the order is
    // load-bearing: two pumps are two threads, and A's release and B's claim
    // race if the release is not on the wire first. The outgoing pump writes
    // nothing (already_written), so if this stops emitting, nothing does —
    // and the wall reports clicks into whatever shell the user lands in.
    // is_tty is true because this leg is about what a focus move WRITES, and
    // the gate that suppresses those writes on a pipe is exactly what it
    // must not be testing.
    const p = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(p[0]);
    defer std.posix.close(p[1]);
    var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [2]Tile = undefined;
    tiles[0] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    tiles[1] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
        .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 1,
        .wake_r = -1,
        // The claim doorbell lands on the SAME pipe as the release: the
        // write order on one fd is the only order a test can read, and the
        // release must come first.
        .wake_w = p[1],
    };
    shared.sel = 0;
    var buf: [512]u8 = undefined;

    // A move to the SAME tile has no outgoing session to take off, so it
    // writes no release — a release here would unset modes the user's own
    // terminal may have arrived with.
    wv.setFocus(&tiles, &shared, 0);
    try std.testing.expect(std.mem.indexOf(u8, fixture.readAvail(p[0], &buf), interact.session_release) == null);

    // A -> B: the release is written under paint_mu, before sel is stored
    // and before B is doorbelled to claim. On the one pipe both share that
    // shows up as the release preceding the claim's wake byte.
    wv.setFocus(&tiles, &shared, 1);
    const out = fixture.readAvail(p[0], &buf);
    const rel = std.mem.indexOf(u8, out, interact.session_release) orelse
        return error.NoReleaseOnFocusMove;
    const bell = std.mem.indexOfScalar(u8, out, 0) orelse
        return error.NoClaimDoorbell;
    try std.testing.expect(rel < bell);
    // The outgoing pump is told to release, the incoming one to claim.
    try std.testing.expect(tiles[0].release_pending.load(.acquire));
    try std.testing.expect(tiles[1].claim_pending.load(.acquire));
    try std.testing.expectEqual(@as(usize, 1), shared.sel);
}

test "setFocus re-arms the wall's own mouse capture itself — a parked pump cannot" {
    // Clicking an [unreachable] tile is a designed act (birthing on one is
    // too), and that tile's pump is parked in `dial`'s backoff loop: the
    // focus claim that used to re-enable mouse reporting never runs there.
    // The release alone then left the terminal deaf to every later click —
    // including the one that would have moved focus back off the dead tile
    // (found live 2026-09-02: one click on a dark laptop's pane killed the
    // mouse for the whole wall until a keyboard chord landed on a live
    // tile). The re-arm is the KEYBOARD's own write, after the release and
    // before the doorbell, so the wall hears clicks with no pump running.
    const p = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(p[0]);
    defer std.posix.close(p[1]);
    var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [2]Tile = undefined;
    tiles[0] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    tiles[1] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
        .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 1,
        .wake_r = -1,
        .wake_w = p[1],
    };
    shared.sel = 0;
    var buf: [512]u8 = undefined;

    wv.setFocus(&tiles, &shared, 1);
    const out = fixture.readAvail(p[0], &buf);
    const rel = std.mem.indexOf(u8, out, interact.session_release) orelse
        return error.NoReleaseOnFocusMove;
    const arm = std.mem.indexOf(u8, out, interact.wall_mouse_capture) orelse
        return error.NoWallRearm;
    const bell = std.mem.indexOfScalar(u8, out, 0) orelse
        return error.NoClaimDoorbell;
    try std.testing.expect(rel < arm);
    try std.testing.expect(arm < bell);
}

test "a focus move drops what the wall's input filters are holding" {
    // The input-filter state that mattered — the drag — moved off the
    // wall's keyboard loop and onto each tile's Core. setFocus drops it the
    // same way relayout does: a repaint_gen bump every pump clears its
    // core.drag on. The wall-level prefix is deliberately KEPT, because a
    // chord is a wall command now and not a tile's; what stopped existing on
    // a focus move is the screen the drag was drawn on, not a half-typed
    // chord about it.
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles: [2]Tile = undefined;
    tiles[0] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "a", .session = "a" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    tiles[1] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "b", .session = "b" },
        .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 1,
        .wake_r = -1,
        .wake_w = -1,
    };
    shared.sel = 0;
    const before = shared.repaint_gen.load(.acquire);
    wv.setFocus(&tiles, &shared, 1);
    try std.testing.expect(shared.repaint_gen.load(.acquire) > before);
}

test "setFocus: the marker moves between two tiles with no pump to move it" {
    // A refused attach and an exited session both leave a tile
    // whose pump has ended: repaint_gen and the doorbell reach nobody, so
    // the bar keeps whatever marker it was born with. Two of them and
    // Ctrl-\ 1 / Ctrl-\ 2 change nothing on screen; one of them beside a
    // live tile shows two `>`. The keyboard is the only thread left that
    // can paint these, which is what this pins.
    const p = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(p[0]);
    defer std.posix.close(p[1]);
    var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles: [3]Tile = undefined;
    tiles[0] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "one", .session = "a" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
        .state = .refused,
        .alive = std.atomic.Value(bool).init(false),
    };
    tiles[1] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "two", .session = "b" },
        .rect = .{ .top = 12, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 1,
        .wake_r = -1,
        .wake_w = -1,
        .state = .refused,
        .alive = std.atomic.Value(bool).init(false),
    };
    // Forgotten, not merely dead: `x` clears `present` and sets `removed`
    // together, and a bar painted into a rect the layout no longer owns
    // writes over a neighbour.
    tiles[2] = Tile{
        .r = .{ .target = .{ .sock = "/s" }, .label = "ghost", .session = "c" },
        .rect = .{ .top = 0, .left = 0, .rows = 12, .cols = 80 },
        .shared = &shared,
        .idx = 2,
        .wake_r = -1,
        .wake_w = -1,
        .state = .exited,
        .alive = std.atomic.Value(bool).init(false),
        .removed = std.atomic.Value(bool).init(true),
    };
    shared.sel = 0;

    wv.setFocus(&tiles, &shared, 1);

    var buf: [2048]u8 = undefined;
    const out = fixture.readAvail(p[0], &buf);
    // The bar is one contiguous write (`paintLabelLocked`), so the cursor
    // move and the marker are adjacent bytes: matching them together is
    // what says the marker landed in THIS tile's bar and not a neighbour's.
    if (std.mem.indexOf(u8, out, "\x1b[13;1H\x1b[7m 2> two") == null)
        return error.DeadTileNeverGainsTheMarker;
    if (std.mem.indexOf(u8, out, "\x1b[1;1H\x1b[7m 1  one") == null)
        return error.DeadTileNeverDropsTheMarker;
    if (std.mem.indexOf(u8, out, "ghost") != null)
        return error.ForgottenTilePaintedItsBar;
}

test "the bar's digit is the tile's chord: idx + 1, and a two-digit tile still says where it is" {
    // Ctrl-\ 1-9 focuses by POSITION (tiles[idx - 1]) while the bar named the
    // session (#0, #2...): two series that stop lining up the moment a tile is
    // forgotten or a session dies. The digit on the bar is the chord's, so the
    // two cannot diverge.
    const p = try std.posix.pipe2(.{ .NONBLOCK = true });
    defer std.posix.close(p[0]);
    defer std.posix.close(p[1]);
    var shared = Shared{ .out_fd = p[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    shared.sel = 0;
    var tiles: [3]Tile = undefined;
    const labels = [_][]const u8{ "one", "two", "twelve" };
    // idx 11 with no tiles 3..10: the digit is the tile's own idx, never a
    // count of what is on screen. Past 9 the chord cannot reach it, but the
    // number still says where it sits — the chord's reach is the chord's
    // limit, not the bar's.
    const idxs = [_]usize{ 0, 1, 11 };
    for (&tiles, labels, idxs, 0..) |*t, label, idx, row| {
        t.* = Tile{
            .r = .{ .target = .{ .sock = "/s" }, .label = label, .session = "a" },
            .rect = .{ .top = @intCast(row * 8), .left = 0, .rows = 8, .cols = 80 },
            .shared = &shared,
            .idx = idx,
            .wake_r = -1,
            .wake_w = -1,
            .state = .up,
            .alive = std.atomic.Value(bool).init(false),
        };
        wv.paintLabelLocked(t);
    }

    var buf: [2048]u8 = undefined;
    const out = fixture.readAvail(p[0], &buf);
    // Cursor move and marker together, as in the setFocus pin: that is what
    // says the digit landed in THIS tile's bar and not a neighbour's.
    if (std.mem.indexOf(u8, out, "\x1b[1;1H\x1b[7m 1> one") == null)
        return error.FocusedBarLostItsChordDigit;
    if (std.mem.indexOf(u8, out, "\x1b[9;1H\x1b[7m 2  two") == null)
        return error.UnfocusedBarLostItsChordDigit;
    if (std.mem.indexOf(u8, out, "\x1b[17;1H\x1b[7m 12  twelve") == null)
        return error.TwoDigitTileLostItsNumber;
}

test "a vanished tile is stepped over" {
    // A tile's end and a poll that no longer lists its session both refocus
    // through stepPresent, and a click's rectHit and the Ctrl-\ l and 1-9
    // chords check the same present array a vanish clears. Tiles are never
    // compacted — pumps hold pointers into the array — so a vanished tile is
    // a hole focus steps over, not a gap it fills. `removed` and present clear
    // together in `vanishTile`, so stepping on present is stepping past
    // `removed`.
    const mid = [_]bool{ true, false, true };
    try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 0, true));
    try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 2, true));
    try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 2, false));
    try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 0, false));
    // Standing on the hole (the moment after x, before the selection has
    // moved) still steps to a real tile.
    try std.testing.expectEqual(@as(?usize, 2), wv.stepPresent(&mid, 1, true));
    try std.testing.expectEqual(@as(?usize, 0), wv.stepPresent(&mid, 1, false));

    // The last tile gone: nothing is a selection any more, and every
    // motion says so rather than picking a tile that is gone.
    const none = [_]bool{ false, false };
    try std.testing.expectEqual(@as(?usize, null), wv.stepPresent(&none, 0, true));
    try std.testing.expectEqual(@as(?usize, null), wv.stepPresent(&none, 0, false));

    // One survivor is its own answer, exactly as a one-tile wall is.
    const one = [_]bool{ false, true, false };
    try std.testing.expectEqual(@as(?usize, 1), wv.stepPresent(&one, 1, true));
    try std.testing.expectEqual(@as(?usize, 1), wv.stepPresent(&one, 0, false));
}

test "tileBanner: a prompt wider than its pane shows the tail inside the pane" {
    // A tile that painted a whole long spelling would run across the rail
    // into its neighbour, so the banner is bounded by the tile's rect and
    // not by the terminal. The tail is what is kept: the cursor end of the
    // line the user is still typing.
    const pipe = try std.posix.pipe();
    defer std.posix.close(pipe[0]);
    var shared = Shared{ .out_fd = pipe[1], .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var t = Tile{
        .r = .{ .target = .{ .sock = "/tmp/x" }, .label = "x", .session = "" },
        .rect = .{ .top = 2, .left = 5, .rows = 5, .cols = 10 },
        .shared = &shared,
        .idx = 0,
        .wake_r = -1,
        .wake_w = -1,
    };
    const text = ": --sock /tmp/very/long/paths_";
    try std.testing.expectEqual(@as(usize, 30), text.len);
    wv.tileBanner(&t, text);
    std.posix.close(pipe[1]);

    var out: [256]u8 = undefined;
    const n = try std.posix.read(pipe[0], &out);
    const got = out[0..n];
    // The tile's own corner: rect.top + labelRows + 1, rect.left + 1.
    try std.testing.expect(std.mem.indexOf(u8, got, "\x1b[4;6H") != null);
    const from = std.mem.indexOf(u8, got, "\x1b[7m").? + 4;
    const to = std.mem.indexOf(u8, got, "\x1b[0m").?;
    try std.testing.expectEqualStrings(text[text.len - 10 ..], got[from..to]);
}

test "endedTile: a pending pane is not an ended one - mux does not exit on a pane no pump ran for" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = true };
    var tiles = fixture.diffFixture(&shared);
    const present = [_]bool{ true, true, true };
    // Plural on purpose: two pending panes, and the focus sits on one of
    // them - the resumed wall's exact first pass.
    for (&tiles) |*t| t.alive.store(false, .release);
    tiles[0].pending = true;
    tiles[1].pending = true;
    tiles[2].end.store(@intFromEnum(EndReason.exited), .release);
    shared.sel = 0;
    // The real exit among the pending panes is still found; neither
    // pending pane is.
    try std.testing.expectEqual(@as(?usize, 2), wv.endedTile(&tiles, &present, &shared));
    try std.testing.expectEqual(@as(?usize, null), wv.endedTile(&tiles, &present, &shared));
    try std.testing.expect(!tiles[0].end_seen and !tiles[1].end_seen);
    // The other arm proves the guard is load-bearing: the same wall with
    // `pending` off reads the focused pane as ended with reason `.none`.
    tiles[0].pending = false;
    try std.testing.expectEqual(@as(?usize, 0), wv.endedTile(&tiles, &present, &shared));
}

test "spawnPump: a remembered shape never dials - a pending tile gets no thread" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
    var tiles = fixture.diffFixture(&shared);
    shared.running.store(false, .release);
    tiles[1].pending = true;
    tiles[1].alive.store(false, .release);
    tiles[1].pump_done.store(true, .release);
    tiles[1].state = .waiting;
    wv.spawnPump(&tiles[1]);
    // A pump's first acts are `t.core = &core` and a `.connecting` label;
    // a refused spawn leaves the seed's own settings alone. Bounded wait,
    // so a pump that DID spawn has time to betray itself.
    std.Thread.sleep(20 * std.time.ns_per_ms);
    try std.testing.expect(!tiles[1].alive.load(.acquire));
    try std.testing.expect(tiles[1].pump_done.load(.acquire));
    try std.testing.expectEqual(State.waiting, tiles[1].state);
    try std.testing.expectEqual(@as(?*interact.Core, null), tiles[1].core);
}

test "State.waiting has a word of its own, and truncation drops the label before it" {
    var buf: [256]u8 = undefined;
    const long = "x" ** 400;
    const bar = wv.labelText(&buf, 30, "2> ", long, State.waiting.word());
    if (!std.mem.endsWith(u8, bar, " [waiting]")) return error.WaitingBarLostTheStateWord;
    if (bar.len > 30) return error.WaitingBarOverranTheTerminal;
}

test "removePane: the pane leaves the wall, its pump is told to detach, and nothing is asked to end" {
    var shared = Shared{ .out_fd = -1, .size = .{ .cols = 120, .rows = 40 }, .is_tty = false };
    defer shared.tree.deinit();
    // The flats `relayout` cuts are the test allocator's, so the wall must
    // free them with it: the default is the page allocator, and a leak here
    // fails the whole file rather than this test.
    shared.flat_alloc = std.testing.allocator;
    defer if (shared.last_flat) |*f| f.deinit(std.testing.allocator);
    defer if (shared.base_flat) |*f| f.deinit(std.testing.allocator);
    var tiles = [_]Tile{ fixture.claimBench(&shared, 0), fixture.claimBench(&shared, 1) };
    var present = [_]bool{ true, true };
    // No pump is spawned here, so nothing will ever clear the liveness a
    // tile is born with and `endPumps` would wait on it forever. `removePane`
    // reads none of it: the detach is stored and the bell rung whatever the
    // pump is doing, which is why this fixture can state it rather than race.
    for (&tiles) |*t| t.alive.store(false, .release);
    try shared.tree.addFirst(0);
    try shared.tree.splitRight(0, 1);
    const w = fixture.wallAll(std.testing.allocator, &tiles, &present, &shared);
    shared.sel = 1;

    wv.removePane(w, 1);
    try std.testing.expect(!present[1]);
    try std.testing.expect(present[0]);
    try std.testing.expect(tiles[1].detach_req.load(.acquire));
    // The daemon was NOT asked to end anything: the ask mailbox is empty.
    try std.testing.expectEqual(@as(u8, 0), tiles[1].ask.load(.acquire));
    try std.testing.expectEqual(@as(usize, 0), shared.sel);
    var buf: [128]u8 = undefined;
    try std.testing.expectEqualStrings("[pane removed - the session is still on its daemon]", wv.takeNotice(&shared, &buf));
    fixture.endPumps(&tiles);
}