* 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>
88 lines
3.8 KiB
JavaScript
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 };
|