Files
msd-core/tests/helpers/git-fixture.cjs
Tom Boucher 2afe17bbdb test(#3143): add the no-unbounded-spawn guard and throw-preserving git fixture (#3150)
* test(#3143): add no-unbounded-spawn guard and throw-preserving git fixture

Adds the ESLint rule local/no-unbounded-spawn, wired into the tests/**/*.cjs
block, plus an allowlist that only ratchets down: a listed file with zero
violations reports its own entry as stale.

The rule resolves renamed destructures and chained requires rather than
matching literal callee names -- both forms exist in the suite today and a
name-only matcher leaves them permanently invisible. It resolves an options
object held in a single-write const, which is what keeps process-seam.cjs,
the bounded reference implementation, from flagging itself.

timeout: 0 and anything above the 600000ms ceiling are rejected as only
nominally bounded.

Adds tests/helpers/git-fixture.cjs so a migrated execSync call site keeps
its throw-on-non-zero contract; process-seam.cjs is unchanged.

* test(#3143): prove the allowlist guards can actually fail

Extracts the D4/D6/D7/D8 checks into pure helpers and drives each against a
synthetic fixture carrying an injected violation. Without this the suite only
proved that today's clean data passes, which a deleted check would also
satisfy.

* fix(#3143): close two ceiling and alias escapes found in review

Nested arithmetic bypassed the ceiling entirely: the numeric evaluator only
resolved a flat literal, so `timeout: 60 * 60 * 1000` (3600000ms, six times
the ceiling) fell through to trusted and reported nothing. The evaluator now
recurses through arithmetic and unary signs with a depth cap.

Alias resolution was traversal-order dependent, not scope dependent: a call
textually above its own require destructure saw an empty alias map and
reported clean. The map is now built in a Program pre-pass.

Also: an explicit timeoutMs:undefined no longer overwrites the git fixture
default via spread, adds the missing seam-routed rule test, and de-duplicates
the repeated try/catch in the fixture tests.

---------

Co-authored-by: sim <sim@local>
2026-08-07 09:43:36 -04:00

88 lines
3.8 KiB
JavaScript

'use strict';
/**
* git-fixture — a throw-preserving wrapper over the process seam's `runGit`.
*
* Why this exists: `execSync`/`execFileSync` throw on any non-zero exit,
* and 237 sites in this repo's test suite are written against that throw —
* they read `err.status`, `err.stdout`, `err.stderr`. `tests/helpers/
* process-seam.cjs` deliberately never throws (see its own header): every
* outcome, including a non-zero exit, a timeout, or a spawn failure, comes
* back as data on a discriminated-union result. Migrating a throwing
* `execSync`/`execFileSync` call site straight onto the seam without this
* wrapper would silently turn a loud test failure (an uncaught throw) into
* a quiet one (a result object nobody checked) — exactly the kind of
* regression a migration must not introduce.
*
* `gitOrThrow` is that bridge: it calls the seam's `runGit` and re-throws in
* the shape the old idiom produced, with the seam's typed fields attached
* alongside it. `tests/helpers/process-seam.cjs` itself is NOT modified by
* this module — its never-throws contract is intact; this is a layer on
* top, not a change underneath.
*/
const { runGit, OUTCOME } = require('./process-seam.cjs');
/**
* Default timeout for `gitOrThrow` calls, in milliseconds.
*
* 15000ms: these are git plumbing operations (rev-parse, branch, log, ...)
* against a small mkdtemp fixture repo — well over any observed local/CI
* duration for that class of call, and far under the seam's own 60000ms
* default so a hung git surfaces fast instead of riding out the seam's full
* budget.
*/
const DEFAULT_GIT_TIMEOUT_MS = 15000;
/**
* Run `git` via the process seam and throw on anything other than a clean
* exit, preserving the legacy `execSync`/`execFileSync` throw-on-failure
* idiom that existing test code is written against.
*
* @param {string[]} args - argv passed to git (never shell-interpreted).
* @param {object} [options] - forwarded to `runGit`; see process-seam.cjs.
* `options.timeoutMs`, if provided, overrides `DEFAULT_GIT_TIMEOUT_MS`.
* @returns {string} `stdout` on a clean (exit 0) run.
* @throws {Error} On any non-zero exit, timeout, kill, or spawn failure.
* The thrown error carries, as own properties:
* - `status` — the exit code (the legacy `execSync`/`execFileSync` name;
* this repo's migrated catch blocks read `err.status`, e.g.
* tests/worktree-safety.test.cjs:1361, tests/read-guard.test.cjs:160,
* tests/security-scan.security.test.cjs:201).
* - `exitCode` — the same value as `status` (the seam's own name; both
* are aliases on purpose, not a rename).
* - `stdout`, `stderr` — strings.
* - `signal` — the seam's `signal` field.
* - `timedOut` — the seam's `timedOut` field.
* - `outcome` — the seam's `OUTCOME` discriminant.
*/
function gitOrThrow(args, options = {}) {
// Destructure (not spread-after) so an explicit `timeoutMs: undefined` in
// `options` still resolves to the default: a destructure default applies
// on `undefined`, whereas `{ timeoutMs: DEFAULT, ...options }` would let
// an own `undefined` key silently overwrite it and fall through to the
// seam's much larger default timeout.
const { timeoutMs = DEFAULT_GIT_TIMEOUT_MS, ...rest } = options;
const r = runGit(args, { ...rest, timeoutMs });
if (r.outcome === OUTCOME.EXITED && r.exitCode === 0) {
return r.stdout;
}
const argvDisplay = ['git', ...args].join(' ');
const err = new Error(
`gitOrThrow: \`${argvDisplay}\` failed — outcome=${r.outcome} exitCode=${r.exitCode} ` +
`stderr=${r.stderr.trim()}`
);
err.status = r.exitCode;
err.exitCode = r.exitCode;
err.stdout = r.stdout;
err.stderr = r.stderr;
err.signal = r.signal;
err.timedOut = r.timedOut;
err.outcome = r.outcome;
throw err;
}
module.exports = { gitOrThrow, DEFAULT_GIT_TIMEOUT_MS };