buroventures-harald-code-core
Version:
Harald Code Core - Core functionality for AI-powered coding assistant
258 lines • 11.6 kB
JavaScript
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { OpenAIContentGenerator } from '../openaiContentGenerator.js';
import OpenAI from 'openai';
// Mock OpenAI
vi.mock('openai');
// Mock logger modules
vi.mock('../../telemetry/loggers.js', () => ({
logApiResponse: vi.fn(),
}));
vi.mock('../../utils/openaiLogger.js', () => ({
openaiLogger: {
logInteraction: vi.fn(),
},
}));
describe('OpenAIContentGenerator Timeout Handling', () => {
let generator;
let mockConfig;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let mockOpenAIClient;
beforeEach(() => {
// Reset mocks
vi.clearAllMocks();
// Mock environment variables
vi.stubEnv('OPENAI_BASE_URL', '');
// Mock config
mockConfig = {
getContentGeneratorConfig: vi.fn().mockReturnValue({
authType: 'openai',
enableOpenAILogging: false,
timeout: 120000,
maxRetries: 3,
}),
};
// Mock OpenAI client
mockOpenAIClient = {
chat: {
completions: {
create: vi.fn(),
},
},
};
vi.mocked(OpenAI).mockImplementation(() => mockOpenAIClient);
// Create generator instance
generator = new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig);
});
afterEach(() => {
vi.restoreAllMocks();
});
describe('timeout error identification through actual requests', () => {
it('should handle various timeout error formats correctly', async () => {
const timeoutErrors = [
new Error('Request timeout'),
new Error('Connection timed out'),
new Error('ETIMEDOUT'),
Object.assign(new Error('Network error'), { code: 'ETIMEDOUT' }),
Object.assign(new Error('Socket error'), { code: 'ESOCKETTIMEDOUT' }),
Object.assign(new Error('API error'), { type: 'timeout' }),
new Error('request timed out'),
new Error('deadline exceeded'),
];
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
for (const error of timeoutErrors) {
mockOpenAIClient.chat.completions.create.mockRejectedValueOnce(error);
try {
await generator.generateContent(request);
}
catch (thrownError) {
// Should contain timeout-specific messaging and troubleshooting tips
const errorMessage = thrownError instanceof Error
? thrownError.message
: String(thrownError);
expect(errorMessage).toMatch(/timeout after \d+s|Troubleshooting tips:/);
}
}
});
it('should handle non-timeout errors without timeout messaging', async () => {
const nonTimeoutErrors = [
new Error('Invalid API key'),
new Error('Rate limit exceeded'),
new Error('Model not found'),
Object.assign(new Error('Auth error'), { code: 'INVALID_REQUEST' }),
Object.assign(new Error('API error'), { type: 'authentication_error' }),
];
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
for (const error of nonTimeoutErrors) {
mockOpenAIClient.chat.completions.create.mockRejectedValueOnce(error);
try {
await generator.generateContent(request);
}
catch (thrownError) {
// Should NOT contain timeout-specific messaging
const errorMessage = thrownError instanceof Error
? thrownError.message
: String(thrownError);
expect(errorMessage).not.toMatch(/timeout after \d+s/);
expect(errorMessage).not.toMatch(/Troubleshooting tips:/);
expect(errorMessage).toMatch(/OpenAI API error:/);
}
}
});
});
describe('generateContent timeout handling', () => {
it('should handle timeout errors with helpful message', async () => {
// Mock timeout error
const timeoutError = new Error('Request timeout');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
await expect(generator.generateContent(request)).rejects.toThrow(/Request timeout after \d+s\. Try reducing input length or increasing timeout in config\./);
});
it('should handle non-timeout errors normally', async () => {
// Mock non-timeout error
const apiError = new Error('Invalid API key');
mockOpenAIClient.chat.completions.create.mockRejectedValue(apiError);
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
await expect(generator.generateContent(request)).rejects.toThrow('OpenAI API error: Invalid API key');
});
it('should include troubleshooting tips for timeout errors', async () => {
const timeoutError = new Error('Connection timed out');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
try {
await generator.generateContent(request);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
expect(errorMessage).toContain('Troubleshooting tips:');
expect(errorMessage).toContain('Reduce input length or complexity');
expect(errorMessage).toContain('Increase timeout in config');
expect(errorMessage).toContain('Check network connectivity');
expect(errorMessage).toContain('Consider using streaming mode');
}
});
});
describe('generateContentStream timeout handling', () => {
it('should handle streaming timeout errors', async () => {
const timeoutError = new Error('Streaming timeout');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
await expect(generator.generateContentStream(request)).rejects.toThrow(/Streaming setup timeout after \d+s\. Try reducing input length or increasing timeout in config\./);
});
it('should include streaming-specific troubleshooting tips', async () => {
const timeoutError = new Error('request timed out');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello' }] }],
model: 'gpt-4',
};
try {
await generator.generateContentStream(request);
}
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
expect(errorMessage).toContain('Streaming setup timeout troubleshooting:');
expect(errorMessage).toContain('Check network connectivity and firewall settings');
expect(errorMessage).toContain('Consider using non-streaming mode');
}
});
});
describe('timeout configuration', () => {
it('should use default timeout configuration', () => {
new OpenAIContentGenerator('test-key', 'gpt-4', mockConfig);
// Verify OpenAI client was created with timeout config
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-key',
baseURL: '',
timeout: 120000,
maxRetries: 3,
});
});
it('should use custom timeout from config', () => {
const customConfig = {
getContentGeneratorConfig: vi.fn().mockReturnValue({
timeout: 300000, // 5 minutes
maxRetries: 5,
}),
};
new OpenAIContentGenerator('test-key', 'gpt-4', customConfig);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-key',
baseURL: '',
timeout: 300000,
maxRetries: 5,
});
});
it('should handle missing timeout config gracefully', () => {
const noTimeoutConfig = {
getContentGeneratorConfig: vi.fn().mockReturnValue({}),
};
new OpenAIContentGenerator('test-key', 'gpt-4', noTimeoutConfig);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-key',
baseURL: '',
timeout: 120000, // default
maxRetries: 3, // default
});
});
});
describe('token estimation on timeout', () => {
it('should estimate tokens even when request times out', async () => {
const timeoutError = new Error('Request timeout');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
// Mock countTokens to return a value
const mockCountTokens = vi.spyOn(generator, 'countTokens');
mockCountTokens.mockResolvedValue({ totalTokens: 100 });
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello world' }] }],
model: 'gpt-4',
};
try {
await generator.generateContent(request);
}
catch (_error) {
// Verify that countTokens was called for estimation
expect(mockCountTokens).toHaveBeenCalledWith({
contents: request.contents,
model: 'gpt-4',
});
}
});
it('should fall back to character-based estimation if countTokens fails', async () => {
const timeoutError = new Error('Request timeout');
mockOpenAIClient.chat.completions.create.mockRejectedValue(timeoutError);
// Mock countTokens to throw error
const mockCountTokens = vi.spyOn(generator, 'countTokens');
mockCountTokens.mockRejectedValue(new Error('Count tokens failed'));
const request = {
contents: [{ role: 'user', parts: [{ text: 'Hello world' }] }],
model: 'gpt-4',
};
// Should not throw due to token counting failure
await expect(generator.generateContent(request)).rejects.toThrow(/Request timeout after \d+s/);
});
});
});
//# sourceMappingURL=openaiTimeoutHandling.test.js.map