UNPKG

sourcesailor

Version:

A CLI tool for analyzing and documenting codebases

386 lines (385 loc) 19.8 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import OpenAI from 'openai'; import OpenAIInferrence from "../openai.mjs"; // Mock OpenAI vi.mock('openai'); // 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 { choices: [{ delta: { content: chunk } }] }; } } } describe('OpenAIInferrence', () => { let openAIInferrence; let mockOpenAI; beforeEach(() => { mockOpenAI = { chat: { completions: { create: vi.fn(), }, }, models: { list: vi.fn(), }, }; OpenAI.mockImplementation(() => mockOpenAI); openAIInferrence = new OpenAIInferrence(); // Mock getModel method vi.spyOn(openAIInferrence, 'getModel').mockResolvedValue('gpt-4'); }); afterEach(() => { vi.resetAllMocks(); }); describe('Prompt Generation', () => { it('should generate a prompt without user expertise', () => { const systemPrompt = 'System prompt'; const userPrompt = 'User prompt'; const result = openAIInferrence.createPrompt(systemPrompt, userPrompt, false); expect(result).toEqual([ { role: 'system', content: 'System prompt' }, { role: 'user', content: 'User prompt' } ]); }); it('should generate a prompt with user expertise', () => { const systemPrompt = 'System prompt'; const userPrompt = 'User prompt'; const userExpertise = 'Expert'; const result = openAIInferrence.createPrompt(systemPrompt, userPrompt, false, userExpertise); expect(result).toEqual([ { role: 'system', content: 'System prompt\n<Expertise>"Expert"</Expertise>' }, { role: 'user', content: 'User prompt' } ]); }); it('should log prompts when verbose is true', () => { const consoleSpy = vi.spyOn(console, 'log'); const systemPrompt = 'System prompt'; const userPrompt = 'User prompt'; openAIInferrence.createPrompt(systemPrompt, userPrompt, true); expect(consoleSpy).toHaveBeenCalledWith('System Prompt: System prompt'); expect(consoleSpy).toHaveBeenCalledWith('User Prompt: User prompt'); }); }); describe('inferProjectDirectory', () => { it('should return a valid inference for a project directory', async () => { const mockResponse = { choices: [ { message: { content: 'Project directory inference', tool_calls: [{ function: { arguments: '{"projectType":"nodejs","mainLanguage":"typescript"}' } }], }, }, ], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); const result = await openAIInferrence.inferProjectDirectory('{"src": {}, "package.json": {}}', false, false); expect(result).toBe('{"projectType":"nodejs","mainLanguage":"typescript"}'); }); it('should handle responses without tool_calls', async () => { const mockResponse = { choices: [ { message: { content: '{"projectType":"python","mainLanguage":"python"}', }, }, ], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); const result = await openAIInferrence.inferProjectDirectory('{"src": {}, "requirements.txt": {}}', false, false); expect(result).toBe('{"projectType":"python","mainLanguage":"python"}'); }); it('should handle empty responses', async () => { const mockResponse = { choices: [ { message: { content: '', }, }, ], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); const result = await openAIInferrence.inferProjectDirectory('{}', false, false); expect(result).toBeUndefined(); }); it('should handle invalid JSON input', async () => { await expect(openAIInferrence.inferProjectDirectory('invalid json', false, false)).rejects.toThrow(); }); it('should use a non-default model when specified', async () => { const mockResponse = { choices: [{ message: { content: 'Project directory inference' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); vi.spyOn(openAIInferrence, 'getModel').mockResolvedValue('gpt-3.5-turbo'); process.env.DEFAULT_OPENAI_MODEL = 'gpt-3.5-turbo'; await openAIInferrence.inferProjectDirectory('{"src": {}}', false, false); expect(mockOpenAI.chat.completions.create).toHaveBeenCalledWith(expect.objectContaining({ model: 'gpt-3.5-turbo' })); delete process.env.DEFAULT_OPENAI_MODEL; }); }); describe('inferDependency', () => { it('should return a valid inference for dependencies', async () => { const mockResponse = { choices: [{ message: { content: 'Dependency inference' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.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']); mockOpenAI.chat.completions.create.mockResolvedValue(mockStream); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferDependency('{"dependencies": {}}', 'test', true, false); 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 = { choices: [{ message: { content: 'No dependencies found' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferDependency('{}', 'build', false, false); expect(result).toBe('No dependencies found'); }); it('should handle invalid workflow input', async () => { mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); await expect(openAIInferrence.inferDependency('{"dependencies": {}}', '', false, false)).rejects.toThrow(); }); }); describe('inferCode', () => { it('should return a valid inference for code', async () => { const mockResponse = { choices: [{ message: { content: 'Code inference' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferCode('function hello() { console.log("Hello"); }', false, false); expect(result).toBe('Code inference'); }); it('should handle streaming responses', async () => { const mockStream = new MockStream(['Code', ' inference', ' streaming']); mockOpenAI.chat.completions.create.mockResolvedValue(mockStream); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferCode('const x = 5;', true, false); 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 streaming'); }); it('should handle empty code input', async () => { const mockResponse = { choices: [{ message: { content: 'No code provided' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferCode('', false, false); expect(result).toBe('No code provided'); }); }); describe('inferInterestingCode', () => { it('should return interesting parts of the code', async () => { const mockResponse = { choices: [{ message: { content: 'Interesting code parts' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferInterestingCode('function complexAlgorithm() { /* ... */ }', false, false); expect(result).toBe('Interesting code parts'); }); it('should handle streaming responses', async () => { const mockStream = new MockStream(['Interesting', ' code', ' parts', ' streaming']); mockOpenAI.chat.completions.create.mockResolvedValue(mockStream); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.inferInterestingCode('class AdvancedComponent { /* ... */ }', true, false); expect(result).toBeInstanceOf(Object); expect(Symbol.asyncIterator in result).toBe(true); let streamedContent = ''; if (result) { for await (const chunk of result) { streamedContent += chunk; } } expect(streamedContent).toBe('Interesting code parts streaming'); }); it('should handle code with no particularly interesting parts', async () => { const mockResponse = { choices: [{ message: { content: 'No particularly interesting code parts found' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.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 = { choices: [{ message: { content: 'Interesting Java code parts' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const javaCode = ` public class HelloWorld { public static void main(String[] args) { System.out.println("Hello, World!"); } } `; const result = await openAIInferrence.inferInterestingCode(javaCode, false, false); expect(result).toBe('Interesting Java code parts'); }); }); describe('generateReadme', () => { it('should generate a README based on provided information', async () => { const mockResponse = { choices: [{ message: { content: 'Generated README content' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateReadme('{"src": {}, "package.json": {}}', '{"dependencies": {"react": "^17.0.2"}}', 'function App() { return <div>Hello</div>; }', false, false); expect(result).toBe('Generated README content'); }); it('should handle empty inputs', async () => { const mockResponse = { choices: [{ message: { content: 'README for empty project' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateReadme('{}', '{}', '{}', false, false); expect(result).toBe('README for empty project'); }); it('should handle streaming responses', async () => { const mockStream = new MockStream(['Generated', ' README', ' content', ' streaming']); mockOpenAI.chat.completions.create.mockResolvedValue(mockStream); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateReadme('{}', '{}', '{}', true, false); expect(result).toBeInstanceOf(Object); if (result && typeof result !== 'string' && Symbol.asyncIterator in result) { expect(Symbol.asyncIterator in result).toBe(true); } else { throw new Error('Result is not an AsyncIterable<string>'); } let streamedContent = ''; for await (const chunk of result) { streamedContent += chunk; } expect(streamedContent).toBe('Generated README content streaming'); }); }); describe('generateMonorepoReadme', () => { it('should generate a README for a monorepo', async () => { const mockResponse = { choices: [{ message: { content: 'Generated Monorepo README content' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateMonorepoReadme('{"packages": {"app1": {}, "app2": {}}}', false, false); expect(result).toBe('Generated Monorepo README content'); }); it('should handle empty monorepo structure', async () => { const mockResponse = { choices: [{ message: { content: 'README for empty monorepo' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateMonorepoReadme('{}', false, false); expect(result).toBe('README for empty monorepo'); }); it('should handle complex monorepo structure', async () => { const complexMonorepo = JSON.stringify({ packages: { app1: { dependencies: {}, src: {} }, app2: { dependencies: {}, src: {} }, shared: { dependencies: {}, src: {} } }, root: { package: {}, tsconfig: {} } }); const mockResponse = { choices: [{ message: { content: 'README for complex monorepo' } }], }; mockOpenAI.chat.completions.create.mockResolvedValue(mockResponse); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateMonorepoReadme(complexMonorepo, false, false); expect(result).toBe('README for complex monorepo'); }); it('should handle streaming responses', async () => { const mockStream = new MockStream(['Generated', ' Monorepo', ' README', ' content', ' streaming']); mockOpenAI.chat.completions.create.mockResolvedValue(mockStream); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); const result = await openAIInferrence.generateMonorepoReadme('{}', true, false); expect(result).toBeInstanceOf(Object); if (result && typeof result !== 'string' && Symbol.asyncIterator in result) { expect(Symbol.asyncIterator in result).toBe(true); } else { throw new Error('Result is not an AsyncIterable<string>'); } let streamedContent = ''; for await (const chunk of result) { streamedContent += chunk; } expect(streamedContent).toBe('Generated Monorepo README content streaming'); }); }); describe('listModels', () => { it('should return a list of available models', async () => { const mockModels = { data: [ { id: 'gpt-4', created: 1000 }, { id: 'gpt-3.5-turbo', created: 900 }, ], }; mockOpenAI.models.list.mockResolvedValue(mockModels); const result = await openAIInferrence.listModels(false); expect(result).toEqual(['gpt-4', 'gpt-3.5-turbo']); }); it('should sort models by creation date in descending order', async () => { const mockModels = { data: [ { id: 'gpt-3.5-turbo', created: 900 }, { id: 'gpt-4', created: 1000 }, { id: 'gpt-4-32k', created: 1100 }, ], }; mockOpenAI.models.list.mockResolvedValue(mockModels); const result = await openAIInferrence.listModels(false); expect(result).toEqual(['gpt-4-32k', 'gpt-4', 'gpt-3.5-turbo']); }); it('should handle empty model list', async () => { mockOpenAI.models.list.mockResolvedValue({ data: [] }); const result = await openAIInferrence.listModels(false); expect(result).toEqual([]); }); it('should handle API call failure', async () => { mockOpenAI.models.list.mockRejectedValue(new Error('API Error')); await expect(openAIInferrence.listModels(false)).rejects.toThrow('API Error'); }); }); describe('General error handling', () => { it('should handle OpenAI API being down', async () => { mockOpenAI.chat.completions.create.mockRejectedValue(new Error('OpenAI API is down')); mockOpenAI.models.list.mockResolvedValue({ data: [{ id: 'gpt-4' }] }); await expect(openAIInferrence.inferProjectDirectory('{"src": {}}', false, false)).rejects.toThrow('OpenAI API is down'); }); }); });