feat: upgrade investigation and presentation runtime
This commit is contained in:
@@ -36,3 +36,81 @@ it("resumes recovery for a worker-interrupted job instead of waiting or completi
|
||||
expect(resume).toContain("不要重复提交");
|
||||
expect(pause).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
for (const failure of [
|
||||
Object.assign(new TypeError("fetch failed"), { cause: { code: "ECONNRESET" } }),
|
||||
new DOMException("Timed out", "TimeoutError"),
|
||||
Object.assign(new Error("busy"), { status: 429 }),
|
||||
Object.assign(new Error("unavailable"), { status: 503 }),
|
||||
...["RATE_LIMITED", "QUERY_TIMEOUT", "SOURCE_UNAVAILABLE"].map((code) => ({ content: [], isError: true, structuredContent: { error: { code } } })),
|
||||
]) {
|
||||
it(`retries transient status failures (${"name" in failure ? failure.name : failure.structuredContent.error.code})`, async () => {
|
||||
let calls = 0;
|
||||
const query = mock(async () => {
|
||||
if (calls++ === 0) { if (failure instanceof Error || failure instanceof DOMException) throw failure; return failure; }
|
||||
return result("succeeded");
|
||||
});
|
||||
const pause = mock(async (_ms: number, _signal: AbortSignal) => {});
|
||||
const wait = createTaskCalculationWaiter({ query }, () => owner, pause, () => 0.5);
|
||||
expect(await wait("s", new AbortController().signal)).toContain("succeeded");
|
||||
expect(pause.mock.calls.map(([ms]) => ms)).toEqual([1000]);
|
||||
expect(query).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
}
|
||||
|
||||
it("bounds exponential retry delays and fails clearly after exhaustion", async () => {
|
||||
const names: string[] = [];
|
||||
const query = mock(async (_id: string, _owner: unknown, name: string) => {
|
||||
names.push(name);
|
||||
throw Object.assign(new Error("unavailable"), { status: 503 });
|
||||
});
|
||||
const pause = mock(async (_ms: number, _signal: AbortSignal) => {});
|
||||
const wait = createTaskCalculationWaiter({ query }, () => owner, pause, () => 1);
|
||||
await expect(wait("s", new AbortController().signal)).rejects.toThrow("after transient retries");
|
||||
expect(pause.mock.calls.map(([ms]) => ms)).toEqual([1250, 2500, 5000]);
|
||||
expect(names).toEqual(Array(4).fill("list_calculation_jobs"));
|
||||
});
|
||||
|
||||
for (const failure of [
|
||||
Object.assign(new Error("denied"), { status: 401 }),
|
||||
Object.assign(new Error("denied"), { status: 403 }),
|
||||
...["AUTH_REQUIRED", "QUERY_CONTEXT_DENIED", "QUERY_CONTEXT_STALE"].map((code) => ({ content: [], isError: true, structuredContent: { error: { code } } })),
|
||||
]) {
|
||||
it("fails authorization and stale context errors without retrying", async () => {
|
||||
const query = mock(async () => { if (failure instanceof Error) throw failure; return failure; });
|
||||
const pause = mock(async (_ms: number, _signal: AbortSignal) => {});
|
||||
await expect(createTaskCalculationWaiter({ query }, () => owner, pause)("s", new AbortController().signal)).rejects.toThrow();
|
||||
expect(query).toHaveBeenCalledTimes(1);
|
||||
expect(pause).not.toHaveBeenCalled();
|
||||
});
|
||||
}
|
||||
|
||||
it("aborts retry backoff without reading again or cancelling jobs", async () => {
|
||||
const controller = new AbortController();
|
||||
const names: string[] = [];
|
||||
const query = mock(async (_id: string, _owner: unknown, name: string) => {
|
||||
names.push(name);
|
||||
throw new DOMException("Timed out", "TimeoutError");
|
||||
});
|
||||
const wait = createTaskCalculationWaiter({ query }, () => owner, async () => { controller.abort(); });
|
||||
await expect(wait("s", controller.signal)).rejects.toThrow();
|
||||
expect(names).toEqual(["list_calculation_jobs"]);
|
||||
});
|
||||
|
||||
it("returns new terminal jobs while other jobs remain pending and checks pending jobs next round", async () => {
|
||||
const mixed = { content: [], structuredContent: { data: [{ job_id: "job-1", status: "succeeded" }, { job_id: "job-2", status: "running" }], page: { has_more: false } } };
|
||||
const finished = { content: [], structuredContent: { data: [{ job_id: "job-1", status: "succeeded" }, { job_id: "job-2", status: "failed" }], page: { has_more: false } } };
|
||||
const outputs = [mixed, mixed, finished];
|
||||
const query = mock(async () => outputs.shift()!);
|
||||
const pause = mock(async (_ms: number, _signal: AbortSignal) => {});
|
||||
const wait = createTaskCalculationWaiter({ query }, () => owner, pause);
|
||||
const signal = new AbortController().signal;
|
||||
const first = await wait("s", signal);
|
||||
expect(first).toContain("job-1");
|
||||
expect(first).not.toContain("job-2");
|
||||
expect(pause).not.toHaveBeenCalled();
|
||||
const second = await wait("s", signal);
|
||||
expect(second).toContain("job-2");
|
||||
expect(second).not.toContain("job-1");
|
||||
expect(pause).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user