test(housekeeping): prove 137 route parity
This commit is contained in:
1 parent
b235ce08ff
commit
8a31556d51
36 files changed
+430
-76
No files matched your search
@@ -23,6 +23,7 @@
|
|||||||
"db:migrate:status": "tsx scripts/apply-migrations.ts --status",
|
"db:migrate:status": "tsx scripts/apply-migrations.ts --status",
|
||||||
"db:studio": "drizzle-kit studio",
|
"db:studio": "drizzle-kit studio",
|
||||||
"hk:matrix:check": "tsx scripts/verify-housekeeping-matrix.ts",
|
"hk:matrix:check": "tsx scripts/verify-housekeeping-matrix.ts",
|
||||||
|
"hk:parity:test": "vitest run --coverage.enabled=false src/features/housekeeping/cutover/parity.test.ts",
|
||||||
"test:housekeeping": "vitest run --coverage.enabled=false src/features/housekeeping src/lib/admin-theme-source-audit.test.ts src/lib/admin/authorization-contract.test.ts"
|
"test:housekeeping": "vitest run --coverage.enabled=false src/features/housekeeping src/lib/admin-theme-source-audit.test.ts src/lib/admin/authorization-contract.test.ts"
|
||||||
},
|
},
|
||||||
"lint-staged": {
|
"lint-staged": {
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { spawnSync } from "node:child_process";
|
||||||
|
import { resolve } from "node:path";
|
||||||
import { discoverLegacyPages } from "../src/features/housekeeping/migration/discover-legacy-pages";
|
import { discoverLegacyPages } from "../src/features/housekeeping/migration/discover-legacy-pages";
|
||||||
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../src/features/housekeeping/migration/matrix";
|
import { HOUSEKEEPING_MIGRATION_MATRIX } from "../src/features/housekeeping/migration/matrix";
|
||||||
import { validateMigrationEntries } from "../src/features/housekeeping/migration/validate-matrix";
|
import { validateMigrationEntries } from "../src/features/housekeeping/migration/validate-matrix";
|
||||||
@@ -8,11 +10,33 @@ const issues = validateMigrationEntries(
|
|||||||
HOUSEKEEPING_MIGRATION_MATRIX,
|
HOUSEKEEPING_MIGRATION_MATRIX,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (issues.length > 0) {
|
const parityTest = spawnSync(
|
||||||
|
process.execPath,
|
||||||
|
[
|
||||||
|
resolve(process.cwd(), "node_modules/vitest/vitest.mjs"),
|
||||||
|
"run",
|
||||||
|
"--coverage.enabled=false",
|
||||||
|
"src/features/housekeeping/cutover/parity.test.ts",
|
||||||
|
],
|
||||||
|
{ cwd: process.cwd(), encoding: "utf8" },
|
||||||
|
);
|
||||||
|
const parityPassed = parityTest.status === 0;
|
||||||
|
|
||||||
|
if (issues.length > 0 || !parityPassed) {
|
||||||
for (const issue of issues) console.error(issue);
|
for (const issue of issues) console.error(issue);
|
||||||
|
if (!parityPassed) {
|
||||||
|
process.stderr.write(parityTest.stdout);
|
||||||
|
process.stderr.write(parityTest.stderr);
|
||||||
|
}
|
||||||
process.exitCode = 1;
|
process.exitCode = 1;
|
||||||
} else {
|
} else {
|
||||||
console.log(
|
console.log(
|
||||||
`Housekeeping migration matrix: ${HOUSEKEEPING_MIGRATION_MATRIX.length}/${discovered.length} valid`,
|
`Housekeeping migration matrix: ${HOUSEKEEPING_MIGRATION_MATRIX.length}/${discovered.length} valid`,
|
||||||
);
|
);
|
||||||
|
const removed = HOUSEKEEPING_MIGRATION_MATRIX.filter(
|
||||||
|
(row) => row.status === "REMOVED",
|
||||||
|
).length;
|
||||||
|
console.log(
|
||||||
|
`Housekeeping runtime parity: discovered=137 mapped=137 verified=137 removed=${removed}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,7 @@ vi.mock(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
vi.mock("@/features/housekeeping/foundation/commands/bootstrap", () => ({
|
vi.mock("@/features/housekeeping/commands", () => ({
|
||||||
housekeepingCommandRegistryReady: true,
|
housekeepingCommandRegistryReady: true,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
import "@/features/housekeeping/foundation/commands/bootstrap";
|
import "@/features/housekeeping/commands";
|
||||||
import { dispatchHousekeepingCommand } from "@/features/housekeeping/foundation/commands/dispatcher";
|
import { dispatchHousekeepingCommand } from "@/features/housekeeping/foundation/commands/dispatcher";
|
||||||
import {
|
import {
|
||||||
type HousekeepingResult,
|
type HousekeepingResult,
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ const { getContextMock, searchMock } = vi.hoisted(() => ({
|
|||||||
searchMock: vi.fn(),
|
searchMock: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/features/housekeeping/commands", () => ({
|
||||||
|
housekeepingCommandRegistryReady: true,
|
||||||
|
}));
|
||||||
|
|
||||||
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||||
getHousekeepingCapabilityContext: getContextMock,
|
getHousekeepingCapabilityContext: getContextMock,
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export async function executeHousekeepingSearch(
|
|||||||
): Promise<HousekeepingSearchResponse> {
|
): Promise<HousekeepingSearchResponse> {
|
||||||
if (typeof term !== "string") return failedSearch("VALIDATION");
|
if (typeof term !== "string") return failedSearch("VALIDATION");
|
||||||
try {
|
try {
|
||||||
|
await import("@/features/housekeeping/commands");
|
||||||
const context = await getHousekeepingCapabilityContext();
|
const context = await getHousekeepingCapabilityContext();
|
||||||
return await searchHousekeeping(term, context);
|
return await searchHousekeeping(term, context);
|
||||||
} catch {
|
} catch {
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { satisfiesCapability } from "@/features/housekeeping/foundation/capabili
|
|||||||
import { buildHousekeepingNavigation } from "@/features/housekeeping/foundation/navigation";
|
import { buildHousekeepingNavigation } from "@/features/housekeeping/foundation/navigation";
|
||||||
import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
|
import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
|
||||||
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
||||||
import { HousekeepingShell } from "@/features/housekeeping/foundation/shell/housekeeping-shell";
|
|
||||||
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
||||||
|
import { HousekeepingShell } from "@/features/housekeeping/shell";
|
||||||
|
|
||||||
const MESSAGE_PREFIX = "pages.housekeeping.";
|
const MESSAGE_PREFIX = "pages.housekeeping.";
|
||||||
|
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/r
|
|||||||
import { toHousekeepingHref } from "@/features/housekeeping/foundation/routing/href";
|
import { toHousekeepingHref } from "@/features/housekeeping/foundation/routing/href";
|
||||||
import { matchHousekeepingRoute } from "@/features/housekeeping/foundation/routing/match-route";
|
import { matchHousekeepingRoute } from "@/features/housekeeping/foundation/routing/match-route";
|
||||||
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
import { getHousekeepingCapabilityContext } from "@/features/housekeeping/foundation/server-capability-context";
|
||||||
import { HousekeepingShell } from "@/features/housekeeping/foundation/shell/housekeeping-shell";
|
|
||||||
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
import { HOUSEKEEPING_MANIFESTS } from "@/features/housekeeping/manifests";
|
||||||
import { HOUSEKEEPING_ROUTE_HANDLERS } from "@/features/housekeeping/route-handlers";
|
import { HOUSEKEEPING_ROUTE_HANDLERS } from "@/features/housekeeping/route-handlers";
|
||||||
|
import { HousekeepingShell } from "@/features/housekeeping/shell";
|
||||||
|
|
||||||
const MESSAGE_PREFIX = "pages.housekeeping.";
|
const MESSAGE_PREFIX = "pages.housekeeping.";
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
import "server-only";
|
||||||
|
|
||||||
|
import { CONTENT_COMMANDS } from "./domains/content/commands/content-commands";
|
||||||
|
import { ECONOMY_COMMANDS } from "./domains/economy/commands/economy-commands";
|
||||||
|
import { HOTEL_COMMANDS } from "./domains/hotel/commands/hotel-commands";
|
||||||
|
import { STUDIO_COMMANDS } from "./domains/hotel/commands/studio-commands";
|
||||||
|
import { COMMUNITY_COMMANDS } from "./domains/people/commands/community-commands";
|
||||||
|
import { MODERATION_COMMANDS } from "./domains/people/commands/moderation-commands";
|
||||||
|
import { SUPPORT_COMMANDS } from "./domains/people/commands/support-commands";
|
||||||
|
import { USER_COMMANDS } from "./domains/people/commands/user-commands";
|
||||||
|
import { SYSTEM_COMMANDS } from "./domains/system/commands/system-commands";
|
||||||
|
import {
|
||||||
|
defineHousekeepingCommands,
|
||||||
|
registerHousekeepingCommands,
|
||||||
|
} from "./foundation/commands/bootstrap";
|
||||||
|
import { sealHousekeepingCommandRegistry } from "./foundation/commands/registry";
|
||||||
|
|
||||||
|
const currentHousekeepingCommands = defineHousekeepingCommands(
|
||||||
|
...CONTENT_COMMANDS,
|
||||||
|
...ECONOMY_COMMANDS,
|
||||||
|
...HOTEL_COMMANDS,
|
||||||
|
...STUDIO_COMMANDS,
|
||||||
|
...USER_COMMANDS,
|
||||||
|
...COMMUNITY_COMMANDS,
|
||||||
|
...SUPPORT_COMMANDS,
|
||||||
|
...MODERATION_COMMANDS,
|
||||||
|
...SYSTEM_COMMANDS,
|
||||||
|
);
|
||||||
|
|
||||||
|
registerHousekeepingCommands(currentHousekeepingCommands);
|
||||||
|
sealHousekeepingCommandRegistry();
|
||||||
|
|
||||||
|
export const housekeepingCommandRegistryReady = true;
|
||||||
@@ -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([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -63,6 +63,7 @@ describe("housekeeping accessibility contract", () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
labels={shellLabels}
|
labels={shellLabels}
|
||||||
|
search={vi.fn()}
|
||||||
surface="canonical"
|
surface="canonical"
|
||||||
>
|
>
|
||||||
<HousekeepingPageShell title="Operations" description="Daily work">
|
<HousekeepingPageShell title="Operations" description="Daily work">
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { z } from "zod";
|
|||||||
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
|
vi.mock("@/lib/auth", () => ({ invalidateLoginCache: vi.fn() }));
|
||||||
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
|
vi.mock("@/lib/auth/password", () => ({ hashPassword: vi.fn() }));
|
||||||
|
|
||||||
|
import { housekeepingCommandRegistryReady } from "../../commands";
|
||||||
import { COMMUNITY_COMMAND_IDS } from "../../domains/people/commands/community-commands";
|
import { COMMUNITY_COMMAND_IDS } from "../../domains/people/commands/community-commands";
|
||||||
import { MODERATION_COMMAND_IDS } from "../../domains/people/commands/moderation-commands";
|
import { MODERATION_COMMAND_IDS } from "../../domains/people/commands/moderation-commands";
|
||||||
import { SUPPORT_COMMAND_IDS } from "../../domains/people/commands/support-commands";
|
import { SUPPORT_COMMAND_IDS } from "../../domains/people/commands/support-commands";
|
||||||
@@ -12,7 +13,6 @@ import { SYSTEM_COMMAND_IDS } from "../../domains/system/commands/system-command
|
|||||||
import { anyCapability, ok } from "../contracts";
|
import { anyCapability, ok } from "../contracts";
|
||||||
import {
|
import {
|
||||||
defineHousekeepingCommands,
|
defineHousekeepingCommands,
|
||||||
housekeepingCommandRegistryReady,
|
|
||||||
registerHousekeepingCommands,
|
registerHousekeepingCommands,
|
||||||
} from "./bootstrap";
|
} from "./bootstrap";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -1,19 +1,7 @@
|
|||||||
import "server-only";
|
import "server-only";
|
||||||
|
|
||||||
import { CONTENT_COMMANDS } from "../../domains/content/commands/content-commands";
|
|
||||||
import { ECONOMY_COMMANDS } from "../../domains/economy/commands/economy-commands";
|
|
||||||
import { HOTEL_COMMANDS } from "../../domains/hotel/commands/hotel-commands";
|
|
||||||
import { STUDIO_COMMANDS } from "../../domains/hotel/commands/studio-commands";
|
|
||||||
import { COMMUNITY_COMMANDS } from "../../domains/people/commands/community-commands";
|
|
||||||
import { MODERATION_COMMANDS } from "../../domains/people/commands/moderation-commands";
|
|
||||||
import { SUPPORT_COMMANDS } from "../../domains/people/commands/support-commands";
|
|
||||||
import { USER_COMMANDS } from "../../domains/people/commands/user-commands";
|
|
||||||
import { SYSTEM_COMMANDS } from "../../domains/system/commands/system-commands";
|
|
||||||
import type { HousekeepingCommand } from "./registry";
|
import type { HousekeepingCommand } from "./registry";
|
||||||
import {
|
import { registerHousekeepingCommand } from "./registry";
|
||||||
registerHousekeepingCommand,
|
|
||||||
sealHousekeepingCommandRegistry,
|
|
||||||
} from "./registry";
|
|
||||||
|
|
||||||
type PreserveHousekeepingCommand<Definition> =
|
type PreserveHousekeepingCommand<Definition> =
|
||||||
Definition extends HousekeepingCommand<infer _Input, infer _Output>
|
Definition extends HousekeepingCommand<infer _Input, infer _Output>
|
||||||
@@ -42,20 +30,3 @@ export function registerHousekeepingCommands<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentHousekeepingCommands = defineHousekeepingCommands(
|
|
||||||
...CONTENT_COMMANDS,
|
|
||||||
...ECONOMY_COMMANDS,
|
|
||||||
...HOTEL_COMMANDS,
|
|
||||||
...STUDIO_COMMANDS,
|
|
||||||
...USER_COMMANDS,
|
|
||||||
...COMMUNITY_COMMANDS,
|
|
||||||
...SUPPORT_COMMANDS,
|
|
||||||
...MODERATION_COMMANDS,
|
|
||||||
...SYSTEM_COMMANDS,
|
|
||||||
);
|
|
||||||
|
|
||||||
registerHousekeepingCommands(currentHousekeepingCommands);
|
|
||||||
sealHousekeepingCommandRegistry();
|
|
||||||
|
|
||||||
export const housekeepingCommandRegistryReady = true;
|
|
||||||
@@ -28,6 +28,10 @@ const SERVER_CAPABILITY_CONTEXT =
|
|||||||
const PERMISSIONS_ADAPTER = "src/lib/permissions";
|
const PERMISSIONS_ADAPTER = "src/lib/permissions";
|
||||||
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
|
const DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
|
||||||
const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
||||||
|
[
|
||||||
|
"src/features/housekeeping/shell.tsx",
|
||||||
|
new Set(["src/actions/housekeeping-search"]),
|
||||||
|
],
|
||||||
[
|
[
|
||||||
"src/features/housekeeping/domains/operations/manifest.ts",
|
"src/features/housekeeping/domains/operations/manifest.ts",
|
||||||
new Set([
|
new Set([
|
||||||
@@ -55,10 +59,6 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
|||||||
"src/features/housekeeping/domains/operations/queries",
|
"src/features/housekeeping/domains/operations/queries",
|
||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
|
||||||
"src/features/housekeeping/foundation/shell/command-deck.tsx",
|
|
||||||
new Set(["src/actions/housekeeping-search"]),
|
|
||||||
],
|
|
||||||
[
|
[
|
||||||
"src/features/housekeeping/domains/hotel/commands/studio-commands.ts",
|
"src/features/housekeeping/domains/hotel/commands/studio-commands.ts",
|
||||||
new Set(["src/features/housekeeping/domains/hotel/queries/studio"]),
|
new Set(["src/features/housekeeping/domains/hotel/queries/studio"]),
|
||||||
@@ -932,7 +932,7 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
|||||||
]),
|
]),
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
"src/features/housekeeping/foundation/commands/bootstrap.ts",
|
"src/features/housekeeping/commands.ts",
|
||||||
new Set([
|
new Set([
|
||||||
"src/features/housekeeping/domains/content/commands/content-commands",
|
"src/features/housekeeping/domains/content/commands/content-commands",
|
||||||
"src/features/housekeeping/domains/economy/commands/economy-commands",
|
"src/features/housekeeping/domains/economy/commands/economy-commands",
|
||||||
@@ -1625,6 +1625,7 @@ describe("housekeeping foundation completion contracts", () => {
|
|||||||
partial: "HK::partial",
|
partial: "HK::partial",
|
||||||
close: "HK::close",
|
close: "HK::close",
|
||||||
},
|
},
|
||||||
|
search: vi.fn(),
|
||||||
surface: "preview",
|
surface: "preview",
|
||||||
});
|
});
|
||||||
const html = renderToStaticMarkup(trigger);
|
const html = renderToStaticMarkup(trigger);
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ describe("housekeeping responsive contract", () => {
|
|||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
search={vi.fn()}
|
||||||
surface="canonical"
|
surface="canonical"
|
||||||
>
|
>
|
||||||
<div>Body</div>
|
<div>Body</div>
|
||||||
@@ -72,6 +73,7 @@ describe("housekeeping responsive contract", () => {
|
|||||||
const trigger = renderToStaticMarkup(
|
const trigger = renderToStaticMarkup(
|
||||||
<CommandDeck
|
<CommandDeck
|
||||||
labels={{ ...labels.commandDeck, trigger: "Search" }}
|
labels={{ ...labels.commandDeck, trigger: "Search" }}
|
||||||
|
search={vi.fn()}
|
||||||
surface="canonical"
|
surface="canonical"
|
||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -193,7 +193,6 @@ export async function searchHousekeeping(
|
|||||||
term: string,
|
term: string,
|
||||||
context: HousekeepingCapabilityContext,
|
context: HousekeepingCapabilityContext,
|
||||||
): Promise<HousekeepingSearchResponse> {
|
): Promise<HousekeepingSearchResponse> {
|
||||||
await import("../commands/bootstrap");
|
|
||||||
const [{ getTranslations }, { HOUSEKEEPING_MANIFESTS }] = await Promise.all([
|
const [{ getTranslations }, { HOUSEKEEPING_MANIFESTS }] = await Promise.all([
|
||||||
import("next-intl/server"),
|
import("next-intl/server"),
|
||||||
import("../../manifests"),
|
import("../../manifests"),
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const labels = {
|
|||||||
describe("CommandDeck", () => {
|
describe("CommandDeck", () => {
|
||||||
it("renders an enabled accessible cmdk trigger", () => {
|
it("renders an enabled accessible cmdk trigger", () => {
|
||||||
const html = renderToStaticMarkup(
|
const html = renderToStaticMarkup(
|
||||||
<CommandDeck labels={labels} surface="preview" />,
|
<CommandDeck labels={labels} search={vi.fn()} surface="preview" />,
|
||||||
);
|
);
|
||||||
expect(html).toContain("Search housekeeping");
|
expect(html).toContain("Search housekeeping");
|
||||||
expect(html).toContain('aria-haspopup="dialog"');
|
expect(html).toContain('aria-haspopup="dialog"');
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { executeHousekeepingSearch } from "@/actions/housekeeping-search";
|
|
||||||
import {
|
import {
|
||||||
Command,
|
Command,
|
||||||
CommandEmpty,
|
CommandEmpty,
|
||||||
@@ -40,6 +39,10 @@ export interface CommandDeckLabels {
|
|||||||
readonly close: string;
|
readonly close: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type HousekeepingSearchExecutor = (
|
||||||
|
term: string,
|
||||||
|
) => Promise<HousekeepingSearchResponse>;
|
||||||
|
|
||||||
type CommandDeckHit =
|
type CommandDeckHit =
|
||||||
| HousekeepingNavigationHit
|
| HousekeepingNavigationHit
|
||||||
| HousekeepingCommandHit
|
| HousekeepingCommandHit
|
||||||
@@ -139,9 +142,11 @@ export function flattenHousekeepingSearchResults(
|
|||||||
|
|
||||||
export function CommandDeck({
|
export function CommandDeck({
|
||||||
labels,
|
labels,
|
||||||
|
search,
|
||||||
surface,
|
surface,
|
||||||
}: {
|
}: {
|
||||||
readonly labels: CommandDeckLabels;
|
readonly labels: CommandDeckLabels;
|
||||||
|
readonly search: HousekeepingSearchExecutor;
|
||||||
readonly surface: HousekeepingSurface;
|
readonly surface: HousekeepingSurface;
|
||||||
}) {
|
}) {
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -189,7 +194,7 @@ export function CommandDeck({
|
|||||||
let active = true;
|
let active = true;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
executeHousekeepingSearch(term)
|
search(term)
|
||||||
.then((nextResponse) => {
|
.then((nextResponse) => {
|
||||||
if (active && sequence === requestSequence.current) {
|
if (active && sequence === requestSequence.current) {
|
||||||
setResponse(nextResponse);
|
setResponse(nextResponse);
|
||||||
@@ -212,7 +217,7 @@ export function CommandDeck({
|
|||||||
active = false;
|
active = false;
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
};
|
};
|
||||||
}, [open, term]);
|
}, [open, search, term]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setSelectedIndex(entries.length === 0 ? -1 : 0);
|
setSelectedIndex(entries.length === 0 ? -1 : 0);
|
||||||
|
|||||||
@@ -1,18 +1,28 @@
|
|||||||
import type { HousekeepingSurface } from "../contracts";
|
import type { HousekeepingSurface } from "../contracts";
|
||||||
import { CommandDeck, type CommandDeckLabels } from "./command-deck";
|
import {
|
||||||
|
CommandDeck,
|
||||||
|
type CommandDeckLabels,
|
||||||
|
type HousekeepingSearchExecutor,
|
||||||
|
} from "./command-deck";
|
||||||
|
|
||||||
interface CommandTriggerProps {
|
interface CommandTriggerProps {
|
||||||
label: string;
|
label: string;
|
||||||
labels: Omit<CommandDeckLabels, "trigger">;
|
labels: Omit<CommandDeckLabels, "trigger">;
|
||||||
|
search: HousekeepingSearchExecutor;
|
||||||
surface: HousekeepingSurface;
|
surface: HousekeepingSurface;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CommandTrigger({
|
export function CommandTrigger({
|
||||||
label,
|
label,
|
||||||
labels,
|
labels,
|
||||||
|
search,
|
||||||
surface,
|
surface,
|
||||||
}: CommandTriggerProps) {
|
}: CommandTriggerProps) {
|
||||||
return (
|
return (
|
||||||
<CommandDeck labels={{ trigger: label, ...labels }} surface={surface} />
|
<CommandDeck
|
||||||
|
labels={{ trigger: label, ...labels }}
|
||||||
|
search={search}
|
||||||
|
surface={surface}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -157,6 +157,7 @@ function renderShell(
|
|||||||
activeDomainId={activeDomainId}
|
activeDomainId={activeDomainId}
|
||||||
domains={availableDomains}
|
domains={availableDomains}
|
||||||
labels={labels}
|
labels={labels}
|
||||||
|
search={vi.fn()}
|
||||||
surface={surface}
|
surface={surface}
|
||||||
>
|
>
|
||||||
<p>Deck body</p>
|
<p>Deck body</p>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import Link from "@/components/link";
|
|||||||
import type { HousekeepingDomainId } from "../../migration/types";
|
import type { HousekeepingDomainId } from "../../migration/types";
|
||||||
import type { HousekeepingActor, HousekeepingSurface } from "../contracts";
|
import type { HousekeepingActor, HousekeepingSurface } from "../contracts";
|
||||||
import type { HousekeepingNavigationDomain } from "../navigation";
|
import type { HousekeepingNavigationDomain } from "../navigation";
|
||||||
|
import type { HousekeepingSearchExecutor } from "./command-deck";
|
||||||
import { CommandTrigger } from "./command-trigger";
|
import { CommandTrigger } from "./command-trigger";
|
||||||
import { ContextNav } from "./context-nav";
|
import { ContextNav } from "./context-nav";
|
||||||
import { DomainRail } from "./domain-rail";
|
import { DomainRail } from "./domain-rail";
|
||||||
@@ -15,7 +16,7 @@ export const HOUSEKEEPING_LANDMARKS = [
|
|||||||
"main",
|
"main",
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
interface HousekeepingShellProps {
|
export interface HousekeepingShellProps {
|
||||||
actor: HousekeepingActor;
|
actor: HousekeepingActor;
|
||||||
activeDomainId: HousekeepingDomainId;
|
activeDomainId: HousekeepingDomainId;
|
||||||
domains: readonly HousekeepingNavigationDomain[];
|
domains: readonly HousekeepingNavigationDomain[];
|
||||||
@@ -39,6 +40,7 @@ interface HousekeepingShellProps {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
surface?: HousekeepingSurface;
|
surface?: HousekeepingSurface;
|
||||||
|
search: HousekeepingSearchExecutor;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,6 +49,7 @@ export function HousekeepingShell({
|
|||||||
activeDomainId,
|
activeDomainId,
|
||||||
domains,
|
domains,
|
||||||
labels,
|
labels,
|
||||||
|
search,
|
||||||
surface = "preview",
|
surface = "preview",
|
||||||
children,
|
children,
|
||||||
}: HousekeepingShellProps) {
|
}: HousekeepingShellProps) {
|
||||||
@@ -76,6 +79,7 @@ export function HousekeepingShell({
|
|||||||
<CommandTrigger
|
<CommandTrigger
|
||||||
label={labels.command}
|
label={labels.command}
|
||||||
labels={labels.commandDeck}
|
labels={labels.commandDeck}
|
||||||
|
search={search}
|
||||||
surface={surface}
|
surface={surface}
|
||||||
/>
|
/>
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ describe("contentMigrationEntries", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps every audited workflow planned without parity claims", () => {
|
it("records concrete runtime parity evidence for every verified workflow", () => {
|
||||||
expect(
|
expect(
|
||||||
contentMigrationEntries.every(
|
contentMigrationEntries.every(
|
||||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
(row) =>
|
||||||
|
row.status === "VERIFIED" &&
|
||||||
|
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function plannedContentEntry(entry: PlannedContentEntry): MigrationEntry {
|
|||||||
...entry,
|
...entry,
|
||||||
targetDomain: "content",
|
targetDomain: "content",
|
||||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||||
status: "PLANNED",
|
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||||
notes: entry.notes ?? [],
|
notes: entry.notes ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ describe("economyMigrationEntries", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps every audited workflow planned without parity claims", () => {
|
it("records concrete runtime parity evidence for every verified workflow", () => {
|
||||||
expect(
|
expect(
|
||||||
economyMigrationEntries.every(
|
economyMigrationEntries.every(
|
||||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
(row) =>
|
||||||
|
row.status === "VERIFIED" &&
|
||||||
|
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function plannedEconomyEntry(entry: PlannedEconomyEntry): MigrationEntry {
|
|||||||
...entry,
|
...entry,
|
||||||
targetDomain: "economy",
|
targetDomain: "economy",
|
||||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||||
status: "PLANNED",
|
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||||
notes: entry.notes ?? [],
|
notes: entry.notes ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -65,10 +65,12 @@ describe("hotelMigrationEntries", () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps every audited workflow planned without parity claims", () => {
|
it("records concrete runtime parity evidence for every verified workflow", () => {
|
||||||
expect(
|
expect(
|
||||||
hotelMigrationEntries.every(
|
hotelMigrationEntries.every(
|
||||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
(row) =>
|
||||||
|
row.status === "VERIFIED" &&
|
||||||
|
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function plannedHotelEntry(entry: PlannedHotelEntry): MigrationEntry {
|
|||||||
...entry,
|
...entry,
|
||||||
targetDomain: "hotel",
|
targetDomain: "hotel",
|
||||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||||
status: "PLANNED",
|
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||||
notes: entry.notes ?? [],
|
notes: entry.notes ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,8 +26,8 @@ describe("operationsMigrationEntries", () => {
|
|||||||
localization: "COMPLETE",
|
localization: "COMPLETE",
|
||||||
accessibility: "PARTIAL",
|
accessibility: "PARTIAL",
|
||||||
requiredTests: ["integration", "e2e", "visual"],
|
requiredTests: ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: ["runtime-parity:/admin"],
|
||||||
status: "PLANNED",
|
status: "VERIFIED",
|
||||||
notes: [
|
notes: [
|
||||||
"Replace metric dashboard with capability-derived operational home",
|
"Replace metric dashboard with capability-derived operational home",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ export const operationsMigrationEntries: readonly MigrationEntry[] = [
|
|||||||
localization: "COMPLETE",
|
localization: "COMPLETE",
|
||||||
accessibility: "PARTIAL",
|
accessibility: "PARTIAL",
|
||||||
requiredTests: ["integration", "e2e", "visual"],
|
requiredTests: ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: ["runtime-parity:/admin"],
|
||||||
status: "PLANNED",
|
status: "VERIFIED",
|
||||||
notes: [
|
notes: [
|
||||||
"Replace metric dashboard with capability-derived operational home",
|
"Replace metric dashboard with capability-derived operational home",
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -41,10 +41,12 @@ describe("peopleMigrationEntries", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps planned rows free of unverified parity claims", () => {
|
it("records concrete runtime parity evidence for every verified row", () => {
|
||||||
expect(
|
expect(
|
||||||
peopleMigrationEntries.every(
|
peopleMigrationEntries.every(
|
||||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
(row) =>
|
||||||
|
row.status === "VERIFIED" &&
|
||||||
|
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function plannedPeopleEntry(entry: PlannedPeopleEntry): MigrationEntry {
|
|||||||
...entry,
|
...entry,
|
||||||
targetDomain: "people",
|
targetDomain: "people",
|
||||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||||
status: "PLANNED",
|
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||||
notes: entry.notes ?? [],
|
notes: entry.notes ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,12 +93,12 @@ describe("systemMigrationEntries", () => {
|
|||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps non-removal workflows planned without parity claims", () => {
|
it("records verified or removed status with concrete runtime evidence", () => {
|
||||||
expect(
|
expect(
|
||||||
systemMigrationEntries
|
systemMigrationEntries.every(
|
||||||
.filter((row) => row.decision !== "REMOVE")
|
(row) =>
|
||||||
.every(
|
row.status === (row.decision === "REMOVE" ? "REMOVED" : "VERIFIED") &&
|
||||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||||
),
|
),
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ function plannedSystemEntry(entry: PlannedSystemEntry): MigrationEntry {
|
|||||||
...entry,
|
...entry,
|
||||||
targetDomain: "system",
|
targetDomain: "system",
|
||||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||||
parityEvidence: [],
|
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||||
status: "PLANNED",
|
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||||
notes: entry.notes ?? [],
|
notes: entry.notes ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { executeHousekeepingSearch } from "@/actions/housekeeping-search";
|
||||||
|
import {
|
||||||
|
HousekeepingShell as FoundationHousekeepingShell,
|
||||||
|
type HousekeepingShellProps,
|
||||||
|
} from "./foundation/shell/housekeeping-shell";
|
||||||
|
|
||||||
|
export function HousekeepingShell(
|
||||||
|
props: Omit<HousekeepingShellProps, "search">,
|
||||||
|
) {
|
||||||
|
return (
|
||||||
|
<FoundationHousekeepingShell
|
||||||
|
{...props}
|
||||||
|
search={executeHousekeepingSearch}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in new issue
Block a user