From 27851fcaeb241364d6e456c15576b2fcc6ec1a6d Mon Sep 17 00:00:00 2001 From: implementer Date: Fri, 28 Aug 2026 23:24:02 +0000 Subject: [PATCH] test: lock in no core package imports a concrete extension (E00-S01-T14) --- tests/no-core-extension-imports.test.mjs | 201 +++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 tests/no-core-extension-imports.test.mjs diff --git a/tests/no-core-extension-imports.test.mjs b/tests/no-core-extension-imports.test.mjs new file mode 100644 index 0000000..02e1f7b --- /dev/null +++ b/tests/no-core-extension-imports.test.mjs @@ -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 }); + } +}); -- 2.54.0