compaction & branch summarization follow retry policy
fixes #6647 compaction (auto & manual) and branch summarization retry on transient failures. use the same retry policy from settings. emit events for the tui to show indication of retries
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { fauxAssistantMessage } from "../src/providers/faux.ts";
|
||||
import { isRetryableAssistantError } from "../src/utils/retry.ts";
|
||||
import { isRetryableAssistantError, type RetryPolicy, retryAssistantCall } from "../src/utils/retry.ts";
|
||||
|
||||
const openAIExplicitRetryMessage =
|
||||
"An error occurred while processing your request. You can retry your request, or contact us through our help center at help.openai.com if the error persists. Please include the request ID req_******** in your message.";
|
||||
@@ -66,3 +66,121 @@ describe("provider retry classification", () => {
|
||||
expect(isRetryableAssistantError(fauxAssistantMessage("not an error"))).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("retryAssistantCall", () => {
|
||||
const disabled: RetryPolicy = { enabled: false, maxRetries: 3, baseDelayMs: 0 };
|
||||
const enabled: RetryPolicy = { enabled: true, maxRetries: 3, baseDelayMs: 0 };
|
||||
|
||||
it("returns a successful response immediately without retrying", async () => {
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("ok"));
|
||||
const res = await retryAssistantCall(produce, enabled, undefined);
|
||||
expect(res.content).toEqual([{ type: "text", text: "ok" }]);
|
||||
expect(produce).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("does not retry an aborted message", async () => {
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "aborted" }));
|
||||
const onRetry = vi.fn();
|
||||
const res = await retryAssistantCall(produce, enabled, undefined, { onRetry });
|
||||
expect(res.stopReason).toBe("aborted");
|
||||
expect(produce).toHaveBeenCalledTimes(1);
|
||||
expect(onRetry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not retry a non-retryable error (quota/billing)", async () => {
|
||||
const produce = vi.fn(async () =>
|
||||
fauxAssistantMessage("", { stopReason: "error", errorMessage: "insufficient_quota" }),
|
||||
);
|
||||
const onRetry = vi.fn();
|
||||
const onRetryEnd = vi.fn();
|
||||
const res = await retryAssistantCall(produce, enabled, undefined, { onRetry, onRetryEnd });
|
||||
expect(res.stopReason).toBe("error");
|
||||
expect(produce).toHaveBeenCalledTimes(1);
|
||||
expect(onRetry).not.toHaveBeenCalled();
|
||||
expect(onRetryEnd).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("retries a transient error up to maxRetries then returns the final error", async () => {
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" }));
|
||||
const onRetry = vi.fn();
|
||||
const onRetryEnd = vi.fn();
|
||||
const res = await retryAssistantCall(produce, enabled, undefined, { onRetry, onRetryEnd });
|
||||
expect(res.stopReason).toBe("error");
|
||||
expect(produce).toHaveBeenCalledTimes(4); // 1 initial + 3 retries
|
||||
expect(onRetry).toHaveBeenCalledTimes(3);
|
||||
expect(onRetryEnd).toHaveBeenCalledWith(false, 3, "terminated");
|
||||
});
|
||||
|
||||
it("stops retrying once a call succeeds", async () => {
|
||||
let n = 0;
|
||||
const produce = vi.fn(async () => {
|
||||
n++;
|
||||
return n < 3
|
||||
? fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" })
|
||||
: fauxAssistantMessage("recovered");
|
||||
});
|
||||
const onRetryEnd = vi.fn();
|
||||
const res = await retryAssistantCall(produce, enabled, undefined, { onRetryEnd });
|
||||
expect(res.content).toEqual([{ type: "text", text: "recovered" }]);
|
||||
expect(produce).toHaveBeenCalledTimes(3);
|
||||
expect(onRetryEnd).toHaveBeenCalledWith(true, 2);
|
||||
});
|
||||
|
||||
it("does not retry when policy is disabled", async () => {
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" }));
|
||||
const onRetry = vi.fn();
|
||||
const onRetryEnd = vi.fn();
|
||||
const res = await retryAssistantCall(produce, disabled, undefined, { onRetry, onRetryEnd });
|
||||
expect(res.stopReason).toBe("error");
|
||||
expect(produce).toHaveBeenCalledTimes(1);
|
||||
expect(onRetry).not.toHaveBeenCalled();
|
||||
expect(onRetryEnd).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("emits onRetryAttemptStart after backoff before each retried call", async () => {
|
||||
const events: string[] = [];
|
||||
let n = 0;
|
||||
const produce = vi.fn(async () => {
|
||||
events.push(`produce:${n}`);
|
||||
n++;
|
||||
return n < 3
|
||||
? fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" })
|
||||
: fauxAssistantMessage("recovered");
|
||||
});
|
||||
const onRetry = vi.fn((attempt: number) => {
|
||||
events.push(`retry:${attempt}`);
|
||||
});
|
||||
const onRetryAttemptStart = vi.fn(() => {
|
||||
events.push("attempt-start");
|
||||
});
|
||||
const res = await retryAssistantCall(produce, enabled, undefined, { onRetry, onRetryAttemptStart });
|
||||
expect(res.content).toEqual([{ type: "text", text: "recovered" }]);
|
||||
expect(onRetry).toHaveBeenCalledTimes(2);
|
||||
expect(onRetryAttemptStart).toHaveBeenCalledTimes(2);
|
||||
expect(events).toEqual([
|
||||
"produce:0",
|
||||
"retry:1",
|
||||
"attempt-start",
|
||||
"produce:1",
|
||||
"retry:2",
|
||||
"attempt-start",
|
||||
"produce:2",
|
||||
]);
|
||||
});
|
||||
|
||||
it("aborts backoff sleep via signal, returns an aborted message, and emits onRetryEnd(false)", async () => {
|
||||
const controller = new AbortController();
|
||||
const produce = vi.fn(async () => fauxAssistantMessage("", { stopReason: "error", errorMessage: "terminated" }));
|
||||
const policy: RetryPolicy = { enabled: true, maxRetries: 5, baseDelayMs: 10_000 };
|
||||
const onRetryEnd = vi.fn();
|
||||
const p = retryAssistantCall(produce, policy, controller.signal, { onRetryEnd });
|
||||
// Let one error call resolve and the first backoff sleep start, then abort.
|
||||
await vi.waitFor(() => expect(produce).toHaveBeenCalled());
|
||||
controller.abort();
|
||||
const res = await p;
|
||||
expect(res.stopReason).toBe("aborted");
|
||||
expect(res.errorMessage).toBeUndefined();
|
||||
expect(produce).toHaveBeenCalledTimes(1);
|
||||
expect(onRetryEnd).toHaveBeenCalledWith(false, 1, "terminated");
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user