[E00-S01-T14] No core package imports a concrete extension #381
@@ -0,0 +1,201 @@
|
||||
// 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 });
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user