Finding (2026-07-24 test-suite timing audit)
A full-suite vitest run --reporter=json timing pass found that test/unit/mcp-cli-*.test.ts (78
files) accounts for 423s of the suite's 1,052s summed test time — 40%. The 13 slowest files alone:
| File |
Time (full-suite, contended) |
run()/runAsync() calls |
| mcp-cli-packets.test.ts |
43.1s |
37 |
| mcp-cli-maintain.test.ts |
38.2s |
53 |
| mcp-cli-doctor.test.ts |
34.6s |
27 |
| mcp-cli-basics.test.ts |
31.9s |
37 |
| mcp-cli-profiles.test.ts |
30.3s |
25 |
| mcp-cli-review-pr.test.ts |
18.5s |
— |
| mcp-cli-telemetry.test.ts |
13.4s |
— |
| mcp-cli-notifications.test.ts |
13.3s |
— |
| mcp-cli-pr-outcomes.test.ts |
12.9s |
— |
| mcp-cli-maintain-tools.test.ts |
12.8s |
— |
| mcp-cli-monitor-open-prs.test.ts |
12.3s |
— |
| mcp-discovery.test.ts |
12.0s |
— |
| mcp-local-telemetry-chokepoint.test.ts |
11.8s |
— |
Root cause
test/unit/support/mcp-cli-harness.ts's run() spawns the REAL CLI as a subprocess
(execFileSync(process.execPath, ["--experimental-strip-types", bin, ...args])) for every call —
deliberately, per its own header comment, because a subprocess is the only way to exercise the real
process entrypoint (argv parsing, exit codes, real stdout/stderr) and because v8 can't Codecov-instrument
a separate process. A parallel -inprocess/-stdio file family already exists specifically to get
coverage numbers via InMemoryTransport instead (those run in ~0.2s each) — this is deliberate
architecture, not an oversight. Do not "fix" this by deleting/weakening the subprocess tests.
Isolated timing (single file, zero contention from other test files):
mcp-cli-packets.test.ts alone: 22 tests, 15.45s "tests" time (~0.4s/spawn) -- close to a single bare
invocation's cost (node --experimental-strip-types bin/loopover-mcp.ts --help = ~0.3-0.4s real time,
vs. ~0.05s for bare node -e "1" -- the ~250-350ms delta is module-graph resolution + type-stripping
the CLI's own import graph, not V8 recompilation).
- The SAME file in the full-suite run: 43.1s -- roughly 3x the isolated cost. This gap is CPU
oversubscription: vitest's default worker pool runs many files concurrently (12 cores on the machine
this was measured on), and dozens of subprocess-spawning files firing real OS process creation
simultaneously overwhelms it.
Already ruled out as a free fix: NODE_COMPILE_CACHE (Node 22's built-in V8 compile cache) -- tested
directly, made no measurable difference (0.27-0.34s either way across 5 runs with/without). The cost is
module resolution, not bytecode recompilation. Also tried --pool=forks vs. the default threads pool
on a single file in isolation -- no meaningful difference (15.78s vs. 16.44s, within run-to-run noise).
Candidate fixes (neither attempted -- both need real design + validation)
- Constrain concurrency for subprocess-heavy files specifically, e.g. a vitest workspace/
projects
split so mcp-cli-*.test.ts (minus the -inprocess/-stdio files) runs with a lower maxWorkers/
poolOptions than the rest of the suite, without reducing parallelism for the other ~1,084 files.
Needs careful validation against how CI currently shards + merges coverage (test:coverage runs
sharded in CI, merged after -- see .claude/skills/contributing-to-loopover/reference.md) so a
projects split doesn't silently break shard balancing or coverage merging. Requires a real controlled
A/B measurement of full-suite wall-clock before/after, not just this one file.
- Reduce subprocess-spawn COUNT per file by converting cases that don't actually need real-process
boundary realism (e.g. argument-validation-only tests, or tests already covered by an -inprocess
sibling) to the in-process harness, file by file. This needs per-file judgment -- risks silently
losing genuine subprocess-boundary coverage (real argv parsing, real exit codes, real stdout framing)
if done carelessly. Start with the 5 heaviest files (packets/maintain/doctor/basics/profiles, 179
combined run() calls) since they dominate the total.
Requirements for whoever picks this up
- Real before/after full-suite wall-clock measurement for any concurrency change (single-file timing
is not representative -- see the 3x isolated-vs-contended gap above).
- Never weaken or delete a subprocess-based test to make it faster; the subprocess boundary is the
point of these specific tests.
- Verify CI's existing shard balancing and coverage-merge behavior is unaffected by any workspace/
projects restructuring, in a dedicated CI run before merging.
Finding (2026-07-24 test-suite timing audit)
A full-suite
vitest run --reporter=jsontiming pass found thattest/unit/mcp-cli-*.test.ts(78files) accounts for 423s of the suite's 1,052s summed test time — 40%. The 13 slowest files alone:
run()/runAsync()callsRoot cause
test/unit/support/mcp-cli-harness.ts'srun()spawns the REAL CLI as a subprocess(
execFileSync(process.execPath, ["--experimental-strip-types", bin, ...args])) for every call —deliberately, per its own header comment, because a subprocess is the only way to exercise the real
process entrypoint (argv parsing, exit codes, real stdout/stderr) and because v8 can't Codecov-instrument
a separate process. A parallel
-inprocess/-stdiofile family already exists specifically to getcoverage numbers via
InMemoryTransportinstead (those run in ~0.2s each) — this is deliberatearchitecture, not an oversight. Do not "fix" this by deleting/weakening the subprocess tests.
Isolated timing (single file, zero contention from other test files):
mcp-cli-packets.test.tsalone: 22 tests, 15.45s "tests" time (~0.4s/spawn) -- close to a single bareinvocation's cost (
node --experimental-strip-types bin/loopover-mcp.ts --help= ~0.3-0.4s real time,vs. ~0.05s for bare
node -e "1"-- the ~250-350ms delta is module-graph resolution + type-strippingthe CLI's own import graph, not V8 recompilation).
oversubscription: vitest's default worker pool runs many files concurrently (12 cores on the machine
this was measured on), and dozens of subprocess-spawning files firing real OS process creation
simultaneously overwhelms it.
Already ruled out as a free fix:
NODE_COMPILE_CACHE(Node 22's built-in V8 compile cache) -- testeddirectly, made no measurable difference (0.27-0.34s either way across 5 runs with/without). The cost is
module resolution, not bytecode recompilation. Also tried
--pool=forksvs. the defaultthreadspoolon a single file in isolation -- no meaningful difference (15.78s vs. 16.44s, within run-to-run noise).
Candidate fixes (neither attempted -- both need real design + validation)
projectssplit so
mcp-cli-*.test.ts(minus the-inprocess/-stdiofiles) runs with a lowermaxWorkers/poolOptionsthan the rest of the suite, without reducing parallelism for the other ~1,084 files.Needs careful validation against how CI currently shards + merges coverage (
test:coveragerunssharded in CI, merged after -- see
.claude/skills/contributing-to-loopover/reference.md) so aprojects split doesn't silently break shard balancing or coverage merging. Requires a real controlled
A/B measurement of full-suite wall-clock before/after, not just this one file.
boundary realism (e.g. argument-validation-only tests, or tests already covered by an
-inprocesssibling) to the in-process harness, file by file. This needs per-file judgment -- risks silently
losing genuine subprocess-boundary coverage (real argv parsing, real exit codes, real stdout framing)
if done carelessly. Start with the 5 heaviest files (packets/maintain/doctor/basics/profiles, 179
combined
run()calls) since they dominate the total.Requirements for whoever picks this up
is not representative -- see the 3x isolated-vs-contended gap above).
point of these specific tests.
projects restructuring, in a dedicated CI run before merging.