@hanzo/dev 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.
@@ -0,0 +1,305 @@
1
+ import { describe, test, expect, beforeEach, jest } from '@jest/globals';
2
+ import { CodeActAgent, AgentState } from '../src/lib/code-act-agent';
3
+ import { FunctionCallingSystem } from '../src/lib/function-calling';
4
+
5
+ describe('CodeActAgent', () => {
6
+ let agent: CodeActAgent;
7
+ let mockFunctionCalling: jest.Mocked<FunctionCallingSystem>;
8
+
9
+ beforeEach(() => {
10
+ // Mock function calling system
11
+ mockFunctionCalling = {
12
+ registerTool: jest.fn(),
13
+ callFunctions: jest.fn(),
14
+ getAvailableTools: jest.fn().mockReturnValue([
15
+ { name: 'view_file', description: 'View file contents' },
16
+ { name: 'str_replace', description: 'Replace string in file' },
17
+ { name: 'run_command', description: 'Run shell command' }
18
+ ]),
19
+ getAllToolSchemas: jest.fn().mockReturnValue([])
20
+ } as any;
21
+
22
+ agent = new CodeActAgent('test-agent', mockFunctionCalling);
23
+ });
24
+
25
+ describe('state management', () => {
26
+ test('should initialize with correct default state', () => {
27
+ const state = agent.getState();
28
+ expect(state.currentTask).toBe('');
29
+ expect(state.plan).toEqual([]);
30
+ expect(state.completedSteps).toEqual([]);
31
+ expect(state.currentStep).toBe(0);
32
+ expect(state.errors).toEqual([]);
33
+ expect(state.observations).toEqual([]);
34
+ });
35
+
36
+ test('should update state correctly', () => {
37
+ const newState: Partial<AgentState> = {
38
+ currentTask: 'Fix bug in login',
39
+ plan: ['Locate login file', 'Fix validation', 'Test changes'],
40
+ currentStep: 1
41
+ };
42
+
43
+ agent.setState(newState);
44
+ const state = agent.getState();
45
+
46
+ expect(state.currentTask).toBe('Fix bug in login');
47
+ expect(state.plan).toHaveLength(3);
48
+ expect(state.currentStep).toBe(1);
49
+ });
50
+ });
51
+
52
+ describe('planning', () => {
53
+ test('should generate plan for task', async () => {
54
+ const task = 'Add user authentication to the API';
55
+
56
+ // Mock LLM response for planning
57
+ const mockPlan = [
58
+ 'Analyze current API structure',
59
+ 'Install authentication dependencies',
60
+ 'Create auth middleware',
61
+ 'Add login/logout endpoints',
62
+ 'Update existing endpoints with auth checks',
63
+ 'Write tests for authentication'
64
+ ];
65
+
66
+ // The agent should generate a plan based on the task
67
+ await agent.plan(task);
68
+
69
+ const state = agent.getState();
70
+ expect(state.currentTask).toBe(task);
71
+ expect(state.plan.length).toBeGreaterThan(0);
72
+ });
73
+
74
+ test('should handle planning errors gracefully', async () => {
75
+ const task = 'Invalid task that causes error';
76
+
77
+ // Even with errors, planning should not throw
78
+ await expect(agent.plan(task)).resolves.not.toThrow();
79
+
80
+ const state = agent.getState();
81
+ expect(state.currentTask).toBe(task);
82
+ });
83
+ });
84
+
85
+ describe('task execution', () => {
86
+ test('should execute single step', async () => {
87
+ // Set up agent with a plan
88
+ agent.setState({
89
+ currentTask: 'Fix typo in README',
90
+ plan: ['View README.md', 'Fix typo', 'Verify changes'],
91
+ currentStep: 0
92
+ });
93
+
94
+ // Mock function calling for view_file
95
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
96
+ success: true,
97
+ content: '# README\n\nThis is a typpo in the readme.'
98
+ }]);
99
+
100
+ const result = await agent.executeStep();
101
+
102
+ expect(result.completed).toBe(false);
103
+ expect(result.action).toBe('View README.md');
104
+ expect(mockFunctionCalling.callFunctions).toHaveBeenCalled();
105
+ });
106
+
107
+ test('should handle step execution errors', async () => {
108
+ agent.setState({
109
+ currentTask: 'Run failing command',
110
+ plan: ['Execute broken command'],
111
+ currentStep: 0
112
+ });
113
+
114
+ // Mock function calling to throw error
115
+ mockFunctionCalling.callFunctions.mockRejectedValueOnce(
116
+ new Error('Command not found')
117
+ );
118
+
119
+ const result = await agent.executeStep();
120
+
121
+ expect(result.completed).toBe(false);
122
+ expect(result.error).toBe('Command not found');
123
+
124
+ const state = agent.getState();
125
+ expect(state.errors).toHaveLength(1);
126
+ expect(state.errors[0]).toContain('Command not found');
127
+ });
128
+
129
+ test('should mark task as completed when all steps done', async () => {
130
+ agent.setState({
131
+ currentTask: 'Simple task',
132
+ plan: ['Step 1', 'Step 2'],
133
+ currentStep: 1,
134
+ completedSteps: ['Step 1']
135
+ });
136
+
137
+ // Mock successful execution
138
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
139
+ success: true
140
+ }]);
141
+
142
+ const result = await agent.executeStep();
143
+
144
+ expect(result.completed).toBe(true);
145
+ expect(result.action).toBe('Step 2');
146
+
147
+ const state = agent.getState();
148
+ expect(state.completedSteps).toHaveLength(2);
149
+ });
150
+ });
151
+
152
+ describe('parallel execution', () => {
153
+ test('should identify parallelizable steps', () => {
154
+ const plan = [
155
+ 'Download file A',
156
+ 'Download file B',
157
+ 'Process file A',
158
+ 'Process file B',
159
+ 'Merge results'
160
+ ];
161
+
162
+ const parallel = agent.identifyParallelSteps(plan);
163
+
164
+ // Downloads can be parallel
165
+ expect(parallel[0]).toEqual([0, 1]);
166
+ // Processing depends on downloads
167
+ expect(parallel[1]).toEqual([2]);
168
+ expect(parallel[2]).toEqual([3]);
169
+ // Merge depends on processing
170
+ expect(parallel[3]).toEqual([4]);
171
+ });
172
+
173
+ test('should execute parallel steps concurrently', async () => {
174
+ agent.setState({
175
+ currentTask: 'Parallel downloads',
176
+ plan: ['Download file1.txt', 'Download file2.txt', 'Merge files'],
177
+ currentStep: 0
178
+ });
179
+
180
+ // Mock both downloads to succeed
181
+ mockFunctionCalling.callFunctions
182
+ .mockResolvedValueOnce([{ success: true, file: 'file1.txt' }])
183
+ .mockResolvedValueOnce([{ success: true, file: 'file2.txt' }]);
184
+
185
+ // Execute should handle parallel steps
186
+ const result1 = await agent.executeStep();
187
+ expect(result1.action).toContain('Download');
188
+
189
+ // The agent should recognize these can be parallel
190
+ const state = agent.getState();
191
+ expect(state.currentStep).toBeLessThanOrEqual(2);
192
+ });
193
+ });
194
+
195
+ describe('self-correction', () => {
196
+ test('should retry failed steps with corrections', async () => {
197
+ agent.setState({
198
+ currentTask: 'Fix syntax error',
199
+ plan: ['Edit file with error'],
200
+ currentStep: 0
201
+ });
202
+
203
+ // First attempt fails
204
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
205
+ success: false,
206
+ error: 'Syntax error in edit'
207
+ }]);
208
+
209
+ // Agent should detect error and retry
210
+ const result1 = await agent.executeStep();
211
+ expect(result1.error).toBeDefined();
212
+
213
+ // Second attempt with correction succeeds
214
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
215
+ success: true
216
+ }]);
217
+
218
+ const result2 = await agent.executeStep();
219
+ expect(result2.error).toBeUndefined();
220
+ expect(result2.retryCount).toBeGreaterThan(0);
221
+ });
222
+
223
+ test('should give up after max retries', async () => {
224
+ agent.setState({
225
+ currentTask: 'Impossible task',
226
+ plan: ['Do impossible thing'],
227
+ currentStep: 0
228
+ });
229
+
230
+ // All attempts fail
231
+ mockFunctionCalling.callFunctions.mockRejectedValue(
232
+ new Error('Cannot do impossible thing')
233
+ );
234
+
235
+ let lastResult;
236
+ for (let i = 0; i < 5; i++) {
237
+ lastResult = await agent.executeStep();
238
+ }
239
+
240
+ expect(lastResult!.error).toBeDefined();
241
+ expect(lastResult!.aborted).toBe(true);
242
+ });
243
+ });
244
+
245
+ describe('observation handling', () => {
246
+ test('should collect and store observations', async () => {
247
+ agent.setState({
248
+ currentTask: 'Analyze codebase',
249
+ plan: ['List files', 'Read main file'],
250
+ currentStep: 0
251
+ });
252
+
253
+ // Mock file listing
254
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
255
+ success: true,
256
+ output: 'file1.js\nfile2.js\nindex.js'
257
+ }]);
258
+
259
+ await agent.executeStep();
260
+
261
+ const state = agent.getState();
262
+ expect(state.observations).toHaveLength(1);
263
+ expect(state.observations[0]).toContain('file1.js');
264
+ });
265
+
266
+ test('should use observations for context', async () => {
267
+ // Pre-populate observations
268
+ agent.setState({
269
+ currentTask: 'Fix bug',
270
+ plan: ['Find bug location', 'Fix bug'],
271
+ currentStep: 1,
272
+ observations: ['Bug is in auth.js on line 42']
273
+ });
274
+
275
+ // The agent should use the observation context
276
+ mockFunctionCalling.callFunctions.mockResolvedValueOnce([{
277
+ success: true,
278
+ result: 'Fixed bug in auth.js'
279
+ }]);
280
+
281
+ const result = await agent.executeStep();
282
+ expect(result.completed).toBe(true);
283
+ });
284
+ });
285
+
286
+ describe('complete task execution', () => {
287
+ test('should execute entire task from plan to completion', async () => {
288
+ const task = 'Add logging to application';
289
+
290
+ // Mock successful execution of all steps
291
+ mockFunctionCalling.callFunctions
292
+ .mockResolvedValueOnce([{ success: true }]) // Install logger
293
+ .mockResolvedValueOnce([{ success: true }]) // Create logger config
294
+ .mockResolvedValueOnce([{ success: true }]) // Add logging statements
295
+ .mockResolvedValueOnce([{ success: true }]); // Test logging
296
+
297
+ await agent.plan(task);
298
+ const result = await agent.execute(task);
299
+
300
+ expect(result.success).toBe(true);
301
+ expect(result.completedSteps.length).toBeGreaterThan(0);
302
+ expect(result.errors).toHaveLength(0);
303
+ });
304
+ });
305
+ });
@@ -0,0 +1,223 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ import * as os from 'os';
5
+ import { Editor, EditCommand } from '../src/lib/editor';
6
+
7
+ describe('Editor', () => {
8
+ let editor: Editor;
9
+ let testDir: string;
10
+ let testFile: string;
11
+
12
+ beforeEach(() => {
13
+ // Create temporary test directory
14
+ testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'hanzo-dev-test-'));
15
+ testFile = path.join(testDir, 'test.txt');
16
+ editor = new Editor(testDir);
17
+ });
18
+
19
+ afterEach(() => {
20
+ // Clean up test directory
21
+ fs.rmSync(testDir, { recursive: true, force: true });
22
+ });
23
+
24
+ describe('create command', () => {
25
+ test('should create a new file with content', async () => {
26
+ const command: EditCommand = {
27
+ command: 'create',
28
+ path: testFile,
29
+ content: 'Hello, World!'
30
+ };
31
+
32
+ const result = await editor.execute(command);
33
+ expect(result.success).toBe(true);
34
+ expect(fs.existsSync(testFile)).toBe(true);
35
+ expect(fs.readFileSync(testFile, 'utf-8')).toBe('Hello, World!');
36
+ });
37
+
38
+ test('should fail when file already exists', async () => {
39
+ fs.writeFileSync(testFile, 'existing content');
40
+
41
+ const command: EditCommand = {
42
+ command: 'create',
43
+ path: testFile,
44
+ content: 'new content'
45
+ };
46
+
47
+ const result = await editor.execute(command);
48
+ expect(result.success).toBe(false);
49
+ expect(result.error).toContain('already exists');
50
+ });
51
+ });
52
+
53
+ describe('view command', () => {
54
+ test('should view entire file when no line range specified', async () => {
55
+ const content = 'Line 1\nLine 2\nLine 3';
56
+ fs.writeFileSync(testFile, content);
57
+
58
+ const command: EditCommand = {
59
+ command: 'view',
60
+ path: testFile
61
+ };
62
+
63
+ const result = await editor.execute(command);
64
+ expect(result.success).toBe(true);
65
+ expect(result.content).toContain('Line 1');
66
+ expect(result.content).toContain('Line 2');
67
+ expect(result.content).toContain('Line 3');
68
+ });
69
+
70
+ test('should view specific line range', async () => {
71
+ const lines = Array.from({ length: 10 }, (_, i) => `Line ${i + 1}`);
72
+ fs.writeFileSync(testFile, lines.join('\n'));
73
+
74
+ const command: EditCommand = {
75
+ command: 'view',
76
+ path: testFile,
77
+ startLine: 3,
78
+ endLine: 5
79
+ };
80
+
81
+ const result = await editor.execute(command);
82
+ expect(result.success).toBe(true);
83
+ expect(result.content).toContain('Line 3');
84
+ expect(result.content).toContain('Line 4');
85
+ expect(result.content).toContain('Line 5');
86
+ expect(result.content).not.toContain('Line 1');
87
+ expect(result.content).not.toContain('Line 10');
88
+ });
89
+ });
90
+
91
+ describe('str_replace command', () => {
92
+ test('should replace string in file', async () => {
93
+ const content = 'Hello, World!\nThis is a test.\nHello again!';
94
+ fs.writeFileSync(testFile, content);
95
+
96
+ const command: EditCommand = {
97
+ command: 'str_replace',
98
+ path: testFile,
99
+ oldStr: 'Hello, World!',
100
+ newStr: 'Hi, Universe!'
101
+ };
102
+
103
+ const result = await editor.execute(command);
104
+ expect(result.success).toBe(true);
105
+
106
+ const newContent = fs.readFileSync(testFile, 'utf-8');
107
+ expect(newContent).toContain('Hi, Universe!');
108
+ expect(newContent).not.toContain('Hello, World!');
109
+ expect(newContent).toContain('Hello again!');
110
+ });
111
+
112
+ test('should fail when old string not found', async () => {
113
+ fs.writeFileSync(testFile, 'Some content');
114
+
115
+ const command: EditCommand = {
116
+ command: 'str_replace',
117
+ path: testFile,
118
+ oldStr: 'Not found',
119
+ newStr: 'Replacement'
120
+ };
121
+
122
+ const result = await editor.execute(command);
123
+ expect(result.success).toBe(false);
124
+ expect(result.error).toContain('not found');
125
+ });
126
+
127
+ test('should fail when old string appears multiple times', async () => {
128
+ const content = 'duplicate\nsome text\nduplicate';
129
+ fs.writeFileSync(testFile, content);
130
+
131
+ const command: EditCommand = {
132
+ command: 'str_replace',
133
+ path: testFile,
134
+ oldStr: 'duplicate',
135
+ newStr: 'unique'
136
+ };
137
+
138
+ const result = await editor.execute(command);
139
+ expect(result.success).toBe(false);
140
+ expect(result.error).toContain('multiple');
141
+ });
142
+ });
143
+
144
+ describe('insert command', () => {
145
+ test('should insert text at specific line', async () => {
146
+ const content = 'Line 1\nLine 2\nLine 3';
147
+ fs.writeFileSync(testFile, content);
148
+
149
+ const command: EditCommand = {
150
+ command: 'insert',
151
+ path: testFile,
152
+ lineNumber: 2,
153
+ content: 'Inserted line'
154
+ };
155
+
156
+ const result = await editor.execute(command);
157
+ expect(result.success).toBe(true);
158
+
159
+ const newContent = fs.readFileSync(testFile, 'utf-8');
160
+ const lines = newContent.split('\n');
161
+ expect(lines[1]).toBe('Inserted line');
162
+ expect(lines[2]).toBe('Line 2');
163
+ });
164
+ });
165
+
166
+ describe('undo_edit command', () => {
167
+ test('should undo last edit', async () => {
168
+ const originalContent = 'Original content';
169
+ fs.writeFileSync(testFile, originalContent);
170
+
171
+ // Make an edit
172
+ await editor.execute({
173
+ command: 'str_replace',
174
+ path: testFile,
175
+ oldStr: 'Original',
176
+ newStr: 'Modified'
177
+ });
178
+
179
+ // Verify edit was made
180
+ expect(fs.readFileSync(testFile, 'utf-8')).toContain('Modified');
181
+
182
+ // Undo the edit
183
+ const result = await editor.execute({
184
+ command: 'undo_edit',
185
+ path: testFile
186
+ });
187
+
188
+ expect(result.success).toBe(true);
189
+ expect(fs.readFileSync(testFile, 'utf-8')).toBe(originalContent);
190
+ });
191
+ });
192
+
193
+ describe('chunk localization', () => {
194
+ test('should find relevant chunks for search query', async () => {
195
+ const codeContent = `
196
+ function calculateTotal(items) {
197
+ let total = 0;
198
+ for (const item of items) {
199
+ total += item.price * item.quantity;
200
+ }
201
+ return total;
202
+ }
203
+
204
+ function applyDiscount(total, discountPercent) {
205
+ return total * (1 - discountPercent / 100);
206
+ }
207
+
208
+ function formatCurrency(amount) {
209
+ return new Intl.NumberFormat('en-US', {
210
+ style: 'currency',
211
+ currency: 'USD'
212
+ }).format(amount);
213
+ }
214
+ `;
215
+ fs.writeFileSync(testFile, codeContent);
216
+
217
+ const chunks = await editor.getRelevantChunks(testFile, 'calculate price total');
218
+ expect(chunks.length).toBeGreaterThan(0);
219
+ expect(chunks[0].content).toContain('calculateTotal');
220
+ expect(chunks[0].content).toContain('price');
221
+ });
222
+ });
223
+ });