Files
claude-task-master/tests/unit/ai-providers/custom-sdk/claude-code/language-model.test.js
Ben Vargas 9995075093 test: add comprehensive tests for ClaudeCodeProvider
Addresses code review feedback about missing automated tests for the ClaudeCodeProvider.

## Changes

- Added unit tests for ClaudeCodeProvider class covering constructor, validateAuth, and getClient methods
- Added unit tests for ClaudeCodeLanguageModel testing lazy loading behavior and error handling
- Added integration tests verifying optional dependency behavior when @anthropic-ai/claude-code is not installed

## Test Coverage

1. **Unit Tests**:
   - ClaudeCodeProvider: Basic functionality, no API key requirement, client creation
   - ClaudeCodeLanguageModel: Model initialization, lazy loading, error messages, warning generation

2. **Integration Tests**:
   - Optional dependency behavior when package is not installed
   - Clear error messages for users about missing package
   - Provider instantiation works but usage fails gracefully

All tests pass and provide comprehensive coverage for the claude-code provider implementation.
2025-06-20 16:25:22 +03:00

200 lines
5.8 KiB
JavaScript

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');
});
});
});