import { describe, it, expect, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { analyze } from './index'; /** * Creates a temporary directory with TypeScript files for testing. */ function createTempProject(files) { const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'analyze-test-')); for (const [filePath, content] of Object.entries(files)) { const fullPath = path.join(tmpDir, filePath); fs.mkdirSync(path.dirname(fullPath), { recursive: true }); fs.writeFileSync(fullPath, content, 'utf-8'); } return tmpDir; } /** * Recursively removes a directory. */ function removeTempDir(dir) { fs.rmSync(dir, { recursive: true, force: true }); } describe('analyze', () => { let tmpDir; afterEach(() => { if (tmpDir) { removeTempDir(tmpDir); } }); it('should analyze a directory with TypeScript files and return correct structure', async () => { tmpDir = createTempProject({ 'package.json': JSON.stringify({ name: 'test-project', description: 'A test' }), 'src/index.ts': ` import { helper } from './utils'; export function main() { return helper(); } `, 'src/utils.ts': ` export function helper() { return 'hello'; } export function anotherHelper() { return 'world'; } `, }); const result = await analyze(tmpDir, { full: true }); // Should have a valid dashboard structure expect(result.dashboard.version).toBe('1.0.0'); expect(result.dashboard.project.name).toBe('test-project'); expect(result.dashboard.nodes.length).toBeGreaterThan(0); expect(result.dashboard.edges.length).toBeGreaterThan(0); expect(result.dashboard.layers).toBeDefined(); expect(result.dashboard.tour).toBeDefined(); // Should have stats — scanner includes package.json as a data file expect(result.stats.filesAnalyzed).toBeGreaterThanOrEqual(2); expect(result.stats.edgesCreated).toBeGreaterThan(0); }); it('should compute correct stats with nodesByType counts', async () => { tmpDir = createTempProject({ 'src/app.ts': ` export class AppService { run() { return true; } } export function bootstrap() { return new AppService(); } `, }); const result = await analyze(tmpDir, { full: true }); // Should have file nodes and function/class nodes expect(result.stats.nodesByType['file']).toBe(1); expect(result.stats.filesAnalyzed).toBe(1); // Should have function and/or class nodes const totalNodes = Object.values(result.stats.nodesByType).reduce((a, b) => a + b, 0); expect(totalNodes).toBeGreaterThan(1); // At least file + function/class }); it('should return empty dashboard for empty directory', async () => { tmpDir = createTempProject({}); const result = await analyze(tmpDir, { full: true }); expect(result.dashboard.nodes).toEqual([]); expect(result.dashboard.edges).toEqual([]); expect(result.stats.filesAnalyzed).toBe(0); expect(result.stats.nodesByType).toEqual({}); expect(result.stats.edgesCreated).toBe(0); expect(result.stats.layersIdentified).toBe(0); }); it('should detect incremental mode when meta.json exists', async () => { tmpDir = createTempProject({ 'src/index.ts': `export function main() { return 1; }`, '.understand-anything/meta.json': JSON.stringify({ lastAnalyzedAt: '2024-01-01T00:00:00.000Z', gitCommitHash: 'abc123nonexistent', version: '1.0.0', analyzedFiles: 1, }), }); // With full=false and a meta.json present, it should attempt incremental // but fall back to full since git diff will fail (not a git repo) const result = await analyze(tmpDir, { full: false }); // Should still produce a valid result (falls back to full) expect(result.dashboard.version).toBe('1.0.0'); expect(result.stats.filesAnalyzed).toBe(1); }); it('should force full rebuild when --full flag is set', async () => { tmpDir = createTempProject({ 'src/index.ts': `export function main() { return 1; }`, '.understand-anything/meta.json': JSON.stringify({ lastAnalyzedAt: '2024-01-01T00:00:00.000Z', gitCommitHash: 'abc123', version: '1.0.0', analyzedFiles: 1, }), '.understand-anything/knowledge-graph.json': JSON.stringify({ version: '1.0.0', project: { name: 'old' }, nodes: [], edges: [], }), }); // With full=true, should ignore meta.json and do full rebuild const result = await analyze(tmpDir, { full: true }); expect(result.dashboard.version).toBe('1.0.0'); expect(result.stats.filesAnalyzed).toBe(1); // Should have nodes from the actual file, not the empty existing graph expect(result.dashboard.nodes.length).toBeGreaterThan(0); }); it('should handle files that fail to parse gracefully', async () => { tmpDir = createTempProject({ 'src/good.ts': `export function hello() { return 'hi'; }`, // Binary-like content that might cause parse issues — but our parser // is regex-based so it won't throw. Instead test with a file that // doesn't exist on disk (scanner won't include it). 'src/another.ts': `export const x = 42;`, }); const result = await analyze(tmpDir, { full: true }); // Should still produce results even if some files are problematic expect(result.stats.filesAnalyzed).toBeGreaterThan(0); expect(result.dashboard.nodes.length).toBeGreaterThan(0); }); it('should include layers in the output', async () => { tmpDir = createTempProject({ 'src/components/Button.tsx': ` export function Button() { return ''; } `, 'src/utils/format.ts': ` export function formatDate(d: Date) { return d.toISOString(); } `, }); const result = await analyze(tmpDir, { full: true }); expect(result.dashboard.layers).toBeDefined(); expect(result.dashboard.layers.length).toBeGreaterThan(0); expect(result.stats.layersIdentified).toBeGreaterThan(0); }); });