From 3563e8ad139be0cb40c8858c7a1c8e6ba7516804 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 07:03:43 -0700 Subject: [PATCH 1/2] Unify the check gate with CI's test command bun run check ran the guard, not the test suite, while CI ran the suite itself, so a local green could mask CI failures. test now holds the seeded randomized suite; check, the guard, and CI all resolve to it. --- .github/workflows/ci.yml | 17 +++++++-------- AGENTS.md | 7 ++++-- CHANGELOG.md | 5 +++++ CONTRIBUTING.md | 5 +++-- package.json | 5 +++-- scripts/guard-real-projects-dir.ts | 6 ++--- tests/unit/check-gate.test.ts | 35 ++++++++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 18 deletions(-) create mode 100644 tests/unit/check-gate.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a5112f4b..894fbe75f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -116,13 +116,12 @@ jobs: - name: Build run: bun run build - # Corbits Code codebase only — ./src and ./tests. The vendored interchange - # inference package (vendor/) is out of scope for this repo's CI. + # Same script the local `bun run check` gate runs: the projects-dir + # guard wraps `bun run test`, which is the seeded, randomized suite + # (bun test ./src ./tests ./evals --randomize --seed 424242) defined + # once in package.json. Randomized order catches tests that only pass + # in the default file order (shared module-level state, an unrestored + # global mock, a leaked env var); the seed is fixed so a failure here + # reproduces locally with `bun run test`. - name: Test - run: bun run test - - # Catches tests that only pass because of the default file order (shared - # module-level state, an unrestored global mock, a leaked env var). The - # seed is fixed so a failure here reproduces locally with the same flag. - - name: Test (randomized order) - run: bun test ./src ./tests ./evals --randomize --seed 424242 + run: bun run check:projects-dir-guard diff --git a/AGENTS.md b/AGENTS.md index dbd7ca50e..6251fa58e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,11 +42,14 @@ When refactoring replaces an old path, delete the old one. No back-compat shims, bun run check ``` -`bun run check` is the single pre-PR gate: it runs `lint`, `typecheck`, `build`, and `test`, in that order, matching CI. +`bun run check` is the single pre-PR gate: it runs `lint`, `typecheck`, +`build`, and `check:projects-dir-guard` — which runs the `test` suite under +the projects-dir sandbox guard — in that order, matching CI. Run the full suite before declaring any task complete. Do not substitute individual targets. If a failure is pre-existing and unrelated to your change, say so explicitly. -`bun run test` runs `bun test ./src ./tests ./evals`. A bare `bun test` also +`bun run test` runs `bun test ./src ./tests ./evals --randomize --seed 424242` — +the same suite CI runs. A bare `bun test` also scans `vendor/`, adding hundreds of unrelated results and making pass/fail counts meaningless to compare across branches — always use `bun run test`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f550068d..1cca9413a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,11 @@ parallel copies under `docs/` or `scripts/notes/`. At cut time: rename ### Changed - Interactive TUI pins OpenTUI 0.5.10 (`@opentui/core`, keymap, solid, and native platform packages in lockstep). +- `bun run check` now runs the full test suite with the same seed CI uses + (`--randomize --seed 424242`), so a local green can no longer mask a CI test + failure. CI's test job invokes the same `check:projects-dir-guard` script + instead of duplicating the test command, and the projects-dir guard no longer + squats on the `test` script name. ## [0.3.14] - 2026-09-03 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 396f1030a..4bf11dc36 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -30,8 +30,9 @@ bun run build bun run test ``` -These match the CI workflow in `.github/workflows/ci.yml`. Run the full suite -before opening a PR. Do not substitute a bare `bun test` (it also scans +These match the CI workflow in `.github/workflows/ci.yml`. Run `bun run check` +(lint, typecheck, build, and the guarded test suite) before opening a PR. Do +not substitute a bare `bun test` (it also scans `vendor/` and pollutes pass/fail counts). ## Commits diff --git a/package.json b/package.json index 74112a3ce..ac1c01adc 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,10 @@ "build": "bun build ./src/index.ts --outdir ./dist --target bun --external '@opentui/core-*' && bun scripts/copy-repo-plugins.ts", "build:bin": "bun build ./src/index.ts --compile --minify --define process.env.NODE_ENV='\"production\"' --outfile ./dist/corbits && bun scripts/copy-repo-plugins.ts", "typecheck": "tsc --noEmit", - "test": "bun scripts/guard-real-projects-dir.ts ./src ./tests ./evals", + "test": "bun test ./src ./tests ./evals --randomize --seed 424242", "lint": "prettier --check --cache . && eslint --cache .", - "check": "bun run lint && bun run typecheck && bun run build && bun run test", + "check:projects-dir-guard": "bun scripts/guard-real-projects-dir.ts", + "check": "bun run lint && bun run typecheck && bun run build && bun run check:projects-dir-guard", "start": "bun run build && bun ./dist/index.js", "eval:capability": "bun scripts/eval-capability.ts", "eval:public-swe-one": "bun scripts/eval-public-swe-one.ts", diff --git a/scripts/guard-real-projects-dir.ts b/scripts/guard-real-projects-dir.ts index c80acaa1e..3c897cb77 100644 --- a/scripts/guard-real-projects-dir.ts +++ b/scripts/guard-real-projects-dir.ts @@ -4,7 +4,8 @@ import { join } from "node:path"; import { mkdir, readdir, rm } from "node:fs/promises"; import { spawn } from "node:child_process"; -// Runs `bun test` and fails the run if any test wrote into the real +// Runs the test suite (`bun run test` — the same seeded, randomized command +// CI runs) and fails the run if any test wrote into the real // ~/.corbits/projects directory. Tests must sandbox state under a temp // `home` (see src/session/index.ts's `home` overrides); nothing running // under this wrapper is allowed to fall back to the developer's own @@ -44,8 +45,7 @@ async function main(): Promise { const runTmpDir = join(tmpdir(), `corbits-test-guard-${runId}`); await mkdir(runTmpDir, { recursive: true }); - const args = process.argv.slice(2); - const child = spawn("bun", ["test", ...args], { + const child = spawn("bun", ["run", "test"], { stdio: "inherit", env: { ...process.env, TMPDIR: runTmpDir, TMP: runTmpDir, TEMP: runTmpDir }, }); diff --git a/tests/unit/check-gate.test.ts b/tests/unit/check-gate.test.ts new file mode 100644 index 000000000..dde327caf --- /dev/null +++ b/tests/unit/check-gate.test.ts @@ -0,0 +1,35 @@ +import { readFileSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, test } from "bun:test"; + +// Guard against the gate drifting apart again (CL-7300): `bun run check` and +// CI's test job must resolve to the same seeded suite, and the projects-dir +// guard must delegate to the `test` script rather than duplicate its command. + +const repoRoot = join(import.meta.dir, "..", ".."); +const pkg = JSON.parse(readFileSync(join(repoRoot, "package.json"), "utf8")) as { + scripts: Record; +}; +const ci = readFileSync(join(repoRoot, ".github", "workflows", "ci.yml"), "utf8"); +const guardSource = readFileSync(join(repoRoot, "scripts", "guard-real-projects-dir.ts"), "utf8"); + +const GUARD_SCRIPT = "check:projects-dir-guard"; +const TEST_SUITE = "bun test ./src ./tests ./evals --randomize --seed 424242"; + +describe("check gate", () => { + test("`test` is the seeded, randomized suite CI runs", () => { + expect(pkg.scripts.test).toBe(TEST_SUITE); + }); + + test("`check` runs the suite through the projects-dir guard", () => { + expect(pkg.scripts[GUARD_SCRIPT]).toContain("scripts/guard-real-projects-dir.ts"); + expect(pkg.scripts.check).toContain(`bun run ${GUARD_SCRIPT}`); + // The guard delegates to `bun run test` so the suite command has one home. + expect(guardSource).toContain('"run", "test"'); + }); + + test("CI's test job invokes the same script, not a raw bun test command", () => { + expect(ci).toContain(`run: bun run ${GUARD_SCRIPT}`); + expect(ci).not.toMatch(/^\s*run: bun test /m); + }); +}); From a5d47421f691bfebebada25793ea169d4d515891 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Thu, 3 Sep 2026 07:20:21 -0700 Subject: [PATCH 2/2] Tighten the gate drift test and document the unguarded test path The negative assertion missed the likeliest regression: an unguarded run: bun run test step in CI, which passes both prior checks while reintroducing the drift and skipping the projects-dir sandbox. Pin the anchor to end-of-line and note the guard scope in CONTRIBUTING. --- CONTRIBUTING.md | 4 +++- tests/unit/check-gate.test.ts | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4bf11dc36..e5c2c8789 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -31,7 +31,9 @@ bun run test ``` These match the CI workflow in `.github/workflows/ci.yml`. Run `bun run check` -(lint, typecheck, build, and the guarded test suite) before opening a PR. Do +(lint, typecheck, build, and the guarded test suite) before opening a PR — +`bun run test` alone skips the projects-dir sandbox guard, which only runs +under `bun run check` and CI. Do not substitute a bare `bun test` (it also scans `vendor/` and pollutes pass/fail counts). diff --git a/tests/unit/check-gate.test.ts b/tests/unit/check-gate.test.ts index dde327caf..13edfa4ab 100644 --- a/tests/unit/check-gate.test.ts +++ b/tests/unit/check-gate.test.ts @@ -28,8 +28,11 @@ describe("check gate", () => { expect(guardSource).toContain('"run", "test"'); }); - test("CI's test job invokes the same script, not a raw bun test command", () => { + test("CI's test job invokes the same script, not a raw test command", () => { expect(ci).toContain(`run: bun run ${GUARD_SCRIPT}`); - expect(ci).not.toMatch(/^\s*run: bun test /m); + // An unguarded suite step here would reintroduce the local-green/CI + // mismatch (and skip the projects-dir sandbox) this gate exists to prevent. + expect(ci).not.toMatch(/^\s*run: bun test(\s|$)/m); + expect(ci).not.toMatch(/^\s*run: bun run test(\s|$)/m); }); });