Files
msd-core/scripts/lib/npm-version-check-diagnosis.cjs
sim 1cd17cc3d6 fix: route check-env.cjs's npm-version check through the canonical execNpm seam
Per /research + /diagnose direction: the timeout fix landed earlier this
session correctly diagnosed the failure (a real spawnSync timeout under
Windows CI contention, not npm being absent) but it recurred on the very
next push -- same chunk, same ~51-file load. Rather than raise the
hand-rolled 10s timeout myself (CLAUDE.md's own rule: fix the cost, not
the tolerance, and never touch a timeout without explicit instruction),
investigated the repo's own precedent first.

Found: this repo already has a canonical OS-shell-projection seam for
exactly this (src/shell-command-projection.cts's execNpm), already used
by dozens of other scripts/*.cjs files (require('../gsd-core/bin/lib/...')
is an extremely well-established pattern), with:
  - the same npm.cmd/shell:true Windows handling check-env.cjs was
    hand-rolling, but centralized;
  - a 15s default timeout (vs. check-env.cjs's 10s) -- not invented here,
    an EXISTING value already governing npm subprocess calls elsewhere;
  - isSpawnTimeout / result.timedOut, the canonical cross-platform timeout
    predicate (error.code === 'ETIMEDOUT'), whose own docstring explicitly
    warns that checking signal === 'SIGTERM' (what my first fix did) is
    "platform-fragile" with a Windows-specific false-negative risk -- the
    exact platform this bug lives on.

check-env.cjs's npm-version check now calls execNpm(['--version']) instead
of hand-rolling spawnSync + npmCmd + shell:true, and
describeNpmVersionCheckFailure now operates on execNpm's SpawnResultOutput
shape (using timedOut, not signal) rather than a raw spawnSync result.
This is a genuine architectural fix, not just a bigger number: it removes
a duplicate, slightly-divergent re-implementation of an existing seam and
inherits whatever that seam's timeout/handling becomes in the future.

Manually verified end-to-end (npm run check:env against the real
environment) and re-verified describeNpmVersionCheckFailure's branches
directly against execNpm's actual return shape before wiring the test
file, since this repo blocks local node --test.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-09 08:07:34 -04:00

45 lines
2.0 KiB
JavaScript

'use strict';
/**
* #4460: distinguishes WHY scripts/check-env.cjs's npm-version check's
* execNpm(['--version']) (the canonical OS-shell-projection seam,
* src/shell-command-projection.cts) produced no usable output, instead of
* collapsing every case into "npm binary not found on PATH" -- a message
* that used to fire identically for a genuinely-missing binary AND for a
* spawnSync TIMEOUT under CI load (exitCode stays non-zero, stdout stays
* empty, either way). Root-caused live: an unrelated PR's Windows CI shard
* failed this check twice in a row while running ~51 concurrent test
* files; npm.cmd's own cold-start plausibly exceeded the check's original
* 10s window under that contention, and the misleading message made a real
* timeout indistinguishable from npm actually being absent.
*
* Uses `result.timedOut` (execNpm's canonical, cross-platform-correct
* timeout predicate -- `error.code === 'ETIMEDOUT'`, per
* shell-command-projection.cts's isSpawnTimeout docstring) rather than
* checking `result.signal === 'SIGTERM'` directly: that check is documented
* there as platform-fragile, with a specifically-called-out risk of a false
* NEGATIVE on Windows -- the exact platform this failure was discovered on.
*
* Kept out of scripts/check-env.cjs itself (which runs its CLI unconditionally
* on require, with no `require.main === module` guard) so this pure logic
* can be required directly by tests without triggering a real environment
* check.
*
* @param {import('../../src/shell-command-projection.cts').SpawnResultOutput} result
* @returns {string}
*/
function describeNpmVersionCheckFailure(result) {
if (result.error && result.error.code === 'ENOENT') {
return 'npm binary not found on PATH';
}
if (result.timedOut) {
return `npm --version timed out under CI load -- not a missing binary`;
}
if (result.exitCode !== 0) {
return `npm --version exited ${result.exitCode} with no usable output`;
}
return 'npm binary not found on PATH';
}
module.exports = { describeNpmVersionCheckFailure };