222 lines
6.4 KiB
TypeScript
222 lines
6.4 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import { PERMS } from "@/lib/permission-slugs";
|
|
import {
|
|
anyCapability,
|
|
fail,
|
|
type HousekeepingCapabilityContext,
|
|
type HousekeepingDomainManifest,
|
|
type HousekeepingInboxSource,
|
|
type HousekeepingWorkItem,
|
|
ok,
|
|
} from "../contracts";
|
|
import type { HousekeepingRegistry } from "../registry";
|
|
import { createHousekeepingInboxService, INBOX_POLICY } from "./inbox-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 item(
|
|
itemId: string,
|
|
options: Partial<HousekeepingWorkItem> = {},
|
|
): HousekeepingWorkItem {
|
|
return {
|
|
sourceId: "people.queue",
|
|
itemId,
|
|
deduplicationKey: `people.queue:${itemId}`,
|
|
domain: "people",
|
|
capability: anyCapability(PERMS.USERS_VIEW),
|
|
severity: "info",
|
|
priority: "normal",
|
|
ageMs: 1_000,
|
|
state: "open",
|
|
occurredAt: "2026-08-30T10:00:00.000Z",
|
|
titleKey: "pages.housekeeping.items.test",
|
|
href: "/ase/people/users",
|
|
freshness: "fresh",
|
|
actions: [],
|
|
...options,
|
|
};
|
|
}
|
|
|
|
function source(
|
|
id: string,
|
|
permission: string,
|
|
getItems: HousekeepingInboxSource["getItems"],
|
|
owner: "people" | "content" = "people",
|
|
): HousekeepingInboxSource {
|
|
return {
|
|
id,
|
|
owner,
|
|
capability: anyCapability(permission),
|
|
getItems,
|
|
};
|
|
}
|
|
|
|
function manifest(
|
|
id: "people" | "content",
|
|
permission: string,
|
|
inboxSources: readonly HousekeepingInboxSource[],
|
|
): HousekeepingDomainManifest {
|
|
return {
|
|
id,
|
|
labelKey: `pages.housekeeping.domains.${id}.title`,
|
|
descriptionKey: `pages.housekeeping.domains.${id}.description`,
|
|
iconId: id === "people" ? "users" : "file-text",
|
|
canonicalHref: `/ase/${id}`,
|
|
capability: anyCapability(permission),
|
|
routes: [],
|
|
searchProviders: [],
|
|
inboxSources,
|
|
widgets: [],
|
|
};
|
|
}
|
|
|
|
function registry(
|
|
...domains: readonly HousekeepingDomainManifest[]
|
|
): HousekeepingRegistry {
|
|
return { domains };
|
|
}
|
|
|
|
describe("Housekeeping inbox service", () => {
|
|
it("locks timeout, cap, and deduplication policy", () => {
|
|
expect(INBOX_POLICY).toEqual({
|
|
timeoutMs: 2_000,
|
|
combinedLimit: 200,
|
|
dedupe: "sourceId:itemId",
|
|
});
|
|
});
|
|
|
|
it("filters domain and source capabilities before invoking sources", async () => {
|
|
const permitted = vi.fn(async () =>
|
|
ok({ availability: "available" as const, items: [] }, "permitted"),
|
|
);
|
|
const forbiddenSource = vi.fn(async () =>
|
|
ok({ availability: "available" as const, items: [] }, "source"),
|
|
);
|
|
const forbiddenDomain = vi.fn(async () =>
|
|
ok({ availability: "available" as const, items: [] }, "domain"),
|
|
);
|
|
const service = createHousekeepingInboxService({
|
|
registry: registry(
|
|
manifest("people", PERMS.USERS_VIEW, [
|
|
source("people.ready", PERMS.USERS_VIEW, permitted),
|
|
source("people.bans", PERMS.BANS_VIEW, forbiddenSource),
|
|
]),
|
|
manifest("content", PERMS.NEWS_VIEW, [
|
|
source("content.users", PERMS.USERS_VIEW, forbiddenDomain, "content"),
|
|
]),
|
|
),
|
|
createCorrelationId: () => "inbox-capabilities",
|
|
});
|
|
|
|
await service(context([PERMS.USERS_VIEW]));
|
|
expect(permitted).toHaveBeenCalledOnce();
|
|
expect(forbiddenSource).not.toHaveBeenCalled();
|
|
expect(forbiddenDomain).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("deduplicates by source/item and orders by priority then oldest age", async () => {
|
|
const items = [
|
|
item("normal", { priority: "normal", ageMs: 900 }),
|
|
item("high-new", { priority: "high", ageMs: 100 }),
|
|
item("critical", { priority: "critical", ageMs: 10 }),
|
|
item("high-old", { priority: "high", ageMs: 1_000 }),
|
|
item("low", { priority: "low", ageMs: 5_000 }),
|
|
item("high-old", { priority: "high", ageMs: 999 }),
|
|
];
|
|
const service = createHousekeepingInboxService({
|
|
registry: registry(
|
|
manifest("people", PERMS.USERS_VIEW, [
|
|
source("people.queue", PERMS.USERS_VIEW, async () =>
|
|
ok({ availability: "available", items }, "ordered"),
|
|
),
|
|
]),
|
|
),
|
|
createCorrelationId: () => "inbox-order",
|
|
});
|
|
|
|
const response = await service(context([PERMS.USERS_VIEW]));
|
|
expect(response.items.map((entry) => entry.itemId)).toEqual([
|
|
"critical",
|
|
"high-old",
|
|
"high-new",
|
|
"normal",
|
|
"low",
|
|
]);
|
|
});
|
|
|
|
it("filters item capabilities before enforcing the 200-item cap", async () => {
|
|
const items = Array.from({ length: 230 }, (_, index) =>
|
|
item(String(index), {
|
|
capability: anyCapability(
|
|
index < 20 ? PERMS.BANS_VIEW : PERMS.USERS_VIEW,
|
|
),
|
|
}),
|
|
);
|
|
const service = createHousekeepingInboxService({
|
|
registry: registry(
|
|
manifest("people", PERMS.USERS_VIEW, [
|
|
source("people.queue", PERMS.USERS_VIEW, async () =>
|
|
ok({ availability: "available", items }, "capped"),
|
|
),
|
|
]),
|
|
),
|
|
createCorrelationId: () => "inbox-cap",
|
|
});
|
|
|
|
const response = await service(context([PERMS.USERS_VIEW]));
|
|
expect(response.items).toHaveLength(200);
|
|
expect(response.items[0]?.itemId).toBe("20");
|
|
expect(response.items.at(-1)?.itemId).toBe("219");
|
|
});
|
|
|
|
it("returns successful items with stable timeout and source errors", async () => {
|
|
let timeoutSignal: AbortSignal | undefined;
|
|
const service = createHousekeepingInboxService({
|
|
registry: registry(
|
|
manifest("people", PERMS.USERS_VIEW, [
|
|
source("people.timeout", PERMS.USERS_VIEW, (_context, signal) => {
|
|
timeoutSignal = signal;
|
|
return new Promise(() => undefined);
|
|
}),
|
|
source("people.failed", PERMS.USERS_VIEW, async () =>
|
|
fail(
|
|
"DEPENDENCY_UNAVAILABLE",
|
|
"errors.housekeeping.dependencyUnavailable",
|
|
"failed",
|
|
),
|
|
),
|
|
source("people.ready", PERMS.USERS_VIEW, async () =>
|
|
ok(
|
|
{
|
|
availability: "available",
|
|
items: [item("ready", { sourceId: "people.ready" })],
|
|
},
|
|
"ready",
|
|
),
|
|
),
|
|
]),
|
|
),
|
|
createCorrelationId: () => "inbox-partial",
|
|
timeoutMs: 5,
|
|
});
|
|
|
|
const response = await service(context([PERMS.USERS_VIEW]));
|
|
expect(response.items.map((entry) => entry.itemId)).toEqual(["ready"]);
|
|
expect(response.errors).toEqual([
|
|
{ sourceId: "people.timeout", code: "TIMEOUT" },
|
|
{ sourceId: "people.failed", code: "DEPENDENCY_UNAVAILABLE" },
|
|
]);
|
|
expect(timeoutSignal?.aborted).toBe(true);
|
|
expect(response.correlationId).toBe("inbox-partial");
|
|
});
|
|
});
|