Skip to content

Give every Bun test runner one scheduling policy (Fixes #3139) - #3143

Merged
acoliver merged 5 commits into
mainfrom
issue3139
Aug 8, 2026
Merged

Give every Bun test runner one scheduling policy (Fixes #3139)#3143
acoliver merged 5 commits into
mainfrom
issue3139

Conversation

@acoliver

@acoliver acoliver commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

TLDR

CI was green on only 9 of the last 20 first attempts. The failures never named the change under review — a different file timed out on each run, and every one of them passed in isolation.

Four workspace test runners each carried their own concurrency and timeout policy. agents was tuned against measurement in #3084; the other three were not, and the shards still running the untuned policy are exactly the shards that fail. The tests were not broken, they were starved.

This gives all of them one policy, and pins the invariant that let them drift.

Reviewers should look at: the measurement table below (it is the whole argument), and scripts/lib/bun-test-policy.ts.

Dive Deeper

The measurement that decides it

Throughput does not degrade gracefully when the pool matches the core count — it collapses. The 680-file cli suite on a 16-core machine:

processes per core wall clock
0.25 184s
0.50 124s ← fastest
1.00 >9 min, not one file completed

GitHub's ubuntu-latest has 4 vCPUs, and cli was asking for 4 processes — precisely the setting where nothing finishes. Half the cores is both the fastest point measured and the one that leaves a test able to finish inside its budget.

What each runner was doing

Runner Concurrency Per-test Per-file First-attempt CI failures
agents (tuned in #3084) half cores, ≤4 180s 300s 4/16
cli full cores, ≤8 30s 120s 5/15
core ≤2 30s 60s 2/14
auth full cores, ≤8 none passed → Bun's 5s default 60s
scripts/run_bun_tests.ts n/a 30s 120s

cli had the worst of both ends. auth passed no --timeout at all — and a [test] timeout key in bunfig.toml does not help, because Bun 1.3.14 ignores it. scripts/run_bun_tests.ts defaulted to 30s while the workspace runner beside it used 180s, so the same test had two different budgets depending on which path CI took.

What is centralised, and what is not

scripts/lib/bun-test-policy.ts owns concurrency and the timeout budgets. Differences that were deliberate are preserved:

  • core keeps its cap of 2 (its files are unusually heavy).
  • cli keeps a larger budget for integration files that spawn the real CLI.
  • Every runner keeps its concurrency override, for pinning a run to 1 while chasing a flake.

What is gone is the divergence nobody chose.

Two defects found while wiring it up

  1. auth scheduled in fixed batches. A batch advanced only when its slowest file finished, leaving workers idle with queued work. It now uses the same worker pool as the other runners.
  2. cli's integration budget was a fixed 120s — which silently became smaller than the unit budget once that rose to 180s, handing the slowest tests in the workspace the tightest bound. It is now a multiple of the shared budget, so it cannot invert again. This was caught by an existing test in the repo, not by me.

Ruled out

  • Not OOM. A CLI test file peaks at 431 MB RSS; four fit comfortably in 16 GB. The SIGKILLs in CI logs are the runners' own per-file timeout kills.
  • Not storage contention. isolateStorageRoots() uses mkdtempSync, so every process gets a unique root.
  • Not the code under review. PermissionsModifyTrustDialog.test.tsx failed twice on a branch whose diff contained zero packages/cli/ files.

Reviewer Test Plan

1. The policy and its invariants.

bun test scripts/tests/bun-test-policy.bun.test.ts    # 17 pass

The last three cases are the ones that matter: no runner may spawn bun test without an explicit --timeout, none may recompute concurrency for itself, and all must derive from the shared module. To see them bite, delete the --timeout argument from any runner and re-run — the suite names the offending file.

2. Confirm the collapse for yourself. On an N-core machine:

cd packages/cli
bun run-bun-tests.ts --concurrency $((N/2))   # fast
bun run-bun-tests.ts --concurrency $N         # does not finish

3. Each runner still works.

cd packages/auth  && bun run-bun-tests.ts    # 43/43
cd packages/core  && bun run-bun-tests.ts    # 359/359
cd packages/agents && bun run-bun-tests.ts

4. The real verdict is this PR's own CI — see the result below.

Result on this PR's own CI

The first attempt was green: 39 pass, 0 fail, 0 reruns.

Shard cost, measured rather than assumed (Test (ubuntu-latest) [cli], attempt 1):

Run Before/after Outcome Duration
31219425232 before failure 10min
31222510277 before success 10min
31223905659 before failure 8min
31225517281 before failure 9min
31236205675 after success 12min

The cli shard costs about two minutes more. That is a real cost and worth stating plainly — but a shard that failed on 3 of those 4 first attempts had to be run roughly twice to land, so the expected time to a green cli shard goes down, and the human round-trip of noticing a spurious failure and clicking rerun disappears.

Every other shard on this run: agents 6min, scripts 8min, providers 4min, rest 3min, core 2min — all green.

One run is not a flake rate. The honest measure is the first-attempt green rate of the PRs that follow this one, against the 9/20 baseline.

Testing Matrix

🍏 🪟 🐧
npm run
npx
Docker
Podman - -
Seatbelt - -

Verified on macOS 26.4 arm64 / Bun 1.3.14:

  • npm run typecheck — exit 0
  • npm run lint — exit 0
  • npm run format — clean
  • npm run build — exit 0
  • Smoke: bun scripts/start.ts --profile-load stepfun-37 "write me a haiku and nothing else" — exit 0
  • bun test scripts/tests/bun-test-policy.bun.test.ts — 19 pass
  • packages/auth — 43/43 · packages/core — 359/359 · packages/cli680/680 files, 8761 tests, 0 failed
  • Cost, measured rather than assumed: core 95.59s before → 95.91s after

npm run test across every workspace exited 1, with two agents files timing out:
createAgent.harness.behavior.test.ts and lspControl.behavior.test.ts. Both pass
together in 1.05s standalone. That run was on a machine at load average 30
with four other checkouts running their own suites — and agents concurrency is
unchanged by this PR, since it was already the tuned runner. Everything else,
including the cli suite this PR changes most, passed.

Windows is worth a look from someone who has one: core keeps its win32 ? 1 : 2 cap, and that path is unchanged, but I cannot exercise it.

Known limitation

availableParallelism() reports the machine's cores, not this process's share of
them. On a CI runner — which is dedicated — those are the same number, so the issue
this PR fixes is unaffected. On a development machine running several checkouts at
once they are not, and half the cores is still more than the runner actually has;
the two agents timeouts above are exactly that. Making the pool load-aware would be
a different change that buys CI nothing, so the LLXPRT_*_TEST_CONCURRENCY override
stays the escape hatch, and the module now says so.

On local repetition

I tried to demonstrate the reduced flake rate by running the agents suite repeatedly. The development machine is shared with other worktrees and sat at load average 20–60 on 16 cores throughout, which is the very condition under test. Numbers taken there would be noise dressed as evidence, so I am not presenting any. The measurements above were taken when the box was quiet, and CI is the honest authority for the flake rate.

Linked issues / bugs

Fixes #3139

Extends the tuning done for agents in #3084 to the runners it never reached.

@coderabbitai

coderabbitai Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The PR adds a shared Bun test policy for timeouts and concurrency. All package runners and the script CLI adopt the policy. Auth tests add explicit per-test timeouts and worker-pool scheduling. Behavioral tests verify policy and runner invariants.

Changes

Bun test execution policy

Layer / File(s) Summary
Policy definition and validation
scripts/lib/bun-test-policy.ts, scripts/tests/bun-test-policy.bun.test.ts, tsconfig.scripts.json
Defines shared timeout and concurrency rules. Tests cover bounds, overrides, macOS CI behavior, timeout ordering, and runner integration.
Package runner policy adoption
packages/agents/run-bun-tests.ts, packages/auth/run-bun-tests.ts, packages/cli/run-bun-tests.ts, packages/core/run-bun-tests.ts
Package runners use shared timeout values and resolveTestConcurrency instead of local CPU-based calculations.
Auth worker-pool scheduling
packages/auth/run-bun-tests.ts, packages/auth/src/__tests__/run-bun-tests.behavior.test.ts
Auth child processes receive explicit per-test timeouts. File execution uses continuously assigned workers, and timeout assertions use the shared policy.
Script timeout integration
scripts/run_bun_tests.ts, packages/core/tsconfig.runner.json
The script CLI uses the shared per-test timeout, and the core runner configuration compiles the shared policy module.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue #3139 by centralizing policy, adding explicit timeouts, preserving overrides, improving auth scheduling, and adding behavioral coverage.
Out of Scope Changes check ✅ Passed The worker-pool change and CLI integration-timeout adjustment directly support the linked issue objectives, with no unrelated changes identified.
Description check ✅ Passed The description covers the required sections, explains the changes and rationale, provides review and testing steps, reports results, and links issue #3139.
Title check ✅ Passed The title clearly and concisely describes the main change: one scheduling policy for every Bun test runner.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue3139

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run label Aug 8, 2026
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Walkthrough

Before this PR, each Bun test runner carried its own timeout, concurrency, and process-management choices, so workspaces such as CLI, auth, agents, core, and scripts could drift apart in how aggressively they parallelized tests and how they handled slow or timed-out suites. After the PR, those runners all draw from one shared Bun test scheduling policy, giving the repository a single source of truth for scheduling behavior while still preserving workspace-specific notes where needed.

Release Notes

New Features

  • Introduced a shared Bun test scheduling policy module so runner timeout, concurrency, and related behavior are defined in one place instead of being duplicated across workspaces.

Bug Fixes

Tests

  • Added behavioral tests that pin the shared scheduling policy and verify runner timeout/concurrency behavior against it.

Refactor

  • Centralized Bun test scheduling policy logic and updated TypeScript runner configs to include the shared module.
  • Replaced per-workspace ad hoc scheduling constants in the adopted runners with the centralized policy.

Changes

Layer File(s) Summary
core scripts/lib/bun-test-policy.ts, packages/core/tsconfig.runner.json, tsconfig.scripts.json Introduces a centralized Bun test scheduling policy module and updates TypeScript configs to include it.
runners packages/cli/run-bun-tests.ts, packages/auth/run-bun-tests.ts, packages/agents/run-bun-tests.ts, packages/core/run-bun-tests.ts, scripts/run_bun_tests.ts Adopts the shared Bun test scheduling policy across all workspace and shard runners, replacing divergent local concurrency and timeout settings.
tests packages/auth/src/tests/run-bun-tests.behavior.test.ts, scripts/tests/bun-test-policy.bun.test.ts Adds behavioral tests pinning the shared scheduling policy and validating runner timeout/concurrency behavior against it.

Sequence Diagram

sequenceDiagram
  participant SharedPolicy as Shared Bun test scheduling policy
  participant Runner as Workspace/shards runner
  participant Child as bun test child process
  Runner->>SharedPolicy: request concurrency and timeout policy
  SharedPolicy-->>Runner: unified scheduling settings
  Runner->>Child: spawn isolated test process with policy-derived timeout/concurrency
  Child-->>Runner: per-file pass/fail result
  Runner->>Runner: aggregate results under shared policy contract
Loading

Magnitude

🎯 2 (M)
523 additions, 85 deletions, 10 changed files across 4 packages, 0 acceptance criteria

Related

Pre-merge Checks

Check Status Note
Title Clear and descriptive title that states the change and references the issue being fixed.
Description Includes all required template sections: TLDR, Dive Deeper, Reviewer Test Plan, Testing Matrix, and Linked issues / bugs.
Linked Issues All four acceptance criteria from #3139 are fulfilled: a single shared policy module is adopted by all four workspace runners plus the shard runner, every runner passes an explicit --timeout flag, behavioral tests pin the policy and runner invariants, and before/after CI measurements are recorded showing first-attempt green improvement.
Out of Scope No out-of-scope changes detected. The PR correctly excludes rewriting slow suites, coverage instrumentation changes, and pre-existing Lint (Javascript) failures, matching the issue's stated out-of-scope items.

Walkthrough generated by LLxprt PR Review. Planner issue: #2256

Comment thread scripts/tests/bun-test-policy.bun.test.ts
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

OpenCodeReview — automatic reviews suspended

Automatic OCR reviews are suspended for this PR after 2 of 2 automatic reviews.

To get more reviews you can:

  • Check the box below to re-enable automatic reviews (resets the counter), or

  • Comment /review, /ocr, or /open-code-review to request a single review on demand.

  • Re-enable automatic reviews

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor

Code Coverage Summary

Package Lines Statements Functions Branches
CLI N/A% N/A% N/A% N/A%
Core N/A% N/A% N/A% N/A%
CLI Package - Full Text Report
CLI full-text-summary.txt not found at: coverage_cli/packages/cli/coverage/full-text-summary.txt
Core Package - Full Text Report
Core full-text-summary.txt not found at: coverage_core/packages/core/coverage/full-text-summary.txt

For detailed HTML reports, please see the 'coverage-reports-24.x-ubuntu-latest' artifact from the main CI run.

Comment thread packages/core/run-bun-tests.ts
CI was green on only 9 of the last 20 first attempts. Nearly every PR
needed a rerun, and the failures never named the change under review: a
different file timed out on each run and every one of them passed in
isolation.

Four workspace runners each carried their own concurrency and timeout
policy. `agents` was tuned against measurement in #3084; the other three
were not, and the shards still running the untuned policy are the shards
that fail — `cli` 5/15 first attempts, `agents` 4/16, `core` 2/14.

The tests were not broken, they were starved. `cli` had the worst of both
ends: a pool sized to the full core count and a 30s per-test bound. `auth`
passed no `--timeout` at all, so it ran on Bun's 5s default; a
`[test] timeout` key in bunfig.toml does not help, because Bun 1.3.14
ignores it. `scripts/run_bun_tests.ts` defaulted to 30s while the workspace
runner beside it used 180s, so the same test had two different budgets
depending on which path CI took.

Throughput does not degrade gracefully when the pool matches the core
count; it collapses. Measured on the 680-file `cli` suite, 16 cores:

    processes per core   wall clock
    0.25                 184s
    0.50                 124s
    1.00                 >9min, not one file completed

GitHub's ubuntu-latest runner has 4 vCPUs, and `cli` was asking for 4
processes — exactly the setting where nothing finishes. Half the cores is
both the fastest point measured and the one that leaves a test able to
finish inside its budget.

scripts/lib/bun-test-policy.ts now owns that policy and the runners consume
it. Differences that were deliberate are preserved: `core` keeps its cap of
2, `cli` keeps a larger budget for the integration files that spawn the real
CLI, and every runner keeps its concurrency override for pinning a run while
chasing a flake. What is gone is the divergence nobody chose.

Two defects surfaced while wiring it up. `auth` scheduled in fixed batches,
so a batch advanced only when its slowest file finished and left workers idle
with queued work; it now uses the same worker pool as the others. And `cli`'s
integration budget was a fixed 120s, which silently became *smaller* than the
unit budget once that rose to 180s, handing the slowest tests in the
workspace the tightest bound — it is now expressed as a multiple of the
shared budget, so it cannot invert again.

The invariant that allowed this is now a test: no runner may spawn `bun test`
without an explicit `--timeout`, none may recompute concurrency for itself,
and all must derive from the shared module. `scripts/tests/` files are listed
individually in tsconfig.scripts.json, so the new suite is registered there —
adding it immediately caught a type error the file would otherwise have
carried.

Cost, measured rather than assumed: `core` 95.59s before, 95.91s after.
scripts/run_bun_tests.ts spawns bun test children and was changed by this
PR to share the timeout policy, but it was missing from RUNNERS — so the
invariants that pin the fix did not actually cover it. Removing its
--timeout or its import of the shared module would have gone unnoticed,
which is the exact failure this suite exists to prevent.

It already satisfies all three invariants; adding it only closes the hole
in the coverage.
The module comment still described a per-file timeout of 60s on POSIX and
180s on Windows. Those values moved to the shared policy in this PR, so the
comment now contradicted the code it sits above.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/auth/run-bun-tests.ts`:
- Around line 216-231: Update the timeout handling in runTestFile so a timed-out
child sets a timeout flag and sends SIGKILL but does not resolve immediately;
resolve the test promise from the child’s close event, following the pattern in
run-bun-tests.ts for the child lifecycle. This keeps the worker occupied until
the process has fully closed while preserving the timeout result.

In `@scripts/lib/bun-test-policy.ts`:
- Around line 116-126: Update the environment override handling in the policy
function around options.envVar to validate the parsed integer with
Number.isSafeInteger() before returning it. Reject values that are not safe
integers using the existing invalid-override error path, while preserving valid
positive integer overrides and the current unclamped behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 76292270-008f-4727-bac6-3ae56f59f59e

📥 Commits

Reviewing files that changed from the base of the PR and between 9658f64 and 88f4a7b.

📒 Files selected for processing (10)
  • packages/agents/run-bun-tests.ts
  • packages/auth/run-bun-tests.ts
  • packages/auth/src/__tests__/run-bun-tests.behavior.test.ts
  • packages/cli/run-bun-tests.ts
  • packages/core/run-bun-tests.ts
  • packages/core/tsconfig.runner.json
  • scripts/lib/bun-test-policy.ts
  • scripts/run_bun_tests.ts
  • scripts/tests/bun-test-policy.bun.test.ts
  • tsconfig.scripts.json

Comment thread packages/auth/run-bun-tests.ts
Comment thread scripts/lib/bun-test-policy.ts Outdated
CodeRabbit caught that auth reported a timed-out file from inside the
timeout callback. kill() only sends a signal, so the result was produced
while the child was still winding down — and because this PR converted auth
from fixed batches to a worker pool, the freed slot is filled immediately.
The pool would exceed its concurrency cap exactly when the machine is
already struggling, which is the failure this PR exists to remove.

cli had the same defect and already used a worker pool, so it was doing this
in production. Both now settle from the exit handler and carry the timeout
reason on a flag; core already awaited the reap and agents already settled
from exit, so those were correct. killProcessTree sends SIGKILL to the
process group, which cannot be ignored, so exit is guaranteed to arrive.

Also rejects a concurrency override too large to be an exact integer: the
digit-shape check accepted an arbitrarily long run of digits, which parseInt
rounds to an imprecise Number that would then size the worker pool.

I dropped the source-scanning test I first wrote for the reaping rule. core
resolves inside its timeout callback too, but only after awaiting the kill,
which is correct — and no text heuristic distinguishes that from resolving
immediately. A guard that cannot tell right from wrong is worse than none,
so the requirement is documented on the budget it belongs to instead.
A full local suite on a machine running several checkouts at once still
timed out two agents files that pass in about a second standalone. That is
not something the policy can size around: availableParallelism() reports the
machine's cores, so half of them is still more than the runner actually has
when other work already owns the box.

CI runners are dedicated, so the two coincide there and the issue this fixes
is unaffected. The env override is the escape hatch on a shared machine, and
the comment now says so rather than leaving the next person to rediscover
it.
@acoliver

acoliver commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator Author

Note on the agents shard failure in run 31240175861

That run's Test (ubuntu-latest) [agents] failure is not a timeout, so it is a different animal from the starvation this PR addresses. It is a fast-check property test failing an assertion:

error: Property failed after 20 tests
error: Failed to initialize chat: Cannot assemble system prompt: no model
identity is resolved from the active configuration. A model must be set
before the system prompt can be built.

in packages/agents/src/api/__tests__/mutationCoverage.auth.behavior.test.ts.

It is not from this PR. The diff contains zero packages/agents/src/ files — the only agents file touched is run-bun-tests.ts, the runner itself. The error text is precisely the model-identity resolution that 52c7778ca ("Resolve system-prompt model identity from config.getModel() not stale snapshot", #3141) changed; that commit is in this branch's base, having merged shortly before the rebase.

Locally the file passes 5 runs out of 5 (22 tests each), so the seed that fails is rare.

One thing worth flagging rather than burying: this PR raises the agents per-test budget path, and a property test given more time completes more generated cases before its budget expires. It is plausible that the longer budget is what let fast-check reach the input that breaks #3141 — in other words, the old bound may have been hiding this defect rather than the new one causing it. That would be a point in favour of the change, not against it, but it does mean a genuine bug in #3141 may surface more often once this lands.

I have re-run the shard. If it recurs I will open a separate issue against #3141 with the failing seed rather than paper over it here, since the fix belongs in the model-identity code and not in the test runners.

@acoliver
acoliver merged commit 339625c into main Aug 8, 2026
65 of 68 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

maintainer:e2e:ok Trusted contributor; maintainer-approved E2E run

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CI is green on only 45% of first attempts: four Bun test runners with divergent starvation budgets

1 participant