buster/packages/ai/tests/utils/retry/healing-strategies.test.ts

280 lines
9.2 KiB
TypeScript

import type { CoreMessage } from 'ai';
import { describe, expect, it } from 'vitest';
import {
applyHealingStrategy,
determineHealingStrategy,
getErrorExplanationForUser,
removeLastAssistantMessage,
shouldRetryWithoutHealing,
} from '../../../src/utils/retry/healing-strategies';
import type { RetryableError } from '../../../src/utils/retry/types';
describe('healing-strategies', () => {
describe('determineHealingStrategy', () => {
it('should keep message and provide healing for tool errors', () => {
const toolError: RetryableError = {
type: 'no-such-tool',
originalError: new Error('Tool not found'),
healingMessage: {
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: '123',
toolName: 'invalidTool',
result: { error: 'Tool not available' },
},
],
},
};
const strategy = determineHealingStrategy(toolError);
expect(strategy.shouldRemoveLastAssistantMessage).toBe(false);
expect(strategy.healingMessage).toBe(toolError.healingMessage);
expect(strategy.backoffMultiplier).toBeUndefined();
});
it('should remove message for empty response errors', () => {
const emptyError: RetryableError = {
type: 'empty-response',
originalError: new Error('Empty response'),
healingMessage: { role: 'user', content: 'Please continue.' },
};
const strategy = determineHealingStrategy(emptyError);
expect(strategy.shouldRemoveLastAssistantMessage).toBe(true);
expect(strategy.healingMessage?.content).toBe('Please continue with your analysis.');
});
it('should remove message for JSON parse errors', () => {
const jsonError: RetryableError = {
type: 'json-parse-error',
originalError: new Error('Invalid JSON'),
healingMessage: { role: 'user', content: 'Format error' },
};
const strategy = determineHealingStrategy(jsonError);
expect(strategy.shouldRemoveLastAssistantMessage).toBe(true);
expect(strategy.healingMessage?.content).toBe('Please continue with your analysis.');
});
it('should retry without healing for network errors', () => {
const networkError: RetryableError = {
type: 'network-timeout',
originalError: new Error('Network timeout'),
healingMessage: { role: 'user', content: 'Network error' },
};
const strategy = determineHealingStrategy(networkError);
expect(strategy.shouldRemoveLastAssistantMessage).toBe(false);
expect(strategy.healingMessage).toBeNull();
expect(strategy.backoffMultiplier).toBe(2);
});
it('should have longer backoff for rate limit errors', () => {
const rateLimitError: RetryableError = {
type: 'rate-limit',
originalError: new Error('Rate limited'),
healingMessage: { role: 'user', content: 'Rate limit' },
};
const strategy = determineHealingStrategy(rateLimitError);
expect(strategy.shouldRemoveLastAssistantMessage).toBe(false);
expect(strategy.healingMessage).toBeNull();
expect(strategy.backoffMultiplier).toBe(3);
});
});
describe('removeLastAssistantMessage', () => {
it('should remove last assistant message', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi there' },
{ role: 'user', content: 'How are you?' },
{ role: 'assistant', content: 'I am doing well' },
];
const result = removeLastAssistantMessage(messages);
expect(result).toHaveLength(3);
expect(result[result.length - 1]?.content).toBe('How are you?');
});
it('should remove assistant message and subsequent tool results', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Hello' },
{
role: 'assistant',
content: [
{ type: 'text', text: 'Let me help' },
{ type: 'tool-call', toolCallId: '123', toolName: 'test', args: {} },
],
},
{
role: 'tool',
content: [{ type: 'tool-result', toolCallId: '123', toolName: 'test', result: {} }],
},
];
const result = removeLastAssistantMessage(messages);
expect(result).toHaveLength(1);
expect(result[0]?.role).toBe('user');
});
it('should handle no assistant messages', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Hello' },
{ role: 'user', content: 'Anyone there?' },
];
const result = removeLastAssistantMessage(messages);
expect(result).toEqual(messages);
});
it('should only remove last assistant message when multiple exist', () => {
const messages: CoreMessage[] = [
{ role: 'assistant', content: 'First response' },
{ role: 'user', content: 'Question' },
{ role: 'assistant', content: 'Second response' },
];
const result = removeLastAssistantMessage(messages);
expect(result).toHaveLength(2);
expect(result[0]?.content).toBe('First response');
expect(result[1]?.content).toBe('Question');
});
});
describe('applyHealingStrategy', () => {
it('should remove message and add healing for empty response', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Tell me about X' },
{ role: 'assistant', content: '' }, // Empty response
];
const strategy = {
shouldRemoveLastAssistantMessage: true,
healingMessage: {
role: 'user',
content: 'Please continue with your analysis.',
} as CoreMessage,
};
const result = applyHealingStrategy(messages, strategy);
expect(result).toHaveLength(2);
expect(result[0]?.content).toBe('Tell me about X');
expect(result[1]?.content).toBe('Please continue with your analysis.');
});
it('should add healing without removing for tool errors', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Analyze data' },
{
role: 'assistant',
content: [{ type: 'tool-call', toolCallId: '123', toolName: 'wrongTool', args: {} }],
},
];
const strategy = {
shouldRemoveLastAssistantMessage: false,
healingMessage: {
role: 'tool',
content: [
{
type: 'tool-result',
toolCallId: '123',
toolName: 'wrongTool',
result: { error: 'Tool not found' },
},
],
} as CoreMessage,
};
const result = applyHealingStrategy(messages, strategy);
expect(result).toHaveLength(3);
expect(result[2]?.role).toBe('tool');
});
it('should handle no healing message', () => {
const messages: CoreMessage[] = [
{ role: 'user', content: 'Hello' },
{ role: 'assistant', content: 'Hi' },
];
const strategy = {
shouldRemoveLastAssistantMessage: false,
healingMessage: null,
};
const result = applyHealingStrategy(messages, strategy);
expect(result).toEqual(messages);
});
});
describe('shouldRetryWithoutHealing', () => {
it('should return true for network/server errors', () => {
expect(shouldRetryWithoutHealing('network-timeout')).toBe(true);
expect(shouldRetryWithoutHealing('server-error')).toBe(true);
expect(shouldRetryWithoutHealing('rate-limit')).toBe(true);
});
it('should return false for other error types', () => {
expect(shouldRetryWithoutHealing('no-such-tool')).toBe(false);
expect(shouldRetryWithoutHealing('empty-response')).toBe(false);
expect(shouldRetryWithoutHealing('json-parse-error')).toBe(false);
expect(shouldRetryWithoutHealing('invalid-tool-arguments')).toBe(false);
});
});
describe('getErrorExplanationForUser', () => {
it('should provide explanations for specific error types', () => {
const emptyError: RetryableError = {
type: 'empty-response',
originalError: new Error(),
healingMessage: { role: 'user', content: '' },
};
expect(getErrorExplanationForUser(emptyError)).toBe(
"The assistant's response was incomplete. Retrying..."
);
const jsonError: RetryableError = {
type: 'json-parse-error',
originalError: new Error(),
healingMessage: { role: 'user', content: '' },
};
expect(getErrorExplanationForUser(jsonError)).toBe(
'There was a formatting issue with the response. Retrying...'
);
const toolError: RetryableError = {
type: 'no-such-tool',
originalError: new Error(),
healingMessage: { role: 'user', content: '' },
};
expect(getErrorExplanationForUser(toolError)).toBe(
"The assistant tried to use a tool that's not available in the current mode."
);
});
it('should return null for errors without explanations', () => {
const networkError: RetryableError = {
type: 'network-timeout',
originalError: new Error(),
healingMessage: { role: 'user', content: '' },
};
expect(getErrorExplanationForUser(networkError)).toBeNull();
});
});
});