test: harden housekeeping foundation boundaries

This commit is contained in:
Simo committed 2026-08-26 18:00:35 +02:00
1 parent a158c78a7c
commit ebc263da35
1 file changed
+121 -23
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync } from "node:fs"; import { existsSync, readdirSync, readFileSync } from "node:fs";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import { join, posix } from "node:path"; import { join, posix } from "node:path";
import { createElement } from "react"; import { createElement, type ReactElement } from "react";
import { renderToStaticMarkup } from "react-dom/server"; import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { HOUSEKEEPING_MANIFESTS } from "../manifests"; import { HOUSEKEEPING_MANIFESTS } from "../manifests";
@@ -51,8 +51,15 @@ interface BabelParser {
): BabelNode; ): BabelNode;
} }
interface ModuleAccess {
kind: "import" | "export" | "runtime";
importedNames: readonly string[];
specifier: string;
typeOnly: boolean;
}
interface ModuleAccessScan { interface ModuleAccessScan {
specifiers: readonly string[]; accesses: readonly ModuleAccess[];
violations: readonly string[]; violations: readonly string[];
} }
@@ -185,13 +192,30 @@ function isTypeOnlyDeclaration(
}) })
); );
} }
function namedImportNames(node: BabelNode): readonly string[] {
const specifiers = Array.isArray(node.specifiers) ? node.specifiers : [];
return specifiers.flatMap((specifier) => {
const declaration = isBabelNode(specifier) ? specifier : null;
if (declaration?.type !== "ImportSpecifier") return [];
const imported = unwrapModuleArgument(declaration.imported);
if (imported?.type === "Identifier" && typeof imported.name === "string") {
return [imported.name];
}
return imported?.type === "StringLiteral" &&
typeof imported.value === "string"
? [imported.value]
: [];
});
}
function scanModuleAccesses(source: string): ModuleAccessScan { function scanModuleAccesses(source: string): ModuleAccessScan {
const root = babelParser.parse(source, { const root = babelParser.parse(source, {
createImportExpressions: true, createImportExpressions: true,
plugins: ["typescript", "jsx"], plugins: ["typescript", "jsx"],
sourceType: "module", sourceType: "module",
}); });
const specifiers: string[] = []; const accesses: ModuleAccess[] = [];
const violations: string[] = []; const violations: string[] = [];
function recordArgument(argument: unknown, kind: "import" | "require") { function recordArgument(argument: unknown, kind: "import" | "require") {
@@ -200,7 +224,12 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
violations.push(`<non-literal ${kind}>`); violations.push(`<non-literal ${kind}>`);
return; return;
} }
specifiers.push(specifier); accesses.push({
kind: "runtime",
importedNames: [],
specifier,
typeOnly: false,
});
} }
function visit(value: unknown): void { function visit(value: unknown): void {
@@ -211,18 +240,28 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
if (!isBabelNode(value)) return; if (!isBabelNode(value)) return;
if (value.type === "ImportDeclaration") { if (value.type === "ImportDeclaration") {
if (!isTypeOnlyDeclaration(value, "importKind")) { const specifier = readLiteralModuleSpecifier(value.source);
const specifier = readLiteralModuleSpecifier(value.source); if (specifier !== null) {
if (specifier !== null) specifiers.push(specifier); accesses.push({
kind: "import",
importedNames: namedImportNames(value),
specifier,
typeOnly: isTypeOnlyDeclaration(value, "importKind"),
});
} }
} else if ( } else if (
(value.type === "ExportNamedDeclaration" || (value.type === "ExportNamedDeclaration" ||
value.type === "ExportAllDeclaration") && value.type === "ExportAllDeclaration") &&
value.source !== null value.source !== null
) { ) {
if (!isTypeOnlyDeclaration(value, "exportKind")) { const specifier = readLiteralModuleSpecifier(value.source);
const specifier = readLiteralModuleSpecifier(value.source); if (specifier !== null) {
if (specifier !== null) specifiers.push(specifier); accesses.push({
kind: "export",
importedNames: [],
specifier,
typeOnly: isTypeOnlyDeclaration(value, "exportKind"),
});
} }
} else if (value.type === "ImportExpression") { } else if (value.type === "ImportExpression") {
recordArgument(value.source, "import"); recordArgument(value.source, "import");
@@ -246,7 +285,7 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
} }
visit(root); visit(root);
return { specifiers, violations }; return { accesses, violations };
} }
function canonicalizeModuleSpecifier( function canonicalizeModuleSpecifier(
@@ -276,12 +315,7 @@ function canonicalizeModuleSpecifier(
} }
return { return {
candidates: [ candidates: [normalized.replace(resolverExtensionPattern, "")],
...new Set([
normalized,
normalized.replace(resolverExtensionPattern, ""),
]),
],
violation: null, violation: null,
}; };
} }
@@ -306,6 +340,21 @@ function isForbiddenModulePath(path: string, sourceFile: string): boolean {
isDomainWorkflowModule(path) isDomainWorkflowModule(path)
); );
} }
function isAllowedPermissionSetTypeImport(
access: ModuleAccess,
canonical: CanonicalModuleSpecifier,
sourceFile: string,
): boolean {
return (
sourceFile ===
"src/features/housekeeping/foundation/capability-context.ts" &&
access.kind === "import" &&
access.typeOnly &&
access.importedNames.length === 1 &&
access.importedNames[0] === "PermissionSet" &&
canonical.candidates.includes(PERMISSIONS_ADAPTER)
);
}
function findHousekeepingImportBoundaryViolations( function findHousekeepingImportBoundaryViolations(
source: string, source: string,
@@ -314,9 +363,11 @@ function findHousekeepingImportBoundaryViolations(
const scan = scanModuleAccesses(source); const scan = scanModuleAccesses(source);
const violations = [...scan.violations]; const violations = [...scan.violations];
for (const specifier of scan.specifiers) { for (const access of scan.accesses) {
const canonical = canonicalizeModuleSpecifier(sourceFile, specifier); const canonical = canonicalizeModuleSpecifier(sourceFile, access.specifier);
if (canonical.violation) violations.push(canonical.violation); if (canonical.violation) violations.push(canonical.violation);
if (isAllowedPermissionSetTypeImport(access, canonical, sourceFile))
continue;
const forbiddenPath = canonical.candidates.find((candidate) => const forbiddenPath = canonical.candidates.find((candidate) =>
isForbiddenModulePath(candidate, sourceFile), isForbiddenModulePath(candidate, sourceFile),
); );
@@ -325,6 +376,14 @@ function findHousekeepingImportBoundaryViolations(
return violations; return violations;
} }
function executablePropNames(element: ReactElement): readonly string[] {
const props = element.props;
if (typeof props !== "object" || props === null) return [];
return Object.entries(props).flatMap(([name, value]) =>
/^on/i.test(name) && typeof value === "function" ? [name] : [],
);
}
describe("housekeeping runtime import boundary", () => { describe("housekeeping runtime import boundary", () => {
it("keeps every runtime module inside the foundation boundary", () => { it("keeps every runtime module inside the foundation boundary", () => {
@@ -344,6 +403,18 @@ describe("housekeeping runtime import boundary", () => {
'import { db } from "@/lib/db";', 'import { db } from "@/lib/db";',
"src/lib/db", "src/lib/db",
], ],
[
"aliased type-only database import",
"src/features/housekeeping/foundation/registry.ts",
'import type { Database } from "@/lib/db";',
"src/lib/db",
],
[
"aliased type-only action export",
"src/features/housekeeping/foundation/registry.ts",
'export type { ActionInput } from "@/actions/users";',
"src/actions/users",
],
[ [
"relative action import", "relative action import",
"src/features/housekeeping/foundation/registry.ts", "src/features/housekeeping/foundation/registry.ts",
@@ -451,6 +522,24 @@ describe("housekeeping runtime import boundary", () => {
), ),
).toEqual([]); ).toEqual([]);
}); });
it("allows a harmless type-only module lookalike", () => {
expect(
findHousekeepingImportBoundaryViolations(
'import type { DatabaseDocument } from "@/lib/database";',
"src/features/housekeeping/manifests.ts",
),
).toEqual([]);
});
it("allows a normalized domain manifest with a resolver extension", () => {
expect(
findHousekeepingImportBoundaryViolations(
'import manifest from "./domains/people/manifest.ts";',
"src/features/housekeeping/manifests.ts",
),
).toEqual([]);
});
}); });
describe("housekeeping foundation completion contracts", () => { describe("housekeeping foundation completion contracts", () => {
@@ -486,16 +575,25 @@ describe("housekeeping foundation completion contracts", () => {
).toBe(false); ).toBe(false);
}); });
it("detects executable React props before markup serialization", () => {
const mutatedTrigger = createElement(
"button",
{ onClick: () => undefined, type: "button" },
"mutation witness",
);
expect(executablePropNames(mutatedTrigger)).toEqual(["onClick"]);
});
it("renders one inert localized command affordance", () => { it("renders one inert localized command affordance", () => {
const sentinel = "HK::comando-localizzato-disabilitato"; const sentinel = "HK::comando-localizzato-disabilitato";
const html = renderToStaticMarkup( const trigger = CommandTrigger({ label: sentinel });
createElement(CommandTrigger, { label: sentinel }), const html = renderToStaticMarkup(trigger);
);
const buttons = html.match(/<button\b[^>]*>/g) ?? []; const buttons = html.match(/<button\b[^>]*>/g) ?? [];
expect(executablePropNames(trigger)).toEqual([]);
expect(buttons).toHaveLength(1); expect(buttons).toHaveLength(1);
expect(buttons[0]).toMatch(/\sdisabled(?:=""|(?=\s|>))/); expect(buttons[0]).toMatch(/\sdisabled(?:=""|(?=\s|>))/);
expect(buttons[0]).not.toMatch(/\son[a-z][a-z0-9-]*\s*=/i);
expect(html).toContain(`>${sentinel}</button>`); expect(html).toContain(`>${sentinel}</button>`);
}); });