aiseerr 1.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.
- package/AGENTS.md +14 -0
- package/CODE-REVIEW.md +333 -0
- package/PRD.md +397 -0
- package/README.md +80 -0
- package/ana-suggestions.md +105 -0
- package/dist/cli.js +37 -0
- package/package.json +37 -0
- package/src/cli.ts +118 -0
- package/src/commands/diff.ts +128 -0
- package/src/commands/env.ts +234 -0
- package/src/commands/init.ts +82 -0
- package/src/commands/read.ts +113 -0
- package/src/commands/scout.ts +93 -0
- package/src/commands/tree.ts +133 -0
- package/src/utils/output.ts +123 -0
- package/tests/cli.test.ts +172 -0
- package/tests/diff.test.ts +169 -0
- package/tests/env.test.ts +69 -0
- package/tests/init.test.ts +164 -0
- package/tests/output.test.ts +49 -0
- package/tests/read.test.ts +169 -0
- package/tests/scout.test.ts +248 -0
- package/tests/tree.test.ts +222 -0
- package/tsconfig.json +15 -0
- package/tsup.config.ts +11 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { handleEnvCommand } from '../src/commands/env';
|
|
3
|
+
import * as child_process from 'child_process';
|
|
4
|
+
import * as fs from 'fs';
|
|
5
|
+
|
|
6
|
+
// We mock readFileSync and existsSync heavily for this test
|
|
7
|
+
vi.mock('fs', async () => {
|
|
8
|
+
const actualFs = await vi.importActual<typeof import('fs')>('fs');
|
|
9
|
+
return {
|
|
10
|
+
...actualFs,
|
|
11
|
+
existsSync: vi.fn(),
|
|
12
|
+
readFileSync: vi.fn()
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
vi.mock('child_process', () => ({
|
|
17
|
+
execFileSync: vi.fn()
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
const mockExistsSync = fs.existsSync as any;
|
|
21
|
+
const mockReadFileSync = fs.readFileSync as any;
|
|
22
|
+
const mockExecFileSync = child_process.execFileSync as any;
|
|
23
|
+
|
|
24
|
+
describe('Env Command', () => {
|
|
25
|
+
it('should detect pnpm and frameworks', () => {
|
|
26
|
+
// Setup virtual filesystem
|
|
27
|
+
mockExistsSync.mockImplementation((path: string) => {
|
|
28
|
+
if (path.includes('package.json')) return true;
|
|
29
|
+
if (path.includes('pnpm-lock.yaml')) return true;
|
|
30
|
+
if (path.includes('.git')) return true;
|
|
31
|
+
return false;
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
mockReadFileSync.mockImplementation((path: string) => {
|
|
35
|
+
if (path.includes('package.json')) {
|
|
36
|
+
return JSON.stringify({
|
|
37
|
+
name: 'test-app',
|
|
38
|
+
engines: { node: '>=18' },
|
|
39
|
+
dependencies: { next: '14.0.0', react: '18.0.0' },
|
|
40
|
+
devDependencies: { vitest: '1.0.0' }
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return '';
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
mockExecFileSync.mockImplementation((cmd: string, args: string[]) => {
|
|
47
|
+
if (cmd === 'pnpm' && args.includes('--version')) return '9.1.0\n';
|
|
48
|
+
if (cmd === 'git' && args.includes('rev-parse')) return 'main\n';
|
|
49
|
+
if (cmd === 'git' && args.includes('--porcelain')) return ' M file.ts\n';
|
|
50
|
+
return '';
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const result = handleEnvCommand();
|
|
54
|
+
|
|
55
|
+
// Assertions
|
|
56
|
+
expect(result.packageManager.name).toBe('pnpm');
|
|
57
|
+
expect(result.packageManager.version).toBe('9.1.0');
|
|
58
|
+
expect(result.frameworks).toContain('next');
|
|
59
|
+
expect(result.frameworks).toContain('react');
|
|
60
|
+
|
|
61
|
+
// Test framework inference
|
|
62
|
+
expect(result.testFramework?.name).toBe('vitest');
|
|
63
|
+
expect(result.testFramework?.runCommand).toContain('vitest run');
|
|
64
|
+
|
|
65
|
+
// Git detection
|
|
66
|
+
expect(result.git.branch).toBe('main');
|
|
67
|
+
expect(result.git.hasUncommitted).toBe(true);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { handleInitCommand } from '../src/commands/init';
|
|
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
|
+
writeFileSync: vi.fn()
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const mockExistsSync = fs.existsSync as any;
|
|
16
|
+
const mockReadFileSync = fs.readFileSync as any;
|
|
17
|
+
const mockWriteFileSync = fs.writeFileSync as any;
|
|
18
|
+
|
|
19
|
+
describe('Init Command', () => {
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
vi.clearAllMocks();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('should create AGENTS.md by default when no rule files exist', () => {
|
|
25
|
+
mockExistsSync.mockReturnValue(false);
|
|
26
|
+
|
|
27
|
+
const result = handleInitCommand([]);
|
|
28
|
+
expect(result.action).toBe('created');
|
|
29
|
+
expect(result.target).toBe('AGENTS.md');
|
|
30
|
+
expect(mockWriteFileSync).toHaveBeenCalledTimes(1);
|
|
31
|
+
// Check content includes the template
|
|
32
|
+
const writtenContent = mockWriteFileSync.mock.calls[0][1];
|
|
33
|
+
expect(writtenContent).toContain('aiseerr');
|
|
34
|
+
expect(writtenContent).toContain('# AI Agent Rules');
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should inject into existing file that does not have marker', () => {
|
|
38
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
39
|
+
const norm = p.replace(/\\/g, '/');
|
|
40
|
+
if (norm.endsWith('AGENTS.md')) return true;
|
|
41
|
+
return false;
|
|
42
|
+
});
|
|
43
|
+
mockReadFileSync.mockReturnValue('# Existing Rules\n\nSome rules here.');
|
|
44
|
+
|
|
45
|
+
const result = handleInitCommand([]);
|
|
46
|
+
expect(result.action).toBe('injected');
|
|
47
|
+
expect(result.target).toBe('AGENTS.md');
|
|
48
|
+
|
|
49
|
+
const writtenContent = mockWriteFileSync.mock.calls[0][1];
|
|
50
|
+
expect(writtenContent).toContain('# Existing Rules');
|
|
51
|
+
expect(writtenContent).toContain('aiseerr:start');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should skip injection if marker already exists', () => {
|
|
55
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
56
|
+
const norm = p.replace(/\\/g, '/');
|
|
57
|
+
if (norm.endsWith('AGENTS.md')) return true;
|
|
58
|
+
return false;
|
|
59
|
+
});
|
|
60
|
+
mockReadFileSync.mockReturnValue('# Rules\n<!-- aiseerr:start -->\nstuff\n<!-- aiseerr:end -->');
|
|
61
|
+
|
|
62
|
+
const result = handleInitCommand([]);
|
|
63
|
+
expect(result.action).toBe('skipped');
|
|
64
|
+
expect(result.reason).toBe('Already injected');
|
|
65
|
+
expect(mockWriteFileSync).not.toHaveBeenCalled();
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should use --format=claude to target CLAUDE.md', () => {
|
|
69
|
+
mockExistsSync.mockReturnValue(false);
|
|
70
|
+
|
|
71
|
+
const result = handleInitCommand(['--format=claude']);
|
|
72
|
+
expect(result.target).toBe('CLAUDE.md');
|
|
73
|
+
expect(result.action).toBe('created');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should use --format=cursorrules to target .cursorrules', () => {
|
|
77
|
+
mockExistsSync.mockReturnValue(false);
|
|
78
|
+
|
|
79
|
+
const result = handleInitCommand(['--format=cursorrules']);
|
|
80
|
+
expect(result.target).toBe('.cursorrules');
|
|
81
|
+
expect(result.action).toBe('created');
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('should use --format=agents to target AGENTS.md', () => {
|
|
85
|
+
mockExistsSync.mockReturnValue(false);
|
|
86
|
+
|
|
87
|
+
const result = handleInitCommand(['--format=agents']);
|
|
88
|
+
expect(result.target).toBe('AGENTS.md');
|
|
89
|
+
expect(result.action).toBe('created');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should throw on unknown format', () => {
|
|
93
|
+
expect(() => handleInitCommand(['--format=unknown'])).toThrow('Unknown format: unknown');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('should auto-detect existing CLAUDE.md and inject there', () => {
|
|
97
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
98
|
+
const norm = p.replace(/\\/g, '/');
|
|
99
|
+
// AGENTS.md doesn't exist, but CLAUDE.md does
|
|
100
|
+
if (norm.endsWith('AGENTS.md')) return false;
|
|
101
|
+
if (norm.endsWith('CLAUDE.md')) return true;
|
|
102
|
+
return false;
|
|
103
|
+
});
|
|
104
|
+
mockReadFileSync.mockReturnValue('# Claude Rules\n\nSome content.');
|
|
105
|
+
|
|
106
|
+
const result = handleInitCommand([]);
|
|
107
|
+
expect(result.target).toBe('CLAUDE.md');
|
|
108
|
+
expect(result.action).toBe('injected');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('should auto-detect .cursorrules before CLAUDE.md', () => {
|
|
112
|
+
// The search order is: AGENTS.md, CLAUDE.md, .cursorrules, copilot-instructions.md
|
|
113
|
+
// But since AGENTS.md is checked first, if it exists it takes priority
|
|
114
|
+
mockExistsSync.mockImplementation((p: string) => {
|
|
115
|
+
const norm = p.replace(/\\/g, '/');
|
|
116
|
+
if (norm.endsWith('AGENTS.md')) return true;
|
|
117
|
+
if (norm.endsWith('CLAUDE.md')) return true;
|
|
118
|
+
if (norm.endsWith('.cursorrules')) return true;
|
|
119
|
+
return false;
|
|
120
|
+
});
|
|
121
|
+
mockReadFileSync.mockReturnValue('# Agent rules');
|
|
122
|
+
|
|
123
|
+
const result = handleInitCommand([]);
|
|
124
|
+
// AGENTS.md is first in the search list
|
|
125
|
+
expect(result.target).toBe('AGENTS.md');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should include linesAdded count in result', () => {
|
|
129
|
+
mockExistsSync.mockReturnValue(false);
|
|
130
|
+
|
|
131
|
+
const result = handleInitCommand([]);
|
|
132
|
+
expect(result.linesAdded).toBeGreaterThan(0);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should include marker info in result', () => {
|
|
136
|
+
mockExistsSync.mockReturnValue(false);
|
|
137
|
+
|
|
138
|
+
const result = handleInitCommand([]);
|
|
139
|
+
expect(result.marker).toBe('aiseerr:start');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('should write UTF-8 encoded files', () => {
|
|
143
|
+
mockExistsSync.mockReturnValue(false);
|
|
144
|
+
|
|
145
|
+
handleInitCommand([]);
|
|
146
|
+
// Check that writeFileSync was called with utf-8 encoding
|
|
147
|
+
expect(mockWriteFileSync).toHaveBeenCalledWith(
|
|
148
|
+
expect.any(String),
|
|
149
|
+
expect.any(String),
|
|
150
|
+
'utf-8'
|
|
151
|
+
);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should inject template with correct content markers', () => {
|
|
155
|
+
mockExistsSync.mockReturnValue(false);
|
|
156
|
+
|
|
157
|
+
handleInitCommand([]);
|
|
158
|
+
const content = mockWriteFileSync.mock.calls[0][1];
|
|
159
|
+
expect(content).toContain('<!-- aiseerr:start -->');
|
|
160
|
+
expect(content).toContain('<!-- aiseerr:end -->');
|
|
161
|
+
expect(content).toContain('npx aiseerr scout --budget=2000');
|
|
162
|
+
expect(content).toContain('npx aiseerr diff --full');
|
|
163
|
+
});
|
|
164
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
});
|
|
@@ -0,0 +1,169 @@
|
|
|
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
|
+
});
|