feat(config): Implement new config system and resolve refactoring errors Introduced config-manager.js and new utilities (resolveEnvVariable, findProjectRoot). Removed old global CONFIG object from utils.js. Updated .taskmasterconfig, mcp.json, and .env.example. Added generateComplexityAnalysisPrompt to ui.js. Removed unused updateSubtaskById from task-manager.js. Resolved SyntaxError and ReferenceError issues across commands.js, ui.js, task-manager.js, and ai-services.js by replacing CONFIG references with config-manager getters (getDebugFlag, getProjectName, getDefaultSubtasks, isApiKeySet). Refactored 'models' command to use getConfig/writeConfig. Simplified version checking. This stabilizes the codebase after initial Task 61 refactoring, fixing CLI errors and enabling subsequent work on Subtasks 61.34 and 61.35.
This commit is contained in:
683
tests/unit/ai-services-unified.test.js
Normal file
683
tests/unit/ai-services-unified.test.js
Normal file
@@ -0,0 +1,683 @@
|
||||
import { jest } from '@jest/globals';
|
||||
|
||||
// Mock ai-client-factory
|
||||
const mockGetClient = jest.fn();
|
||||
jest.unstable_mockModule('../../scripts/modules/ai-client-factory.js', () => ({
|
||||
getClient: mockGetClient
|
||||
}));
|
||||
|
||||
// Mock AI SDK Core
|
||||
const mockGenerateText = jest.fn();
|
||||
jest.unstable_mockModule('ai', () => ({
|
||||
generateText: mockGenerateText
|
||||
// Mock other AI SDK functions like streamText as needed
|
||||
}));
|
||||
|
||||
// Mock utils logger
|
||||
const mockLog = jest.fn();
|
||||
jest.unstable_mockModule('../../scripts/modules/utils.js', () => ({
|
||||
log: mockLog
|
||||
// Keep other exports if utils has more, otherwise just log
|
||||
}));
|
||||
|
||||
// Import the module to test (AFTER mocks)
|
||||
const { generateTextService } = await import(
|
||||
'../../scripts/modules/ai-services-unified.js'
|
||||
);
|
||||
|
||||
describe('Unified AI Services', () => {
|
||||
beforeEach(() => {
|
||||
// Clear mocks before each test
|
||||
mockGetClient.mockClear();
|
||||
mockGenerateText.mockClear();
|
||||
mockLog.mockClear(); // Clear log mock
|
||||
});
|
||||
|
||||
describe('generateTextService', () => {
|
||||
test('should get client and call generateText with correct parameters', async () => {
|
||||
const mockClient = { type: 'mock-client' };
|
||||
mockGetClient.mockResolvedValue(mockClient);
|
||||
mockGenerateText.mockResolvedValue({ text: 'Mock response' });
|
||||
|
||||
const serviceParams = {
|
||||
role: 'main',
|
||||
session: { env: { SOME_KEY: 'value' } }, // Example session
|
||||
overrideOptions: { provider: 'override' }, // Example overrides
|
||||
prompt: 'Test prompt',
|
||||
// Other generateText options like maxTokens, temperature etc.
|
||||
maxTokens: 100
|
||||
};
|
||||
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
// Verify getClient call
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(1);
|
||||
expect(mockGetClient).toHaveBeenCalledWith(
|
||||
serviceParams.role,
|
||||
serviceParams.session,
|
||||
serviceParams.overrideOptions
|
||||
);
|
||||
|
||||
// Verify generateText call
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(1);
|
||||
expect(mockGenerateText).toHaveBeenCalledWith({
|
||||
model: mockClient, // Ensure the correct client is passed
|
||||
prompt: serviceParams.prompt,
|
||||
maxTokens: serviceParams.maxTokens
|
||||
// Add other expected generateText options here
|
||||
});
|
||||
|
||||
// Verify result
|
||||
expect(result).toEqual({ text: 'Mock response' });
|
||||
});
|
||||
|
||||
test('should retry generateText on specific errors and succeed', async () => {
|
||||
const mockClient = { type: 'mock-client' };
|
||||
mockGetClient.mockResolvedValue(mockClient);
|
||||
|
||||
// Simulate failure then success
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Rate limit exceeded')) // Retryable error
|
||||
.mockRejectedValueOnce(new Error('Service temporarily unavailable')) // Retryable error
|
||||
.mockResolvedValue({ text: 'Success after retries' });
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Retry test' };
|
||||
|
||||
// Use jest.advanceTimersByTime for delays if implemented
|
||||
// jest.useFakeTimers();
|
||||
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(1); // Client fetched once
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(3); // Initial call + 2 retries
|
||||
expect(result).toEqual({ text: 'Success after retries' });
|
||||
|
||||
// jest.useRealTimers(); // Restore real timers if faked
|
||||
});
|
||||
|
||||
test('should fail after exhausting retries', async () => {
|
||||
jest.setTimeout(15000); // Increase timeout further
|
||||
const mockClient = { type: 'mock-client' };
|
||||
mockGetClient.mockResolvedValue(mockClient);
|
||||
|
||||
// Simulate persistent failure
|
||||
mockGenerateText.mockRejectedValue(new Error('Rate limit exceeded'));
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Retry failure test' };
|
||||
|
||||
await expect(generateTextService(serviceParams)).rejects.toThrow(
|
||||
'Rate limit exceeded'
|
||||
);
|
||||
|
||||
// Sequence is main -> fallback -> research. It tries all client gets even if main fails.
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(3);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(3); // Initial call + max retries (assuming 2 retries)
|
||||
});
|
||||
|
||||
test('should not retry on non-retryable errors', async () => {
|
||||
const mockMainClient = { type: 'mock-main' };
|
||||
const mockFallbackClient = { type: 'mock-fallback' };
|
||||
const mockResearchClient = { type: 'mock-research' };
|
||||
|
||||
// Simulate a non-retryable error
|
||||
const nonRetryableError = new Error('Invalid request parameters');
|
||||
mockGenerateText.mockRejectedValueOnce(nonRetryableError); // Fail only once
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'No retry test' };
|
||||
|
||||
// Sequence is main -> fallback -> research. Even if main fails non-retryably,
|
||||
// it will still try to get clients for fallback and research before throwing.
|
||||
// Let's assume getClient succeeds for all three.
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient)
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
|
||||
await expect(generateTextService(serviceParams)).rejects.toThrow(
|
||||
'Invalid request parameters'
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(3); // Tries main, fallback, research
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(1); // Called only once for main
|
||||
});
|
||||
|
||||
test('should log service entry, client info, attempts, and success', async () => {
|
||||
const mockClient = {
|
||||
type: 'mock-client',
|
||||
provider: 'test-provider',
|
||||
model: 'test-model'
|
||||
}; // Add mock details
|
||||
mockGetClient.mockResolvedValue(mockClient);
|
||||
mockGenerateText.mockResolvedValue({ text: 'Success' });
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Log test' };
|
||||
await generateTextService(serviceParams);
|
||||
|
||||
// Check logs (in order)
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'info',
|
||||
'generateTextService called',
|
||||
{ role: 'main' }
|
||||
);
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'info',
|
||||
'Attempting service call with role: main'
|
||||
);
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'info',
|
||||
'Retrieved AI client',
|
||||
{
|
||||
provider: mockClient.provider,
|
||||
model: mockClient.model
|
||||
}
|
||||
);
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
expect.stringMatching(
|
||||
/Attempt 1\/3 calling generateText for role main/i
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
5,
|
||||
'info',
|
||||
'generateText succeeded for role main on attempt 1' // Original success log from helper
|
||||
);
|
||||
expect(mockLog).toHaveBeenNthCalledWith(
|
||||
6,
|
||||
'info',
|
||||
'generateTextService succeeded using role: main' // Final success log from service
|
||||
);
|
||||
|
||||
// Ensure no failure/retry logs were called
|
||||
expect(mockLog).not.toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining('failed')
|
||||
);
|
||||
expect(mockLog).not.toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining('Retrying')
|
||||
);
|
||||
});
|
||||
|
||||
test('should log retry attempts and eventual failure', async () => {
|
||||
jest.setTimeout(15000); // Increase timeout further
|
||||
const mockClient = {
|
||||
type: 'mock-client',
|
||||
provider: 'test-provider',
|
||||
model: 'test-model'
|
||||
};
|
||||
const mockFallbackClient = { type: 'mock-fallback' };
|
||||
const mockResearchClient = { type: 'mock-research' };
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient)
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
mockGenerateText.mockRejectedValue(new Error('Rate limit'));
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Log retry failure' };
|
||||
await expect(generateTextService(serviceParams)).rejects.toThrow(
|
||||
'Rate limit'
|
||||
);
|
||||
|
||||
// Check logs
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'generateTextService called',
|
||||
{ role: 'main' }
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: main'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith('info', 'Retrieved AI client', {
|
||||
provider: mockClient.provider,
|
||||
model: mockClient.model
|
||||
});
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/Attempt 1\/3 calling generateText for role main/i
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Attempt 1 failed for role main: Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Retryable error detected. Retrying in 1s...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/Attempt 2\/3 calling generateText for role main/i
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Attempt 2 failed for role main: Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Retryable error detected. Retrying in 2s...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
expect.stringMatching(
|
||||
/Attempt 3\/3 calling generateText for role main/i
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Attempt 3 failed for role main: Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Non-retryable error or max retries reached for role main (generateText).'
|
||||
);
|
||||
// Check subsequent fallback attempts (which also fail)
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: fallback'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role fallback: Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: research'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role research: Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'All roles in the sequence [main,fallback,research] failed.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should use fallback client after primary fails, then succeed', async () => {
|
||||
const mockMainClient = { type: 'mock-client', provider: 'main-provider' };
|
||||
const mockFallbackClient = {
|
||||
type: 'mock-client',
|
||||
provider: 'fallback-provider'
|
||||
};
|
||||
|
||||
// Setup calls: main client fails, fallback succeeds
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient) // First call for 'main' role
|
||||
.mockResolvedValueOnce(mockFallbackClient); // Second call for 'fallback' role
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Main Rate limit')) // Main attempt 1 fail
|
||||
.mockRejectedValueOnce(new Error('Main Rate limit')) // Main attempt 2 fail
|
||||
.mockRejectedValueOnce(new Error('Main Rate limit')) // Main attempt 3 fail
|
||||
.mockResolvedValue({ text: 'Fallback success' }); // Fallback attempt 1 success
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Fallback test' };
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
// Check calls
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(2);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'main',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'fallback',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(4); // 3 main fails, 1 fallback success
|
||||
expect(mockGenerateText).toHaveBeenNthCalledWith(4, {
|
||||
model: mockFallbackClient,
|
||||
prompt: 'Fallback test'
|
||||
});
|
||||
expect(result).toEqual({ text: 'Fallback success' });
|
||||
|
||||
// Check logs for fallback attempt
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role main: Main Rate limit'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Retries exhausted or non-retryable error for role main, trying next role in sequence...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: fallback'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'generateTextService succeeded using role: fallback'
|
||||
);
|
||||
});
|
||||
|
||||
test('should use research client after primary and fallback fail, then succeed', async () => {
|
||||
const mockMainClient = { type: 'mock-client', provider: 'main-provider' };
|
||||
const mockFallbackClient = {
|
||||
type: 'mock-client',
|
||||
provider: 'fallback-provider'
|
||||
};
|
||||
const mockResearchClient = {
|
||||
type: 'mock-client',
|
||||
provider: 'research-provider'
|
||||
};
|
||||
|
||||
// Setup calls: main fails, fallback fails, research succeeds
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient)
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Main fail 1')) // Main 1
|
||||
.mockRejectedValueOnce(new Error('Main fail 2')) // Main 2
|
||||
.mockRejectedValueOnce(new Error('Main fail 3')) // Main 3
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 1')) // Fallback 1
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 2')) // Fallback 2
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 3')) // Fallback 3
|
||||
.mockResolvedValue({ text: 'Research success' }); // Research 1 success
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Research fallback test' };
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
// Check calls
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(3);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'main',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'fallback',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'research',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(7); // 3 main, 3 fallback, 1 research
|
||||
expect(mockGenerateText).toHaveBeenNthCalledWith(7, {
|
||||
model: mockResearchClient,
|
||||
prompt: 'Research fallback test'
|
||||
});
|
||||
expect(result).toEqual({ text: 'Research success' });
|
||||
|
||||
// Check logs for fallback attempt
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role main: Main fail 3' // Error from last attempt for role
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Retries exhausted or non-retryable error for role main, trying next role in sequence...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role fallback: Fallback fail 3' // Error from last attempt for role
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Retries exhausted or non-retryable error for role fallback, trying next role in sequence...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: research'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'generateTextService succeeded using role: research'
|
||||
);
|
||||
});
|
||||
|
||||
test('should fail if primary, fallback, and research clients all fail', async () => {
|
||||
const mockMainClient = { type: 'mock-client', provider: 'main' };
|
||||
const mockFallbackClient = { type: 'mock-client', provider: 'fallback' };
|
||||
const mockResearchClient = { type: 'mock-client', provider: 'research' };
|
||||
|
||||
// Setup calls: all fail
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient)
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Main fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Main fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Main fail 3'))
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 3'))
|
||||
.mockRejectedValueOnce(new Error('Research fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Research fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Research fail 3')); // Last error
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'All fail test' };
|
||||
|
||||
await expect(generateTextService(serviceParams)).rejects.toThrow(
|
||||
'Research fail 3' // Should throw the error from the LAST failed attempt
|
||||
);
|
||||
|
||||
// Check calls
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(3);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(9); // 3 for each role
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'All roles in the sequence [main,fallback,research] failed.'
|
||||
);
|
||||
});
|
||||
|
||||
test('should handle error getting fallback client', async () => {
|
||||
const mockMainClient = { type: 'mock-client', provider: 'main' };
|
||||
|
||||
// Setup calls: main fails, getting fallback client fails, research succeeds (to test sequence)
|
||||
const mockResearchClient = { type: 'mock-client', provider: 'research' };
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient)
|
||||
.mockRejectedValueOnce(new Error('Cannot get fallback client'))
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Main fail 1'))
|
||||
.mockRejectedValueOnce(new Error('Main fail 2'))
|
||||
.mockRejectedValueOnce(new Error('Main fail 3')) // Main fails 3 times
|
||||
.mockResolvedValue({ text: 'Research success' }); // Research succeeds on its 1st attempt
|
||||
|
||||
const serviceParams = { role: 'main', prompt: 'Fallback client error' };
|
||||
|
||||
// Should eventually succeed with research after main+fallback fail
|
||||
const result = await generateTextService(serviceParams);
|
||||
expect(result).toEqual({ text: 'Research success' });
|
||||
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(3); // Tries main, fallback (fails), research
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(4); // 3 main attempts, 1 research attempt
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role fallback: Cannot get fallback client'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Could not get client for role fallback, trying next role in sequence...'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: research'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining(
|
||||
'generateTextService succeeded using role: research'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should try research after fallback fails if initial role is fallback', async () => {
|
||||
const mockFallbackClient = { type: 'mock-client', provider: 'fallback' };
|
||||
const mockResearchClient = { type: 'mock-client', provider: 'research' };
|
||||
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockFallbackClient)
|
||||
.mockResolvedValueOnce(mockResearchClient);
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 1')) // Fallback 1
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 2')) // Fallback 2
|
||||
.mockRejectedValueOnce(new Error('Fallback fail 3')) // Fallback 3
|
||||
.mockResolvedValue({ text: 'Research success' }); // Research 1
|
||||
|
||||
const serviceParams = { role: 'fallback', prompt: 'Start with fallback' };
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(2); // Fallback, Research
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'fallback',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'research',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(4); // 3 fallback, 1 research
|
||||
expect(result).toEqual({ text: 'Research success' });
|
||||
|
||||
// Check logs for sequence
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: fallback'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role fallback: Fallback fail 3'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(
|
||||
'Retries exhausted or non-retryable error for role fallback'
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: research'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining(
|
||||
'generateTextService succeeded using role: research'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should try fallback after research fails if initial role is research', async () => {
|
||||
const mockResearchClient = { type: 'mock-client', provider: 'research' };
|
||||
const mockFallbackClient = { type: 'mock-client', provider: 'fallback' };
|
||||
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockResearchClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient);
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Research fail 1')) // Research 1
|
||||
.mockRejectedValueOnce(new Error('Research fail 2')) // Research 2
|
||||
.mockRejectedValueOnce(new Error('Research fail 3')) // Research 3
|
||||
.mockResolvedValue({ text: 'Fallback success' }); // Fallback 1
|
||||
|
||||
const serviceParams = { role: 'research', prompt: 'Start with research' };
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(2); // Research, Fallback
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'research',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'fallback',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(4); // 3 research, 1 fallback
|
||||
expect(result).toEqual({ text: 'Fallback success' });
|
||||
|
||||
// Check logs for sequence
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: research'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'error',
|
||||
'Service call failed for role research: Research fail 3'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
expect.stringContaining(
|
||||
'Retries exhausted or non-retryable error for role research'
|
||||
)
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
'Attempting service call with role: fallback'
|
||||
);
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'info',
|
||||
expect.stringContaining(
|
||||
'generateTextService succeeded using role: fallback'
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
test('should use default sequence and log warning for unknown initial role', async () => {
|
||||
const mockMainClient = { type: 'mock-client', provider: 'main' };
|
||||
const mockFallbackClient = { type: 'mock-client', provider: 'fallback' };
|
||||
|
||||
mockGetClient
|
||||
.mockResolvedValueOnce(mockMainClient)
|
||||
.mockResolvedValueOnce(mockFallbackClient);
|
||||
mockGenerateText
|
||||
.mockRejectedValueOnce(new Error('Main fail 1')) // Main 1
|
||||
.mockRejectedValueOnce(new Error('Main fail 2')) // Main 2
|
||||
.mockRejectedValueOnce(new Error('Main fail 3')) // Main 3
|
||||
.mockResolvedValue({ text: 'Fallback success' }); // Fallback 1
|
||||
|
||||
const serviceParams = {
|
||||
role: 'invalid-role',
|
||||
prompt: 'Unknown role test'
|
||||
};
|
||||
const result = await generateTextService(serviceParams);
|
||||
|
||||
// Check warning log for unknown role
|
||||
expect(mockLog).toHaveBeenCalledWith(
|
||||
'warn',
|
||||
'Unknown initial role: invalid-role. Defaulting to main -> fallback -> research sequence.'
|
||||
);
|
||||
|
||||
// Check it followed the default main -> fallback sequence
|
||||
expect(mockGetClient).toHaveBeenCalledTimes(2); // Main, Fallback
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'main',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGetClient).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'fallback',
|
||||
undefined,
|
||||
undefined
|
||||
);
|
||||
expect(mockGenerateText).toHaveBeenCalledTimes(4); // 3 main, 1 fallback
|
||||
expect(result).toEqual({ text: 'Fallback success' });
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user