112 lines
3.0 KiB
TypeScript
112 lines
3.0 KiB
TypeScript
import { describe, expect, it, vi } from "vitest";
|
|
import {
|
|
anyCapability,
|
|
type HousekeepingCapabilityContext,
|
|
ok,
|
|
} from "../contracts";
|
|
import { orchestrateProviders, type ProviderPolicy } from "./orchestrate";
|
|
import type { HousekeepingProvider } from "./run-provider";
|
|
|
|
interface Item {
|
|
key: string;
|
|
label: string;
|
|
order: number;
|
|
}
|
|
|
|
const policy: ProviderPolicy<Item> = {
|
|
timeoutMs: 2_000,
|
|
perProviderLimit: 2,
|
|
combinedLimit: 3,
|
|
sort: (items) => [...items].sort((left, right) => left.order - right.order),
|
|
dedupeKey: (item) => item.key,
|
|
};
|
|
|
|
const allowedContext: HousekeepingCapabilityContext = {
|
|
actor: { id: 42, username: "operator", rank: 0 },
|
|
isSuperAdmin: false,
|
|
has: (slug) => slug === "housekeeping.read",
|
|
hasAny: (...slugs) => slugs.includes("housekeeping.read"),
|
|
hasAll: (...slugs) => slugs.every((slug) => slug === "housekeeping.read"),
|
|
};
|
|
|
|
function provider(
|
|
id: string,
|
|
items: readonly Item[],
|
|
): HousekeepingProvider<Item> {
|
|
return {
|
|
id,
|
|
capability: anyCapability("housekeeping.read"),
|
|
run: async () => ok(items, `${id}-correlation`),
|
|
};
|
|
}
|
|
|
|
describe("orchestrateProviders", () => {
|
|
it("preserves a successful sibling when another provider times out", async () => {
|
|
vi.useFakeTimers();
|
|
const slow: HousekeepingProvider<Item> = {
|
|
id: "slow",
|
|
capability: anyCapability("housekeeping.read"),
|
|
run: async (_context, signal) =>
|
|
await new Promise((_, reject) => {
|
|
signal.addEventListener("abort", () => reject(new Error("aborted")));
|
|
}),
|
|
};
|
|
|
|
const result = orchestrateProviders(
|
|
[slow, provider("fast", [{ key: "fast", label: "Fast", order: 1 }])],
|
|
allowedContext,
|
|
policy,
|
|
);
|
|
await vi.advanceTimersByTimeAsync(2_000);
|
|
|
|
expect(await result).toMatchObject({
|
|
items: [{ key: "fast", label: "Fast", order: 1 }],
|
|
errors: [{ providerId: "slow", code: "TIMEOUT" }],
|
|
});
|
|
vi.useRealTimers();
|
|
});
|
|
|
|
it("keeps the first duplicate before applying deterministic sorting", async () => {
|
|
const result = await orchestrateProviders(
|
|
[
|
|
provider("first", [
|
|
{ key: "shared", label: "First", order: 2 },
|
|
{ key: "unique", label: "Unique", order: 1 },
|
|
]),
|
|
provider("second", [{ key: "shared", label: "Second", order: 0 }]),
|
|
],
|
|
allowedContext,
|
|
policy,
|
|
);
|
|
|
|
expect(result.items).toEqual([
|
|
{ key: "unique", label: "Unique", order: 1 },
|
|
{ key: "shared", label: "First", order: 2 },
|
|
]);
|
|
});
|
|
|
|
it("enforces per-provider and combined caps before sorting", async () => {
|
|
const result = await orchestrateProviders(
|
|
[
|
|
provider("first", [
|
|
{ key: "one", label: "One", order: 3 },
|
|
{ key: "two", label: "Two", order: 2 },
|
|
{ key: "discarded", label: "Discarded", order: 0 },
|
|
]),
|
|
provider("second", [
|
|
{ key: "three", label: "Three", order: 1 },
|
|
{ key: "combined-discarded", label: "Combined discarded", order: 0 },
|
|
]),
|
|
],
|
|
allowedContext,
|
|
policy,
|
|
);
|
|
|
|
expect(result.items).toEqual([
|
|
{ key: "three", label: "Three", order: 1 },
|
|
{ key: "two", label: "Two", order: 2 },
|
|
{ key: "one", label: "One", order: 3 },
|
|
]);
|
|
});
|
|
});
|