ai-xray 1.2.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,49 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { estimateTokens, applyBudget } from '../src/utils/output';
3
-
4
- describe('Output & Budget Manager', () => {
5
- it('should estimate tokens roughly correctly', () => {
6
- // 4 chars ~ 1 token
7
- expect(estimateTokens('1234')).toBe(1);
8
- expect(estimateTokens('12345678')).toBe(2);
9
- expect(estimateTokens('')).toBe(1); // min 1
10
- });
11
-
12
- it('should pass through data when budget is undefined or unlimited', () => {
13
- const data = { hello: 'world' };
14
- const res = applyBudget(data, 0);
15
- expect(res).toEqual({ hello: 'world' });
16
- });
17
-
18
- it('should actually truncate data when budget is exceeded', () => {
19
- const data = { a: 'b', c: 'a very long string that will exceed a tiny budget' };
20
- // estimated tokens for this stringified JSON is roughly ~15-20
21
- const res = applyBudget(data, 5);
22
-
23
- // With actual truncation, data should be hard-truncated since budget is very small (5 tokens = 20 chars)
24
- expect(res._meta.truncated).toBe(true);
25
- expect(res._meta.budget).toBe(5);
26
- expect(res._meta.hint).toContain('exceeded budget');
27
- });
28
-
29
- it('should prune large fields when moderately over budget', () => {
30
- const data = { name: 'test', items: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j'] };
31
- // Give a budget that's smaller than full data but large enough to keep some
32
- const serialized = JSON.stringify(data);
33
- const fullTokens = Math.ceil(serialized.length / 4);
34
- const res = applyBudget(data, Math.floor(fullTokens * 0.6));
35
-
36
- expect(res._meta.truncated).toBe(true);
37
- expect(res._meta.hint).toContain('exceeded budget');
38
- });
39
-
40
- it('should attach standard meta when budget is met', () => {
41
- const data = { a: 'b' };
42
- const res = applyBudget(data, 100);
43
-
44
- expect(res.a).toBe('b');
45
- expect(res._meta.truncated).toBe(false);
46
- expect(res._meta.budget).toBe(100);
47
- expect(res._meta.tokensEstimate).toBeGreaterThan(0);
48
- });
49
- });
@@ -1,169 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleReadCommand } from '../src/commands/read';
3
- import * as fs from 'fs';
4
-
5
- vi.mock('fs', async () => {
6
- const actual = await vi.importActual<typeof import('fs')>('fs');
7
- return {
8
- ...actual,
9
- existsSync: vi.fn(),
10
- readFileSync: vi.fn(),
11
- statSync: vi.fn()
12
- };
13
- });
14
-
15
- const mockExistsSync = fs.existsSync as any;
16
- const mockReadFileSync = fs.readFileSync as any;
17
- const mockStatSync = fs.statSync as any;
18
-
19
- describe('Read Command', () => {
20
- beforeEach(() => {
21
- vi.clearAllMocks();
22
- mockStatSync.mockReturnValue({ isDirectory: () => false });
23
- });
24
-
25
- it('should throw when no files are provided', () => {
26
- expect(() => handleReadCommand([])).toThrow('No files provided to read.');
27
- });
28
-
29
- it('should read a single file and return content', () => {
30
- mockExistsSync.mockReturnValue(true);
31
- mockReadFileSync.mockReturnValue('line1\nline2\nline3\n');
32
-
33
- const result = handleReadCommand(['test.ts']);
34
- expect(result.file).toBe('test.ts');
35
- expect(result.content).toBe('line1\nline2\nline3\n');
36
- expect(result.lines).toBe(4); // 3 lines + trailing newline = 4 elements from split
37
- });
38
-
39
- it('should handle missing files gracefully', () => {
40
- mockExistsSync.mockReturnValue(false);
41
-
42
- const result = handleReadCommand(['missing.ts']);
43
- expect(result.file).toBe('missing.ts');
44
- expect(result.error).toBe('File not found');
45
- });
46
-
47
- it('should handle directory targets', () => {
48
- mockExistsSync.mockReturnValue(true);
49
- mockStatSync.mockReturnValue({ isDirectory: () => true });
50
-
51
- const result = handleReadCommand(['somedir']);
52
- expect(result.error).toBe('Target is a directory, not a file');
53
- });
54
-
55
- it('should extract line ranges with --lines flag', () => {
56
- mockExistsSync.mockReturnValue(true);
57
- mockReadFileSync.mockReturnValue('line1\nline2\nline3\nline4\nline5\n');
58
-
59
- const result = handleReadCommand(['file.ts', '--lines=2-4']);
60
- expect(result.content).toBe('line2\nline3\nline4');
61
- expect(result.context.range).toEqual([2, 4]);
62
- });
63
-
64
- it('should extract JSON keys with --keys flag', () => {
65
- mockExistsSync.mockReturnValue(true);
66
- const jsonContent = JSON.stringify({
67
- name: 'test',
68
- version: '1.0.0',
69
- dependencies: { react: '18.0.0' },
70
- devDependencies: { vitest: '1.0.0' }
71
- });
72
- mockReadFileSync.mockReturnValue(jsonContent);
73
-
74
- const result = handleReadCommand(['package.json', '--keys=name,dependencies']);
75
- expect(result.content).toEqual({
76
- name: 'test',
77
- dependencies: { react: '18.0.0' }
78
- });
79
- });
80
-
81
- it('should fallback to raw text for --keys on invalid JSON', () => {
82
- mockExistsSync.mockReturnValue(true);
83
- mockReadFileSync.mockReturnValue('not valid json {{{');
84
-
85
- const result = handleReadCommand(['data.json', '--keys=name']);
86
- // Should fallback to raw content string since JSON.parse fails
87
- expect(typeof result.content).toBe('string');
88
- expect(result.content).toBe('not valid json {{{');
89
- });
90
-
91
- it('should read multiple files and return array', () => {
92
- mockExistsSync.mockReturnValue(true);
93
- mockReadFileSync.mockImplementation((path: string) => {
94
- if (path === 'a.ts') return 'content a';
95
- if (path === 'b.ts') return 'content b';
96
- return '';
97
- });
98
-
99
- const result = handleReadCommand(['a.ts', 'b.ts']);
100
- expect(result.files).toHaveLength(2);
101
- expect(result.files[0].file).toBe('a.ts');
102
- expect(result.files[1].file).toBe('b.ts');
103
- });
104
-
105
- it('should handle mixed existing and missing files', () => {
106
- mockExistsSync.mockImplementation((p: string) => p === 'exists.ts');
107
- mockReadFileSync.mockReturnValue('content');
108
-
109
- const result = handleReadCommand(['exists.ts', 'missing.ts']);
110
- expect(result.files).toHaveLength(2);
111
- expect(result.files[0].content).toBe('content');
112
- expect(result.files[1].error).toBe('File not found');
113
- });
114
-
115
- it('should truncate content when budget is exceeded', () => {
116
- mockExistsSync.mockReturnValue(true);
117
- // Create a large content string (over budget)
118
- const bigContent = 'x'.repeat(1000);
119
- mockReadFileSync.mockReturnValue(bigContent);
120
-
121
- // Budget of 50 tokens = ~200 chars
122
- const result = handleReadCommand(['big.ts', '--budget=50'], 50);
123
- // The content should be truncated
124
- if (result.truncated) {
125
- expect(result.content.length).toBeLessThan(bigContent.length);
126
- expect(result.content).toContain('[TRUNCATED BY BUDGET]');
127
- }
128
- });
129
-
130
- it('should skip file entirely if budget is exhausted', () => {
131
- mockExistsSync.mockReturnValue(true);
132
- const bigContent = 'x'.repeat(2000);
133
- mockReadFileSync.mockReturnValue(bigContent);
134
-
135
- // Very tiny budget
136
- const result = handleReadCommand(['file1.ts', 'file2.ts'], 5);
137
- // With a budget of 5 tokens (~20 chars), first file gets truncated or error,
138
- // second file should be skipped
139
- expect(result.files).toBeDefined();
140
- });
141
-
142
- it('should not apply --keys extraction to non-JSON files', () => {
143
- mockExistsSync.mockReturnValue(true);
144
- mockReadFileSync.mockReturnValue('const x = 1;');
145
-
146
- // --keys only works on .json files
147
- const result = handleReadCommand(['code.ts', '--keys=name']);
148
- expect(typeof result.content).toBe('string');
149
- expect(result.content).toBe('const x = 1;');
150
- });
151
-
152
- it('should handle line range that exceeds file length', () => {
153
- mockExistsSync.mockReturnValue(true);
154
- mockReadFileSync.mockReturnValue('line1\nline2\nline3');
155
-
156
- const result = handleReadCommand(['file.ts', '--lines=1-100']);
157
- expect(result.content).toBe('line1\nline2\nline3');
158
- expect(result.context.range[1]).toBe(3); // capped to actual line count
159
- });
160
-
161
- it('should handle file read errors gracefully', () => {
162
- mockExistsSync.mockReturnValue(true);
163
- mockStatSync.mockReturnValue({ isDirectory: () => false });
164
- mockReadFileSync.mockImplementation(() => { throw new Error('Permission denied'); });
165
-
166
- const result = handleReadCommand(['protected.ts']);
167
- expect(result.error).toBe('Permission denied');
168
- });
169
- });
@@ -1,248 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleScoutCommand } from '../src/commands/scout';
3
- import * as fs from 'fs';
4
- import * as child_process from 'child_process';
5
-
6
- vi.mock('fs', async () => {
7
- const actual = await vi.importActual<typeof import('fs')>('fs');
8
- return {
9
- ...actual,
10
- existsSync: vi.fn(),
11
- readFileSync: vi.fn(),
12
- readdirSync: vi.fn(),
13
- statSync: vi.fn()
14
- };
15
- });
16
-
17
- vi.mock('child_process', () => ({
18
- execFileSync: vi.fn()
19
- }));
20
-
21
- const mockExistsSync = fs.existsSync as any;
22
- const mockReadFileSync = fs.readFileSync as any;
23
- const mockReaddirSync = fs.readdirSync as any;
24
- const mockStatSync = fs.statSync as any;
25
- const mockExecFileSync = child_process.execFileSync as any;
26
-
27
- function setupBasicProject() {
28
- mockExistsSync.mockImplementation((p: string) => {
29
- const norm = p.replace(/\\/g, '/');
30
- if (norm.endsWith('package.json')) return true;
31
- if (norm.endsWith('.git')) return true;
32
- if (norm.endsWith('README.md')) return true;
33
- if (norm.endsWith('.gitignore')) return false;
34
- if (norm.endsWith('.cursorrules')) return false;
35
- if (norm.endsWith('CLAUDE.md')) return false;
36
- if (norm.endsWith('AGENTS.md')) return false;
37
- if (norm.endsWith('copilot-instructions.md')) return false;
38
- if (norm.endsWith('Readme.md')) return false;
39
- if (norm.endsWith('readme.md')) return false;
40
- return true;
41
- });
42
-
43
- mockReadFileSync.mockImplementation((p: string) => {
44
- const norm = p.replace(/\\/g, '/');
45
- if (norm.endsWith('package.json')) {
46
- return JSON.stringify({
47
- name: 'test-project',
48
- dependencies: { react: '18.0.0' },
49
- devDependencies: { vitest: '1.0.0' }
50
- });
51
- }
52
- if (norm.endsWith('README.md')) return '# Test Project\n\nA test project.';
53
- return '';
54
- });
55
-
56
- mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
57
- if (args.includes('--version')) return '9.0.0\n';
58
- if (args.includes('--abbrev-ref')) return 'main\n';
59
- if (args.includes('--porcelain')) return '';
60
- if (args.includes('remote.origin.url')) return 'https://github.com/test/test.git\n';
61
- if (args.includes('--left-right')) return '0\t0\n';
62
- return '';
63
- });
64
-
65
- mockReaddirSync.mockReturnValue(['src', 'package.json', 'README.md']);
66
- mockStatSync.mockImplementation((p: string, opts?: any) => {
67
- const norm = p.replace(/\\/g, '/');
68
- if (norm.endsWith('src')) return { isDirectory: () => true };
69
- return { isDirectory: () => false };
70
- });
71
- }
72
-
73
- describe('Scout Command', () => {
74
- beforeEach(() => {
75
- vi.clearAllMocks();
76
- });
77
-
78
- it('should return a combined result with env, tree, readme, diff sections', () => {
79
- setupBasicProject();
80
-
81
- const result = handleScoutCommand();
82
- expect(result.env).toBeDefined();
83
- expect(result.tree).toBeDefined();
84
- expect(result.readme).toBeDefined();
85
- expect(result.diff).toBeDefined();
86
- });
87
-
88
- it('should include readme content (truncated to 60 lines)', () => {
89
- setupBasicProject();
90
-
91
- const result = handleScoutCommand();
92
- expect(result.readme).toContain('# Test Project');
93
- });
94
-
95
- it('should truncate long readmes to 60 lines', () => {
96
- setupBasicProject();
97
- // Override README to be very long
98
- const longReadme = Array.from({ length: 100 }, (_, i) => `Line ${i + 1}`).join('\n');
99
- mockReadFileSync.mockImplementation((p: string) => {
100
- const norm = p.replace(/\\/g, '/');
101
- if (norm.endsWith('package.json')) {
102
- return JSON.stringify({ name: 'test', dependencies: {} });
103
- }
104
- if (norm.endsWith('README.md')) return longReadme;
105
- return '';
106
- });
107
-
108
- const result = handleScoutCommand();
109
- expect(result.readme).toContain('[TRUNCATED TO 60 LINES]');
110
- });
111
-
112
- it('should handle missing env gracefully (error in env)', () => {
113
- // No package.json, no git
114
- mockExistsSync.mockReturnValue(false);
115
- mockReadFileSync.mockReturnValue('');
116
- mockReaddirSync.mockReturnValue([]);
117
- mockStatSync.mockReturnValue({ isDirectory: () => false });
118
-
119
- const result = handleScoutCommand();
120
- // env should still be present (returns a "not node project" result)
121
- expect(result.env).toBeDefined();
122
- });
123
-
124
- it('should handle missing readme', () => {
125
- mockExistsSync.mockImplementation((p: string) => {
126
- const norm = p.replace(/\\/g, '/');
127
- if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
128
- if (norm.endsWith('package.json')) return true;
129
- if (norm.endsWith('.git')) return false;
130
- if (norm.endsWith('.gitignore')) return false;
131
- if (norm.endsWith('.cursorrules')) return false;
132
- if (norm.endsWith('CLAUDE.md')) return false;
133
- if (norm.endsWith('AGENTS.md')) return false;
134
- if (norm.endsWith('copilot-instructions.md')) return false;
135
- return true;
136
- });
137
- mockReadFileSync.mockImplementation((p: string) => {
138
- if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
139
- return '';
140
- });
141
- mockReaddirSync.mockReturnValue([]);
142
- mockStatSync.mockReturnValue({ isDirectory: () => false });
143
-
144
- const result = handleScoutCommand();
145
- expect(result.readme).toBeNull();
146
- });
147
-
148
- it('should set diff to null when not a git repo', () => {
149
- mockExistsSync.mockImplementation((p: string) => {
150
- const norm = p.replace(/\\/g, '/');
151
- if (norm.endsWith('.git')) return false;
152
- if (norm.endsWith('package.json')) return true;
153
- if (norm.endsWith('.gitignore')) return false;
154
- if (norm.endsWith('.cursorrules')) return false;
155
- if (norm.endsWith('CLAUDE.md')) return false;
156
- if (norm.endsWith('AGENTS.md')) return false;
157
- if (norm.endsWith('copilot-instructions.md')) return false;
158
- if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
159
- return true;
160
- });
161
- mockReadFileSync.mockImplementation((p: string) => {
162
- if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
163
- return '';
164
- });
165
- mockReaddirSync.mockReturnValue([]);
166
- mockStatSync.mockReturnValue({ isDirectory: () => false });
167
-
168
- const result = handleScoutCommand();
169
- expect(result.diff).toBeNull();
170
- });
171
-
172
- it('should strip full diffs from scout diff output (stagedDiff/unstagedDiff removed)', () => {
173
- setupBasicProject();
174
- // Even if handleDiffCommand returned these, scout should delete them
175
- const result = handleScoutCommand();
176
- if (result.diff) {
177
- expect(result.diff.stagedDiff).toBeUndefined();
178
- expect(result.diff.unstagedDiff).toBeUndefined();
179
- }
180
- });
181
-
182
- it('should set agentRules to null when no rule files exist', () => {
183
- mockExistsSync.mockImplementation((p: string) => {
184
- const norm = p.replace(/\\/g, '/');
185
- if (norm.endsWith('.cursorrules')) return false;
186
- if (norm.endsWith('CLAUDE.md')) return false;
187
- if (norm.endsWith('AGENTS.md')) return false;
188
- if (norm.endsWith('copilot-instructions.md')) return false;
189
- if (norm.endsWith('README.md') || norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
190
- if (norm.endsWith('package.json')) return true;
191
- if (norm.endsWith('.git')) return false;
192
- if (norm.endsWith('.gitignore')) return false;
193
- return true;
194
- });
195
- mockReadFileSync.mockImplementation((p: string) => {
196
- if (p.includes('package.json')) return JSON.stringify({ name: 'test' });
197
- return '';
198
- });
199
- mockReaddirSync.mockReturnValue([]);
200
- mockStatSync.mockReturnValue({ isDirectory: () => false });
201
-
202
- const result = handleScoutCommand();
203
- expect(result.agentRules).toBeNull();
204
- });
205
-
206
- it('should detect agent rules from .cursorrules', () => {
207
- setupBasicProject();
208
- mockExistsSync.mockImplementation((p: string) => {
209
- const norm = p.replace(/\\/g, '/');
210
- if (norm.endsWith('.cursorrules')) return true;
211
- if (norm.endsWith('package.json')) return true;
212
- if (norm.endsWith('.git')) return true;
213
- if (norm.endsWith('README.md')) return true;
214
- if (norm.endsWith('.gitignore')) return false;
215
- if (norm.endsWith('CLAUDE.md')) return false;
216
- if (norm.endsWith('AGENTS.md')) return false;
217
- if (norm.endsWith('copilot-instructions.md')) return false;
218
- if (norm.endsWith('Readme.md') || norm.endsWith('readme.md')) return false;
219
- return true;
220
- });
221
- mockReadFileSync.mockImplementation((p: string) => {
222
- const norm = p.replace(/\\/g, '/');
223
- if (norm.endsWith('.cursorrules')) return 'Use TypeScript strict mode';
224
- if (norm.endsWith('package.json')) return JSON.stringify({ name: 'test', dependencies: {} });
225
- if (norm.endsWith('README.md')) return '# Test';
226
- return '';
227
- });
228
-
229
- const result = handleScoutCommand();
230
- expect(result.agentRules).toBeDefined();
231
- expect(result.agentRules.source).toBe('.cursorrules');
232
- expect(result.agentRules.content).toContain('TypeScript strict mode');
233
- });
234
-
235
- it('should pass budget allocation to tree (approx 50%)', () => {
236
- setupBasicProject();
237
- // We verify that tree gets called with a budget argument
238
- // by checking the result completes without error with a budget
239
- const result = handleScoutCommand(1000);
240
- expect(result.tree).toBeDefined();
241
- });
242
-
243
- it('should set workspaces to null when not present', () => {
244
- setupBasicProject();
245
- const result = handleScoutCommand();
246
- expect(result.workspaces).toBeNull();
247
- });
248
- });
@@ -1,222 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach } from 'vitest';
2
- import { handleTreeCommand } from '../src/commands/tree';
3
- import * as fs from 'fs';
4
-
5
- vi.mock('fs', async () => {
6
- const actual = await vi.importActual<typeof import('fs')>('fs');
7
- return {
8
- ...actual,
9
- existsSync: vi.fn(),
10
- readFileSync: vi.fn(),
11
- readdirSync: vi.fn(),
12
- statSync: vi.fn()
13
- };
14
- });
15
-
16
- const mockExistsSync = fs.existsSync as any;
17
- const mockReadFileSync = fs.readFileSync as any;
18
- const mockReaddirSync = fs.readdirSync as any;
19
- const mockStatSync = fs.statSync as any;
20
-
21
- // Helper to build a virtual filesystem
22
- function setupVirtualFS(tree: Record<string, 'dir' | 'file' | null>) {
23
- mockExistsSync.mockImplementation((p: string) => {
24
- // Normalize path separators for matching
25
- const normalized = p.replace(/\\/g, '/');
26
- // Check .gitignore existence
27
- if (normalized.endsWith('.gitignore')) {
28
- return tree['.gitignore'] !== undefined;
29
- }
30
- return true; // default: exists
31
- });
32
-
33
- mockReaddirSync.mockImplementation((dirPath: string) => {
34
- const normalized = dirPath.replace(/\\/g, '/');
35
- const entries: string[] = [];
36
- for (const [key, type] of Object.entries(tree)) {
37
- // Get entries that are direct children of dirPath
38
- const parent = key.substring(0, key.lastIndexOf('/'));
39
- const name = key.substring(key.lastIndexOf('/') + 1);
40
- if (!key.includes('/') && type !== null) {
41
- // Root-level entries
42
- entries.push(key);
43
- }
44
- }
45
- return entries;
46
- });
47
-
48
- mockStatSync.mockImplementation((p: string, opts?: any) => {
49
- const normalized = p.replace(/\\/g, '/');
50
- const name = normalized.substring(normalized.lastIndexOf('/') + 1);
51
- const entry = tree[name];
52
- if (entry === 'dir') return { isDirectory: () => true };
53
- if (entry === 'file') return { isDirectory: () => false };
54
- // Default: file
55
- return { isDirectory: () => false };
56
- });
57
- }
58
-
59
- describe('Tree Command', () => {
60
- beforeEach(() => {
61
- vi.clearAllMocks();
62
- // Default: no .gitignore
63
- mockReadFileSync.mockReturnValue('');
64
- });
65
-
66
- it('should throw when target directory does not exist', () => {
67
- mockExistsSync.mockReturnValue(false);
68
- expect(() => handleTreeCommand(['nonexistent'])).toThrow('Directory not found: nonexistent');
69
- });
70
-
71
- it('should return tree structure with files and directories', () => {
72
- mockExistsSync.mockReturnValue(true);
73
- mockReadFileSync.mockReturnValue(''); // empty .gitignore
74
-
75
- // Root has: src/ (dir), README.md (file), package.json (file)
76
- mockReaddirSync.mockImplementation((dirPath: string) => {
77
- const normalized = dirPath.replace(/\\/g, '/');
78
- if (normalized.endsWith('src')) return ['index.ts', 'utils.ts'];
79
- return ['src', 'README.md', 'package.json'];
80
- });
81
-
82
- mockStatSync.mockImplementation((p: string, opts?: any) => {
83
- const normalized = p.replace(/\\/g, '/');
84
- if (normalized.endsWith('src')) return { isDirectory: () => true };
85
- return { isDirectory: () => false };
86
- });
87
-
88
- const result = handleTreeCommand([]);
89
- expect(result.root).toBe('.');
90
- expect(result.depth).toBe(3);
91
- expect(result.structure).toBeDefined();
92
- expect(result.stats.totalFiles).toBeGreaterThan(0);
93
- });
94
-
95
- it('should respect --depth flag', () => {
96
- mockExistsSync.mockReturnValue(true);
97
- mockReadFileSync.mockReturnValue('');
98
-
99
- mockReaddirSync.mockImplementation((dirPath: string) => {
100
- const norm = dirPath.replace(/\\/g, '/');
101
- if (norm.endsWith('deep')) return ['deeper'];
102
- if (norm.endsWith('deeper')) return ['deepest.txt'];
103
- return ['deep'];
104
- });
105
-
106
- mockStatSync.mockImplementation((p: string) => {
107
- const norm = p.replace(/\\/g, '/');
108
- if (norm.endsWith('deep') || norm.endsWith('deeper')) return { isDirectory: () => true };
109
- return { isDirectory: () => false };
110
- });
111
-
112
- // depth=1 should not traverse into subdirs beyond first level
113
- const result = handleTreeCommand(['--depth=1']);
114
- expect(result.depth).toBe(1);
115
- // 'deep/' at depth 1, its children are at depth 2 which exceeds limit
116
- expect(result.structure['deep/']).toEqual({ _more: true });
117
- });
118
-
119
- it('should ignore default directories (node_modules, .git, dist)', () => {
120
- mockExistsSync.mockReturnValue(true);
121
- mockReadFileSync.mockReturnValue('');
122
-
123
- mockReaddirSync.mockReturnValue(['src', 'node_modules', '.git', 'dist', 'index.ts']);
124
-
125
- mockStatSync.mockImplementation((p: string) => {
126
- const name = p.replace(/\\/g, '/').split('/').pop();
127
- if (['src', 'node_modules', '.git', 'dist'].includes(name!)) {
128
- return { isDirectory: () => true };
129
- }
130
- return { isDirectory: () => false };
131
- });
132
-
133
- const result = handleTreeCommand([]);
134
- // node_modules, .git, dist should be in ignored list
135
- expect(result.stats.ignored).toContain('node_modules');
136
- expect(result.stats.ignored).toContain('.git');
137
- expect(result.stats.ignored).toContain('dist');
138
- });
139
-
140
- it('should respect .gitignore patterns', () => {
141
- mockExistsSync.mockReturnValue(true);
142
- // .gitignore ignores "*.log" and "build/"
143
- mockReadFileSync.mockReturnValue('*.log\nbuild/\n');
144
-
145
- mockReaddirSync.mockReturnValue(['src', 'build', 'app.ts', 'debug.log']);
146
-
147
- mockStatSync.mockImplementation((p: string) => {
148
- const name = p.replace(/\\/g, '/').split('/').pop();
149
- if (['src', 'build'].includes(name!)) return { isDirectory: () => true };
150
- return { isDirectory: () => false };
151
- });
152
-
153
- const result = handleTreeCommand([]);
154
- // build should be ignored (from .gitignore), debug.log should be ignored (*.log pattern)
155
- expect(result.stats.ignored).toContain('build');
156
- });
157
-
158
- it('should use _files compression for directories with only files', () => {
159
- mockExistsSync.mockReturnValue(true);
160
- mockReadFileSync.mockReturnValue('');
161
-
162
- // Root has only files
163
- mockReaddirSync.mockReturnValue(['a.ts', 'b.ts', 'c.ts']);
164
- mockStatSync.mockReturnValue({ isDirectory: () => false });
165
-
166
- const result = handleTreeCommand([]);
167
- // When a dir has only files, it uses the _files array optimization
168
- expect(result.structure._files).toEqual(['a.ts', 'b.ts', 'c.ts']);
169
- });
170
-
171
- it('should handle empty directories', () => {
172
- mockExistsSync.mockReturnValue(true);
173
- mockReadFileSync.mockReturnValue('');
174
- mockReaddirSync.mockReturnValue([]);
175
-
176
- const result = handleTreeCommand([]);
177
- expect(result.structure).toEqual({});
178
- expect(result.stats.totalFiles).toBe(0);
179
- });
180
-
181
- it('should accept custom target directory', () => {
182
- mockExistsSync.mockReturnValue(true);
183
- mockReadFileSync.mockReturnValue('');
184
- mockReaddirSync.mockReturnValue(['app.ts']);
185
- mockStatSync.mockReturnValue({ isDirectory: () => false });
186
-
187
- const result = handleTreeCommand(['src']);
188
- expect(result.root).toBe('src');
189
- });
190
-
191
- it('should handle permission errors on directories', () => {
192
- mockExistsSync.mockReturnValue(true);
193
- mockReadFileSync.mockReturnValue('');
194
- mockReaddirSync.mockImplementation(() => { throw new Error('EACCES'); });
195
-
196
- const result = handleTreeCommand([]);
197
- // Should return empty structure, not throw
198
- expect(result.structure).toEqual({});
199
- });
200
-
201
- it('should handle mixed directories and files', () => {
202
- mockExistsSync.mockReturnValue(true);
203
- mockReadFileSync.mockReturnValue('');
204
-
205
- mockReaddirSync.mockImplementation((dirPath: string) => {
206
- const norm = dirPath.replace(/\\/g, '/');
207
- if (norm.endsWith('utils')) return ['helper.ts'];
208
- return ['utils', 'index.ts', 'config.ts'];
209
- });
210
-
211
- mockStatSync.mockImplementation((p: string) => {
212
- const norm = p.replace(/\\/g, '/');
213
- if (norm.endsWith('utils')) return { isDirectory: () => true };
214
- return { isDirectory: () => false };
215
- });
216
-
217
- const result = handleTreeCommand([]);
218
- // Mixed dir: files should be individual keys, not in _files
219
- expect(result.structure['utils/']).toBeDefined();
220
- expect(result.structure['index.ts']).toBeDefined();
221
- });
222
- });