feat(housekeeping): unify complete rebuild in gated preview
CI / check (pull_request) Failing after 10s
CI / deploy (pull_request) Skipped
CI / release (pull_request) Skipped

This commit is contained in:
Simo committed 2026-08-31 20:37:51 +02:00
1 parent 4ad007d241
commit 43470ebf82
402 files changed
+78313 -1663

No files matched your search

@@ -0,0 +1,119 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";
import { PERMS } from "@/lib/permission-slugs";
import type { HousekeepingCommand } from "../commands/registry";
import type {
HousekeepingCapabilityContext,
HousekeepingDomainManifest,
} from "../contracts";
import type { HousekeepingRegistry } from "../registry";
import { searchNavigationAndCommands } from "./navigation-search";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 7 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
const peopleManifest = {
id: "people",
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
canonicalHref: "/ase-next/people",
capability: { mode: "any", slugs: [PERMS.USERS_VIEW] },
routes: [
{
id: "people.users",
href: "/ase-next/people/users",
labelKey: "pages.housekeeping.routes.people.users",
capability: { mode: "any", slugs: [PERMS.USERS_VIEW] },
},
{
id: "people.moderation",
href: "/ase-next/people/moderation",
labelKey: "pages.housekeeping.routes.people.moderation",
capability: { mode: "any", slugs: [PERMS.MODERATION_VIEW] },
},
],
searchProviders: [],
inboxSources: [],
widgets: [],
} as const satisfies HousekeepingDomainManifest;
const registry: HousekeepingRegistry = { domains: [peopleManifest] };
function command(
id: string,
permission: string,
): HousekeepingCommand<Record<string, never>, never> {
return {
id,
owner: "people",
risk: "safe",
capability: { mode: "any", slugs: [permission] },
input: z.object({}),
requiresReason: false,
rateLimit: { attempts: 3, windowMs: 60_000 },
execute: async () => ({
ok: true,
data: undefined as never,
correlationId: "command",
}),
};
}
const translations: Record<string, string> = {
"pages.housekeeping.domains.people.title": "People",
"pages.housekeeping.domains.people.description": "Accounts and moderation",
"pages.housekeeping.routes.people.users": "User accounts",
"pages.housekeeping.routes.people.moderation": "Moderation queue",
};
describe("navigation and command search", () => {
it("matches permitted domain/routes/commands and omits forbidden entries", () => {
const result = searchNavigationAndCommands({
registry,
commands: [
command("people.user.refresh", PERMS.USERS_VIEW),
command("people.moderation.ban", PERMS.MODERATION_VIEW),
],
context: context([PERMS.USERS_VIEW]),
term: "user",
translate: (key) => translations[key] ?? key,
});
expect(result.navigation.map((hit) => hit.id)).toEqual([
"route:people.users",
]);
expect(result.commands.map((hit) => hit.commandId)).toEqual([
"people.user.refresh",
]);
expect(result.navigation[0]?.href).toBe("/ase-next/people/users");
expect(result.commands[0]?.href).toBe("/ase-next/people");
});
it("normalizes terms and keeps registry/command order stable", () => {
const result = searchNavigationAndCommands({
registry,
commands: [
command("people.user.refresh", PERMS.USERS_VIEW),
command("people.user.inspect", PERMS.USERS_VIEW),
],
context: context([PERMS.USERS_VIEW]),
term: " PEOPLE ",
translate: (key) => translations[key] ?? key,
});
expect(result.navigation.map((hit) => hit.id)).toEqual(["domain:people"]);
expect(result.commands.map((hit) => hit.commandId)).toEqual([
"people.user.refresh",
"people.user.inspect",
]);
});
});
@@ -0,0 +1,149 @@
import { satisfiesCapability } from "../capability-context";
import type {
CanonicalHousekeepingHref,
CapabilityRequirement,
HousekeepingCapabilityContext,
HousekeepingSearchResult,
} from "../contracts";
import type { HousekeepingRegistry } from "../registry";
export interface HousekeepingNavigationHit
extends Omit<HousekeepingSearchResult, "type"> {
readonly type: "navigation";
}
export interface HousekeepingCommandHit
extends Omit<HousekeepingSearchResult, "type"> {
readonly type: "command";
readonly commandId: string;
readonly risk: "safe" | "sensitive";
readonly requiresReason: boolean;
}
export interface DiscoverableHousekeepingCommand {
readonly id: string;
readonly owner: HousekeepingCommandHit["domain"];
readonly risk: HousekeepingCommandHit["risk"];
readonly capability: CapabilityRequirement;
readonly requiresReason: boolean;
}
export interface NavigationAndCommandSearchInput {
readonly registry: HousekeepingRegistry;
readonly commands: readonly DiscoverableHousekeepingCommand[];
readonly context: HousekeepingCapabilityContext;
readonly term: string;
readonly translate: (key: string) => string;
}
export interface NavigationAndCommandSearchResult {
readonly navigation: readonly HousekeepingNavigationHit[];
readonly commands: readonly HousekeepingCommandHit[];
}
export function normalizeHousekeepingSearchTerm(term: string): string {
return term.normalize("NFC").trim().slice(0, 128);
}
function searchTokens(term: string): readonly string[] {
return normalizeHousekeepingSearchTerm(term)
.toLocaleLowerCase()
.split(/\s+/u)
.filter(Boolean);
}
function matches(tokens: readonly string[], ...values: readonly string[]) {
if (tokens.length === 0) return true;
const haystack = values.join(" ").normalize("NFC").toLocaleLowerCase();
return tokens.every((token) => haystack.includes(token));
}
function commandTitle(
commandId: string,
translate: (key: string) => string,
): string {
const key = `pages.housekeeping.commands.${commandId}`;
const translated = translate(key);
if (translated !== key) return translated;
return commandId
.split(".")
.slice(1)
.flatMap((segment) => segment.split("-"))
.map((segment) =>
segment.length === 0
? segment
: `${segment[0]?.toLocaleUpperCase()}${segment.slice(1)}`,
)
.join(" ");
}
export function searchNavigationAndCommands({
registry,
commands,
context,
term,
translate,
}: NavigationAndCommandSearchInput): NavigationAndCommandSearchResult {
const tokens = searchTokens(term);
const navigation: HousekeepingNavigationHit[] = [];
for (const domain of registry.domains) {
if (!satisfiesCapability(context, domain.capability)) continue;
const domainTitle = translate(domain.labelKey);
const domainDescription = translate(domain.descriptionKey);
if (matches(tokens, domain.id, domainTitle, domainDescription)) {
navigation.push({
id: `domain:${domain.id}`,
domain: domain.id,
type: "navigation",
title: domainTitle,
description: domainDescription,
href: domain.canonicalHref,
capability: domain.capability,
});
}
for (const route of domain.routes) {
if (!satisfiesCapability(context, route.capability)) continue;
const title = translate(route.labelKey);
const localRouteId = route.id.split(".").slice(1).join(" ");
if (!matches(tokens, localRouteId, title)) continue;
navigation.push({
id: `route:${route.id}`,
domain: domain.id,
type: "navigation",
title,
href: route.href,
capability: route.capability,
});
}
}
const commandsFound: HousekeepingCommandHit[] = [];
for (const command of commands) {
if (!satisfiesCapability(context, command.capability)) continue;
const domain = registry.domains.find(
(candidate) => candidate.id === command.owner,
);
if (!domain || !satisfiesCapability(context, domain.capability)) continue;
const title = commandTitle(command.id, translate);
if (!matches(tokens, command.id, command.owner, title)) continue;
commandsFound.push({
id: `command:${command.id}`,
commandId: command.id,
domain: command.owner,
type: "command",
title,
description: command.id,
href: domain.canonicalHref as CanonicalHousekeepingHref,
capability: command.capability,
risk: command.risk,
requiresReason: command.requiresReason,
});
}
return {
navigation: navigation.slice(0, 50),
commands: commandsFound.slice(0, 50),
};
}
@@ -0,0 +1,198 @@
import { describe, expect, it, vi } from "vitest";
import { PERMS } from "@/lib/permission-slugs";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingDomainManifest,
type HousekeepingSearchProvider,
ok,
} from "../contracts";
import type { HousekeepingRegistry } from "../registry";
import {
createHousekeepingSearchService,
SEARCH_POLICY,
} from "./search-service";
function context(granted: readonly string[]): HousekeepingCapabilityContext {
const permissions = new Set(granted);
return {
actor: { id: 42, username: "operator", rank: 7 },
isSuperAdmin: false,
has: (slug) => permissions.has(slug),
hasAny: (...slugs) => slugs.some((slug) => permissions.has(slug)),
hasAll: (...slugs) => slugs.every((slug) => permissions.has(slug)),
};
}
function provider(
id: string,
permission: string,
search: HousekeepingSearchProvider["search"],
): HousekeepingSearchProvider {
return {
id,
owner: "people",
capability: anyCapability(permission),
search,
};
}
function registry(
searchProviders: readonly HousekeepingSearchProvider[],
): HousekeepingRegistry {
return {
domains: [
{
id: "people",
labelKey: "pages.housekeeping.domains.people.title",
descriptionKey: "pages.housekeeping.domains.people.description",
iconId: "users",
canonicalHref: "/ase-next/people",
capability: anyCapability(PERMS.USERS_VIEW),
routes: [
{
id: "people.users",
href: "/ase-next/people/users",
labelKey: "pages.housekeeping.routes.people.users",
capability: anyCapability(PERMS.USERS_VIEW),
},
],
searchProviders,
inboxSources: [],
widgets: [],
} satisfies HousekeepingDomainManifest,
],
};
}
function entity(id: string) {
return {
id,
domain: "people" as const,
type: "entity" as const,
title: `User ${id}`,
href: "/ase-next/people/users" as const,
capability: anyCapability(PERMS.USERS_VIEW),
};
}
describe("Housekeeping search service", () => {
it("locks the provider timeout and result limits", () => {
expect(SEARCH_POLICY).toEqual({
minimumEntityTermLength: 2,
timeoutMs: 2_000,
providerLimit: 25,
combinedEntityLimit: 50,
});
});
it("searches navigation/commands only below two trimmed characters", async () => {
const search = vi.fn(async () => ok([entity("1")], "provider"));
const service = createHousekeepingSearchService({
registry: registry([provider("people.users", PERMS.USERS_VIEW, search)]),
commands: [],
translate: (key) =>
key.endsWith("people.users") ? "Accounts" : "People",
createCorrelationId: () => "search-short",
});
const response = await service(" a ", context([PERMS.USERS_VIEW]));
expect(search).not.toHaveBeenCalled();
expect(response.navigation.map((hit) => hit.title)).toContain("Accounts");
expect(response.entities).toEqual([]);
expect(response.correlationId).toBe("search-short");
});
it("invokes only permitted providers for two-character terms", async () => {
const permitted = vi.fn(async () => ok([entity("1")], "permitted"));
const forbidden = vi.fn(async () => ok([entity("2")], "forbidden"));
const service = createHousekeepingSearchService({
registry: registry([
provider("people.users", PERMS.USERS_VIEW, permitted),
provider("people.bans", PERMS.BANS_VIEW, forbidden),
]),
commands: [],
translate: (key) => key,
createCorrelationId: () => "search-two",
});
const response = await service("ab", context([PERMS.USERS_VIEW]));
expect(permitted).toHaveBeenCalledWith(expect.any(Object), {
term: "ab",
limit: 25,
});
expect(forbidden).not.toHaveBeenCalled();
expect(response.entities.map((item) => item.id)).toEqual(["1"]);
});
it("does not invoke providers owned by a forbidden domain", async () => {
const search = vi.fn(async () => ok([entity("1")], "forbidden-domain"));
const service = createHousekeepingSearchService({
registry: registry([provider("people.bans", PERMS.BANS_VIEW, search)]),
commands: [],
translate: (key) => key,
createCorrelationId: () => "search-forbidden-domain",
});
const response = await service("ab", context([PERMS.BANS_VIEW]));
expect(search).not.toHaveBeenCalled();
expect(response.entities).toEqual([]);
});
it("caps providers at 25, deduplicates stably, and caps combined entities at 50", async () => {
const ranges = [
Array.from({ length: 30 }, (_, index) => entity(String(index))),
Array.from({ length: 30 }, (_, index) => entity(String(index + 20))),
Array.from({ length: 30 }, (_, index) => entity(String(index + 45))),
];
const service = createHousekeepingSearchService({
registry: registry(
ranges.map((items, index) =>
provider(`people.${index}`, PERMS.USERS_VIEW, async () =>
ok(items, `provider-${index}`),
),
),
),
commands: [],
translate: (key) => key,
createCorrelationId: () => "search-limits",
});
const response = await service("user", context([PERMS.USERS_VIEW]));
expect(response.entities).toHaveLength(50);
expect(response.entities.map((item) => item.id)).toEqual(
Array.from({ length: 50 }, (_, index) => String(index)),
);
});
it("returns successful entities with stable typed timeout/provider errors", async () => {
const never = new Promise<never>(() => undefined);
const service = createHousekeepingSearchService({
registry: registry([
provider("people.timeout", PERMS.USERS_VIEW, () => never),
provider("people.failed", PERMS.USERS_VIEW, async () =>
fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
"failed",
),
),
provider("people.ready", PERMS.USERS_VIEW, async () =>
ok([entity("ready")], "ready"),
),
]),
commands: [],
translate: (key) => key,
createCorrelationId: () => "search-partial",
timeoutMs: 5,
});
const response = await service("user", context([PERMS.USERS_VIEW]));
expect(response.entities.map((item) => item.id)).toEqual(["ready"]);
expect(response.errors).toEqual([
{ providerId: "people.timeout", code: "TIMEOUT" },
{ providerId: "people.failed", code: "DEPENDENCY_UNAVAILABLE" },
]);
});
});
@@ -0,0 +1,210 @@
import "server-only";
import { satisfiesCapability } from "../capability-context";
import { listHousekeepingCommands } from "../commands/registry";
import type {
HousekeepingCapabilityContext,
HousekeepingErrorCode,
HousekeepingSearchProvider,
HousekeepingSearchResult,
} from "../contracts";
import { createCorrelationId } from "../correlation";
import { isSafeHousekeepingHref } from "../housekeeping-href";
import {
createHousekeepingRegistry,
type HousekeepingRegistry,
} from "../registry";
import {
type DiscoverableHousekeepingCommand,
type HousekeepingCommandHit,
type HousekeepingNavigationHit,
normalizeHousekeepingSearchTerm,
searchNavigationAndCommands,
} from "./navigation-search";
export const SEARCH_POLICY = Object.freeze({
minimumEntityTermLength: 2,
timeoutMs: 2_000,
providerLimit: 25,
combinedEntityLimit: 50,
} as const);
export interface HousekeepingSearchResponse {
readonly navigation: readonly HousekeepingNavigationHit[];
readonly commands: readonly HousekeepingCommandHit[];
readonly entities: readonly HousekeepingSearchResult[];
readonly errors: readonly {
providerId: string;
code: HousekeepingErrorCode;
}[];
readonly correlationId: string;
}
export interface HousekeepingSearchServiceDependencies {
readonly registry: HousekeepingRegistry;
readonly commands: readonly DiscoverableHousekeepingCommand[];
readonly translate: (key: string) => string;
readonly createCorrelationId?: () => string;
readonly timeoutMs?: number;
}
type ProviderOutcome = {
readonly providerId: string;
readonly items: readonly HousekeepingSearchResult[];
readonly error?: HousekeepingErrorCode;
};
function validEntityResult(
item: HousekeepingSearchResult,
provider: HousekeepingSearchProvider,
context: HousekeepingCapabilityContext,
): boolean {
return (
item !== null &&
typeof item === "object" &&
item.type === "entity" &&
item.domain === provider.owner &&
typeof item.id === "string" &&
item.id.trim().length > 0 &&
typeof item.title === "string" &&
item.title.trim().length > 0 &&
typeof item.href === "string" &&
isSafeHousekeepingHref(item.href) &&
satisfiesCapability(context, item.capability)
);
}
async function runProvider(
provider: HousekeepingSearchProvider,
context: HousekeepingCapabilityContext,
term: string,
timeoutMs: number,
): Promise<ProviderOutcome> {
let timeout: ReturnType<typeof setTimeout> | undefined;
const timedOut = new Promise<ProviderOutcome>((resolve) => {
timeout = setTimeout(
() =>
resolve({
providerId: provider.id,
items: [],
error: "TIMEOUT",
}),
timeoutMs,
);
});
const searched = provider
.search(context, { term, limit: SEARCH_POLICY.providerLimit })
.then<ProviderOutcome>((result) => {
if (!result.ok) {
return {
providerId: provider.id,
items: [],
error: result.error.code,
};
}
return {
providerId: provider.id,
items: result.data
.filter((item) => validEntityResult(item, provider, context))
.slice(0, SEARCH_POLICY.providerLimit),
};
})
.catch<ProviderOutcome>(() => ({
providerId: provider.id,
items: [],
error: "INTERNAL",
}));
try {
return await Promise.race([searched, timedOut]);
} finally {
if (timeout !== undefined) clearTimeout(timeout);
}
}
export function createHousekeepingSearchService({
registry,
commands,
translate,
createCorrelationId: correlationIdFactory = createCorrelationId,
timeoutMs = SEARCH_POLICY.timeoutMs,
}: HousekeepingSearchServiceDependencies) {
return async function runSearch(
term: string,
context: HousekeepingCapabilityContext,
): Promise<HousekeepingSearchResponse> {
const normalizedTerm = normalizeHousekeepingSearchTerm(term);
const navigationAndCommands = searchNavigationAndCommands({
registry,
commands,
context,
term: normalizedTerm,
translate,
});
if (normalizedTerm.length < SEARCH_POLICY.minimumEntityTermLength) {
return {
...navigationAndCommands,
entities: [],
errors: [],
correlationId: correlationIdFactory(),
};
}
const providers = registry.domains.flatMap((domain) => {
if (!satisfiesCapability(context, domain.capability)) return [];
return domain.searchProviders.filter((provider) =>
satisfiesCapability(context, provider.capability),
);
});
const outcomes = await Promise.all(
providers.map((provider) =>
runProvider(provider, context, normalizedTerm, timeoutMs),
),
);
const seen = new Set<string>();
const entities: HousekeepingSearchResult[] = [];
for (const outcome of outcomes) {
for (const item of outcome.items) {
const key = `${item.domain}:${item.type}:${item.id}`;
if (seen.has(key)) continue;
seen.add(key);
entities.push(item);
if (entities.length === SEARCH_POLICY.combinedEntityLimit) break;
}
if (entities.length === SEARCH_POLICY.combinedEntityLimit) break;
}
return {
...navigationAndCommands,
entities,
errors: outcomes.flatMap((outcome) =>
outcome.error
? [{ providerId: outcome.providerId, code: outcome.error }]
: [],
),
correlationId: correlationIdFactory(),
};
};
}
const MESSAGE_PREFIX = "pages.housekeeping.";
export async function searchHousekeeping(
term: string,
context: HousekeepingCapabilityContext,
): Promise<HousekeepingSearchResponse> {
const [{ getTranslations }, { HOUSEKEEPING_MANIFESTS }] = await Promise.all([
import("next-intl/server"),
import("../../manifests"),
]);
const translate = await getTranslations("pages.housekeeping");
const service = createHousekeepingSearchService({
registry: createHousekeepingRegistry(HOUSEKEEPING_MANIFESTS),
commands: listHousekeepingCommands(),
translate: (key) => {
if (!key.startsWith(MESSAGE_PREFIX)) return key;
return translate(key.slice(MESSAGE_PREFIX.length) as never);
},
});
return service(term, context);
}