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
+119 -21
@@ -1,7 +1,7 @@
import { existsSync, readdirSync, readFileSync } from "node:fs";
import { createRequire } from "node:module";
import { join, posix } from "node:path";
import { createElement } from "react";
import { createElement, type ReactElement } from "react";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vitest";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
@@ -51,8 +51,15 @@ interface BabelParser {
): BabelNode;
}
interface ModuleAccess {
kind: "import" | "export" | "runtime";
importedNames: readonly string[];
specifier: string;
typeOnly: boolean;
}
interface ModuleAccessScan {
specifiers: readonly string[];
accesses: readonly ModuleAccess[];
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 {
const root = babelParser.parse(source, {
createImportExpressions: true,
plugins: ["typescript", "jsx"],
sourceType: "module",
});
const specifiers: string[] = [];
const accesses: ModuleAccess[] = [];
const violations: string[] = [];
function recordArgument(argument: unknown, kind: "import" | "require") {
@@ -200,7 +224,12 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
violations.push(`<non-literal ${kind}>`);
return;
}
specifiers.push(specifier);
accesses.push({
kind: "runtime",
importedNames: [],
specifier,
typeOnly: false,
});
}
function visit(value: unknown): void {
@@ -211,18 +240,28 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
if (!isBabelNode(value)) return;
if (value.type === "ImportDeclaration") {
if (!isTypeOnlyDeclaration(value, "importKind")) {
const specifier = readLiteralModuleSpecifier(value.source);
if (specifier !== null) specifiers.push(specifier);
if (specifier !== null) {
accesses.push({
kind: "import",
importedNames: namedImportNames(value),
specifier,
typeOnly: isTypeOnlyDeclaration(value, "importKind"),
});
}
} else if (
(value.type === "ExportNamedDeclaration" ||
value.type === "ExportAllDeclaration") &&
value.source !== null
) {
if (!isTypeOnlyDeclaration(value, "exportKind")) {
const specifier = readLiteralModuleSpecifier(value.source);
if (specifier !== null) specifiers.push(specifier);
if (specifier !== null) {
accesses.push({
kind: "export",
importedNames: [],
specifier,
typeOnly: isTypeOnlyDeclaration(value, "exportKind"),
});
}
} else if (value.type === "ImportExpression") {
recordArgument(value.source, "import");
@@ -246,7 +285,7 @@ function scanModuleAccesses(source: string): ModuleAccessScan {
}
visit(root);
return { specifiers, violations };
return { accesses, violations };
}
function canonicalizeModuleSpecifier(
@@ -276,12 +315,7 @@ function canonicalizeModuleSpecifier(
}
return {
candidates: [
...new Set([
normalized,
normalized.replace(resolverExtensionPattern, ""),
]),
],
candidates: [normalized.replace(resolverExtensionPattern, "")],
violation: null,
};
}
@@ -306,6 +340,21 @@ function isForbiddenModulePath(path: string, sourceFile: string): boolean {
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(
source: string,
@@ -314,9 +363,11 @@ function findHousekeepingImportBoundaryViolations(
const scan = scanModuleAccesses(source);
const violations = [...scan.violations];
for (const specifier of scan.specifiers) {
const canonical = canonicalizeModuleSpecifier(sourceFile, specifier);
for (const access of scan.accesses) {
const canonical = canonicalizeModuleSpecifier(sourceFile, access.specifier);
if (canonical.violation) violations.push(canonical.violation);
if (isAllowedPermissionSetTypeImport(access, canonical, sourceFile))
continue;
const forbiddenPath = canonical.candidates.find((candidate) =>
isForbiddenModulePath(candidate, sourceFile),
);
@@ -325,6 +376,14 @@ function findHousekeepingImportBoundaryViolations(
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", () => {
it("keeps every runtime module inside the foundation boundary", () => {
@@ -344,6 +403,18 @@ describe("housekeeping runtime import boundary", () => {
'import { db } from "@/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",
"src/features/housekeeping/foundation/registry.ts",
@@ -451,6 +522,24 @@ describe("housekeeping runtime import boundary", () => {
),
).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", () => {
@@ -486,16 +575,25 @@ describe("housekeeping foundation completion contracts", () => {
).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", () => {
const sentinel = "HK::comando-localizzato-disabilitato";
const html = renderToStaticMarkup(
createElement(CommandTrigger, { label: sentinel }),
);
const trigger = CommandTrigger({ label: sentinel });
const html = renderToStaticMarkup(trigger);
const buttons = html.match(/<button\b[^>]*>/g) ?? [];
expect(executablePropNames(trigger)).toEqual([]);
expect(buttons).toHaveLength(1);
expect(buttons[0]).toMatch(/\sdisabled(?:=""|(?=\s|>))/);
expect(buttons[0]).not.toMatch(/\son[a-z][a-z0-9-]*\s*=/i);
expect(html).toContain(`>${sentinel}</button>`);
});