* 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>
120 lines
4.2 KiB
JavaScript
120 lines
4.2 KiB
JavaScript
'use strict';
|
||
|
||
/**
|
||
* Property-based test for the gsd-write-guard SHRINK_RATIO/FLOOR_LINES budget
|
||
* contract (#2255 review, Major 5 — CLAUDE.md requires a fast-check property
|
||
* test for every budget-limit contract).
|
||
*
|
||
* Property: for any on-disk file with oldLines > FLOOR_LINES-1 (i.e. at or
|
||
* above the exclusive floor), a pending Write of newLines is
|
||
* blocked ⟺ newLines < oldLines * SHRINK_RATIO
|
||
* and for any oldLines < FLOOR_LINES the Write always passes, regardless of
|
||
* how far it shrinks.
|
||
*
|
||
* The hook is a standalone stdin-driven script, so each sample spawns it at
|
||
* the real seam (same as the unit suite). Sample counts are bounded below the
|
||
* global numRuns to keep the spawn cost sane; the boundary cases the property
|
||
* must not miss (floor-1/floor/floor+1, ratio-1/ratio/ratio+1) are pinned as
|
||
* explicit examples.
|
||
*/
|
||
|
||
const { describe, test, before, after } = require('node:test');
|
||
const assert = require('node:assert/strict');
|
||
const fs = require('node:fs');
|
||
const path = require('node:path');
|
||
const fc = require('./helpers/fast-check-setup.cjs');
|
||
const { createTempDir, cleanup } = require('./helpers.cjs');
|
||
const { runHook } = require('./helpers/process-seam.cjs');
|
||
|
||
const HOOK_PATH = path.join(__dirname, '..', 'hooks', 'gsd-write-guard.js');
|
||
|
||
// 5000ms: this hook does a handful of sync fs reads/stat calls on a small
|
||
// fixture file and exits — a hang here would stall every fast-check sample
|
||
// (40+ per run), so the bound is kept low rather than reused from a slower
|
||
// class of call.
|
||
const HOOK_TIMEOUT_MS = 5000;
|
||
|
||
// Mirror the hook's published contract (hooks/gsd-write-guard.js).
|
||
const SHRINK_RATIO = 0.4;
|
||
const FLOOR_LINES = 40;
|
||
|
||
function lines(n) {
|
||
return Array.from({ length: n }, (_, i) => `line ${i + 1}`).join('\n') + '\n';
|
||
}
|
||
|
||
let projectDir;
|
||
let roadmapPath;
|
||
|
||
before(() => {
|
||
projectDir = createTempDir('gsd-write-guard-prop-');
|
||
fs.mkdirSync(path.join(projectDir, '.planning'), { recursive: true });
|
||
roadmapPath = path.join(projectDir, '.planning', 'ROADMAP.md');
|
||
});
|
||
|
||
after(() => {
|
||
cleanup(projectDir);
|
||
});
|
||
|
||
function guardVerdict(oldLines, newLines) {
|
||
fs.writeFileSync(roadmapPath, lines(oldLines));
|
||
const env = { ...process.env };
|
||
delete env.GSD_ALLOW_PLANNING_SHRINK;
|
||
const r = runHook(HOOK_PATH, [], {
|
||
input: JSON.stringify({
|
||
hook_event_name: 'PreToolUse',
|
||
tool_name: 'Write',
|
||
tool_input: { file_path: roadmapPath, content: lines(newLines) },
|
||
}),
|
||
env,
|
||
timeoutMs: HOOK_TIMEOUT_MS,
|
||
});
|
||
return r.exitCode === 2 ? 'blocked' : 'passed';
|
||
}
|
||
|
||
describe('gsd-write-guard.js: SHRINK_RATIO/FLOOR_LINES budget contract (property)', () => {
|
||
|
||
test('for any oldLines ≥ FLOOR_LINES: blocked ⟺ newLines < oldLines * SHRINK_RATIO', () => {
|
||
fc.assert(
|
||
fc.property(
|
||
fc.integer({ min: FLOOR_LINES, max: 400 }),
|
||
fc.integer({ min: 1, max: 400 }),
|
||
(oldLines, newLines) => {
|
||
const expected = newLines < oldLines * SHRINK_RATIO ? 'blocked' : 'passed';
|
||
assert.equal(
|
||
guardVerdict(oldLines, newLines), expected,
|
||
`oldLines=${oldLines} newLines=${newLines} ratio=${newLines / oldLines}`
|
||
);
|
||
}
|
||
),
|
||
{
|
||
numRuns: 40, // each sample spawns the hook process — bound the cost
|
||
examples: [
|
||
[FLOOR_LINES, Math.ceil(FLOOR_LINES * SHRINK_RATIO) - 1], // floor × just-under-ratio
|
||
[FLOOR_LINES, Math.ceil(FLOOR_LINES * SHRINK_RATIO)], // floor × at-ratio
|
||
[FLOOR_LINES + 1, 1], // floor+1 × deep shrink
|
||
[100, 39], [100, 40], [100, 41], // ratio-1 / ratio / ratio+1
|
||
],
|
||
}
|
||
);
|
||
});
|
||
|
||
test('for any oldLines < FLOOR_LINES: never blocked, however deep the shrink', () => {
|
||
fc.assert(
|
||
fc.property(
|
||
fc.integer({ min: 1, max: FLOOR_LINES - 1 }),
|
||
fc.integer({ min: 1, max: 400 }),
|
||
(oldLines, newLines) => {
|
||
assert.equal(
|
||
guardVerdict(oldLines, newLines), 'passed',
|
||
`sub-floor oldLines=${oldLines} newLines=${newLines} must be exempt`
|
||
);
|
||
}
|
||
),
|
||
{
|
||
numRuns: 20,
|
||
examples: [[FLOOR_LINES - 1, 1]], // floor-1 × deepest shrink
|
||
}
|
||
);
|
||
});
|
||
});
|