Files
PersonalBlog/tests/no-core-extension-imports.test.mjs
T
2026-08-28 23:24:02 +00:00

202 lines
8.5 KiB
JavaScript

// No-core-extension-imports test — locks in the [E00-S01-T14] rule that no
// core package imports a concrete extension.
//
// Acceptance criteria covered (each test fails without the committed state):
// - "no core package imports a concrete extension" -> the architecture
// import test (E00-S01-T04, tests/architecture-import.test.mjs) is part
// of the root test suite (the root `scripts.test` glob) and passes
// against the real workspace graph with zero violations: no `packages/*`
// source file imports anything that resolves to `extensions/*`.
// - "the architecture import test catches any such import" -> the test
// file is committed with the real-graph enforcement, the rule file
// (dependency-boundaries.json) forbids the `packages -> extensions`
// edge, and a mutation probe proves detection: in a temp workspace copy
// with a core -> extension import injected into
// packages/core/src/index.ts, the architecture import test exits
// non-zero and reports the offending file and the
// `packages -> extensions` rule.
//
// The passing real-graph probe is non-vacuous: the mutation probe proves the
// architecture import test fails on an injected core -> extension import, so
// a compliant result means the scanner really ran and found nothing.
//
// Run: `node --test tests/no-core-extension-imports.test.mjs`
// (node:test — built into Node >= 18; no dependencies, lockfile untouched.)
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, cpSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs';
import { spawnSync } from 'node:child_process';
import os from 'node:os';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const read = (relPath) => readFileSync(path.join(REPO_ROOT, relPath), 'utf8');
/** The architecture import test named by the acceptance criterion (E00-S01-T04). */
const ARCH_TEST = 'tests/architecture-import.test.mjs';
/** The rule file that declares the forbidden packages -> extensions edge. */
const RULE_FILE = 'dependency-boundaries.json';
/** The root test glob (root `scripts.test`, E00-S01-T05/T12) that runs every suite. */
const ROOT_TEST_GLOB = 'tests/**/*.test.mjs';
/** The integration test inside the architecture import test that checks the real graph. */
const REAL_GRAPH_TEST = 'the architecture import test passes on the current compliant workspace graph';
/** A core -> extension import that must be caught by the architecture import test. */
const FORBIDDEN_IMPORT = "import { register } from '@personal-blog/example-extension';\n";
/**
* Runs the architecture import test with the given cwd, using the same Node
* runtime that runs this suite, and returns the spawn result.
*
* `NODE_TEST_CONTEXT` is stripped from the child env: when this suite runs
* under `node --test`, that variable marks the process as a test-runner
* child, and a nested `node --test` invocation would otherwise be treated as
* a recursive run and skip its files.
*/
function runArchitectureTest(cwd) {
const env = { ...process.env };
delete env.NODE_TEST_CONTEXT;
return spawnSync(process.execPath, ['--test', ARCH_TEST], {
cwd,
env,
encoding: 'utf8',
timeout: 120_000,
});
}
/** True when the root test glob covers a repo-root-relative test path. */
function coveredByRootGlob(relPath) {
// tests/**/*.test.mjs — "tests/" prefix plus "/*.test.mjs" at any depth.
return relPath.startsWith('tests/') && relPath.endsWith('.test.mjs');
}
/** A minimal package.json for a temp workspace package (only the name matters here). */
function tempManifest(name) {
return `${JSON.stringify({ name, version: '0.0.0', private: true }, null, 2)}\n`;
}
/**
* Builds a minimal but complete temp workspace copy — the rule file, the
* architecture import test, and the three expected workspace packages
* (apps/server, packages/core, extensions/example) — with
* `packages/core/src/index.ts` set to `coreSource`. Returns the temp dir.
*/
function writeTempWorkspace({ coreSource }) {
const dir = mkdtempSync(path.join(os.tmpdir(), 'eppp-core-ext-'));
// The artifact under test plus the rule it enforces.
cpSync(path.join(REPO_ROOT, RULE_FILE), path.join(dir, RULE_FILE));
mkdirSync(path.join(dir, 'tests'), { recursive: true });
cpSync(path.join(REPO_ROOT, ARCH_TEST), path.join(dir, ARCH_TEST));
// The three workspace packages the architecture test's integration case
// expects to discover (apps/server, packages/core, extensions/example).
const packages = {
'apps/server': { name: '@personal-blog/server', src: 'export {};\n' },
'packages/core': { name: '@personal-blog/core', src: coreSource },
'extensions/example': { name: '@personal-blog/example-extension', src: 'export {};\n' },
};
for (const [relDir, pkg] of Object.entries(packages)) {
mkdirSync(path.join(dir, relDir, 'src'), { recursive: true });
writeFileSync(path.join(dir, relDir, 'package.json'), tempManifest(pkg.name));
writeFileSync(path.join(dir, relDir, 'src', 'index.ts'), pkg.src);
}
return dir;
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
test('the architecture import test is committed, wired into the root suite, and forbids the edge', () => {
// The artifact named by the criterion exists and still carries the
// real-graph enforcement ("no core package may import a concrete extension").
const archTest = read(ARCH_TEST);
assert.match(
archTest,
/no core package may import a concrete extension/,
`${ARCH_TEST} must contain the core -> extension enforcement message`,
);
assert.match(
archTest,
new RegExp(REAL_GRAPH_TEST.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')),
`${ARCH_TEST} must contain the real-workspace integration test "${REAL_GRAPH_TEST}"`,
);
// It is picked up by the root test command (`pnpm test` -> the root
// `scripts.test` glob), so a violation fails the whole root suite.
const scripts = JSON.parse(read('package.json')).scripts ?? {};
assert.equal(
scripts.test,
`node --test "${ROOT_TEST_GLOB}"`,
`root scripts.test must run the "${ROOT_TEST_GLOB}" glob so ${ARCH_TEST} runs with the rest of the suite`,
);
assert.ok(
coveredByRootGlob(ARCH_TEST),
`${ARCH_TEST} must be covered by the root test glob "${ROOT_TEST_GLOB}"`,
);
// The rule file forbids the packages -> extensions edge, so the
// architecture import test has the forbidden edge to enforce.
const rule = JSON.parse(read(RULE_FILE));
const forbidden = (rule.boundaries ?? []).filter((b) => !b.allow);
assert.ok(
forbidden.some((b) => b.from === 'packages' && b.to === 'extensions'),
`${RULE_FILE} must forbid the packages -> extensions edge`,
);
});
test('no core package imports a concrete extension (architecture import test passes on the real workspace)', () => {
const result = runArchitectureTest(REPO_ROOT);
assert.equal(
result.status,
0,
`the architecture import test must pass on the real workspace graph ` +
`(a core package may not import a concrete extension):\n${result.stdout}${result.stderr}`,
);
});
test('the architecture import test catches a core -> extension import (mutation probe)', () => {
const dir = writeTempWorkspace({ coreSource: FORBIDDEN_IMPORT });
try {
const result = runArchitectureTest(dir);
assert.notEqual(
result.status,
0,
'the architecture import test must fail when a core package imports a concrete extension',
);
const output = `${result.stdout}\n${result.stderr}`;
assert.match(
output,
/packages[\\/]core[\\/]src[\\/]index\.ts/,
'the violation report must name the offending core source file',
);
assert.match(
output,
/violates packages -> extensions/,
'the violation report must cite the packages -> extensions rule',
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});
test('a compliant temp workspace passes the architecture import test (probe sanity)', () => {
const dir = writeTempWorkspace({ coreSource: 'export {};\n' });
try {
const result = runArchitectureTest(dir);
assert.equal(
result.status,
0,
'a temp workspace without core -> extension imports must pass the architecture import test ' +
`(so the mutation probe failure is caused by the injected import):\n${result.stdout}${result.stderr}`,
);
} finally {
rmSync(dir, { recursive: true, force: true });
}
});