fix(housekeeping): contain provider failures

This commit is contained in:
Simo committed 2026-08-27 18:11:26 +02:00
1 parent 4c399873d1
commit ca666d9f90
3 files changed
+152 -2

No files matched your search

@@ -66,6 +66,68 @@ describe("orchestrateProviders", () => {
vi.useRealTimers();
});
it("preserves a successful sibling when another provider throws synchronously", async () => {
const synchronousFailure: HousekeepingProvider<Item> = {
id: "synchronously-broken",
capability: anyCapability("housekeeping.read"),
run: () => {
throw new Error("synchronous dependency failure");
},
};
expect(
await orchestrateProviders(
[
synchronousFailure,
provider("fast", [{ key: "fast", label: "Fast", order: 1 }]),
],
allowedContext,
policy,
),
).toMatchObject({
items: [{ key: "fast", label: "Fast", order: 1 }],
errors: [{ providerId: "synchronously-broken", code: "INTERNAL" }],
});
});
it("keeps the timeout result and sibling success when a provider rejects late", async () => {
vi.useFakeTimers();
let observedAbort = false;
const lateFailure: HousekeepingProvider<Item> = {
id: "late-failure",
capability: anyCapability("housekeeping.read"),
run: async (_context, signal) =>
await new Promise<never>((_resolve, reject) => {
signal.addEventListener("abort", () => {
observedAbort = true;
});
setTimeout(() => reject(new Error("late dependency failure")), 2_001);
}),
};
const batch = orchestrateProviders(
[
lateFailure,
provider("fast", [{ key: "fast", label: "Fast", order: 1 }]),
],
allowedContext,
policy,
);
await vi.advanceTimersByTimeAsync(2_000);
expect(await batch).toMatchObject({
items: [{ key: "fast", label: "Fast", order: 1 }],
errors: [{ providerId: "late-failure", code: "TIMEOUT" }],
});
expect(observedAbort).toBe(true);
await vi.advanceTimersByTimeAsync(1);
expect(await batch).toMatchObject({
items: [{ key: "fast", label: "Fast", order: 1 }],
errors: [{ providerId: "late-failure", code: "TIMEOUT" }],
});
vi.useRealTimers();
});
it("keeps the first duplicate before applying deterministic sorting", async () => {
const result = await orchestrateProviders(
[
@@ -108,4 +170,20 @@ describe("orchestrateProviders", () => {
{ key: "one", label: "One", order: 3 },
]);
});
it("applies the per-provider cap before cross-provider deduplication", async () => {
const result = await orchestrateProviders(
[
provider("first", [{ key: "shared", label: "First", order: 1 }]),
provider("second", [
{ key: "shared", label: "Second", order: 2 },
{ key: "unique", label: "Unique", order: 3 },
]),
],
allowedContext,
{ ...policy, perProviderLimit: 1, combinedLimit: 5 },
);
expect(result.items).toEqual([{ key: "shared", label: "First", order: 1 }]);
});
});
@@ -1,6 +1,7 @@
import { describe, expect, it, vi } from "vitest";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
ok,
} from "../contracts";
@@ -75,4 +76,75 @@ describe("runProvider", () => {
await runProvider(provider, context(["housekeeping.read"]), 2_000),
).toEqual({ providerId: "broken", items: [], error: "INTERNAL" });
});
it("maps a synchronous provider throw to one INTERNAL partial error", async () => {
const provider: HousekeepingProvider<string> = {
id: "synchronously-broken",
capability: anyCapability("housekeeping.read"),
run: () => {
throw new Error("synchronous dependency failure");
},
};
await expect(
runProvider(provider, context(["housekeeping.read"]), 2_000),
).resolves.toEqual({
providerId: "synchronously-broken",
items: [],
error: "INTERNAL",
});
});
it("times out an abort-ignoring provider exactly at 2,000 ms", async () => {
vi.useFakeTimers();
let observedAbort = false;
const provider: HousekeepingProvider<string> = {
id: "abort-ignoring",
capability: anyCapability("housekeeping.read"),
run: async (_context, signal) => {
signal.addEventListener("abort", () => {
observedAbort = true;
});
return await new Promise<never>(() => undefined);
},
};
const result = runProvider(provider, context(["housekeeping.read"]), 2_000);
let settled = false;
void result.then(() => {
settled = true;
});
await vi.advanceTimersByTimeAsync(1_999);
expect(settled).toBe(false);
await vi.advanceTimersByTimeAsync(1);
expect(await result).toEqual({
providerId: "abort-ignoring",
items: [],
error: "TIMEOUT",
});
expect(observedAbort).toBe(true);
vi.useRealTimers();
});
it("preserves a returned typed provider failure code once", async () => {
const provider: HousekeepingProvider<string> = {
id: "dependency-failure",
capability: anyCapability("housekeeping.read"),
run: async () =>
fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
"provider-correlation",
),
};
expect(
await runProvider(provider, context(["housekeeping.read"]), 2_000),
).toEqual({
providerId: "dependency-failure",
items: [],
error: "DEPENDENCY_UNAVAILABLE",
});
});
});
@@ -32,8 +32,8 @@ export async function runProvider<T>(
const controller = new AbortController();
let timeout: ReturnType<typeof setTimeout> | undefined;
const run = provider
.run(context, controller.signal)
const run = Promise.resolve()
.then(() => provider.run(context, controller.signal))
.then(
(result): ProviderRunResult<T> =>
result.ok