UNPKG

sourcesailor

Version:

A CLI tool for analyzing and documenting codebases

296 lines (295 loc) 14.4 kB
/* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { handler } from '../commands/analyse.mjs'; import * as directoryProcessor from '../directoryProcessor.mjs'; import ModelUtils from '../modelUtils.mjs'; import * as utils from '../utils.mjs'; import fs from 'fs'; import ora from 'ora'; import * as inquirer from '@inquirer/prompts'; import * as setExpertise from '../commands/setExpertise.mjs'; import chalk from "chalk"; // Remove this mock as it won't work correctly // const mockProcessCwd = vi.fn(() => '/test/current-project') // vi.mock('process', () => ({ // default: { // cwd: mockProcessCwd // }, // cwd: mockProcessCwd // })) vi.mock('../directoryProcessor.mjs'); vi.mock('../modelUtils.mjs'); vi.mock('../utils.mjs'); vi.mock('fs'); vi.mock('ora'); vi.mock('@inquirer/prompts'); vi.mock('../commands/setExpertise.mjs'); describe('analyse command', () => { beforeEach(() => { vi.resetAllMocks(); // Mock process.cwd const mockCwd = vi.spyOn(process, 'cwd'); mockCwd.mockReturnValue('/test/current-project'); vi.mocked(ora).mockReturnValue({ start: vi.fn().mockReturnThis(), stop: vi.fn().mockReturnThis(), clear: vi.fn().mockReturnThis(), stopAndPersist: vi.fn().mockReturnThis(), text: '', }); vi.mocked(inquirer.confirm).mockResolvedValue(false); vi.mocked(setExpertise.handler).mockResolvedValue(undefined); }); afterEach(() => { vi.resetAllMocks(); vi.restoreAllMocks(); // Add this to cleanup process.cwd mock }); it('should analyze a non-monorepo project correctly', async () => { const mockArgv = { path: '/test/project', verbose: false, openai: true, streaming: false, ignore: [], }; const mockDirectoryStructure = { name: 'project', children: [ { name: 'src', children: [] }, { name: 'package.json', content: '{}' }, ], }; const mockDirectoryInference = { isMonorepo: false, workflow: 'nodejs', dependenciesFile: 'package.json', programmingLanguage: 'javascript', }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), inferCode: vi.fn().mockResolvedValue('Mocked code inference'), inferInterestingCode: vi.fn().mockResolvedValue('Mocked interesting code'), inferDependency: vi.fn().mockResolvedValue('Mocked dependency inference'), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test', userExpertise: 'intermediate' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); await handler(mockArgv); expect(directoryProcessor.getDirStructure).toHaveBeenCalledWith('/test/project', [], false); expect(utils.writeAnalysis).toHaveBeenCalledTimes(5); // directoryStructure, directoryStructureWithFileContent, directoryInference, codeInference, dependencyInference expect(utils.addAnalysisInGitIgnore).not.toHaveBeenCalled(); }); it('should stop analysis if programming language is not defined for non-monorepo project', async () => { const mockArgv = { path: '/test/project', verbose: false, openai: true, streaming: false, ignore: [], }; const mockDirectoryStructure = { name: 'project', children: [ { name: 'src', children: [] }, { name: 'package.json', content: '{}' }, ], }; const mockDirectoryInference = { isMonorepo: false, workflow: 'nodejs', dependenciesFile: 'package.json', // No programming language defined }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); await expect(handler(mockArgv)).rejects.toThrow('Programming language not defined for non-monorepo project'); expect(directoryProcessor.getDirStructure).toHaveBeenCalledWith('/test/project', [], false); expect(utils.writeAnalysis).toHaveBeenCalledTimes(3); // directoryStructure, directoryStructureWithFileContent, directoryInference expect(utils.writeError).toHaveBeenCalledWith(expect.anything(), 'Analysis', expect.stringContaining('Programming language not defined for non-monorepo project'), expect.anything()); }); it('should analyze a monorepo project correctly', async () => { const mockArgv = { path: '/test/monorepo', verbose: true, openai: true, streaming: false, ignore: ['node_modules'], }; const mockDirectoryStructure = { name: 'monorepo', children: [ { name: 'package1', children: [{ name: 'package.json', content: '{}' }] }, { name: 'package2', children: [{ name: 'package.json', content: '{}' }] }, ], }; const mockDirectoryInference = { isMonorepo: true, workflow: 'nodejs', directories: ['package1', 'package2'], }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), inferCode: vi.fn().mockResolvedValue('Mocked code inference'), inferInterestingCode: vi.fn().mockResolvedValue('Mocked interesting code'), inferDependency: vi.fn().mockResolvedValue('Mocked dependency inference'), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); const callsBeforeInferringMonoRepo = 3; const callsForInferringCallsPerMonorepo = 2; const writeAnalysisCalls = callsBeforeInferringMonoRepo + (callsForInferringCallsPerMonorepo * (mockDirectoryStructure.children.length)); await handler(mockArgv); expect(directoryProcessor.getDirStructure).toHaveBeenCalledWith('/test/monorepo', ['node_modules'], true); expect(utils.writeAnalysis).toHaveBeenCalledTimes(writeAnalysisCalls); // 3 calls before we expect(utils.addAnalysisInGitIgnore).not.toHaveBeenCalled(); }); it('should handle errors during analysis', async () => { const mockArgv = { path: '/test/error-project', verbose: true, openai: true, streaming: false, ignore: [], }; const mockDirectoryStructure = { name: 'error-project', children: [ { name: 'src', children: [] }, { name: 'package.json', content: '{}' }, ], }; const mockDirectoryInference = { isMonorepo: false, workflow: 'nodejs', dependenciesFile: 'package.json', programmingLanguage: 'javascript', }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), inferCode: vi.fn().mockRejectedValue(new Error('Mocked error')), inferInterestingCode: vi.fn().mockRejectedValue(new Error('Mocked error')), inferDependency: vi.fn().mockResolvedValue('Mocked dependency inference'), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); await handler(mockArgv); expect(directoryProcessor.getDirStructure).toHaveBeenCalledWith('/test/error-project', [], true); expect(utils.writeAnalysis).toHaveBeenCalledTimes(5); // directoryStructure, directoryStructureWithFileContent, directoryInference, codeInference (with error), dependencyInference expect(utils.writeAnalysis).toHaveBeenCalledWith(expect.anything(), 'codeInferrence', expect.stringContaining('Error inferring code: Mocked error'), expect.anything(), expect.anything()); expect(utils.writeAnalysis).toHaveBeenCalledWith(expect.anything(), 'codeInferrence', expect.stringContaining('Error inferring interesting code: Mocked error'), expect.anything(), expect.anything()); expect(utils.writeError).not.toHaveBeenCalled(); }); it('should prompt for user expertise if not set', async () => { const mockArgv = { path: '/test/project', verbose: false, openai: true, streaming: false, ignore: [], }; const mockDirectoryStructure = { name: 'project', children: [ { name: 'src', children: [] }, { name: 'package.json', content: '{}' }, ], }; const mockDirectoryInference = { isMonorepo: false, workflow: 'nodejs', dependenciesFile: 'package.json', programmingLanguage: 'javascript', }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), inferCode: vi.fn().mockResolvedValue('Mocked code inference'), inferInterestingCode: vi.fn().mockResolvedValue('Mocked interesting code'), inferDependency: vi.fn().mockResolvedValue('Mocked dependency inference'), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); vi.mocked(inquirer.confirm).mockResolvedValue(true); await handler(mockArgv); expect(inquirer.confirm).toHaveBeenCalledWith({ message: "Would you like to set your expertise now?", default: true }); expect(setExpertise.handler).toHaveBeenCalled(); }); it('should handle current directory analysis correctly', async () => { const mockArgv = { path: '.', verbose: false, openai: true, streaming: false, ignore: [], }; const mockDirectoryStructure = { name: 'current-project', children: [ { name: 'src', children: [] }, { name: 'package.json', content: '{}' }, ], }; const mockDirectoryInference = { isMonorepo: false, workflow: 'nodejs', dependenciesFile: 'package.json', programmingLanguage: 'javascript', }; vi.mocked(directoryProcessor.getDirStructure).mockResolvedValue(mockDirectoryStructure); const mockModelUtils = { initializeModels: vi.fn().mockResolvedValue(undefined), getLlmInterface: vi.fn().mockReturnValue({ getName: vi.fn().mockReturnValue('Mocked model'), inferProjectDirectory: vi.fn().mockResolvedValue(JSON.stringify(mockDirectoryInference)), inferCode: vi.fn().mockResolvedValue('Mocked code inference'), inferInterestingCode: vi.fn().mockResolvedValue('Mocked interesting code'), inferDependency: vi.fn().mockResolvedValue('Mocked dependency inference'), }), }; vi.mocked(ModelUtils.getInstance).mockReturnValue(mockModelUtils); vi.mocked(utils.readConfig).mockReturnValue({ ANALYSIS_DIR: '/test' }); vi.mocked(fs.readFileSync).mockReturnValue('{}'); const consoleSpy = vi.spyOn(console, 'log'); await handler(mockArgv); expect(consoleSpy).toHaveBeenCalledWith('Analyzing current directory: current-project'); expect(consoleSpy).toHaveBeenCalledWith(`Analysing ${chalk.redBright('current-project')}'s file structure to getting started.`); expect(directoryProcessor.getDirStructure).toHaveBeenCalledWith('.', [], false); expect(utils.writeAnalysis).toHaveBeenCalledWith('/test/.SourceSailor/current-project', 'directoryStructure', expect.any(Object), true, false); consoleSpy.mockRestore(); }); });