Public: anyone can read this; the crew can change it.
"migrate > a migrated fork shows its trail, and its owner can hide it and bring it back" passes every time on a loaded check machine #5548
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/5326-migrate-a-migrated-fork-shows-its-trail → main 1 commit fork
2 files
changed +96
−59
src/apps/scaffold.ts +87 −57
| @@ -2674,7 +2674,7 @@ const SITE_TEST = `/** | ||
| 2674 | 2674 | * inherits every one of these and fails its own \`bun test\` if it loses one. |
| 2675 | 2675 | */ |
| 2676 | 2676 | import { afterAll, expect, test } from "bun:test"; |
| 2677 | import { mkdtempSync, readFileSync } from "node:fs"; | |
| 2677 | import { mkdtempSync, readFileSync, rmSync } from "node:fs"; | |
| 2678 | 2678 | import { tmpdir } from "node:os"; |
| 2679 | 2679 | import { join } from "node:path"; |
| 2680 | 2680 | import { compress, MIN_BYTES } from "./compress.ts"; |
| @@ -2685,13 +2685,17 @@ import { CARD, websiteLd } from "./site.ts"; | ||
| 2685 | 2685 | import { serve } from "./server.ts"; |
| 2686 | 2686 | import { ANON } from "./who.ts"; |
| 2687 | 2687 | |
| 2688 | const dataDir = mkdtempSync(join(tmpdir(), "app-")); | |
| 2688 | 2689 | const server = serve({ |
| 2689 | 2690 | port: 0, |
| 2690 | dataDir: mkdtempSync(join(tmpdir(), "app-")), | |
| 2691 | dataDir, | |
| 2691 | 2692 | identityKey: "a-key-only-the-forge-holds", |
| 2692 | 2693 | }); |
| 2693 | 2694 | const base = \`http://\${server.hostname}:\${server.port}\`; |
| 2694 | afterAll(() => server.stop(true)); | |
| 2695 | afterAll(() => { | |
| 2696 | server.stop(true); | |
| 2697 | rmSync(dataDir, { recursive: true, force: true }); | |
| 2698 | }); | |
| 2695 | 2699 | const get = (path: string, headers: Record<string, string> = {}) => |
| 2696 | 2700 | fetch(base + path, { headers, redirect: "manual" }); |
| 2697 | 2701 | |
| @@ -3481,8 +3485,8 @@ test("the terminal trusts its environment", () => { | ||
| 3481 | 3485 | }); |
| 3482 | 3486 | `; |
| 3483 | 3487 | |
| 3484 | const SIGNIN_TEST = `import { beforeAll, expect, test } from "bun:test"; | |
| 3485 | import { mkdtempSync } from "node:fs"; | |
| 3488 | const SIGNIN_TEST = `import { afterAll, beforeAll, expect, test } from "bun:test"; | |
| 3489 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 3486 | 3490 | import { tmpdir } from "node:os"; |
| 3487 | 3491 | import { join } from "node:path"; |
| 3488 | 3492 | import { openDb } from "./db.ts"; |
| @@ -3490,6 +3494,10 @@ import { begin, finish, sessionWho, verify, type Signin } from "./signin.ts"; | ||
| 3490 | 3494 | |
| 3491 | 3495 | const dir = mkdtempSync(join(tmpdir(), "app-")); |
| 3492 | 3496 | const db = openDb(dir); |
| 3497 | afterAll(() => { | |
| 3498 | db.close(); | |
| 3499 | rmSync(dir, { recursive: true, force: true }); | |
| 3500 | }); | |
| 3493 | 3501 | const ISS = "https://forge.test"; |
| 3494 | 3502 | const b64 = (v: unknown): string => Buffer.from(JSON.stringify(v)).toString("base64url"); |
| 3495 | 3503 | const generate = () => |
| @@ -3639,7 +3647,7 @@ test("a session is a cookie that names a person, and only until it expires", () | ||
| 3639 | 3647 | `; |
| 3640 | 3648 | |
| 3641 | 3649 | const DB_TEST = `import { expect, test } from "bun:test"; |
| 3642 | import { mkdtempSync } from "node:fs"; | |
| 3650 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 3643 | 3651 | import { tmpdir } from "node:os"; |
| 3644 | 3652 | import { join } from "node:path"; |
| 3645 | 3653 | import { check, migrate, migrations, openDb } from "./db.ts"; |
| @@ -3647,31 +3655,41 @@ import { check, migrate, migrations, openDb } from "./db.ts"; | ||
| 3647 | 3655 | test("migrations apply once, in order, and the file checks out", () => { |
| 3648 | 3656 | const dir = mkdtempSync(join(tmpdir(), "app-db-")); |
| 3649 | 3657 | const db = openDb(dir); |
| 3650 | expect(check(db).version).toBe(migrations.length); | |
| 3651 | // Opening again finds nothing to do — a migration that has run is history. | |
| 3652 | expect(migrate(db)).toBe(migrations.length); | |
| 3653 | expect(() => openDb(dir)).not.toThrow(); | |
| 3654 | expect(check(db)).toEqual({ version: migrations.length, integrity: "ok" }); | |
| 3655 | expect( | |
| 3656 | db.query<{ journal_mode: string }, []>("PRAGMA journal_mode").get() | |
| 3657 | ?.journal_mode, | |
| 3658 | ).toBe("wal"); | |
| 3659 | expect( | |
| 3660 | db.query<{ foreign_keys: number }, []>("PRAGMA foreign_keys").get() | |
| 3661 | ?.foreign_keys, | |
| 3662 | ).toBe(1); | |
| 3658 | try { | |
| 3659 | expect(check(db).version).toBe(migrations.length); | |
| 3660 | // Opening again finds nothing to do — a migration that has run is history. | |
| 3661 | expect(migrate(db)).toBe(migrations.length); | |
| 3662 | expect(() => openDb(dir)).not.toThrow(); | |
| 3663 | expect(check(db)).toEqual({ version: migrations.length, integrity: "ok" }); | |
| 3664 | expect( | |
| 3665 | db.query<{ journal_mode: string }, []>("PRAGMA journal_mode").get() | |
| 3666 | ?.journal_mode, | |
| 3667 | ).toBe("wal"); | |
| 3668 | expect( | |
| 3669 | db.query<{ foreign_keys: number }, []>("PRAGMA foreign_keys").get() | |
| 3670 | ?.foreign_keys, | |
| 3671 | ).toBe(1); | |
| 3672 | } finally { | |
| 3673 | db.close(); | |
| 3674 | rmSync(dir, { recursive: true, force: true }); | |
| 3675 | } | |
| 3663 | 3676 | }); |
| 3664 | 3677 | `; |
| 3665 | 3678 | |
| 3666 | const ACTIONS_TEST = `import { expect, test } from "bun:test"; | |
| 3667 | import { mkdtempSync } from "node:fs"; | |
| 3679 | const ACTIONS_TEST = `import { afterAll, expect, test } from "bun:test"; | |
| 3680 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 3668 | 3681 | import { tmpdir } from "node:os"; |
| 3669 | 3682 | import { join } from "node:path"; |
| 3670 | 3683 | import { addNote, deleteNote, listNotes } from "./actions.ts"; |
| 3671 | 3684 | import { openDb } from "./db.ts"; |
| 3672 | 3685 | import type { Who } from "./who.ts"; |
| 3673 | 3686 | |
| 3674 | const db = openDb(mkdtempSync(join(tmpdir(), "app-actions-"))); | |
| 3687 | const dir = mkdtempSync(join(tmpdir(), "app-actions-")); | |
| 3688 | const db = openDb(dir); | |
| 3689 | afterAll(() => { | |
| 3690 | db.close(); | |
| 3691 | rmSync(dir, { recursive: true, force: true }); | |
| 3692 | }); | |
| 3675 | 3693 | const anon: Who = { role: "anon" }; |
| 3676 | 3694 | const alice: Who = { role: "user", id: "u1", name: "alice" }; |
| 3677 | 3695 | const bob: Who = { role: "user", id: "u2", name: "bob" }; |
| @@ -3729,8 +3747,8 @@ const CONTRACT_TEST = `/** | ||
| 3729 | 3747 | * someone else — and calls the real actions. A rule written by hand in an |
| 3730 | 3748 | * action, or a cell changed without its door, fails here. |
| 3731 | 3749 | */ |
| 3732 | import { expect, test } from "bun:test"; | |
| 3733 | import { mkdtempSync } from "node:fs"; | |
| 3750 | import { afterAll, expect, test } from "bun:test"; | |
| 3751 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 3734 | 3752 | import { tmpdir } from "node:os"; |
| 3735 | 3753 | import { join } from "node:path"; |
| 3736 | 3754 | import { |
| @@ -3748,7 +3766,12 @@ import { contract } from "./contract.ts"; | ||
| 3748 | 3766 | import { openDb } from "./db.ts"; |
| 3749 | 3767 | import type { Who } from "./who.ts"; |
| 3750 | 3768 | |
| 3751 | const db = openDb(mkdtempSync(join(tmpdir(), "app-contract-"))); | |
| 3769 | const dir = mkdtempSync(join(tmpdir(), "app-contract-")); | |
| 3770 | const db = openDb(dir); | |
| 3771 | afterAll(() => { | |
| 3772 | db.close(); | |
| 3773 | rmSync(dir, { recursive: true, force: true }); | |
| 3774 | }); | |
| 3752 | 3775 | |
| 3753 | 3776 | /** The identity the evaluator acts as, as the forge would have signed it. */ |
| 3754 | 3777 | function whoIs(identity: string): Who { |
| @@ -3843,7 +3866,7 @@ test("every door answers as the table says, as every identity", () => { | ||
| 3843 | 3866 | `; |
| 3844 | 3867 | |
| 3845 | 3868 | const SERVER_TEST = `import { afterAll, expect, test } from "bun:test"; |
| 3846 | import { mkdtempSync } from "node:fs"; | |
| 3869 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 3847 | 3870 | import { tmpdir } from "node:os"; |
| 3848 | 3871 | import { join } from "node:path"; |
| 3849 | 3872 | import { openDb } from "./db.ts"; |
| @@ -3855,7 +3878,10 @@ const KEY = "a-key-only-the-forge-holds"; | ||
| 3855 | 3878 | const dataDir = mkdtempSync(join(tmpdir(), "app-")); |
| 3856 | 3879 | const server = serve({ port: 0, dataDir, identityKey: KEY }); |
| 3857 | 3880 | const base = \`http://\${server.hostname}:\${server.port}\`; |
| 3858 | afterAll(() => server.stop(true)); | |
| 3881 | afterAll(() => { | |
| 3882 | server.stop(true); | |
| 3883 | rmSync(dataDir, { recursive: true, force: true }); | |
| 3884 | }); | |
| 3859 | 3885 | |
| 3860 | 3886 | /** The headers the forge sends for a signed-in person. */ |
| 3861 | 3887 | const as = ( |
| @@ -4925,7 +4951,7 @@ const DESIGN_TEST = `/** | ||
| 4925 | 4951 | * anywhere but design/tokens.json. |
| 4926 | 4952 | */ |
| 4927 | 4953 | import { expect, test } from "bun:test"; |
| 4928 | import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; | |
| 4954 | import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; | |
| 4929 | 4955 | import { tmpdir } from "node:os"; |
| 4930 | 4956 | import { join } from "node:path"; |
| 4931 | 4957 | import { lint, rawColours, unknownTokens } from "../scripts/lint-design.ts"; |
| @@ -5022,13 +5048,17 @@ test("the lint refuses a raw colour and a token that is not there", () => { | ||
| 5022 | 5048 | ), |
| 5023 | 5049 | ).toEqual(["a.css:1: --spcae-2 is not a token in design/tokens.json"]); |
| 5024 | 5050 | const root = mkdtempSync(join(tmpdir(), "app-lint-")); |
| 5025 | mkdirSync(join(root, "static")); | |
| 5026 | mkdirSync(join(root, "src")); | |
| 5027 | writeFileSync(join(root, "static", "extra.css"), "a {\\n color: #fff;\\n}\\n"); | |
| 5028 | writeFileSync(join(root, "src", "ok.test.ts"), "const red = '#f00';\\n"); | |
| 5029 | expect(lint(root)).toEqual([ | |
| 5030 | "static/extra.css:2: raw colour #fff — name it in design/tokens.json and use var(--…)", | |
| 5031 | ]); | |
| 5051 | try { | |
| 5052 | mkdirSync(join(root, "static")); | |
| 5053 | mkdirSync(join(root, "src")); | |
| 5054 | writeFileSync(join(root, "static", "extra.css"), "a {\\n color: #fff;\\n}\\n"); | |
| 5055 | writeFileSync(join(root, "src", "ok.test.ts"), "const red = '#f00';\\n"); | |
| 5056 | expect(lint(root)).toEqual([ | |
| 5057 | "static/extra.css:2: raw colour #fff — name it in design/tokens.json and use var(--…)", | |
| 5058 | ]); | |
| 5059 | } finally { | |
| 5060 | rmSync(root, { recursive: true, force: true }); | |
| 5061 | } | |
| 5032 | 5062 | }); |
| 5033 | 5063 | `; |
| 5034 | 5064 | |
| @@ -5038,7 +5068,7 @@ const AUDIT_TEST = `/** | ||
| 5038 | 5068 | * written costs a log line, never the request. |
| 5039 | 5069 | */ |
| 5040 | 5070 | import { afterAll, expect, spyOn, test } from "bun:test"; |
| 5041 | import { mkdtempSync } from "node:fs"; | |
| 5071 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 5042 | 5072 | import { tmpdir } from "node:os"; |
| 5043 | 5073 | import { join } from "node:path"; |
| 5044 | 5074 | import { addNote } from "./actions.ts"; |
| @@ -5048,13 +5078,13 @@ import { serve } from "./server.ts"; | ||
| 5048 | 5078 | import { sign } from "./who.ts"; |
| 5049 | 5079 | |
| 5050 | 5080 | const KEY = "a-key-only-the-forge-holds"; |
| 5051 | const server = serve({ | |
| 5052 | port: 0, | |
| 5053 | dataDir: mkdtempSync(join(tmpdir(), "app-audit-")), | |
| 5054 | identityKey: KEY, | |
| 5055 | }); | |
| 5081 | const dataDir = mkdtempSync(join(tmpdir(), "app-audit-")); | |
| 5082 | const server = serve({ port: 0, dataDir, identityKey: KEY }); | |
| 5056 | 5083 | const base = \`http://\${server.hostname}:\${server.port}\`; |
| 5057 | afterAll(() => server.stop(true)); | |
| 5084 | afterAll(() => { | |
| 5085 | server.stop(true); | |
| 5086 | rmSync(dataDir, { recursive: true, force: true }); | |
| 5087 | }); | |
| 5058 | 5088 | |
| 5059 | 5089 | const as = ( |
| 5060 | 5090 | id: string, |
| @@ -5196,7 +5226,7 @@ test("only an admin reads the trail, at every door the table names", async () => | ||
| 5196 | 5226 | }); |
| 5197 | 5227 | |
| 5198 | 5228 | test("a trail that cannot be written is a log line, and the change still happens", () => { |
| 5199 | const db = openDb(mkdtempSync(join(tmpdir(), "app-audit-broken-"))); | |
| 5229 | const db = openDb(mkdtempSync(join(dataDir, "broken-"))); | |
| 5200 | 5230 | db.exec("DROP TABLE audit"); |
| 5201 | 5231 | const said = spyOn(console, "error").mockImplementation(() => {}); |
| 5202 | 5232 | try { |
| @@ -5227,7 +5257,7 @@ const SECURITY_TEST = `/** | ||
| 5227 | 5257 | * table says could never pass — and nobody it would let through. |
| 5228 | 5258 | */ |
| 5229 | 5259 | import { afterAll, expect, test } from "bun:test"; |
| 5230 | import { mkdtempSync } from "node:fs"; | |
| 5260 | import { mkdtempSync, rmSync } from "node:fs"; | |
| 5231 | 5261 | import { tmpdir } from "node:os"; |
| 5232 | 5262 | import { join } from "node:path"; |
| 5233 | 5263 | import { readContract } from "./can.ts"; |
| @@ -5245,13 +5275,13 @@ import { serve } from "./server.ts"; | ||
| 5245 | 5275 | import { sign } from "./who.ts"; |
| 5246 | 5276 | |
| 5247 | 5277 | const KEY = "a-key-only-the-forge-holds"; |
| 5248 | const server = serve({ | |
| 5249 | port: 0, | |
| 5250 | dataDir: mkdtempSync(join(tmpdir(), "app-door-")), | |
| 5251 | identityKey: KEY, | |
| 5252 | }); | |
| 5278 | const dataDir = mkdtempSync(join(tmpdir(), "app-door-")); | |
| 5279 | const server = serve({ port: 0, dataDir, identityKey: KEY }); | |
| 5253 | 5280 | const base = \`http://\${server.hostname}:\${server.port}\`; |
| 5254 | afterAll(() => server.stop(true)); | |
| 5281 | afterAll(() => { | |
| 5282 | server.stop(true); | |
| 5283 | rmSync(dataDir, { recursive: true, force: true }); | |
| 5284 | }); | |
| 5255 | 5285 | const as = (id: string, role: "user" | "admin"): Record<string, string> => { |
| 5256 | 5286 | const identity = \`\${id}|\${id}|\${role}|\${Date.now() + 60_000}\`; |
| 5257 | 5287 | return { |
| @@ -5366,7 +5396,7 @@ test("the API's description is shut to strangers when the table opens them no do | ||
| 5366 | 5396 | }); |
| 5367 | 5397 | |
| 5368 | 5398 | test("strangers are rationed at the API, reads too", () => { |
| 5369 | const db = openDb(mkdtempSync(join(tmpdir(), "app-door-rate-"))); | |
| 5399 | const db = openDb(mkdtempSync(join(dataDir, "rate-"))); | |
| 5370 | 5400 | const knock = () => |
| 5371 | 5401 | door( |
| 5372 | 5402 | db, |
| @@ -5417,7 +5447,7 @@ const OPENAPI_TEST = `/** | ||
| 5417 | 5447 | * app serves exactly that file. |
| 5418 | 5448 | */ |
| 5419 | 5449 | import { afterAll, expect, test } from "bun:test"; |
| 5420 | import { mkdtempSync, readFileSync } from "node:fs"; | |
| 5450 | import { mkdtempSync, readFileSync, rmSync } from "node:fs"; | |
| 5421 | 5451 | import { tmpdir } from "node:os"; |
| 5422 | 5452 | import { join } from "node:path"; |
| 5423 | 5453 | import { emit, openapi } from "./openapi.ts"; |
| @@ -5474,12 +5504,12 @@ test("every API route is described, and every door names its cell of the table", | ||
| 5474 | 5504 | }); |
| 5475 | 5505 | |
| 5476 | 5506 | test("GET /openapi.json serves the committed file", async () => { |
| 5477 | const server = serve({ | |
| 5478 | port: 0, | |
| 5479 | dataDir: mkdtempSync(join(tmpdir(), "app-openapi-")), | |
| 5480 | identityKey: "k", | |
| 5507 | const dataDir = mkdtempSync(join(tmpdir(), "app-openapi-")); | |
| 5508 | const server = serve({ port: 0, dataDir, identityKey: "k" }); | |
| 5509 | afterAll(() => { | |
| 5510 | server.stop(true); | |
| 5511 | rmSync(dataDir, { recursive: true, force: true }); | |
| 5481 | 5512 | }); |
| 5482 | afterAll(() => server.stop(true)); | |
| 5483 | 5513 | const response = await fetch( |
| 5484 | 5514 | \`http://\${server.hostname}:\${server.port}/openapi.json\`, |
| 5485 | 5515 | ); |
test/scaffold.test.ts +9 −2
| @@ -32,13 +32,14 @@ const SUITE_BUDGET_MS = 3_000; | ||
| 32 | 32 | const run = async ( |
| 33 | 33 | cwd: string, |
| 34 | 34 | argv: string[], |
| 35 | temp = process.env.TMPDIR ?? tmpdir(), | |
| 35 | 36 | ): Promise<{ code: number; out: string }> => { |
| 36 | 37 | const child = Bun.spawn(argv, { |
| 37 | 38 | cwd, |
| 38 | 39 | env: { |
| 39 | 40 | PATH: process.env.PATH ?? "/usr/bin:/bin", |
| 40 | 41 | HOME: process.env.HOME ?? cwd, |
| 41 | TMPDIR: process.env.TMPDIR ?? tmpdir(), | |
| 42 | TMPDIR: temp, | |
| 42 | 43 | }, |
| 43 | 44 | stdout: "pipe", |
| 44 | 45 | stderr: "pipe", |
| @@ -74,12 +75,18 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 74 | 75 | test("its own suite is green, inside its 3 s budget", async () => { |
| 75 | 76 | // Every deploy and every pull on the app waits on this suite, so it is |
| 76 | 77 | // held to a budget (docs/APPS.md, "What a new app starts as"). |
| 78 | const temp = await mkdtemp(join(tmpdir(), "forge-scaffold-tmp-")); | |
| 77 | 79 | const started = performance.now(); |
| 78 | const { code, out } = await run(dir, [process.execPath, "test"]); | |
| 80 | const { code, out } = await run(dir, [process.execPath, "test"], temp); | |
| 79 | 81 | const ms = Math.round(performance.now() - started); |
| 80 | 82 | console.log(`the scaffold's own suite ran in ${ms} ms (budget ${SUITE_BUDGET_MS} ms)`); |
| 81 | 83 | expect([code, out.includes(" 0 fail")]).toEqual([0, true]); |
| 82 | 84 | expect(ms).toBeLessThanOrEqual(SUITE_BUDGET_MS); |
| 85 | // It leaves its temporary directory as it found it: a check cell's /tmp | |
| 86 | // is a small tmpfs every scaffolded suite in the forge's own check shares | |
| 87 | // (#5326), and every app's own check cell has one too. | |
| 88 | expect(await readdir(temp)).toEqual([]); | |
| 89 | await rm(temp, { recursive: true, force: true }); | |
| 83 | 90 | // Each guarantee has its own file of tests in the suite. |
| 84 | 91 | for (const name of [ |
| 85 | 92 | "design.test.ts", |
new above
offline