* test(#3144): bound the git/worktree cluster onto the process seam Migrates 180 unbounded sync spawn sites across 19 files. Every previously unbounded call now carries an explicit timeout with a comment giving the number and why. The migration is not a callee swap. execSync and execFileSync throw on a non-zero exit and the seam never does, so each site was classified first: sites that rely on the throw route to gitOrThrow, and sites that already read .status to detect an EXPECTED non-zero -- an intended cherry-pick conflict, a rev-parse outside a repo driving a skip -- route to the never-throwing runGit instead, which would otherwise throw on exactly the exit being probed for. Two same-named git() helpers in worktree-cleanup.test.cjs have different return contracts, one trimmed and one raw; both are preserved rather than unified. Collapses five hand-rolled throw wrappers onto one throwIfFailed in git-fixture.cjs, which gitOrThrow now also uses so the shape cannot drift. Allowlist drops 139 to 120; BASELINE lowered to match. * test(#3144): fix pre-PR review findings Documents throwIfFailed in the CONTEXT.md glossary and CONTRIBUTING.md -- it became the shared throw mechanism without either doc naming it. Routes the sixth and seventh hand-rolled copies of the throw shape through throwIfFailed (worktree-baseref-install, worktree-safety-reap); the first consolidation missed both. Converts ci-rebase-check's 8 fixture-setup calls from unchecked runGit to gitOrThrow so a failed setup step aborts where it fails rather than surfacing later as a confusing failure against the wrong subject. Adds 12 direct unit tests for throwIfFailed, which until now was only exercised transitively. Splits verify.test.cjs's non-git grep/sed bound off GIT_TIMEOUT_MS. --------- Co-authored-by: sim <sim@local>
115 lines
5.3 KiB
JavaScript
115 lines
5.3 KiB
JavaScript
'use strict';
|
|
|
|
/**
|
|
* git-fixture — the shared throw-on-failure mechanism for process-seam
|
|
* results, plus a throw-preserving wrapper over the 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.
|
|
*
|
|
* `throwIfFailed` is that mechanism: given any process-seam result and a
|
|
* human-readable name for what ran, it throws in the shape the legacy
|
|
* `execSync`/`execFileSync` idiom produced, with the seam's typed fields
|
|
* attached alongside it — or returns quietly on a clean exit. `gitOrThrow`
|
|
* is `throwIfFailed` specialized to `runGit`. Every other local test helper
|
|
* that needs the same throw-on-failure bridge (over `runNode`, `runHook`,
|
|
* etc.) calls `throwIfFailed` directly instead of hand-rolling its own copy
|
|
* of this shape — five call sites did exactly that before this module
|
|
* exported it, and drifted from each other in the process (#3144).
|
|
* `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;
|
|
|
|
/**
|
|
* Throw on anything other than a clean (exit 0) process-seam result,
|
|
* preserving the legacy `execSync`/`execFileSync` throw-on-failure idiom
|
|
* that existing test code is written against. Returns quietly (no return
|
|
* value) on a clean exit — callers that need `stdout` read it off `result`
|
|
* themselves; this only decides whether to throw.
|
|
*
|
|
* @param {object} result - a process-seam result: `{outcome, exitCode,
|
|
* stdout, stderr, timedOut, signal}` (plus any seam-specific fields,
|
|
* e.g. `code`, which are ignored here).
|
|
* @param {string} displayName - human string naming what ran, e.g.
|
|
* `'git commit -m seed'` or `'bash <quick-guard snippet>'`. Embedded in
|
|
* the thrown message so failures are attributable at a glance.
|
|
* @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 throwIfFailed(result, displayName) {
|
|
if (result.outcome === OUTCOME.EXITED && result.exitCode === 0) {
|
|
return;
|
|
}
|
|
|
|
const err = new Error(
|
|
`${displayName} failed — outcome=${result.outcome} exitCode=${result.exitCode} ` +
|
|
`stderr=${result.stderr.trim()}`
|
|
);
|
|
err.status = result.exitCode;
|
|
err.exitCode = result.exitCode;
|
|
err.stdout = result.stdout;
|
|
err.stderr = result.stderr;
|
|
err.signal = result.signal;
|
|
err.timedOut = result.timedOut;
|
|
err.outcome = result.outcome;
|
|
throw err;
|
|
}
|
|
|
|
/**
|
|
* 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} See `throwIfFailed` for the exact shape thrown.
|
|
*/
|
|
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 });
|
|
|
|
throwIfFailed(r, `gitOrThrow: \`${['git', ...args].join(' ')}\``);
|
|
return r.stdout;
|
|
}
|
|
|
|
module.exports = { gitOrThrow, throwIfFailed, DEFAULT_GIT_TIMEOUT_MS };
|