fix(cli): clear stale retry/loading state after cancellation (#21096) (#21960)

Co-authored-by: Aashir Javed <Aaxhirrr@users.noreply.github.com>
Co-authored-by: Dev Randalpura <devrandalpura@google.com>
This commit is contained in:
Aashir Javed
2026-04-02 12:44:39 -07:00
committed by GitHub
parent c0dfa1aec3
commit 77027dff82
8 changed files with 194 additions and 35 deletions
+52
View File
@@ -634,6 +634,58 @@ describe('retryWithBackoff', () => {
);
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('should not emit onRetry when aborted before catch retry handling', async () => {
const abortController = new AbortController();
const onRetry = vi.fn();
const mockFn = vi.fn().mockImplementation(async () => {
const error = new Error('Server error') as HttpError;
error.status = 500;
abortController.abort();
throw error;
});
const promise = retryWithBackoff(mockFn, {
maxAttempts: 3,
initialDelayMs: 100,
signal: abortController.signal,
onRetry,
});
await expect(promise).rejects.toThrow(
expect.objectContaining({ name: 'AbortError' }),
);
expect(onRetry).not.toHaveBeenCalled();
expect(debugLogger.warn).not.toHaveBeenCalled();
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('should not emit onRetry when aborted before content retry handling', async () => {
const abortController = new AbortController();
const onRetry = vi.fn();
const shouldRetryOnContent = vi.fn().mockImplementation(() => {
abortController.abort();
return true;
});
const mockFn = vi.fn().mockResolvedValue({});
const promise = retryWithBackoff(mockFn, {
maxAttempts: 3,
initialDelayMs: 100,
signal: abortController.signal,
onRetry,
shouldRetryOnContent,
});
await expect(promise).rejects.toThrow(
expect.objectContaining({ name: 'AbortError' }),
);
expect(onRetry).not.toHaveBeenCalled();
expect(debugLogger.warn).not.toHaveBeenCalled();
expect(shouldRetryOnContent).toHaveBeenCalledTimes(1);
expect(mockFn).toHaveBeenCalledTimes(1);
});
it('should trigger fallback for OAuth personal users on persistent 500 errors', async () => {
const fallbackCallback = vi.fn().mockResolvedValue('gemini-2.5-flash');
+7
View File
@@ -232,6 +232,11 @@ export async function retryWithBackoff<T>(
let attempt = 0;
let currentDelay = initialDelayMs;
const throwIfAborted = () => {
if (signal?.aborted) {
throw createAbortError();
}
};
while (attempt < maxAttempts) {
if (signal?.aborted) {
@@ -246,6 +251,7 @@ export async function retryWithBackoff<T>(
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
shouldRetryOnContent(result as GenerateContentResponse)
) {
throwIfAborted();
const jitter = currentDelay * 0.3 * (Math.random() * 2 - 1);
const delayWithJitter = Math.max(0, currentDelay + jitter);
if (onRetry) {
@@ -266,6 +272,7 @@ export async function retryWithBackoff<T>(
if (error instanceof Error && error.name === 'AbortError') {
throw error;
}
throwIfAborted();
const classifiedError = classifyGoogleError(error);