-
Notifications
You must be signed in to change notification settings - Fork 973
fix(test): install gui dependencies the local runner already needs #2957
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,5 @@ | ||
| import { randomUUID } from "node:crypto"; | ||
| import { mkdirSync, mkdtempSync, rmSync } from "node:fs"; | ||
| import { existsSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; | ||
| import { homedir, tmpdir } from "node:os"; | ||
| import { join } from "node:path"; | ||
| import { acquireTestRunLock, TEST_RUN_ID_ENV } from "./test-run-lock"; | ||
|
|
@@ -433,15 +433,67 @@ async function runTestLane(lane: BunTestLane, runId: string, capture = false): P | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * `gui` is not a workspace of the root package and declares React only in `gui/package.json`, so a | ||
| * root `bun install` never creates `gui/node_modules`. Twenty-five files under `tests/` import | ||
| * modules from `gui/src`, which makes those tests fail on a fresh clone or worktree with | ||
| * `Cannot find package 'react'` — reported as an "Unhandled error between tests" that names no | ||
| * test, so the cause is not obvious from the output. | ||
| * | ||
| * `.github/workflows/ci.yml` already installs them explicitly for exactly this reason; the local | ||
| * runner had no equivalent. Install on demand rather than fail, because the tests genuinely | ||
| * require the dependency and `gui/node_modules` is a gitignored build artifact, not source. | ||
| */ | ||
| export function ensureGuiDependencies(io: { | ||
| cwd?: string; | ||
| exists?: (path: string) => boolean; | ||
| install?: (guiDir: string) => { ok: boolean; detail: string }; | ||
| log?: (message: string) => void; | ||
| } = {}): { kind: "present" | "installed" | "absent" | "failed"; detail?: string } { | ||
| const cwd = io.cwd ?? process.cwd(); | ||
| const exists = io.exists ?? existsSync; | ||
| const log = io.log ?? (message => console.warn(message)); | ||
| const guiDir = join(cwd, "gui"); | ||
| if (!exists(join(guiDir, "package.json"))) return { kind: "absent" }; | ||
| if (exists(join(guiDir, "node_modules", "react", "package.json"))) return { kind: "present" }; | ||
|
|
||
| log("[test] gui dependencies are missing or incomplete; installing them so tests importing gui/src can resolve React."); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win 🔎 Supported by static analysis🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
test "$(bun --version)" = "1.4.0"
bun -e 'try { Bun.spawnSync(["__missing_bun_executable__", "--version"]); console.log("returned"); } catch (error) { console.log("threw", String(error)); }'Repository: lidge-jun/opencodex Length of output: 194 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- applicable repository guidance ---'
find /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732 -maxdepth 2 -type f -name '*.md' -print
printf '%s\n' '--- scripts/test.ts relevant symbols ---'
rg -n -C 18 'gui dependencies|Bun\.spawnSync|spawnSync|exitCode|kind: "failed"|manual recovery|install' scripts/test.ts
printf '%s\n' '--- declared Bun version and direct callers ---'
rg -n -C 4 '"bun"|bun@|packageManager|test\.ts|runTests|install' package.json bun.lockb bun.lock README.md scripts 2>/dev/null || trueRepository: lidge-jun/opencodex Length of output: 50377 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- scripts conventions ---'
cat /tmp/coderabbit-repo-knowledge/lidge-jun-opencodex-7afea732/conventions/scripts.md
printf '%s\n' '--- relevant tests for ensureGuiDependencies ---'
rg -n -C 12 'ensureGuiDependencies|gui dependencies|install.*frozen-lockfile|kind: "failed"' tests scripts --glob '*.ts'
printf '%s\n' '--- exact implementation and entry-point flow ---'
sed -n '436,490p' scripts/test.tsRepository: lidge-jun/opencodex Length of output: 39437 🌐 Web query:
💡 Result: In Bun 1.4.0, the behavior of Citations:
🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- decodeOutput definition and uses ---'
rg -n -C 10 'function decodeOutput|const decodeOutput|decodeOutput\(' scripts/test.ts
printf '%s\n' '--- Bun type declarations available in the repository ---'
rg -n -C 12 'interface SyncSubprocess|type SyncSubprocess|class SyncSubprocess|spawnSync' node_modules/@types/bun node_modules/bun-types 2>/dev/null || true
printf '%s\n' '--- lockfile and package declarations for Bun 1.4.0 ---'
rg -n -C 3 '"bun": "1\.4\.0"|bun-types.*1\.4\.0|`@types/bun`.*1\.4\.0' package.json bun.lockRepository: lidge-jun/opencodex Length of output: 5726 🌐 Web query:
💡 Result: In Bun, a spawnSync ENOENT error indicates that the process could not execute the specified command, typically because the file was not found, the working directory (cwd) was invalid, or an environment path lookup failed [1][2][3]. Research into Bun 1.4.0 indicates that several factors can contribute to ENOENT errors in spawnSync: 1. Stale Environment PATH: Bun 1.4.0 and earlier versions have been documented to resolve the PATH for Bun.spawnSync from a startup snapshot of the environment, meaning changes made to process.env.PATH at runtime are sometimes ignored [4]. This can cause commands that should be found to fail with ENOENT [4]. A fix involving inheriting the live process.env is in development [5][4]. 2. Invalid or Renamed Working Directory: If the directory specified by the cwd option (or the default cwd) is renamed or does not exist at the time of execution, the spawn attempt will fail with ENOENT [1][2][3]. In some cases, the error message erroneously attributes the path to the executable rather than the missing cwd [2]. Improvements have been implemented to better attribute these errors to the cwd when applicable [2]. 3. Node.js Compatibility: While Bun aims for compatibility with Node's child_process.spawnSync, earlier versions showed discrepancies in the returned object shape when a spawn failed (e.g., status/pid/output properties) [6][7]. Updates in Bun's development cycle have aimed to align these error return shapes with Node.js [6][7]. If you are encountering this error, it is recommended to: - Verify that the target executable exists and is accessible in the environment PATH used by the Bun process [8][4]. - Ensure that the directory specified in the cwd option exists and has not been moved or renamed [1][3]. - Check if passing an explicit { PATH } in the spawn options resolves the issue, as this bypasses the potential stale environment snapshot issue [4]. Citations:
🌐 Web query:
💡 Result: In the Bun runtime, when using Bun.spawnSync, the function attempts to resolve the provided command to an absolute executable path [1]. If the executable cannot be found or is otherwise invalid, the spawn operation typically fails during the initiation phase [1]. Because the process fails to start, a SyncSubprocess object may not be successfully created or initialized in the state expected for a running process [1]. Consequently, you cannot rely on checking properties like exitCode, stdout, or stderr on a non-existent process [1]. To handle cases where an executable might be missing, it is recommended to verify the existence of the command before attempting to spawn it [1]. You can use the Bun.which utility for this purpose: const bin = "your-command"; if (Bun.which(bin)) { const subprocess = Bun.spawnSync([bin]); // Process execution logic } else { console.error( Citations: 🏁 Script executed: #!/usr/bin/env bash
set -euo pipefail
for endpoint in \
'https://api.github.com/repos/oven-sh/bun/issues/31767' \
'https://api.github.com/repos/oven-sh/bun/pulls/40543' \
'https://api.github.com/repos/oven-sh/bun/issues/40846'
do
printf '\n--- %s ---\n' "$endpoint"
curl -fsSL -H 'Accept: application/vnd.github+json' "$endpoint" |
jq '{number, title, state, body, merged_at, created_at, updated_at}'
doneRepository: lidge-jun/opencodex Length of output: 10337 Catch Bun 1.4.0 can throw when it cannot start 🤖 Prompt for AI Agents |
||
| const install = io.install ?? ((dir: string) => { | ||
| const result = Bun.spawnSync(["bun", "install", "--frozen-lockfile"], { | ||
| cwd: dir, | ||
| stdout: "pipe", | ||
| stderr: "pipe", | ||
| }); | ||
| return { | ||
| ok: result.exitCode === 0, | ||
| detail: decodeOutput(result.stderr) || decodeOutput(result.stdout), | ||
| }; | ||
| }); | ||
| const outcome = install(guiDir); | ||
| if (outcome.ok) return { kind: "installed" }; | ||
| return { kind: "failed", detail: outcome.detail }; | ||
| } | ||
|
|
||
| if (import.meta.main) { | ||
| const requestedTests = process.argv.slice(2); | ||
| let changedRun: ReturnType<typeof inspectChangedRun> = null; | ||
| try { | ||
| changedRun = inspectChangedRun(requestedTests); | ||
| } catch (error) { | ||
| console.error(error instanceof Error ? error.message : String(error)); | ||
| const guiDependencies = ensureGuiDependencies(); | ||
| if (guiDependencies.kind === "failed") { | ||
| console.error( | ||
| "[test] could not install gui/node_modules, which tests importing gui/src need to resolve React.\n" | ||
| + " Run it manually: cd gui && bun install --frozen-lockfile\n" | ||
| + (guiDependencies.detail ? ` ${guiDependencies.detail.trim().split("\n").slice(-3).join("\n ")}` : ""), | ||
| ); | ||
| process.exitCode = 1; | ||
| } | ||
| let changedRun: ReturnType<typeof inspectChangedRun> = null; | ||
| if (process.exitCode !== 1) { | ||
| try { | ||
| changedRun = inspectChangedRun(requestedTests); | ||
| } catch (error) { | ||
| console.error(error instanceof Error ? error.message : String(error)); | ||
| process.exitCode = 1; | ||
| } | ||
| } | ||
| if (process.exitCode !== 1) { | ||
| if (changedRun) { | ||
| console.warn( | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,6 +5,7 @@ import { isAbsolute, join } from "node:path"; | |||||||||||||||||||||
| import { | ||||||||||||||||||||||
| changedSelectionFailure, | ||||||||||||||||||||||
| createIsolatedTestEnvironment, | ||||||||||||||||||||||
| ensureGuiDependencies, | ||||||||||||||||||||||
| inspectChangedRun, | ||||||||||||||||||||||
| resolveBunTestArgs, | ||||||||||||||||||||||
| resolveBunTestPlan, | ||||||||||||||||||||||
|
|
@@ -459,3 +460,87 @@ describe("bun test machine lock", () => { | |||||||||||||||||||||
| } | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| describe("ensureGuiDependencies", () => { | ||||||||||||||||||||||
| // `gui` is not a workspace, so a root `bun install` leaves gui/node_modules absent and the | ||||||||||||||||||||||
| // twenty-five tests importing gui/src die on `Cannot find package 'react'` — an "Unhandled error | ||||||||||||||||||||||
| // between tests" that names no test. CI already installs them; this closes the local gap. | ||||||||||||||||||||||
| const paths = (present: string[]) => { | ||||||||||||||||||||||
| const normalized = present.map(path => path.replaceAll("\\", "/")); | ||||||||||||||||||||||
| return (path: string) => normalized.some(entry => path.replaceAll("\\", "/").endsWith(entry)); | ||||||||||||||||||||||
|
Comment on lines
+468
to
+470
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win Use a boundary-aware path comparison. The const normalized = present.map(path => path.replaceAll("\\", "/"));
return (path: string) => {
- return normalized.some(entry => path.replaceAll("\\", "/").endsWith(entry));
+ const candidate = path.replaceAll("\\", "/");
+ return normalized.some(entry => candidate === entry || candidate.endsWith(`/${entry}`));
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||
| }; | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("mocked paths match POSIX and Windows separators", () => { | ||||||||||||||||||||||
| const exists = paths(["gui/package.json"]); | ||||||||||||||||||||||
| expect(exists("/repo/gui/package.json")).toBe(true); | ||||||||||||||||||||||
| expect(exists("C:\\repo\\gui\\package.json")).toBe(true); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("installs when gui/package.json exists but node_modules does not", () => { | ||||||||||||||||||||||
| const installed: string[] = []; | ||||||||||||||||||||||
| const logged: string[] = []; | ||||||||||||||||||||||
| const result = ensureGuiDependencies({ | ||||||||||||||||||||||
| cwd: "/repo", | ||||||||||||||||||||||
| exists: paths(["gui/package.json"]), | ||||||||||||||||||||||
| install: dir => { installed.push(dir); return { ok: true, detail: "" }; }, | ||||||||||||||||||||||
| log: message => logged.push(message), | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(result).toEqual({ kind: "installed" }); | ||||||||||||||||||||||
| expect(installed).toEqual([join("/repo", "gui")]); | ||||||||||||||||||||||
| expect(logged[0]).toContain("gui dependencies are missing or incomplete"); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("retries when node_modules exists without the required dependency", () => { | ||||||||||||||||||||||
| let installs = 0; | ||||||||||||||||||||||
| const result = ensureGuiDependencies({ | ||||||||||||||||||||||
| cwd: "/repo", | ||||||||||||||||||||||
| exists: paths(["gui/package.json", "gui/node_modules"]), | ||||||||||||||||||||||
| install: () => { installs += 1; return { ok: true, detail: "" }; }, | ||||||||||||||||||||||
| log: () => {}, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(result).toEqual({ kind: "installed" }); | ||||||||||||||||||||||
| expect(installs).toBe(1); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| test("does nothing when the required dependency is already there", () => { | ||||||||||||||||||||||
| let installs = 0; | ||||||||||||||||||||||
| const result = ensureGuiDependencies({ | ||||||||||||||||||||||
| cwd: "/repo", | ||||||||||||||||||||||
| exists: paths(["gui/package.json", "gui/node_modules/react/package.json"]), | ||||||||||||||||||||||
| install: () => { installs += 1; return { ok: true, detail: "" }; }, | ||||||||||||||||||||||
| log: () => {}, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(result).toEqual({ kind: "present" }); | ||||||||||||||||||||||
| expect(installs).toBe(0); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // A published install tree has no gui/ at all; the runner must not try to install there. | ||||||||||||||||||||||
| test("does nothing when there is no gui package", () => { | ||||||||||||||||||||||
| let installs = 0; | ||||||||||||||||||||||
| const result = ensureGuiDependencies({ | ||||||||||||||||||||||
| cwd: "/repo", | ||||||||||||||||||||||
| exists: () => false, | ||||||||||||||||||||||
| install: () => { installs += 1; return { ok: true, detail: "" }; }, | ||||||||||||||||||||||
| log: () => {}, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(result).toEqual({ kind: "absent" }); | ||||||||||||||||||||||
| expect(installs).toBe(0); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| // Offline or a lockfile drift has to surface as its own message, not as twenty-five | ||||||||||||||||||||||
| // unexplained React failures once the lanes start. | ||||||||||||||||||||||
| test("reports the failure detail instead of continuing", () => { | ||||||||||||||||||||||
| const result = ensureGuiDependencies({ | ||||||||||||||||||||||
| cwd: "/repo", | ||||||||||||||||||||||
| exists: paths(["gui/package.json"]), | ||||||||||||||||||||||
| install: () => ({ ok: false, detail: "lockfile had changes" }), | ||||||||||||||||||||||
| log: () => {}, | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
|
|
||||||||||||||||||||||
| expect(result).toEqual({ kind: "failed", detail: "lockfile had changes" }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
| }); | ||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
🔎 Supported by static analysis
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 3368
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 50376
🏁 Script executed:
Repository: lidge-jun/opencodex
Length of output: 1243
Remove or update the hard-coded GUI test count.
scripts/test.ts:438states that 25 test files importgui/src, but the current source contains 27 such files. Remove the count or update it to 27.🤖 Prompt for AI Agents