voyage/tests/lib/lib-imports.test.mjs
Kjell Tore Guttormsen 651be3468b
test(r8): move the live tests out of the round files
R8 of cleanup R-a (plan .claude/projects/2026-11-07-cleanup-r-a, appendix A).
The live tests of the round files move into the test named after their
source; each moved test was seen red under a mutation of its source.
New: tests/lib/lib-imports.test.mjs, tests/lib/timing.test.mjs (from
timing-under-load). The [#1] --gates rule and shim-line pins move into
tests/lib/autonomy-gate.test.mjs.

Removed: the round test files (retterunde, retterunde-prose-pins,
trial-mechanical-fixes, trial2-fixes, trial2-minor, trial2-minor-prose-pins,
fix-round-4-5a-prose, fix-round-4-5b2-b-prose, timing-under-load), the 8 R8
e2e scripts, and seven round test files on the R-a removal list that pinned
them (fix-round-4-5b-prose, fix-round-4-5b-part-2/3/4-scripts,
fix-round-4-5b-part-4-prose, fix-round-4-5b2-b/c-scripts). The diff-mode e2e
pin (test 24) and the red-script round-wrapper cases go too.

tests/synthetic/plan-run-C.md becomes tests/fixtures/plan-run-c.md,
regenerated from buildPlan({ version: '1.9' }). The red judge, reporter and
judge test headers now name the shared red script.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-07 10:19:49 +01:00

118 lines
5.7 KiB
JavaScript

// tests/lib/lib-imports.test.mjs
// Architecture rule: code under lib/ never imports from scripts/ (or, for the
// review modules, from hooks/). Each check runs on the real files and on a
// planted import, so a scanner that sees nothing cannot pass.
import { test } from 'node:test';
import { strict as assert } from 'node:assert';
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
const read = (rel) => readFileSync(join(ROOT, rel), 'utf8');
// ---- lib/ never imports from scripts/ ----------------------------------------
/**
* The import specifiers in `text` (a file at `file`) that resolve into the
* repo's scripts/ directory: static and side-effect imports, re-exports and
* dynamic imports with a string literal.
*/
function importsIntoScripts(file, text) {
const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g;
const scripts = join(ROOT, 'scripts') + sep;
const hits = [];
for (const m of text.matchAll(re)) {
const spec = m[1] ?? m[2];
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
if (resolve(dirname(file), spec).startsWith(scripts)) hits.push(spec);
}
return hits;
}
/** Every .mjs file under `dir`, recursively. */
function mjsFiles(dir) {
const out = [];
for (const e of readdirSync(dir, { withFileTypes: true })) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...mjsFiles(p));
else if (e.name.endsWith('.mjs')) out.push(p);
}
return out;
}
test('arch: lib/ never imports from scripts/, and the scan reports a planted import (control)', () => {
const planted = importsIntoScripts(join(ROOT, 'lib', 'review', 'x.mjs'), "import x from '../../scripts/release-check.mjs';\n");
assert.deepEqual(planted, ['../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/');
const files = mjsFiles(join(ROOT, 'lib'));
assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`);
const bad = files.flatMap((f) => importsIntoScripts(f, readFileSync(f, 'utf8')).map((s) => `${f.slice(ROOT.length + 1)}: ${s}`));
assert.deepEqual(bad, [], 'lib/ imports from scripts/');
});
// ---- the review modules import no scripts/ or hooks/ code ---------------------
const REVIEW_LIB = [
'lib/review/review-answers.mjs', 'lib/review/pre-existing.mjs', 'lib/review/review-rounds.mjs',
'lib/review/review-policy.mjs', 'lib/review/engine-compare.mjs', 'lib/review/instruction-scope.mjs',
'lib/review/review-finalize.mjs', 'lib/review/merge-evidence.mjs', 'lib/util/doors.mjs',
];
const FORBIDDEN_DIRS = ['scripts', 'hooks'];
const PLANTED = "import { checkReview } from '../../scripts/release-check.mjs';\n";
/** The specifiers of a module's import lines: `import … from`, bare `import '…'`, `export … from`, `import('…')`. */
function importSpecifiers(src) {
const out = [];
for (const m of src.matchAll(/^[ \t]*import\b(?!\s*\()[^;]*?(['"])([^'"\n]+)\1/gm)) out.push(m[2]);
for (const m of src.matchAll(/^[ \t]*export\s*(?:\*|\{)[^;]*?\bfrom\s*(['"])([^'"\n]+)\1/gm)) out.push(m[2]);
for (const m of src.matchAll(/\bimport\s*\(\s*(['"])([^'"\n]+)\1\s*\)/g)) out.push(m[2]);
return out;
}
/** `file: specifier` for each import of `files` (paths under `root`) that resolves into scripts/ or hooks/ of `root`. */
function forbiddenImports(root, files) {
const out = [];
for (const rel of files) {
const abs = join(root, rel);
for (const spec of importSpecifiers(readFileSync(abs, 'utf8'))) {
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
const target = relative(root, resolve(dirname(abs), spec)).split(sep).join('/');
if (FORBIDDEN_DIRS.some((d) => target === d || target.startsWith(`${d}/`))) out.push(`${rel}: ${spec}`);
}
}
return out;
}
test('arch: the review modules import no scripts/ or hooks/ code', () => {
assert.deepEqual([...REVIEW_LIB].sort(), [
'lib/review/engine-compare.mjs', 'lib/review/instruction-scope.mjs', 'lib/review/merge-evidence.mjs',
'lib/review/pre-existing.mjs', 'lib/review/review-answers.mjs', 'lib/review/review-finalize.mjs',
'lib/review/review-policy.mjs', 'lib/review/review-rounds.mjs', 'lib/util/doors.mjs',
], 'the scanner reads exactly these nine files');
for (const rel of REVIEW_LIB) assert.ok(existsSync(join(ROOT, rel)), `missing file ${rel}`);
// The real files: nothing.
assert.deepEqual(forbiddenImports(ROOT, REVIEW_LIB), [], 'a review module imports scripts/ or hooks/ code');
const tmp = mkdtempSync(join(tmpdir(), 'lib-imports-'));
try {
for (const rel of REVIEW_LIB) {
mkdirSync(dirname(join(tmp, rel)), { recursive: true });
writeFileSync(join(tmp, rel), read(rel));
}
// Control: the copy reads like the real files.
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [], 'Control: the clean copy reports nothing');
// A planted import of a scripts/ module is reported.
writeFileSync(join(tmp, REVIEW_LIB[0]), PLANTED + read(REVIEW_LIB[0]));
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [`${REVIEW_LIB[0]}: ../../scripts/release-check.mjs`],
'the scanner reports the planted import of ../../scripts/release-check.mjs');
// The clean copy again: nothing.
writeFileSync(join(tmp, REVIEW_LIB[0]), read(REVIEW_LIB[0]));
assert.deepEqual(forbiddenImports(tmp, REVIEW_LIB), [], 'the clean copy again reports nothing');
} finally {
rmSync(tmp, { recursive: true, force: true });
}
});