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>
118 lines
5.7 KiB
JavaScript
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 });
|
|
}
|
|
});
|