mirror of https://github.com/buster-so/buster.git
203 lines
5.9 KiB
TypeScript
203 lines
5.9 KiB
TypeScript
import { describe, it, expect, vi } from 'vitest';
|
|
import type { CoreMessage } from 'ai';
|
|
import { APICallError } from 'ai';
|
|
import { detectRetryableError, handleRetryWithHealing } from '../../../src/utils/retry';
|
|
import type { RetryableError, WorkflowContext } from '../../../src/utils/retry';
|
|
|
|
describe('529 error handling', () => {
|
|
it('should detect 529 as overloaded-error type', () => {
|
|
const error = new APICallError({
|
|
message: 'Server overloaded',
|
|
statusCode: 529,
|
|
responseHeaders: {},
|
|
responseBody: 'Server is overloaded, please try again',
|
|
url: 'https://api.example.com',
|
|
requestBodyValues: {}
|
|
});
|
|
|
|
const result = detectRetryableError(error);
|
|
expect(result).not.toBeNull();
|
|
expect(result?.type).toBe('overloaded-error');
|
|
expect(result?.requiresMessageCleanup).toBe(true);
|
|
expect(result?.healingMessage.role).toBe('user');
|
|
expect(result?.healingMessage.content).toContain('Server overloaded (529)');
|
|
});
|
|
|
|
it('should apply cleanup for overloaded errors in handleRetryWithHealing', async () => {
|
|
const messagesWithOrphan: CoreMessage[] = [
|
|
{
|
|
role: 'user',
|
|
content: 'Please analyze this data'
|
|
},
|
|
{
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'text', text: 'Let me analyze that for you' },
|
|
{ type: 'tool-call', toolCallId: 'tc-123', toolName: 'analyzeData', args: { data: 'test' } }
|
|
]
|
|
}
|
|
// No tool result - connection interrupted
|
|
];
|
|
|
|
const retryableError: RetryableError = {
|
|
type: 'overloaded-error',
|
|
originalError: new Error('529'),
|
|
healingMessage: { role: 'user', content: 'Server overloaded (529). Retrying after cleanup...' },
|
|
requiresMessageCleanup: true
|
|
};
|
|
|
|
const context: WorkflowContext = {
|
|
currentStep: 'analyst',
|
|
availableTools: new Set(['analyzeData', 'createMetrics'])
|
|
};
|
|
|
|
const result = await handleRetryWithHealing(
|
|
retryableError,
|
|
messagesWithOrphan,
|
|
0,
|
|
context
|
|
);
|
|
|
|
expect(result.healedMessages).toHaveLength(1); // Only user message remains
|
|
expect(result.healedMessages[0]?.role).toBe('user');
|
|
expect(result.shouldContinueWithoutHealing).toBe(false);
|
|
expect(result.backoffDelay).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('should handle 529 differently from other 5xx errors', () => {
|
|
const error529 = new APICallError({
|
|
message: 'Server overloaded',
|
|
statusCode: 529,
|
|
responseHeaders: {},
|
|
responseBody: 'Overloaded',
|
|
url: 'https://api.example.com',
|
|
requestBodyValues: {}
|
|
});
|
|
|
|
const error500 = new APICallError({
|
|
message: 'Internal server error',
|
|
statusCode: 500,
|
|
responseHeaders: {},
|
|
responseBody: 'Internal error',
|
|
url: 'https://api.example.com',
|
|
requestBodyValues: {}
|
|
});
|
|
|
|
const result529 = detectRetryableError(error529);
|
|
const result500 = detectRetryableError(error500);
|
|
|
|
expect(result529?.type).toBe('overloaded-error');
|
|
expect(result529?.requiresMessageCleanup).toBe(true);
|
|
|
|
expect(result500?.type).toBe('server-error');
|
|
expect(result500?.requiresMessageCleanup).toBeUndefined();
|
|
});
|
|
|
|
it('should use longer backoff for 529 errors', async () => {
|
|
const messages: CoreMessage[] = [
|
|
{ role: 'user', content: 'Test' }
|
|
];
|
|
|
|
const retryableError: RetryableError = {
|
|
type: 'overloaded-error',
|
|
originalError: new Error('529'),
|
|
healingMessage: { role: 'user', content: 'Retrying...' }
|
|
};
|
|
|
|
const context: WorkflowContext = {
|
|
currentStep: 'analyst', // Changed from 'test' to valid value
|
|
availableTools: new Set()
|
|
};
|
|
|
|
const result = await handleRetryWithHealing(
|
|
retryableError,
|
|
messages,
|
|
1, // retryCount
|
|
context
|
|
);
|
|
|
|
// Backoff should be multiplied by 2 for overloaded errors
|
|
expect(result.backoffDelay).toBeGreaterThan(1000); // Base backoff is usually 1000ms for retry 1
|
|
});
|
|
|
|
it('should log cleanup details', async () => {
|
|
const consoleSpy = vi.spyOn(console, 'info');
|
|
|
|
const messagesWithMultipleOrphans: CoreMessage[] = [
|
|
{
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'tool-call', toolCallId: 'tc-1', toolName: 'tool1', args: {} },
|
|
{ type: 'tool-call', toolCallId: 'tc-2', toolName: 'tool2', args: {} }
|
|
]
|
|
}
|
|
];
|
|
|
|
const retryableError: RetryableError = {
|
|
type: 'overloaded-error',
|
|
originalError: new Error('529'),
|
|
healingMessage: { role: 'user', content: 'Retrying...' }
|
|
};
|
|
|
|
const context: WorkflowContext = {
|
|
currentStep: 'analyst',
|
|
availableTools: new Set()
|
|
};
|
|
|
|
await handleRetryWithHealing(
|
|
retryableError,
|
|
messagesWithMultipleOrphans,
|
|
0,
|
|
context
|
|
);
|
|
|
|
expect(consoleSpy).toHaveBeenCalledWith(
|
|
'analyst: Cleaned incomplete tool calls after 529 error',
|
|
expect.objectContaining({
|
|
originalCount: 1,
|
|
cleanedCount: 0,
|
|
removed: 1
|
|
})
|
|
);
|
|
|
|
consoleSpy.mockRestore();
|
|
});
|
|
|
|
it('should preserve messages when no cleanup is needed', async () => {
|
|
const completeMessages: CoreMessage[] = [
|
|
{
|
|
role: 'assistant',
|
|
content: [
|
|
{ type: 'tool-call', toolCallId: 'tc-123', toolName: 'getTodo', args: {} }
|
|
]
|
|
},
|
|
{
|
|
role: 'tool',
|
|
content: [
|
|
{ type: 'tool-result', toolCallId: 'tc-123', toolName: 'getTodo', result: { todo: 'test' } }
|
|
]
|
|
}
|
|
];
|
|
|
|
const retryableError: RetryableError = {
|
|
type: 'overloaded-error',
|
|
originalError: new Error('529'),
|
|
healingMessage: { role: 'user', content: 'Retrying...' }
|
|
};
|
|
|
|
const context: WorkflowContext = {
|
|
currentStep: 'analyst',
|
|
availableTools: new Set()
|
|
};
|
|
|
|
const result = await handleRetryWithHealing(
|
|
retryableError,
|
|
completeMessages,
|
|
0,
|
|
context
|
|
);
|
|
|
|
expect(result.healedMessages).toHaveLength(2); // No cleanup needed
|
|
expect(result.healedMessages).toEqual(completeMessages);
|
|
});
|
|
}); |