Public: anyone can read this; the crew can change it.
test/scaffold.test.ts passes in the builder's sandbox: the scaffolded app's typecheck and its OpenID sign-in #11058
Check · Review · Land · Live done — Landed and live in production.
scout/forge:scout/11051-test-scaffold-test-ts-passes-in-the-buil → main 1 commit fork
1 file
changed +31
−19
test/scaffold.test.ts +31 −19
| @@ -28,11 +28,16 @@ import { scaffoldFiles } from "../src/apps/scaffold.ts"; | ||
| 28 | 28 | /** How long the written-out app's `bun test` may take, wall clock. */ |
| 29 | 29 | const SUITE_BUDGET_MS = 3_000; |
| 30 | 30 | |
| 31 | /** A child that inherits nothing it was not handed. */ | |
| 31 | /** | |
| 32 | * A child that inherits nothing it was not handed. Its TMPDIR is a directory | |
| 33 | * made inside the test's own temp dir: the builder's sandbox holds anything | |
| 34 | * else read-only, and a child bun refuses to start without a tempdir it can | |
| 35 | * write (#11051). | |
| 36 | */ | |
| 32 | 37 | const run = async ( |
| 33 | 38 | cwd: string, |
| 34 | 39 | argv: string[], |
| 35 | temp = process.env.TMPDIR ?? tmpdir(), | |
| 40 | temp: string, | |
| 36 | 41 | ): Promise<{ code: number; out: string }> => { |
| 37 | 42 | const child = Bun.spawn(argv, { |
| 38 | 43 | cwd, |
| @@ -53,7 +58,9 @@ const run = async ( | ||
| 53 | 58 | }; |
| 54 | 59 | |
| 55 | 60 | describe("a scaffolded app, written out and run on its own", () => { |
| 61 | let scratch: string; | |
| 56 | 62 | let dir: string; |
| 63 | let temp: string; | |
| 57 | 64 | const files = scaffoldFiles({ name: "field-notes", description: "" }); |
| 58 | 65 | const content = (path: string): string => { |
| 59 | 66 | const file = files.find((f) => f.path === path); |
| @@ -63,21 +70,24 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 63 | 70 | }; |
| 64 | 71 | |
| 65 | 72 | beforeAll(async () => { |
| 66 | dir = await mkdtemp(join(tmpdir(), "forge-scaffold-")); | |
| 73 | scratch = await mkdtemp(join(tmpdir(), "forge-scaffold-")); | |
| 74 | dir = join(scratch, "app"); | |
| 75 | temp = join(scratch, "tmp"); | |
| 76 | await mkdir(temp); | |
| 67 | 77 | for (const file of files) { |
| 68 | 78 | const path = join(dir, file.path); |
| 69 | 79 | await mkdir(dirname(path), { recursive: true }); |
| 70 | 80 | await writeFile(path, file.content as string); |
| 71 | 81 | } |
| 72 | 82 | }); |
| 73 | afterAll(() => rm(dir, { recursive: true, force: true })); | |
| 83 | afterAll(() => rm(scratch, { recursive: true, force: true })); | |
| 74 | 84 | |
| 75 | 85 | test("its own suite is green, inside its 3 s budget", async () => { |
| 76 | 86 | // Every deploy and every pull on the app waits on this suite, so it is |
| 77 | 87 | // held to a budget (docs/APPS.md, "What a new app starts as"). |
| 78 | const temp = await mkdtemp(join(tmpdir(), "forge-scaffold-tmp-")); | |
| 88 | const own = await mkdtemp(join(scratch, "suite-tmp-")); | |
| 79 | 89 | const started = performance.now(); |
| 80 | const { code, out } = await run(dir, [process.execPath, "test"], temp); | |
| 90 | const { code, out } = await run(dir, [process.execPath, "test"], own); | |
| 81 | 91 | const ms = Math.round(performance.now() - started); |
| 82 | 92 | console.log(`the scaffold's own suite ran in ${ms} ms (budget ${SUITE_BUDGET_MS} ms)`); |
| 83 | 93 | expect([code, out.includes(" 0 fail")]).toEqual([0, true]); |
| @@ -85,8 +95,7 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 85 | 95 | // It leaves its temporary directory as it found it: a check cell's /tmp |
| 86 | 96 | // is a small tmpfs every scaffolded suite in the forge's own check shares |
| 87 | 97 | // (#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 }); | |
| 98 | expect(await readdir(own)).toEqual([]); | |
| 90 | 99 | // Each guarantee has its own file of tests in the suite. |
| 91 | 100 | for (const name of [ |
| 92 | 101 | "design.test.ts", |
| @@ -101,12 +110,12 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 101 | 110 | |
| 102 | 111 | test("the design lint passes, and refuses a colour spelled out", async () => { |
| 103 | 112 | expect( |
| 104 | (await run(dir, [process.execPath, "run", "lint:design"])).code, | |
| 113 | (await run(dir, [process.execPath, "run", "lint:design"], temp)).code, | |
| 105 | 114 | ).toBe(0); |
| 106 | 115 | const css = join(dir, "static", "app.css"); |
| 107 | 116 | const before = await readFile(css, "utf8"); |
| 108 | 117 | await writeFile(css, `${before}\n.oops { color: #ff0000; }\n`); |
| 109 | const refused = await run(dir, [process.execPath, "run", "lint:design"]); | |
| 118 | const refused = await run(dir, [process.execPath, "run", "lint:design"], temp); | |
| 110 | 119 | await writeFile(css, before); |
| 111 | 120 | expect(refused.code).toBe(1); |
| 112 | 121 | expect(refused.out).toContain("static/app.css"); |
| @@ -115,7 +124,7 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 115 | 124 | |
| 116 | 125 | test("the committed API description is exactly what contract:emit writes", async () => { |
| 117 | 126 | const committed = content("contract/openapi.json"); |
| 118 | const emitted = await run(dir, [process.execPath, "run", "contract:emit"]); | |
| 127 | const emitted = await run(dir, [process.execPath, "run", "contract:emit"], temp); | |
| 119 | 128 | expect(emitted.code).toBe(0); |
| 120 | 129 | expect(await readFile(join(dir, "contract", "openapi.json"), "utf8")).toBe( |
| 121 | 130 | committed, |
| @@ -130,9 +139,11 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 130 | 139 | }, 30_000); |
| 131 | 140 | |
| 132 | 141 | test("it typechecks under strict settings, with the forge's own type definitions", async () => { |
| 133 | // The app ships no dependencies, so the checker borrows the forge's | |
| 134 | // Bun types; the app's code itself must be clean under the same strictness | |
| 135 | // the forge holds itself to. | |
| 142 | // The app ships no dependencies, so the checker borrows the forge's own | |
| 143 | // tsc and Bun types, run straight from node_modules: `bun x` would reach | |
| 144 | // for a package cache and the registry, neither of which a sandbox | |
| 145 | // promises (#11051). The app's code itself must be clean under the same | |
| 146 | // strictness the forge holds itself to. | |
| 136 | 147 | const root = fileURLToPath(new URL("..", import.meta.url)); |
| 137 | 148 | const sources = [ |
| 138 | 149 | ...(await readdir(join(dir, "src"))).map((f) => join("src", f)), |
| @@ -140,8 +151,7 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 140 | 151 | ].filter((f) => f.endsWith(".ts")); |
| 141 | 152 | const { code, out } = await run(dir, [ |
| 142 | 153 | process.execPath, |
| 143 | "x", | |
| 144 | "tsc", | |
| 154 | join(root, "node_modules", "typescript", "bin", "tsc"), | |
| 145 | 155 | "--noEmit", |
| 146 | 156 | "--strict", |
| 147 | 157 | "--target", |
| @@ -158,9 +168,8 @@ describe("a scaffolded app, written out and run on its own", () => { | ||
| 158 | 168 | "--typeRoots", |
| 159 | 169 | `${root}node_modules/@types,${root}node_modules`, |
| 160 | 170 | ...sources, |
| 161 | ]); | |
| 162 | // tsc's exit code is the verdict: `bun x` may print its own install | |
| 163 | // lines on a fresh machine (#664). On a failure, the output is the | |
| 171 | ], temp); | |
| 172 | // tsc's exit code is the verdict. On a failure, the output is the | |
| 164 | 173 | // reason, so the assertion shows it beside the code. |
| 165 | 174 | expect({ code, out: code === 0 ? "" : out.trim() }).toEqual({ code: 0, out: "" }); |
| 166 | 175 | }, 120_000); |
| @@ -180,6 +189,8 @@ describe("a scaffolded app signs a person in through the forge", () => { | ||
| 180 | 189 | const root = await mkdtemp(join(tmpdir(), "forge-signin-")); |
| 181 | 190 | const appDir = join(root, "app"); |
| 182 | 191 | const dataDir = join(root, "data"); |
| 192 | const temp = join(root, "tmp"); | |
| 193 | await mkdir(temp); | |
| 183 | 194 | for (const file of scaffoldFiles({ name: "field-notes", description: "" })) { |
| 184 | 195 | const path = join(appDir, file.path); |
| 185 | 196 | await mkdir(dirname(path), { recursive: true }); |
| @@ -211,6 +222,7 @@ describe("a scaffolded app signs a person in through the forge", () => { | ||
| 211 | 222 | FORGE_APP_DATA: dataDir, |
| 212 | 223 | FORGE_APP_URL: base, |
| 213 | 224 | FORGE_URL: servers.url, |
| 225 | TMPDIR: temp, | |
| 214 | 226 | }, |
| 215 | 227 | stdout: "ignore", |
| 216 | 228 | stderr: "ignore", |