tests/workspace.test.ts
Rama main · réplica de lectura del repositorio original.
// The workspace model (client/workspace.ts).
//
// This is the spine of panes, windows and the buffer registry, and it is pure —
// so it can be proven here, before any of it is wired to a component. The
// property tests follow check-sections.mjs's shape: throw thousands of mutated
// inputs at the model and assert the invariants hold, because the bugs in a
// layout model are never in the edit itself, they are in the fifth thing the
// edit invalidated.
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
CALENDAR_TAB,
MAX_COLUMNS,
MAX_PANES,
MAX_ROWS,
activeTabOf,
allPaths,
closeAfterIn,
closeAllIn,
closeAllPanes,
closeOthersIn,
closePane,
closePathsUnder,
closeTabIn,
stepTab,
emptyWorkspace,
fromStoredTabs,
holdersOf,
isCalendarTab,
isTabbablePath,
isVirtualTab,
moveTab,
dropTabSplit,
openInPane,
setBookTarget,
paneAt,
paneInDirection,
panesInOrder,
parseWorkspace,
SIGILS_TAB,
pruneWorkspace,
remapWorkspace,
serializeWorkspace,
setPinned,
soloWorkspace,
splitPane,
surfaceOf,
type Workspace,
} from "../client/workspace.ts";
import { pick, rng } from "./helpers/vault.ts";
function tabs(...paths: string[]) {
return paths.map((path) => ({ path, pinned: false, ephemeral: false }));
}
/** Every invariant parseWorkspace promises, asserted against a live value. */
function assertInvariants(ws: Workspace, why: string): void {
const flat = ws.layout.columns.flat();
assert.ok(ws.layout.columns.length >= 1, `${why}: at least one column`);
assert.ok(ws.layout.columns.length <= MAX_COLUMNS, `${why}: column cap`);
assert.ok(flat.length <= MAX_PANES, `${why}: pane cap`);
for (const col of ws.layout.columns) {
assert.ok(col.length >= 1 && col.length <= MAX_ROWS, `${why}: rows per column`);
}
assert.equal(new Set(flat).size, flat.length, `${why}: no pane placed twice`);
assert.deepEqual(new Set(flat), new Set(Object.keys(ws.panes)), `${why}: layout and panes agree`);
for (const id of flat) {
const pane = ws.panes[id];
if (pane.tabs.length === 0) assert.equal(pane.active, -1, `${why}: empty pane has active -1`);
else assert.ok(pane.active >= 0 && pane.active < pane.tabs.length, `${why}: active in range`);
assert.equal(
new Set(pane.tabs.map((t) => t.path)).size,
pane.tabs.length,
`${why}: no path twice in one pane`,
);
for (const t of pane.tabs) {
assert.ok(!(t.pinned && t.ephemeral), `${why}: pinned and ephemeral are opposites`);
}
if (pane.follow !== null) assert.equal(pane.tabs.length, 0, `${why}: a follower holds no tabs`);
}
assert.ok(flat.includes(ws.focus), `${why}: focus names a live pane`);
assert.equal(ws.panes[ws.focus].follow, null, `${why}: focus is never a follower`);
assert.ok(flat.includes(ws.noteFocus), `${why}: noteFocus names a live pane`);
const cw = ws.layout.colWeights;
assert.equal(cw.length, ws.layout.columns.length, `${why}: one weight per column`);
assert.ok(cw.every((w) => w > 0), `${why}: weights positive`);
assert.ok(Math.abs(cw.reduce((a, b) => a + b, 0) - 1) < 1e-9, `${why}: weights normalized`);
}
describe("workspace: tabs", () => {
it("an ephemeral tab is replaced by the next ephemeral open, not stacked", () => {
let ws = soloWorkspace(tabs("Kept.md"), "Kept.md");
const p = ws.focus;
ws = openInPane(ws, p, "First.md", { ephemeral: true });
ws = openInPane(ws, p, "Second.md", { ephemeral: true });
assert.deepEqual(allPaths(ws).sort(), ["Kept.md", "Second.md"]);
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "Second.md");
});
it("returning to an ephemeral tab commits it — a second visit is intent", () => {
let ws = soloWorkspace([], null);
const p = ws.focus;
ws = openInPane(ws, p, "A.md", { ephemeral: true });
ws = openInPane(ws, p, "B.md", { ephemeral: true }); // replaces A
ws = openInPane(ws, p, "B.md", { ephemeral: true }); // revisit: commits
ws = openInPane(ws, p, "C.md", { ephemeral: true }); // so C does not replace B
assert.deepEqual(allPaths(ws).sort(), ["B.md", "C.md"]);
});
it("newTab forces a real tab past a standing preview", () => {
let ws = soloWorkspace([], null);
const p = ws.focus;
ws = openInPane(ws, p, "A.md", { ephemeral: true });
ws = openInPane(ws, p, "B.md", { ephemeral: true, newTab: true });
assert.deepEqual(allPaths(ws).sort(), ["A.md", "B.md"]);
});
it("no bulk close takes a pinned tab, and that rule is the same in every row", () => {
const start = () => setPinned(soloWorkspace(tabs("A.md", "B.md", "C.md"), "B.md"), "p", "A.md", true);
let ws = soloWorkspace(tabs("A.md", "B.md", "C.md"), "B.md");
const p = ws.focus;
ws = setPinned(ws, p, "A.md", true);
assert.deepEqual(allPaths(closeOthersIn(ws, p, "C.md")).sort(), ["A.md", "C.md"]);
assert.deepEqual(allPaths(closeAllIn(ws, p)), ["A.md"]);
assert.deepEqual(allPaths(closeAllPanes(ws)), ["A.md"]);
void start;
});
it("close-after takes only what follows, in tab order", () => {
let ws = soloWorkspace(tabs("A.md", "B.md", "C.md", "D.md"), "A.md");
const p = ws.focus;
assert.deepEqual(allPaths(closeAfterIn(ws, p, "B.md")).sort(), ["A.md", "B.md"]);
});
it("pinning sorts to the head and commits the tab", () => {
let ws = soloWorkspace([], null);
const p = ws.focus;
ws = openInPane(ws, p, "A.md");
ws = openInPane(ws, p, "B.md", { ephemeral: true });
ws = setPinned(ws, p, "B.md", true);
const pane = paneAt(ws, p)!;
assert.equal(pane.tabs[0].path, "B.md");
assert.equal(pane.tabs[0].ephemeral, false);
// The active tab follows its identity through the reorder, not its index.
assert.equal(activeTabOf(pane)!.path, "B.md");
});
it("closing the active tab lands on the one after it", () => {
let ws = soloWorkspace(tabs("A.md", "B.md", "C.md"), "B.md");
const p = ws.focus;
ws = closeTabIn(ws, p, "B.md");
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "C.md");
});
it("the tab chords walk the strip and wrap at both ends (F12)", () => {
let ws = soloWorkspace(tabs("A.md", "B.md", "C.md"), "A.md");
const p = ws.focus;
ws = stepTab(ws, p, 1);
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "B.md");
ws = stepTab(ws, p, -1);
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "A.md");
// Backwards off the first tab lands on the last, not on nothing.
ws = stepTab(ws, p, -1);
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "C.md");
ws = stepTab(ws, p, 1);
assert.equal(activeTabOf(paneAt(ws, p)!)!.path, "A.md");
});
it("stepping a pane with one tab (or none) is a no-op, not a throw", () => {
const one = soloWorkspace(tabs("A.md"), "A.md");
assert.equal(stepTab(one, one.focus, 1), one);
const none = soloWorkspace([], null);
assert.equal(stepTab(none, none.focus, -1), none);
assert.equal(stepTab(one, "no-such-pane", 1), one);
});
it("stepping past a preview tab does NOT commit it — a glance is not intent", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
const p = ws.focus;
ws = openInPane(ws, p, "Preview.md", { ephemeral: true });
ws = stepTab(ws, p, 1);
ws = stepTab(ws, p, -1);
assert.equal(paneAt(ws, p)!.tabs.find((t) => t.path === "Preview.md")!.ephemeral, true);
});
it("only notes and books may be tabs", () => {
const ws = fromStoredTabs({ tabs: ["A.md", "img.png", "Paper.tex", "Book.pdf"], open: "A.md" });
assert.deepEqual(allPaths(ws).sort(), ["A.md", "Book.pdf", "Paper.tex"]);
});
});
describe("workspace: panes", () => {
it("splits along both axes and refuses past the caps", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
for (let i = 0; i < MAX_COLUMNS - 1; i++) {
const next = splitPane(ws, ws.focus, "inline", null);
assert.ok(next, "inline split within the cap");
ws = next;
assertInvariants(ws, "after inline split");
}
assert.equal(splitPane(ws, ws.focus, "inline", null), null, "column cap refuses by name");
let stacked = ws;
for (let i = 0; i < MAX_ROWS - 1; i++) {
const next = splitPane(stacked, stacked.focus, "block", null);
assert.ok(next, "block split within the cap");
stacked = next;
}
assert.equal(splitPane(stacked, stacked.focus, "block", null), null, "row cap");
});
it("closing a pane adopts its tabs rather than closing the reader's notes", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
const first = ws.focus;
ws = splitPane(ws, first, "inline", null)!;
ws = openInPane(ws, ws.focus, "B.md");
ws = openInPane(ws, ws.focus, "C.md");
const second = ws.focus;
ws = closePane(ws, second);
assert.deepEqual(allPaths(ws).sort(), ["A.md", "B.md", "C.md"]);
assertInvariants(ws, "after closePane");
});
it("the last pane never closes — there is always somewhere to type", () => {
const ws = soloWorkspace(tabs("A.md"), "A.md");
assert.deepEqual(closePane(ws, ws.focus), ws);
});
it("noteFocus stays on a note while a book pane holds the keyboard", () => {
let ws = soloWorkspace(tabs("Note.md"), "Note.md");
const noteP = ws.focus;
ws = splitPane(ws, noteP, "inline", null)!;
ws = openInPane(ws, ws.focus, "Ihya.pdf");
assert.equal(ws.focus, paneAt(ws, ws.focus)!.id, "the book pane has the keyboard");
assert.equal(surfaceOf(paneAt(ws, ws.focus)!), "book");
assert.equal(ws.noteFocus, noteP, "…and noteFocus still names the note beside it");
});
it("OpenHow.book lands on the pane, for a fresh open and for a re-open", () => {
const aim = { page: 212, rect: null, id: null };
let ws = soloWorkspace(tabs("Note.md"), "Note.md");
ws = openInPane(ws, ws.focus, "Ihya.pdf", { book: aim });
let pane = paneAt(ws, ws.focus)!;
assert.equal(surfaceOf(pane), "book");
assert.deepEqual(pane.bookTarget, aim, "a citation rides the open itself");
// Consumed by the reader…
ws = setBookTarget(ws, ws.focus, null);
// …and a LATER citation into the already-open book must still jump: the
// re-open path sets the target too, or only the first citation ever lands.
const again = { page: 7, rect: null, id: null };
ws = openInPane(ws, ws.focus, "Ihya.pdf", { book: again });
pane = paneAt(ws, ws.focus)!;
assert.deepEqual(pane.bookTarget, again, "a re-open aims the pane again");
// A plain open leaves a pending target alone rather than wiping it.
ws = openInPane(ws, ws.focus, "Ihya.pdf");
assert.deepEqual(paneAt(ws, ws.focus)!.bookTarget, again);
});
it("dropTabSplit: the drag-to-edge gesture moves the tab into a new pane", () => {
let ws = soloWorkspace(tabs("A.md", "B.md"), "A.md");
const solo = ws.focus;
ws = dropTabSplit(ws, solo, "B.md", solo, "end-inline");
assert.equal(panesInOrder(ws).length, 2, "the drop split the solo pane");
const [first, second] = panesInOrder(ws);
assert.deepEqual(first.tabs.map((t) => t.path), ["A.md"]);
assert.deepEqual(second.tabs.map((t) => t.path), ["B.md"], "the tab travelled");
assert.equal(ws.focus, second.id, "…and took the keyboard with it");
// start-inline lands the new pane BEFORE the target in reading order.
let ws2 = soloWorkspace(tabs("A.md", "B.md"), "A.md");
ws2 = dropTabSplit(ws2, ws2.focus, "B.md", ws2.focus, "start-inline");
assert.deepEqual(panesInOrder(ws2)[0].tabs.map((t) => t.path), ["B.md"]);
});
it("dropTabSplit: refuses whole rather than half-applying at a cap", () => {
// Fill the columns, then try to drop across — the close must not happen
// when the split cannot.
let ws = soloWorkspace(tabs("A.md", "B.md"), "A.md");
const p0 = ws.focus;
for (let i = 0; i < MAX_COLUMNS - 1; i++) ws = splitPane(ws, ws.focus, "inline", null)!; // MAX_COLUMNS reached
const before = ws;
ws = dropTabSplit(ws, p0, "B.md", ws.focus, "end-inline");
assert.deepEqual(allPaths(ws).sort(), allPaths(before).sort(), "no tab eaten");
assert.deepEqual(paneAt(ws, p0)!.tabs.map((t) => t.path), ["A.md", "B.md"]);
});
it("dropTabSplit: an emptied source pane closes behind the drag", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
const p0 = ws.focus;
ws = splitPane(ws, p0, "inline", null)!;
const p1 = ws.focus;
ws = openInPane(ws, p1, "B.md");
// Drag B — p1's only tab — onto p0's block edge: p1 must vanish.
ws = dropTabSplit(ws, p1, "B.md", p0, "end-block");
assert.equal(paneAt(ws, p1), null, "the emptied pane closed behind it");
assert.deepEqual(allPaths(ws).sort(), ["A.md", "B.md"]);
assert.equal(panesInOrder(ws).length, 2);
});
it("closing a split pane's last tab closes the pane, and the tab is NOT adopted", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
const p0 = ws.focus;
ws = splitPane(ws, p0, "inline", null)!;
const p1 = ws.focus;
ws = openInPane(ws, p1, "B.md");
ws = closeTabIn(ws, p1, "B.md");
assert.equal(paneAt(ws, p1), null, "the emptied split pane folded");
assert.deepEqual(allPaths(ws), ["A.md"], "the closed tab did not reappear next door");
// The LAST pane may be empty — the solo empty state is a real state.
let solo = soloWorkspace(tabs("A.md"), "A.md");
solo = closeTabIn(solo, solo.focus, "A.md");
assert.equal(panesInOrder(solo).length, 1);
assert.deepEqual(allPaths(solo), []);
});
it("dropTabSplit from the TREE (no source pane) splits without removing anything", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
ws = dropTabSplit(ws, null, "Ihya.pdf", ws.focus, "end-inline");
const [first, second] = panesInOrder(ws);
assert.deepEqual(first.tabs.map((t) => t.path), ["A.md"]);
assert.deepEqual(second.tabs.map((t) => t.path), ["Ihya.pdf"]);
assert.equal(surfaceOf(second), "book");
});
it("dropTabSplit: a solo pane's only tab dropped on its own edge is a no-op", () => {
const ws = soloWorkspace(tabs("A.md"), "A.md");
const out = dropTabSplit(ws, ws.focus, "A.md", ws.focus, "end-inline");
assert.deepEqual(out, ws);
});
});
describe("the Calendar page is a tab", () => {
it("is a virtual tab with a surface of its own", () => {
assert.ok(isCalendarTab(CALENDAR_TAB));
assert.ok(!isCalendarTab("~sigils"));
assert.ok(!isCalendarTab("Calendar.md"));
assert.ok(isVirtualTab(CALENDAR_TAB));
assert.ok(isTabbablePath(CALENDAR_TAB));
const ws = soloWorkspace(tabs(CALENDAR_TAB), CALENDAR_TAB);
assert.equal(surfaceOf(paneAt(ws, ws.focus)!), "calendar");
});
it("survives a save and a reload, and the vault never prunes it", () => {
const ws = soloWorkspace(tabs("A.md", CALENDAR_TAB), CALENDAR_TAB);
const back = parseWorkspace(JSON.parse(JSON.stringify(serializeWorkspace(ws))));
assert.ok(back);
assert.deepEqual(allPaths(back).sort(), ["A.md", CALENDAR_TAB].sort());
// It names no file, so a tree that has never heard of it leaves it alone.
assert.deepEqual(allPaths(pruneWorkspace(ws, new Set(["A.md"]))).sort(), ["A.md", CALENDAR_TAB].sort());
});
});
describe("workspace: the vault changes underneath", () => {
it("a rename follows every tab, and collapses onto a tab that already exists", () => {
let ws = soloWorkspace(tabs("Old.md", "New.md"), "Old.md");
ws = remapWorkspace(ws, "Old.md", "New.md");
assert.deepEqual(allPaths(ws), ["New.md"]);
assertInvariants(ws, "after a collapsing rename");
});
it("a delete and a sign-out ignore pins — a pin is about intent, not contents", () => {
let ws = soloWorkspace(tabs("A.md", "folder/B.md"), "A.md");
ws = setPinned(ws, ws.focus, "folder/B.md", true);
assert.deepEqual(allPaths(closePathsUnder(ws, "folder")), ["A.md"]);
assert.deepEqual(allPaths(pruneWorkspace(ws, new Set(["A.md"]))), ["A.md"]);
});
it("counts holders, because two panes on one note is one document", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
ws = splitPane(ws, ws.focus, "inline", null)!;
ws = openInPane(ws, ws.focus, "A.md");
assert.equal(holdersOf(ws, "A.md"), 2);
assert.equal(holdersOf(ws, "B.md"), 0);
});
});
describe("workspace: the calendar is a tab", () => {
it("is virtual, tabbable, and renders the calendar surface", () => {
assert.equal(isTabbablePath(CALENDAR_TAB), true);
assert.equal(isVirtualTab(CALENDAR_TAB), true);
assert.equal(isCalendarTab(CALENDAR_TAB), true);
// It is nobody else's sentinel, and no note is it.
assert.equal(isCalendarTab(SIGILS_TAB), false);
assert.equal(isCalendarTab("Calendar.md"), false);
const ws = soloWorkspace(tabs(CALENDAR_TAB), CALENDAR_TAB);
assert.equal(surfaceOf(paneAt(ws, ws.focus)!), "calendar");
});
it("survives a save and a reload, like the Sigils tab beside it", () => {
const ws = soloWorkspace(tabs(CALENDAR_TAB, "A.md"), CALENDAR_TAB);
const back = parseWorkspace(serializeWorkspace(ws))!;
assert.deepEqual(allPaths(back), [CALENDAR_TAB, "A.md"]);
});
it("opens beside a note rather than over it, and closes back onto it", () => {
let ws = soloWorkspace(tabs("A.md"), "A.md");
ws = openInPane(ws, ws.focus, CALENDAR_TAB);
assert.deepEqual(allPaths(ws).sort(), ["A.md", CALENDAR_TAB]);
assert.equal(surfaceOf(paneAt(ws, ws.focus)!), "calendar");
ws = closeTabIn(ws, ws.focus, CALENDAR_TAB);
assert.equal(activeTabOf(paneAt(ws, ws.focus)!)?.path, "A.md");
});
});
describe("workspace: geometry", () => {
const rect = (left: number, top: number): DOMRect =>
({ left, top, width: 100, height: 100 }) as DOMRect;
it("resolves direction from the screen, so left is left in Arabic too", () => {
const rects = { a: rect(0, 0), b: rect(200, 0), c: rect(0, 200) };
assert.equal(paneInDirection(rects, "a", "right"), "b");
assert.equal(paneInDirection(rects, "b", "left"), "a");
assert.equal(paneInDirection(rects, "a", "down"), "c");
assert.equal(paneInDirection(rects, "a", "up"), null);
});
it("prefers the pane straight across over a nearer diagonal one", () => {
const rects = { a: rect(0, 0), straight: rect(300, 0), diagonal: rect(200, 400) };
assert.equal(paneInDirection(rects, "a", "right"), "straight");
});
});
describe("workspace: persistence", () => {
it("round-trips through serialize/parse", () => {
let ws = soloWorkspace(tabs("A.md", "B.md"), "B.md");
ws = splitPane(ws, ws.focus, "inline", null)!;
ws = openInPane(ws, ws.focus, "C.md");
ws = setPinned(ws, ws.focus, "C.md", true);
const back = parseWorkspace(JSON.parse(JSON.stringify(serializeWorkspace(ws))));
assert.ok(back);
assert.deepEqual(allPaths(back).sort(), allPaths(ws).sort());
assert.equal(back.layout.columns.length, ws.layout.columns.length);
assertInvariants(back, "round-trip");
});
it("migrates astrolabe.tabs without losing an open note", () => {
const ws = fromStoredTabs({ tabs: ["a.md", "b/c.md"], open: "b/c.md" });
assert.deepEqual(allPaths(ws), ["a.md", "b/c.md"]);
assert.equal(activeTabOf(paneAt(ws, ws.focus)!)!.path, "b/c.md");
assertInvariants(ws, "migration");
});
it("recovers the reader's notes from a layout that is structurally broken", () => {
// The layout is nonsense; the tabs are not. The rule is that a corrupt
// layout must never cost anyone their open notes.
const damaged = {
v: 2,
workspace: {
panes: { p1: { id: "p1", tabs: [{ path: "Alive.md" }], active: 0, mode: "edit" } },
layout: { columns: "not an array", colWeights: null, rowWeights: 7 },
focus: "gone",
noteFocus: null,
},
};
const ws = parseWorkspace(damaged);
assert.ok(ws, "recovered rather than discarded");
assert.deepEqual(allPaths(ws), ["Alive.md"]);
assertInvariants(ws, "recovery");
});
it("adopts the tabs of a pane the layout forgot to place", () => {
const orphaned = {
v: 2,
workspace: {
panes: {
p1: { id: "p1", tabs: [{ path: "Placed.md" }], active: 0, mode: "edit" },
p9: { id: "p9", tabs: [{ path: "Orphan.md" }], active: 0, mode: "edit" },
},
layout: { columns: [["p1"]], colWeights: [1], rowWeights: { p1: 1 } },
focus: "p1",
noteFocus: "p1",
},
};
const ws = parseWorkspace(orphaned);
assert.ok(ws);
assert.deepEqual(allPaths(ws).sort(), ["Orphan.md", "Placed.md"]);
assertInvariants(ws, "orphan adoption");
});
it("is TOTAL over garbage — it never throws", () => {
const junk: unknown[] = [
null, undefined, 0, "", [], {}, { v: 1 }, { v: 2 }, { v: 2, workspace: null },
{ v: 2, workspace: { panes: {}, layout: { columns: [] } } },
{ v: 2, workspace: { panes: { a: 1 }, layout: { columns: [["a", "a"]] } } },
];
for (const raw of junk) {
const ws = parseWorkspace(raw);
if (ws !== null) assertInvariants(ws, `junk ${JSON.stringify(raw)}`);
}
});
});
describe("workspace: property tests", () => {
it("no sequence of edits can break an invariant (4000 random walks)", () => {
const paths = ["A.md", "B.md", "c/D.md", "Paper.tex", "Book.pdf"];
for (let seed = 0; seed < 4000; seed++) {
const next = rng(seed);
let ws = emptyWorkspace();
for (let step = 0; step < 12; step++) {
const ids = panesInOrder(ws).map((p) => p.id);
const id = pick(next, ids);
const path = pick(next, paths);
switch (Math.floor(next() * 12)) {
case 0: ws = splitPane(ws, id, "inline", null, next() < 0.5) ?? ws; break;
case 1: ws = splitPane(ws, id, "block", null, next() < 0.5) ?? ws; break;
case 2: ws = closePane(ws, id); break;
case 3: ws = openInPane(ws, id, path, { ephemeral: next() < 0.5 }); break;
case 4: ws = closeTabIn(ws, id, path); break;
case 5: ws = closeOthersIn(ws, id, path); break;
case 6: ws = setPinned(ws, id, path, next() < 0.5); break;
case 7: ws = moveTab(ws, id, path, pick(next, ids), Math.floor(next() * 4)); break;
case 8: ws = remapWorkspace(ws, path, pick(next, paths)); break;
case 9:
ws = dropTabSplit(
ws, id, path, pick(next, ids),
pick(next, ["start-inline", "end-inline", "start-block", "end-block"] as const),
);
break;
case 10: ws = closeAllIn(ws, id); break;
default: ws = closeAllIn(ws, id); break;
}
assertInvariants(ws, `seed ${seed} step ${step}`);
}
// …and whatever that walk produced still survives a save and a reload.
const back = parseWorkspace(JSON.parse(JSON.stringify(serializeWorkspace(ws))));
assert.ok(back, `seed ${seed}: round-trip`);
assertInvariants(back, `seed ${seed}: after round-trip`);
assert.deepEqual(allPaths(back).sort(), allPaths(ws).sort(), `seed ${seed}: no tab lost`);
}
});
});