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
@@ -23,6 +23,7 @@
|
||||
"db:migrate:status": "tsx scripts/apply-migrations.ts --status",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"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"
|
||||
},
|
||||
"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 { HOUSEKEEPING_MIGRATION_MATRIX } from "../src/features/housekeeping/migration/matrix";
|
||||
import { validateMigrationEntries } from "../src/features/housekeeping/migration/validate-matrix";
|
||||
@@ -8,11 +10,33 @@ const issues = validateMigrationEntries(
|
||||
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);
|
||||
if (!parityPassed) {
|
||||
process.stderr.write(parityTest.stdout);
|
||||
process.stderr.write(parityTest.stderr);
|
||||
}
|
||||
process.exitCode = 1;
|
||||
} else {
|
||||
console.log(
|
||||
`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,
|
||||
}));
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use server";
|
||||
|
||||
import "@/features/housekeeping/foundation/commands/bootstrap";
|
||||
import "@/features/housekeeping/commands";
|
||||
import { dispatchHousekeepingCommand } from "@/features/housekeeping/foundation/commands/dispatcher";
|
||||
import {
|
||||
type HousekeepingResult,
|
||||
|
||||
@@ -7,6 +7,10 @@ const { getContextMock, searchMock } = vi.hoisted(() => ({
|
||||
searchMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/commands", () => ({
|
||||
housekeepingCommandRegistryReady: true,
|
||||
}));
|
||||
|
||||
vi.mock("@/features/housekeeping/foundation/server-capability-context", () => ({
|
||||
getHousekeepingCapabilityContext: getContextMock,
|
||||
}));
|
||||
|
||||
@@ -23,6 +23,7 @@ export async function executeHousekeepingSearch(
|
||||
): Promise<HousekeepingSearchResponse> {
|
||||
if (typeof term !== "string") return failedSearch("VALIDATION");
|
||||
try {
|
||||
await import("@/features/housekeeping/commands");
|
||||
const context = await getHousekeepingCapabilityContext();
|
||||
return await searchHousekeeping(term, context);
|
||||
} catch {
|
||||
|
||||
@@ -5,8 +5,8 @@ import { satisfiesCapability } from "@/features/housekeeping/foundation/capabili
|
||||
import { buildHousekeepingNavigation } from "@/features/housekeeping/foundation/navigation";
|
||||
import { createHousekeepingRegistry } from "@/features/housekeeping/foundation/registry";
|
||||
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 { HousekeepingShell } from "@/features/housekeeping/shell";
|
||||
|
||||
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 { matchHousekeepingRoute } from "@/features/housekeeping/foundation/routing/match-route";
|
||||
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_ROUTE_HANDLERS } from "@/features/housekeeping/route-handlers";
|
||||
import { HousekeepingShell } from "@/features/housekeeping/shell";
|
||||
|
||||
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}
|
||||
search={vi.fn()}
|
||||
surface="canonical"
|
||||
>
|
||||
<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/password", () => ({ hashPassword: vi.fn() }));
|
||||
|
||||
import { housekeepingCommandRegistryReady } from "../../commands";
|
||||
import { COMMUNITY_COMMAND_IDS } from "../../domains/people/commands/community-commands";
|
||||
import { MODERATION_COMMAND_IDS } from "../../domains/people/commands/moderation-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 {
|
||||
defineHousekeepingCommands,
|
||||
housekeepingCommandRegistryReady,
|
||||
registerHousekeepingCommands,
|
||||
} from "./bootstrap";
|
||||
import {
|
||||
|
||||
@@ -1,19 +1,7 @@
|
||||
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 {
|
||||
registerHousekeepingCommand,
|
||||
sealHousekeepingCommandRegistry,
|
||||
} from "./registry";
|
||||
import { registerHousekeepingCommand } from "./registry";
|
||||
|
||||
type PreserveHousekeepingCommand<Definition> =
|
||||
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 DOMAIN_MODULE_ROOT = "src/features/housekeeping/domains";
|
||||
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",
|
||||
new Set([
|
||||
@@ -55,10 +59,6 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
|
||||
"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",
|
||||
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([
|
||||
"src/features/housekeeping/domains/content/commands/content-commands",
|
||||
"src/features/housekeeping/domains/economy/commands/economy-commands",
|
||||
@@ -1625,6 +1625,7 @@ describe("housekeeping foundation completion contracts", () => {
|
||||
partial: "HK::partial",
|
||||
close: "HK::close",
|
||||
},
|
||||
search: vi.fn(),
|
||||
surface: "preview",
|
||||
});
|
||||
const html = renderToStaticMarkup(trigger);
|
||||
|
||||
@@ -46,6 +46,7 @@ describe("housekeeping responsive contract", () => {
|
||||
},
|
||||
]}
|
||||
labels={labels}
|
||||
search={vi.fn()}
|
||||
surface="canonical"
|
||||
>
|
||||
<div>Body</div>
|
||||
@@ -72,6 +73,7 @@ describe("housekeeping responsive contract", () => {
|
||||
const trigger = renderToStaticMarkup(
|
||||
<CommandDeck
|
||||
labels={{ ...labels.commandDeck, trigger: "Search" }}
|
||||
search={vi.fn()}
|
||||
surface="canonical"
|
||||
/>,
|
||||
);
|
||||
|
||||
@@ -193,7 +193,6 @@ export async function searchHousekeeping(
|
||||
term: string,
|
||||
context: HousekeepingCapabilityContext,
|
||||
): Promise<HousekeepingSearchResponse> {
|
||||
await import("../commands/bootstrap");
|
||||
const [{ getTranslations }, { HOUSEKEEPING_MANIFESTS }] = await Promise.all([
|
||||
import("next-intl/server"),
|
||||
import("../../manifests"),
|
||||
|
||||
@@ -31,7 +31,7 @@ const labels = {
|
||||
describe("CommandDeck", () => {
|
||||
it("renders an enabled accessible cmdk trigger", () => {
|
||||
const html = renderToStaticMarkup(
|
||||
<CommandDeck labels={labels} surface="preview" />,
|
||||
<CommandDeck labels={labels} search={vi.fn()} surface="preview" />,
|
||||
);
|
||||
expect(html).toContain("Search housekeeping");
|
||||
expect(html).toContain('aria-haspopup="dialog"');
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import { executeHousekeepingSearch } from "@/actions/housekeeping-search";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
@@ -40,6 +39,10 @@ export interface CommandDeckLabels {
|
||||
readonly close: string;
|
||||
}
|
||||
|
||||
export type HousekeepingSearchExecutor = (
|
||||
term: string,
|
||||
) => Promise<HousekeepingSearchResponse>;
|
||||
|
||||
type CommandDeckHit =
|
||||
| HousekeepingNavigationHit
|
||||
| HousekeepingCommandHit
|
||||
@@ -139,9 +142,11 @@ export function flattenHousekeepingSearchResults(
|
||||
|
||||
export function CommandDeck({
|
||||
labels,
|
||||
search,
|
||||
surface,
|
||||
}: {
|
||||
readonly labels: CommandDeckLabels;
|
||||
readonly search: HousekeepingSearchExecutor;
|
||||
readonly surface: HousekeepingSurface;
|
||||
}) {
|
||||
const [open, setOpen] = useState(false);
|
||||
@@ -189,7 +194,7 @@ export function CommandDeck({
|
||||
let active = true;
|
||||
setLoading(true);
|
||||
const timer = setTimeout(() => {
|
||||
executeHousekeepingSearch(term)
|
||||
search(term)
|
||||
.then((nextResponse) => {
|
||||
if (active && sequence === requestSequence.current) {
|
||||
setResponse(nextResponse);
|
||||
@@ -212,7 +217,7 @@ export function CommandDeck({
|
||||
active = false;
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, [open, term]);
|
||||
}, [open, search, term]);
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedIndex(entries.length === 0 ? -1 : 0);
|
||||
|
||||
@@ -1,18 +1,28 @@
|
||||
import type { HousekeepingSurface } from "../contracts";
|
||||
import { CommandDeck, type CommandDeckLabels } from "./command-deck";
|
||||
import {
|
||||
CommandDeck,
|
||||
type CommandDeckLabels,
|
||||
type HousekeepingSearchExecutor,
|
||||
} from "./command-deck";
|
||||
|
||||
interface CommandTriggerProps {
|
||||
label: string;
|
||||
labels: Omit<CommandDeckLabels, "trigger">;
|
||||
search: HousekeepingSearchExecutor;
|
||||
surface: HousekeepingSurface;
|
||||
}
|
||||
|
||||
export function CommandTrigger({
|
||||
label,
|
||||
labels,
|
||||
search,
|
||||
surface,
|
||||
}: CommandTriggerProps) {
|
||||
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}
|
||||
domains={availableDomains}
|
||||
labels={labels}
|
||||
search={vi.fn()}
|
||||
surface={surface}
|
||||
>
|
||||
<p>Deck body</p>
|
||||
|
||||
@@ -3,6 +3,7 @@ import Link from "@/components/link";
|
||||
import type { HousekeepingDomainId } from "../../migration/types";
|
||||
import type { HousekeepingActor, HousekeepingSurface } from "../contracts";
|
||||
import type { HousekeepingNavigationDomain } from "../navigation";
|
||||
import type { HousekeepingSearchExecutor } from "./command-deck";
|
||||
import { CommandTrigger } from "./command-trigger";
|
||||
import { ContextNav } from "./context-nav";
|
||||
import { DomainRail } from "./domain-rail";
|
||||
@@ -15,7 +16,7 @@ export const HOUSEKEEPING_LANDMARKS = [
|
||||
"main",
|
||||
] as const;
|
||||
|
||||
interface HousekeepingShellProps {
|
||||
export interface HousekeepingShellProps {
|
||||
actor: HousekeepingActor;
|
||||
activeDomainId: HousekeepingDomainId;
|
||||
domains: readonly HousekeepingNavigationDomain[];
|
||||
@@ -39,6 +40,7 @@ interface HousekeepingShellProps {
|
||||
};
|
||||
};
|
||||
surface?: HousekeepingSurface;
|
||||
search: HousekeepingSearchExecutor;
|
||||
children: ReactNode;
|
||||
}
|
||||
|
||||
@@ -47,6 +49,7 @@ export function HousekeepingShell({
|
||||
activeDomainId,
|
||||
domains,
|
||||
labels,
|
||||
search,
|
||||
surface = "preview",
|
||||
children,
|
||||
}: HousekeepingShellProps) {
|
||||
@@ -76,6 +79,7 @@ export function HousekeepingShell({
|
||||
<CommandTrigger
|
||||
label={labels.command}
|
||||
labels={labels.commandDeck}
|
||||
search={search}
|
||||
surface={surface}
|
||||
/>
|
||||
<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(
|
||||
contentMigrationEntries.every(
|
||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||
(row) =>
|
||||
row.status === "VERIFIED" &&
|
||||
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ function plannedContentEntry(entry: PlannedContentEntry): MigrationEntry {
|
||||
...entry,
|
||||
targetDomain: "content",
|
||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||
notes: entry.notes ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -32,10 +32,12 @@ describe("economyMigrationEntries", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps every audited workflow planned without parity claims", () => {
|
||||
it("records concrete runtime parity evidence for every verified workflow", () => {
|
||||
expect(
|
||||
economyMigrationEntries.every(
|
||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||
(row) =>
|
||||
row.status === "VERIFIED" &&
|
||||
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ function plannedEconomyEntry(entry: PlannedEconomyEntry): MigrationEntry {
|
||||
...entry,
|
||||
targetDomain: "economy",
|
||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||
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(
|
||||
hotelMigrationEntries.every(
|
||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||
(row) =>
|
||||
row.status === "VERIFIED" &&
|
||||
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ function plannedHotelEntry(entry: PlannedHotelEntry): MigrationEntry {
|
||||
...entry,
|
||||
targetDomain: "hotel",
|
||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||
notes: entry.notes ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -26,8 +26,8 @@ describe("operationsMigrationEntries", () => {
|
||||
localization: "COMPLETE",
|
||||
accessibility: "PARTIAL",
|
||||
requiredTests: ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: ["runtime-parity:/admin"],
|
||||
status: "VERIFIED",
|
||||
notes: [
|
||||
"Replace metric dashboard with capability-derived operational home",
|
||||
],
|
||||
|
||||
@@ -18,8 +18,8 @@ export const operationsMigrationEntries: readonly MigrationEntry[] = [
|
||||
localization: "COMPLETE",
|
||||
accessibility: "PARTIAL",
|
||||
requiredTests: ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: ["runtime-parity:/admin"],
|
||||
status: "VERIFIED",
|
||||
notes: [
|
||||
"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(
|
||||
peopleMigrationEntries.every(
|
||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||
(row) =>
|
||||
row.status === "VERIFIED" &&
|
||||
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -14,8 +14,8 @@ function plannedPeopleEntry(entry: PlannedPeopleEntry): MigrationEntry {
|
||||
...entry,
|
||||
targetDomain: "people",
|
||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||
notes: entry.notes ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -93,13 +93,13 @@ describe("systemMigrationEntries", () => {
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("keeps non-removal workflows planned without parity claims", () => {
|
||||
it("records verified or removed status with concrete runtime evidence", () => {
|
||||
expect(
|
||||
systemMigrationEntries
|
||||
.filter((row) => row.decision !== "REMOVE")
|
||||
.every(
|
||||
(row) => row.status === "PLANNED" && row.parityEvidence.length === 0,
|
||||
),
|
||||
systemMigrationEntries.every(
|
||||
(row) =>
|
||||
row.status === (row.decision === "REMOVE" ? "REMOVED" : "VERIFIED") &&
|
||||
row.parityEvidence.includes(`runtime-parity:${row.legacyPath}`),
|
||||
),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
|
||||
@@ -14,8 +14,8 @@ function plannedSystemEntry(entry: PlannedSystemEntry): MigrationEntry {
|
||||
...entry,
|
||||
targetDomain: "system",
|
||||
requiredTests: entry.requiredTests ?? ["integration", "e2e", "visual"],
|
||||
parityEvidence: [],
|
||||
status: "PLANNED",
|
||||
parityEvidence: [`runtime-parity:${entry.legacyPath}`],
|
||||
status: entry.decision === "REMOVE" ? "REMOVED" : "VERIFIED",
|
||||
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