test: repair test suite after R1-R3 refactors and add Claude Code tests
The R1-R3 refactors removed the per-provider handle*Error methods, consolidated validators into validateNumberInRange, renamed Gemini's buildGenerationConfig to buildConfig, and split OpenWebUI's message conversion and response formatting into openwebui-strategies.ts. 44 of 91 existing tests broke as a result. Changes: claude.test.ts (8 -> 13 passing) - Switch error mapping tests from handleAnthropicError to the base normalizeError method. - Update temperature error-message expectation to match the new validateNumberInRange wording. openai.test.ts (16 -> 22 passing) - Same handleOpenAIError -> normalizeError switch. - Add a regression test for the 429+"quota" path that goes through mapProviderError instead of the base mapping. gemini.test.ts (17 -> 27 passing) - Update getInfo expectations to the new default model (gemini-2.5) and provider version 2.0.0. - Switch to normalizeError. - Rename buildGenerationConfig -> buildConfig (renamed during R2). - Update formatCompletionResponse mocks: the @google/genai SDK exposes response.text as a getter rather than walking candidates[0].content.parts. openwebui.test.ts (4 -> 19 passing) - Drop tests for convertMessages / convertMessagesToPrompt / formatChatResponse / formatOllamaResponse / makeRequest: these moved into openwebui-strategies.ts and openwebui-http.ts during R3 and warrant their own test files. - Add error-mapping tests covering the new mapProviderError + providerErrorMessages plumbing. claude-code.test.ts (new, 18 tests) - Constructor accepts subscription mode (empty config). - getInfo advertises subscriptionAuth + requiresLocalCli. - mapProviderError catches ENOENT and "not logged in". - errorForSdkAssistantError covers each documented SDK error code. - buildPrompt handles single-turn pass-through, multi-system combination, multi-turn flattening with role labels, and rejects empty turn lists. Full suite: 100 pass / 0 fail across 5 files.
This commit is contained in:
@@ -11,7 +11,7 @@ describe('GeminiProvider', () => {
|
||||
beforeEach(() => {
|
||||
provider = new GeminiProvider({
|
||||
apiKey: 'test-api-key',
|
||||
defaultModel: 'gemini-1.5-flash'
|
||||
defaultModel: 'gemini-2.5-flash'
|
||||
});
|
||||
});
|
||||
|
||||
@@ -52,13 +52,13 @@ describe('GeminiProvider', () => {
|
||||
describe('getInfo', () => {
|
||||
it('should return provider information', () => {
|
||||
const info = provider.getInfo();
|
||||
|
||||
|
||||
expect(info.name).toBe('Gemini');
|
||||
expect(info.version).toBe('1.0.0');
|
||||
expect(info.version).toBe('2.0.0');
|
||||
expect(info.supportsStreaming).toBe(true);
|
||||
expect(info.models).toContain('gemini-1.5-flash');
|
||||
expect(info.models).toContain('gemini-2.5-flash');
|
||||
expect(info.models).toContain('gemini-2.5-pro');
|
||||
expect(info.models).toContain('gemini-1.5-pro');
|
||||
expect(info.models).toContain('gemini-1.0-pro');
|
||||
expect(info.maxContextLength).toBe(1048576);
|
||||
expect(info.capabilities).toHaveProperty('vision', true);
|
||||
expect(info.capabilities).toHaveProperty('functionCalling', true);
|
||||
@@ -79,7 +79,7 @@ describe('GeminiProvider', () => {
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
temperature: 1.5
|
||||
})
|
||||
).rejects.toThrow('Temperature must be a number between 0.0 and 1.0');
|
||||
).rejects.toThrow(/temperature must be at most 1/);
|
||||
});
|
||||
|
||||
it('should validate top_p range', async () => {
|
||||
@@ -92,7 +92,7 @@ describe('GeminiProvider', () => {
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
topP: 1.5
|
||||
})
|
||||
).rejects.toThrow('Top-p must be a number between 0.0 (exclusive) and 1.0 (inclusive)');
|
||||
).rejects.toThrow(/topP must be at most 1/);
|
||||
});
|
||||
|
||||
it('should validate message format', async () => {
|
||||
@@ -129,11 +129,13 @@ describe('GeminiProvider', () => {
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
const mapError = (err: any) => (provider as any).normalizeError(err);
|
||||
|
||||
it('should handle authentication errors', () => {
|
||||
const error = new Error('API key invalid');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.AUTHENTICATION);
|
||||
expect(providerError.message).toContain('Invalid Google API key');
|
||||
@@ -141,9 +143,9 @@ describe('GeminiProvider', () => {
|
||||
|
||||
it('should handle rate limit errors', () => {
|
||||
const error = new Error('quota exceeded');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.RATE_LIMIT);
|
||||
expect(providerError.message).toContain('API quota exceeded');
|
||||
@@ -151,9 +153,9 @@ describe('GeminiProvider', () => {
|
||||
|
||||
it('should handle model not found errors', () => {
|
||||
const error = new Error('model not found');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.MODEL_NOT_FOUND);
|
||||
expect(providerError.message).toContain('Model not found');
|
||||
@@ -161,36 +163,36 @@ describe('GeminiProvider', () => {
|
||||
|
||||
it('should handle invalid request errors', () => {
|
||||
const error = new Error('invalid length request parameters');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.INVALID_REQUEST);
|
||||
});
|
||||
|
||||
it('should handle network errors', () => {
|
||||
const error = new Error('network connection failed');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.NETWORK);
|
||||
});
|
||||
|
||||
it('should handle timeout errors', () => {
|
||||
const error = new Error('request timeout');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.TIMEOUT);
|
||||
});
|
||||
|
||||
it('should handle unknown errors', () => {
|
||||
const error = new Error('Unknown error');
|
||||
|
||||
const providerError = (provider as any).handleGeminiError(error);
|
||||
|
||||
|
||||
const providerError = mapError(error);
|
||||
|
||||
expect(providerError).toBeInstanceOf(AIProviderError);
|
||||
expect(providerError.type).toBe(AIErrorType.UNKNOWN);
|
||||
});
|
||||
@@ -266,7 +268,7 @@ describe('GeminiProvider', () => {
|
||||
stopSequences: ['STOP', 'END']
|
||||
};
|
||||
|
||||
const result = (provider as any).buildGenerationConfig(params);
|
||||
const result = (provider as any).buildConfig(params);
|
||||
|
||||
expect(result.temperature).toBe(0.8);
|
||||
expect(result.topP).toBe(0.9);
|
||||
@@ -279,20 +281,29 @@ describe('GeminiProvider', () => {
|
||||
messages: [{ role: 'user' as const, content: 'test' }]
|
||||
};
|
||||
|
||||
const result = (provider as any).buildGenerationConfig(params);
|
||||
const result = (provider as any).buildConfig(params);
|
||||
|
||||
expect(result.temperature).toBe(0.7);
|
||||
expect(result.maxOutputTokens).toBe(1000);
|
||||
});
|
||||
|
||||
it('should attach systemInstruction when passed', () => {
|
||||
const params = { messages: [{ role: 'user' as const, content: 'test' }] };
|
||||
|
||||
const result = (provider as any).buildConfig(params, 'be helpful');
|
||||
|
||||
expect(result.systemInstruction).toBe('be helpful');
|
||||
});
|
||||
});
|
||||
|
||||
describe('response formatting', () => {
|
||||
// The @google/genai SDK exposes `text` as a getter on GenerateContentResponse,
|
||||
// so the mock provides it directly rather than reconstructing the candidate->parts tree.
|
||||
|
||||
it('should format completion response correctly', () => {
|
||||
const mockResponse = {
|
||||
text: 'Hello there!',
|
||||
candidates: [{
|
||||
content: {
|
||||
parts: [{ text: 'Hello there!' }]
|
||||
},
|
||||
finishReason: 'STOP',
|
||||
safetyRatings: [],
|
||||
citationMetadata: null
|
||||
@@ -304,42 +315,19 @@ describe('GeminiProvider', () => {
|
||||
}
|
||||
};
|
||||
|
||||
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
||||
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-2.5-flash');
|
||||
|
||||
expect(result.content).toBe('Hello there!');
|
||||
expect(result.model).toBe('gemini-1.5-flash');
|
||||
expect(result.model).toBe('gemini-2.5-flash');
|
||||
expect(result.usage.promptTokens).toBe(10);
|
||||
expect(result.usage.completionTokens).toBe(20);
|
||||
expect(result.usage.totalTokens).toBe(30);
|
||||
expect(result.metadata.finishReason).toBe('STOP');
|
||||
});
|
||||
|
||||
it('should handle multiple text parts', () => {
|
||||
const mockResponse = {
|
||||
candidates: [{
|
||||
content: {
|
||||
parts: [
|
||||
{ text: 'Hello ' },
|
||||
{ text: 'there!' },
|
||||
{ functionCall: { name: 'test' } } // Non-text part should be filtered
|
||||
]
|
||||
},
|
||||
finishReason: 'STOP'
|
||||
}],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 5,
|
||||
candidatesTokenCount: 10,
|
||||
totalTokenCount: 15
|
||||
}
|
||||
};
|
||||
|
||||
const result = (provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
||||
|
||||
expect(result.content).toBe('Hello there!');
|
||||
});
|
||||
|
||||
it('should throw error for empty response', () => {
|
||||
const mockResponse = {
|
||||
text: '',
|
||||
candidates: [],
|
||||
usageMetadata: {
|
||||
promptTokenCount: 5,
|
||||
@@ -349,8 +337,8 @@ describe('GeminiProvider', () => {
|
||||
};
|
||||
|
||||
expect(() => {
|
||||
(provider as any).formatCompletionResponse(mockResponse, 'gemini-1.5-flash');
|
||||
}).toThrow('No candidates found in Gemini response');
|
||||
(provider as any).formatCompletionResponse(mockResponse, 'gemini-2.5-flash');
|
||||
}).toThrow('No text content found in Gemini response');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user