feat(housekeeping): compose operations workspace
CI / check (pull_request) Successful in 37s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-30 17:23:55 +02:00
1 parent 5574e601bb
commit 85ad0452d3
22 files changed
+1240 -87

No files matched your search

@@ -7,6 +7,7 @@ import { describe, expect, it, vi } from "vitest";
import { CONTENT_ROUTE_IDS } from "../domains/content/routes";
import { ECONOMY_ROUTE_IDS } from "../domains/economy/routes";
import { HOTEL_ROUTE_IDS } from "../domains/hotel/routes";
import { OPERATIONS_ROUTES } from "../domains/operations/routes";
import { PEOPLE_ROUTE_IDS } from "../domains/people/routes";
import { SYSTEM_ROUTE_IDS } from "../domains/system/routes";
import { HOUSEKEEPING_MANIFESTS } from "../manifests";
@@ -27,6 +28,33 @@ 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/domains/operations/manifest.ts",
new Set([
"src/features/housekeeping/domains/operations/inbox",
"src/features/housekeeping/domains/operations/routes",
"src/features/housekeeping/domains/operations/search",
"src/features/housekeeping/domains/operations/widgets",
]),
],
[
"src/features/housekeeping/domains/operations/pages/personalization.tsx",
new Set(["src/actions/housekeeping-preferences"]),
],
[
"src/features/housekeeping/domains/operations/pages/workspace.tsx",
new Set([
"src/features/housekeeping/domains/operations/pages/personalization",
"src/features/housekeeping/domains/operations/queries",
]),
],
[
"src/features/housekeeping/domains/operations/route-handlers.ts",
new Set([
"src/features/housekeeping/domains/operations/pages/workspace",
"src/features/housekeeping/domains/operations/queries",
]),
],
[
"src/features/housekeeping/foundation/shell/command-deck.tsx",
new Set(["src/actions/housekeeping-search"]),
@@ -923,6 +951,7 @@ const approvedRuntimeImports = new Map<string, ReadonlySet<string>>([
"src/features/housekeeping/domains/content/route-handlers",
"src/features/housekeeping/domains/economy/route-handlers",
"src/features/housekeeping/domains/hotel/route-handlers",
"src/features/housekeeping/domains/operations/route-handlers",
"src/features/housekeeping/domains/people/route-handlers",
"src/features/housekeeping/domains/system/route-handlers",
]),
@@ -1523,7 +1552,7 @@ describe("housekeeping foundation completion contracts", () => {
}
});
it("creates the real six-domain registry with People, Content, Economy, Hotel, and System routes enabled", () => {
it("creates the real six-domain registry with every domain route enabled", () => {
const registry = createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS);
expect(registry.domains.map((domain) => domain.id)).toEqual([
@@ -1536,14 +1565,9 @@ describe("housekeeping foundation completion contracts", () => {
]);
expect(
registry.domains
.filter(
(domain) =>
!["people", "content", "economy", "hotel", "system"].includes(
domain.id,
),
)
.every((domain) => domain.routes.length === 0),
).toBe(true);
.find((domain) => domain.id === "operations")
?.routes.map((route) => route.id),
).toEqual(OPERATIONS_ROUTES.map((route) => route.id));
expect(
registry.domains
.find((domain) => domain.id === "people")
@@ -148,6 +148,7 @@ const routeMocks = vi.hoisted(() => {
"routes.hotel.studio.repair-icons": "HK::hotel-studio-repair-icons",
"routes.hotel.studio.sync": "HK::hotel-studio-sync",
"routes.hotel.studio.upload": "HK::hotel-studio-upload",
"routes.operations.workspace": "HK::operations-workspace",
"states.empty.title": "Localized empty title",
"states.empty.description": "Localized empty description",
};
@@ -276,6 +277,10 @@ vi.mock("@/features/housekeeping/manifests", async (importOriginal) => {
});
vi.mock("@/features/housekeeping/route-handlers", () => ({
HOUSEKEEPING_ROUTE_HANDLERS: [
{
routeId: "operations.workspace",
render: routeMocks.renderHousekeepingRoute,
},
{ routeId: "people.users", render: routeMocks.renderHousekeepingRoute },
{
routeId: "people.user-detail",
@@ -617,24 +622,30 @@ describe("/ase-next preview gate", () => {
);
});
describe("/ase-next first visible domain", () => {
describe("/ase-next operational home", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("redirects an administrator to the first permitted registered route", async () => {
it("renders the Operations workspace directly for an administrator", async () => {
routeMocks.getHousekeepingCapabilityContext.mockResolvedValue(
capabilityContext([PERMS.ADMIN_DASHBOARD, PERMS.USERS_VIEW]),
);
await expect(AdminNextPage()).rejects.toThrow(
"NEXT_REDIRECT:/ase-next/people/users",
const html = await renderRoute(AdminNextPage());
expect(html).toContain("Rendered operations.workspace");
expect(html).toContain("HK::operations-workspace");
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith(
expect.objectContaining({
match: expect.objectContaining({ routeId: "operations.workspace" }),
surface: "preview",
}),
);
expect(routeMocks.redirect).toHaveBeenCalledWith("/ase-next/people/users");
expect(routeMocks.redirect).not.toHaveBeenCalled();
expect(routeMocks.getHousekeepingCapabilityContext).toHaveBeenCalledTimes(
1,
);
expect(routeMocks.getTranslations).not.toHaveBeenCalled();
expect(routeMocks.getTranslations).toHaveBeenCalledTimes(1);
});
it("redirects a moderator with only an approved mod view capability to People", async () => {
@@ -790,6 +801,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledTimes(1);
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.users",
domain: "people",
@@ -815,6 +827,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.user-detail",
domain: "people",
@@ -839,6 +852,7 @@ describe("/ase-next/[domain]/[[...segments]] page", () => {
expect(routeMocks.renderHousekeepingRoute).toHaveBeenCalledWith({
context,
surface: "preview",
match: {
routeId: "people.literal-tool",
domain: "people",
@@ -12,6 +12,10 @@ import { HOTEL_INBOX_SOURCES } from "../domains/hotel/inbox";
import { HOTEL_ROUTES } from "../domains/hotel/routes";
import { HOTEL_SEARCH_PROVIDERS } from "../domains/hotel/search";
import { HOTEL_WIDGETS } from "../domains/hotel/widgets";
import { OPERATIONS_INBOX_SOURCES } from "../domains/operations/inbox";
import { OPERATIONS_ROUTES } from "../domains/operations/routes";
import { OPERATIONS_SEARCH_PROVIDERS } from "../domains/operations/search";
import { OPERATIONS_WIDGETS } from "../domains/operations/widgets";
import { PEOPLE_INBOX_SOURCES } from "../domains/people/inbox";
import { PEOPLE_ROUTES } from "../domains/people/routes";
import { PEOPLE_SEARCH_PROVIDERS } from "../domains/people/search";
@@ -380,56 +384,64 @@ describe("housekeeping registry", () => {
descriptionKey: expected.descriptionKey,
});
expect(actual.routes).toEqual(
expected.id === "people"
? PEOPLE_ROUTES
: expected.id === "content"
? CONTENT_ROUTES
: expected.id === "economy"
? ECONOMY_ROUTES
: expected.id === "hotel"
? HOTEL_ROUTES
: expected.id === "system"
? SYSTEM_ROUTES
: [],
expected.id === "operations"
? OPERATIONS_ROUTES
: expected.id === "people"
? PEOPLE_ROUTES
: expected.id === "content"
? CONTENT_ROUTES
: expected.id === "economy"
? ECONOMY_ROUTES
: expected.id === "hotel"
? HOTEL_ROUTES
: expected.id === "system"
? SYSTEM_ROUTES
: [],
);
expect(actual.searchProviders).toEqual(
expected.id === "people"
? PEOPLE_SEARCH_PROVIDERS
: expected.id === "content"
? CONTENT_SEARCH_PROVIDERS
: expected.id === "economy"
? ECONOMY_SEARCH_PROVIDERS
: expected.id === "hotel"
? HOTEL_SEARCH_PROVIDERS
: expected.id === "system"
? SYSTEM_SEARCH_PROVIDERS
: [],
expected.id === "operations"
? OPERATIONS_SEARCH_PROVIDERS
: expected.id === "people"
? PEOPLE_SEARCH_PROVIDERS
: expected.id === "content"
? CONTENT_SEARCH_PROVIDERS
: expected.id === "economy"
? ECONOMY_SEARCH_PROVIDERS
: expected.id === "hotel"
? HOTEL_SEARCH_PROVIDERS
: expected.id === "system"
? SYSTEM_SEARCH_PROVIDERS
: [],
);
expect(actual.inboxSources).toEqual(
expected.id === "people"
? PEOPLE_INBOX_SOURCES
: expected.id === "content"
? CONTENT_INBOX_SOURCES
: expected.id === "economy"
? ECONOMY_INBOX_SOURCES
: expected.id === "hotel"
? HOTEL_INBOX_SOURCES
: expected.id === "system"
? SYSTEM_INBOX_SOURCES
: [],
expected.id === "operations"
? OPERATIONS_INBOX_SOURCES
: expected.id === "people"
? PEOPLE_INBOX_SOURCES
: expected.id === "content"
? CONTENT_INBOX_SOURCES
: expected.id === "economy"
? ECONOMY_INBOX_SOURCES
: expected.id === "hotel"
? HOTEL_INBOX_SOURCES
: expected.id === "system"
? SYSTEM_INBOX_SOURCES
: [],
);
expect(actual.widgets).toEqual(
expected.id === "people"
? PEOPLE_WIDGETS
: expected.id === "content"
? CONTENT_WIDGETS
: expected.id === "economy"
? ECONOMY_WIDGETS
: expected.id === "hotel"
? HOTEL_WIDGETS
: expected.id === "system"
? SYSTEM_WIDGETS
: [],
expected.id === "operations"
? OPERATIONS_WIDGETS
: expected.id === "people"
? PEOPLE_WIDGETS
: expected.id === "content"
? CONTENT_WIDGETS
: expected.id === "economy"
? ECONOMY_WIDGETS
: expected.id === "hotel"
? HOTEL_WIDGETS
: expected.id === "system"
? SYSTEM_WIDGETS
: [],
);
expect(actual.capability).toEqual({ mode: "any", slugs: expected.slugs });
}
@@ -40,7 +40,10 @@ const {
};
});
vi.mock("react", () => ({ cache: wrapCache }));
vi.mock("react", async (importOriginal) => ({
...(await importOriginal<typeof import("react")>()),
cache: wrapCache,
}));
vi.mock("next/navigation", () => ({ notFound, redirect }));
vi.mock("next-intl/server", () => ({ getTranslations }));
vi.mock("@/lib/auth", () => ({ auth }));
@@ -74,17 +77,17 @@ function adminContext(
};
}
async function invokeRequestConsumers(expectedPageError = "NEXT_NOT_FOUND") {
async function invokeRequestConsumers() {
const shell = await AdminNextDomainLayout({
children: null,
params: Promise.resolve({ domain: "operations" }),
});
await expect(AdminNextPage()).rejects.toThrow(expectedPageError);
const page = await AdminNextPage();
const commandContext = await requireHousekeepingCapability(
anyCapability("admin.dashboard"),
);
return { commandContext, shell };
return { commandContext, page, shell };
}
describe("getHousekeepingCapabilityContext", () => {
@@ -102,11 +105,10 @@ describe("getHousekeepingCapabilityContext", () => {
});
it("isolates real shell, page, and command preflight consumers between logical requests", async () => {
const first = await invokeRequestConsumers(
"NEXT_REDIRECT:/ase-next/people/users",
);
const first = await invokeRequestConsumers();
expect(first.shell).toBeDefined();
expect(first.page).toBeDefined();
expect(first.commandContext.actor).toEqual({
id: 42,
username: "operator",
@@ -123,6 +125,7 @@ describe("getHousekeepingCapabilityContext", () => {
const second = await invokeRequestConsumers();
expect(second.shell).toBeDefined();
expect(second.page).toBeDefined();
expect(second.commandContext.actor).toEqual({
id: 84,
username: "second-operator",
@@ -2,6 +2,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it, vi } from "vitest";
import type { HousekeepingSurface } from "../contracts";
import type { HousekeepingNavigationDomain } from "../navigation";
import { HousekeepingShell } from "./housekeeping-shell";
@@ -147,6 +148,7 @@ const componentBoundaryRules = [
function renderShell(
activeDomainId: HousekeepingNavigationDomain["id"] = "operations",
availableDomains: readonly HousekeepingNavigationDomain[] = domains,
surface: HousekeepingSurface = "preview",
): string {
return renderToStaticMarkup(
<HousekeepingShell
@@ -154,6 +156,7 @@ function renderShell(
activeDomainId={activeDomainId}
domains={availableDomains}
labels={labels}
surface={surface}
>
<p>Deck body</p>
</HousekeepingShell>,
@@ -230,6 +233,13 @@ describe("HousekeepingShell", () => {
}
});
it("only marks the preview surface as preview", () => {
expect(renderShell()).toContain(">Preview<");
expect(renderShell("operations", domains, "canonical")).not.toContain(
">Preview<",
);
});
it("does not throw or render contextual items when a valid active domain is absent", () => {
const domainsWithoutSystem = domains.filter(
(domain) => domain.id !== "system",
@@ -54,9 +54,11 @@ export function HousekeepingShell({
</a>
<header className="flex flex-wrap items-center justify-between gap-3 border-b border-[var(--admin-border)] bg-[var(--admin-surface)] px-4 py-3">
<div className="flex items-center gap-3">
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
{labels.preview}
</span>
{surface === "preview" ? (
<span className="rounded-full border border-[var(--admin-border)] px-2 py-1 text-xs text-[var(--admin-accent)]">
{labels.preview}
</span>
) : null}
<OperatorSummary actor={actor} />
</div>
<div className="flex items-center gap-2">