File size: 6,616 Bytes
fd8cdf5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
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 '<button>Click</button>';

}

`,
            '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);
    });
});