fix(housekeeping): contain provider failures
This commit is contained in:
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
|
||||
|
||||
Reference in new issue
Block a user