Improve the UX for API request retries

This commit is contained in:
Matt Rubens
2025-01-10 16:02:35 -05:00
parent bcebba8dd8
commit 97a93ba731
4 changed files with 143 additions and 16 deletions

View File

@@ -628,6 +628,133 @@ describe('Cline', () => {
});
});
it('should handle API retry with countdown', async () => {
const cline = new Cline(
mockProvider,
mockApiConfig,
undefined,
false,
undefined,
'test task'
);
// Mock delay to track countdown timing
const mockDelay = jest.fn().mockResolvedValue(undefined);
jest.spyOn(require('delay'), 'default').mockImplementation(mockDelay);
// Mock say to track messages
const saySpy = jest.spyOn(cline, 'say');
// Create a stream that fails on first chunk
const mockError = new Error('API Error');
const mockFailedStream = {
async *[Symbol.asyncIterator]() {
throw mockError;
},
async next() {
throw mockError;
},
async return() {
return { done: true, value: undefined };
},
async throw(e: any) {
throw e;
},
async [Symbol.asyncDispose]() {
// Cleanup
}
} as AsyncGenerator<ApiStreamChunk>;
// Create a successful stream for retry
const mockSuccessStream = {
async *[Symbol.asyncIterator]() {
yield { type: 'text', text: 'Success' };
},
async next() {
return { done: true, value: { type: 'text', text: 'Success' } };
},
async return() {
return { done: true, value: undefined };
},
async throw(e: any) {
throw e;
},
async [Symbol.asyncDispose]() {
// Cleanup
}
} as AsyncGenerator<ApiStreamChunk>;
// Mock createMessage to fail first then succeed
let firstAttempt = true;
jest.spyOn(cline.api, 'createMessage').mockImplementation(() => {
if (firstAttempt) {
firstAttempt = false;
return mockFailedStream;
}
return mockSuccessStream;
});
// Set alwaysApproveResubmit and requestDelaySeconds
mockProvider.getState = jest.fn().mockResolvedValue({
alwaysApproveResubmit: true,
requestDelaySeconds: 3
});
// Mock previous API request message
cline.clineMessages = [{
ts: Date.now(),
type: 'say',
say: 'api_req_started',
text: JSON.stringify({
tokensIn: 100,
tokensOut: 50,
cacheWrites: 0,
cacheReads: 0,
request: 'test request'
})
}];
// Trigger API request
const iterator = cline.attemptApiRequest(0);
await iterator.next();
// Verify countdown messages
expect(saySpy).toHaveBeenCalledWith(
'api_req_retry_delayed',
expect.stringContaining('Retrying in 3 seconds'),
undefined,
true
);
expect(saySpy).toHaveBeenCalledWith(
'api_req_retry_delayed',
expect.stringContaining('Retrying in 2 seconds'),
undefined,
true
);
expect(saySpy).toHaveBeenCalledWith(
'api_req_retry_delayed',
expect.stringContaining('Retrying in 1 seconds'),
undefined,
true
);
expect(saySpy).toHaveBeenCalledWith(
'api_req_retry_delayed',
expect.stringContaining('Retrying now'),
undefined,
false
);
// Verify delay was called correctly
expect(mockDelay).toHaveBeenCalledTimes(3);
expect(mockDelay).toHaveBeenCalledWith(1000);
// Verify error message content
const errorMessage = saySpy.mock.calls.find(
call => call[1]?.includes(mockError.message)
)?.[1];
expect(errorMessage).toBe(`${mockError.message}\n\nRetrying in 3 seconds...`);
});
describe('loadContext', () => {
it('should process mentions in task and feedback tags', async () => {
const cline = new Cline(