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 HousekeepingWidgetDefinition,
type HousekeepingWorkItem,
mapUnknownError,
ok,
} from ".";
@@ -25,18 +26,32 @@ const workItem = {
domain: "people",
capability,
severity: "warning",
priority: "high",
ageMs: 60_000,
state: "open",
occurredAt: "2026-08-24T12:00:00.000Z",
titleKey: "pages.housekeeping.items.ticket",
context: { ticketId: "42" },
href: "/ase-next/people/tickets/42",
href: "/ase/people/tickets/42",
freshness: "fresh",
actions: [
{
id: "people.ticket.open",
labelKey: "pages.housekeeping.items.ticket.open",
commandId: "people.ticket.open",
capability,
},
],
} satisfies HousekeepingWorkItem;
const searchResult = {
id: "user-42",
domain: "people",
type: "entity",
title: "operator",
href: "/ase-next/people/users/42",
description: "A Housekeeping operator",
href: "/ase/people/users/42",
capability,
} satisfies HousekeepingSearchResult;
const searchProvider: HousekeepingSearchProvider = {
@@ -87,7 +102,7 @@ const manifest: HousekeepingDomainManifest = {
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
canonicalHref: "/ase-next/people",
canonicalHref: "/ase/people",
capability,
routes: [],
searchProviders: [searchProvider],
@@ -96,19 +111,47 @@ const manifest: HousekeepingDomainManifest = {
};
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({
ok: true,
data: { count: 2 },
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,
error: { code: "CAPABILITY_DENIED" },
error: {
code,
messageKey: `errors.housekeeping.${code.toLowerCase()}`,
fieldErrors: { name: ["errors.validation.required"] },
},
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", () => {
expect(() => anyCapability()).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";
export type HousekeepingSurface = "preview" | "canonical";
export type CanonicalHousekeepingHref = `/ase${string}`;
export type CanonicalHousekeepingHref = "/ase" | `/ase/${string}`;
export interface HousekeepingRouteDefinition {
id: string;
labelKey: string;
@@ -3,12 +3,25 @@ import type {
CapabilityRequirement,
HousekeepingCapabilityContext,
} from "./capability";
import type { CanonicalHousekeepingHref } from "./domain";
import type { HousekeepingResult } from "./result";
export type HousekeepingWorkItemSeverity = "info" | "warning" | "critical";
export type HousekeepingWorkItemFreshness = "fresh" | "stale";
export type HousekeepingWorkItemPriority =
| "low"
| "normal"
| "high"
| "critical";
export type HousekeepingInboxSourceAvailability = "available" | "unavailable";
export interface HousekeepingWorkItemAction {
id: string;
labelKey: string;
commandId: string;
capability: CapabilityRequirement;
}
export interface HousekeepingWorkItem {
sourceId: string;
itemId: string;
@@ -16,11 +29,15 @@ export interface HousekeepingWorkItem {
domain: HousekeepingDomainId;
capability: CapabilityRequirement;
severity: HousekeepingWorkItemSeverity;
priority: HousekeepingWorkItemPriority;
ageMs: number;
state: string;
occurredAt: string;
titleKey: string;
context?: Readonly<Record<string, unknown>>;
href: string;
href: CanonicalHousekeepingHref;
freshness: HousekeepingWorkItemFreshness;
actions: readonly HousekeepingWorkItemAction[];
}
export interface HousekeepingInboxSourceResult {
@@ -1,3 +1,4 @@
export { createCorrelationId } from "../correlation";
export {
allCapabilities,
anyCapability,
@@ -17,20 +18,25 @@ export type {
HousekeepingInboxSourceAvailability,
HousekeepingInboxSourceResult,
HousekeepingWorkItem,
HousekeepingWorkItemAction,
HousekeepingWorkItemFreshness,
HousekeepingWorkItemPriority,
HousekeepingWorkItemSeverity,
} from "./inbox";
export type { HousekeepingQuery } from "./query";
export {
fail,
type HousekeepingError,
type HousekeepingErrorCode,
type HousekeepingResult,
mapUnknownError,
ok,
} from "./result";
export type {
HousekeepingSearchInput,
HousekeepingSearchProvider,
HousekeepingSearchResult,
HousekeepingSearchResultType,
} from "./search";
export type {
HousekeepingWidgetDefinition,
@@ -1,20 +1,23 @@
export type HousekeepingErrorCode =
| "VALIDATION_FAILED"
| "AUTHENTICATION_REQUIRED"
| "CAPABILITY_DENIED"
| "UNAUTHENTICATED"
| "FORBIDDEN"
| "VALIDATION"
| "NOT_FOUND"
| "CONFLICT"
| "RATE_LIMITED"
| "DEPENDENCY_UNAVAILABLE"
| "PARTIAL_RESULT"
| "INTERNAL_ERROR";
| "TIMEOUT"
| "INTERNAL";
export interface HousekeepingError {
code: HousekeepingErrorCode;
messageKey: string;
fieldErrors?: Readonly<Record<string, readonly string[]>>;
}
export type HousekeepingResult<T> =
| { ok: true; data: T; correlationId: string }
| {
ok: false;
error: { code: HousekeepingErrorCode };
correlationId: string;
};
| { ok: false; error: HousekeepingError; correlationId: string };
export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> {
return { ok: true, data, correlationId };
@@ -22,7 +25,24 @@ export function ok<T>(data: T, correlationId: string): HousekeepingResult<T> {
export function fail(
code: HousekeepingErrorCode,
messageKey: string,
correlationId: string,
fieldErrors?: Readonly<Record<string, readonly string[]>>,
): 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,
HousekeepingCapabilityContext,
} from "./capability";
import type { CanonicalHousekeepingHref } from "./domain";
import type { HousekeepingResult } from "./result";
export interface HousekeepingSearchInput {
@@ -10,11 +11,16 @@ export interface HousekeepingSearchInput {
limit: number;
}
export type HousekeepingSearchResultType = "navigation" | "entity" | "command";
export interface HousekeepingSearchResult {
id: string;
domain: HousekeepingDomainId;
type: HousekeepingSearchResultType;
title: string;
href: string;
description?: string;
href: CanonicalHousekeepingHref;
capability: CapabilityRequirement;
}
export interface HousekeepingSearchProvider {
@@ -14,5 +14,6 @@ export interface HousekeepingWidgetDefinition {
kind: HousekeepingWidgetKind;
load(
context: HousekeepingCapabilityContext,
signal: AbortSignal,
): 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";
import {
anyCapability,
type CanonicalHousekeepingHref,
type CapabilityRequirement,
type HousekeepingDomainManifest,
type HousekeepingInboxSource,
@@ -232,7 +233,7 @@ describe("housekeeping registry", () => {
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`,
href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW),
},
{
@@ -253,13 +254,13 @@ describe("housekeeping registry", () => {
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`,
href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW),
},
{
id: "staff",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`,
href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW),
},
],
@@ -272,7 +273,7 @@ describe("housekeeping registry", () => {
const route = {
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`,
href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW),
};
@@ -285,7 +286,7 @@ describe("housekeeping registry", () => {
createHousekeepingRegistry([
{
...manifest("people"),
routes: [{ ...route, href: " " as `/ase${string}` }],
routes: [{ ...route, href: " " as CanonicalHousekeepingHref }],
},
]),
).toThrow("empty route href");
@@ -484,7 +485,7 @@ describe("housekeeping registry", () => {
{
id: "users",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/people/users" as `/ase${string}`,
href: "/ase/people/users" as CanonicalHousekeepingHref,
capability: { mode: "any", slugs: [] },
},
],
@@ -497,7 +498,7 @@ describe("housekeeping registry", () => {
const route = {
id: "shared",
labelKey: "pages.housekeeping.domains.people.title",
href: "/ase/shared" as `/ase${string}`,
href: "/ase/shared" as CanonicalHousekeepingHref,
capability: anyCapability(PERMS.USERS_VIEW),
};
@@ -507,7 +508,10 @@ describe("housekeeping registry", () => {
{
...manifest("content"),
routes: [
{ ...route, href: "/ase/content/shared" as `/ase${string}` },
{
...route,
href: "/ase/content/shared" as CanonicalHousekeepingHref,
},
],
},
]),