refactor(housekeeping): stabilize service contracts

This commit is contained in:
Simo committed 2026-08-26 21:04:42 +02:00
1 parent 67cb4b488d
commit b7edbca043
10 files changed
+145 -28

No files matched your search

@@ -13,6 +13,7 @@ import {
type HousekeepingSearchResult, type HousekeepingSearchResult,
type HousekeepingWidgetDefinition, type HousekeepingWidgetDefinition,
type HousekeepingWorkItem, type HousekeepingWorkItem,
mapUnknownError,
ok, ok,
} from "."; } from ".";
@@ -25,18 +26,32 @@ const workItem = {
domain: "people", domain: "people",
capability, capability,
severity: "warning", severity: "warning",
priority: "high",
ageMs: 60_000,
state: "open",
occurredAt: "2026-08-24T12:00:00.000Z", occurredAt: "2026-08-24T12:00:00.000Z",
titleKey: "pages.housekeeping.items.ticket", titleKey: "pages.housekeeping.items.ticket",
context: { ticketId: "42" }, context: { ticketId: "42" },
href: "/ase-next/people/tickets/42", href: "/ase/people/tickets/42",
freshness: "fresh", freshness: "fresh",
actions: [
{
id: "people.ticket.open",
labelKey: "pages.housekeeping.items.ticket.open",
commandId: "people.ticket.open",
capability,
},
],
} satisfies HousekeepingWorkItem; } satisfies HousekeepingWorkItem;
const searchResult = { const searchResult = {
id: "user-42", id: "user-42",
domain: "people", domain: "people",
type: "entity",
title: "operator", title: "operator",
href: "/ase-next/people/users/42", description: "A Housekeeping operator",
href: "/ase/people/users/42",
capability,
} satisfies HousekeepingSearchResult; } satisfies HousekeepingSearchResult;
const searchProvider: HousekeepingSearchProvider = { const searchProvider: HousekeepingSearchProvider = {
@@ -87,7 +102,7 @@ const manifest: HousekeepingDomainManifest = {
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description", descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users", iconId: "users",
canonicalHref: "/ase-next/people", canonicalHref: "/ase/people",
capability, capability,
routes: [], routes: [],
searchProviders: [searchProvider], searchProviders: [searchProvider],
@@ -96,19 +111,47 @@ const manifest: HousekeepingDomainManifest = {
}; };
describe("housekeeping foundation contracts", () => { describe("housekeeping foundation contracts", () => {
it("creates typed success and error results", () => { it("preserves correlation IDs for successful results", () => {
expect(ok({ count: 2 }, "corr-1")).toEqual({ expect(ok({ count: 2 }, "corr-1")).toEqual({
ok: true, ok: true,
data: { count: 2 }, data: { count: 2 },
correlationId: "corr-1", correlationId: "corr-1",
}); });
expect(fail("CAPABILITY_DENIED", "corr-2")).toEqual({ });
it.each([
"UNAUTHENTICATED",
"FORBIDDEN",
"VALIDATION",
"NOT_FOUND",
"CONFLICT",
"RATE_LIMITED",
"DEPENDENCY_UNAVAILABLE",
"TIMEOUT",
"INTERNAL",
] as const)("returns the approved %s error contract", (code) => {
expect(
fail(code, `errors.housekeeping.${code.toLowerCase()}`, "corr-2", {
name: ["errors.validation.required"],
}),
).toEqual({
ok: false, ok: false,
error: { code: "CAPABILITY_DENIED" }, error: {
code,
messageKey: `errors.housekeeping.${code.toLowerCase()}`,
fieldErrors: { name: ["errors.validation.required"] },
},
correlationId: "corr-2", correlationId: "corr-2",
}); });
}); });
it("sanitizes unknown errors with the stable internal taxonomy", () => {
expect(mapUnknownError(new Error("database password exposed"))).toEqual({
code: "INTERNAL",
messageKey: "errors.housekeeping.internal",
});
});
it("rejects empty capability requirements", () => { it("rejects empty capability requirements", () => {
expect(() => anyCapability()).toThrow("capability requirement is empty"); expect(() => anyCapability()).toThrow("capability requirement is empty");
expect(() => allCapabilities()).toThrow("capability requirement is empty"); expect(() => allCapabilities()).toThrow("capability requirement is empty");
@@ -5,7 +5,7 @@ import type { HousekeepingSearchProvider } from "./search";
import type { HousekeepingWidgetDefinition } from "./widget"; import type { HousekeepingWidgetDefinition } from "./widget";
export type HousekeepingSurface = "preview" | "canonical"; export type HousekeepingSurface = "preview" | "canonical";
export type CanonicalHousekeepingHref = `/ase${string}`; export type CanonicalHousekeepingHref = "/ase" | `/ase/${string}`;
export interface HousekeepingRouteDefinition { export interface HousekeepingRouteDefinition {
id: string; id: string;
labelKey: string; labelKey: string;
@@ -3,12 +3,25 @@ import type {
CapabilityRequirement, CapabilityRequirement,
HousekeepingCapabilityContext, HousekeepingCapabilityContext,
} from "./capability"; } from "./capability";
import type { CanonicalHousekeepingHref } from "./domain";
import type { HousekeepingResult } from "./result"; import type { HousekeepingResult } from "./result";
export type HousekeepingWorkItemSeverity = "info" | "warning" | "critical"; export type HousekeepingWorkItemSeverity = "info" | "warning" | "critical";
export type HousekeepingWorkItemFreshness = "fresh" | "stale"; export type HousekeepingWorkItemFreshness = "fresh" | "stale";
export type HousekeepingWorkItemPriority =
| "low"
| "normal"
| "high"
| "critical";
export type HousekeepingInboxSourceAvailability = "available" | "unavailable"; export type HousekeepingInboxSourceAvailability = "available" | "unavailable";
export interface HousekeepingWorkItemAction {
id: string;
labelKey: string;
commandId: string;
capability: CapabilityRequirement;
}
export interface HousekeepingWorkItem { export interface HousekeepingWorkItem {
sourceId: string; sourceId: string;
itemId: string; itemId: string;
@@ -16,11 +29,15 @@ export interface HousekeepingWorkItem {
domain: HousekeepingDomainId; domain: HousekeepingDomainId;
capability: CapabilityRequirement; capability: CapabilityRequirement;
severity: HousekeepingWorkItemSeverity; severity: HousekeepingWorkItemSeverity;
priority: HousekeepingWorkItemPriority;
ageMs: number;
state: string;
occurredAt: string; occurredAt: string;
titleKey: string; titleKey: string;
context?: Readonly<Record<string, unknown>>; context?: Readonly<Record<string, unknown>>;
href: string; href: CanonicalHousekeepingHref;
freshness: HousekeepingWorkItemFreshness; freshness: HousekeepingWorkItemFreshness;
actions: readonly HousekeepingWorkItemAction[];
} }
export interface HousekeepingInboxSourceResult { export interface HousekeepingInboxSourceResult {
@@ -1,3 +1,4 @@
export { createCorrelationId } from "../correlation";
export { export {
allCapabilities, allCapabilities,
anyCapability, anyCapability,
@@ -17,20 +18,25 @@ export type {
HousekeepingInboxSourceAvailability, HousekeepingInboxSourceAvailability,
HousekeepingInboxSourceResult, HousekeepingInboxSourceResult,
HousekeepingWorkItem, HousekeepingWorkItem,
HousekeepingWorkItemAction,
HousekeepingWorkItemFreshness, HousekeepingWorkItemFreshness,
HousekeepingWorkItemPriority,
HousekeepingWorkItemSeverity, HousekeepingWorkItemSeverity,
} from "./inbox"; } from "./inbox";
export type { HousekeepingQuery } from "./query"; export type { HousekeepingQuery } from "./query";
export { export {
fail, fail,
type HousekeepingError,
type HousekeepingErrorCode, type HousekeepingErrorCode,
type HousekeepingResult, type HousekeepingResult,
mapUnknownError,
ok, ok,
} from "./result"; } from "./result";
export type { export type {
HousekeepingSearchInput, HousekeepingSearchInput,
HousekeepingSearchProvider, HousekeepingSearchProvider,
HousekeepingSearchResult, HousekeepingSearchResult,
HousekeepingSearchResultType,
} from "./search"; } from "./search";
export type { export type {
HousekeepingWidgetDefinition, HousekeepingWidgetDefinition,
@@ -1,20 +1,23 @@
export type HousekeepingErrorCode = export type HousekeepingErrorCode =
| "VALIDATION_FAILED" | "UNAUTHENTICATED"
| "AUTHENTICATION_REQUIRED" | "FORBIDDEN"
| "CAPABILITY_DENIED" | "VALIDATION"
| "NOT_FOUND" | "NOT_FOUND"
| "CONFLICT" | "CONFLICT"
| "RATE_LIMITED"
| "DEPENDENCY_UNAVAILABLE" | "DEPENDENCY_UNAVAILABLE"
| "PARTIAL_RESULT" | "TIMEOUT"
| "INTERNAL_ERROR"; | "INTERNAL";
export interface HousekeepingError {
code: HousekeepingErrorCode;
messageKey: string;
fieldErrors?: Readonly<Record<string, readonly string[]>>;
}
export type HousekeepingResult<T> = export type HousekeepingResult<T> =
| { ok: true; data: T; correlationId: string } | { ok: true; data: T; correlationId: string }
| { | { ok: false; error: HousekeepingError; correlationId: string };
ok: false;
error: { code: HousekeepingErrorCode };
correlationId: string;
};
export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> { export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> {
return { ok: true, data, correlationId }; return { ok: true, data, correlationId };
@@ -22,7 +25,24 @@ export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> {
export function fail( export function fail(
code: HousekeepingErrorCode, code: HousekeepingErrorCode,
messageKey: string,
correlationId: string, correlationId: string,
fieldErrors?: Readonly<Record<string, readonly string[]>>,
): HousekeepingResult<never> { ): HousekeepingResult<never> {
return { ok: false, error: { code }, correlationId }; return {
ok: false,
error: {
code,
messageKey,
...(fieldErrors === undefined ? {} : { fieldErrors }),
},
correlationId,
};
}
export function mapUnknownError(_error: unknown): HousekeepingError {
return {
code: "INTERNAL",
messageKey: "errors.housekeeping.internal",
};
} }
@@ -3,6 +3,7 @@ import type {
CapabilityRequirement, CapabilityRequirement,
HousekeepingCapabilityContext, HousekeepingCapabilityContext,
} from "./capability"; } from "./capability";
import type { CanonicalHousekeepingHref } from "./domain";
import type { HousekeepingResult } from "./result"; import type { HousekeepingResult } from "./result";
export interface HousekeepingSearchInput { export interface HousekeepingSearchInput {
@@ -10,11 +11,16 @@ export interface HousekeepingSearchInput {
limit: number; limit: number;
} }
export type HousekeepingSearchResultType = "navigation" | "entity" | "command";
export interface HousekeepingSearchResult { export interface HousekeepingSearchResult {
id: string; id: string;
domain: HousekeepingDomainId; domain: HousekeepingDomainId;
type: HousekeepingSearchResultType;
title: string; title: string;
href: string; description?: string;
href: CanonicalHousekeepingHref;
capability: CapabilityRequirement;
} }
export interface HousekeepingSearchProvider { export interface HousekeepingSearchProvider {
@@ -14,5 +14,6 @@ export interface HousekeepingWidgetDefinition {
kind: HousekeepingWidgetKind; kind: HousekeepingWidgetKind;
load( load(
context: HousekeepingCapabilityContext, context: HousekeepingCapabilityContext,
signal: AbortSignal,
): Promise<HousekeepingResult<unknown>>; ): Promise<HousekeepingResult<unknown>>;
} }
@@ -0,0 +1,17 @@
import { describe, expect, it } from "vitest";
import { createCorrelationId } from "./correlation";
describe("createCorrelationId", () => {
it("creates UUID-backed identifiers within the audit storage limit", () => {
const correlationId = createCorrelationId();
expect(correlationId).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
);
expect(correlationId.length).toBeLessThanOrEqual(64);
});
it("does not reuse an identifier for separate operations", () => {
expect(createCorrelationId()).not.toBe(createCorrelationId());
});
});
@@ -0,0 +1,3 @@
export function createCorrelationId(): string {
return crypto.randomUUID();
}
@@ -8,6 +8,7 @@ import {
} from "../migration/types"; } from "../migration/types";
import { import {
anyCapability, anyCapability,
type CanonicalHousekeepingHref,
type CapabilityRequirement, type CapabilityRequirement,
type HousekeepingDomainManifest, type HousekeepingDomainManifest,
type HousekeepingInboxSource, type HousekeepingInboxSource,
@@ -232,7 +233,7 @@ describe("housekeeping registry", () => {
{ {
id: "users", id: "users",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`, href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW), capability: anyCapability(PERMS.USERS_VIEW),
}, },
{ {
@@ -253,13 +254,13 @@ describe("housekeeping registry", () => {
{ {
id: "users", id: "users",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`, href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW), capability: anyCapability(PERMS.USERS_VIEW),
}, },
{ {
id: "staff", id: "staff",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`, href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW), capability: anyCapability(PERMS.USERS_VIEW),
}, },
], ],
@@ -272,7 +273,7 @@ describe("housekeeping registry", () => {
const route = { const route = {
id: "users", id: "users",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`, href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW), capability: anyCapability(PERMS.USERS_VIEW),
}; };
@@ -285,7 +286,7 @@ describe("housekeeping registry", () => {
createHousekeepingRegistry([ createHousekeepingRegistry([
{ {
...manifest("people"), ...manifest("people"),
routes: [{ ...route, href: " " as `/ase${string}` }], routes: [{ ...route, href: " " as CanonicalHousekeepingHref }],
}, },
]), ]),
).toThrow("empty route href"); ).toThrow("empty route href");
@@ -484,7 +485,7 @@ describe("housekeeping registry", () => {
{ {
id: "users", id: "users",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`, href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: { mode: "any", slugs: [] }, capability: { mode: "any", slugs: [] },
}, },
], ],
@@ -497,7 +498,7 @@ describe("housekeeping registry", () => {
const route = { const route = {
id: "shared", id: "shared",
labelKey: "pages.housekeeping.domains.people.title", labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/shared" as `/ase${string}`, href: "/ase/shared" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW), capability: anyCapability(PERMS.USERS_VIEW),
}; };
@@ -507,7 +508,10 @@ describe("housekeeping registry", () => {
{ {
...manifest("content"), ...manifest("content"),
routes: [ routes: [
{ ...route, href: "/ase/content/shared" as `/ase${string}` }, {
...route,
href: "/ase/content/shared" as CanonicalHousekeepingHref,
},
], ],
}, },
]), ]),