Ctrl K
ComunidadGuías…

tools/frontend_check.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
/* Pure front-end logic, tested without a browser.
 *
 * The panel scripts have no build step and no module system, which is the point
 * — and it also means the functions in them have nowhere to be tested from. Most of the
 * file is DOM work that only makes sense in a page, but the decisions are not:
 * what needs you first, and what counts as unread, are sorts over plain data,
 * and they are exactly the kind of thing that breaks quietly.
 *
 * So: read the file, cut out the region under test, eval it against stubs for
 * the handful of things it reads. No jsdom, no bundler, no package.json — the
 * same bargain the rest of the project makes.
 *
 *     node tools/frontend_check.js
 */

const fs = require("fs");
const path = require("path");

const ROOT = path.resolve(__dirname, "..");
const SRC = ["file-preview.js", "providers.js", "panes.js", "review.js", "app.js"]
  .map((name) => fs.readFileSync(path.join(ROOT, "clique/web", name), "utf8"))
  .join("\n");

let passed = 0;
let failed = 0;

function check(label, cond, detail) {
  if (cond) { passed++; console.log(`  ok   ${label}`); }
  else { failed++; console.log(`  FAIL ${label} ${detail === undefined ? "" : detail}`); }
}

/* A named region of the panel scripts, so a test names the code it covers rather than a
 * line number that moves the next time anything is inserted above it. */
function region(from, to) {
  const start = SRC.indexOf(from);
  const end = SRC.indexOf(to);
  if (start < 0 || end < 0 || end <= start) {
    console.log(`  FAIL could not find the region ${from} … ${to}`);
    failed++;
    return "";
  }
  return SRC.slice(start, end);
}

console.log("what needs you first");
{
  const code = region("const NEXT_RANK", "/* Long press, because touch");

  let state;
  let activeId = null;
  const document = { hasFocus: () => true };
  const workState = (s) => {
    if (!s.alive) return "stopped";
    if (s.signal === "error") return "error";
    if (s.signal === "waiting") return "asking";
    if (s.busy) return "working";
    return "idle";
  };
  const unread = (s) => Boolean(s.alive && s.id !== activeId && s.activity > (s.last_seen || 0));
  const ago = () => "11m";
  void document; void workState; void unread; void ago;

  // `eval` of a block that declares functions puts them in this scope, so the
  // region is evaluated and then referenced — declaring a holder first would
  // collide with the declaration inside the code being tested.
  eval(code);

  const now = Math.floor(Date.now() / 1000);
  state = { sessions: [
    { id: "a", name: "fresh-error",   alive: 1, signal: "error",   activity: now - 5,   last_seen: now },
    { id: "b", name: "old-waiting",   alive: 1, signal: "waiting", activity: now - 660, last_seen: now },
    { id: "c", name: "new-waiting",   alive: 1, signal: "waiting", activity: now - 10,  last_seen: now },
    { id: "d", name: "unread-only",   alive: 1, signal: "",        activity: now - 300, last_seen: now - 900 },
    { id: "e", name: "busy",          alive: 1, signal: "", busy: 1, activity: now,     last_seen: 0 },
    { id: "f", name: "idle-and-read", alive: 1, signal: "",        activity: now - 90,  last_seen: now },
    { id: "g", name: "dead",          alive: 0, signal: "error",   activity: now - 30,  last_seen: now },
    { id: "h", name: "archived",      alive: 1, archived: 1, signal: "waiting", activity: now - 60, last_seen: now },
  ]};

  const got = nextUp();
  const named = (n) => got.findIndex((r) => r.s.name === n);
  const has = (n) => named(n) >= 0;

  // Stopped badly outranks blocked, even when the blocked one has been blocked
  // for eleven minutes: an error is not going to resolve itself.
  check("an error outranks a much older waiting", got[0] && got[0].s.name === "fresh-error",
        got.map((r) => r.s.name).join(","));
  check("among equals, the one blocked longest comes first",
        named("old-waiting") < named("new-waiting"));
  check("unread ranks below both", named("unread-only") === got.length - 1);

  // The list has to be short to be worth reading, so everything that does not
  // need a person is absent rather than ranked low.
  check("a working session never appears", !has("busy"));
  check("nor one that is idle and already read", !has("idle-and-read"));
  check("nor a dead one", !has("dead"));
  check("nor an archived one", !has("archived"));

  activeId = "b";
  check("nor the one you are already looking at", !nextUp().some((r) => r.s.id === "b"));
}

console.log("directories the panel already knows");
{
  const code = region("const CWD_SUGGESTIONS", "function openModal()");

  let state;
  let resumable;
  const shortPath = (p) => p;
  void shortPath;

  eval(code);

  const now = Math.floor(Date.now() / 1000);
  state = { sessions: [
    { cwd: "/srv/old",   alive: 0, last_seen: now - 10 },
    { cwd: "/srv/live",  alive: 1, last_seen: now - 5000 },
    { cwd: "/srv/dupe",  alive: 0, last_seen: now - 900 },
    { cwd: "/srv/dupe",  alive: 1, last_seen: now - 9000 },
    { cwd: "/srv/gone",  alive: 0, last_seen: now, archived: 1 },
    { cwd: "",           alive: 1, last_seen: now },
  ]};
  resumable = [{ cwd: "/srv/history", updated: now }, { cwd: "/srv/live", updated: now }];

  const dirs = knownDirs().map((d) => d.cwd);
  const kindOf = (cwd) => (knownDirs().find((d) => d.cwd === cwd) || {}).kind;
  // A live session is where you are working, so it beats a more recent look at
  // something that has since stopped.
  check("a running directory outranks a recently-viewed dead one",
        dirs.indexOf("/srv/live") < dirs.indexOf("/srv/old"), dirs.join(","));
  check("a directory appears once however many sessions are in it",
        dirs.filter((d) => d === "/srv/dupe").length === 1, dirs.join(","));
  check("and it is ranked by its best session, not its worst",
        dirs.indexOf("/srv/dupe") < dirs.indexOf("/srv/old"), dirs.join(","));
  check("past conversations are offered too", dirs.includes("/srv/history"));
  check("but rank below anything with a session", dirs.indexOf("/srv/history") > 0, dirs.join(","));
  check("archived is not somewhere you are working", !dirs.includes("/srv/gone"));
  check("and an empty path is never offered", !dirs.includes(""));

  // The groups the picker draws. A directory with a live session is "running
  // now" whatever else it also is, which is why the kind travels with the
  // winning score rather than being decided afterwards.
  check("a live session is grouped as running", kindOf("/srv/live") === "running");
  check("a stopped one as recent", kindOf("/srv/old") === "recent");
  check("and one only history knows about as history",
        kindOf("/srv/history") === "history");
  check("a directory with both is running, not recent", kindOf("/srv/dupe") === "running");
}

console.log("which app is holding this page");
{
  const code = region("function shellLabel", "function renderVersion");
  const { shellLabel } = new Function(code + "; return { shellLabel };")();

  check("a desktop shell is named with its version",
        shellLabel({ kind: "desktop", version: "0.3.12" }) === "desktop 0.3.12");
  check("a browser has no shell, so nothing is drawn",
        shellLabel(undefined) === "");
  check("and neither does a page that never set it",
        shellLabel(null) === "");
  check("a shell with no version still says what it is",
        shellLabel({ kind: "desktop" }) === "desktop");
  check("whitespace is not a version",
        shellLabel({ kind: "desktop", version: "   " }) === "desktop");
  check("a shell with no kind says nothing, because the version alone would " +
        "read as the panel's",
        shellLabel({ version: "0.3.12" }) === "");
  check("a non-string version is ignored rather than printed",
        shellLabel({ kind: "desktop", version: 3 }) === "desktop");
  check("something that is not an object is not a shell",
        shellLabel("desktop 0.3.12") === "");
}

console.log("paths a pane printed");
{
  const code = region("const LINK_RE", "function openLink");
  // const in eval is trapped in the eval; a Function returns the bindings.
  const { PATH_RE, trimPath, pathFromText } = new Function(
    code + "; return { PATH_RE, trimPath, pathFromText };")();
  check("strips a compiler suffix", trimPath("src/app.js:42:7") === "src/app.js");
  check("strips a trailing period", trimPath("docs/foo.md.") === "docs/foo.md");
  check("a selected relative path is a path", pathFromText("docs/foo.md") === "docs/foo.md");
  check("a selected absolute path is a path", pathFromText("/tmp/foo.md") === "/tmp/foo.md");
  check("a selected sentence is not a path", pathFromText("hello world") === "");
  const paths = (text) => {
    const out = [];
    PATH_RE.lastIndex = 0;
    let match;
    while ((match = PATH_RE.exec(text)) !== null) out.push(trimPath(match[1]));
    return out;
  };
  check("an absolute path", paths("see /tmp/foo.md")[0] === "/tmp/foo.md");
  check("a home path", paths("open ~/src/app.js")[0] === "~/src/app.js");
  check("a relative path with a slash and an extension",
        paths("wrote docs/foo.md")[0] === "docs/foo.md");
  check("dot-slash", paths("./src/app.js")[0] === "./src/app.js");
  check("a bare word is not a path", paths("hello world").length === 0);
  check("and a URL is not a path",
        paths("https://example.com/docs/foo.md").length === 0);
}

console.log("a login link that wrapped is still one link");
{
  const code = region("function paneRowsText", "function openLink");
  const {
    paneRowsText, paneUrlSegments, urlNeedsLocalCallback,
    paneHostIsRemote, tidyCopiedLink,
  } = new Function(code +
    "; return { paneRowsText, paneUrlSegments, urlNeedsLocalCallback, paneHostIsRemote, tidyCopiedLink };")();
  const parts = [
    { y: 8, text: "https://auth.openai.com/oauth/authorize?foo=1&code_challeng" },
    { y: 9, text: "e_method=S256&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback" },
  ];
  const joined = paneRowsText(parts);
  check("the two halves join without a gap",
        joined.indexOf("code_challenge_method") > 0);
  const segs = paneUrlSegments(parts, 0, joined.length);
  check("each row keeps its own slice",
        segs.length === 2 && segs[0].y === 8 && segs[1].y === 9, segs);
  check("a localhost callback is a remote-box login",
        urlNeedsLocalCallback(joined));
  check("a device-code page is not",
        !urlNeedsLocalCallback("https://auth.openai.com/codex/device"));
  check("opening this panel on the box itself is local",
        !paneHostIsRemote("127.0.0.1") && !paneHostIsRemote("localhost"));
  check("opening it over the tailnet is remote",
        paneHostIsRemote("box.tail1234.ts.net"));
  check("copying a wrapped URL drops the line break",
        tidyCopiedLink("https://auth.openai.com/codex/device\n/extra")
          === "https://auth.openai.com/codex/device/extra");
}

console.log("a path that wrapped is still one link");
{
  const code = region("function paneRowsText", "function openLink");
  const { panePathLinks } = new Function(code + "; return { panePathLinks };")();
  const full = "/root/platform/clique/.claude-images/paste-1789750882398-fc77b1bf.png";
  // 69 characters, so a narrow pane or a phone splits it. Each half on its own
  // matches nothing, which is why long paths were the ones that never worked.
  const parts = [
    { y: 12, text: "wrote /root/platform/clique/.claude-imag" },
    { y: 13, text: "es/paste-1789750882398-fc77b1bf.png" },
  ];
  const first = panePathLinks(parts, 12);
  const second = panePathLinks(parts, 13);
  check("the first row carries a link", first.length === 1, first);
  check("so does the second", second.length === 1, second);
  check("both name the whole path, not their own half",
        first[0] && second[0] && first[0].path === full && second[0].path === full,
        [first, second]);
  check("the first row's slice starts after the word before it",
        first[0] && first[0].x0 === 7 && first[0].x1 === 40, first);
  check("the second row's slice is the remainder",
        second[0] && second[0].x0 === 1 && second[0].x1 === 35, second);
  const one = panePathLinks([{ y: 3, text: "see /tmp/foo.md here" }], 3);
  check("a path that fits on one row still works",
        one.length === 1 && one[0].path === "/tmp/foo.md", one);
  check("and a URL is still not a path",
        panePathLinks([{ y: 4, text: "https://example.com/docs/foo.md" }], 4).length === 0);
}

console.log("a host with no https:// in front of it");
{
  const code = region("function paneRowsText", "function openLink");
  const { paneBareLinks, panePathLinks } = new Function(
    code + "; return { paneBareLinks, panePathLinks };")();
  const bare = (text, y) => paneBareLinks([{ y: y || 1, text }], y || 1);
  check("our own output is a link",
        bare("repo at fdroid.useclique.dev/repo (ver")[0]
          && bare("repo at fdroid.useclique.dev/repo (ver")[0].url
             === "https://fdroid.useclique.dev/repo", bare("repo at fdroid.useclique.dev/repo (ver"));
  check("the scheme is added, not assumed present",
        bare("see github.com/thejdubb02/clique")[0].url
          === "https://github.com/thejdubb02/clique");
  check("a trailing period belongs to the sentence",
        bare("go to useclique.dev/docs.")[0].url === "https://useclique.dev/docs");
  check("and a wrapping bracket to the prose",
        bare("(useclique.dev/docs)")[0].url === "https://useclique.dev/docs");
  // The slash is the whole defence against filenames: .md, .sh, .pl, .zip and
  // .mov are all real TLDs, so a bare dotted word can never be enough.
  check("README.md is not a domain", bare("edit README.md now").length === 0);
  check("neither is build.sh", bare("run build.sh now").length === 0);
  check("nor a Perl script", bare("perl gen.pl now").length === 0);
  check("a dotted directory in a path is not a host",
        bare("wrote /root/.claude/projects/x").length === 0);
  check("an email address is not a host",
        bare("mail user@host.com/x").length === 0);
  check("the tail of a real URL is not matched a second time",
        bare("https://example.com/docs").length === 0);
  check("a version number is not a TLD", bare("in v1.2/file.txt").length === 0);
  const parts = [
    { y: 5, text: "install from fdroid.usecliq" },
    { y: 6, text: "ue.dev/repo today" },
  ];
  check("a host that wrapped is still one link",
        paneBareLinks(parts, 5).length === 1 && paneBareLinks(parts, 6).length === 1
          && paneBareLinks(parts, 5)[0].url === "https://fdroid.useclique.dev/repo",
        [paneBareLinks(parts, 5), paneBareLinks(parts, 6)]);
  // The two passes must not both claim the same text, or the pane gets two
  // overlapping links on one run of characters.
  check("a host with a file on the end is a link, not a path",
        panePathLinks([{ y: 7, text: "see fdroid.useclique.dev/repo/index.xml" }], 7).length === 0);
  check("and a real relative path is still a path",
        panePathLinks([{ y: 8, text: "wrote docs/foo.md" }], 8).length === 1);
}

console.log("a suggested shell command, stitched back into one line");
{
  const code = region("const SHELL_SUGGEST_RE", "const PATH_RE");
  const { SHELL_SUGGEST_RE } = new Function(code + "; return { SHELL_SUGGEST_RE };")();
  const run = (text) => { const m = SHELL_SUGGEST_RE.exec(text); SHELL_SUGGEST_RE.lastIndex = 0; return m; };
  const hit = run("! cd /root/ventures/x && git push");
  check("the command is everything after '! '",
        hit && hit[1] === "cd /root/ventures/x && git push", hit);
  check("ordinary output starting with '!' but no space is not a suggestion",
        !run("!important: read this"));
  check("a line with no leading '!' is not a suggestion", !run("cd /root/ventures/x"));
}

console.log("things that sit on top of other things");
{
  // A preview that outranks a menu is a menu nobody can use — and it fails
  // silently, because the click still lands on the control you cannot see.
  // The stack is small enough to assert outright.
  const css = fs.readFileSync(path.join(ROOT, "clique/web/app.css"), "utf8");
  const layerOf = (selector) => {
    const at = css.indexOf(selector + " {");
    if (at < 0) return null;
    const found = /z-index:\s*(\d+)/.exec(css.slice(at, css.indexOf("}", at)));
    return found ? Number(found[1]) : null;
  };
  const menu = layerOf("#menu");
  const palette = layerOf("#palette");
  const follow = layerOf("#follow");
  const file = layerOf("#file");
  check("the palette sits above the context menu", palette > menu, `${palette} vs ${menu}`);
  check("and the menu above the pane's own controls", menu > follow, `${menu} vs ${follow}`);
  check("the file sheet sits above the palette", file > palette, `${file} vs ${palette}`);
}

console.log("tinted greys keep their contrast");
{
  // The point of tinting the 256-colour greyscale ramp is that a monochrome
  // theme owns the shades a CLI paints with. The point of doing it by hue
  // rather than by interpolation is that an application picking 233 chose how
  // far from the background it wanted to be — that choice is not ours to move,
  // and moving it is what makes text stop being readable on top.
  global.window = {};
  require(path.join(ROOT, "clique/web/themes.js"));
  const CUBE = [0, 95, 135, 175, 215, 255];
  eval(region("function hexRgb", "const _termThemes").replace(/^const CUBE.*$/m, ""));

  const theme = global.window.CLIQUE_THEMES.trinity;
  const ramp = extendedAnsi(theme).slice(216);
  const lum = (hex) => {
    const p = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255);
    return 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2];
  };

  check("the ramp still has 24 steps", ramp.length === 24, ramp.length);
  let held = true;
  let rising = true;
  for (let i = 0; i < 24; i++) {
    const v = 8 + 10 * i;
    const standard = "#" + [v, v, v].map((c) => c.toString(16).padStart(2, "0")).join("");
    if (Math.abs(lum(standard) - lum(ramp[i])) > 0.07) held = false;
    if (i && lum(ramp[i]) <= lum(ramp[i - 1])) rising = false;
  }
  check("every step keeps the lightness xterm would have given it", held, ramp.slice(0, 3));
  check("and the ramp still climbs from dark to light", rising);

  // The cube is not ours. An application asking for colour 82 wants that
  // green, not a theme's idea of one.
  const cube = extendedAnsi(theme).slice(0, 216);
  check("the 6x6x6 cube is left exactly as xterm defines it",
        cube[0] === "#000000" && cube[215] === "#ffffff", [cube[0], cube[215]]);

  const plain = global.window.CLIQUE_THEMES[""];
  check("a theme that did not ask keeps the standard ramp", !plain.tint_greys);
}

console.log("copy from a pane that is eating the mouse");
{
  const clickCode = region("function paneAtLiveScreen", "const PANE_DRAG_PX");
  const {
    paneAtLiveScreen, paneSgrClick, paneGridCell,
  } = new Function(clickCode +
    "; return { paneAtLiveScreen, paneSgrClick, paneGridCell };")();
  check("a click at the live screen is a click",
        paneAtLiveScreen({ buffer: { active: { viewportY: 10, baseY: 10 } } }));
  check("a click on older scrollback is not",
        !paneAtLiveScreen({ buffer: { active: { viewportY: 2, baseY: 10 } } }));
  check("the click report is SGR press then release",
        paneSgrClick(12, 4) === "\x1b[<0;12;4M\x1b[<0;12;4m");
  const cell = paneGridCell(
    { element: { getBoundingClientRect: () => ({ left: 0, top: 0, width: 100, height: 40 }) },
      cols: 10, rows: 4 },
    50, 10);
  check("a click in the middle maps to a 1-based cell",
        cell.col === 6 && cell.row === 2, cell);

  const code = region("const PANE_DRAG_PX", "function wirePaneClipboard");
  const {
    paneForceSelectMods, paneDragFarEnough, paneShouldStealMouse, paneEdgeScrollDir,
  } = new Function(code +
    "; return { paneForceSelectMods, paneDragFarEnough, paneShouldStealMouse, paneEdgeScrollDir };")();
  const click = { button: 0, ctrlKey: false, metaKey: false, shiftKey: false, altKey: false };
  check("a Mac uses Option, not Shift, to force a select",
        paneForceSelectMods("MacIntel").altKey && !paneForceSelectMods("MacIntel").shiftKey);
  check("everything else uses Shift",
        paneForceSelectMods("Linux x86_64").shiftKey && !paneForceSelectMods("Linux x86_64").altKey);
  check("five pixels is a drag", paneDragFarEnough(0, 0, 5, 0));
  check("four is not", !paneDragFarEnough(0, 0, 3, 3));
  check("a drag is stolen when the CLI is eating the mouse",
        paneShouldStealMouse(click, true, true));
  check("a click with a modifier is left alone",
        !paneShouldStealMouse({ ...click, shiftKey: true }, true, true));
  check("and so is Ctrl-click, which drops a path",
        !paneShouldStealMouse({ ...click, ctrlKey: true }, true, true));
  check("past the top edge scrolls up", paneEdgeScrollDir(5, 10, 110, 24) === -1);
  check("past the bottom edge scrolls down", paneEdgeScrollDir(95, 10, 110, 24) === 1);
  check("mid-pane does not scroll", paneEdgeScrollDir(50, 10, 110, 24) === 0);
  check("exactly on the threshold does not scroll yet",
        paneEdgeScrollDir(34, 10, 110, 24) === 0 && paneEdgeScrollDir(86, 10, 110, 24) === 0);
  check("a phone is not stolen from — the Copy chip is the way",
        !paneShouldStealMouse(click, true, false));
  check("and a shell with no mouse tracking selects on its own",
        !paneShouldStealMouse(click, false, true));
  check("a touch-synthesised mouse is left alone",
        !paneShouldStealMouse({ ...click, sourceCapabilities: { firesTouchEvents: true } },
                              true, true));
}

console.log("the terminal wears the theme");
{
  const code = region("function mix(", "function currentTheme");
  const { mix, termTokens, luminanceOf } = new Function(
    "function luminance(hex){const v=hex.replace('#','');"
    + "const f=v.length===3?[...v].map(c=>c+c).join(''):v;"
    + "const ch=[0,2,4].map(i=>parseInt(f.slice(i,i+2),16)/255)"
    + ".map(c=>c<=0.03928?c/12.92:Math.pow((c+0.055)/1.055,2.4));"
    + "return 0.2126*ch[0]+0.7152*ch[1]+0.0722*ch[2];}"
    + "const _termThemes = new WeakMap();"
    + code + "; return { mix, termTokens, luminanceOf: luminance };")();
  check("halfway between black and white is grey",
        mix("#000000", "#ffffff", 0.5) === "#808080");
  check("no distance is the colour you started from",
        mix("#1e1e1e", "#ffffff", 0) === "#1e1e1e");
  check("short hex is read the same as long",
        mix("#fff", "#000000", 0) === "#ffffff");

  const dark = { term: { background: "#1e1e1e", selectionBackground: "#264f78" } };
  const light = { term: { background: "#ffffff", selectionBackground: "#b6d7ff" } };
  check("the character under a block cursor gets the theme's background",
        termTokens(dark).cursorAccent === "#1e1e1e");
  check("a dark selection is written on in light text",
        termTokens(dark).selectionForeground === "#f5f5f5");
  check("and a pale one in dark text",
        termTokens(light).selectionForeground === "#101010");
  check("an unfocused selection sits halfway back to the background",
        termTokens(dark).selectionInactiveBackground === mix("#264f78", "#1e1e1e", 0.5));
  check("a theme that states these keeps them",
        termTokens({ term: { background: "#000", selectionBackground: "#333",
                             selectionForeground: "#0f0" } }).selectionForeground === "#0f0");
  check("and a theme with no selection colour is left alone",
        termTokens({ term: { background: "#000" } }).selectionForeground === undefined);

  /* Every shipped theme, not just a made-up one. The point of deriving these
   * is that a new theme cannot arrive with a selection you cannot read on, so
   * the guarantee is worth asserting against the real list. */
  const themeSrc = fs.readFileSync(
    path.join(__dirname, "..", "clique", "web", "themes.js"), "utf8");
  const themes = new Function("const window = {}; " + themeSrc
    + "; return window.CLIQUE_THEMES;")();
  const names = Object.keys(themes);
  check("there are themes to check", names.length >= 4, names.length);
  const contrast = (a, b) => {
    const [hi, lo] = [luminanceOf(a), luminanceOf(b)].sort((x, y) => y - x);
    return (hi + 0.05) / (lo + 0.05);
  };
  const thin = names.filter((name) => {
    const t = termTokens(themes[name]);
    if (!t.selectionBackground) return false;
    return contrast(t.selectionForeground, t.selectionBackground) < 4.5;
  });
  check("every theme's selection can be read on", thin.length === 0, thin);
  const noCursor = names.filter((name) => !termTokens(themes[name]).cursorAccent);
  check("and every theme says what sits under its cursor", noCursor.length === 0, noCursor);
}

console.log("zoom a boxed pane instead of wrapping it");
{
  const code = region("const PANE_ZOOM_MIN", "function paneForceSelectMods");
  const {
    paneWidthScale, paneShouldZoom, paneQueueOut, PANE_ZOOM_MIN,
  } = new Function(code +
    "; return { paneWidthScale, paneShouldZoom, paneQueueOut, PANE_ZOOM_MIN };")();
  check("room to spare is no zoom",
        paneWidthScale(800, 80, 8) === 1);
  check("half the width zooms to half",
        Math.abs(paneWidthScale(320, 80, 8) - 0.5) < 0.01);
  check("a boxed CLI at half size zooms",
        paneShouldZoom(true, 0.5));
  check("a shell never zooms",
        !paneShouldZoom(false, 0.5));
  check("a phone-sized shrink still resizes for real",
        !paneShouldZoom(true, PANE_ZOOM_MIN - 0.01));
  const held = paneQueueOut([], "hi", 8);
  check("keys typed while reconnecting are kept",
        held.join("") === "hi");
  const capped = paneQueueOut(["aaaaaaaa"], "bbbb", 8);
  check("and an 8k flood cannot pile up forever",
        capped.join("").length <= 8 && capped.join("").endsWith("bbbb"));
}

console.log("what's new");
{
  const code = region("function baseVersion", "let loadedVersion");
  const {
    baseVersion, changelogHasNews, CLOG_SHOW,
  } = new Function(code +
    "; return { baseVersion, changelogHasNews, CLOG_SHOW };")();
  check("the sheet holds five releases", CLOG_SHOW === 5);
  check("a build suffix is not a new release",
        baseVersion("0.50.24+abc") === "0.50.24");
  check("an unread upgrade is news",
        changelogHasNews("0.50.24", "0.50.23"));
  check("the same version is not",
        !changelogHasNews("0.50.24", "0.50.24"));
  check("and neither is a first look, before anything is stamped",
        !changelogHasNews("0.50.24", ""));
}

console.log("pairing QR sign-in URL");
{
  const code = region(
    "function pairSignInTarget(base, code, origin)",
    "/* ------------------------------------------------------------------- modal */"
  );
  const { pairSignInTarget } = new Function(
    code + "; return { pairSignInTarget };"
  )();
  check(
    "a public URL plus a code is the sign-in link",
    pairSignInTarget("https://box.tail1234.ts.net/clique", "K7PM-3XQF", "")
      === "https://box.tail1234.ts.net/clique/?pair=K7PM-3XQF"
  );
  check(
    "a trailing slash is not doubled",
    pairSignInTarget("https://box.example.ts.net/clique/", "ABCD-EFGH", "")
      === "https://box.example.ts.net/clique/?pair=ABCD-EFGH"
  );
  check(
    "localhost is not a QR",
    pairSignInTarget("http://localhost:3200", "ABCD-EFGH", "") === ""
  );
  check(
    "127.0.0.1 is not a QR",
    pairSignInTarget("http://127.0.0.1:3200", "ABCD-EFGH", "https://ok.example") === ""
  );
  check(
    "IPv6 loopback is not a QR either",
    pairSignInTarget("http://[::1]:3200", "ABCD-EFGH", "") === ""
  );
  check(
    "an empty setting falls back to the page origin",
    pairSignInTarget("", "ABCD-EFGH", "https://box.example.ts.net")
      === "https://box.example.ts.net/?pair=ABCD-EFGH"
  );
  check(
    "and a loopback origin with no setting is still not a QR",
    pairSignInTarget("", "ABCD-EFGH", "http://127.0.0.1:3200") === ""
  );
  check(
    "a hostname that merely contains 127.0.0.1 is fine",
    pairSignInTarget("https://127.0.0.1.example.com", "ABCD-EFGH", "")
      === "https://127.0.0.1.example.com/?pair=ABCD-EFGH"
  );
  check(
    "a scheme-less public host is accepted",
    pairSignInTarget("box.example.ts.net/clique", "ABCD-EFGH", "")
      === "https://box.example.ts.net/clique/?pair=ABCD-EFGH"
  );
}

console.log("markdown in the file sheet");
{
  const code = region(
    "/* parseMarkdown: blocks from a string, no DOM. */",
    "function renderMarkdown("
  );
  const { parseMarkdown } = new Function(code + "; return { parseMarkdown };")();
  const walk = (n, acc) => {
    if (!n) return acc;
    if (Array.isArray(n)) { n.forEach((x) => walk(x, acc)); return acc; }
    acc.push(n);
    walk(n.inlines, acc); walk(n.children, acc); walk(n.items, acc);
    walk(n.header, acc); walk(n.rows, acc);
    return acc;
  };

  for (let n = 1; n <= 6; n++) {
    const b = parseMarkdown("#".repeat(n) + " Title")[0];
    check("heading h" + n, b && b.type === "heading" && b.level === n, b);
  }

  const para = parseMarkdown("hello world")[0];
  check("a paragraph", para && para.type === "paragraph"
    && para.inlines[0] && para.inlines[0].text === "hello world", para);

  const fenced = parseMarkdown("```js\n# not a heading\n- not a list\n```");
  check("fenced contents are not markdown",
        fenced.length === 1 && fenced[0].type === "code"
        && fenced[0].text.indexOf("# not a heading") >= 0
        && fenced[0].text.indexOf("- not a list") >= 0, fenced);
  check("and keeps the language", fenced[0] && fenced[0].lang === "js", fenced[0]);

  const tildes = parseMarkdown("~~~\n# still code\n~~~")[0];
  check("tilde fences are code too",
        tildes && tildes.type === "code" && tildes.text.indexOf("# still code") >= 0,
        tildes);

  const ul = parseMarkdown("- a\n- b")[0];
  check("unordered list",
        ul && ul.type === "list" && !ul.ordered && ul.items.length === 2, ul);

  const ol = parseMarkdown("1. a\n2. b")[0];
  check("ordered list",
        ol && ol.type === "list" && ol.ordered && ol.items.length === 2, ol);

  const table = parseMarkdown("| a | b |\n| --- | ---: |\n| 1 | 2 |")[0];
  check("a table", table && table.type === "table" && table.rows.length === 1, table);
  check("and its alignment row",
        table && table.align && table.align[1] === "right", table && table.align);

  const hr = parseMarkdown("---")[0];
  check("a rule", hr && hr.type === "hr", hr);

  const quote = parseMarkdown("> quoted")[0];
  check("a blockquote", quote && quote.type === "blockquote", quote);

  const inline = parseMarkdown(
    "use `x` **bold** __also__ *em* _em2_ [ok](https://ex.com) "
    + "[bad](javascript:alert(1)) \\*star\\*"
  )[0];
  const nodes = walk(inline, []);
  check("inline code", nodes.some((n) => n.type === "code" && n.text === "x"));
  check("** and __ are bold", nodes.filter((n) => n.type === "strong").length === 2);
  check("* and _ are italic", nodes.filter((n) => n.type === "em").length === 2);
  check("http link survives",
        nodes.some((n) => n.type === "link" && n.href === "https://ex.com"));
  check("javascript: is not a link",
        !nodes.some((n) => n.type === "link" && /javascript:/i.test(n.href || "")),
        nodes.filter((n) => n.type === "link"));
  const jsOnly = parseMarkdown("[XSS](javascript:alert(1))")[0];
  const jsText = walk(jsOnly, []).filter((n) => n.type === "text").map((n) => n.text).join("");
  check("and it does not leave a stray parenthesis", jsText === "XSS", jsText);
  const esc = parseMarkdown("\\*star\\*")[0];
  check("escaped asterisk is text",
        esc && esc.type === "paragraph"
        && esc.inlines.length === 1
        && esc.inlines[0].type === "text"
        && esc.inlines[0].text === "*star*",
        esc);
}

console.log("settings queue, send errors, file save race, hidden polling");
{
  const toasts = [];
  const toast = (t, bad) => toasts.push(t);
  let state = { settings: {} }, calls = [], pend = [];
  const api = () => new Promise((res, rej) => { pend.push({ res, rej }); });
  const applySettings = () => {}, reclaimSize = () => {}, renderTree = () => {}, renderGroups = () => {}, renderTabs = () => {};
  eval(region("let settingsQueue", "function fmtSize") + "; globalThis.__s = saveSettings;");
  const save = globalThis.__s;
  const tick = () => new Promise((r) => setTimeout(r, 5));
  const pre = (async () => {
    const a = save({ a: 1 }), b = save({ b: 1 });
    await tick();
    check("second settings write waits for the first", pend.length === 1, pend.length);
    pend[0].res({ v: "first" }); await tick();
    pend[1].res({ v: "second" }); await a; await b;
    check("settings responses apply in order", state.settings.v === "second", state.settings);
    pend = [];
    const c = save({ c: 1 }); await tick(); pend[0].rej(new Error("down"));
    let threw = false; try { await c; } catch { threw = true; }
    check("a failed settings save is shown AND rejected to the caller", threw && toasts.some((t) => /down/.test(t)), toasts);
    const d = save({ d: 1 }); await tick();
    check("and the queue keeps working after a failure", pend.length === 2); pend[1].res({ v: "ok" }); await d;

    // prompt send failure keeps the draft and says so
    toasts.length = 0; pend = [];
    let sending = false, activeId = "s", repeat = 1, cleared = 0;
    const reviewLockedOf = () => false, hintReviewLocked = () => {}, session = () => ({ name: "n" });
    const okToSend = async () => true, control = () => false, maybeAutoTitle = () => {};
    const clearPrompt = () => { cleared++; }, saveDraft = () => {}, setRepeat = () => {};
    eval(region("async function run(text)", "async function runShell") + "; globalThis.__run = run;");
    const r = globalThis.__run("hello"); await tick(); pend[0].rej(new Error("503")); await r;
    check("a failed prompt send shows an error and keeps the draft",
          toasts.some((t) => /503/.test(t)) && cleared === 0 && sending === false, [toasts, cleared]);

    // file save finishing after the sheet moved on
    toasts.length = 0; pend = [];
    let fileEditing = true, fileSession = "s", fileAsked = "a.txt", shown = 0;
    let currentFile = { path: "a.txt", name: "a.txt", text: "old" };
    const btn = { disabled: false };
    const $ = (q) => (q === "#fileSave" ? btn : { value: "new", hidden: false });
    const showFile = () => { shown++; };
    eval(region("let fileSaveSeq", "function closeFileSheet") + "; globalThis.__sf = saveFileEdit; globalThis.__swap = () => { currentFile = { path: 'b.txt', name: 'b.txt', text: 'B' }; }; globalThis.__cf = () => currentFile;");
    const f = globalThis.__sf(); await tick(); globalThis.__swap(); pend[0].res({}); await f;
    check("a late file save does not repaint another file", shown === 0 && globalThis.__cf().text === "B", [shown, globalThis.__cf()]);

    // two overlapping saves: the first finishing must not free the button
    pend = [];
    currentFile = null; globalThis.__swap(); fileEditing = true;
    const f1 = globalThis.__sf(); const f2 = globalThis.__sf(); await tick();
    pend[0].res({}); await f1;
    check("an older save finishing leaves Save disabled for the newer one", btn.disabled === true);
    pend[1].res({}); await f2;
    check("and the newest finishing re-enables it", btn.disabled === false);

    // runShell failing after it switched tabs goes back with the command kept
    toasts.length = 0; pend = [];
    let act = "src", boxv = "", saved = [];
    const box = { get value() { return boxv; }, set value(v) { boxv = v; } };
    const $2 = () => box;
    const growPrompt = () => {}, refresh = async () => {};
    const openSession = async (id) => { act = id; boxv = id === "src" ? "" : "shell draft"; };
    const sessionX = (id) => ({ id, cwd: "/w", folder: "f", cli: "x" });
    const stateX = { sessions: [{ id: "sh", cli: "shell", cwd: "/w", alive: true }] };
    const rs = new Function("session", "state", "activeId", "reviewLockedOf", "hintReviewLocked", "okToSend", "api", "openSession", "toast", "clearPrompt", "saveDraft", "$", "growPrompt", "refresh", "sending", "clearSent",
      "return (" + region("async function runShell", "function setRepeat").replace(/^async function runShell/, "async function") + ")")(
      sessionX, stateX, "src", () => false, () => {}, async () => true, () => new Promise((res, rej) => pend.push({ res, rej })),
      openSession, (t) => toasts.push(t), () => {}, (n) => saved.push(boxv), $2, growPrompt, refresh, false,
      (id, text) => clearedFor.push(id + ":" + text));
    const clearedFor = [];
    const p = rs("ls -la"); await tick(); pend[0].rej(new Error("boom")); await p;
    check("a failed shell send returns to the source tab with the command in the draft",
          act === "src" && boxv === "ls -la" && toasts.some((t) => /boom/.test(t)), [act, boxv, toasts]);
    pend = []; act = "src"; boxv = "pwd";
    const ok = rs("pwd"); await tick(); pend[0].res({}); await ok;
    check("a sent shell command clears the source tab's draft, not the shell's",
          clearedFor.join() === "src:pwd" && boxv === "shell draft", [clearedFor, boxv]);
  })().then(() => {
    // hidden tabs do not poll
    let fn = null, calls2 = 0;
    const setInterval = (f) => { if (!fn) fn = f; };
    const document = { hidden: true }; const refresh = () => { calls2++; };
    const pollArtifacts = () => {}, ART_POLL_MS = 1, renderClock = () => {}, activeId = null;
    eval(region("setInterval(() => { if (!document.hidden) refresh", "// Slower than the sidebar"));
    fn(); check("a hidden tab does not poll state", calls2 === 0, calls2);
    document.hidden = false; fn(); check("a visible tab does", calls2 === 1, calls2);
    const listeners = {}; let n3 = 0;
    const doc = { hidden: false, addEventListener: (e, f) => { listeners[e] = f; } };
    const mk = (document, refresh) => new Function("document", "refresh", "pollArtifacts", "scheduleWake",
      region('document.addEventListener("visibilitychange", () => {\n  if (!document.hidden) {\n    refresh', 'addEventListener("focus"') .replace(/\naddEventListener\("focus"[\s\S]*$/, "") + "");
    mk(doc, () => { n3++; })(doc, () => { n3++; }, () => {}, () => {});
    listeners.visibilitychange();
    check("coming back to the tab refreshes (listener is on document)", typeof listeners.visibilitychange === "function" && n3 === 1, n3);
  });
  globalThis.__pre = pre;
}

/* Drafts: the shipped loadDraft/saveDraft region, fake timers, a hand-driven
 * server. Issue #32: typing in B used to cancel A's pending save, and a flush
 * on hide sent nothing because the optimistic copy already "matched". */
globalThis.__pre = globalThis.__pre.then(async () => {
  console.log("drafts survive tab switches, hiding, slow and failed saves");
  const code = region("function loadDraft(id)", "/* Move a half-typed thought");
  let timers = [], reqs = [], toasts = [];
  const box = { value: "", style: {}, scrollHeight: 0 };
  const state = { sessions: [{ id: "A", draft: "" }, { id: "B", draft: "" }] };
  const f = new Function("$", "session", "showDraftMove", "growPrompt", "api", "toast",
    "setTimeout", "clearTimeout", "state",
    "let draftFor = null, draftTimer = null;\n" + code
    + "\nreturn { loadDraft, saveDraft, flush: typeof flushDrafts === 'function' ? flushDrafts : () => saveDraft(true) };")(
    () => box, (id) => state.sessions.find((s) => s.id === id), () => {}, () => {},
    (path, opts) => new Promise((res, rej) => reqs.push({ id: path.split("/").pop(), draft: JSON.parse(opts.body).draft, rev: JSON.parse(opts.body).draft_rev, keepalive: opts.keepalive, res, rej })),
    (t) => toasts.push(t),
    (fn) => { const t = { fn, live: true }; timers.push(t); return t; },
    (t) => { if (t) t.live = false; }, state);
  const fire = () => { const due = timers.filter((t) => t.live); timers = []; due.forEach((t) => { t.live = false; t.fn(); }); };
  const settle = () => new Promise((r) => setImmediate(r));
  const type = (text) => { box.value = text; f.saveDraft(); };
  const sent = () => reqs.map((r) => `${r.id}:${r.draft}`);

  // rapid A -> B: the shipped switch path is saveDraft(true) then loadDraft
  f.loadDraft("A"); type("for A");
  f.saveDraft(true); f.loadDraft("B"); type("for B");
  fire(); await settle();
  check("switching A to B mid-debounce still saves A's draft", sent().includes("A:for A"), sent());
  reqs.forEach((r) => r.res({})); await settle(); reqs = [];

  // hide / pagehide during the debounce
  type("for B, more");
  f.flush(); await settle();
  check("a flush on hide sends the pending text at once", sent().includes("B:for B, more"), sent());
  check("and asks for keepalive so a closing page does not abort it", reqs.length > 0 && reqs.every((r) => r.keepalive), reqs.map((r) => r.keepalive));
  reqs.forEach((r) => r.res({})); await settle(); reqs = []; fire();

  // delayed response: a newer draft waits for the older write, never races it
  type("v1"); fire(); await settle();
  type("v2"); fire(); await settle();
  check("only one write per session is in flight", reqs.length === 1 && reqs[0].draft === "v1", sent());
  reqs.shift().res({}); await settle();
  check("the newer draft goes after the older reply", reqs.length === 1 && reqs[0].draft === "v2", sent());
  reqs.shift().res({}); await settle(); fire();
  check("and nothing else is sent once they match", reqs.length === 0, sent());

  // page closes while a save is in flight: the exit write cannot wait, so it
  // must carry a later rev from the same writer for the server to keep it
  type("older"); fire(); await settle();
  type("latest"); f.flush(); await settle();
  const [older, latest] = reqs;
  check("an exit save goes out even with one in flight", reqs.length === 2 && latest.draft === "latest", sent());
  check("and outranks it, so the server drops the older if it lands second",
        older && latest && older.rev && latest.rev && older.rev[0] === latest.rev[0] && latest.rev[1] > older.rev[1],
        reqs.map((r) => r.rev));
  reqs.forEach((r) => r.res({})); await settle(); reqs.forEach((r) => r.res({})); await settle(); reqs = []; fire();

  // cleared while a save is in flight, then the page closes: the clear must
  // still go, or the old text comes back on reload
  type(""); fire(); await settle(); reqs.forEach((r) => r.res({})); await settle(); reqs = [];
  check("starting from a saved empty draft", reqs.length === 0 && timers.every((t) => !t.live), sent());
  type("hello"); fire(); await settle();
  type(""); f.flush(); await settle();
  check("clearing during a save and then closing still sends the clear",
        reqs.length === 2 && reqs[1].draft === "" && reqs[1].rev[1] > reqs[0].rev[1], sent());
  reqs.forEach((r) => r.res({})); await settle(); reqs.forEach((r) => r.res({})); await settle(); reqs = []; fire();

  // a poll replaces A's object with a stale copy while A's save is in flight:
  // after the save lands, coming back to A must show the saved text, and the
  // next switch must not write the stale copy over it
  f.loadDraft("A"); type("keep A"); f.saveDraft(true); f.loadDraft("B"); await settle();
  state.sessions = state.sessions.map((x) => ({ ...x, draft: x.id === "A" ? "" : x.draft }));
  reqs.forEach((r) => r.res({})); await settle(); reqs = [];
  f.saveDraft(true); f.loadDraft("A");
  check("a stale poll during A's save does not empty A's box", box.value === "keep A", box.value);
  f.saveDraft(true); f.loadDraft("B"); await settle();
  check("and switching away does not write the stale copy back", !reqs.some((r) => r.id === "A"), sent());
  reqs.forEach((r) => r.res({})); await settle(); reqs = []; fire();

  // failed save: visible, retried, not forgotten
  type("keep me"); fire(); await settle();
  reqs.shift().rej(new Error("offline")); await settle();
  check("a failed draft save is shown", toasts.some((t) => /not saved/i.test(t)), toasts);
  fire(); await settle();
  check("and retried", reqs.length === 1 && reqs[0].draft === "keep me", sent());
  reqs.shift().res({}); await settle();
  f.loadDraft("A"); f.loadDraft("B");
  check("the retried text is what the box reloads", box.value === "keep me", box.value);

  type("orphan"); fire(); await settle();
  state.sessions = state.sessions.filter((s) => s.id !== "B");
  reqs.shift().rej(new Error("gone")); await settle(); fire(); await settle();
  check("a removed session's draft is not retried forever", reqs.length === 0, sent());

  const src = region('document.addEventListener("visibilitychange", () => {\n    if (document.visibilityState === "hidden")', '$("#palQ").oninput');
  check("hide and pagehide both flush every session's drafts", (src.match(/flushDrafts\(\)/g) || []).length === 2, src);
});

/* Issue #33: a send awaits, and the tab in front or the text in the box can
 * change before it returns. Every send and every follow-up must stay with the
 * session it was started from. Runs the shipped clearPrompt .. run region. */
globalThis.__pre = globalThis.__pre.then(async () => {
  console.log("a prompt send stays bound to the session it started in");
  const code = region("function clearPrompt(sent)", "async function runShell");
  const settle = () => new Promise((r) => setImmediate(r));
  const mk = (repeat = 1) => {
    const w = { activeId: "a", repeat, draftFor: "a", reqs: [], titled: [], writes: [], toasts: [], repeatSet: [],
      drafts: { a: "", b: "" }, box: { value: "", style: {}, blur() {}, focus() {} } };
    w.run = new Function("w", `
      let repeat = w.repeat, activeId = w.activeId;
      const $ = () => w.box, handheld = () => false, document = {}, growPrompt = () => {}, showDraftMove = () => {};
      const requestAnimationFrame = (f) => f();
      const reviewLockedOf = () => false, hintReviewLocked = () => {}, okToSend = async () => true;
      const session = (id) => ({ id, name: id });
      const control = () => false;
      const api = (path) => new Promise((res, rej) => w.reqs.push({ path, res, rej }));
      const maybeAutoTitle = (id) => w.titled.push(id);
      const toast = (t) => w.toasts.push(t);
      const setRepeat = (v) => { repeat = v; w.repeatSet.push(v); };
      const draftOf = (id) => w.drafts[id];
      const writeDraft = (id, text) => { w.drafts[id] = text; w.writes.push(id + ":" + text); };
      const saveDraft = () => { w.drafts[draftFor] = w.box.value; w.writes.push(draftFor + ":" + w.box.value); };
      let draftFor = w.draftFor;
      w.switchTo = (id) => { saveDraft(); activeId = id; draftFor = id; w.box.value = w.drafts[id]; };
      ${code}
      return run;`)(w);
    return w;
  };
  const sent = (w) => w.reqs.map((r) => r.path);

  // the reported case: repeat 2, HTTP fallback, switch to B while the first is held
  let w = mk(2); w.box.value = "prompt for A"; w.drafts.b = "B's own words";
  let p = w.run("prompt for A"); await settle();
  w.switchTo("b"); w.reqs[0].res({}); await settle(); w.reqs[1] && w.reqs[1].res({}); await p;
  check("every repeat goes to the session it was sent from",
        sent(w).length === 2 && sent(w).every((x) => x === "api/sessions/a/send"), sent(w));
  check("the follow-up title is for that session, not the one in front", w.titled.join() === "a", w.titled);
  check("text composed in the other tab is left alone", w.box.value === "B's own words" && w.drafts.b === "B's own words", [w.box.value, w.drafts]);
  check("and the source session's sent draft is cleared", w.drafts.a === "", w.drafts);

  // repeat 1, still on A, but a newer draft was typed while in flight
  w = mk(); w.box.value = "first";
  p = w.run("first"); await settle();
  w.box.value = "second thought"; w.reqs[0].res({}); await p;
  check("a newer draft typed while sending is not cleared", w.box.value === "second thought", w.box.value);
  check("and it is not overwritten with an empty draft", !w.writes.includes("a:"), w.writes);

  // repeat 1, nothing changed: the ordinary send still empties the box
  w = mk(); w.box.value = "plain";
  p = w.run("plain"); await settle(); w.reqs[0].res({}); await p;
  check("an ordinary send still empties the box and the draft", w.box.value === "" && w.drafts.a === "", [w.box.value, w.drafts]);
  check("and resets repeat", w.repeatSet.join() === "1", w.repeatSet);

  // a quick command sent while the box holds something else
  w = mk(); w.box.value = "half a sentence"; w.drafts.a = "half a sentence";
  p = w.run("git status"); await settle(); w.reqs[0].res({}); await p;
  check("a quick command does not wipe the half-typed box", w.box.value === "half a sentence", w.box.value);

  // failed send after switching tabs: nothing cleared anywhere
  w = mk(2); w.box.value = "doomed";
  p = w.run("doomed"); await settle(); w.switchTo("b");
  w.reqs[0].rej(new Error("503")); await p;
  check("a failed send keeps the source draft and touches nothing else",
        w.drafts.a === "doomed" && w.box.value === "" && w.titled.length === 0 && w.toasts.some((t) => /503/.test(t)),
        [w.drafts, w.box.value, w.titled, w.toasts]);
});

console.log("stale polls, artifact races, vanished sessions");
{
  const code = region("let refreshSeq", "  if (!workspaceRestored) restoreWorkspace();") + "  return true;\n}\n"
    + region("async function pollArtifacts", "function setArtifacts");
  let state = { sessions: [] }, openTabs = [], activeId = "a", pollFailures = 0, artItems = [];
  const terms = new Map(), closed = [], set = [];
  const session = (id) => state.sessions.find((x) => x.id === id);
  const closeTab = (id) => { closed.push(id); terms.delete(id); };
  const setArtifacts = (i) => set.push(i);
  const $ = () => ({}); const document = { hidden: false };
  let pending = [];
  const api = () => new Promise((res) => pending.push(res));
  eval(code + "; globalThis.__t = { refresh, pollArtifacts, setActive: (v) => { activeId = v; }, st: () => state };");
  const t = globalThis.__t;
  (async () => {
    await globalThis.__pre;
    const r1 = t.refresh(), r2 = t.refresh();
    pending[1]({ sessions: [{ id: "a" }], v: "new" });
    await r2;
    pending[0]({ sessions: [], v: "old" });
    await r1;
    check("an older poll finishing late does not replace newer state", t.st().v === "new", t.st().v);
    pending = [];
    state = { sessions: [{ id: "a" }], settings: {} };
    terms.set("gone", {});
    const r3 = t.refresh(); pending[0]({ sessions: [{ id: "a" }], settings: {} }); await r3;
    check("a terminal whose session vanished is torn down", closed.includes("gone") && !terms.has("gone"), closed);
    pending = [];
    const p = t.pollArtifacts(); t.setActive("b"); pending[0]([{ rel: "x" }]); await p;
    check("artifacts for a tab we left are dropped", set.length === 0, set);
  })().then(() => {
    console.log(`\n${passed} passed, ${failed} failed`);
    process.exit(failed ? 1 : 0);
  });
  return;
}

console.log(`\n${passed} passed, ${failed} failed`);
process.exit(failed ? 1 : 0);