import { jest } from '@jest/globals'; // Mock modules before importing jest.unstable_mockModule('@ai-sdk/provider', () => ({ NoSuchModelError: class NoSuchModelError extends Error { constructor({ modelId, modelType }) { super(`No such model: ${modelId}`); this.modelId = modelId; this.modelType = modelType; } } })); jest.unstable_mockModule('@ai-sdk/provider-utils', () => ({ generateId: jest.fn(() => 'test-id-123') })); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/message-converter.js', () => ({ convertToClaudeCodeMessages: jest.fn((prompt) => ({ messagesPrompt: 'converted-prompt', systemPrompt: 'system' })) }) ); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/json-extractor.js', () => ({ extractJson: jest.fn((text) => text) }) ); jest.unstable_mockModule( '../../../../../src/ai-providers/custom-sdk/claude-code/errors.js', () => ({ createAPICallError: jest.fn((opts) => new Error(opts.message)), createAuthenticationError: jest.fn((opts) => new Error(opts.message)) }) ); // This mock will be controlled by tests let mockClaudeCodeModule = null; jest.unstable_mockModule('@anthropic-ai/claude-code', () => { if (mockClaudeCodeModule) { return mockClaudeCodeModule; } throw new Error("Cannot find module '@anthropic-ai/claude-code'"); }); // Import the module under test const { ClaudeCodeLanguageModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); describe('ClaudeCodeLanguageModel', () => { beforeEach(() => { jest.clearAllMocks(); // Reset the module mock mockClaudeCodeModule = null; // Clear module cache to ensure fresh imports jest.resetModules(); }); describe('constructor', () => { it('should initialize with valid model ID', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: { maxTurns: 5 } }); expect(model.modelId).toBe('opus'); expect(model.settings).toEqual({ maxTurns: 5 }); expect(model.provider).toBe('claude-code'); }); it('should throw NoSuchModelError for invalid model ID', async () => { expect( () => new ClaudeCodeLanguageModel({ id: '', settings: {} }) ).toThrow('No such model: '); expect( () => new ClaudeCodeLanguageModel({ id: null, settings: {} }) ).toThrow('No such model: null'); }); }); describe('lazy loading of @anthropic-ai/claude-code', () => { it('should throw error when package is not installed', async () => { // Keep mockClaudeCodeModule as null to simulate missing package const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow( "Claude Code SDK is not installed. Please install '@anthropic-ai/claude-code' to use the claude-code provider." ); }); it('should load package successfully when available', async () => { // Mock successful package load const mockQuery = jest.fn(async function* () { yield { type: 'assistant', message: { content: [{ type: 'text', text: 'Hello' }] } }; yield { type: 'result', subtype: 'done', usage: { output_tokens: 10, input_tokens: 5 } }; }); mockClaudeCodeModule = { query: mockQuery, AbortError: class AbortError extends Error {} }; // Need to re-import to get fresh module with mocks jest.resetModules(); const { ClaudeCodeLanguageModel: FreshModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); const model = new FreshModel({ id: 'opus', settings: {} }); const result = await model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }); expect(result.text).toBe('Hello'); expect(mockQuery).toHaveBeenCalled(); }); it('should only attempt to load package once', async () => { // Get a fresh import to ensure clean state jest.resetModules(); const { ClaudeCodeLanguageModel: TestModel } = await import( '../../../../../src/ai-providers/custom-sdk/claude-code/language-model.js' ); const model = new TestModel({ id: 'opus', settings: {} }); // First call should throw await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow('Claude Code SDK is not installed'); // Second call should also throw without trying to load again await expect( model.doGenerate({ prompt: [{ role: 'user', content: 'test' }], mode: { type: 'regular' } }) ).rejects.toThrow('Claude Code SDK is not installed'); }); }); describe('generateUnsupportedWarnings', () => { it('should generate warnings for unsupported parameters', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); const warnings = model.generateUnsupportedWarnings({ temperature: 0.7, maxTokens: 1000, topP: 0.9, seed: 42 }); expect(warnings).toHaveLength(4); expect(warnings[0]).toEqual({ type: 'unsupported-setting', setting: 'temperature', details: 'Claude Code CLI does not support the temperature parameter. It will be ignored.' }); }); it('should return empty array when no unsupported parameters', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); const warnings = model.generateUnsupportedWarnings({}); expect(warnings).toEqual([]); }); }); describe('getModel', () => { it('should map model IDs correctly', () => { const model = new ClaudeCodeLanguageModel({ id: 'opus', settings: {} }); expect(model.getModel()).toBe('opus'); }); it('should return unmapped model IDs as-is', () => { const model = new ClaudeCodeLanguageModel({ id: 'custom-model', settings: {} }); expect(model.getModel()).toBe('custom-model'); }); }); });