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.
138 lines
4.4 KiB
JavaScript
138 lines
4.4 KiB
JavaScript
#!/usr/bin/env node
|
|
'use strict';
|
|
|
|
/**
|
|
* lint-fix-has-regression-tests.cjs — gate: every fix(#NNNN) commit must
|
|
* include at least one behavioral test file (tests/*.test.cjs) that is NOT
|
|
* an auto-generated fixture/baseline.
|
|
*
|
|
* Renamed (was `lint-fix-has-regression-test.cjs`, singular): the old
|
|
* basename matched Node's `*-test.EXT` test-collection pattern, so the
|
|
* remote push-gate runner collected and executed this SOURCE file as a
|
|
* test while GitHub CI (which globs only tests/**\/*.test.cjs) never saw
|
|
* it — see scripts/lint-source-test-name-collision.cjs.
|
|
*
|
|
* ## Why
|
|
*
|
|
* CONTRIBUTING.md:47: "Fix it. Write a test that would have caught the bug."
|
|
* CLAUDE.md: "Regression Protocol: Write the regression test first."
|
|
*
|
|
* Three merged PRs shipped without regression tests because the golden-fixture
|
|
* regeneration (20+ files under tests/) camouflaged the absence of real
|
|
* *.test.cjs changes. This gate makes the rule machine-enforced.
|
|
*
|
|
* ## What this enforces
|
|
*
|
|
* For every commit in `git log origin/next..HEAD` whose subject matches
|
|
* /^fix\(#\d+\)/ or /^feat\(#\d+\)/, the cumulative diff must include at
|
|
* least one file under tests/ matching /\.test\.cjs$/ that is NOT:
|
|
* - under tests/fixtures/
|
|
* - under tests/install-tree/
|
|
* - a *-baseline.json file
|
|
*
|
|
* If the filtered list is empty, the gate fails with a message naming the
|
|
* fix commits and directing the author to add a regression test.
|
|
*
|
|
* ## Overrides
|
|
*
|
|
* Set MSD_SKIP_REGRESSION_TEST_GATE=1 for legitimate exceptions (e.g. a
|
|
* pure-config or pure-refactor PR where no behavioral test is possible).
|
|
* This env var is auditable in CI logs.
|
|
*/
|
|
|
|
const { execSync } = require('child_process');
|
|
const { ExitError, runMain } = require('./lib/cli-exit.cjs');
|
|
|
|
const FIX_OR_FEAT_RE = /^(?:fix|feat)\(#(\d+)\)/;
|
|
|
|
const EXCLUDE_PATTERNS = [
|
|
/tests[\\/]fixtures[\\/]/,
|
|
/tests[\\/]install-tree[\\/]/,
|
|
/-baseline\.json$/,
|
|
];
|
|
|
|
function isRealTestFile(filePath) {
|
|
if (!filePath.includes('tests/')) return false;
|
|
if (!/\.test\.cjs$/.test(filePath)) return false;
|
|
return !EXCLUDE_PATTERNS.some((re) => re.test(filePath));
|
|
}
|
|
|
|
function getFixCommits(baseRef) {
|
|
const log = execSync(
|
|
`git log ${baseRef}..HEAD --format='%H%x09%s' --no-merges`,
|
|
{ encoding: 'utf8', timeout: 10000 },
|
|
);
|
|
return log
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean)
|
|
.map((line) => {
|
|
const [sha, subject] = line.split('\t');
|
|
const match = subject.match(FIX_OR_FEAT_RE);
|
|
return match ? { sha: sha.slice(0, 12), subject, issue: match[1] } : null;
|
|
})
|
|
.filter(Boolean);
|
|
}
|
|
|
|
function getChangedTestFiles(baseRef) {
|
|
const diff = execSync(
|
|
`git diff ${baseRef}...HEAD --name-only --no-merges`,
|
|
{ encoding: 'utf8', timeout: 10000 },
|
|
);
|
|
return diff
|
|
.trim()
|
|
.split('\n')
|
|
.filter(Boolean)
|
|
.filter(isRealTestFile);
|
|
}
|
|
|
|
function main() {
|
|
if (process.env.MSD_SKIP_REGRESSION_TEST_GATE === '1') {
|
|
console.log('lint-fix-has-regression-tests: SKIPPED (MSD_SKIP_REGRESSION_TEST_GATE=1)');
|
|
return;
|
|
}
|
|
|
|
const baseRef = process.env.MSD_REGRESSION_GATE_BASE || 'origin/next';
|
|
|
|
let fixCommits;
|
|
try {
|
|
fixCommits = getFixCommits(baseRef);
|
|
} catch {
|
|
console.log(`lint-fix-has-regression-tests: no fix/feat commits found vs ${baseRef}, skipping`);
|
|
return;
|
|
}
|
|
|
|
if (fixCommits.length === 0) {
|
|
console.log('lint-fix-has-regression-tests: no fix/feat commits, passing');
|
|
return;
|
|
}
|
|
|
|
let testFiles;
|
|
try {
|
|
testFiles = getChangedTestFiles(baseRef);
|
|
} catch {
|
|
testFiles = [];
|
|
}
|
|
|
|
if (testFiles.length === 0) {
|
|
const commitList = fixCommits
|
|
.map((c) => ` ${c.sha} ${c.subject}`)
|
|
.join('\n');
|
|
throw new ExitError(1,
|
|
`lint-fix-has-regression-tests: ${fixCommits.length} fix/feat commit(s) but ZERO behavioral test files (*.test.cjs) in the diff.\n` +
|
|
`Auto-generated fixtures (tests/fixtures/, *-baseline.json) do NOT count.\n\n` +
|
|
`Fix commits:\n${commitList}\n\n` +
|
|
`CONTRIBUTING.md:47: "Write a test that would have caught the bug."\n` +
|
|
`Add a regression test to an existing tests/*.test.cjs file, or set ` +
|
|
`MSD_SKIP_REGRESSION_TEST_GATE=1 if no behavioral test is possible (auditable in CI).`
|
|
);
|
|
}
|
|
|
|
console.log(
|
|
`lint-fix-has-regression-tests: PASS — ${fixCommits.length} fix/feat commit(s), ` +
|
|
`${testFiles.length} behavioral test file(s): ${testFiles.join(', ')}`
|
|
);
|
|
}
|
|
|
|
runMain(main);
|