238 lines
6.0 KiB
JavaScript
238 lines
6.0 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');
|
|
});
|
|
});
|
|
});
|