sourcesailor
Version:
A CLI tool for analyzing and documenting codebases
294 lines (293 loc) • 15.5 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import Anthropic from '@anthropic-ai/sdk';
import AnthropicInterface from '../anthropic.mjs';
// Mock Anthropic
vi.mock('@anthropic-ai/sdk');
// Mock console.log to prevent output during tests
vi.spyOn(console, 'log').mockImplementation(() => { });
// Mock Stream
class MockStream {
constructor(chunks) {
this.chunks = chunks;
}
async *[Symbol.asyncIterator]() {
for (const chunk of this.chunks) {
yield { type: 'content_block_delta', delta: { type: 'text_delta', text: chunk } };
}
}
}
describe('AnthropicInterface', () => {
let anthropicInterface;
let mockAnthropic;
beforeEach(() => {
mockAnthropic = {
messages: {
create: vi.fn(),
},
};
Anthropic.mockImplementation(() => mockAnthropic);
anthropicInterface = new AnthropicInterface();
// Mock getModel method
vi.spyOn(anthropicInterface, 'getModel').mockResolvedValue('claude-3-opus-20240229');
});
afterEach(() => {
vi.resetAllMocks();
});
describe('getName', () => {
it('should return "Anthropic"', () => {
expect(anthropicInterface.getName()).toBe('Anthropic');
});
});
describe('listModels', () => {
it('should return a list of available models', async () => {
const expectedModels = [
'claude-3-haiku',
'claude-3.5-haiku',
'claude-3.5-haiku-latest',
'claude-3-sonnet',
'claude-3-opus',
'claude-3.5-sonnet',
'claude-3.5-sonnet-legacy',
'claude-3.5-sonnet-latest',
'haiku-3',
'haiku-3.5',
'haiku-3.5-latest',
'sonnet-3',
'opus-3',
'sonnet-3.5',
'sonnet-3.5-legacy',
'sonnet-3.5-latest'
];
const result = await anthropicInterface.listModels(false);
expect(result).toEqual(expectedModels);
});
it('should log models when verbose is true', async () => {
const consoleSpy = vi.spyOn(console, 'log');
await anthropicInterface.listModels(true);
expect(consoleSpy).toHaveBeenCalledWith('Available Anthropic models:', expect.any(String));
});
});
describe('inferProjectDirectory', () => {
it('should return a valid inference for a project directory', async () => {
const mockResponse = {
content: [{
type: 'text',
text: '{"projectType":"nodejs","mainLanguage":"typescript"}'
}],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferProjectDirectory('{"src": {}, "package.json": {}}', false, false);
expect(result).toBe('{"projectType":"nodejs","mainLanguage":"typescript"}');
});
it('should handle responses without tool use', async () => {
const mockResponse = {
content: [{ type: 'text', text: '{"projectType":"python","mainLanguage":"python"}' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferProjectDirectory('{"src": {}, "requirements.txt": {}}', false, false);
expect(result).toBe('{"projectType":"python","mainLanguage":"python"}');
});
it('should handle empty responses', async () => {
const mockResponse = {
content: [],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferProjectDirectory('{}', false, false);
expect(result).toBe('');
});
it('should handle invalid JSON input', async () => {
await expect(anthropicInterface.inferProjectDirectory('invalid json', false, false)).rejects.toThrow();
});
it('should use a non-default model when specified', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Project directory inference' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
vi.spyOn(anthropicInterface, 'getModel').mockReturnValue('claude-3-sonnet-20240229');
await anthropicInterface.inferProjectDirectory('{"src": {}}', false, false, undefined, 'claude-3-sonnet-20240229');
expect(mockAnthropic.messages.create).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-sonnet-20240229' }));
});
});
describe('inferDependency', () => {
it('should return a valid inference for dependencies', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Dependency inference' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferDependency('{"dependencies": {"react": "^17.0.2"}}', 'build', false, false);
expect(result).toBe('Dependency inference');
});
it('should handle streaming responses', async () => {
const mockStream = new MockStream(['Dependency', ' inference', ' streaming']);
mockAnthropic.messages.create.mockResolvedValue(mockStream);
const result = await anthropicInterface.inferDependency('{"dependencies": {}}', 'test', true, true);
expect(result).toBeInstanceOf(Object);
expect(Symbol.asyncIterator in result).toBe(true);
let streamedContent = '';
for await (const chunk of result) {
streamedContent += chunk;
}
expect(streamedContent).toBe('Dependency inference streaming');
});
it('should handle empty dependency file', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'No dependencies found' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferDependency('{}', 'build', false, false);
expect(result).toBe('No dependencies found');
});
it('should handle invalid workflow input', async () => {
await expect(anthropicInterface.inferDependency('{"dependencies": {}}', '', false, false)).rejects.toThrow();
});
});
describe('inferCode', () => {
it('should return a valid inference for code', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Code inference: This is a React component' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferCode('function App() { return <div>Hello</div>; }', false, false);
expect(result).toBe('Code inference: This is a React component');
});
it('should handle streaming responses', async () => {
const mockStream = new MockStream(['Code', ' inference:', ' This', ' is', ' a', ' React', ' component']);
mockAnthropic.messages.create.mockResolvedValue(mockStream);
const result = await anthropicInterface.inferCode('function App() { return <div>Hello</div>; }', true, true);
expect(result).toBeInstanceOf(Object);
expect(Symbol.asyncIterator in result).toBe(true);
let streamedContent = '';
for await (const chunk of result) {
streamedContent += chunk;
}
expect(streamedContent).toBe('Code inference: This is a React component');
});
it('should handle empty code input', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'No code provided' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferCode('', false, false);
expect(result).toBe('No code provided');
});
it('should use a non-default model when specified', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Code inference' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
vi.spyOn(anthropicInterface, 'getModel').mockReturnValue('claude-3-sonnet-20240229');
await anthropicInterface.inferCode('const x = 5;', false, true, undefined, 'claude-3-sonnet-20240229');
expect(mockAnthropic.messages.create).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-sonnet-20240229' }));
});
});
describe('inferInterestingCode', () => {
it('should return interesting parts of the code', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Interesting code: Complex algorithm found' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferInterestingCode('function complexAlgorithm() { /* ... */ }', false, false);
expect(result).toBe('Interesting code: Complex algorithm found');
});
it('should handle streaming responses', async () => {
const mockStream = new MockStream(['Interesting', ' code:', ' Complex', ' algorithm', ' found']);
mockAnthropic.messages.create.mockResolvedValue(mockStream);
const result = await anthropicInterface.inferInterestingCode('class AdvancedComponent { /* ... */ }', true, true);
expect(result).toBeInstanceOf(Object);
expect(Symbol.asyncIterator in result).toBe(true);
let streamedContent = '';
for await (const chunk of result) {
streamedContent += chunk;
}
expect(streamedContent).toBe('Interesting code: Complex algorithm found');
});
it('should handle code with no particularly interesting parts', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'No particularly interesting code parts found' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.inferInterestingCode('console.log("Hello, World!");', false, false);
expect(result).toBe('No particularly interesting code parts found');
});
it('should handle code in different languages', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Interesting Java code: Main method implementation' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const javaCode = `
public class HelloWorld {
public static void main(String[] args) {
System.out.println("Hello, World!");
}
}
`;
const result = await anthropicInterface.inferInterestingCode(javaCode, false, false);
expect(result).toBe('Interesting Java code: Main method implementation');
});
it('should use a non-default model when specified', async () => {
const mockResponse = {
content: [{ type: 'text', text: 'Interesting code found' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
vi.spyOn(anthropicInterface, 'getModel').mockReturnValue('claude-3-sonnet-20240229');
await anthropicInterface.inferInterestingCode('function complexAlgorithm() {}', false, true, undefined, 'claude-3-sonnet-20240229');
expect(mockAnthropic.messages.create).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-sonnet-20240229' }));
});
});
describe('generateReadme', () => {
it('should generate a README based on provided information', async () => {
const mockResponse = {
content: [{ type: 'text', text: '# Project Name\n\nThis is a generated README.' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.generateReadme('{"src": {}, "package.json": {}}', '{"dependencies": {"react": "^17.0.2"}}', 'function App() { return <div>Hello</div>; }', false, false);
expect(result).toBe('# Project Name\n\nThis is a generated README.');
});
it('should handle empty inputs', async () => {
const mockResponse = {
content: [{ type: 'text', text: '# Empty Project\n\nNo project structure or code provided.' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
const result = await anthropicInterface.generateReadme('{}', '{}', '{}', false, false);
expect(result).toBe('# Empty Project\n\nNo project structure or code provided.');
});
it('should handle streaming responses', async () => {
const mockStream = new MockStream(['# Project', ' Name\n\n', 'This is a ', 'generated README.']);
mockAnthropic.messages.create.mockResolvedValue(mockStream);
const result = await anthropicInterface.generateReadme('{"src": {}}', '{"dependencies": {}}', 'const x = 5;', true, true);
expect(result).toBeInstanceOf(Object);
expect(Symbol.asyncIterator in result).toBe(true);
let streamedContent = '';
for await (const chunk of result) {
streamedContent += chunk;
}
expect(streamedContent).toBe('# Project Name\n\nThis is a generated README.');
});
it('should use a non-default model when specified', async () => {
const mockResponse = {
content: [{ type: 'text', text: '# Generated README' }],
stop_reason: 'end_turn'
};
mockAnthropic.messages.create.mockResolvedValue(mockResponse);
vi.spyOn(anthropicInterface, 'getModel').mockReturnValue('claude-3-sonnet-20240229');
await anthropicInterface.generateReadme('{}', '{}', '{}', false, true, undefined, 'claude-3-sonnet-20240229');
expect(mockAnthropic.messages.create).toHaveBeenCalledWith(expect.objectContaining({ model: 'claude-3-sonnet-20240229' }));
});
});
});