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

test/shards.test.ts runs in under 1 s again: it took 0.3 s on 2026-10-21 and 10.4 s on main today #8315

Merged opened this

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

Merged

mason/forge:mason/8310-test-shards-test-ts-runs-in-under-1-s-ag → main 1 commit fork 1 thread · 1 unresolved

  • Merged. mason/8310-test-shards-test-ts-runs-in-under-1-s-ag was merged into main as 3631e96.

Review

Sign in to review this change.
7 files changed +441 −416
scripts/test-times.json +1
@@ -46,6 +46,7 @@
4646 "test/offline.test.ts": 3.5,
4747 "test/pulls.test.ts": 14.6,
4848 "test/run-secrets.test.ts": 2.8,
49 "test/shards-run.test.ts": 12.5,
4950 "test/shards.test.ts": 0.3,
5051 "test/share.test.ts": 16.2,
5152 "test/spawn-plan.test.ts": 0.2,
test/fixtures/shard-broken.test.ts +1 −1
@@ -1,4 +1,4 @@
1 // Fixture for test/shards.test.ts, kept out of the gate: fails alone too.
1 // Fixture for test/shards-run.test.ts, kept out of the gate: fails alone too.
22 import { expect, test } from "bun:test";
33
44 test("broken", () => {
test/fixtures/shard-errors.test.ts +1 −1
@@ -1,2 +1,2 @@
1 // Fixture for test/shards.test.ts, kept out of the gate: errors while loading.
1 // Fixture for test/shards-run.test.ts, kept out of the gate: errors while loading.
22 throw new Error("boom at load");
test/fixtures/shard-poison.test.ts +1 −1
@@ -1,4 +1,4 @@
1 // Fixture for test/shards.test.ts, kept out of the gate: sets a global that
1 // Fixture for test/shards-run.test.ts, kept out of the gate: sets a global that
22 // shard-victim.test.ts cannot stand beside. Both pass alone.
33 import { test } from "bun:test";
44
test/fixtures/shard-victim.test.ts +1 −1
@@ -1,4 +1,4 @@
1 // Fixture for test/shards.test.ts, kept out of the gate: fails only in a
1 // Fixture for test/shards-run.test.ts, kept out of the gate: fails only in a
22 // process where shard-poison.test.ts has already run.
33 import { expect, test } from "bun:test";
44
test/shards-run.test.ts added +423
@@ -0,0 +1,423 @@
1 /**
2 * The gate's runner (`scripts/test-shards.ts`) end to end: each test spawns
3 * it, and it spawns `bun test`, so each costs a few hundred milliseconds.
4 * The pure parts it is built from (`scripts/shards.ts`) are proved in
5 * test/shards.test.ts, which stays fast because it spawns nothing.
6 */
7 import { afterAll, describe, expect, test } from "bun:test";
8 import { mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
9 import { tmpdir } from "node:os";
10 import { join } from "node:path";
11 import { CRASH_RERUNS } from "../scripts/shards.ts";
12 import { EXCLUDED, gateFiles, presentTests } from "../scripts/gate.ts";
13
14 describe("the shard's output", () => {
15 // A check's log is this script's stdout, line by line, as the forge
16 // reads it (`checks/service.ts`). Holding a shard's output until it
17 // ended meant a forty-minute gate showed nothing at all until the end.
18 test("one shard at a time: a line is written when the test says it, not when the shard ends", async () => {
19 const dir = mkdtempSync(join(tmpdir(), "shards-live-"));
20 writeFileSync(
21 join(dir, "live.test.ts"),
22 `import { test } from "bun:test";
23 test("says two things, a pause apart", async () => {
24 console.log("tick-one");
25 await Bun.sleep(1500);
26 console.log("tick-two");
27 });
28 `,
29 );
30 const script = new URL("../scripts/test-shards.ts", import.meta.url)
31 .pathname;
32 const child = Bun.spawn(["bun", script, "live.test.ts"], {
33 cwd: dir,
34 env: { ...process.env, FORGE_CHECK_SHARDS: "1" },
35 stdin: "ignore",
36 stdout: "pipe",
37 stderr: "pipe",
38 });
39 let text = "";
40 let firstAt = -1;
41 const decoder = new TextDecoder();
42 const reading = (async () => {
43 for await (const chunk of child.stdout) {
44 text += decoder.decode(chunk, { stream: true });
45 if (firstAt < 0 && text.includes("tick-one"))
46 firstAt = performance.now();
47 }
48 })();
49 const code = await child.exited;
50 const ended = performance.now();
51 await reading;
52 rmSync(dir, { recursive: true, force: true });
53 expect(code).toBe(0);
54 expect(firstAt).toBeGreaterThan(0);
55 // The first line reached us while the test was still asleep.
56 expect(ended - firstAt).toBeGreaterThan(1000);
57 expect(text).toContain("tick-two");
58 expect(text).toContain("── shard 1/1: live.test.ts");
59 expect(text).toMatch(/── shard 1\/1 done \(exit 0, [\d.]+s\)/);
60 }, 30_000);
61 });
62
63 describe("the gate audit before any shard", () => {
64 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
65 const listed = presentTests();
66 const git = (dir: string, ...args: string[]) =>
67 Bun.spawnSync(["git", "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...args], {
68 cwd: dir,
69 stdout: "ignore",
70 stderr: "ignore",
71 }).exitCode;
72 // `stopAt`: kill the runner once stderr carries it. The audit and the slice
73 // banner come before any bun test spawns; what runs after them is the
74 // machine's load, not the code under test.
75 const run = async (
76 extra: string[],
77 args: string[],
78 drop: string[] = [],
79 commit = false,
80 stopAt?: string,
81 ) => {
82 const dir = mkdtempSync(join(tmpdir(), "forge-audit-"));
83 mkdirSync(join(dir, "test"));
84 for (const file of listed) if (!drop.includes(file)) writeFileSync(join(dir, file), "");
85 if (commit) {
86 expect(git(dir, "init", "-q")).toBe(0);
87 expect(git(dir, "add", "-A")).toBe(0);
88 expect(git(dir, "commit", "-qm", "base")).toBe(0);
89 }
90 for (const file of extra) writeFileSync(join(dir, file), "");
91 if (commit) {
92 expect(git(dir, "add", "-A")).toBe(0);
93 expect(git(dir, "commit", "-qm", "add a test")).toBe(0);
94 }
95 const started = performance.now();
96 const child = Bun.spawn(["bun", script, ...args], {
97 cwd: dir,
98 stdin: "ignore",
99 stdout: "pipe",
100 stderr: "pipe",
101 });
102 const watch = async (stream: ReadableStream<Uint8Array>) => {
103 let text = "";
104 for await (const chunk of stream) {
105 text += new TextDecoder().decode(chunk);
106 if (stopAt !== undefined && text.includes(stopAt)) child.kill();
107 }
108 return text;
109 };
110 const [out, err] = await Promise.all([new Response(child.stdout).text(), watch(child.stderr)]);
111 const code = await child.exited;
112 rmSync(dir, { recursive: true, force: true });
113 return { code, out, err, ms: performance.now() - started };
114 };
115
116 test("a stale exclusion is refused by name, last line, nothing spawned", async () => {
117 const gone = EXCLUDED[0]!.file;
118 for (const args of [["--gate"], ["--tier", "fast", "--diff", "a..b"]]) {
119 const got = await run([], args, [gone]);
120 expect(got.code).toBe(2);
121 const last = got.err.trim().split("\n").pop()!;
122 expect(last).toBe(`test-shards: ${gone}: fix its EXCLUDED entry in scripts/gate.ts`);
123 expect(got.out + got.err).not.toContain("start ");
124 expect(got.out).not.toContain("── shard");
125 expect(got.ms).toBeLessThan(1000);
126 }
127 });
128
129 test("a new file is in the gate with no list naming it, and explicit files are not audited", async () => {
130 // One slice holds every file, so the count includes the new one; a shard
131 // per file would outgrow the three digits a shard spec allows.
132 const all = String(gateFiles().length + 1);
133 const got = await run(["test/zz-new.test.ts"], ["--gate", "--shard", "1/1"], [], false, "matrix slice");
134 expect(got.err).not.toContain("EXCLUDED");
135 expect(got.out).toContain(`matrix slice 1/1 (${all} of ${all} files)`);
136 const explicit = await run(["test/zz-new.test.ts"], ["test/zz-new.test.ts"], [listed[0]!], false, "test-shards: 1 files");
137 expect(explicit.err).not.toContain("EXCLUDED");
138 }, 30_000);
139
140 test("a pull that only adds test/x.test.ts is gated on it (#2037)", async () => {
141 const got = await run(["test/zz-new.test.ts"], ["--tier", "fast", "--diff", "HEAD~1..HEAD"], [], true);
142 expect(got.err).not.toContain("EXCLUDED");
143 expect(got.out).toContain("for 1 changed");
144 expect(got.out).toContain(" test/zz-new.test.ts: changed\n");
145 }, 30_000);
146
147 test("a pull no test reaches is green with nothing to run, not a usage error (#3579)", async () => {
148 const dir = mkdtempSync(join(tmpdir(), "forge-audit-"));
149 mkdirSync(join(dir, "test"));
150 for (const file of listed) writeFileSync(join(dir, file), "");
151 writeFileSync(join(dir, "impact.json"), JSON.stringify(Object.fromEntries(listed.map((f) => [f, []]))));
152 expect(git(dir, "init", "-q")).toBe(0);
153 expect(git(dir, "add", "-A")).toBe(0);
154 expect(git(dir, "commit", "-qm", "base")).toBe(0);
155 mkdirSync(join(dir, "scripts"));
156 writeFileSync(join(dir, "scripts", "test-times.json"), "{}\n");
157 expect(git(dir, "add", "-A")).toBe(0);
158 expect(git(dir, "commit", "-qm", "a table only")).toBe(0);
159 const child = Bun.spawn(["bun", script, "--tier", "fast", "--diff", "HEAD~1..HEAD"], {
160 cwd: dir,
161 env: { ...process.env, FORGE_CHECK_IMPACT: "impact.json" },
162 stdin: "ignore",
163 stdout: "pipe",
164 stderr: "pipe",
165 });
166 const [out, err] = await Promise.all([
167 new Response(child.stdout).text(),
168 new Response(child.stderr).text(),
169 ]);
170 const code = await child.exited;
171 rmSync(dir, { recursive: true, force: true });
172 expect(out).toContain(`0 of ${gateFiles().length} files for 1 changed`);
173 expect(out).toContain("test-shards: the change reaches no tests; nothing to run");
174 expect(err).not.toContain("name the test files");
175 expect(code).toBe(0);
176 }, 30_000);
177 });
178
179 describe("the gate audit alone, first in the check", () => {
180 const script = new URL("../scripts/gate.ts", import.meta.url).pathname;
181 const listed = presentTests();
182 // The check sets FORGE_CHECK_BASE; the scratch dir is no repo, so it has none.
183 const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== "FORGE_CHECK_BASE"));
184 const run = async (extra: string[], drop: string[] = []) => {
185 const dir = mkdtempSync(join(tmpdir(), "forge-gate-"));
186 mkdirSync(join(dir, "test"));
187 for (const file of [...listed, ...extra]) if (!drop.includes(file)) writeFileSync(join(dir, file), "");
188 const child = Bun.spawn(["bun", script], { cwd: dir, env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
189 const err = await new Response(child.stderr).text();
190 const code = await child.exited;
191 rmSync(dir, { recursive: true, force: true });
192 return { code, err };
193 };
194
195 test("a stale exclusion fails it by name; a new unlisted file passes", async () => {
196 const gone = EXCLUDED[0]!.file;
197 const red = await run([], [gone]);
198 expect(red.code).toBe(2);
199 expect(red.err.trim().split("\n").pop()).toBe(
200 `gate: ${gone}: fix its EXCLUDED entry in scripts/gate.ts`,
201 );
202 const green = await run(["test/zz-new.test.ts"]);
203 expect(green.code).toBe(0);
204 // The scratch dir is no git repo, so the pin check says it had no base.
205 expect(green.err).toBe("gate: no base to compare to (set FORGE_CHECK_BASE): migration pins not checked\n");
206 }, 30_000);
207 });
208
209 describe("a failing file is run once more alone", () => {
210 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
211 const dir = "test/fixtures";
212 // Heavy by bytes, so the splitter gives it a shard of its own and the
213 // other fixtures share the second one. Written here, not committed.
214 const scratch = mkdtempSync(join(tmpdir(), "shard-filler-"));
215 const filler = join(scratch, "shard-filler.test.ts");
216 writeFileSync(filler, `import { test } from "bun:test";\ntest("filler", () => {});\n// ${"padding ".repeat(4000)}\n`);
217 afterAll(() => rmSync(scratch, { recursive: true, force: true }));
218 // The split must not follow the host: pin the count and the cell's CPUs, and
219 // drop any FORGE_CHECK_* the surrounding gate exported (#2726).
220 const env = { ...process.env, FORGE_CHECK_SHARDS: "2", FORGE_CELL_CPUS: "4" } as Record<string, string | undefined>;
221 for (const name of Object.keys(env)) if (name.startsWith("FORGE_CHECK_") && name !== "FORGE_CHECK_SHARDS") delete env[name];
222 const run = async (names: string[]) => {
223 const child = Bun.spawn(
224 ["bun", script, ...names.map((name) => (name === "filler" ? filler : `${dir}/shard-${name}.test.ts`))],
225 {
226 stdin: "ignore",
227 stdout: "pipe",
228 stderr: "pipe",
229 env,
230 },
231 );
232 const [out, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
233 return { code: await child.exited, out, err };
234 };
235
236 test("red only beside another file: the shard is green and the tail names it", async () => {
237 // filler is heavy by bytes, so poison and victim share the other shard.
238 const got = await run(["filler", "poison", "victim"]);
239 expect(got.out).toContain("fails only beside others: test/fixtures/shard-victim.test.ts (shard-mates: none)");
240 expect(got.code).toBe(0);
241 }, 30_000);
242
243 test("red alone too: it stays red", async () => {
244 const got = await run(["filler", "poison", "victim", "broken"]);
245 expect(got.out).toContain("fails only beside others: test/fixtures/shard-victim.test.ts (shard-mates: test/fixtures/shard-broken.test.ts)");
246 expect(got.code).toBe(1);
247 expect(got.out).toContain("(fail) broken");
248 }, 30_000);
249
250 test("one file fails beside others and another errors: the shard stays red", async () => {
251 const got = await run(["filler", "poison", "victim", "errors"]);
252 expect(got.code).toBe(1);
253 expect(got.out).not.toContain("fails only beside others");
254 expect(got.out).toContain("boom at load");
255 }, 30_000);
256
257 test("a lone file that fails stays red with no verdict line", async () => {
258 const got = await run(["broken"]);
259 expect(got.code).toBe(1);
260 expect(got.out).not.toContain("fails only beside others");
261 }, 30_000);
262 });
263
264 describe("every shard runs to the end, and a crashed one once more", () => {
265 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
266 const scratch = mkdtempSync(join(tmpdir(), "shard-crash-"));
267 // Heavy by bytes, so it runs first and gets a shard of its own.
268 const heavyRed = join(scratch, "heavy-red.test.ts");
269 writeFileSync(heavyRed, `import { expect, test } from "bun:test";\ntest("heavy red", () => { expect(1).toBe(2); });\n// ${"padding ".repeat(4000)}\n`);
270 const filler = join(scratch, "shard-filler.test.ts");
271 writeFileSync(filler, `import { test } from "bun:test";\ntest("filler", () => {});\n// ${"padding ".repeat(4000)}\n`);
272 afterAll(() => rmSync(scratch, { recursive: true, force: true }));
273 const run = async (shards: string, files: string[], mark: string) => {
274 const env = { ...process.env, FORGE_CHECK_SHARDS: shards, FORGE_CELL_CPUS: "4", SHARD_CRASH_MARK: mark } as Record<string, string | undefined>;
275 for (const name of Object.keys(env)) if (name.startsWith("FORGE_CHECK_") && name !== "FORGE_CHECK_SHARDS") delete env[name];
276 const child = Bun.spawn(["bun", script, ...files], { stdin: "ignore", stdout: "pipe", stderr: "pipe", env });
277 const [out, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
278 return { code: await child.exited, out, err };
279 };
280
281 test("a red file does not stop the files after it", async () => {
282 // One worker, heaviest first: the red file runs before the green one.
283 const got = await run("1", [heavyRed, "test/fixtures/shard-victim.test.ts"], join(scratch, "unused"));
284 expect(got.code).toBe(1);
285 expect(got.out).toContain("(fail) heavy red");
286 expect(got.out).toContain("── shard 2/2: test/fixtures/shard-victim.test.ts");
287 expect(got.out).not.toContain("fail-fast");
288 }, 30_000);
289
290 test("a crash between tests re-runs its shard once, and the re-run is the verdict", async () => {
291 const got = await run("2", [filler, "test/fixtures/shard-crash-once.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "crashed-once"));
292 expect(got.out).toContain("crashed outside any test (exit 1); re-running it once. The crashed run:");
293 expect(got.out).toContain(" │ error: Cannot use a closed database");
294 expect(got.code).toBe(0);
295 // Green on the re-run: the crash is named for the check to record as a flake.
296 expect(got.err).toMatch(/test-shards: crash flake \d+\/\d+: error: Cannot use a closed database/);
297 }, 30_000);
298
299 test("a single worker quotes the crashed run too: no bare error lines, and the re-run is green", async () => {
300 const got = await run("1", ["test/fixtures/shard-crash-once.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "crashed-serial"));
301 expect(got.out).toContain("crashed outside any test (exit 1); re-running it once. The crashed run:");
302 expect(got.out).toContain(" │ error: Cannot use a closed database");
303 const bare = got.out.split("\n").filter((line) => !line.startsWith(" │ "));
304 expect(bare.some((line) => line.includes("Cannot use a closed database"))).toBe(false);
305 expect(bare.some((line) => line.includes("# Unhandled error"))).toBe(false);
306 expect(got.code).toBe(0);
307 }, 30_000);
308
309 test("only CRASH_RERUNS crashed shards are re-run; the next stays red unre-run", async () => {
310 const crashing = [1, 2, 3, 4].map((n) => {
311 const file = join(scratch, `crash-${n}.test.ts`);
312 writeFileSync(file, `throw new Error("boom at load ${n}");\n`);
313 return file;
314 });
315 const got = await run("4", crashing, join(scratch, "unused"));
316 expect(got.out.split("re-running it once").length - 1).toBe(CRASH_RERUNS);
317 expect(got.code).toBe(1);
318 }, 60_000);
319
320 test("a crash that happens again stays red", async () => {
321 const got = await run("2", [filler, "test/fixtures/shard-errors.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "unused"));
322 expect(got.out).toContain("re-running it once");
323 expect(got.out).toContain("boom at load");
324 expect(got.code).toBe(1);
325 }, 30_000);
326 });
327
328 describe("a fixture suite in six shards (#7617)", () => {
329 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
330 const fixture = (files: Record<string, string>): string => {
331 const dir = mkdtempSync(join(tmpdir(), "shards-fixture-"));
332 for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text);
333 return dir;
334 };
335 const run = async (
336 dir: string,
337 shards: string,
338 names: string[],
339 env: Record<string, string> = {},
340 ): Promise<{ code: number; out: string; stdout: string }> => {
341 const child = Bun.spawn(["bun", script, ...names], {
342 cwd: dir,
343 env: { ...process.env, FORGE_CHECK_SHARDS: shards, ...env },
344 stdin: "ignore",
345 stdout: "pipe",
346 stderr: "pipe",
347 });
348 const [stdout, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
349 const code = await child.exited;
350 return { code, out: stdout + err, stdout };
351 };
352 const passing = (n: number): string =>
353 `import { test } from "bun:test";\ntest("passes ${n}", () => {});\n`;
354 /** The files named on the `── shard N/M…: files` lines, in the order shards ran. */
355 const finished = (stdout: string): string[] =>
356 [...stdout.matchAll(/^── shard \d+\/\d+(?: \(exit \d+, [\d.]+s\))?: (.*)$/gm)].map((m) => m[1]!);
357
358 test("twelve files in six shards: the plan has six slices and each file runs exactly once", async () => {
359 const names = Array.from({ length: 12 }, (_, i) => `s${i}.test.ts`);
360 const dir = fixture(Object.fromEntries(names.map((name, i) => [name, passing(i)])));
361 const six = await run(dir, "6", names);
362 rmSync(dir, { recursive: true, force: true });
363 expect(six.code).toBe(0);
364 expect(six.out).toContain("12 files in 6 shards");
365 expect(six.out.match(/test-shards: start \d+\/6 /g)?.length).toBe(6);
366 const ran = finished(six.stdout).flatMap((line) => line.split(" "));
367 for (const name of names) expect(ran.filter((file) => file === name).length).toBe(1);
368 expect(six.out).toMatch(/Ran 12 tests across 12 files/);
369 }, 60_000);
370
371 test("a failure in any shard reds the run and names the test", async () => {
372 const files: Record<string, string> = {};
373 for (let i = 0; i < 5; i++) files[`ok${i}.test.ts`] = passing(i);
374 files["bad.test.ts"] = `import { test, expect } from "bun:test";\ntest("the broken one", () => { expect(1).toBe(2); });\n`;
375 const dir = fixture(files);
376 const got = await run(dir, "3", Object.keys(files));
377 rmSync(dir, { recursive: true, force: true });
378 expect(got.code).not.toBe(0);
379 expect(got.out).toContain("(fail) the broken one");
380 }, 60_000);
381
382 test("each shard gets its own empty FORGE_APP_DATA and TMPDIR, removed after", async () => {
383 const probe = (n: number): string =>
384 `import { test, expect } from "bun:test";
385 import { readdirSync, writeFileSync } from "node:fs";
386 import { tmpdir } from "node:os";
387 test("probe ${n}", () => {
388 const data = process.env.FORGE_APP_DATA!;
389 expect(readdirSync(data)).toEqual([]);
390 expect(readdirSync(tmpdir())).toEqual([]);
391 writeFileSync(data + "/mark", "${n}");
392 console.log("PROBE-DATA " + data + " PROBE-TMP " + tmpdir());
393 });
394 `;
395 const names = ["p0.test.ts", "p1.test.ts", "p2.test.ts"];
396 const dir = fixture(Object.fromEntries(names.map((name, i) => [name, probe(i)])));
397 const root = mkdtempSync(join(tmpdir(), "shards-root-"));
398 const got = await run(dir, "3", names, { TMPDIR: root, FORGE_APP_DATA: root });
399 rmSync(dir, { recursive: true, force: true });
400 const seen = [...got.out.matchAll(/PROBE-DATA (\S+) PROBE-TMP (\S+)/g)];
401 const leaked = readdirSync(root);
402 rmSync(root, { recursive: true, force: true });
403 expect(got.code).toBe(0);
404 expect(new Set(seen.map((m) => m[1])).size).toBe(3);
405 expect(new Set(seen.map((m) => m[2])).size).toBe(3);
406 for (const m of seen) expect(m[1]!.startsWith(root)).toBe(true);
407 expect(leaked).toEqual([]);
408 }, 60_000);
409
410 test("a FORGE_APP_DATA the cell set is nested under, not replaced", async () => {
411 const dir = fixture({
412 "d.test.ts": `import { test } from "bun:test";\ntest("d", () => { console.log("PROBE-DATA " + process.env.FORGE_APP_DATA); });\n`,
413 });
414 const data = mkdtempSync(join(tmpdir(), "shards-data-"));
415 const got = await run(dir, "1", ["d.test.ts"], { FORGE_APP_DATA: data });
416 rmSync(dir, { recursive: true, force: true });
417 const leaked = readdirSync(data);
418 rmSync(data, { recursive: true, force: true });
419 expect(got.code).toBe(0);
420 expect(got.out).toContain(`PROBE-DATA ${data}/data-0`);
421 expect(leaked).toEqual([]);
422 }, 60_000);
423 });
test/shards.test.ts +13 −412
@@ -1,12 +1,12 @@
11 /**
22 * The gate's shards (`scripts/shards.ts`): every file runs exactly once,
33 * the count follows the host and the override, and one red shard is a
4 * red run whose tail names every failing test from every shard.
4 * red run whose tail names every failing test from every shard. Pure: it
5 * spawns nothing, so it stays fast; the runner end to end is
6 * test/shards-run.test.ts.
57 */
6 import { afterAll, describe, expect, test } from "bun:test";
7 import { mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
8 import { tmpdir } from "node:os";
9 import { join } from "node:path";
8 import { describe, expect, test } from "bun:test";
9 import { readFileSync } from "node:fs";
1010 import { resolveLimits } from "../src/apps/contain.ts";
1111 import {
1212 SECONDS_PER_BYTE_DEFAULT,
@@ -40,7 +40,7 @@ import {
4040 type ShardResult,
4141 } from "../scripts/shards.ts";
4242 import { parseCheck } from "../src/apps/manifest.ts";
43 import { EXCLUDED, gateFiles, presentTests } from "../scripts/gate.ts";
43 import { gateFiles } from "../scripts/gate.ts";
4444 import { CHECK_CACHE } from "../src/checks/cache.ts";
4545
4646 const files = Array.from({ length: 23 }, (_, i) => `test/f${i}.test.ts`);
@@ -569,200 +569,7 @@ describe("sliceFiles", () => {
569569 });
570570 });
571571
572 describe("the shard's output", () => {
573 // A check's log is this script's stdout, line by line, as the forge
574 // reads it (`checks/service.ts`). Holding a shard's output until it
575 // ended meant a forty-minute gate showed nothing at all until the end.
576 test("one shard at a time: a line is written when the test says it, not when the shard ends", async () => {
577 const dir = mkdtempSync(join(tmpdir(), "shards-live-"));
578 writeFileSync(
579 join(dir, "live.test.ts"),
580 `import { test } from "bun:test";
581 test("says two things, a pause apart", async () => {
582 console.log("tick-one");
583 await Bun.sleep(1500);
584 console.log("tick-two");
585 });
586 `,
587 );
588 const script = new URL("../scripts/test-shards.ts", import.meta.url)
589 .pathname;
590 const child = Bun.spawn(["bun", script, "live.test.ts"], {
591 cwd: dir,
592 env: { ...process.env, FORGE_CHECK_SHARDS: "1" },
593 stdin: "ignore",
594 stdout: "pipe",
595 stderr: "pipe",
596 });
597 let text = "";
598 let firstAt = -1;
599 const decoder = new TextDecoder();
600 const reading = (async () => {
601 for await (const chunk of child.stdout) {
602 text += decoder.decode(chunk, { stream: true });
603 if (firstAt < 0 && text.includes("tick-one"))
604 firstAt = performance.now();
605 }
606 })();
607 const code = await child.exited;
608 const ended = performance.now();
609 await reading;
610 rmSync(dir, { recursive: true, force: true });
611 expect(code).toBe(0);
612 expect(firstAt).toBeGreaterThan(0);
613 // The first line reached us while the test was still asleep.
614 expect(ended - firstAt).toBeGreaterThan(1000);
615 expect(text).toContain("tick-two");
616 expect(text).toContain("── shard 1/1: live.test.ts");
617 expect(text).toMatch(/── shard 1\/1 done \(exit 0, [\d.]+s\)/);
618 }, 30_000);
619 });
620
621 describe("the gate audit before any shard", () => {
622 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
623 const listed = presentTests();
624 const git = (dir: string, ...args: string[]) =>
625 Bun.spawnSync(["git", "-c", "user.name=t", "-c", "user.email=t@t", "-c", "commit.gpgsign=false", ...args], {
626 cwd: dir,
627 stdout: "ignore",
628 stderr: "ignore",
629 }).exitCode;
630 // `stopAt`: kill the runner once stderr carries it. The audit and the slice
631 // banner come before any bun test spawns; what runs after them is the
632 // machine's load, not the code under test.
633 const run = async (
634 extra: string[],
635 args: string[],
636 drop: string[] = [],
637 commit = false,
638 stopAt?: string,
639 ) => {
640 const dir = mkdtempSync(join(tmpdir(), "forge-audit-"));
641 mkdirSync(join(dir, "test"));
642 for (const file of listed) if (!drop.includes(file)) writeFileSync(join(dir, file), "");
643 if (commit) {
644 expect(git(dir, "init", "-q")).toBe(0);
645 expect(git(dir, "add", "-A")).toBe(0);
646 expect(git(dir, "commit", "-qm", "base")).toBe(0);
647 }
648 for (const file of extra) writeFileSync(join(dir, file), "");
649 if (commit) {
650 expect(git(dir, "add", "-A")).toBe(0);
651 expect(git(dir, "commit", "-qm", "add a test")).toBe(0);
652 }
653 const started = performance.now();
654 const child = Bun.spawn(["bun", script, ...args], {
655 cwd: dir,
656 stdin: "ignore",
657 stdout: "pipe",
658 stderr: "pipe",
659 });
660 const watch = async (stream: ReadableStream<Uint8Array>) => {
661 let text = "";
662 for await (const chunk of stream) {
663 text += new TextDecoder().decode(chunk);
664 if (stopAt !== undefined && text.includes(stopAt)) child.kill();
665 }
666 return text;
667 };
668 const [out, err] = await Promise.all([new Response(child.stdout).text(), watch(child.stderr)]);
669 const code = await child.exited;
670 rmSync(dir, { recursive: true, force: true });
671 return { code, out, err, ms: performance.now() - started };
672 };
673
674 test("a stale exclusion is refused by name, last line, nothing spawned", async () => {
675 const gone = EXCLUDED[0]!.file;
676 for (const args of [["--gate"], ["--tier", "fast", "--diff", "a..b"]]) {
677 const got = await run([], args, [gone]);
678 expect(got.code).toBe(2);
679 const last = got.err.trim().split("\n").pop()!;
680 expect(last).toBe(`test-shards: ${gone}: fix its EXCLUDED entry in scripts/gate.ts`);
681 expect(got.out + got.err).not.toContain("start ");
682 expect(got.out).not.toContain("── shard");
683 expect(got.ms).toBeLessThan(1000);
684 }
685 });
686
687 test("a new file is in the gate with no list naming it, and explicit files are not audited", async () => {
688 // One slice holds every file, so the count includes the new one; a shard
689 // per file would outgrow the three digits a shard spec allows.
690 const all = String(gateFiles().length + 1);
691 const got = await run(["test/zz-new.test.ts"], ["--gate", "--shard", "1/1"], [], false, "matrix slice");
692 expect(got.err).not.toContain("EXCLUDED");
693 expect(got.out).toContain(`matrix slice 1/1 (${all} of ${all} files)`);
694 const explicit = await run(["test/zz-new.test.ts"], ["test/zz-new.test.ts"], [listed[0]!], false, "test-shards: 1 files");
695 expect(explicit.err).not.toContain("EXCLUDED");
696 }, 30_000);
697
698 test("a pull that only adds test/x.test.ts is gated on it (#2037)", async () => {
699 const got = await run(["test/zz-new.test.ts"], ["--tier", "fast", "--diff", "HEAD~1..HEAD"], [], true);
700 expect(got.err).not.toContain("EXCLUDED");
701 expect(got.out).toContain("for 1 changed");
702 expect(got.out).toContain(" test/zz-new.test.ts: changed\n");
703 }, 30_000);
704
705 test("a pull no test reaches is green with nothing to run, not a usage error (#3579)", async () => {
706 const dir = mkdtempSync(join(tmpdir(), "forge-audit-"));
707 mkdirSync(join(dir, "test"));
708 for (const file of listed) writeFileSync(join(dir, file), "");
709 writeFileSync(join(dir, "impact.json"), JSON.stringify(Object.fromEntries(listed.map((f) => [f, []]))));
710 expect(git(dir, "init", "-q")).toBe(0);
711 expect(git(dir, "add", "-A")).toBe(0);
712 expect(git(dir, "commit", "-qm", "base")).toBe(0);
713 mkdirSync(join(dir, "scripts"));
714 writeFileSync(join(dir, "scripts", "test-times.json"), "{}\n");
715 expect(git(dir, "add", "-A")).toBe(0);
716 expect(git(dir, "commit", "-qm", "a table only")).toBe(0);
717 const child = Bun.spawn(["bun", script, "--tier", "fast", "--diff", "HEAD~1..HEAD"], {
718 cwd: dir,
719 env: { ...process.env, FORGE_CHECK_IMPACT: "impact.json" },
720 stdin: "ignore",
721 stdout: "pipe",
722 stderr: "pipe",
723 });
724 const [out, err] = await Promise.all([
725 new Response(child.stdout).text(),
726 new Response(child.stderr).text(),
727 ]);
728 const code = await child.exited;
729 rmSync(dir, { recursive: true, force: true });
730 expect(out).toContain(`0 of ${gateFiles().length} files for 1 changed`);
731 expect(out).toContain("test-shards: the change reaches no tests; nothing to run");
732 expect(err).not.toContain("name the test files");
733 expect(code).toBe(0);
734 }, 30_000);
735 });
736
737572 describe("the gate audit alone, first in the check", () => {
738 const script = new URL("../scripts/gate.ts", import.meta.url).pathname;
739 const listed = presentTests();
740 // The check sets FORGE_CHECK_BASE; the scratch dir is no repo, so it has none.
741 const env = Object.fromEntries(Object.entries(process.env).filter(([k]) => k !== "FORGE_CHECK_BASE"));
742 const run = async (extra: string[], drop: string[] = []) => {
743 const dir = mkdtempSync(join(tmpdir(), "forge-gate-"));
744 mkdirSync(join(dir, "test"));
745 for (const file of [...listed, ...extra]) if (!drop.includes(file)) writeFileSync(join(dir, file), "");
746 const child = Bun.spawn(["bun", script], { cwd: dir, env, stdin: "ignore", stdout: "pipe", stderr: "pipe" });
747 const err = await new Response(child.stderr).text();
748 const code = await child.exited;
749 rmSync(dir, { recursive: true, force: true });
750 return { code, err };
751 };
752
753 test("a stale exclusion fails it by name; a new unlisted file passes", async () => {
754 const gone = EXCLUDED[0]!.file;
755 const red = await run([], [gone]);
756 expect(red.code).toBe(2);
757 expect(red.err.trim().split("\n").pop()).toBe(
758 `gate: ${gone}: fix its EXCLUDED entry in scripts/gate.ts`,
759 );
760 const green = await run(["test/zz-new.test.ts"]);
761 expect(green.code).toBe(0);
762 // The scratch dir is no git repo, so the pin check says it had no base.
763 expect(green.err).toBe("gate: no base to compare to (set FORGE_CHECK_BASE): migration pins not checked\n");
764 }, 30_000);
765
766573 test("forge.json audits before tsc, and the native tsc runs incremental from the check's cache", async () => {
767574 const check = parseCheck(readFileSync(new URL("../forge.json", import.meta.url), "utf8"), undefined);
768575 if (!check.ok || check.value === undefined) throw new Error("no check");
@@ -897,61 +704,6 @@ describe("onlyTestFailures", () => {
897704 });
898705 });
899706
900 describe("a failing file is run once more alone", () => {
901 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
902 const dir = "test/fixtures";
903 // Heavy by bytes, so the splitter gives it a shard of its own and the
904 // other fixtures share the second one. Written here, not committed.
905 const scratch = mkdtempSync(join(tmpdir(), "shard-filler-"));
906 const filler = join(scratch, "shard-filler.test.ts");
907 writeFileSync(filler, `import { test } from "bun:test";\ntest("filler", () => {});\n// ${"padding ".repeat(4000)}\n`);
908 afterAll(() => rmSync(scratch, { recursive: true, force: true }));
909 // The split must not follow the host: pin the count and the cell's CPUs, and
910 // drop any FORGE_CHECK_* the surrounding gate exported (#2726).
911 const env = { ...process.env, FORGE_CHECK_SHARDS: "2", FORGE_CELL_CPUS: "4" } as Record<string, string | undefined>;
912 for (const name of Object.keys(env)) if (name.startsWith("FORGE_CHECK_") && name !== "FORGE_CHECK_SHARDS") delete env[name];
913 const run = async (names: string[]) => {
914 const child = Bun.spawn(
915 ["bun", script, ...names.map((name) => (name === "filler" ? filler : `${dir}/shard-${name}.test.ts`))],
916 {
917 stdin: "ignore",
918 stdout: "pipe",
919 stderr: "pipe",
920 env,
921 },
922 );
923 const [out, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
924 return { code: await child.exited, out, err };
925 };
926
927 test("red only beside another file: the shard is green and the tail names it", async () => {
928 // filler is heavy by bytes, so poison and victim share the other shard.
929 const got = await run(["filler", "poison", "victim"]);
930 expect(got.out).toContain("fails only beside others: test/fixtures/shard-victim.test.ts (shard-mates: none)");
931 expect(got.code).toBe(0);
932 }, 30_000);
933
934 test("red alone too: it stays red", async () => {
935 const got = await run(["filler", "poison", "victim", "broken"]);
936 expect(got.out).toContain("fails only beside others: test/fixtures/shard-victim.test.ts (shard-mates: test/fixtures/shard-broken.test.ts)");
937 expect(got.code).toBe(1);
938 expect(got.out).toContain("(fail) broken");
939 }, 30_000);
940
941 test("one file fails beside others and another errors: the shard stays red", async () => {
942 const got = await run(["filler", "poison", "victim", "errors"]);
943 expect(got.code).toBe(1);
944 expect(got.out).not.toContain("fails only beside others");
945 expect(got.out).toContain("boom at load");
946 }, 30_000);
947
948 test("a lone file that fails stays red with no verdict line", async () => {
949 const got = await run(["broken"]);
950 expect(got.code).toBe(1);
951 expect(got.out).not.toContain("fails only beside others");
952 }, 30_000);
953 });
954
955707 describe("a crash outside any test", () => {
956708 const ran = "Ran 3 tests across 2 files. [9ms]";
957709 test("is a red whose report is not only failed tests", () => {
@@ -978,163 +730,12 @@ describe("a crash outside any test", () => {
978730 });
979731 });
980732
981 describe("every shard runs to the end, and a crashed one once more", () => {
982 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
983 const scratch = mkdtempSync(join(tmpdir(), "shard-crash-"));
984 // Heavy by bytes, so it runs first and gets a shard of its own.
985 const heavyRed = join(scratch, "heavy-red.test.ts");
986 writeFileSync(heavyRed, `import { expect, test } from "bun:test";\ntest("heavy red", () => { expect(1).toBe(2); });\n// ${"padding ".repeat(4000)}\n`);
987 const filler = join(scratch, "shard-filler.test.ts");
988 writeFileSync(filler, `import { test } from "bun:test";\ntest("filler", () => {});\n// ${"padding ".repeat(4000)}\n`);
989 afterAll(() => rmSync(scratch, { recursive: true, force: true }));
990 const run = async (shards: string, files: string[], mark: string) => {
991 const env = { ...process.env, FORGE_CHECK_SHARDS: shards, FORGE_CELL_CPUS: "4", SHARD_CRASH_MARK: mark } as Record<string, string | undefined>;
992 for (const name of Object.keys(env)) if (name.startsWith("FORGE_CHECK_") && name !== "FORGE_CHECK_SHARDS") delete env[name];
993 const child = Bun.spawn(["bun", script, ...files], { stdin: "ignore", stdout: "pipe", stderr: "pipe", env });
994 const [out, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
995 return { code: await child.exited, out, err };
996 };
997733
998 test("a red file does not stop the files after it", async () => {
999 // One worker, heaviest first: the red file runs before the green one.
1000 const got = await run("1", [heavyRed, "test/fixtures/shard-victim.test.ts"], join(scratch, "unused"));
1001 expect(got.code).toBe(1);
1002 expect(got.out).toContain("(fail) heavy red");
1003 expect(got.out).toContain("── shard 2/2: test/fixtures/shard-victim.test.ts");
1004 expect(got.out).not.toContain("fail-fast");
1005 }, 30_000);
1006
1007 test("a crash between tests re-runs its shard once, and the re-run is the verdict", async () => {
1008 const got = await run("2", [filler, "test/fixtures/shard-crash-once.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "crashed-once"));
1009 expect(got.out).toContain("crashed outside any test (exit 1); re-running it once. The crashed run:");
1010 expect(got.out).toContain(" │ error: Cannot use a closed database");
1011 expect(got.code).toBe(0);
1012 // Green on the re-run: the crash is named for the check to record as a flake.
1013 expect(got.err).toMatch(/test-shards: crash flake \d+\/\d+: error: Cannot use a closed database/);
1014 }, 30_000);
1015
1016 test("a single worker quotes the crashed run too: no bare error lines, and the re-run is green", async () => {
1017 const got = await run("1", ["test/fixtures/shard-crash-once.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "crashed-serial"));
1018 expect(got.out).toContain("crashed outside any test (exit 1); re-running it once. The crashed run:");
1019 expect(got.out).toContain(" │ error: Cannot use a closed database");
1020 const bare = got.out.split("\n").filter((line) => !line.startsWith(" │ "));
1021 expect(bare.some((line) => line.includes("Cannot use a closed database"))).toBe(false);
1022 expect(bare.some((line) => line.includes("# Unhandled error"))).toBe(false);
1023 expect(got.code).toBe(0);
1024 }, 30_000);
1025
1026 test("only CRASH_RERUNS crashed shards are re-run; the next stays red unre-run", async () => {
1027 const crashing = [1, 2, 3, 4].map((n) => {
1028 const file = join(scratch, `crash-${n}.test.ts`);
1029 writeFileSync(file, `throw new Error("boom at load ${n}");\n`);
1030 return file;
1031 });
1032 const got = await run("4", crashing, join(scratch, "unused"));
1033 expect(got.out.split("re-running it once").length - 1).toBe(CRASH_RERUNS);
1034 expect(got.code).toBe(1);
1035 }, 60_000);
1036
1037 test("a crash that happens again stays red", async () => {
1038 const got = await run("2", [filler, "test/fixtures/shard-errors.test.ts", "test/fixtures/shard-victim.test.ts"], join(scratch, "unused"));
1039 expect(got.out).toContain("re-running it once");
1040 expect(got.out).toContain("boom at load");
1041 expect(got.code).toBe(1);
1042 }, 30_000);
1043 });
1044
1045 describe("a fixture suite in six shards (#7617)", () => {
1046 const script = new URL("../scripts/test-shards.ts", import.meta.url).pathname;
1047 const fixture = (files: Record<string, string>): string => {
1048 const dir = mkdtempSync(join(tmpdir(), "shards-fixture-"));
1049 for (const [name, text] of Object.entries(files)) writeFileSync(join(dir, name), text);
1050 return dir;
1051 };
1052 const run = async (
1053 dir: string,
1054 shards: string,
1055 names: string[],
1056 env: Record<string, string> = {},
1057 ): Promise<{ code: number; out: string; stdout: string }> => {
1058 const child = Bun.spawn(["bun", script, ...names], {
1059 cwd: dir,
1060 env: { ...process.env, FORGE_CHECK_SHARDS: shards, ...env },
1061 stdin: "ignore",
1062 stdout: "pipe",
1063 stderr: "pipe",
1064 });
1065 const [stdout, err] = await Promise.all([new Response(child.stdout).text(), new Response(child.stderr).text()]);
1066 const code = await child.exited;
1067 return { code, out: stdout + err, stdout };
1068 };
1069 const passing = (n: number): string =>
1070 `import { test } from "bun:test";\ntest("passes ${n}", () => {});\n`;
1071 /** The files named on the `── shard N/M…: files` lines, in the order shards ran. */
1072 const finished = (stdout: string): string[] =>
1073 [...stdout.matchAll(/^── shard \d+\/\d+(?: \(exit \d+, [\d.]+s\))?: (.*)$/gm)].map((m) => m[1]!);
1074
1075 test("twelve files in six shards: the plan has six slices and each file runs exactly once", async () => {
1076 const names = Array.from({ length: 12 }, (_, i) => `s${i}.test.ts`);
1077 const dir = fixture(Object.fromEntries(names.map((name, i) => [name, passing(i)])));
1078 const six = await run(dir, "6", names);
1079 rmSync(dir, { recursive: true, force: true });
1080 expect(six.code).toBe(0);
1081 expect(six.out).toContain("12 files in 6 shards");
1082 expect(six.out.match(/test-shards: start \d+\/6 /g)?.length).toBe(6);
1083 const ran = finished(six.stdout).flatMap((line) => line.split(" "));
1084 for (const name of names) expect(ran.filter((file) => file === name).length).toBe(1);
1085 expect(six.out).toMatch(/Ran 12 tests across 12 files/);
1086 }, 60_000);
1087
1088 test("a failure in any shard reds the run and names the test", async () => {
1089 const files: Record<string, string> = {};
1090 for (let i = 0; i < 5; i++) files[`ok${i}.test.ts`] = passing(i);
1091 files["bad.test.ts"] = `import { test, expect } from "bun:test";\ntest("the broken one", () => { expect(1).toBe(2); });\n`;
1092 const dir = fixture(files);
1093 const got = await run(dir, "3", Object.keys(files));
1094 rmSync(dir, { recursive: true, force: true });
1095 expect(got.code).not.toBe(0);
1096 expect(got.out).toContain("(fail) the broken one");
1097 }, 60_000);
1098
1099 test("each shard gets its own empty FORGE_APP_DATA and TMPDIR, removed after", async () => {
1100 const probe = (n: number): string =>
1101 `import { test, expect } from "bun:test";
1102 import { readdirSync, writeFileSync } from "node:fs";
1103 import { tmpdir } from "node:os";
1104 test("probe ${n}", () => {
1105 const data = process.env.FORGE_APP_DATA!;
1106 expect(readdirSync(data)).toEqual([]);
1107 expect(readdirSync(tmpdir())).toEqual([]);
1108 writeFileSync(data + "/mark", "${n}");
1109 console.log("PROBE-DATA " + data + " PROBE-TMP " + tmpdir());
1110 });
1111 `;
1112 const names = ["p0.test.ts", "p1.test.ts", "p2.test.ts"];
1113 const dir = fixture(Object.fromEntries(names.map((name, i) => [name, probe(i)])));
1114 const root = mkdtempSync(join(tmpdir(), "shards-root-"));
1115 const got = await run(dir, "3", names, { TMPDIR: root, FORGE_APP_DATA: root });
1116 rmSync(dir, { recursive: true, force: true });
1117 const seen = [...got.out.matchAll(/PROBE-DATA (\S+) PROBE-TMP (\S+)/g)];
1118 const leaked = readdirSync(root);
1119 rmSync(root, { recursive: true, force: true });
1120 expect(got.code).toBe(0);
1121 expect(new Set(seen.map((m) => m[1])).size).toBe(3);
1122 expect(new Set(seen.map((m) => m[2])).size).toBe(3);
1123 for (const m of seen) expect(m[1]!.startsWith(root)).toBe(true);
1124 expect(leaked).toEqual([]);
1125 }, 60_000);
1126
1127 test("a FORGE_APP_DATA the cell set is nested under, not replaced", async () => {
1128 const dir = fixture({
1129 "d.test.ts": `import { test } from "bun:test";\ntest("d", () => { console.log("PROBE-DATA " + process.env.FORGE_APP_DATA); });\n`,
1130 });
1131 const data = mkdtempSync(join(tmpdir(), "shards-data-"));
1132 const got = await run(dir, "1", ["d.test.ts"], { FORGE_APP_DATA: data });
1133 rmSync(dir, { recursive: true, force: true });
1134 const leaked = readdirSync(data);
1135 rmSync(data, { recursive: true, force: true });
1136 expect(got.code).toBe(0);
1137 expect(got.out).toContain(`PROBE-DATA ${data}/data-0`);
1138 expect(leaked).toEqual([]);
1139 }, 60_000);
734 describe("this file", () => {
735 // Every crew run runs it as a cross file (CLAUDE.md), so it stays under a
736 // second (#8310): a test that spawns the runner goes in shards-run.test.ts.

The guard only matches Bun.spawn. Bun.spawnSync would also match by substring, but node:child_process execSync/spawn would not. Extend the check to cover child_process too. Minor; the doubled blank line above it should also go.

737 test("spawns nothing", () => {
738 const source = readFileSync(new URL(import.meta.url).pathname, "utf8");
739 expect(source.includes("Bun." + "spawn")).toBe(false);
740 });
1140741 });

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs