From 97d89d8728a55bcaf8ca3446c12a7912474a2172 Mon Sep 17 00:00:00 2001 From: LogicDuke Date: Mon, 24 Aug 2026 20:09:19 +0200 Subject: [PATCH] test(cockpit): harden D3 import scanner --- tests/cockpit-host/purity.test.ts | 95 +++++++++++++++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/tests/cockpit-host/purity.test.ts b/tests/cockpit-host/purity.test.ts index 4508f1b..ee9f6ab 100644 --- a/tests/cockpit-host/purity.test.ts +++ b/tests/cockpit-host/purity.test.ts @@ -23,6 +23,47 @@ function hostSources(): readonly { readonly file: string; readonly text: string .map((name) => ({ file: name, text: readFileSync(join(hostDir, name), 'utf8') })); } +/** + * Extract every module specifier from TypeScript/NodeNext ESM source. + * + * The import-discipline checks below judge *specifiers*, so a specifier this + * helper fails to surface is silently exempt from the boundary. It therefore + * covers all the static-graph import forms this project can use, in either + * quote style, so no forbidden dependency can hide behind syntax: + * + * - static: `import x from '...'` / `import x from "..."` + * (including multi-line `import type { ... } from '...'`) + * - side-effect: `import '...'` / `import "..."` + * - dynamic: `import('...')` / `import("...")` + * - re-export: `export { x } from '...'` / `export * from "..."` + * + * `import.meta.url` is deliberately not matched: the `import` keyword must be + * followed by whitespace (static/side-effect) or `(` (dynamic), and `.` is + * neither. This is a bounded lexical scan, not a parser — no AST dependency is + * introduced, and, like the original scanner, it does not exclude specifiers + * that appear inside comments. + */ +function extractModuleSpecifiers(source: string): readonly string[] { + const patterns: readonly RegExp[] = [ + // static (`import x from 'S'`) and side-effect (`import 'S'`) imports. + /\bimport\s+(?:[^'"]*?\bfrom\s+)?['"]([^'"]+)['"]/g, + // dynamic imports: `import('S')`. + /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g, + // re-export bindings: `export { x } from 'S'`, `export * from 'S'`. + /\bexport\b[^'"]*?\bfrom\s+['"]([^'"]+)['"]/g, + ]; + const specifiers: string[] = []; + for (const pattern of patterns) { + for (const match of source.matchAll(pattern)) { + const specifier = match[1]; + if (specifier !== undefined) { + specifiers.push(specifier); + } + } + } + return specifiers; +} + describe('D3 host has no mutation, subprocess, secret, or Git capability', () => { it('references no subprocess, environment, or Git operation', () => { const forbidden: readonly RegExp[] = [ @@ -45,12 +86,9 @@ describe('D3 host has no mutation, subprocess, secret, or Git capability', () => }); describe('D3 host import discipline', () => { - const importSpecifiers = /from\s+'([^']+)'/g; - it('imports only node builtins, itself, or the Cockpit boundary', () => { for (const { file, text } of hostSources()) { - for (const match of text.matchAll(importSpecifiers)) { - const specifier = match[1] ?? ''; + for (const specifier of extractModuleSpecifiers(text)) { const allowed = specifier.startsWith('node:') || specifier.startsWith('./') || @@ -62,8 +100,7 @@ describe('D3 host import discipline', () => { it('never imports an adapter, transport, or authority module (nor the domain kernel directly)', () => { for (const { file, text } of hostSources()) { - for (const match of text.matchAll(importSpecifiers)) { - const specifier = match[1] ?? ''; + for (const specifier of extractModuleSpecifiers(text)) { expect( /adapter|transport|authorization|repair-job|permit|\.\.\/domain\//i.test(specifier), `${file} imports forbidden module: ${specifier}`, @@ -72,3 +109,49 @@ describe('D3 host import discipline', () => { } }); }); + +describe('D3 host import scanner recognizes every supported ESM form (D3-CR-F1)', () => { + // A forbidden domain/adapter import must be surfaced no matter which valid + // import syntax hides it — otherwise the discipline checks above are blind to + // it. Each fixture below is a single valid TypeScript/NodeNext ESM statement. + const forbiddenForms: readonly { readonly form: string; readonly source: string }[] = [ + { form: 'single-quoted static from', source: `import x from '../domain/foo.js';` }, + { form: 'double-quoted static from', source: `import x from "../domain/foo.js";` }, + { form: 'single-quoted side-effect', source: `import '../adapters/foo.js';` }, + { form: 'double-quoted side-effect', source: `import "../adapters/foo.js";` }, + { form: 'single-quoted dynamic', source: `const m = import('../domain/foo.js');` }, + { form: 'double-quoted dynamic', source: `const m = import("../domain/foo.js");` }, + { form: 're-export from', source: `export { y } from "../domain/foo.js";` }, + ]; + + for (const { form, source } of forbiddenForms) { + it(`extracts the forbidden specifier from a ${form} import`, () => { + const specifiers = extractModuleSpecifiers(source); + const forbidden = specifiers.filter((s) => /\.\.\/(?:domain|adapters)\//.test(s)); + expect(forbidden.length, `no specifier extracted from: ${source}`).toBeGreaterThan(0); + }); + } + + it('extracts allowed node builtin, local, and Cockpit-boundary specifiers', () => { + expect(extractModuleSpecifiers(`import http from 'node:http';`)).toContain('node:http'); + expect(extractModuleSpecifiers(`import { a } from "./local.js";`)).toContain('./local.js'); + expect( + extractModuleSpecifiers(`import { readCockpitSnapshot } from '../cockpit/index.js';`), + ).toContain('../cockpit/index.js'); + }); + + it('extracts a multi-line `import type { ... } from` specifier', () => { + const source = [ + 'import type {', + ' CockpitSnapshot,', + ' CockpitFindingReadModel,', + "} from '../cockpit/index.js';", + ].join('\n'); + expect(extractModuleSpecifiers(source)).toContain('../cockpit/index.js'); + }); + + it('does not treat `import.meta.url` as a module specifier', () => { + const source = `const isEntry = import.meta.url === pathToFileURL(entry).href;`; + expect(extractModuleSpecifiers(source)).toEqual([]); + }); +});