test(kb-update): gate tracked text against a local-only excluded-terms list

The gate reads `tests/.excluded-terms.local.json` (gitignored via
`*.local.json`): a pattern plus known positives and negatives. With the
list present it fails on any tracked text line that matches; without it
both tests are skipped with a message, so a fresh clone stays green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Kjell Tore Guttormsen 2026-09-23 14:04:26 +02:00
commit f774e2b158
Signed by: ktg
SSH key fingerprint: SHA256:JakMjO6FTBBzN0Bhfj9saOoEjaFxlSdYuZQQpM/lF9Q

View file

@ -0,0 +1,65 @@
// tests/kb-update/test-excluded-terms.test.mjs
// Preventive gate: no tracked text file may contain a term from the
// maintainer's excluded-terms list. Examples, fixtures and placeholders use
// generic, fictitious material instead.
//
// The list lives in `tests/.excluded-terms.local.json` (gitignored via
// `*.local.json`) and holds `pattern`, `flags`, `positives` and `negatives`.
// Without that file the gate is skipped with a message, so a fresh clone runs
// green; with it, the known positives prove the pattern can find before the
// empty result over the tree is trusted.
//
// Population: `git ls-files` (tracked files only). Binary files (NUL byte in
// the first 8000 bytes, as git decides) are skipped: text in a screenshot is
// rendered pixels, so a byte match there says nothing about what it shows.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { execFileSync } from 'node:child_process';
import { existsSync, readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const pluginRoot = fileURLToPath(new URL('../../', import.meta.url));
const listPath = fileURLToPath(new URL('../.excluded-terms.local.json', import.meta.url));
const skip = existsSync(listPath)
? false
: 'tests/.excluded-terms.local.json not present (local-only list) — gate skipped';
function loadList() {
const list = JSON.parse(readFileSync(listPath, 'utf8'));
return { ...list, regex: new RegExp(list.pattern, list.flags ?? '') };
}
function trackedFiles() {
return execFileSync('git', ['ls-files', '-z'], { cwd: pluginRoot, encoding: 'utf8' })
.split('\0')
.filter(Boolean);
}
function isBinary(buf) {
return buf.subarray(0, 8000).includes(0);
}
test('pattern matches every known positive and no known negative', { skip }, () => {
const { regex, positives = [], negatives = [] } = loadList();
assert.ok(positives.length > 0, 'the list must carry known positives');
for (const s of positives) assert.match(s, regex, s);
for (const s of negatives) assert.doesNotMatch(s, regex, s);
});
test('no tracked text file contains an excluded term', { skip }, () => {
const { regex } = loadList();
const files = trackedFiles();
assert.ok(files.length > 500, `git ls-files returned ${files.length} files — population too small to trust`);
const hits = [];
for (const rel of files) {
let buf;
try { buf = readFileSync(pluginRoot + rel); } catch { continue; }
if (isBinary(buf)) continue;
buf.toString('utf8').split('\n').forEach((line, i) => {
if (regex.test(line)) hits.push(`${rel}:${i + 1}`);
});
}
assert.deepEqual(hits, [], `excluded terms in tracked files:\n ${hits.join('\n ')}`);
});