- Fixed MCP server initialization warnings by refactoring config-manager.js to handle missing project roots silently during startup - Added project root tracking (loadedConfigRoot) to improve config caching and prevent unnecessary reloads - Modified _loadAndValidateConfig to return defaults without warnings when no explicitRoot provided - Improved getConfig to only update cache when loading config with a specific project root - Ensured warning messages still appear when explicitly specified roots have missing/invalid configs - Prevented console output during MCP startup that was causing JSON parsing errors - Verified parse_prd and other MCP tools still work correctly with the new config loading approach. - Replaces test perplexity api key in mcp.json and rolls it. It's invalid now.
684 lines
22 KiB
JavaScript
684 lines
22 KiB
JavaScript
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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI service call with role: fallback'
|
|
);
|
|
expect(mockLog).toHaveBeenCalledWith(
|
|
'error',
|
|
'Service call failed for role fallback: Rate limit'
|
|
);
|
|
expect(mockLog).toHaveBeenCalledWith(
|
|
'info',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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',
|
|
'New AI 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' });
|
|
});
|
|
});
|
|
});
|