Files
callstack__agent-device/scripts/lib/vitest-concurrency.ts
Michał Pierzchała c4b1a6131e dx(test): opt-in worker-count override for solo local vitest runs (#1964)
* dx(test): opt-in worker-count override for solo local vitest runs

resolveVitestMaxWorkers() caps local runs at 2 workers so parallel
worktrees and spawn-heavy tests keep headroom, but a solo run that owns
the machine pays 6x on a 12-core host for no benefit.

Add AGENT_DEVICE_VITEST_MAX_WORKERS to opt in to a higher cap. It is
clamped to os.cpus().length so a runaway value can't oversubscribe the
host, and it is a no-op in CI (CI already derives its own worker count).
A missing, blank, non-numeric, non-integer, or non-positive value falls
through to the existing default cap rather than throwing. Default
(unset) behavior is unchanged.

Closes #1962

* docs: tighten the worker-override note to fit the agent-guidance budget

docs/agents/testing.md sits at a 10,000-byte per-file ceiling enforced by
check:agent-guidance, and the first phrasing pushed it to 10,065. Restate
the override in one tighter bullet that leads with the "solo run only"
caveat, which is the constraint a reader most needs.

* fix(test): clamp the worker override with os.availableParallelism()

Node documents cpus().length as unfit for sizing application parallelism:
it ignores CPU affinity and cgroup limits, so it can report a pool wider
than the process may actually use. Clamping against it would inflate the
very ceiling this override's safety clamp exists to enforce.

availableParallelism() honors those constraints, so the clamp now means
what it claims on constrained hosts. Test updated to match.

* test: keep the resolver cases in the already-included setup test

Review feedback: a new test file beside the resolver, plus its entry in
vitest.config.ts's unit-core include list, is a change to test discovery
that the mutation lane's `vitest related` graph reads. Fold the override
cases into src/__tests__/hermetic-env-setup.test.ts, which is already in
the unit suite and already imports the resolver, and drop the config edit
entirely so this PR no longer touches test discovery at all.

Same six assertions, no coverage lost.
2026-08-24 10:26:25 +02:00

39 lines
1.7 KiB
TypeScript

import os from 'node:os';
/**
* Keep one Vitest invocation modest enough to coexist with two other Codex
* worktrees on a 12-core development host: 3 agents + (3 suites * 2 workers)
* leaves roughly 3 cores for runners, subprocesses, simulators, and the OS.
*/
export const DEFAULT_VITEST_MAX_WORKERS = 2;
/**
* Opt-in escape hatch for a solo local run that owns the whole machine (see
* docs/agents/testing.md). Ignored in CI, which already derives its own
* worker count from the isolated runner's CPU pool.
*/
export const VITEST_MAX_WORKERS_OVERRIDE_ENV = 'AGENT_DEVICE_VITEST_MAX_WORKERS';
export function resolveVitestMaxWorkers(env: NodeJS.ProcessEnv = process.env): number | undefined {
if (env.CI === 'true') return undefined;
const override = parsePositiveInt(env[VITEST_MAX_WORKERS_OVERRIDE_ENV]);
// Clamp rather than trust the override literally: a typo like `999` must not
// oversubscribe the host the way the default cap above exists to prevent.
// availableParallelism(), not cpus().length: Node documents the latter as
// unfit for sizing parallelism because it ignores CPU affinity and cgroup
// limits, which would inflate the ceiling this clamp exists to enforce.
if (override !== undefined) return Math.min(override, os.availableParallelism());
return DEFAULT_VITEST_MAX_WORKERS;
}
// A missing, blank, non-numeric, non-integer, or non-positive value falls
// through to the default cap instead of throwing or coercing to something
// surprising (e.g. `Number('')` is 0, not NaN).
function parsePositiveInt(value: string | undefined): number | undefined {
if (value === undefined || value.trim() === '') return undefined;
const parsed = Number(value);
return Number.isInteger(parsed) && parsed > 0 ? parsed : undefined;
}