sourcesailor
Version:
A CLI tool for analyzing and documenting codebases
280 lines (279 loc) • 12.9 kB
JavaScript
// @ts-nocheck
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { readConfig, addAnalysisInGitIgnore, writeAnalysis, writeError, getAnalysis } from '../utils.mjs';
import fs from 'fs';
import path from 'path';
vi.mock('fs');
vi.mock('path');
const mockProjectStructure = {
'project-root': {
'.gitignore': '',
'src': {
'index.js': 'console.log("Hello, world!");',
'utils': {
'helper.js': 'function helper() { return "I\'m helping!"; }'
}
},
'package.json': '{ "name": "test-project", "version": "1.0.0" }',
'.SourceSailor': {
'analysis': {
'dependency-analysis.json': '{ "dependencies": {} }',
'code-analysis.md': '# Code Analysis\n\nThis is a mock analysis.'
}
}
}
};
function setupMockFileSystem() {
const mockFs = {
existsSync: vi.fn(),
readFileSync: vi.fn(),
writeFileSync: vi.fn(),
appendFileSync: vi.fn(),
mkdirSync: vi.fn(),
readdirSync: vi.fn(),
statSync: vi.fn()
};
function mockFsTraversal(structure, currentPath = '') {
for (const [name, content] of Object.entries(structure)) {
const fullPath = path.join(currentPath, name);
if (typeof content === 'string') {
mockFs.existsSync.mockImplementation((path) => path === fullPath ? true : false);
mockFs.readFileSync.mockImplementation((path, _) => path === fullPath ? content : '');
}
else if (typeof content === 'object') {
mockFs.existsSync.mockImplementation((path) => path === fullPath ? true : false);
mockFs.readdirSync.mockImplementation((path) => path === fullPath ? Object.keys(content) : []);
mockFs.statSync.mockImplementation((path) => ({
isDirectory: () => path === fullPath
}));
mockFsTraversal(content, fullPath);
}
}
}
mockFsTraversal(mockProjectStructure);
vi.spyOn(fs, 'existsSync').mockImplementation(mockFs.existsSync);
vi.spyOn(fs, 'readFileSync').mockImplementation(mockFs.readFileSync);
vi.spyOn(fs, 'writeFileSync').mockImplementation(mockFs.writeFileSync);
vi.spyOn(fs, 'appendFileSync').mockImplementation(mockFs.appendFileSync);
vi.spyOn(fs, 'mkdirSync').mockImplementation(mockFs.mkdirSync);
vi.spyOn(fs, 'readdirSync').mockImplementation(mockFs.readdirSync);
vi.spyOn(fs, 'statSync').mockImplementation(mockFs.statSync);
}
beforeEach(() => {
setupMockFileSystem();
});
afterEach(() => {
vi.resetAllMocks();
});
describe('readConfig', () => {
it('should return an empty object when config file doesn\'t exist', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const config = readConfig();
expect(config).toEqual({});
});
it('should return the correct config when file exists', () => {
const mockConfig = { OPENAI_API_KEY: 'test-key', DEFAULT_OPENAI_MODEL: 'gpt-3.5-turbo' };
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(JSON.stringify(mockConfig));
const config = readConfig();
expect(config).toEqual(mockConfig);
});
it('should handle JSON parsing errors', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue('invalid json');
const config = readConfig();
expect(config).toEqual({});
});
});
describe('addAnalysisInGitIgnore', () => {
it('should add .SourceSailor/analysis to .gitignore if it doesn\'t exist', () => {
const mockGitignoreContent = 'node_modules\n';
const mockGitignorePath = '/mock/project-root/.gitignore';
vi.spyOn(path, 'join').mockReturnValue(mockGitignorePath);
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(mockGitignoreContent);
const appendSpy = vi.spyOn(fs, 'appendFileSync');
addAnalysisInGitIgnore('/mock/project-root');
expect(appendSpy).toHaveBeenCalledWith(mockGitignorePath, '\n.SourceSailor/analysis');
});
it('should not add duplicate entries if .SourceSailor/analysis already exists', () => {
const mockGitignoreContent = 'node_modules\n.SourceSailor/analysis\n';
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readFileSync').mockReturnValue(mockGitignoreContent);
const appendSpy = vi.spyOn(fs, 'appendFileSync');
addAnalysisInGitIgnore('project-root');
expect(appendSpy).not.toHaveBeenCalled();
});
it('should not create .gitignore if it doesn\'t exist', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const writeSpy = vi.spyOn(fs, 'writeFileSync');
const appendSpy = vi.spyOn(fs, 'appendFileSync');
addAnalysisInGitIgnore('project-root');
expect(writeSpy).not.toHaveBeenCalled();
expect(appendSpy).not.toHaveBeenCalled();
});
});
describe('writeAnalysis', () => {
const mockProjectRoot = '/mock/project-root';
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => { });
vi.spyOn(path, 'join').mockImplementation((...args) => args.join('/'));
});
it('should create analysis directory if it doesn\'t exist', () => {
writeAnalysis(mockProjectRoot, 'test-analysis', 'Test content');
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/project-root/analysis', { recursive: true });
});
it('should write JSON file correctly', () => {
const analysisContent = { key: 'value' };
writeAnalysis(mockProjectRoot, 'test-json', analysisContent, true);
expect(fs.writeFileSync).toHaveBeenCalledWith('/mock/project-root/analysis/test-json.json', JSON.stringify(analysisContent, null, 4));
});
it('should write markdown file correctly', () => {
const analysisContent = '# Test Markdown';
writeAnalysis(mockProjectRoot, 'test-md', analysisContent);
expect(fs.writeFileSync).toHaveBeenCalledWith('/mock/project-root/analysis/test-md.md', analysisContent);
});
it('should use project root for analysis directory when isProjectRoot is true', () => {
writeAnalysis(mockProjectRoot, 'test-root', 'Test content', false, true);
expect(fs.mkdirSync).toHaveBeenCalledWith('/mock/project-root/.SourceSailor/analysis', { recursive: true });
});
it('should handle errors in file writing', () => {
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
throw new Error('Write error');
});
expect(() => writeAnalysis(mockProjectRoot, 'error-test', 'Test content')).toThrow('Write error');
});
});
describe('writeError', () => {
const mockProjectRoot = '/mock/project-root';
const mockErrorDir = '/mock/project-root/errors';
const mockErrorFile = '/mock/project-root/errors/test-error.txt';
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(path, 'join')
.mockImplementationOnce(() => mockErrorDir)
.mockImplementationOnce(() => mockErrorFile);
});
it('should create error directory if it doesn\'t exist', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
writeError(mockProjectRoot, 'test-error', 'Error content', 'Error message');
expect(mkdirSpy).toHaveBeenCalledWith(mockErrorDir, { recursive: true });
});
it('should not create error directory if it already exists', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
const mkdirSpy = vi.spyOn(fs, 'mkdirSync').mockImplementation(() => undefined);
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
writeError(mockProjectRoot, 'test-error', 'Error content', 'Error message');
expect(mkdirSpy).not.toHaveBeenCalled();
});
it('should write error file correctly', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
const writeSpy = vi.spyOn(fs, 'writeFileSync').mockImplementation(() => undefined);
writeError(mockProjectRoot, 'test-error', 'Error content', 'Error message');
expect(writeSpy).toHaveBeenCalledWith(mockErrorFile, 'Error content\n\nError message');
});
it('should handle errors in file writing', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'writeFileSync').mockImplementation(() => {
throw new Error('Write error');
});
expect(() => writeError(mockProjectRoot, 'test-error', 'Error content', 'Error message'))
.toThrow('Write error');
});
});
describe('getAnalysis', () => {
const mockProjectRoot = '/mock/project-root';
beforeEach(() => {
vi.resetAllMocks();
vi.spyOn(path, 'join').mockImplementation((...args) => args.join('/'));
vi.spyOn(path, 'basename').mockImplementation((p) => {
if (typeof p === 'string') {
return p.split('/').pop() || '';
}
return p.toString().split('/').pop() || '';
});
vi.spyOn(path, 'extname').mockImplementation((p) => {
if (typeof p === 'string') {
const parts = p.split('.');
return parts.length > 1 ? '.' + parts.pop() : '';
}
return '';
});
});
it('should return an empty object if analysis directory doesn\'t exist', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
const result = getAnalysis(mockProjectRoot, false);
expect(result).toEqual({});
});
it('should correctly read and return analysis files', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readdirSync').mockReturnValue(['file1.json', 'file2.md']);
vi.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => false
});
vi.spyOn(fs, 'readFileSync')
.mockReturnValueOnce('{"key": "value"}')
.mockReturnValueOnce('# Markdown content');
const result = getAnalysis(mockProjectRoot, false);
expect(result).toEqual({
'file1.json': '{"key": "value"}',
'file2.md': '# Markdown content'
});
});
it('should handle nested directories in analysis', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readdirSync')
.mockReturnValueOnce(['file1.json', 'nested'])
.mockReturnValueOnce(['file2.md']);
vi.spyOn(fs, 'statSync')
.mockReturnValueOnce({
isDirectory: () => false
})
.mockReturnValueOnce({
isDirectory: () => true
})
.mockReturnValueOnce({
isDirectory: () => false
});
vi.spyOn(fs, 'readFileSync')
.mockReturnValueOnce('{"key": "value"}')
.mockReturnValueOnce('# Nested content');
const result = getAnalysis(mockProjectRoot, false);
expect(result).toEqual({
'file1.json': '{"key": "value"}',
nested: {
'file2.md': '# Nested content'
}
});
});
it('should handle both JSON and markdown files', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readdirSync').mockReturnValue(['file1.json', 'file2.md']);
vi.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => false
});
vi.spyOn(fs, 'readFileSync')
.mockReturnValueOnce('{"key": "value"}')
.mockReturnValueOnce('# Markdown content');
const result = getAnalysis(mockProjectRoot, false);
expect(result).toEqual({
'file1.json': '{"key": "value"}',
'file2.md': '# Markdown content'
});
});
it('should use .SourceSailor/analysis directory when isProjectRoot is true', () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(true);
vi.spyOn(fs, 'readdirSync').mockReturnValue(['file1.json']);
vi.spyOn(fs, 'statSync').mockReturnValue({
isDirectory: () => false
});
vi.spyOn(fs, 'readFileSync').mockReturnValue('{"key": "value"}');
getAnalysis(mockProjectRoot, true);
expect(path.join).toHaveBeenCalledWith(mockProjectRoot, '.SourceSailor', 'analysis');
});
});