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.
@@ -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
+ });