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>>;
}