a73x

src/tui/predict.zig

Ref:   Size: 59.5 KiB   History

//! Speculative local echo, as an OVERLAY: predictions live in a queue beside
//! the replica and never enter it, so a wrong guess costs a repaint and never a
//! desync. Engine-free — `reconcile` takes its grid duck-typed.
//!
//! Judgment is about EVIDENCE, not arrival order: a frame showing the predicted
//! cell unchanged was probably built before the keystroke arrived, so the
//! prediction stays pending. Only a cell that moved to something that is
//! neither our guess nor what was there refutes.
//!
//! The tiers describe ECHO bits: readline echoes itself, so a shell prompt is
//! `.adaptive` and never `.always`. Predictions COPY bytes and the queue is
//! read by index — a slice goes stale on the next append.
const std = @import("std");
const proto = @import("term").protocol;

/// One predicted character at one place on the grid. Printable ASCII only,
/// deliberately: `ch` is a byte by value, so there is nothing here that can
/// outlive what it was copied from.
pub const Cell = struct { row: u16, col: u16, ch: u8 };

const Pred = struct {
    cell: Cell,
    /// What the cell showed when the prediction was made. The load-bearing
    /// field of reconcile v2: a cell that STILL shows this is a cell the
    /// authoritative stream has said nothing about yet, which is a
    /// different thing from one that disagrees with us.
    prev_ch: u8,
    /// The authoritative seq the client held when this was predicted. A
    /// frame carrying a HIGHER seq is the first one that could possibly
    /// have been built after the keystroke reached the daemon, and so the
    /// first one entitled to have an opinion about it.
    made_seq: u64,
    /// Wall clock at prediction time, supplied by the caller — the module
    /// never reads a clock, so every deadline in here is testable.
    made_ms: i64,
    /// Judging frames that have looked at this cell and found it unchanged.
    /// Bounded by expire_after_frames, because "no evidence yet" must not
    /// be a state a prediction can sit in forever.
    frames: u8 = 0,
    /// Whether this prediction has ever been drawn. Not the same question as
    /// whether the overlay is confident now: one queued while unconfident
    /// and painted later, after a promotion, has reached the screen exactly
    /// once and must be counted exactly once.
    painted: bool = false,
};

/// Units are mixed on purpose and stated per field: `contradicted` counts
/// EVENTS while `made` counts PREDICTIONS, so `confirmed + contradicted` totals
/// nothing. `made == confirmed + abandoned + pending` is the identity that
/// holds, and `abandoned` exists so it can.
pub const Counters = struct {
    /// PER PREDICTION: queued, whether or not it was ever shown.
    made: u64 = 0,
    /// PER PREDICTION: ever reached the screen, counted the first time it is
    /// painted — whether that was when it was made or later, when a promotion
    /// made an already-queued one visible. Never exceeds `made`.
    displayed: u64 = 0,
    /// PER PREDICTION: retired because the authoritative grid agreed.
    confirmed: u64 = 0,
    /// PER EVENT: one refutation or expiry, however many predictions it
    /// discarded — a contradiction takes the whole queue, so this counts
    /// how often we were wrong, not how much was thrown away.
    contradicted: u64 = 0,
    /// PER EVENT: the subset of `contradicted` where nothing ever answered
    /// rather than something disagreed. Counted in both, so a reader can tell
    /// "we guessed wrong" apart from "the application went quiet".
    expired: u64 = 0,
    /// PER PREDICTION: queued but discarded without a verdict — by a
    /// contradiction, an expiry, or a flush. The flush case is counted
    /// nowhere else, which is what used to leave predictions unaccounted
    /// for: made, never confirmed, and no number saying where they went.
    abandoned: u64 = 0,
    /// PER INPUT: keystrokes declined for prediction, BY WHOEVER MADE THE CALL.
    /// Most are `predictAt`'s own refusals, but the client increments this
    /// directly for input it declines to offer at all — a paste, whose lead byte
    /// is printable. So this is NOT "times `predictAt` said no", and none of
    /// these became predictions.
    suppressed: u64 = 0,
    /// PER PREDICTION: queued with display earned, and hidden anyway
    /// because the path was measured too fast to show it. A subset of
    /// `made` and disjoint from `displayed`; the number that says whether
    /// the local gate is what is keeping an overlay quiet.
    local: u64 = 0,
};

/// What the pty's mode bits say about predicting here.
///   .always   — icanon && echo: the line discipline prints the character
///               itself, so predicting it is deduction.
///   .never    — icanon && !echo: a password prompt.
///   .adaptive — !icanon: raw mode, where the application decides what a
///               keystroke looks like and the guess must be earned.
const Context = enum { always, never, adaptive };

/// Consecutive confirmations that earn display in `.adaptive`.
const promote_after: u8 = 2;

/// How many judging frames a prediction may go unanswered before it is given up
/// on — the phantom-glyph guard. An application that consumes a keystroke and
/// repaints some OTHER row leaves the predicted cell untouched forever.
const expire_after_frames: u8 = 8;

/// The same guard in wall time, for the case the frame bound cannot catch:
/// the application answers by going quiet. Milliseconds.
const expire_after_ms: i64 = 1000;

/// The round trip below which a prediction is never worth SHOWING. On a local
/// socket a confirm lands inside the frame the keystroke was painted in, so a
/// correct prediction is invisible by construction and the only ones the eye
/// catches are the wrong ones, underlined until they expire.
///
/// Two triggers rather than one, with the smoothed estimate moving between
/// them, so jitter around the line does not flap the overlay per keystroke. The
/// 20/30 pair is mosh's; the smoothing is TCP's 1/8.
const local_below_ms: i64 = 20;
const local_above_ms: i64 = 30;

/// Engine-free mirror of the engine's cursor position.
pub const CursorPos = struct { x: u16 = 0, y: u16 = 0 };

/// A struct, not four positional arguments: `ch` and `prev_ch` are
/// adjacent bytes, so transposing them compiles silently and turns every
/// prediction into a no-op or a wrong guess.
const Keystroke = struct {
    cursor: CursorPos,
    /// The byte the user typed.
    ch: u8,
    /// What the client's replica shows at that cell right now. The caller
    /// reads it; the overlay stays engine-free.
    prev_ch: u8,
    now_ms: i64,
};

pub const Outcome = union(enum) {
    /// Refused. Nothing queued, nothing painted; the keystroke still goes
    /// to the daemon exactly as it would have.
    suppressed,
    /// Queued, and to be painted at this cell now.
    display: Cell,
    /// Queued but deliberately invisible — adaptive mode gathering the evidence
    /// that lets the next one be seen. The paint decision arrives WITH the cell
    /// rather than as a question the caller must remember to ask.
    hidden: Cell,
};

pub const Verdict = enum {
    /// Nothing pending was old enough to judge.
    none,
    /// At least one prediction was confirmed and retired; none were wrong.
    confirmed,
    /// One was wrong, so the queue is empty and the caller should repaint.
    contradicted,
};

/// Reads cells out of a plain grid dump: rows joined by '\n', trailing
/// blanks absent, which is the shape `Engine.dumpPlain` produces. Holds a
/// borrowed slice and is meant to be built, used and dropped inside one
/// reconcile call — never stored.
pub const PlainGrid = struct {
    text: []const u8,
    cols: u16,

    /// null means "outside the grid", not "blank": a dump carries no
    /// trailing blanks, so past a row's end or the dump's end is blank. A
    /// multi-byte cell answers with its lead byte, which can never equal a
    /// predicted printable ASCII byte — so it contradicts, the safe way.
    pub fn cellChar(self: PlainGrid, row: u16, col: u16) ?u8 {
        if (col >= self.cols) return null;
        var y: u16 = 0;
        var it = std.mem.splitScalar(u8, self.text, '\n');
        while (it.next()) |line| : (y += 1) {
            if (y != row) continue;
            if (col >= line.len) return ' ';
            return line[col];
        }
        return ' ';
    }
};

pub const Overlay = struct {
    alloc: std.mem.Allocator,
    pending: std.ArrayList(Pred) = .empty,
    ctx: Context = .never,
    /// What the POLICY says about painting: true in `.always`, false in
    /// `.never`, and earned in `.adaptive`. Not the gate itself any more —
    /// that is `visible`, which also asks whether the path is worth it.
    confident: bool = false,
    streak: u8 = 0,
    /// Smoothed keystroke-to-confirm round trip, or null until the first
    /// confirm has measured one. Counted from the keystroke's own clock to
    /// the frame that confirmed it, so it is the whole path — tty read,
    /// wire, daemon, pty, engine, wire back — and not a transport ping.
    srtt_ms: ?i64 = null,
    /// The path is too fast to show predictions on. Moves only when
    /// `srtt_ms` crosses a trigger, so it carries the hysteresis; an
    /// unmeasured path is not local, which keeps a fresh WAN attach on the
    /// behaviour the numbers in decisions.md were measured against.
    local: bool = false,
    counters: Counters = .{},
    cols: u16,
    rows: u16,
    scroll_mode: bool = false,
    resize_pending: bool = false,
    /// The last mode byte adopted, or null before any pty_mode frame has
    /// arrived. The raw byte rather than the derived context, because the
    /// churn rule is about the BITS moving: a change inside one tier is
    /// still an application taking the terminal somewhere else.
    mode_byte: ?u8 = null,
    /// The last authoritative seq the client applied; stamped onto each new
    /// prediction as `made_seq`.
    last_seq: u64 = 0,

    pub fn init(alloc: std.mem.Allocator, cols: u16, rows: u16) Overlay {
        return .{ .alloc = alloc, .cols = cols, .rows = rows };
    }

    pub fn deinit(self: *Overlay) void {
        self.pending.deinit(self.alloc);
    }

    /// Adopt what the daemon says the pty is doing. No frame ever arriving
    /// leaves the overlay at `.never`, the safe default an old daemon gets free.
    ///
    /// ANY change to the bits flushes the queue and un-earns display, even one
    /// within the same tier: predictions in flight across a mode transition are
    /// unverifiable, and confidence under one line discipline is not evidence
    /// about the next. Re-sending the SAME bits is not a change.
    pub fn setMode(self: *Overlay, flags: proto.PtyModeFlags) void {
        const byte: u8 = @bitCast(flags);
        if (self.mode_byte) |prev| {
            if (prev == byte) return;
        }
        self.mode_byte = byte;
        const next: Context = blk: {
            // A bit we do not understand means the byte describes a
            // terminal we cannot reason about. Predict nothing rather than
            // mask it off and carry on as though we had understood.
            if (flags._pad != 0) break :blk .never;
            if (!flags.icanon) break :blk .adaptive;
            break :blk if (flags.echo) .always else .never;
        };
        self.ctx = next;
        // Outstanding predictions were made under the old line discipline,
        // and the new one may be that they should never have been visible.
        self.flush();
        self.streak = 0;
        self.confident = (next == .always);
    }

    /// A resize invalidates every prediction on the old grid, and moves the
    /// edge that the last-column refusal is measured against.
    pub fn setGrid(self: *Overlay, cols: u16, rows: u16) void {
        if (cols == self.cols and rows == self.rows) return;
        self.cols = cols;
        self.rows = rows;
        self.flush();
    }

    pub fn setScrollMode(self: *Overlay, on: bool) void {
        if (on == self.scroll_mode) return;
        self.scroll_mode = on;
        // Entering scroll mode the cursor stops being where the user is
        // looking; leaving it, the whole viewport is repainted. Either way
        // what is queued no longer describes the screen.
        self.flush();
    }

    pub fn setResizePending(self: *Overlay, pending: bool) void {
        if (pending == self.resize_pending) return;
        self.resize_pending = pending;
        if (pending) self.flush();
    }

    /// For a frame applied without judging it — a snapshot.
    pub fn noteSeq(self: *Overlay, seq: u64) void {
        self.last_seq = seq;
    }

    /// Speculate one printable byte at the cursor, or refuse to. Every refusal
    /// is a place where being wrong costs more than being slow: a control byte,
    /// a multi-byte sequence, the last column, a scrolled viewport, a pending
    /// resize, a `.never` context. Infallible by construction — an allocation
    /// failure suppresses rather than propagating.
    pub fn predictAt(self: *Overlay, k: Keystroke) Outcome {
        if (self.ctx == .never) return self.suppress();
        if (self.scroll_mode or self.resize_pending) return self.suppress();
        if (k.ch < 0x20 or k.ch >= 0x7f) return self.suppress();
        if (self.cols == 0 or self.rows == 0) return self.suppress();
        if (k.cursor.y >= self.rows) return self.suppress();
        if (k.cursor.x >= self.cols -| 1) return self.suppress();
        // The glyph is already there. Painting it changes nothing on screen,
        // and queueing it would put a prediction into the queue that cannot
        // be told apart from "no evidence yet" no matter what happens next.
        if (k.ch == k.prev_ch) return self.suppress();

        const cell: Cell = .{ .row = k.cursor.y, .col = k.cursor.x, .ch = k.ch };
        self.pending.append(self.alloc, .{
            .cell = cell,
            .prev_ch = k.prev_ch,
            .made_seq = self.last_seq,
            .made_ms = k.now_ms,
        }) catch return self.suppress();

        self.counters.made += 1;
        if (self.visible()) {
            self.markPainted(self.pending.items.len - 1);
            return .{ .display = cell };
        }
        // Queued even when local, deliberately: a hidden prediction is still
        // judged, so it keeps measuring the path and keeps the streak. That
        // is what lets the overlay come back on if the box gets slow, and
        // what made the local-gate tests cheap — no second state machine.
        if (self.confident) self.counters.local += 1;
        return .{ .hidden = cell };
    }

    /// The gate on painting: earned, AND on a path slow enough to show it.
    pub fn visible(self: *const Overlay) bool {
        return self.confident and !self.local;
    }

    /// Fold one measured round trip into the estimate and move `local`
    /// if it crossed a trigger. Confirms only — an expiry says nothing
    /// about how fast the path is, only that the application went quiet.
    fn samplePath(self: *Overlay, rtt_ms: i64) void {
        const srtt = if (self.srtt_ms) |s| s + @divTrunc(rtt_ms - s, 8) else rtt_ms;
        self.srtt_ms = srtt;
        if (srtt <= local_below_ms) self.local = true;
        if (srtt >= local_above_ms) self.local = false;
    }

    fn suppress(self: *Overlay) Outcome {
        self.recordSuppressed();
        return .suppressed;
    }

    /// A refusal the CALLER made: a plain-ASCII paste, whose lead byte is
    /// printable.
    pub fn recordSuppressed(self: *Overlay) void {
        self.counters.suppressed += 1;
    }

    /// A prediction queued while unconfident is invisible; a later
    /// promotion makes the next repaint draw it. Count it once, however
    /// many repaints redraw the cell.
    pub fn markPainted(self: *Overlay, i: usize) void {
        if (self.pending.items[i].painted) return;
        self.pending.items[i].painted = true;
        self.counters.displayed += 1;
    }

    /// Judge everything the newly applied frame is entitled to judge. A
    /// confirmed prediction retires and lengthens the streak; a contradicted one
    /// takes the WHOLE queue, because every prediction made after a wrong one was
    /// made against a screen that never existed.
    pub fn reconcile(self: *Overlay, reader: anytype, applied_seq: u64, now_ms: i64) Verdict {
        var verdict: Verdict = .none;
        var i: usize = 0;
        while (i < self.pending.items.len) {
            const p = self.pending.items[i];
            if (p.made_seq >= applied_seq) {
                i += 1; // too new to be evidence about
                continue;
            }
            const shown = reader.cellChar(p.cell.row, p.cell.col);

            if (shown != null and shown.? == p.cell.ch) {
                _ = self.pending.orderedRemove(i);
                self.counters.confirmed += 1;
                self.samplePath(now_ms -| p.made_ms);
                self.streak +|= 1;
                if (self.ctx == .adaptive and self.streak >= promote_after) {
                    self.confident = true;
                }
                verdict = .confirmed;
                continue; // index i now holds the next prediction
            }

            if (shown != null and shown.? == p.prev_ch) {
                // Silence, not disagreement. This frame was almost certainly
                // built before the keystroke landed — which over any real
                // path is the ordinary case for everything typed after the
                // first character of a burst.
                self.pending.items[i].frames +|= 1;
                if (self.pending.items[i].frames >= expire_after_frames or
                    now_ms -| p.made_ms >= expire_after_ms)
                {
                    self.counters.expired += 1;
                    self.noteSeq(applied_seq);
                    return self.abandonAll();
                }
                i += 1;
                continue;
            }

            // The cell moved to something that is neither our guess nor what
            // was there before: somebody else wrote it, and every prediction
            // behind this one was made against a screen that never existed.
            self.noteSeq(applied_seq);
            return self.abandonAll();
        }
        self.noteSeq(applied_seq);
        return verdict;
    }

    /// An app that swallows a keystroke then goes quiet produces no more
    /// frames, so `reconcile` never runs again and the phantom sits there
    /// forever. The client calls this from its idle path.
    pub fn expire(self: *Overlay, now_ms: i64) Verdict {
        for (self.pending.items) |p| {
            if (now_ms -| p.made_ms < expire_after_ms) continue;
            self.counters.expired += 1;
            return self.abandonAll();
        }
        return .none;
    }

    /// Drop everything and stop trusting ourselves: the shared tail of a
    /// contradiction and an expiry, which differ only in what made them
    /// necessary. Callers repaint in full.
    fn abandonAll(self: *Overlay) Verdict {
        self.counters.contradicted += 1;
        self.counters.abandoned += self.pending.items.len;
        // Re-earning display costs the full promote_after again. Without
        // this the streak survives the demotion, one confirm re-promotes,
        // and leg 3 of the criterion — display stops until it is earned back
        // — quietly becomes display stops for one keystroke.
        self.streak = 0;
        // Demotion is an adaptive-only idea. In canonical echo the pty is
        // going to print the character whatever we believe, so a
        // disagreement means we put it in the wrong place, not that we
        // should stop predicting.
        if (self.ctx == .adaptive) self.confident = false;
        self.pending.clearRetainingCapacity();
        return .contradicted;
    }

    /// Snapshot, resize, scroll mode, reconnect: nothing here is wrong, so
    /// confidence and the streak survive.
    pub fn flush(self: *Overlay) void {
        self.counters.abandoned += self.pending.items.len;
        self.pending.clearRetainingCapacity();
    }

    /// Where the cursor appears to be, given what is queued: the
    /// authoritative position advanced past the last pending prediction.
    /// With nothing pending it is the daemon's own answer, untouched.
    pub fn predictedCursor(self: *const Overlay, base: CursorPos) CursorPos {
        const last = self.pending.getLastOrNull() orelse return base;
        return .{ .x = last.cell.col + 1, .y = last.cell.row };
    }

    pub fn pendingCount(self: *const Overlay) usize {
        return self.pending.items.len;
    }

    /// By value: the queue reallocates, so a slice would go stale on the
    /// next keystroke.
    pub fn pendingAt(self: *const Overlay, i: usize) Pred {
        return self.pending.items[i];
    }
};

// ---- tests ------------------------------------------------------------

/// A grid row set built the way a plain dump arrives: rows joined by '\n',
/// trailing blanks absent. Caller frees.
fn plainOf(alloc: std.mem.Allocator, rows: []const []const u8) ![]u8 {
    return std.mem.join(alloc, "\n", rows);
}

/// Type one character into a cell that is currently blank — the ordinary
/// case, and the one most of these tests are about.
fn typeAt(ov: *Overlay, x: u16, y: u16, ch: u8) Outcome {
    return ov.predictAt(.{
        .cursor = .{ .x = x, .y = y },
        .ch = ch,
        .prev_ch = ' ',
        .now_ms = 0,
    });
}

/// A frame from a remote path. Tests about time or the gate use `seeRowsAt`.
fn seeRows(
    alloc: std.mem.Allocator,
    ov: *Overlay,
    rows: []const []const u8,
    seq: u64,
) !Verdict {
    // 100ms after the keystroke, not 0: at 0 every confirm is a measured
    // 0ms round trip, the local gate closes, and every `.display` the
    // policy tests assert goes `.hidden` for a reason they are not about.
    return seeRowsAt(alloc, ov, rows, seq, 100);
}

fn seeRowsAt(
    alloc: std.mem.Allocator,
    ov: *Overlay,
    rows: []const []const u8,
    seq: u64,
    now_ms: i64,
) !Verdict {
    const text = try plainOf(alloc, rows);
    defer alloc.free(text);
    return ov.reconcile(PlainGrid{ .text = text, .cols = 80 }, seq, now_ms);
}

test "setMode maps the pty's two bits onto the three policies" {
    const alloc = std.testing.allocator;
    const cases = [_]struct { icanon: bool, echo: bool, want: Context }{
        // Canonical and echoing: the tty itself will put the character on
        // the screen, so predicting it is not a guess at all.
        .{ .icanon = true, .echo = true, .want = .always },
        // Canonical and silent: a password prompt. Nothing may be shown.
        .{ .icanon = true, .echo = false, .want = .never },
        // Raw: the application decides what a keystroke looks like, and the
        // only way to find out is to be right about it repeatedly.
        .{ .icanon = false, .echo = true, .want = .adaptive },
        .{ .icanon = false, .echo = false, .want = .adaptive },
    };
    for (cases) |c| {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = c.icanon, .echo = c.echo });
        try std.testing.expectEqual(c.want, ov.ctx);
    }
}

test "a mode byte carrying a bit we do not understand predicts nothing" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    try std.testing.expectEqual(Context.always, ov.ctx);

    // A future daemon defines a third bit. Read as canonical-and-echoing
    // with the extra bit masked away, this would keep predicting against a
    // terminal whose description we have only partly understood.
    ov.setMode(.{ .icanon = true, .echo = true, ._pad = 1 });
    try std.testing.expectEqual(Context.never, ov.ctx);
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .suppressed);
}

test "an overlay predicts nothing until it has been told what the pty is" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    // No pty_mode frame has arrived (or the daemon is too old to send one).
    // No frame, no prediction: the safe direction is the default one.
    try std.testing.expectEqual(Context.never, ov.ctx);
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .suppressed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
}

test "always: the first keystroke paints, with no evidence required" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    const out = typeAt(&ov, 3, 2, 'k');
    try std.testing.expect(out == .display);
    try std.testing.expectEqual(@as(u16, 3), out.display.col);
    try std.testing.expectEqual(@as(u16, 2), out.display.row);
    try std.testing.expectEqual(@as(u8, 'k'), out.display.ch);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.made);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed);
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());
}

test "never: nothing is made, so nothing can leak" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = false });

    for ("hunter2") |ch| {
        try std.testing.expect(typeAt(&ov, 0, 0, ch) == .suppressed);
    }
    // Both counters, deliberately: leg 3 of the criterion asserts made as
    // well as displayed, because "made but hidden" in a password context
    // would still put the password in a buffer the overlay paints from.
    try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed);
    try std.testing.expectEqual(@as(u64, 7), ov.counters.suppressed);
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
}

test "a burst outruns the round trip without refuting itself" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // Three characters typed faster than the daemon can answer.
    try std.testing.expect(typeAt(&ov, 0, 0, 'h') == .display);
    try std.testing.expect(typeAt(&ov, 1, 0, 'e') == .display);
    try std.testing.expect(typeAt(&ov, 2, 0, 'l') == .display);

    // The first frame back was built when the daemon had seen only 'h'. The
    // other two cells are still blank, which is what they were when we predicted
    // — so this frame said nothing about them, and reading that silence as
    // disagreement flushes the queue once per round trip.
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"h"}, 1));
    try std.testing.expectEqual(@as(usize, 2), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);

    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"he"}, 2));
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());

    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"hel"}, 3));
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());

    // Every one of them landed, and nothing was ever called wrong.
    try std.testing.expectEqual(@as(u64, 3), ov.counters.confirmed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.expired);
}

test "predicting what is already on the screen is a no-op, and refused" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // Painting an 'a' over an 'a' changes nothing, and the prediction could
    // never be judged: "still shows 'a'" would be both the confirmation and
    // the no-evidence-yet answer, which are the two things reconcile exists
    // to tell apart.
    const out = ov.predictAt(.{
        .cursor = .{ .x = 0, .y = 0 },
        .ch = 'a',
        .prev_ch = 'a',
        .now_ms = 0,
    });
    try std.testing.expect(out == .suppressed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.suppressed);
}

test "adaptive earns the right to display, one confirm at a time" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    // Written out with literal counts rather than a loop over promote_after:
    // a loop parameterised by the constant moves its own goalposts when the
    // constant is mutated, and would have passed at 1 and at 3 alike.
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));

    // Still hidden: one confirm is not two.
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));

    try std.testing.expectEqual(@as(u64, 2), ov.counters.confirmed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed);

    // Two consecutive confirms, and the third keystroke paints.
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .display);
    try std.testing.expectEqual(@as(u64, 3), ov.counters.made);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed);
}

test "a path that answers faster than the eye can see earns display and still hides" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    // Two confirms, each 1ms after its keystroke: a unix socket to a daemon
    // on the same box. Confidence is earned exactly as over a WAN...
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"a"}, 1, 1));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"ab"}, 2, 1));
    try std.testing.expect(ov.confident);

    // ...and the third keystroke is queued but NOT shown: a prediction the
    // real glyph overtakes within a frame can only ever be seen when it is
    // wrong. `displayed` stays at zero, which is what the repro counted.
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .hidden);
    try std.testing.expect(!ov.visible());
    try std.testing.expectEqual(@as(u64, 3), ov.counters.made);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.local);
}

test "the local gate has hysteresis, and an unmeasured path is not local" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });
    // Before any confirm there is no measurement, and the WAN behaviour
    // stands unchanged: earn display, show.
    try std.testing.expect(!ov.local);
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"a"}, 1, 150));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"ab"}, 2, 150));
    try std.testing.expect(ov.visible());
    try std.testing.expectEqual(@as(i64, 150), ov.srtt_ms.?);

    // One fast confirm does not flip it: the estimate is smoothed
    // (150 - 150/8 = 132), and nothing above `local_below_ms` turns it off.
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .display);
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"abc"}, 3, 0));
    try std.testing.expectEqual(@as(i64, 132), ov.srtt_ms.?);
    try std.testing.expect(ov.visible());

    // A run of fast confirms does. The keystroke clock stays at 0 (typeAt),
    // so each confirm at 0 is a 0ms round trip.
    var col: u16 = 3;
    var row = [_]u8{ 'a', 'b', 'c' } ++ [_]u8{' '} ** 60;
    while (ov.visible()) : (col += 1) {
        row[col] = 'x';
        _ = typeAt(&ov, col, 0, 'x');
        try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{row[0 .. col + 1]}, col + 1, 0));
    }
    try std.testing.expect(ov.local);
    try std.testing.expect(ov.srtt_ms.? <= local_below_ms);

    // Back above the upper trigger and it shows again; the band between the
    // two is where a jittery path would otherwise flap every keystroke.
    while (!ov.visible()) : (col += 1) {
        row[col] = 'x';
        _ = typeAt(&ov, col, 0, 'x');
        try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{row[0 .. col + 1]}, col + 1, 400));
    }
    try std.testing.expect(ov.srtt_ms.? >= local_above_ms);
}

test "a mode change keeps the path estimate: the wire did not move" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });
    _ = typeAt(&ov, 0, 0, 'a');
    try std.testing.expectEqual(Verdict.confirmed, try seeRowsAt(alloc, &ov, &.{"a"}, 1, 1));
    try std.testing.expect(ov.local);
    // readline hands the tty back and forth around every command; the
    // daemon is no further away afterwards.
    ov.setMode(.{ .icanon = true, .echo = true });
    try std.testing.expect(ov.local);
    try std.testing.expect(!ov.visible());
}

test "a contradiction flushes the whole queue, not merely the cell that was wrong" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    _ = typeAt(&ov, 0, 0, 'a');
    _ = typeAt(&ov, 1, 0, 'b');
    _ = typeAt(&ov, 2, 0, 'c');
    try std.testing.expectEqual(@as(usize, 3), ov.pendingCount());

    // Somebody else wrote the FIRST cell: it holds neither our guess nor the
    // blank that was there, so this is refutation and not silence. The two
    // behind it would each have matched, which is the point.
    try std.testing.expectEqual(
        Verdict.contradicted,
        try seeRows(alloc, &ov, &.{"xbc"}, 1),
    );

    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 1), ov.counters.contradicted);
    // Zero, not two. An implementation that retired only the wrong cell and
    // carried on judging would count the other two as confirmed, and would
    // be claiming agreement about a screen it had already been told it was
    // wrong about.
    try std.testing.expectEqual(@as(u64, 0), ov.counters.confirmed);
    // A refutation is not an expiry, and the counters read these apart.
    try std.testing.expectEqual(@as(u64, 0), ov.counters.expired);
}

test "a prediction nothing ever answers is given up on, by frame count" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // The nvim case: a keystroke the application consumes silently. The
    // cell it was predicted into never changes, so no frame ever confirms
    // or refutes it, and the glyph we painted would stay there forever.
    _ = typeAt(&ov, 0, 0, 'j');

    // Seven frames of silence are patience, not evidence. Literal counts,
    // for the same reason as promote_after: a loop over the constant would
    // pass at any value of it.
    var n: u64 = 1;
    while (n <= 7) : (n += 1) {
        try std.testing.expectEqual(Verdict.none, try seeRows(alloc, &ov, &.{""}, n));
        try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());
    }

    // The eighth is where patience runs out.
    try std.testing.expectEqual(Verdict.contradicted, try seeRows(alloc, &ov, &.{""}, 8));
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 1), ov.counters.expired);
    // Counted as a contradiction too: evidentially that is what it is, and
    // leg 2 of the criterion asks that no abandoned prediction outlive the
    // frame that abandoned it, however it was abandoned.
    try std.testing.expectEqual(@as(u64, 1), ov.counters.contradicted);
}

test "a prediction nothing ever answers is given up on, by the clock" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    _ = typeAt(&ov, 0, 0, 'j'); // made at now_ms = 0

    // A frame short of the deadline leaves it alone...
    try std.testing.expectEqual(
        Verdict.none,
        try seeRowsAt(alloc, &ov, &.{""}, 1, 999),
    );
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());

    // ...and one at it does not. The clock comes from the caller, so this
    // deadline is exercised without anything here reading a real one.
    try std.testing.expectEqual(
        Verdict.contradicted,
        try seeRowsAt(alloc, &ov, &.{""}, 2, 1000),
    );
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 1), ov.counters.expired);
}

test "expire gives up on a silent prediction with no frame to prompt it" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    _ = typeAt(&ov, 0, 0, 'j');

    // The frame bound cannot reach the case that motivates the guard: an
    // application that swallows the keystroke and then says nothing at all
    // produces no further frames, so reconcile is never called again. This
    // is the client's idle path, and without it the phantom is permanent.
    try std.testing.expectEqual(Verdict.none, ov.expire(999));
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());

    try std.testing.expectEqual(Verdict.contradicted, ov.expire(1000));
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 1), ov.counters.expired);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.contradicted);
}

test "a promotion mid-queue counts the predictions it makes visible, once each" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    // One confirm banked, so the overlay is one short of promotion.
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));

    // Two more typed while still invisible — and the second confirmation
    // arrives while the last of them is still outstanding.
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .hidden);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.displayed);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));
    try std.testing.expect(ov.confident);
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());

    // The next repaint draws 'c', which was queued invisible and has just
    // become visible without anybody typing anything. It reached the screen,
    // so it counts.
    ov.markPainted(0);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed);

    // Every subsequent repaint redraws the same cell — a delta rewrites the
    // row and the overlay is re-laid on top — and none of them is a second
    // arrival on screen.
    ov.markPainted(0);
    ov.markPainted(0);
    try std.testing.expectEqual(@as(u64, 1), ov.counters.displayed);
    try std.testing.expect(ov.counters.displayed <= ov.counters.made);
}

test "a refusal the caller made on its own authority still counts" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // The client declines chunks the overlay never sees — a paste, whose
    // lead byte is printable and would otherwise be predicted as though it
    // were a keystroke. The decision is the client's; the count belongs
    // with every other refusal, or the numbers disagree with the behaviour.
    ov.recordSuppressed();
    try std.testing.expectEqual(@as(u64, 1), ov.counters.suppressed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.made);
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());

    // And it is the same counter predictAt's own refusals land in.
    try std.testing.expect(typeAt(&ov, 0, 0, 0x1b) == .suppressed);
    try std.testing.expectEqual(@as(u64, 2), ov.counters.suppressed);
}

test "stale frames age nothing: only a frame that could have seen it counts" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    ov.noteSeq(10);
    _ = typeAt(&ov, 0, 0, 'j');

    // Eight frames the daemon built before it could have seen the keystroke.
    // The expiry bound counts evidence, and these are not evidence: age a
    // prediction on frames that predate it and a busy session kills its own
    // predictions faster the more output it produces.
    var n: usize = 0;
    while (n < 8) : (n += 1) {
        try std.testing.expectEqual(Verdict.none, try seeRows(alloc, &ov, &.{""}, 10));
        try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());
    }
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);

    // The first frame that could have seen it is the first one that ages it,
    // so the prediction is still alive here rather than eight frames stale.
    try std.testing.expectEqual(Verdict.none, try seeRows(alloc, &ov, &.{""}, 11));
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 0), ov.counters.expired);
}

test "a flush costs the queue but never the confidence that was earned" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));
    try std.testing.expect(ov.confident);
    try std.testing.expectEqual(@as(u8, 2), ov.streak);

    _ = typeAt(&ov, 2, 0, 'c');
    ov.flush();

    // The confirmations that earned display were real, and a resize is not
    // evidence against them. Demote here and every window resize would cost
    // the next two keystrokes their visibility.
    try std.testing.expect(ov.confident);
    try std.testing.expectEqual(@as(u8, 2), ov.streak);
    try std.testing.expect(typeAt(&ov, 3, 0, 'd') == .display);
}

test "every prediction is accounted for: made = confirmed + abandoned + pending" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // One confirmed...
    _ = typeAt(&ov, 0, 0, 'a');
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));

    // ...two thrown away by a refutation, which is ONE event...
    _ = typeAt(&ov, 1, 0, 'b');
    _ = typeAt(&ov, 2, 0, 'c');
    try std.testing.expectEqual(Verdict.contradicted, try seeRows(alloc, &ov, &.{"aZ"}, 2));
    try std.testing.expectEqual(@as(u64, 1), ov.counters.contradicted);
    try std.testing.expectEqual(@as(u64, 2), ov.counters.abandoned);

    // ...one thrown away by a flush, which is counted nowhere else...
    _ = typeAt(&ov, 1, 0, 'd');
    ov.flush();
    try std.testing.expectEqual(@as(u64, 3), ov.counters.abandoned);
    // ...and a flush is still not an accusation.
    try std.testing.expectEqual(@as(u64, 1), ov.counters.contradicted);

    // ...leaving one outstanding.
    _ = typeAt(&ov, 1, 0, 'e');

    const c = ov.counters;
    try std.testing.expectEqual(@as(u64, 5), c.made);
    try std.testing.expectEqual(
        c.made,
        c.confirmed + c.abandoned + @as(u64, ov.pendingCount()),
    );
}

test "an expiry demotes adaptive exactly as a refutation would" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));
    try std.testing.expect(ov.confident);

    // A keystroke the application swallows: the confidence that earned
    // display was evidence about a mode the application has left.
    _ = typeAt(&ov, 2, 0, 'c');
    try std.testing.expectEqual(Verdict.contradicted, ov.expire(1000));
    try std.testing.expect(!ov.confident);
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .hidden);
}

test "adaptive is demoted by one contradiction and must earn display again" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = false, .echo = false });

    // Earn it.
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .display);

    // Lose it: the application put its own content in the cell we drew in,
    // which is a refutation and not silence.
    try std.testing.expectEqual(
        Verdict.contradicted,
        try seeRows(alloc, &ov, &.{"abX"}, 3),
    );
    try std.testing.expect(!ov.confident);

    // ...and the very next keystroke is invisible again. One contradicted
    // prediction, one demotion: that is leg 3 of the criterion.
    try std.testing.expect(typeAt(&ov, 3, 0, 'd') == .hidden);

    // Re-earning costs the FULL `promote_after`, not one confirm: otherwise
    // "display stops until it is earned again" means "for one keystroke", and an
    // application that contradicts every other one paints half of them wrong.
    try std.testing.expectEqual(@as(u8, 0), ov.streak);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"abXd"}, 4));
    try std.testing.expect(!ov.confident);
    try std.testing.expect(typeAt(&ov, 4, 0, 'e') == .hidden);

    // The second confirmation is the one that earns it back.
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"abXde"}, 5));
    try std.testing.expect(ov.confident);
    try std.testing.expect(typeAt(&ov, 5, 0, 'f') == .display);
}

test "always is never demoted: a contradiction costs the queue, not the policy" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    _ = typeAt(&ov, 0, 0, 'a');
    try std.testing.expectEqual(Verdict.contradicted, try seeRows(alloc, &ov, &.{"z"}, 1));
    // In canonical echo the pty is going to print the character whatever we
    // do, so a disagreement means we mis-placed it, not that we should stop
    // predicting. Confidence here is not earned and cannot be lost.
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .display);
}

test "predictAt refuses everything the plan says it must" {
    const alloc = std.testing.allocator;

    // Non-printables: control bytes carry meaning we cannot render, and the
    // high half is a multi-byte sequence whose width we do not know.
    for ([_]u8{ 0x00, 0x08, 0x09, 0x0a, 0x0d, 0x1b, 0x7f, 0x80, 0xc3, 0xff }) |ch| {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = true, .echo = true });
        try std.testing.expect(typeAt(&ov, 0, 0, ch) == .suppressed);
        try std.testing.expectEqual(@as(u64, 1), ov.counters.suppressed);
    }

    // The last column: what happens there is the application's policy
    // (wrap, scroll, truncate, refuse) and we do not get to guess it.
    {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = true, .echo = true });
        try std.testing.expect(typeAt(&ov, 78, 0, 'a') == .display);
        try std.testing.expect(typeAt(&ov, 79, 0, 'a') == .suppressed);
    }

    // Off the grid entirely.
    {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = true, .echo = true });
        try std.testing.expect(typeAt(&ov, 0, 24, 'a') == .suppressed);
    }

    // Scrolled back: the cursor is not where the user is looking, so a
    // prediction painted at it would land in the middle of history.
    {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = true, .echo = true });
        ov.setScrollMode(true);
        try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .suppressed);
        ov.setScrollMode(false);
        try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .display);
    }

    // A resize we have asked for but not yet been answered about: the grid
    // the prediction would be painted on is about to stop existing.
    {
        var ov = Overlay.init(alloc, 80, 24);
        defer ov.deinit();
        ov.setMode(.{ .icanon = true, .echo = true });
        ov.setResizePending(true);
        try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .suppressed);
        ov.setResizePending(false);
        try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .display);
    }
}

test "a frame that cannot have seen the keystroke does not get to judge it" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // The client is holding seq 7 when the key is pressed.
    ov.noteSeq(7);
    _ = typeAt(&ov, 0, 0, 'a');

    // A frame numbered 7 is the one we already had. Even though it carries
    // content that WOULD refute the prediction, it is not evidence about a
    // keystroke made after it, and must not be read as any.
    try std.testing.expectEqual(Verdict.none, try seeRows(alloc, &ov, &.{"Z"}, 7));
    try std.testing.expectEqual(@as(usize, 1), ov.pendingCount());
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.confirmed);

    // Seq 8 is the first frame the daemon could have built after seeing it.
    try std.testing.expectEqual(
        Verdict.contradicted,
        try seeRows(alloc, &ov, &.{"Z"}, 8),
    );
}

test "flush drops predictions without calling any of them wrong" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    _ = typeAt(&ov, 0, 0, 'a');
    _ = typeAt(&ov, 1, 0, 'b');
    ov.flush();

    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    // A snapshot, a resize or a reconnect is not evidence that a prediction
    // was mistaken — it is evidence that we can no longer find out. Counting
    // it as a contradiction would make the demotion machinery fire on a
    // window resize.
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.confirmed);
    try std.testing.expectEqual(@as(u64, 0), ov.counters.expired);
}

test "a flush leaves scroll mode exactly where it found it" {
    // `flush` empties the queue; it does not decide where the viewport is, and
    // must not, because every snapshot flushes and a snapshot is no reason to
    // leave history. So leaving scroll mode is the CLIENT's job on every path —
    // an overlay left scrolled suppresses every keystroke for the session.
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    ov.setScrollMode(true);
    ov.flush();
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .suppressed);

    // And the mirror: a flush does not switch it ON either, so an ordinary
    // reconnect on a live screen keeps predicting.
    ov.setScrollMode(false);
    ov.flush();
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .display);
}

test "the same mode bits again cost nothing" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    _ = typeAt(&ov, 0, 0, 'a');
    _ = typeAt(&ov, 1, 0, 'b');

    // A reattach re-states the mode, and at an idle prompt the daemon's
    // poll re-reads bits that have not moved. Neither is a transition, and
    // treating them as one would throw away predictions that are still
    // perfectly good — on every reconnect.
    ov.setMode(.{ .icanon = true, .echo = true });
    try std.testing.expectEqual(@as(usize, 2), ov.pendingCount());
}

test "any move in the bits flushes and un-earns display, tier or no tier" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();

    // Into a password prompt with predictions outstanding: they go.
    ov.setMode(.{ .icanon = true, .echo = true });
    _ = typeAt(&ov, 0, 0, 'a');
    ov.setMode(.{ .icanon = true, .echo = false });
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());

    // The case a tier comparison misses: both are raw mode, so the policy tier
    // does not change — but the application has taken the terminal somewhere
    // else, and predictions made before are about a screen that has moved on.
    ov.setMode(.{ .icanon = false, .echo = false });
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"a"}, 1));
    try std.testing.expect(typeAt(&ov, 1, 0, 'b') == .hidden);
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"ab"}, 2));
    try std.testing.expect(typeAt(&ov, 2, 0, 'c') == .display);
    try std.testing.expect(ov.confident);

    // Raw to raw, one bit different.
    ov.setMode(.{ .icanon = false, .echo = true });
    try std.testing.expectEqual(Context.adaptive, ov.ctx);
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expect(!ov.confident);
    try std.testing.expectEqual(@as(u8, 0), ov.streak);
    try std.testing.expect(typeAt(&ov, 3, 0, 'd') == .hidden);

    // Flushing is not an accusation: nothing here was contradicted.
    try std.testing.expectEqual(@as(u64, 0), ov.counters.contradicted);
}

test "entering raw mode starts unconfident however confident we just were" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    try std.testing.expect(ov.confident);

    ov.setMode(.{ .icanon = false, .echo = false });
    try std.testing.expect(!ov.confident);
    try std.testing.expect(typeAt(&ov, 0, 0, 'a') == .hidden);
}

test "a resize flushes and moves the edge the last column is measured from" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });
    _ = typeAt(&ov, 0, 0, 'a');

    ov.setGrid(40, 12);
    try std.testing.expectEqual(@as(usize, 0), ov.pendingCount());
    try std.testing.expect(typeAt(&ov, 39, 0, 'a') == .suppressed);
    try std.testing.expect(typeAt(&ov, 38, 0, 'a') == .display);

    // The same size again is not a resize and costs nothing.
    _ = typeAt(&ov, 0, 1, 'b');
    const before = ov.pendingCount();
    ov.setGrid(40, 12);
    try std.testing.expectEqual(before, ov.pendingCount());
}

test "predictedCursor advances the base past everything pending" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // With nothing pending the cursor is the daemon's, untouched.
    try std.testing.expectEqual(
        CursorPos{ .x = 5, .y = 1 },
        ov.predictedCursor(.{ .x = 5, .y = 1 }),
    );

    var cur = CursorPos{ .x = 5, .y = 1 };
    for ("abc") |ch| {
        _ = typeAt(&ov, cur.x, cur.y, ch);
        cur = ov.predictedCursor(.{ .x = 5, .y = 1 });
    }
    try std.testing.expectEqual(CursorPos{ .x = 8, .y = 1 }, cur);

    // And it comes back to the authoritative cursor when the queue empties.
    ov.flush();
    try std.testing.expectEqual(
        CursorPos{ .x = 5, .y = 1 },
        ov.predictedCursor(.{ .x = 5, .y = 1 }),
    );
}

test "a queued prediction owns its byte; the caller's buffer may be reused" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // Exactly the shape of the client's stdin path: bytes are read into a
    // buffer that is about to be read into again. Anything the overlay keeps
    // pointing at that buffer is the UAF-that-never-crashes.
    const src = try alloc.alloc(u8, 3);
    defer alloc.free(src);
    @memcpy(src, "abc");
    for (src, 0..) |ch, i| {
        _ = typeAt(&ov, @intCast(i), 0, ch);
    }
    @memset(src, 0xFF);

    try std.testing.expectEqual(@as(u8, 'a'), ov.pendingAt(0).cell.ch);
    try std.testing.expectEqual(@as(u8, 'b'), ov.pendingAt(1).cell.ch);
    try std.testing.expectEqual(@as(u8, 'c'), ov.pendingAt(2).cell.ch);

    // And the judgement is made against what was typed, not against
    // whatever the buffer holds by the time the frame comes back.
    try std.testing.expectEqual(Verdict.confirmed, try seeRows(alloc, &ov, &.{"abc"}, 1));
    try std.testing.expectEqual(@as(u64, 3), ov.counters.confirmed);
}

test "the queue survives its own growth" {
    const alloc = std.testing.allocator;
    var ov = Overlay.init(alloc, 80, 24);
    defer ov.deinit();
    ov.setMode(.{ .icanon = true, .echo = true });

    // Well past any initial capacity, so the backing array is reallocated
    // several times underneath the predictions already in it.
    var i: u16 = 0;
    while (i < 70) : (i += 1) {
        const ch: u8 = 'a' + @as(u8, @intCast(i % 26));
        try std.testing.expect(typeAt(&ov, i, 0, ch) == .display);
    }
    try std.testing.expectEqual(@as(usize, 70), ov.pendingCount());

    i = 0;
    while (i < 70) : (i += 1) {
        const want: u8 = 'a' + @as(u8, @intCast(i % 26));
        try std.testing.expectEqual(want, ov.pendingAt(i).cell.ch);
        try std.testing.expectEqual(i, ov.pendingAt(i).cell.col);
    }
}

test "PlainGrid reads a cell out of a dump, and blanks where the dump stops" {
    const alloc = std.testing.allocator;
    const text = try plainOf(alloc, &.{ "ab", "cd" });
    defer alloc.free(text);
    const g = PlainGrid{ .text = text, .cols = 80 };

    try std.testing.expectEqual(@as(?u8, 'a'), g.cellChar(0, 0));
    try std.testing.expectEqual(@as(?u8, 'b'), g.cellChar(0, 1));
    try std.testing.expectEqual(@as(?u8, 'c'), g.cellChar(1, 0));
    try std.testing.expectEqual(@as(?u8, 'd'), g.cellChar(1, 1));

    // A dump carries no trailing blanks, so a cell past the end of a row —
    // or past the last row — is a blank cell, not a missing one.
    try std.testing.expectEqual(@as(?u8, ' '), g.cellChar(0, 2));
    try std.testing.expectEqual(@as(?u8, ' '), g.cellChar(9, 0));

    // Outside the grid is a different answer: nothing to compare against.
    try std.testing.expectEqual(@as(?u8, null), g.cellChar(0, 80));
}

// Forces semantic analysis of every pub decl under `zig build test`, so an
// unreferenced decl must at least compile (the silent-module-loss hazard,
// decisions.md). Pub decls only: std.meta.declarations sees nothing private.
test {
    std.testing.refAllDeclsRecursive(@This());
}