Files
msd-core/tests/no-unbounded-spawn-allowlist.test.cjs
Tom Boucher 1d208e5af6 test(#3144): bound the git/worktree cluster onto the process seam (#3152)
* 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>
2026-08-07 10:58:34 -04:00

168 lines
6.5 KiB
JavaScript

// allow-test-rule: structural-regression-guard [#3143]
/**
* no-unbounded-spawn-allowlist.test.cjs
*
* Structural guards on `eslint-rules/no-unbounded-spawn.allowlist.json` —
* matrix D4-D8 of
* .gsd/phase/chore-3143-no-unbounded-spawn-guard/50-test-matrix.md.
*
* D7 needs to inspect test-file *contents* for an inline directive that
* disables this rule by name — the absence of that pattern is the contract
* this guard protects (a contributor cannot silence the check by disabling
* it inline instead of fixing the timeout). That is a `readFileSync` +
* text-search on `.cjs` files, which is exactly what `local/no-source-grep`
* exists to catch — hence the
* `// allow-test-rule: structural-regression-guard [#3143]` annotation on
* its own line above, per CONTRIBUTING.md's documented exemption.
*/
'use strict';
const { describe, test } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('fs');
const path = require('path');
const ALLOWLIST_PATH = path.join(__dirname, '..', 'eslint-rules', 'no-unbounded-spawn.allowlist.json');
const TESTS_DIR = path.join(__dirname);
const REPO_ROOT = path.join(__dirname, '..');
// The allowlist only ratchets DOWN. This baseline is the length observed at
// the time this guard was written (139 entries) — each future migration
// wave lowers it as files are moved off the allowlist by adding real
// timeouts; it must never grow back up. Lowered to 120 by the #3144 Wave-1
// process-seam migration (19 files' unbounded spawns bounded).
const BASELINE = 120;
function readAllowlist() {
const raw = fs.readFileSync(ALLOWLIST_PATH, 'utf8');
return JSON.parse(raw);
}
function listTestFiles() {
const out = [];
for (const entry of fs.readdirSync(TESTS_DIR, { withFileTypes: true })) {
if (entry.isFile() && entry.name.endsWith('.cjs')) {
out.push(path.join(TESTS_DIR, entry.name));
}
}
return out;
}
// Pure detection helpers, extracted so each is unit-testable against a
// synthetic fixture — proving the check itself can fail, not only that
// today's real data happens to pass it (a check that never runs against an
// injected violation is a vacuous-truth risk).
function findDeadEntries(list, root) {
return list.filter((entry) => !fs.existsSync(path.join(root, entry)));
}
function findBackslashEntries(list) {
return list.filter((entry) => entry.includes('\\'));
}
function isSorted(list) {
return JSON.stringify(list) === JSON.stringify([...list].sort());
}
function findDuplicates(list) {
const seen = new Set();
const dupes = new Set();
for (const entry of list) {
if (seen.has(entry)) dupes.add(entry);
seen.add(entry);
}
return [...dupes];
}
// Built via concatenation, not a string literal, so this file does not
// itself contain the literal directive text (`local/no-unbounded-spawn`)
// that D7 below scans every test file for — a literal here would make this
// guard flag itself.
const GUARDED_RULE = 'local' + '/' + 'no-unbounded-spawn';
function containsDisableDirective(contents, ruleName) {
return new RegExp(`eslint-disable[^\\n]*${ruleName}`).test(contents);
}
describe('no-unbounded-spawn allowlist: D4 — no dead entries', () => {
test('every allowlist entry resolves to a file that exists on disk', () => {
const list = readAllowlist();
const dead = findDeadEntries(list, REPO_ROOT);
assert.deepEqual(dead, [], `dead allowlist entries (file does not exist): ${JSON.stringify(dead)}`);
});
test('detection logic actually flags a synthetic dead entry', () => {
const synthetic = ['tests/does-not-exist-xyz.test.cjs', 'tests/no-unbounded-spawn.test.cjs'];
const dead = findDeadEntries(synthetic, REPO_ROOT);
assert.deepEqual(dead, ['tests/does-not-exist-xyz.test.cjs']);
});
});
describe('no-unbounded-spawn allowlist: D5 — never grows', () => {
test('allowlist length is at or under the recorded baseline', () => {
const list = readAllowlist();
assert.ok(
list.length <= BASELINE,
`allowlist grew to ${list.length} entries, exceeding the BASELINE of ${BASELINE}. ` +
`The allowlist only ratchets down — if this is a legitimate new violation, ` +
`lower BASELINE only after confirming it, never raise it to paper over growth.`
);
});
});
describe('no-unbounded-spawn allowlist: D6 — separator normalization', () => {
test('every entry uses / separators, never \\', () => {
const list = readAllowlist();
const withBackslash = findBackslashEntries(list);
assert.deepEqual(withBackslash, [], `entries with a backslash separator: ${JSON.stringify(withBackslash)}`);
});
test('detection logic actually flags a synthetic backslash entry', () => {
const synthetic = ['tests\\foo.test.cjs', 'tests/bar.test.cjs'];
assert.deepEqual(findBackslashEntries(synthetic), ['tests\\foo.test.cjs']);
});
});
describe('no-unbounded-spawn allowlist: D7 — no inline disable of this rule', () => {
test('no test file inline-disables the unbounded-spawn guard', () => {
const offenders = [];
for (const filePath of listTestFiles()) {
const contents = fs.readFileSync(filePath, 'utf8');
if (containsDisableDirective(contents, GUARDED_RULE)) {
offenders.push(path.relative(REPO_ROOT, filePath));
}
}
assert.deepEqual(
offenders,
[],
`test files inline-disabling the unbounded-spawn guard (forbidden — fix the timeout instead): ${JSON.stringify(offenders)}`
);
});
test('detection logic actually flags a synthetic inline-disable directive', () => {
const syntheticContents = [
"'use strict';",
'// eslint-disable-next-line ' + GUARDED_RULE,
"spawnSync('git', ['status'], {});",
].join('\n');
assert.equal(containsDisableDirective(syntheticContents, GUARDED_RULE), true);
assert.equal(containsDisableDirective("'use strict';\nspawnSync('git', ['status'], {});", GUARDED_RULE), false);
});
});
describe('no-unbounded-spawn allowlist: D8 — canonical form', () => {
test('allowlist is valid JSON, sorted, with no duplicates', () => {
const list = readAllowlist();
assert.ok(Array.isArray(list), 'allowlist.json must parse to an array');
assert.ok(isSorted(list), 'allowlist entries must be sorted');
assert.deepEqual(findDuplicates(list), [], 'allowlist entries must be unique');
});
test('detection logic actually flags a synthetic unsorted/duplicate list', () => {
assert.equal(isSorted(['b.test.cjs', 'a.test.cjs']), false);
assert.deepEqual(findDuplicates(['a.test.cjs', 'b.test.cjs', 'a.test.cjs']), ['a.test.cjs']);
});
});