Files
msd-core/tests/gsd-write-guard.property.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

120 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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
}
);
});
});