test(hygiene): no tracked file may carry a retired-repository reference (red)
A node:test pin that scans every tracked text file and path for the terms
in a gitignored local list (tests/.excluded-terms.local.txt), so the terms
never live in tracked text. The scanner is validated against a planted
known-positive (content and path name), an empty scan fails closed, the
named EXEMPT list is empty and rot-checked, and without the local list the
scan is skipped with an explicit message.
Red on 0be152a with the list present: 12 hits in 7 files.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
0be152ad29
commit
e1637c2530
1 changed files with 91 additions and 0 deletions
91
tests/lib/retired-references.test.mjs
Normal file
91
tests/lib/retired-references.test.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
// tests/lib/retired-references.test.mjs
|
||||||
|
// No tracked text file, and no tracked path, may contain a term from the
|
||||||
|
// local-only list in tests/.excluded-terms.local.txt (dead references to
|
||||||
|
// retired repositories). The list is gitignored on purpose: the terms never
|
||||||
|
// live in tracked text. Without the file the scan is skipped, loudly.
|
||||||
|
//
|
||||||
|
// When this test fails, rewrite the hit neutrally (describe the pattern, not
|
||||||
|
// the repository); do NOT widen EXEMPT to hide it.
|
||||||
|
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import { strict as assert } from 'node:assert';
|
||||||
|
import { execFileSync } from 'node:child_process';
|
||||||
|
import { existsSync, mkdtempSync, writeFileSync, readFileSync, rmSync } from 'node:fs';
|
||||||
|
import { join, dirname } from 'node:path';
|
||||||
|
import { tmpdir } from 'node:os';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const ROOT = join(HERE, '..', '..');
|
||||||
|
const TERMS_FILE = join(ROOT, 'tests', '.excluded-terms.local.txt');
|
||||||
|
const MISSING = `${TERMS_FILE} not found; retired-reference scan not run`;
|
||||||
|
|
||||||
|
// Named exceptions: tracked path -> reason. Empty on purpose; an entry must
|
||||||
|
// still hit (a stale exemption fails below).
|
||||||
|
const EXEMPT = new Map([]);
|
||||||
|
|
||||||
|
function loadTerms() {
|
||||||
|
return readFileSync(TERMS_FILE, 'utf-8').split('\n')
|
||||||
|
.map(l => l.trim().toLowerCase())
|
||||||
|
.filter(l => l && !l.startsWith('#'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function trackedFiles(root) {
|
||||||
|
const out = execFileSync('git', ['ls-files', '-z'], { cwd: root, encoding: 'utf-8' });
|
||||||
|
return out.split('\0').filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns one "path:line" (or "path: <path name>") entry per hit. The matched
|
||||||
|
// text is not echoed, so a failure message never repeats a term.
|
||||||
|
function scan(root, files, terms) {
|
||||||
|
const hits = [];
|
||||||
|
const hit = s => { const l = s.toLowerCase(); return terms.some(t => l.includes(t)); };
|
||||||
|
for (const rel of files) {
|
||||||
|
if (hit(rel)) hits.push(`${rel}: <path name>`);
|
||||||
|
const buf = readFileSync(join(root, rel));
|
||||||
|
if (buf.includes(0)) continue; // binary
|
||||||
|
buf.toString('utf-8').split('\n').forEach((line, i) => {
|
||||||
|
if (hit(line)) hits.push(`${rel}:${i + 1}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return hits;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('scan finds a planted term in content and in a path name (known-positive)', (t) => {
|
||||||
|
if (!existsSync(TERMS_FILE)) return t.skip(MISSING);
|
||||||
|
const terms = loadTerms();
|
||||||
|
assert.ok(terms.length > 0, `${TERMS_FILE} holds no terms`);
|
||||||
|
const dir = mkdtempSync(join(tmpdir(), 'retired-refs-'));
|
||||||
|
try {
|
||||||
|
const named = `${terms[0]}-notes.txt`;
|
||||||
|
writeFileSync(join(dir, 'clean.md'), 'nothing to see\n');
|
||||||
|
writeFileSync(join(dir, 'content.md'), `line one\nsee ${terms[0].toUpperCase()} here\n`);
|
||||||
|
writeFileSync(join(dir, named), 'clean body\n');
|
||||||
|
const hits = scan(dir, ['clean.md', 'content.md', named], terms);
|
||||||
|
assert.deepEqual(hits, ['content.md:2', `${named}: <path name>`]);
|
||||||
|
} finally {
|
||||||
|
rmSync(dir, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('the term list itself is not tracked', () => {
|
||||||
|
const tracked = trackedFiles(ROOT);
|
||||||
|
assert.ok(!tracked.includes('tests/.excluded-terms.local.txt'));
|
||||||
|
});
|
||||||
|
|
||||||
|
test('no tracked file or path contains a retired term', (t) => {
|
||||||
|
if (!existsSync(TERMS_FILE)) return t.skip(MISSING);
|
||||||
|
const files = trackedFiles(ROOT);
|
||||||
|
assert.ok(files.length > 100, `git ls-files returned ${files.length} files; refusing to pass on an empty scan`);
|
||||||
|
const hits = scan(ROOT, files.filter(f => !EXEMPT.has(f)), loadTerms());
|
||||||
|
assert.deepEqual(hits, [], `${hits.length} retired reference(s):\n${hits.join('\n')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('every EXEMPT entry is tracked and still hits', (t) => {
|
||||||
|
if (!existsSync(TERMS_FILE)) return t.skip(MISSING);
|
||||||
|
const files = new Set(trackedFiles(ROOT));
|
||||||
|
for (const rel of EXEMPT.keys()) {
|
||||||
|
assert.ok(files.has(rel), `EXEMPT names untracked ${rel}`);
|
||||||
|
assert.ok(scan(ROOT, [rel], loadTerms()).length > 0, `EXEMPT entry ${rel} no longer hits; remove it`);
|
||||||
|
}
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue