From 0c83ea74174cbe6fbb942e4c27bab961ad2ff4a3 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 22 Aug 2026 20:43:29 -0700 Subject: [PATCH 1/5] fix(test): pass --parallel so the full suite finishes instead of reading as hung MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bun run test` spawned `bun test --isolate ./tests/`. With `--isolate` and no `--parallel`, Bun re-evaluates the module graph once per file on a single core. Past ~900 files that stops looking slow and starts looking hung. Measured on this tree (902 files): without --parallel 1 h 29 m, zero output, ~57 % CPU, 8.5 MB RSS, killed with --parallel ~110-190 s, 10x PARALLEL The failure mode is what makes this worth fixing rather than documenting: there is no progress output, one core is pinned, and RSS stays tiny, so it reads as a deadlock. A contributor's reasonable conclusion is that the suite is broken. The stale "normally runs in about 210s" warning is updated for the same reason — that number predates the file count that made the flag necessary. `resolveBunTestArgs` is exported and pinned by tests so the flag cannot be dropped again silently, including the two easy-to-regress cases: a caller supplying `--parallel=N` must not be overridden, and an option-only argv such as `--timeout=30000` must still count as a full-suite run and keep `./tests/`. Gate: 14436 pass / 2 fail; both also fail on untouched upstream/dev at this commit (baseline: 4 fail, a superset). Zero regressions. Note on scope: this is the smallest change that makes the suite runnable. Two adjacent changes are deliberately left out and will be proposed separately — narrowing the exclusive-run lock to full-suite runs (a behavior change that lets two focused runs share one sandboxed HOME), and a `test:changed` script with the contributing-guide updates that go with it. Separately and not addressed here: `tests/key-login-live-update.test.ts` fails standalone and serially on a clean tree, so every full run is red by at least one test regardless of this change. --- bunfig.toml | 1 + scripts/test.ts | 31 +++++++++++++++++++++++++++++-- tests/test-runner.test.ts | 31 ++++++++++++++++++++++++++++++- 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/bunfig.toml b/bunfig.toml index 00cbc1231e..318845b44a 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -5,6 +5,7 @@ # so a bare `bun test` — or `bun test tests/` (a substring filter that also matches # devlog/opencode-cursor/tests/) — drags them in and reports hundreds of spurious failures. # `root` pins discovery to ./tests so every invocation stays on the real suite. +# File-level `--parallel` has no bunfig key; `scripts/test.ts` passes it for `bun run test`. # The npm script already uses `bun test ./tests/`; this makes a bare `bun test` behave the same. [test] root = "tests" diff --git a/scripts/test.ts b/scripts/test.ts index 5297a17722..7e055a700f 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -59,6 +59,33 @@ export function createIsolatedTestEnvironment( }; } +function hasCliFlag(requested: string[], name: string): boolean { + return requested.some(arg => arg === name || arg.startsWith(`${name}=`)); +} + +/** True for a filter-less `bun run test`. `--timeout` / `--dots` / `--parallel=N` still count. */ +function isFullSuiteRun(requested: string[]): boolean { + return !requested.some(arg => arg !== "-" && !arg.startsWith("-")); +} + +/** + * Default `bun test` argv for this repo. + * + * `--isolate` keeps a fresh global per file, and is the substring the exclusive-run pgrep + * matches. `--parallel` is what makes the suite finishable: with isolate alone Bun re-evaluates + * the module graph once per file on a single core, so past ~900 files the run stops looking slow + * and starts looking hung — measured here at 1 h 29 m with zero output, ~57 % CPU and 8.5 MB RSS, + * against ~110-190 s for the identical suite with `--parallel`. A caller-supplied `--parallel=N` + * is left alone. + */ +export function resolveBunTestArgs(requested: string[]): string[] { + const args = ["--isolate"]; + if (!hasCliFlag(requested, "--parallel")) args.push("--parallel"); + args.push(...requested); + if (isFullSuiteRun(requested)) args.push("./tests/"); + return args; +} + /** * Other `bun test` runners already on this machine. * @@ -141,7 +168,7 @@ if (import.meta.main) { await waitForExclusiveRun(process.pid); const startedAt = Date.now(); const child = Bun.spawnSync( - [process.execPath, "test", "--isolate", ...(requestedTests.length > 0 ? requestedTests : ["./tests/"])], + [process.execPath, "test", ...resolveBunTestArgs(requestedTests)], { env: isolated.env, stdin: "inherit", @@ -152,7 +179,7 @@ if (import.meta.main) { const elapsedSeconds = Math.round((Date.now() - startedAt) / 1000); if (requestedTests.length === 0 && elapsedSeconds > 600) { console.warn( - `[test] the suite took ${elapsedSeconds}s; it normally runs in about 210s on an idle machine. ` + `[test] the suite took ${elapsedSeconds}s; with --parallel it should finish in a few minutes on an idle machine. ` + "Check for another test runner, a busy CPU, or a test that started polling something real.", ); } diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index ef48654f15..253a9afb41 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "bun:test"; import { existsSync } from "node:fs"; import { isAbsolute, join } from "node:path"; -import { createIsolatedTestEnvironment } from "../scripts/test"; +import { createIsolatedTestEnvironment, resolveBunTestArgs } from "../scripts/test"; import { decodeWindowsIdentityPowerShellOutputForTests, windowsIdentityPowerShellCommandForTests, @@ -68,3 +68,32 @@ describe("test runner isolation", () => { }, ); }); + +/** + * Without `--parallel`, `--isolate` re-evaluates the module graph once per file on a single + * core. Past ~900 files that stops reading as slow and starts reading as hung: measured at + * 1 h 29 m with zero output, ~57 % CPU and 8.5 MB RSS, against ~110-190 s for the identical + * suite with the flag. These pin the argv so the flag cannot be dropped again silently. + */ +describe("bun test argv", () => { + test("a filter-less run gets isolate, parallel and the suite path", () => { + expect(resolveBunTestArgs([])).toEqual(["--isolate", "--parallel", "./tests/"]); + }); + + test("a file filter keeps isolate and parallel but no suite path", () => { + expect(resolveBunTestArgs(["tests/foo.test.ts"])) + .toEqual(["--isolate", "--parallel", "tests/foo.test.ts"]); + }); + + test("a caller-supplied concurrency is left alone", () => { + expect(resolveBunTestArgs(["--parallel=2"])) + .toEqual(["--isolate", "--parallel=2", "./tests/"]); + expect(resolveBunTestArgs(["--parallel"])) + .toEqual(["--isolate", "--parallel", "./tests/"]); + }); + + test("option-only arguments still count as a full suite run", () => { + expect(resolveBunTestArgs(["--timeout=30000"])) + .toEqual(["--isolate", "--parallel", "--timeout=30000", "./tests/"]); + }); +}); From a1d3e15e8b4f45779ce5d29ac00e85710f7f80d9 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 22 Aug 2026 21:33:14 -0700 Subject: [PATCH 2/5] fix(test): parse the -- delimiter and pin the flag end to end Two review findings. hasCliFlag/isFullSuiteRun read the whole argv, so `test -- --parallel=2` suppressed the default --parallel even though everything after -- is passed through, and a bare - was classified as an option so `test -` was treated as a full-suite run. The tests also asserted only resolveBunTestArgs output: reverting the spawn call to a hardcoded argv left every assertion green. A spawn test now runs the wrapper against a non-matching filter and asserts bun reports PARALLEL. --- scripts/test.ts | 10 ++++++++-- tests/test-runner.test.ts | 25 +++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/scripts/test.ts b/scripts/test.ts index 7e055a700f..956c81303d 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -60,12 +60,18 @@ export function createIsolatedTestEnvironment( } function hasCliFlag(requested: string[], name: string): boolean { - return requested.some(arg => arg === name || arg.startsWith(`${name}=`)); + const delimiterIndex = requested.indexOf("--"); + const wrapperArgs = delimiterIndex === -1 ? requested : requested.slice(0, delimiterIndex); + return wrapperArgs.some(arg => arg === name || arg.startsWith(`${name}=`)); } /** True for a filter-less `bun run test`. `--timeout` / `--dots` / `--parallel=N` still count. */ function isFullSuiteRun(requested: string[]): boolean { - return !requested.some(arg => arg !== "-" && !arg.startsWith("-")); + const delimiterIndex = requested.indexOf("--"); + const wrapperArgs = delimiterIndex === -1 ? requested : requested.slice(0, delimiterIndex); + const passedThrough = delimiterIndex === -1 ? [] : requested.slice(delimiterIndex + 1); + return passedThrough.length === 0 + && !wrapperArgs.some(arg => arg === "-" || !arg.startsWith("-")); } /** diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index 253a9afb41..11bf5723f9 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -83,6 +83,8 @@ describe("bun test argv", () => { test("a file filter keeps isolate and parallel but no suite path", () => { expect(resolveBunTestArgs(["tests/foo.test.ts"])) .toEqual(["--isolate", "--parallel", "tests/foo.test.ts"]); + expect(resolveBunTestArgs(["-"])) + .toEqual(["--isolate", "--parallel", "-"]); }); test("a caller-supplied concurrency is left alone", () => { @@ -96,4 +98,27 @@ describe("bun test argv", () => { expect(resolveBunTestArgs(["--timeout=30000"])) .toEqual(["--isolate", "--parallel", "--timeout=30000", "./tests/"]); }); + + test("arguments after the delimiter are passed through instead of parsed as wrapper flags", () => { + expect(resolveBunTestArgs(["--", "--parallel=2"])) + .toEqual(["--isolate", "--parallel", "--", "--parallel=2"]); + }); + + test("the wrapper passes parallel execution through to bun", () => { + const result = Bun.spawnSync([ + process.execPath, + join(import.meta.dir, "../scripts/test.ts"), + "--pass-with-no-tests", + join(import.meta.dir, "__no_matching_test_file__.test.ts"), + ], { + cwd: join(import.meta.dir, ".."), + env: { ...process.env, OCX_TEST_NO_QUEUE: "1" }, + stdout: "pipe", + stderr: "pipe", + }); + + const output = new TextDecoder().decode(result.stdout) + + new TextDecoder().decode(result.stderr); + expect(output).toContain("PARALLEL"); + }); }); From 560f464715064ea269fc2eb00328cfaeecd4c643 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sat, 22 Aug 2026 22:56:19 -0700 Subject: [PATCH 3/5] fix(test): only required-value options consume the next argument MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two CodeRabbit findings, plus a regression the first attempt introduced. isFullSuiteRun read a space-separated option value as a file filter, so `bun run test --timeout 30000` silently stopped being a full-suite run and dropped ./tests/. Bun 1.4.0 accepts that form. The first fix over-corrected: treating every value-taking option as consuming the next argument swallowed the filter in ["--parallel", "tests/foo.test.ts"], so a focused run became a full-suite run — worse than the original bug, and silent. --parallel, --changed, --timings and --coverage take OPTIONAL values, which Bun expects attached with =. Now only required-value options consume the next argument, and all six boundary shapes are pinned as tests. The spawn test also asserted only that the output contained PARALLEL, so it could pass after a nonzero wrapper exit; it now asserts exitCode 0 first, against a real fixture file so a successful run is meaningful. --- scripts/test.ts | 82 ++++++++++++++++++++++++++++++++++++++- tests/test-runner.test.ts | 53 ++++++++++++++++++------- 2 files changed, 118 insertions(+), 17 deletions(-) diff --git a/scripts/test.ts b/scripts/test.ts index 956c81303d..3fcaf7b594 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -65,13 +65,91 @@ function hasCliFlag(requested: string[], name: string): boolean { return wrapperArgs.some(arg => arg === name || arg.startsWith(`${name}=`)); } +// Bun 1.4.0 builds `bun test` options from its test, runtime, transpiler, and base tables. +// Only required values consume the next argument. Optional values such as `--parallel=2` +// must stay attached so a bare option cannot hide the positional filter that follows it. +const BUN_TEST_OPTIONS_REQUIRING_VALUES = new Set([ + // Test options. + "--timeout", + "--rerun-each", + "--retry", + "--seed", + "--coverage-reporter", + "--coverage-dir", + "-t", + "--test-name-pattern", + "--grep", + "--reporter", + "--reporter-outfile", + "--max-concurrency", + "--path-ignore-patterns", + "--parallel-delay", + "--shard", + // Runtime options accepted by `bun test`. + "--watch-kill-signal", + "-r", + "--preload", + "--require", + "--import", + "--cpu-prof-name", + "--cpu-prof-dir", + "--cpu-prof-interval", + "--heap-prof-name", + "--heap-prof-dir", + "--heap-prof-interval", + "--install", + "-e", + "--eval", + "-p", + "--print", + "--port", + "--origin", + "--conditions", + "--fetch-preconnect", + "--max-http-header-size", + "--dns-result-order", + "--redirect-warnings", + "--disable-warning", + "--title", + "--unhandled-rejections", + "--console-depth", + "--user-agent", + "--cron-title", + "--cron-period", + "--trace-event-categories", + "--trace-event-file-pattern", + "--stack-trace-limit", + // Transpiler and base options accepted by `bun test`. + "--main-fields", + "--extension-order", + "--tsconfig-override", + "-d", + "--define", + "--drop", + "--feature", + "-l", + "--loader", + "--jsx-factory", + "--jsx-fragment", + "--jsx-import-source", + "--jsx-runtime", + "--env-file", + "--cwd", +]); + /** True for a filter-less `bun run test`. `--timeout` / `--dots` / `--parallel=N` still count. */ function isFullSuiteRun(requested: string[]): boolean { const delimiterIndex = requested.indexOf("--"); const wrapperArgs = delimiterIndex === -1 ? requested : requested.slice(0, delimiterIndex); const passedThrough = delimiterIndex === -1 ? [] : requested.slice(delimiterIndex + 1); - return passedThrough.length === 0 - && !wrapperArgs.some(arg => arg === "-" || !arg.startsWith("-")); + if (passedThrough.length > 0) return false; + + for (let index = 0; index < wrapperArgs.length; index++) { + const arg = wrapperArgs[index]; + if (arg === "-" || !arg.startsWith("-")) return false; + if (!arg.includes("=") && BUN_TEST_OPTIONS_REQUIRING_VALUES.has(arg)) index++; + } + return true; } /** diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index 11bf5723f9..810acd33c8 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "bun:test"; -import { existsSync } from "node:fs"; +import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; import { isAbsolute, join } from "node:path"; import { createIsolatedTestEnvironment, resolveBunTestArgs } from "../scripts/test"; import { @@ -92,11 +93,26 @@ describe("bun test argv", () => { .toEqual(["--isolate", "--parallel=2", "./tests/"]); expect(resolveBunTestArgs(["--parallel"])) .toEqual(["--isolate", "--parallel", "./tests/"]); + expect(resolveBunTestArgs(["--parallel", "tests/foo.test.ts"])) + .toEqual(["--isolate", "--parallel", "tests/foo.test.ts"]); + expect(resolveBunTestArgs(["--parallel=2", "tests/foo.test.ts"])) + .toEqual(["--isolate", "--parallel=2", "tests/foo.test.ts"]); }); test("option-only arguments still count as a full suite run", () => { expect(resolveBunTestArgs(["--timeout=30000"])) .toEqual(["--isolate", "--parallel", "--timeout=30000", "./tests/"]); + expect(resolveBunTestArgs(["--timeout", "30000"])) + .toEqual(["--isolate", "--parallel", "--timeout", "30000", "./tests/"]); + expect(resolveBunTestArgs(["--timeout", "30000", "tests/foo.test.ts"])) + .toEqual(["--isolate", "--parallel", "--timeout", "30000", "tests/foo.test.ts"]); + expect(resolveBunTestArgs(["-t", "serial test"])).toEqual([ + "--isolate", + "--parallel", + "-t", + "serial test", + "./tests/", + ]); }); test("arguments after the delimiter are passed through instead of parsed as wrapper flags", () => { @@ -105,20 +121,27 @@ describe("bun test argv", () => { }); test("the wrapper passes parallel execution through to bun", () => { - const result = Bun.spawnSync([ - process.execPath, - join(import.meta.dir, "../scripts/test.ts"), - "--pass-with-no-tests", - join(import.meta.dir, "__no_matching_test_file__.test.ts"), - ], { - cwd: join(import.meta.dir, ".."), - env: { ...process.env, OCX_TEST_NO_QUEUE: "1" }, - stdout: "pipe", - stderr: "pipe", - }); + const fixtureRoot = mkdtempSync(join(tmpdir(), "opencodex-test-runner-")); + const fixturePath = join(fixtureRoot, "parallel-smoke.test.ts"); + writeFileSync(fixturePath, 'import { test } from "bun:test"; test("smoke", () => {});\n'); + try { + const result = Bun.spawnSync([ + process.execPath, + join(import.meta.dir, "../scripts/test.ts"), + fixturePath, + ], { + cwd: join(import.meta.dir, ".."), + env: { ...process.env, OCX_TEST_NO_QUEUE: "1" }, + stdout: "pipe", + stderr: "pipe", + }); - const output = new TextDecoder().decode(result.stdout) - + new TextDecoder().decode(result.stderr); - expect(output).toContain("PARALLEL"); + const output = new TextDecoder().decode(result.stdout) + + new TextDecoder().decode(result.stderr); + expect(result.exitCode).toBe(0); + expect(output).toContain("PARALLEL"); + } finally { + rmSync(fixtureRoot, { recursive: true, force: true }); + } }); }); From 8d6425a4ddf942a0b619b0bd11a2fd3f727c5e14 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sun, 23 Aug 2026 02:03:24 -0700 Subject: [PATCH 4/5] fix(test): parse separated timings paths --- scripts/test.ts | 1 + tests/test-runner.test.ts | 8 ++++++++ 2 files changed, 9 insertions(+) diff --git a/scripts/test.ts b/scripts/test.ts index 3fcaf7b594..7d1d73cdbe 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -85,6 +85,7 @@ const BUN_TEST_OPTIONS_REQUIRING_VALUES = new Set([ "--path-ignore-patterns", "--parallel-delay", "--shard", + "--timings", // Runtime options accepted by `bun test`. "--watch-kill-signal", "-r", diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index 810acd33c8..320d8402c7 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -106,6 +106,14 @@ describe("bun test argv", () => { .toEqual(["--isolate", "--parallel", "--timeout", "30000", "./tests/"]); expect(resolveBunTestArgs(["--timeout", "30000", "tests/foo.test.ts"])) .toEqual(["--isolate", "--parallel", "--timeout", "30000", "tests/foo.test.ts"]); + expect(resolveBunTestArgs(["--timings", ".bun-test-timings/current.json"])) + .toEqual([ + "--isolate", + "--parallel", + "--timings", + ".bun-test-timings/current.json", + "./tests/", + ]); expect(resolveBunTestArgs(["-t", "serial test"])).toEqual([ "--isolate", "--parallel", From 3187fd4befce4131ceb484fc973a84b765cae7f4 Mon Sep 17 00:00:00 2001 From: olddonkey Date: Sun, 23 Aug 2026 02:15:26 -0700 Subject: [PATCH 5/5] test: cover config values and fixture execution --- scripts/test.ts | 2 ++ tests/test-runner.test.ts | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/scripts/test.ts b/scripts/test.ts index 7d1d73cdbe..41d141b7fd 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -136,6 +136,8 @@ const BUN_TEST_OPTIONS_REQUIRING_VALUES = new Set([ "--jsx-runtime", "--env-file", "--cwd", + "-c", + "--config", ]); /** True for a filter-less `bun run test`. `--timeout` / `--dots` / `--parallel=N` still count. */ diff --git a/tests/test-runner.test.ts b/tests/test-runner.test.ts index 320d8402c7..408d5dcf89 100644 --- a/tests/test-runner.test.ts +++ b/tests/test-runner.test.ts @@ -114,6 +114,10 @@ describe("bun test argv", () => { ".bun-test-timings/current.json", "./tests/", ]); + for (const configFlag of ["-c", "--config"]) { + expect(resolveBunTestArgs([configFlag, "ci.bunfig.toml"])) + .toEqual(["--isolate", "--parallel", configFlag, "ci.bunfig.toml", "./tests/"]); + } expect(resolveBunTestArgs(["-t", "serial test"])).toEqual([ "--isolate", "--parallel", @@ -131,7 +135,11 @@ describe("bun test argv", () => { test("the wrapper passes parallel execution through to bun", () => { const fixtureRoot = mkdtempSync(join(tmpdir(), "opencodex-test-runner-")); const fixturePath = join(fixtureRoot, "parallel-smoke.test.ts"); - writeFileSync(fixturePath, 'import { test } from "bun:test"; test("smoke", () => {});\n'); + const markerPath = join(fixtureRoot, "executed.marker"); + writeFileSync( + fixturePath, + `import { test } from "bun:test"; import { writeFileSync } from "node:fs"; test("smoke", () => writeFileSync(${JSON.stringify(markerPath)}, "executed"));\n`, + ); try { const result = Bun.spawnSync([ process.execPath, @@ -148,6 +156,7 @@ describe("bun test argv", () => { + new TextDecoder().decode(result.stderr); expect(result.exitCode).toBe(0); expect(output).toContain("PARALLEL"); + expect(existsSync(markerPath)).toBe(true); } finally { rmSync(fixtureRoot, { recursive: true, force: true }); }