Public: anyone can read this; the crew can change it.

test/snapshot.test.ts, test/board-windows.test.ts and test/tui-asked.test.ts read only the state they made, and run in the gate #11096

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/11083-test-snapshot-test-ts-test-board-windows → main 1 commit fork

  • Merged. scout/11083-test-snapshot-test-ts-test-board-windows was merged into main as 9ba9f45.

Review

Sign in to review this change.
4 files changed +69 −56
scripts/gate.ts −15
@@ -44,11 +44,6 @@ export const EXCLUDED: readonly Exclusion[] = [
4444 reason: "red alone under --randomize (2026-11-10): an environment nobody asks for sleeps, and the next request wakes it — times out at 30s",
4545 issue: 11049,
4646 },
47 {
48 file: "test/board-windows.test.ts",
49 reason: "red alone under --randomize (2026-11-10): the batch agrees with each account read alone — order-dependent",
50 issue: 11049,
51 },
5247 {
5348 file: "test/crew-interrupted.test.ts",
5449 reason: "not yet admitted",
@@ -76,11 +71,6 @@ export const EXCLUDED: readonly Exclusion[] = [
7671 reason: "not yet admitted",
7772 issue: 261,
7873 },
79 {
80 file: "test/snapshot.test.ts",
81 reason: "red alone under --randomize (2026-11-10): retention, restore and upload tests share one store and depend on order",
82 issue: 11049,
83 },
8474 { file: "test/ssh-app.test.ts", reason: "not yet admitted", issue: 261 },
8575 {
8676 file: "test/ssh-port-taken.test.ts",
@@ -88,11 +78,6 @@ export const EXCLUDED: readonly Exclusion[] = [
8878 issue: 261,
8979 },
9080 { file: "test/ssh.test.ts", reason: "not yet admitted", issue: 261 },
91 {
92 file: "test/tui-asked.test.ts",
93 reason: "red alone under --randomize (2026-11-10): the asked screen lists asks other tests filed — order-dependent",
94 issue: 11049,
95 },
9681 ];
9782
9883 /**
test/board-windows.test.ts +2 −1
@@ -250,7 +250,8 @@ describe("efficiency and standing, grouped", () => {
250250 });
251251
252252 test("the batch agrees with each account read alone, in three statements however many", async () => {
253 const repoId = repos.one!;
253 // Its own repository and accounts, so no other test's rows reach it.
254 const repoId = seedRepo("batch");
254255 const now = new Date("2026-10-02T12:00:00.000Z");
255256 const a = account();
256257 const b = account();
test/snapshot.test.ts +64 −39
@@ -6,7 +6,7 @@
66 * data and can still open the variable. Then the store is pruned and the
77 * restore refuses what it must.
88 */
9 import { afterAll, beforeAll, describe, expect, test } from "bun:test";
9 import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test";
1010 import {
1111 existsSync,
1212 mkdirSync,
@@ -49,7 +49,27 @@ describe("a forge back from a snapshot", () => {
4949 let root: string;
5050 let app: App;
5151 let alice: User;
52 const store = () => dirStore(join(root, "store"));
52 let made = 0;
53 /** A directory under this block's root that no other test has used. */
54 const fresh = (what: string) => join(root, `${what}-${++made}`);
55 const NAME = "2026-09-22T08-00-00Z";
56 /** A store of its own holding one snapshot of the forge, taken at 08:00 on 22 Sep. */
57 const snapshotted = async () => {
58 const dir = fresh("store");
59 const store = dirStore(dir);
60 const manifest = await takeSnapshot(
61 {
62 db: app.db,
63 dataDir: app.config.dataDir,
64 reposRoot: app.reposRoot,
65 version: "t",
66 passphrase: PASS,
67 },
68 store,
69 new Date("2026-09-22T08:00:00Z"),
70 );
71 return { dir, store, manifest };
72 };
5373
5474 beforeAll(async () => {
5575 root = mkdtempSync(join(tmpdir(), "forge-snapshot-"));
@@ -117,18 +137,8 @@ describe("a forge back from a snapshot", () => {
117137 });
118138
119139 test("the snapshot holds what cannot be rebuilt, consistently, while the forge runs", async () => {
120 const manifest = await takeSnapshot(
121 {
122 db: app.db,
123 dataDir: app.config.dataDir,
124 reposRoot: app.reposRoot,
125 version: "t",
126 passphrase: PASS,
127 },
128 store(),
129 new Date("2026-09-22T08:00:00Z"),
130 );
131 expect(manifest.name).toBe("2026-09-22T08-00-00Z");
140 const { dir, store, manifest } = await snapshotted();
141 expect(manifest.name).toBe(NAME);
132142 expect(manifest.repos).toBe(1);
133143 expect(manifest.apps).toBe(1);
134144 const paths = manifest.files.map((f) => f.path);
@@ -145,7 +155,7 @@ describe("a forge back from a snapshot", () => {
145155 if (!f.startsWith("forge.db") && !f.includes("/"))
146156 expect(
147157 readFileSync(
148 join(root, "store", "snapshots", manifest.name, f),
158 join(dir, "snapshots", manifest.name, f),
149159 ).includes(rawKey),
150160 ).toBe(false);
151161 expect(paths).toContain("repos/alice/notes.bundle");
@@ -165,8 +175,7 @@ describe("a forge back from a snapshot", () => {
165175 const { Database } = await import("bun:sqlite");
166176 const copy = new Database(
167177 join(
168 root,
169 "store",
178 dir,
170179 "snapshots",
171180 manifest.name,
172181 "apps",
@@ -181,14 +190,15 @@ describe("a forge back from a snapshot", () => {
181190 n: 2,
182191 });
183192 copy.close();
184 expect((await listSnapshots(store())).map((m) => m.name)).toEqual([
193 expect((await listSnapshots(store)).map((m) => m.name)).toEqual([
185194 manifest.name,
186195 ]);
187196 });
188197
189198 test("restored into an empty directory, a forge booted there is the same forge", async () => {
190 const back = join(root, "back");
191 const done = await restoreSnapshot(store(), "2026-09-22T08-00-00Z", back, {
199 const { store } = await snapshotted();
200 const back = fresh("back");
201 const done = await restoreSnapshot(store, NAME, back, {
192202 passphrase: PASS,
193203 });
194204 expect(done.repos).toBe(1);
@@ -234,8 +244,9 @@ describe("a forge back from a snapshot", () => {
234244 });
235245
236246 test("without the passphrase repos restore and sealed values are reported by name", async () => {
237 const back = join(root, "back-nokey");
238 const done = await restoreSnapshot(store(), "2026-09-22T08-00-00Z", back);
247 const { store } = await snapshotted();
248 const back = fresh("back-nokey");
249 const done = await restoreSnapshot(store, NAME, back);
239250 expect(done.key).toBe("missing");
240251 expect(done.sealedMissing).toEqual(["SECRET_WORD (app notes)"]);
241252 expect(existsSync(join(back, "secret.key.wrapped"))).toBe(false);
@@ -254,9 +265,10 @@ describe("a forge back from a snapshot", () => {
254265 });
255266
256267 test("a wrong passphrase refuses the restore and writes nothing", async () => {
257 const back = join(root, "back-wrong");
268 const { store } = await snapshotted();
269 const back = fresh("back-wrong");
258270 await expect(
259 restoreSnapshot(store(), "2026-09-22T08-00-00Z", back, {
271 restoreSnapshot(store, NAME, back, {
260272 passphrase: "not the one",
261273 }),
262274 ).rejects.toThrow("passphrase");
@@ -264,7 +276,7 @@ describe("a forge back from a snapshot", () => {
264276 });
265277
266278 test("a snapshot taken without a passphrase carries no key at all", async () => {
267 const s = dirStore(join(root, "store-nokey"));
279 const s = dirStore(fresh("store-nokey"));
268280 const m = await takeSnapshot(
269281 {
270282 db: app.db,
@@ -290,19 +302,21 @@ describe("a forge back from a snapshot", () => {
290302 });
291303
292304 test("a restore refuses a full directory and an unknown snapshot", async () => {
305 const { store } = await snapshotted();
306 const full = fresh("full");
307 mkdirSync(full, { recursive: true });
308 writeFileSync(join(full, "already.txt"), "here\n");
309 await expect(restoreSnapshot(store, NAME, full)).rejects.toThrow("not empty");
293310 await expect(
294 restoreSnapshot(store(), "2026-09-22T08-00-00Z", join(root, "back")),
295 ).rejects.toThrow("not empty");
296 await expect(
297 restoreSnapshot(store(), "2026-01-01T00-00-00Z", join(root, "nowhere")),
311 restoreSnapshot(store, "2026-01-01T00-00-00Z", fresh("nowhere")),
298312 ).rejects.toThrow("no snapshot");
299313 await expect(
300 restoreSnapshot(store(), "../etc", join(root, "nowhere")),
314 restoreSnapshot(store, "../etc", fresh("nowhere")),
301315 ).rejects.toThrow("not a snapshot name");
302316 });
303317
304318 test("the store keeps the newest fourteen and no more", async () => {
305 const s = store();
319 const { store: s } = await snapshotted();
306320 for (let d = 1; d <= 16; d++)
307321 await s.put(
308322 `snapshots/2026-08-${String(d).padStart(2, "0")}T08-00-00Z/manifest.json`,
@@ -320,7 +334,7 @@ describe("a forge back from a snapshot", () => {
320334 });
321335
322336 test("the service takes one on demand and knows when the night is", async () => {
323 const svc = startSnapshots(app, dirStore(join(root, "store2")), {
337 const svc = startSnapshots(app, dirStore(fresh("store")), {
324338 version: "t",
325339 nightly: false,
326340 });
@@ -355,7 +369,8 @@ describe("a forge back from a snapshot", () => {
355369 await fetch(`${servers.url}/settings/backups`, { headers: { cookie } })
356370 ).text();
357371 expect(none).toContain("No snapshots are taken");
358 const svc = startSnapshots(app, dirStore(join(root, "store")), {
372 const { store } = await snapshotted();
373 const svc = startSnapshots(app, store, {
359374 version: "t",
360375 nightly: false,
361376 });
@@ -365,7 +380,7 @@ describe("a forge back from a snapshot", () => {
365380 headers: { cookie },
366381 })
367382 ).text();
368 expect(listed).toContain("2026-09-22T08-00-00Z");
383 expect(listed).toContain(NAME);
369384 expect(listed).toContain("Snapshot now");
370385 // Off by default: the page says no key travels, and what that costs.
371386 expect(listed).toContain("Snapshots carry no key");
@@ -388,7 +403,7 @@ describe("a forge back from a snapshot", () => {
388403 } finally {
389404 await svc.stop();
390405 }
391 const wrapping = startSnapshots(app, dirStore(join(root, "store")), {
406 const wrapping = startSnapshots(app, store, {
392407 version: "t",
393408 nightly: false,
394409 passphrase: PASS,
@@ -417,14 +432,15 @@ describe("a forge back from a snapshot", () => {
417432 });
418433
419434 describe("a snapshot's uploads", () => {
435 // Each test gets a forge of its own, so no test reads the logs another wrote.
420436 let root: string;
421437 let app: App;
422 beforeAll(() => {
438 beforeEach(() => {
423439 root = mkdtempSync(join(tmpdir(), "forge-snapshot-queue-"));
424440 app = createApp({ dataDir: join(root, "live"), port: 0 });
425441 mkdirSync(join(app.config.dataDir, "runs"), { recursive: true });
426442 });
427 afterAll(async () => {
443 afterEach(async () => {
428444 app.close();
429445 await rm(root, { recursive: true, force: true });
430446 });
@@ -455,6 +471,11 @@ describe("a snapshot's uploads", () => {
455471 return { store, seen };
456472 };
457473 const logsDir = () => join(app.config.dataDir, "runs");
474 /** Forty fresh run logs, new-00.log to new-39.log. */
475 const newLogs = () => {
476 for (let i = 0; i < 40; i++)
477 writeFileSync(join(logsDir(), `new-${String(i).padStart(2, "0")}.log`), "x");
478 };
458479 const old = (name: string, body: string, day: string) => {
459480 const file = join(logsDir(), name);
460481 writeFileSync(file, body);
@@ -463,8 +484,7 @@ describe("a snapshot's uploads", () => {
463484 };
464485
465486 test("at most eight puts are in flight, and the manifest keeps its order", async () => {
466 for (let i = 0; i < 40; i++)
467 writeFileSync(join(logsDir(), `new-${String(i).padStart(2, "0")}.log`), "x");
487 newLogs();
468488 const { store, seen } = slowStore(15);
469489 const manifest = await takeSnapshot(forge(), store);
470490 expect(seen.peak).toBeGreaterThan(1);
@@ -501,6 +521,7 @@ describe("a snapshot's uploads", () => {
501521 }, 20_000);
502522
503523 test("a failed put fails the snapshot and names its key", async () => {
524 newLogs();
504525 const { store } = slowStore(1, "runs/new-07.log");
505526 await expect(takeSnapshot(forge(), store)).rejects.toThrow(
506527 /could not put snapshots\/.*runs\/new-07\.log/,
@@ -508,6 +529,7 @@ describe("a snapshot's uploads", () => {
508529 }, 15_000);
509530
510531 test("a slow store gets a progress line every so many objects", async () => {
532 newLogs();
511533 const { store } = slowStore(10);
512534 const lines: { objects: number; bytes: number }[] = [];
513535 await takeSnapshot(
@@ -533,6 +555,7 @@ describe("a snapshot's uploads", () => {
533555 });
534556
535557 test("older run logs go up as one object a day and restore as the logs they were", async () => {
558 newLogs();
536559 old("a.log", "alpha\n", "2026-10-10");
537560 old("b.log", "beta\n", "2026-10-10");
538561 old("c.log", "gamma\n", "2026-10-11");
@@ -574,6 +597,7 @@ describe("a snapshot's uploads", () => {
574597 }, 15_000);
575598
576599 test("the service shows a running snapshot's progress while it runs", async () => {
600 newLogs();
577601 const { store } = slowStore(40);
578602 const svc = startSnapshots(app, store, { version: "t", nightly: false });
579603 try {
@@ -597,6 +621,7 @@ describe("a snapshot's uploads", () => {
597621 }, 15_000);
598622
599623 test("the service says how long the snapshot in flight has run, for the release keeper to wait on (#9653)", async () => {
624 newLogs();
600625 const { store } = slowStore(40);
601626 const svc = startSnapshots(app, store, { version: "t", nightly: false });
602627 try {
test/tui-asked.test.ts +3 −1
@@ -124,7 +124,9 @@ describe("the asked screen", () => {
124124 });
125125
126126 test("asks are unshipped, oldest first, with their address, and an unreadable repository's is absent", () => {
127 const { asks } = askedLines(app.db, alice);
127 // Only the asks this file filed up front: other tests file their own.
128 const filed = new Set(["Newer ask for the lobby", "Older ask for the lobby", "Already shipped ask", "Secret ask in the vault"]);
129 const asks = askedLines(app.db, alice).asks.filter((a) => filed.has(a.title));
128130 expect(asks.map((a) => a.title)).toEqual(["Older ask for the lobby", "Newer ask for the lobby"]);
129131 expect(asks[0]!.secondary).toContain("alice/lobby #");
130132 expect(asks[0]!.secondary).toContain("alice");

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs