engine-compare imports the coordinator's title limit and trim instead of a copy whose reason went with Arm B; the autonomy gate exports one parser and its test runs in-process with two CLI runs; one import scanner in lib-imports; the fenced-section walker shared from tests/helpers/md-section.mjs instead of six copies; plan-gate hash in-process; living-paths scans once; routing drops the orchestrator filter whose files are gone; small leftovers removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
72 lines
3.3 KiB
JavaScript
72 lines
3.3 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 { readFileSync, readdirSync } from 'node:fs';
|
|
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');
|
|
|
|
/**
|
|
* The import specifiers in `text`: static and side-effect imports, re-exports
|
|
* and dynamic imports with a string literal.
|
|
*/
|
|
function specifiers(text) {
|
|
const re = /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)|\b(?:import|export)\s+(?:[^'";]*?\s+from\s+)?['"]([^'"]+)['"]/g;
|
|
return [...text.matchAll(re)].map((m) => m[1] ?? m[2]);
|
|
}
|
|
|
|
/**
|
|
* `file: specifier` for each import of `files` (repo-relative) that resolves
|
|
* into one of `dirs` of the repo. `textOf` reads a file's text.
|
|
*/
|
|
function forbiddenImports(files, dirs, textOf = read) {
|
|
const out = [];
|
|
for (const rel of files) {
|
|
for (const spec of specifiers(textOf(rel))) {
|
|
if (!spec.startsWith('.') && !spec.startsWith('/')) continue;
|
|
const target = relative(ROOT, resolve(ROOT, dirname(rel), spec)).split(sep).join('/');
|
|
if (dirs.some((d) => target === d || target.startsWith(`${d}/`))) out.push(`${rel}: ${spec}`);
|
|
}
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/** Every .mjs file under the repo-relative `dir`, recursively, repo-relative. */
|
|
function mjsFiles(dir) {
|
|
const out = [];
|
|
for (const e of readdirSync(join(ROOT, dir), { withFileTypes: true })) {
|
|
const p = `${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 = "import x from '../../scripts/release-check.mjs';\n";
|
|
assert.deepEqual(forbiddenImports(['lib/review/x.mjs'], ['scripts'], () => planted),
|
|
['lib/review/x.mjs: ../../scripts/release-check.mjs'], 'control: the scan does not report a planted import from scripts/');
|
|
const files = mjsFiles('lib');
|
|
assert.ok(files.length > 10, `the scan found only ${files.length} files under lib/`);
|
|
assert.deepEqual(forbiddenImports(files, ['scripts']), [], 'lib/ imports from scripts/');
|
|
});
|
|
|
|
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',
|
|
];
|
|
|
|
test('arch: the review modules import no hooks/ code, and the scan reports a planted import (control)', () => {
|
|
const planted = (rel) => (rel === REVIEW_LIB[0] ? "await import('../../hooks/scripts/pre-bash-executor.mjs');\n" : '') + read(rel);
|
|
assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks'], planted),
|
|
[`${REVIEW_LIB[0]}: ../../hooks/scripts/pre-bash-executor.mjs`], 'control: the scan does not report a planted import from hooks/');
|
|
assert.deepEqual(forbiddenImports(REVIEW_LIB, ['hooks']), [], 'a review module imports hooks/ code');
|
|
});
|