Files
msd-core/tests/lint-docs-required.test.cjs
Tom Boucher a28dcec981 chore(#597): replace count-based ratchet guards with AST lint + named-set allowlists (#603)
The windows-test-parity ratchet greps test source for fs.rmSync-without-
maxRetries (and six other Windows-portability anti-patterns), failing when an
integer offender COUNT exceeds a frozen baseline (rmSync: 95). A count ratchet
is a Goodhart metric: fixing one offender and adding another keeps the count
constant, so a new defect slips through green. Replace it — and every other
count ratchet in the repo — with a layered, masking-proof design.

Behavioral seam test
- tests/helpers-cleanup.test.cjs proves helpers.cleanup() carries the Windows
  EBUSY retry budget. cleanup() delegates retries to Node's fs.rmSync via
  maxRetries (it owns no loop), so the test asserts the option contract
  (recursive/force/maxRetries>0/retryDelay>0) + real-FS removal + the cwd-guard,
  rather than a loop that does not exist. The EBUSY risk is now tested ONCE at
  the helper, not approximated textually at every call site.

Write-time ESLint rule (AST-accurate, replaces the grep)
- eslint-rules/no-raw-rmsync-in-tests.cjs (error in tests/**/*.test.cjs) bans
  raw fs.rmSync, steering to cleanup(). Catches member, computed (fs['rmSync']),
  destructured and aliased forms; escape hatch is inline
  `// eslint-disable-next-line local/no-raw-rmsync-in-tests -- <reason>` only.
- Migrated 336 raw fs.rmSync teardown calls across ~116 test files to cleanup().
  ~18 genuinely load-bearing sites (mid-test SUT/fault-injection removals,
  error-swallowing or name-colliding local teardown helpers) keep the raw call
  with an inline eslint-disable + reason.

Shared anti-ratchet primitive
- scripts/lib/allowlist-ratchet.cjs:
  - assertWithinAllowlist: fails on NOVEL ids (new offender introduced) AND on
    STALE ids (a known offender was fixed but not pruned) — identity, not count,
    and a ratchet DOWN toward zero.
  - assertTightCeiling: a size/length budget whose ceiling must stay within a
    grace band of the high-water mark, so budgets may only tighten, never creep.

Ratchets converted onto the primitive
- windows-test-parity-guard.test.cjs: rmSync rule deleted (now ESLint-enforced);
  the remaining six patterns moved from integer baselines to named-set
  allowlists with ratchet-down.
- scripts/lint-test-file-count.{cjs,allowlist.json}: per-module integer counts →
  named filename sets (closes the swap-a-file-keep-the-count blind spot); a
  module dropping under cap now FAILS to force pruning its allowlist entry.
- enh-2790 skill-count `<= 63` → named skill allowlist (ratchets toward ~58).

Size budgets hardened (tighten-only)
- agent-size / workflow-size / feat-3039 help-tiered: ceilings lowered to the
  current high-water mark and an assertTightCeiling anti-creep check added per
  tier. Fixed external-contract limits (description ≤100 chars, agent ≤100 KB)
  are intentionally left as-is — they are not grandfathered creeping budgets.

No user-facing behavior change (tests + tooling only); no USER_FACING_PREFIXES
touched, so no changeset fragment is required.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-01 22:43:49 -04:00

376 lines
14 KiB
JavaScript

'use strict';
process.env.GSD_TEST_MODE = '1';
const { test, describe } = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const os = require('node:os');
const path = require('node:path');
const { cleanup } = require('./helpers.cjs');
const {
evaluateLint,
readFragmentsFromDisk,
LINT_REASON,
OPT_OUT_LABEL,
TRIGGERING_TYPES,
isFragmentPath,
isDocsFile,
isExemptFragment,
} = require(path.join(__dirname, '..', 'scripts', 'lint-docs-required.cjs'));
// evaluateLint is pure over the resolved inputs (changedFiles, fragments,
// labels, malformed). Tests assert on the structured verdict:
// { ok, reason: LINT_REASON.X, triggering: string[], malformed? }.
describe('docs-required lint: pure verdict (#3213)', () => {
test('LINT_REASON enum exposes the documented codes', () => {
assert.deepEqual(
Object.keys(LINT_REASON).sort(),
[
'FAIL_DOCS_MISSING',
'FAIL_MALFORMED_FRAGMENT',
'OK_DOCS_UPDATED',
'OK_FRAGMENTS_EXEMPT',
'OK_NO_TRIGGERING_FRAGMENTS',
'OK_OPT_OUT_LABEL',
].sort(),
);
});
test('TRIGGERING_TYPES covers the four user-facing non-fix types', () => {
assert.deepEqual(
[...TRIGGERING_TYPES].sort(),
['Added', 'Changed', 'Deprecated', 'Removed'].sort(),
);
});
test('OPT_OUT_LABEL is no-docs (matches CONTRIBUTING)', () => {
assert.equal(OPT_OUT_LABEL, 'no-docs');
});
test('OK_NO_TRIGGERING_FRAGMENTS when no fragments touched at all', () => {
const verdict = evaluateLint({
changedFiles: ['bin/install.js'],
fragments: [],
labels: [],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_NO_TRIGGERING_FRAGMENTS);
assert.deepEqual(verdict.triggering, []);
});
test('OK_NO_TRIGGERING_FRAGMENTS for Fixed-only fragments (bug-class)', () => {
const verdict = evaluateLint({
changedFiles: ['bin/install.js', '.changeset/silly-bears-dance.md'],
fragments: [
{ path: '.changeset/silly-bears-dance.md', type: 'Fixed', body: 'fix typo', docsExempt: null },
],
labels: [],
});
assert.deepEqual(verdict, {
ok: true,
reason: LINT_REASON.OK_NO_TRIGGERING_FRAGMENTS,
triggering: [],
});
});
test('OK_NO_TRIGGERING_FRAGMENTS for Security-only fragments', () => {
const verdict = evaluateLint({
changedFiles: [],
fragments: [{ path: '.changeset/a.md', type: 'Security', body: 'cve', docsExempt: null }],
labels: [],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_NO_TRIGGERING_FRAGMENTS);
});
test('OK_DOCS_UPDATED when Added fragment ships alongside a docs/ change', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'docs/COMMANDS.md'],
fragments: [{ path: '.changeset/a.md', type: 'Added', body: 'new cmd', docsExempt: null }],
labels: [],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_DOCS_UPDATED);
assert.deepEqual(verdict.triggering, ['.changeset/a.md']);
});
test('OK_DOCS_UPDATED for nested docs/ paths (docs/adr/, docs/agents/)', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'docs/adr/0099-new.md'],
fragments: [{ path: '.changeset/a.md', type: 'Changed', body: '...', docsExempt: null }],
labels: [],
});
assert.equal(verdict.reason, LINT_REASON.OK_DOCS_UPDATED);
});
for (const type of ['Added', 'Changed', 'Deprecated', 'Removed']) {
test(`FAIL_DOCS_MISSING when ${type} fragment has no docs/ change and no escape hatch`, () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'bin/install.js'],
fragments: [{ path: '.changeset/a.md', type, body: '...', docsExempt: null }],
labels: [],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_DOCS_MISSING);
assert.deepEqual(verdict.triggering, ['.changeset/a.md']);
});
}
test('OK_OPT_OUT_LABEL when no-docs label present overrides triggering fragments', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'bin/install.js'],
fragments: [{ path: '.changeset/a.md', type: 'Added', body: '...', docsExempt: null }],
labels: ['no-docs'],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_OPT_OUT_LABEL);
});
test('per-fragment docsExempt reason exempts that fragment', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'bin/install.js'],
fragments: [
{ path: '.changeset/a.md', type: 'Added', body: 'foo', docsExempt: 'internal-only' },
],
labels: [],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_FRAGMENTS_EXEMPT);
assert.deepEqual(verdict.triggering, ['.changeset/a.md']);
});
test('docsExempt empty string does NOT exempt — defense-in-depth (CodeRabbit finding)', () => {
// parse.cjs no longer produces empty-string docsExempt (the marker regex
// requires a non-empty reason). evaluateLint defends against any caller
// that constructs a fragment with `docsExempt: ''` directly — empty or
// whitespace-only reasons are not a valid audit trail.
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'bin/install.js'],
fragments: [
{ path: '.changeset/a.md', type: 'Added', body: 'foo', docsExempt: '' },
],
labels: [],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_DOCS_MISSING);
});
test('docsExempt whitespace-only does NOT exempt — defense-in-depth', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', 'bin/install.js'],
fragments: [
{ path: '.changeset/a.md', type: 'Added', body: 'foo', docsExempt: ' \t' },
],
labels: [],
});
assert.equal(verdict.reason, LINT_REASON.FAIL_DOCS_MISSING);
});
test('partial exemption fails — one un-marked triggering fragment is enough to require docs', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', '.changeset/b.md', 'bin/install.js'],
fragments: [
{ path: '.changeset/a.md', type: 'Added', body: 'foo', docsExempt: 'x' },
{ path: '.changeset/b.md', type: 'Changed', body: 'no marker here', docsExempt: null },
],
labels: [],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_DOCS_MISSING);
assert.deepEqual(verdict.triggering.sort(), ['.changeset/a.md', '.changeset/b.md']);
});
test('mixed Fixed + Added with no docs still fails — Added triggers', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/a.md', '.changeset/b.md'],
fragments: [
{ path: '.changeset/a.md', type: 'Fixed', body: '...', docsExempt: null },
{ path: '.changeset/b.md', type: 'Added', body: '...', docsExempt: null },
],
labels: [],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_DOCS_MISSING);
assert.deepEqual(verdict.triggering, ['.changeset/b.md']);
});
});
describe('docs-required lint: malformed fragments fail closed (#3213, Codex finding)', () => {
test('FAIL_MALFORMED_FRAGMENT when a touched fragment failed to parse', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/bad.md'],
fragments: [],
labels: [],
malformed: [{ path: '.changeset/bad.md', reason: 'missing_frontmatter' }],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_MALFORMED_FRAGMENT);
assert.deepEqual(verdict.malformed, [{ path: '.changeset/bad.md', reason: 'missing_frontmatter' }]);
});
test('FAIL_MALFORMED_FRAGMENT outranks OK_DOCS_UPDATED — malformed must be fixed first', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/bad.md', '.changeset/ok.md', 'docs/USER-GUIDE.md'],
fragments: [{ path: '.changeset/ok.md', type: 'Added', body: 'fine', docsExempt: null }],
labels: ['no-docs'],
malformed: [{ path: '.changeset/bad.md', reason: 'invalid_type', detail: 'Bogus' }],
});
assert.equal(verdict.ok, false);
assert.equal(verdict.reason, LINT_REASON.FAIL_MALFORMED_FRAGMENT);
});
test('no-docs label cannot bypass FAIL_MALFORMED_FRAGMENT', () => {
const verdict = evaluateLint({
changedFiles: ['.changeset/bad.md'],
fragments: [],
labels: ['no-docs'],
malformed: [{ path: '.changeset/bad.md', reason: 'missing_pr' }],
});
assert.equal(verdict.reason, LINT_REASON.FAIL_MALFORMED_FRAGMENT);
});
test('malformed defaults to [] when omitted — back-compat with simple test inputs', () => {
const verdict = evaluateLint({
changedFiles: [],
fragments: [],
labels: [],
});
assert.equal(verdict.ok, true);
assert.equal(verdict.reason, LINT_REASON.OK_NO_TRIGGERING_FRAGMENTS);
});
});
describe('docs-required lint: helpers', () => {
test('isFragmentPath accepts .changeset/<slug>.md, rejects README', () => {
assert.equal(isFragmentPath('.changeset/foo.md'), true);
assert.equal(isFragmentPath('.changeset/silly-bears-dance.md'), true);
assert.equal(isFragmentPath('.changeset/README.md'), false);
assert.equal(isFragmentPath('.changeset/nested/foo.md'), false);
assert.equal(isFragmentPath('docs/COMMANDS.md'), false);
assert.equal(isFragmentPath('bin/install.js'), false);
});
test('isDocsFile matches docs/ prefix only', () => {
assert.equal(isDocsFile('docs/COMMANDS.md'), true);
assert.equal(isDocsFile('docs/adr/0001-foo.md'), true);
assert.equal(isDocsFile('docs/agents/triage-labels.md'), true);
assert.equal(isDocsFile('docs'), false); // exact 'docs' without slash is not a file under docs/
assert.equal(isDocsFile('CONTRIBUTING.md'), false);
assert.equal(isDocsFile('README.md'), false);
});
test('isExemptFragment checks docsExempt is a non-empty string, not body content', () => {
assert.equal(isExemptFragment({ docsExempt: 'reason' }), true);
assert.equal(isExemptFragment({ docsExempt: 'a' }), true);
// Empty/whitespace-only reason → no audit trail → not exempt.
assert.equal(isExemptFragment({ docsExempt: '' }), false);
assert.equal(isExemptFragment({ docsExempt: ' \t' }), false);
assert.equal(isExemptFragment({ docsExempt: null }), false);
assert.equal(isExemptFragment({ docsExempt: undefined }), false);
assert.equal(isExemptFragment({}), false);
// Body content is irrelevant — parse.cjs extracts the marker into docsExempt.
assert.equal(
isExemptFragment({ body: '<!-- docs-exempt: x -->', docsExempt: null }),
false,
);
});
});
describe('docs-required lint: readFragmentsFromDisk', () => {
function withTempRepo(fn) {
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'gsd-docs-lint-'));
try {
fs.mkdirSync(path.join(tmp, '.changeset'), { recursive: true });
fn(tmp);
} finally {
cleanup(tmp);
}
}
test('returns { fragments, malformed } shape', () => {
withTempRepo((tmp) => {
const out = readFragmentsFromDisk([], tmp);
assert.ok('fragments' in out, 'has fragments');
assert.ok('malformed' in out, 'has malformed');
assert.deepEqual(out.fragments, []);
assert.deepEqual(out.malformed, []);
});
});
test('parses valid fragments and skips non-fragment paths', () => {
withTempRepo((tmp) => {
fs.writeFileSync(
path.join(tmp, '.changeset', 'a.md'),
'---\ntype: Added\npr: 1\n---\nnew feature\n',
);
fs.writeFileSync(
path.join(tmp, '.changeset', 'b.md'),
'---\ntype: Fixed\npr: 2\n---\nbug fix\n',
);
const { fragments, malformed } = readFragmentsFromDisk(
['.changeset/a.md', '.changeset/b.md', 'bin/x.js'],
tmp,
);
assert.equal(fragments.length, 2);
assert.equal(fragments[0].path, '.changeset/a.md');
assert.equal(fragments[0].type, 'Added');
assert.equal(fragments[0].docsExempt, null);
assert.equal(fragments[1].type, 'Fixed');
assert.deepEqual(malformed, []);
});
});
test('skips deleted fragments (path in diff but file gone)', () => {
withTempRepo((tmp) => {
const { fragments, malformed } = readFragmentsFromDisk(['.changeset/deleted.md'], tmp);
assert.deepEqual(fragments, []);
assert.deepEqual(malformed, []);
});
});
test('routes malformed fragments to the malformed list with typed reason', () => {
withTempRepo((tmp) => {
fs.writeFileSync(path.join(tmp, '.changeset', 'bad.md'), 'no frontmatter here\n');
const { fragments, malformed } = readFragmentsFromDisk(['.changeset/bad.md'], tmp);
assert.deepEqual(fragments, []);
assert.equal(malformed.length, 1);
assert.equal(malformed[0].path, '.changeset/bad.md');
assert.equal(malformed[0].reason, 'missing_frontmatter');
});
});
test('Added fragment with bad pr surfaces as malformed (Codex finding regression test)', () => {
withTempRepo((tmp) => {
fs.writeFileSync(
path.join(tmp, '.changeset', 'a.md'),
'---\ntype: Added\n---\nbody but no pr field\n',
);
const { fragments, malformed } = readFragmentsFromDisk(['.changeset/a.md'], tmp);
assert.deepEqual(fragments, []);
assert.equal(malformed.length, 1);
assert.equal(malformed[0].reason, 'missing_pr');
// End-to-end: feed straight into evaluateLint and confirm fail-closed.
const verdict = evaluateLint({ changedFiles: ['.changeset/a.md'], fragments, malformed, labels: [] });
assert.equal(verdict.reason, LINT_REASON.FAIL_MALFORMED_FRAGMENT);
});
});
test('extracts docs-exempt marker into typed field and strips it from body', () => {
withTempRepo((tmp) => {
fs.writeFileSync(
path.join(tmp, '.changeset', 'a.md'),
'---\ntype: Added\npr: 3\n---\nnew thing\n\n<!-- docs-exempt: internal-only -->\n',
);
const { fragments } = readFragmentsFromDisk(['.changeset/a.md'], tmp);
assert.equal(fragments.length, 1);
assert.equal(fragments[0].docsExempt, 'internal-only');
// The marker no longer appears in the rendered body — renderers append
// `(#NNNN)` to body's last line, so the marker would otherwise leak into
// CHANGELOG.md / GitHub release notes.
assert.doesNotMatch(fragments[0].body, /docs-exempt/);
});
});
});