Files
msd-core/tests/msd-write-guard.property.test.cjs
Jakub Zych a9a7a328e6 refactor: hard-fork GSD -> MSD (Make Software Done)
Mechanical rename produced by scripts/msd-rename.cjs: gsd/Gsd/GSD -> msd/Msd/MSD
across contents and paths, upstream package/repo coordinates -> @golem15/msd-core
and golem15com/msd-core. Deep links into upstream history, sibling upstream
packages, the GSD-2 import feature, CHANGELOG.md and .changeset/ are kept as-is.

Hand edits on top: MSD block-letter banner and logos, LICENSE copyright line,
package/plugin identity, regenerated lockfile, install-tree fixtures, derived
registries and benchmark baseline; migration checksum baseline re-locked
(MSD keeps its own install state, so no install had applied the old sums);
sort-order and regex-escaped expectations in tests adjusted.
2026-10-06 01:47:40 +02: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 msd-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', 'msd-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/msd-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('msd-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.MSD_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('msd-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
}
);
});
});