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
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11083-test-snapshot-test-ts-test-board-windows → main 1 commit fork
4 files
changed +69
−56
scripts/gate.ts −15
| @@ -44,11 +44,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 44 | 44 | reason: "red alone under --randomize (2026-11-10): an environment nobody asks for sleeps, and the next request wakes it — times out at 30s", |
| 45 | 45 | issue: 11049, |
| 46 | 46 | }, |
| 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 | }, | |
| 52 | 47 | { |
| 53 | 48 | file: "test/crew-interrupted.test.ts", |
| 54 | 49 | reason: "not yet admitted", |
| @@ -76,11 +71,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 76 | 71 | reason: "not yet admitted", |
| 77 | 72 | issue: 261, |
| 78 | 73 | }, |
| 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 | }, | |
| 84 | 74 | { file: "test/ssh-app.test.ts", reason: "not yet admitted", issue: 261 }, |
| 85 | 75 | { |
| 86 | 76 | file: "test/ssh-port-taken.test.ts", |
| @@ -88,11 +78,6 @@ export const EXCLUDED: readonly Exclusion[] = [ | ||
| 88 | 78 | issue: 261, |
| 89 | 79 | }, |
| 90 | 80 | { 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 | }, | |
| 96 | 81 | ]; |
| 97 | 82 | |
| 98 | 83 | /** |
test/board-windows.test.ts +2 −1
| @@ -250,7 +250,8 @@ describe("efficiency and standing, grouped", () => { | ||
| 250 | 250 | }); |
| 251 | 251 | |
| 252 | 252 | 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"); | |
| 254 | 255 | const now = new Date("2026-10-02T12:00:00.000Z"); |
| 255 | 256 | const a = account(); |
| 256 | 257 | const b = account(); |
test/snapshot.test.ts +64 −39
| @@ -6,7 +6,7 @@ | ||
| 6 | 6 | * data and can still open the variable. Then the store is pruned and the |
| 7 | 7 | * restore refuses what it must. |
| 8 | 8 | */ |
| 9 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; | |
| 9 | import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, test } from "bun:test"; | |
| 10 | 10 | import { |
| 11 | 11 | existsSync, |
| 12 | 12 | mkdirSync, |
| @@ -49,7 +49,27 @@ describe("a forge back from a snapshot", () => { | ||
| 49 | 49 | let root: string; |
| 50 | 50 | let app: App; |
| 51 | 51 | 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 | }; | |
| 53 | 73 | |
| 54 | 74 | beforeAll(async () => { |
| 55 | 75 | root = mkdtempSync(join(tmpdir(), "forge-snapshot-")); |
| @@ -117,18 +137,8 @@ describe("a forge back from a snapshot", () => { | ||
| 117 | 137 | }); |
| 118 | 138 | |
| 119 | 139 | 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); | |
| 132 | 142 | expect(manifest.repos).toBe(1); |
| 133 | 143 | expect(manifest.apps).toBe(1); |
| 134 | 144 | const paths = manifest.files.map((f) => f.path); |
| @@ -145,7 +155,7 @@ describe("a forge back from a snapshot", () => { | ||
| 145 | 155 | if (!f.startsWith("forge.db") && !f.includes("/")) |
| 146 | 156 | expect( |
| 147 | 157 | readFileSync( |
| 148 | join(root, "store", "snapshots", manifest.name, f), | |
| 158 | join(dir, "snapshots", manifest.name, f), | |
| 149 | 159 | ).includes(rawKey), |
| 150 | 160 | ).toBe(false); |
| 151 | 161 | expect(paths).toContain("repos/alice/notes.bundle"); |
| @@ -165,8 +175,7 @@ describe("a forge back from a snapshot", () => { | ||
| 165 | 175 | const { Database } = await import("bun:sqlite"); |
| 166 | 176 | const copy = new Database( |
| 167 | 177 | join( |
| 168 | root, | |
| 169 | "store", | |
| 178 | dir, | |
| 170 | 179 | "snapshots", |
| 171 | 180 | manifest.name, |
| 172 | 181 | "apps", |
| @@ -181,14 +190,15 @@ describe("a forge back from a snapshot", () => { | ||
| 181 | 190 | n: 2, |
| 182 | 191 | }); |
| 183 | 192 | copy.close(); |
| 184 | expect((await listSnapshots(store())).map((m) => m.name)).toEqual([ | |
| 193 | expect((await listSnapshots(store)).map((m) => m.name)).toEqual([ | |
| 185 | 194 | manifest.name, |
| 186 | 195 | ]); |
| 187 | 196 | }); |
| 188 | 197 | |
| 189 | 198 | 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, { | |
| 192 | 202 | passphrase: PASS, |
| 193 | 203 | }); |
| 194 | 204 | expect(done.repos).toBe(1); |
| @@ -234,8 +244,9 @@ describe("a forge back from a snapshot", () => { | ||
| 234 | 244 | }); |
| 235 | 245 | |
| 236 | 246 | 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); | |
| 239 | 250 | expect(done.key).toBe("missing"); |
| 240 | 251 | expect(done.sealedMissing).toEqual(["SECRET_WORD (app notes)"]); |
| 241 | 252 | expect(existsSync(join(back, "secret.key.wrapped"))).toBe(false); |
| @@ -254,9 +265,10 @@ describe("a forge back from a snapshot", () => { | ||
| 254 | 265 | }); |
| 255 | 266 | |
| 256 | 267 | 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"); | |
| 258 | 270 | await expect( |
| 259 | restoreSnapshot(store(), "2026-09-22T08-00-00Z", back, { | |
| 271 | restoreSnapshot(store, NAME, back, { | |
| 260 | 272 | passphrase: "not the one", |
| 261 | 273 | }), |
| 262 | 274 | ).rejects.toThrow("passphrase"); |
| @@ -264,7 +276,7 @@ describe("a forge back from a snapshot", () => { | ||
| 264 | 276 | }); |
| 265 | 277 | |
| 266 | 278 | 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")); | |
| 268 | 280 | const m = await takeSnapshot( |
| 269 | 281 | { |
| 270 | 282 | db: app.db, |
| @@ -290,19 +302,21 @@ describe("a forge back from a snapshot", () => { | ||
| 290 | 302 | }); |
| 291 | 303 | |
| 292 | 304 | 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"); | |
| 293 | 310 | 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")), | |
| 298 | 312 | ).rejects.toThrow("no snapshot"); |
| 299 | 313 | await expect( |
| 300 | restoreSnapshot(store(), "../etc", join(root, "nowhere")), | |
| 314 | restoreSnapshot(store, "../etc", fresh("nowhere")), | |
| 301 | 315 | ).rejects.toThrow("not a snapshot name"); |
| 302 | 316 | }); |
| 303 | 317 | |
| 304 | 318 | test("the store keeps the newest fourteen and no more", async () => { |
| 305 | const s = store(); | |
| 319 | const { store: s } = await snapshotted(); | |
| 306 | 320 | for (let d = 1; d <= 16; d++) |
| 307 | 321 | await s.put( |
| 308 | 322 | `snapshots/2026-08-${String(d).padStart(2, "0")}T08-00-00Z/manifest.json`, |
| @@ -320,7 +334,7 @@ describe("a forge back from a snapshot", () => { | ||
| 320 | 334 | }); |
| 321 | 335 | |
| 322 | 336 | 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")), { | |
| 324 | 338 | version: "t", |
| 325 | 339 | nightly: false, |
| 326 | 340 | }); |
| @@ -355,7 +369,8 @@ describe("a forge back from a snapshot", () => { | ||
| 355 | 369 | await fetch(`${servers.url}/settings/backups`, { headers: { cookie } }) |
| 356 | 370 | ).text(); |
| 357 | 371 | 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, { | |
| 359 | 374 | version: "t", |
| 360 | 375 | nightly: false, |
| 361 | 376 | }); |
| @@ -365,7 +380,7 @@ describe("a forge back from a snapshot", () => { | ||
| 365 | 380 | headers: { cookie }, |
| 366 | 381 | }) |
| 367 | 382 | ).text(); |
| 368 | expect(listed).toContain("2026-09-22T08-00-00Z"); | |
| 383 | expect(listed).toContain(NAME); | |
| 369 | 384 | expect(listed).toContain("Snapshot now"); |
| 370 | 385 | // Off by default: the page says no key travels, and what that costs. |
| 371 | 386 | expect(listed).toContain("Snapshots carry no key"); |
| @@ -388,7 +403,7 @@ describe("a forge back from a snapshot", () => { | ||
| 388 | 403 | } finally { |
| 389 | 404 | await svc.stop(); |
| 390 | 405 | } |
| 391 | const wrapping = startSnapshots(app, dirStore(join(root, "store")), { | |
| 406 | const wrapping = startSnapshots(app, store, { | |
| 392 | 407 | version: "t", |
| 393 | 408 | nightly: false, |
| 394 | 409 | passphrase: PASS, |
| @@ -417,14 +432,15 @@ describe("a forge back from a snapshot", () => { | ||
| 417 | 432 | }); |
| 418 | 433 | |
| 419 | 434 | describe("a snapshot's uploads", () => { |
| 435 | // Each test gets a forge of its own, so no test reads the logs another wrote. | |
| 420 | 436 | let root: string; |
| 421 | 437 | let app: App; |
| 422 | beforeAll(() => { | |
| 438 | beforeEach(() => { | |
| 423 | 439 | root = mkdtempSync(join(tmpdir(), "forge-snapshot-queue-")); |
| 424 | 440 | app = createApp({ dataDir: join(root, "live"), port: 0 }); |
| 425 | 441 | mkdirSync(join(app.config.dataDir, "runs"), { recursive: true }); |
| 426 | 442 | }); |
| 427 | afterAll(async () => { | |
| 443 | afterEach(async () => { | |
| 428 | 444 | app.close(); |
| 429 | 445 | await rm(root, { recursive: true, force: true }); |
| 430 | 446 | }); |
| @@ -455,6 +471,11 @@ describe("a snapshot's uploads", () => { | ||
| 455 | 471 | return { store, seen }; |
| 456 | 472 | }; |
| 457 | 473 | 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 | }; | |
| 458 | 479 | const old = (name: string, body: string, day: string) => { |
| 459 | 480 | const file = join(logsDir(), name); |
| 460 | 481 | writeFileSync(file, body); |
| @@ -463,8 +484,7 @@ describe("a snapshot's uploads", () => { | ||
| 463 | 484 | }; |
| 464 | 485 | |
| 465 | 486 | 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(); | |
| 468 | 488 | const { store, seen } = slowStore(15); |
| 469 | 489 | const manifest = await takeSnapshot(forge(), store); |
| 470 | 490 | expect(seen.peak).toBeGreaterThan(1); |
| @@ -501,6 +521,7 @@ describe("a snapshot's uploads", () => { | ||
| 501 | 521 | }, 20_000); |
| 502 | 522 | |
| 503 | 523 | test("a failed put fails the snapshot and names its key", async () => { |
| 524 | newLogs(); | |
| 504 | 525 | const { store } = slowStore(1, "runs/new-07.log"); |
| 505 | 526 | await expect(takeSnapshot(forge(), store)).rejects.toThrow( |
| 506 | 527 | /could not put snapshots\/.*runs\/new-07\.log/, |
| @@ -508,6 +529,7 @@ describe("a snapshot's uploads", () => { | ||
| 508 | 529 | }, 15_000); |
| 509 | 530 | |
| 510 | 531 | test("a slow store gets a progress line every so many objects", async () => { |
| 532 | newLogs(); | |
| 511 | 533 | const { store } = slowStore(10); |
| 512 | 534 | const lines: { objects: number; bytes: number }[] = []; |
| 513 | 535 | await takeSnapshot( |
| @@ -533,6 +555,7 @@ describe("a snapshot's uploads", () => { | ||
| 533 | 555 | }); |
| 534 | 556 | |
| 535 | 557 | test("older run logs go up as one object a day and restore as the logs they were", async () => { |
| 558 | newLogs(); | |
| 536 | 559 | old("a.log", "alpha\n", "2026-10-10"); |
| 537 | 560 | old("b.log", "beta\n", "2026-10-10"); |
| 538 | 561 | old("c.log", "gamma\n", "2026-10-11"); |
| @@ -574,6 +597,7 @@ describe("a snapshot's uploads", () => { | ||
| 574 | 597 | }, 15_000); |
| 575 | 598 | |
| 576 | 599 | test("the service shows a running snapshot's progress while it runs", async () => { |
| 600 | newLogs(); | |
| 577 | 601 | const { store } = slowStore(40); |
| 578 | 602 | const svc = startSnapshots(app, store, { version: "t", nightly: false }); |
| 579 | 603 | try { |
| @@ -597,6 +621,7 @@ describe("a snapshot's uploads", () => { | ||
| 597 | 621 | }, 15_000); |
| 598 | 622 | |
| 599 | 623 | test("the service says how long the snapshot in flight has run, for the release keeper to wait on (#9653)", async () => { |
| 624 | newLogs(); | |
| 600 | 625 | const { store } = slowStore(40); |
| 601 | 626 | const svc = startSnapshots(app, store, { version: "t", nightly: false }); |
| 602 | 627 | try { |
test/tui-asked.test.ts +3 −1
| @@ -124,7 +124,9 @@ describe("the asked screen", () => { | ||
| 124 | 124 | }); |
| 125 | 125 | |
| 126 | 126 | 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)); | |
| 128 | 130 | expect(asks.map((a) => a.title)).toEqual(["Older ask for the lobby", "Newer ask for the lobby"]); |
| 129 | 131 | expect(asks[0]!.secondary).toContain("alice/lobby #"); |
| 130 | 132 | expect(asks[0]!.secondary).toContain("alice"); |