test(housekeeping): prove 137 route parity
This commit is contained in:
1 parent
b235ce08ff
commit
8a31556d51
36 files changed
+431
-77
No files matched your search
@@ -0,0 +1,47 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { createHousekeepingRegistry } from "../foundation/registry";
|
||||
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
|
||||
import { discoverLegacyPages } from "../migration/discover-legacy-pages";
|
||||
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../migration/matrix";
|
||||
import { HOUSEKEEPING_ROUTE_HANDLERS } from "../route-handlers";
|
||||
import { verifyHousekeepingRuntimeParity } from "./parity";
|
||||
|
||||
describe("Housekeeping runtime parity", () => {
|
||||
const report = verifyHousekeepingRuntimeParity(
|
||||
HOUSEKEEPING_MIGRATION_MATRIX,
|
||||
createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS),
|
||||
HOUSEKEEPING_ROUTE_HANDLERS,
|
||||
);
|
||||
|
||||
it("closes all 137 discovered migration rows against the runtime", () => {
|
||||
expect(discoverLegacyPages()).toHaveLength(137);
|
||||
expect(report).toEqual({
|
||||
discovered: 137,
|
||||
mapped: 137,
|
||||
verified: 137,
|
||||
removed: 2,
|
||||
unresolved: [],
|
||||
capabilityGaps: [],
|
||||
handlerGaps: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps every retained target under /ase with concrete parity evidence", () => {
|
||||
for (const row of HOUSEKEEPING_MIGRATION_MATRIX) {
|
||||
if (row.decision === "REMOVE") {
|
||||
expect(row.targetPath, row.legacyPath).toBeNull();
|
||||
expect(row.status, row.legacyPath).toBe("REMOVED");
|
||||
expect(row.parityEvidence, row.legacyPath).toContain(
|
||||
`runtime-parity:${row.legacyPath}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
expect(row.targetPath, row.legacyPath).toMatch(/^\/ase(?:\/|$)/);
|
||||
expect(row.status, row.legacyPath).toBe("VERIFIED");
|
||||
expect(row.parityEvidence, row.legacyPath).toContain(
|
||||
`runtime-parity:${row.legacyPath}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { HousekeepingRegistry } from "../foundation/registry";
|
||||
import type { MigrationEntry } from "../migration/types";
|
||||
import type { HousekeepingRouteHandler } from "../route-handlers";
|
||||
|
||||
export interface HousekeepingParityReport {
|
||||
readonly discovered: 137;
|
||||
readonly mapped: number;
|
||||
readonly verified: number;
|
||||
readonly removed: number;
|
||||
readonly unresolved: readonly string[];
|
||||
readonly capabilityGaps: readonly string[];
|
||||
readonly handlerGaps: readonly string[];
|
||||
}
|
||||
|
||||
function expectedEvidence(row: MigrationEntry): string {
|
||||
return `runtime-parity:${row.legacyPath}`;
|
||||
}
|
||||
|
||||
function capabilityMatches(
|
||||
row: MigrationEntry,
|
||||
route: HousekeepingRegistry["domains"][number]["routes"][number],
|
||||
): boolean {
|
||||
const declared = new Set(row.capabilities.read);
|
||||
if (route.capability.mode === "all") {
|
||||
return route.capability.slugs.every((slug) => declared.has(slug));
|
||||
}
|
||||
return route.capability.slugs.some((slug) => declared.has(slug));
|
||||
}
|
||||
|
||||
export function verifyHousekeepingRuntimeParity(
|
||||
matrix: readonly MigrationEntry[],
|
||||
registry: HousekeepingRegistry,
|
||||
handlers: readonly HousekeepingRouteHandler[],
|
||||
): HousekeepingParityReport {
|
||||
if (matrix.length !== 137) {
|
||||
throw new Error(
|
||||
`Housekeeping parity requires exactly 137 discovered rows; received ${matrix.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
const routes = registry.domains.flatMap((domain) => domain.routes);
|
||||
const routesByHref = new Map<string, (typeof routes)[number]>(
|
||||
routes.map((route) => [route.href, route]),
|
||||
);
|
||||
const routesById = new Map(routes.map((route) => [route.id, route]));
|
||||
const handlersByRouteId = new Map<string, HousekeepingRouteHandler[]>();
|
||||
for (const handler of handlers) {
|
||||
const matching = handlersByRouteId.get(handler.routeId) ?? [];
|
||||
matching.push(handler);
|
||||
handlersByRouteId.set(handler.routeId, matching);
|
||||
}
|
||||
|
||||
const unresolved = new Set<string>();
|
||||
const capabilityGaps = new Set<string>();
|
||||
const handlerGaps = new Set<string>();
|
||||
let mapped = 0;
|
||||
let verified = 0;
|
||||
let removed = 0;
|
||||
|
||||
for (const row of matrix) {
|
||||
let rowMapped = false;
|
||||
let rowCapabilityVerified = true;
|
||||
let rowHandlerVerified = true;
|
||||
|
||||
if (row.decision === "REMOVE") {
|
||||
removed += 1;
|
||||
rowMapped = row.targetPath === null;
|
||||
if (!rowMapped)
|
||||
unresolved.add(`${row.legacyPath}: removed target exists`);
|
||||
} else if (row.targetPath === null) {
|
||||
unresolved.add(`${row.legacyPath}: retained target is missing`);
|
||||
} else {
|
||||
const route = routesByHref.get(row.targetPath);
|
||||
if (!route) {
|
||||
unresolved.add(
|
||||
`${row.legacyPath}: no registered route for ${row.targetPath}`,
|
||||
);
|
||||
} else {
|
||||
rowMapped = true;
|
||||
if (!capabilityMatches(row, route)) {
|
||||
rowCapabilityVerified = false;
|
||||
capabilityGaps.add(
|
||||
`${row.legacyPath}: ${row.targetPath} capability mismatch`,
|
||||
);
|
||||
}
|
||||
|
||||
const matchingHandlers = handlersByRouteId.get(route.id) ?? [];
|
||||
if (matchingHandlers.length !== 1) {
|
||||
rowHandlerVerified = false;
|
||||
handlerGaps.add(
|
||||
`${row.legacyPath}: ${route.id} has ${matchingHandlers.length} handlers`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (rowMapped) mapped += 1;
|
||||
const expectedStatus = row.decision === "REMOVE" ? "REMOVED" : "VERIFIED";
|
||||
if (
|
||||
rowMapped &&
|
||||
rowCapabilityVerified &&
|
||||
rowHandlerVerified &&
|
||||
row.status === expectedStatus &&
|
||||
row.parityEvidence.includes(expectedEvidence(row))
|
||||
) {
|
||||
verified += 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (const route of routes) {
|
||||
const matchingHandlers = handlersByRouteId.get(route.id) ?? [];
|
||||
if (matchingHandlers.length !== 1) {
|
||||
handlerGaps.add(
|
||||
`registry route ${route.id} has ${matchingHandlers.length} handlers`,
|
||||
);
|
||||
}
|
||||
}
|
||||
for (const handler of handlers) {
|
||||
if (!routesById.has(handler.routeId)) {
|
||||
handlerGaps.add(`orphan handler ${handler.routeId}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
discovered: 137,
|
||||
mapped,
|
||||
verified,
|
||||
removed,
|
||||
unresolved: [...unresolved].sort(),
|
||||
capabilityGaps: [...capabilityGaps].sort(),
|
||||
handlerGaps: [...handlerGaps].sort(),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import { readdirSync, readFileSync } from "node:fs";
|
||||
import { dirname, relative, resolve, sep } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const housekeepingRoot = resolve(process.cwd(), "src/features/housekeeping");
|
||||
|
||||
function sourceFiles(directory: string): string[] {
|
||||
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
|
||||
const path = resolve(directory, entry.name);
|
||||
if (entry.isDirectory()) return sourceFiles(path);
|
||||
if (!/\.(?:ts|tsx)$/u.test(entry.name) || /\.test\./u.test(entry.name)) {
|
||||
return [];
|
||||
}
|
||||
return [path];
|
||||
});
|
||||
}
|
||||
|
||||
function moduleSpecifiers(source: string): string[] {
|
||||
return [
|
||||
...source.matchAll(
|
||||
/(?:from\s+|import\s*\(|import\s+|require\s*\()\s*["']([^"']+)["']/gu,
|
||||
),
|
||||
].flatMap((match) => (match[1] ? [match[1]] : []));
|
||||
}
|
||||
|
||||
function resolveHousekeepingImport(
|
||||
file: string,
|
||||
specifier: string,
|
||||
): string | null {
|
||||
if (specifier.startsWith(".")) {
|
||||
return resolve(dirname(file), specifier).replaceAll("\\", "/");
|
||||
}
|
||||
const prefix = "@/features/housekeeping/";
|
||||
if (specifier.startsWith(prefix)) {
|
||||
return resolve(housekeepingRoot, specifier.slice(prefix.length)).replaceAll(
|
||||
"\\",
|
||||
"/",
|
||||
);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function displayPath(path: string): string {
|
||||
return relative(process.cwd(), path).split(sep).join("/");
|
||||
}
|
||||
|
||||
describe("Housekeeping source boundaries", () => {
|
||||
it("keeps foundation independent from domain internals, database, and actions", () => {
|
||||
const violations = sourceFiles(resolve(housekeepingRoot, "foundation"))
|
||||
.flatMap((file) =>
|
||||
moduleSpecifiers(readFileSync(file, "utf8")).flatMap((specifier) => {
|
||||
const resolved = resolveHousekeepingImport(file, specifier);
|
||||
const importsDomain =
|
||||
resolved?.includes("/features/housekeeping/domains/") === true;
|
||||
const importsInfrastructure =
|
||||
specifier === "@/db" ||
|
||||
specifier.startsWith("@/db/") ||
|
||||
specifier === "@/actions" ||
|
||||
specifier.startsWith("@/actions/");
|
||||
return importsDomain || importsInfrastructure
|
||||
? [`${displayPath(file)} -> ${specifier}`]
|
||||
: [];
|
||||
}),
|
||||
)
|
||||
.sort();
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
|
||||
it("prevents one domain from importing another domain's internals", () => {
|
||||
const domainsRoot = resolve(housekeepingRoot, "domains");
|
||||
const violations = sourceFiles(domainsRoot)
|
||||
.flatMap((file) => {
|
||||
const owner = relative(domainsRoot, file).split(sep)[0];
|
||||
return moduleSpecifiers(readFileSync(file, "utf8")).flatMap(
|
||||
(specifier) => {
|
||||
const resolved = resolveHousekeepingImport(file, specifier);
|
||||
if (!resolved) return [];
|
||||
const marker = "/features/housekeeping/domains/";
|
||||
const offset = resolved.indexOf(marker);
|
||||
if (offset < 0) return [];
|
||||
const target = resolved.slice(offset + marker.length).split("/")[0];
|
||||
return target && target !== owner
|
||||
? [`${displayPath(file)} -> ${specifier}`]
|
||||
: [];
|
||||
},
|
||||
);
|
||||
})
|
||||
.sort();
|
||||
|
||||
expect(violations).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user