Two comments in build-rollup-register.{mjs,test.mjs} used a since-deleted
repository as the example of a nested same-name git repo. Replaced with the
invented `example-repo/example-repo`; the test fixture (`outer/outer`) and
what it proves are unchanged.
Adds scripts/check-retired-refs.mjs (+ test): fails when a tracked text
file names a retired term in its content or path. The term list is
local-only (scripts/retired-terms.local.md, ignored via *.local.md) and
never tracked; a missing or empty list reports NOT CHECKED (exit 2), and
the repo-level test skips with that reason instead of passing silently.
Named exemption list, empty. Chosen *.local.md because the existing ignore
rule already covers it, so .gitignore is untouched.
Red on 69fb250 (112 files, 2 hits, exit 1) -> green (114 files, 0 hits).
Suite 217 -> 227 tests, 211 -> 221 pass; the same 6 check-okf-parity
failures before and after, caused by the worktree path breaking its
sibling-repo lookup (9/9 green in the main checkout). check-versions:
12/12 OK, 0 ERROR.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
146 lines
5.9 KiB
JavaScript
146 lines
5.9 KiB
JavaScript
// Tests for the retired-reference gate. The unit tests use synthetic terms; only the last
|
|
// test reads the real, local-only term list, and it SKIPS loudly when that file is absent.
|
|
import { test } from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { execFileSync, spawnSync } from 'node:child_process';
|
|
import { mkdtempSync, mkdirSync, writeFileSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import {
|
|
DEFAULT_TERMS_FILE,
|
|
EXEMPT_PATHS,
|
|
loadTerms,
|
|
buildPattern,
|
|
scanEntries,
|
|
runCheck,
|
|
} from './check-retired-refs.mjs';
|
|
|
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
const REPO = join(HERE, '..');
|
|
const CLI = join(HERE, 'check-retired-refs.mjs');
|
|
const TERMS = ['retired-alpha', 'Beta.Tool', 'gamma'];
|
|
const PATTERN = buildPattern(TERMS);
|
|
|
|
function tmpDir(prefix) {
|
|
return mkdtempSync(join(tmpdir(), prefix));
|
|
}
|
|
|
|
function termsFile(lines) {
|
|
const file = join(tmpDir('retired-terms-'), 'terms.local.md');
|
|
writeFileSync(file, lines.join('\n'));
|
|
return file;
|
|
}
|
|
|
|
test('loadTerms: one term per line, blanks and # lines ignored, missing file -> null', () => {
|
|
const file = termsFile(['# comment', '', ' retired-alpha ', 'gamma', '']);
|
|
assert.deepEqual(loadTerms(file), ['retired-alpha', 'gamma']);
|
|
assert.equal(loadTerms(join(tmpDir('retired-none-'), 'absent.md')), null);
|
|
});
|
|
|
|
test('buildPattern: case-insensitive plain substrings, regex metacharacters escaped', () => {
|
|
assert.match('uses RETIRED-ALPHA here', PATTERN);
|
|
assert.match('beta.tool', PATTERN);
|
|
assert.doesNotMatch('betaXtool', PATTERN, '"." is literal, not a wildcard');
|
|
assert.doesNotMatch('repo-mailbox, check-versions', PATTERN);
|
|
assert.throws(() => buildPattern([]), /empty term list/);
|
|
});
|
|
|
|
test('scanEntries reports content hits with line numbers and path hits', () => {
|
|
const findings = scanEntries(
|
|
[
|
|
{ path: 'clean.md', content: 'nothing here\n' },
|
|
{ path: 'a.md', content: 'line one\nuses retired-alpha/retired-alpha\n' },
|
|
{ path: 'docs/gamma.md', content: 'clean body\n' },
|
|
],
|
|
{ pattern: PATTERN },
|
|
);
|
|
assert.deepEqual(
|
|
findings.map((f) => [f.path, f.kind, f.line]),
|
|
[
|
|
['a.md', 'content', 2],
|
|
['docs/gamma.md', 'path', 0],
|
|
],
|
|
);
|
|
});
|
|
|
|
test('scanEntries skips binary content but still checks its path', () => {
|
|
const findings = scanEntries(
|
|
[
|
|
{ path: 'img.png', content: null },
|
|
{ path: 'gamma.bin', content: null },
|
|
],
|
|
{ pattern: PATTERN },
|
|
);
|
|
assert.deepEqual(findings.map((f) => f.path), ['gamma.bin']);
|
|
});
|
|
|
|
test('scanEntries honours the named exemption list', () => {
|
|
const entries = [{ path: 'keep.md', content: 'gamma' }];
|
|
assert.equal(scanEntries(entries, { pattern: PATTERN }).length, 1);
|
|
assert.equal(scanEntries(entries, { pattern: PATTERN, exempt: ['keep.md'] }).length, 0);
|
|
assert.deepEqual(EXEMPT_PATHS, [], 'the shipped exemption list is empty');
|
|
});
|
|
|
|
function tmpRepo(files) {
|
|
const root = tmpDir('retired-refs-');
|
|
execFileSync('git', ['init', '-q', root]);
|
|
for (const [path, body] of Object.entries(files)) {
|
|
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
writeFileSync(join(root, path), body);
|
|
}
|
|
execFileSync('git', ['-C', root, 'add', '-A']);
|
|
return root;
|
|
}
|
|
|
|
test('runCheck reads the git index: a staged hit is found, an untracked one is not', () => {
|
|
const root = tmpRepo({ 'README.md': 'points at gamma\n' });
|
|
writeFileSync(join(root, 'untracked.md'), 'retired-alpha');
|
|
const { checked, findings } = runCheck(root, TERMS);
|
|
assert.equal(checked, 1);
|
|
assert.deepEqual(findings.map((f) => `${f.path}:${f.line}`), ['README.md:1']);
|
|
});
|
|
|
|
test('CLI exits 1 on a hit and 0 on a clean tree, always printing the denominator', () => {
|
|
const terms = termsFile(TERMS);
|
|
const run = (root) => spawnSync(process.execPath, [CLI, root, '--terms', terms], { encoding: 'utf8' });
|
|
const dirty = run(tmpRepo({ 'x.md': 'retired-alpha' }));
|
|
assert.equal(dirty.status, 1);
|
|
assert.match(dirty.stdout, /\[ERROR\] retired reference — x\.md:1/);
|
|
assert.match(dirty.stdout, /checked 1 tracked files against 3 terms — 1 hit/);
|
|
const clean = run(tmpRepo({ 'x.md': 'clean\n' }));
|
|
assert.equal(clean.status, 0);
|
|
assert.match(clean.stdout, /checked 1 tracked files against 3 terms — 0 hit/);
|
|
});
|
|
|
|
test('CLI fails on an empty index: 0 files checked verified nothing', () => {
|
|
const empty = tmpDir('retired-refs-empty-');
|
|
execFileSync('git', ['init', '-q', empty]);
|
|
const r = spawnSync(process.execPath, [CLI, empty, '--terms', termsFile(TERMS)], { encoding: 'utf8' });
|
|
assert.equal(r.status, 1);
|
|
assert.match(r.stdout, /verified nothing/);
|
|
});
|
|
|
|
test('CLI reports NOT CHECKED (exit 2) for a missing or empty term list, never a pass', () => {
|
|
const root = tmpRepo({ 'x.md': 'retired-alpha' });
|
|
const missing = spawnSync(process.execPath, [CLI, root, '--terms', join(root, 'absent.md')], { encoding: 'utf8' });
|
|
assert.equal(missing.status, 2);
|
|
assert.match(missing.stdout, /NOT CHECKED — term list missing/);
|
|
const empty = spawnSync(process.execPath, [CLI, root, '--terms', termsFile(['# only a comment'])], { encoding: 'utf8' });
|
|
assert.equal(empty.status, 2);
|
|
assert.match(empty.stdout, /NOT CHECKED — term list empty/);
|
|
});
|
|
|
|
// The FERDIG criterion: this repository's own tracked files carry zero retired references.
|
|
// The real term list is local-only by design; without it this test cannot measure anything,
|
|
// so it skips with the reason instead of passing silently.
|
|
test('this repository: 0 retired references across its tracked files', (t) => {
|
|
const terms = loadTerms();
|
|
if (!terms || terms.length === 0) {
|
|
t.skip(`NOT CHECKED — local term list missing or empty: ${DEFAULT_TERMS_FILE}`);
|
|
return;
|
|
}
|
|
const { checked, findings } = runCheck(REPO, terms);
|
|
assert.ok(checked > 0, 'checked 0 files — verified nothing');
|
|
assert.deepEqual(findings.map((f) => `${f.path}:${f.line}`), []);
|
|
});
|