codeep 1.2.17 → 1.2.19

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.
Files changed (64) hide show
  1. package/README.md +20 -7
  2. package/dist/api/index.d.ts +7 -0
  3. package/dist/api/index.js +21 -17
  4. package/dist/config/providers.d.ts +6 -0
  5. package/dist/config/providers.js +11 -0
  6. package/dist/renderer/App.d.ts +1 -5
  7. package/dist/renderer/App.js +106 -486
  8. package/dist/renderer/agentExecution.d.ts +36 -0
  9. package/dist/renderer/agentExecution.js +394 -0
  10. package/dist/renderer/commands.d.ts +16 -0
  11. package/dist/renderer/commands.js +838 -0
  12. package/dist/renderer/handlers.d.ts +87 -0
  13. package/dist/renderer/handlers.js +260 -0
  14. package/dist/renderer/highlight.d.ts +18 -0
  15. package/dist/renderer/highlight.js +130 -0
  16. package/dist/renderer/main.d.ts +4 -2
  17. package/dist/renderer/main.js +103 -1550
  18. package/dist/utils/agent.d.ts +5 -15
  19. package/dist/utils/agent.js +9 -693
  20. package/dist/utils/agentChat.d.ts +46 -0
  21. package/dist/utils/agentChat.js +343 -0
  22. package/dist/utils/agentStream.d.ts +23 -0
  23. package/dist/utils/agentStream.js +216 -0
  24. package/dist/utils/keychain.js +3 -2
  25. package/dist/utils/learning.js +9 -3
  26. package/dist/utils/mcpIntegration.d.ts +61 -0
  27. package/dist/utils/mcpIntegration.js +154 -0
  28. package/dist/utils/project.js +8 -3
  29. package/dist/utils/skills.js +21 -11
  30. package/dist/utils/smartContext.d.ts +4 -0
  31. package/dist/utils/smartContext.js +51 -14
  32. package/dist/utils/toolExecution.d.ts +27 -0
  33. package/dist/utils/toolExecution.js +525 -0
  34. package/dist/utils/toolParsing.d.ts +18 -0
  35. package/dist/utils/toolParsing.js +302 -0
  36. package/dist/utils/tools.d.ts +11 -24
  37. package/dist/utils/tools.js +22 -1187
  38. package/package.json +3 -1
  39. package/dist/config/config.test.d.ts +0 -1
  40. package/dist/config/config.test.js +0 -157
  41. package/dist/config/providers.test.d.ts +0 -1
  42. package/dist/config/providers.test.js +0 -187
  43. package/dist/hooks/index.d.ts +0 -4
  44. package/dist/hooks/index.js +0 -4
  45. package/dist/hooks/useAgent.d.ts +0 -29
  46. package/dist/hooks/useAgent.js +0 -148
  47. package/dist/utils/agent.test.d.ts +0 -1
  48. package/dist/utils/agent.test.js +0 -315
  49. package/dist/utils/git.test.d.ts +0 -1
  50. package/dist/utils/git.test.js +0 -193
  51. package/dist/utils/gitignore.test.d.ts +0 -1
  52. package/dist/utils/gitignore.test.js +0 -167
  53. package/dist/utils/project.test.d.ts +0 -1
  54. package/dist/utils/project.test.js +0 -212
  55. package/dist/utils/ratelimit.test.d.ts +0 -1
  56. package/dist/utils/ratelimit.test.js +0 -131
  57. package/dist/utils/retry.test.d.ts +0 -1
  58. package/dist/utils/retry.test.js +0 -163
  59. package/dist/utils/smartContext.test.d.ts +0 -1
  60. package/dist/utils/smartContext.test.js +0 -382
  61. package/dist/utils/tools.test.d.ts +0 -1
  62. package/dist/utils/tools.test.js +0 -681
  63. package/dist/utils/validation.test.d.ts +0 -1
  64. package/dist/utils/validation.test.js +0 -164
@@ -1,681 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { getOpenAITools, getAnthropicTools, parseToolCalls, parseOpenAIToolCalls, parseAnthropicToolCalls, createActionLog, AGENT_TOOLS, } from './tools.js';
3
- // ─── CONSTANTS ───────────────────────────────────────────────────────────────
4
- const ALL_TOOL_NAMES = Object.keys(AGENT_TOOLS);
5
- // MCP tools are filtered out when no API key is configured (e.g. in tests)
6
- const ZAI_MCP_TOOLS = ['web_search', 'web_read', 'github_read'];
7
- const MINIMAX_MCP_TOOLS = ['minimax_web_search', 'minimax_understand_image'];
8
- const MCP_TOOLS = [...ZAI_MCP_TOOLS, ...MINIMAX_MCP_TOOLS];
9
- const CORE_TOOL_NAMES = ALL_TOOL_NAMES.filter(n => !MCP_TOOLS.includes(n));
10
- // ─── getOpenAITools ──────────────────────────────────────────────────────────
11
- describe('getOpenAITools', () => {
12
- it('should return one entry per AGENT_TOOLS definition', () => {
13
- const tools = getOpenAITools();
14
- // MCP tools are excluded when no Z.AI API key is configured
15
- expect(tools).toHaveLength(CORE_TOOL_NAMES.length);
16
- });
17
- it('should wrap every tool in the OpenAI function-calling envelope', () => {
18
- const tools = getOpenAITools();
19
- for (const tool of tools) {
20
- expect(tool.type).toBe('function');
21
- expect(tool.function).toBeDefined();
22
- expect(typeof tool.function.name).toBe('string');
23
- expect(typeof tool.function.description).toBe('string');
24
- expect(tool.function.parameters.type).toBe('object');
25
- expect(tool.function.parameters.properties).toBeDefined();
26
- expect(Array.isArray(tool.function.parameters.required)).toBe(true);
27
- }
28
- });
29
- it('should include all core tool names from AGENT_TOOLS', () => {
30
- const tools = getOpenAITools();
31
- const names = tools.map(t => t.function.name);
32
- for (const name of CORE_TOOL_NAMES) {
33
- expect(names).toContain(name);
34
- }
35
- });
36
- it('should mark required parameters correctly for read_file', () => {
37
- const tools = getOpenAITools();
38
- const readFile = tools.find(t => t.function.name === 'read_file');
39
- expect(readFile.function.parameters.required).toEqual(['path']);
40
- expect(readFile.function.parameters.properties.path.type).toBe('string');
41
- });
42
- it('should mark required parameters correctly for write_file', () => {
43
- const tools = getOpenAITools();
44
- const writeFile = tools.find(t => t.function.name === 'write_file');
45
- expect(writeFile.function.parameters.required).toContain('path');
46
- expect(writeFile.function.parameters.required).toContain('content');
47
- });
48
- it('should mark required parameters correctly for edit_file', () => {
49
- const tools = getOpenAITools();
50
- const editFile = tools.find(t => t.function.name === 'edit_file');
51
- expect(editFile.function.parameters.required).toContain('path');
52
- expect(editFile.function.parameters.required).toContain('old_text');
53
- expect(editFile.function.parameters.required).toContain('new_text');
54
- });
55
- it('should distinguish required from optional parameters for list_files', () => {
56
- const tools = getOpenAITools();
57
- const listFiles = tools.find(t => t.function.name === 'list_files');
58
- expect(listFiles.function.parameters.required).toContain('path');
59
- expect(listFiles.function.parameters.required).not.toContain('recursive');
60
- expect(listFiles.function.parameters.properties.recursive).toBeDefined();
61
- });
62
- it('should represent array-typed parameters with items', () => {
63
- const tools = getOpenAITools();
64
- const execCmd = tools.find(t => t.function.name === 'execute_command');
65
- const argsParam = execCmd.function.parameters.properties.args;
66
- expect(argsParam.type).toBe('array');
67
- expect(argsParam.items).toEqual({ type: 'string' });
68
- });
69
- it('should not include optional parameters in required array for execute_command', () => {
70
- const tools = getOpenAITools();
71
- const execCmd = tools.find(t => t.function.name === 'execute_command');
72
- expect(execCmd.function.parameters.required).toContain('command');
73
- expect(execCmd.function.parameters.required).not.toContain('args');
74
- });
75
- });
76
- // ─── getAnthropicTools ───────────────────────────────────────────────────────
77
- describe('getAnthropicTools', () => {
78
- it('should return one entry per AGENT_TOOLS definition', () => {
79
- const tools = getAnthropicTools();
80
- // MCP tools are excluded when no Z.AI API key is configured
81
- expect(tools).toHaveLength(CORE_TOOL_NAMES.length);
82
- });
83
- it('should use Anthropic tool-use shape (name, description, input_schema)', () => {
84
- const tools = getAnthropicTools();
85
- for (const tool of tools) {
86
- expect(typeof tool.name).toBe('string');
87
- expect(typeof tool.description).toBe('string');
88
- expect(tool.input_schema).toBeDefined();
89
- expect(tool.input_schema.type).toBe('object');
90
- expect(tool.input_schema.properties).toBeDefined();
91
- expect(Array.isArray(tool.input_schema.required)).toBe(true);
92
- }
93
- });
94
- it('should include all core tool names from AGENT_TOOLS', () => {
95
- const tools = getAnthropicTools();
96
- const names = tools.map(t => t.name);
97
- for (const name of CORE_TOOL_NAMES) {
98
- expect(names).toContain(name);
99
- }
100
- });
101
- it('should have same required params as OpenAI format', () => {
102
- const openai = getOpenAITools();
103
- const anthropic = getAnthropicTools();
104
- for (const name of CORE_TOOL_NAMES) {
105
- const oTool = openai.find(t => t.function.name === name);
106
- const aTool = anthropic.find(t => t.name === name);
107
- expect(aTool.input_schema.required).toEqual(oTool.function.parameters.required);
108
- }
109
- });
110
- it('should represent array parameters with items for execute_command', () => {
111
- const tools = getAnthropicTools();
112
- const execCmd = tools.find(t => t.name === 'execute_command');
113
- const argsParam = execCmd.input_schema.properties.args;
114
- expect(argsParam.type).toBe('array');
115
- expect(argsParam.items).toEqual({ type: 'string' });
116
- });
117
- it('should not wrap tools in a function envelope (unlike OpenAI)', () => {
118
- const tools = getAnthropicTools();
119
- for (const tool of tools) {
120
- expect(tool.type).toBeUndefined();
121
- expect(tool.function).toBeUndefined();
122
- }
123
- });
124
- });
125
- // ─── parseToolCalls ──────────────────────────────────────────────────────────
126
- describe('parseToolCalls', () => {
127
- it('should return empty array for response with no tool calls', () => {
128
- expect(parseToolCalls('No tools here, just some text.')).toEqual([]);
129
- });
130
- it('should return empty array for empty string', () => {
131
- expect(parseToolCalls('')).toEqual([]);
132
- });
133
- // ── XML <tool_call> format ──
134
- it('should parse <tool_call> XML tags with valid JSON', () => {
135
- const response = `
136
- Let me read the file.
137
- <tool_call>{"tool": "read_file", "parameters": {"path": "src/index.ts"}}</tool_call>
138
- `;
139
- const calls = parseToolCalls(response);
140
- expect(calls).toHaveLength(1);
141
- expect(calls[0].tool).toBe('read_file');
142
- expect(calls[0].parameters).toEqual({ path: 'src/index.ts' });
143
- });
144
- it('should parse <toolcall> XML tags (no underscore)', () => {
145
- const response = `
146
- <toolcall>{"tool": "read_file", "parameters": {"path": "README.md"}}</toolcall>
147
- `;
148
- const calls = parseToolCalls(response);
149
- expect(calls).toHaveLength(1);
150
- expect(calls[0].tool).toBe('read_file');
151
- });
152
- it('should parse multiple <tool_call> tags', () => {
153
- const response = `
154
- <tool_call>{"tool": "read_file", "parameters": {"path": "a.ts"}}</tool_call>
155
- <tool_call>{"tool": "read_file", "parameters": {"path": "b.ts"}}</tool_call>
156
- `;
157
- const calls = parseToolCalls(response);
158
- expect(calls).toHaveLength(2);
159
- });
160
- it('should normalize tool names in XML format', () => {
161
- const response = `
162
- <tool_call>{"tool": "readFile", "parameters": {"path": "a.ts"}}</tool_call>
163
- `;
164
- const calls = parseToolCalls(response);
165
- expect(calls).toHaveLength(1);
166
- expect(calls[0].tool).toBe('read_file');
167
- });
168
- // ── Code block format ──
169
- it('should parse ```tool code blocks', () => {
170
- const response = [
171
- 'I will read the file.',
172
- '```tool',
173
- '{"tool": "read_file", "parameters": {"path": "package.json"}}',
174
- '```',
175
- ].join('\n');
176
- const calls = parseToolCalls(response);
177
- expect(calls).toHaveLength(1);
178
- expect(calls[0].tool).toBe('read_file');
179
- expect(calls[0].parameters).toEqual({ path: 'package.json' });
180
- });
181
- it('should parse ```json code blocks with tool property', () => {
182
- const response = [
183
- '```json',
184
- '{"tool": "list_files", "parameters": {"path": ".", "recursive": true}}',
185
- '```',
186
- ].join('\n');
187
- const calls = parseToolCalls(response);
188
- expect(calls).toHaveLength(1);
189
- expect(calls[0].tool).toBe('list_files');
190
- expect(calls[0].parameters).toEqual({ path: '.', recursive: true });
191
- });
192
- it('should ignore code blocks that do not contain tool JSON', () => {
193
- const response = [
194
- '```json',
195
- '{"name": "my-project", "version": "1.0.0"}',
196
- '```',
197
- ].join('\n');
198
- const calls = parseToolCalls(response);
199
- expect(calls).toHaveLength(0);
200
- });
201
- // ── Deduplication ──
202
- it('should deduplicate identical tool calls from different formats', () => {
203
- // A tool call that could match both XML and code block parsers
204
- const response = [
205
- '<tool_call>{"tool": "read_file", "parameters": {"path": "dup.ts"}}</tool_call>',
206
- '```tool',
207
- '{"tool": "read_file", "parameters": {"path": "dup.ts"}}',
208
- '```',
209
- ].join('\n');
210
- const calls = parseToolCalls(response);
211
- // The XML match is found first; code block dedup should prevent the second
212
- const readDupCalls = calls.filter(c => c.tool === 'read_file' && c.parameters.path === 'dup.ts');
213
- expect(readDupCalls).toHaveLength(1);
214
- });
215
- // ── Malformed JSON handling ──
216
- it('should handle trailing commas in JSON', () => {
217
- const response = `
218
- <tool_call>{"tool": "read_file", "parameters": {"path": "test.ts",}}</tool_call>
219
- `;
220
- const calls = parseToolCalls(response);
221
- expect(calls).toHaveLength(1);
222
- expect(calls[0].tool).toBe('read_file');
223
- });
224
- it('should extract tool info from partially malformed JSON', () => {
225
- // tryParseToolCall's fallback: extracts "tool" and string params manually
226
- const response = `
227
- <tool_call>{"tool": "search_code", "parameters: {"pattern": "TODO"}}</tool_call>
228
- `;
229
- const calls = parseToolCalls(response);
230
- // Even with malformed JSON, the fallback extractor should find tool + pattern
231
- expect(calls).toHaveLength(1);
232
- expect(calls[0].tool).toBe('search_code');
233
- });
234
- // ── Inline JSON fallback ──
235
- it('should use inline JSON fallback when no other format matches', () => {
236
- const response = `
237
- Here is my action: {"tool": "read_file", "parameters": {"path": "hello.ts"}}
238
- `;
239
- const calls = parseToolCalls(response);
240
- expect(calls).toHaveLength(1);
241
- expect(calls[0].tool).toBe('read_file');
242
- expect(calls[0].parameters).toEqual({ path: 'hello.ts' });
243
- });
244
- // ── Malformed <toolcall>toolname{...} format ──
245
- it('should parse malformed <toolcall>toolname{...} format', () => {
246
- const response = `
247
- <toolcall>readfile{"path": "src/main.ts"}</toolcall>
248
- `;
249
- const calls = parseToolCalls(response);
250
- expect(calls).toHaveLength(1);
251
- expect(calls[0].tool).toBe('read_file');
252
- expect(calls[0].parameters).toEqual({ path: 'src/main.ts' });
253
- });
254
- it('should parse <toolcall>executecommand with parameters key', () => {
255
- const response = `
256
- <toolcall>executecommand, "parameters": {"command": "npm", "args": ["test"]}</toolcall>
257
- `;
258
- const calls = parseToolCalls(response);
259
- const execCalls = calls.filter(c => c.tool === 'execute_command');
260
- expect(execCalls.length).toBeGreaterThanOrEqual(1);
261
- expect(execCalls[0].parameters).toHaveProperty('command', 'npm');
262
- });
263
- });
264
- // ─── parseOpenAIToolCalls ────────────────────────────────────────────────────
265
- describe('parseOpenAIToolCalls', () => {
266
- it('should return empty array for null input', () => {
267
- expect(parseOpenAIToolCalls(null)).toEqual([]);
268
- });
269
- it('should return empty array for undefined input', () => {
270
- expect(parseOpenAIToolCalls(undefined)).toEqual([]);
271
- });
272
- it('should return empty array for empty array', () => {
273
- expect(parseOpenAIToolCalls([])).toEqual([]);
274
- });
275
- it('should parse a single valid tool call', () => {
276
- const toolCalls = [
277
- {
278
- id: 'call_1',
279
- function: {
280
- name: 'read_file',
281
- arguments: '{"path": "src/index.ts"}',
282
- },
283
- },
284
- ];
285
- const result = parseOpenAIToolCalls(toolCalls);
286
- expect(result).toHaveLength(1);
287
- expect(result[0].tool).toBe('read_file');
288
- expect(result[0].parameters).toEqual({ path: 'src/index.ts' });
289
- expect(result[0].id).toBe('call_1');
290
- });
291
- it('should parse multiple tool calls', () => {
292
- const toolCalls = [
293
- {
294
- id: 'call_1',
295
- function: { name: 'read_file', arguments: '{"path": "a.ts"}' },
296
- },
297
- {
298
- id: 'call_2',
299
- function: { name: 'write_file', arguments: '{"path": "b.ts", "content": "hello"}' },
300
- },
301
- ];
302
- const result = parseOpenAIToolCalls(toolCalls);
303
- expect(result).toHaveLength(2);
304
- expect(result[0].tool).toBe('read_file');
305
- expect(result[1].tool).toBe('write_file');
306
- expect(result[1].parameters).toEqual({ path: 'b.ts', content: 'hello' });
307
- });
308
- // ── Name normalization ──
309
- it('should normalize camelCase tool names to snake_case', () => {
310
- const toolCalls = [
311
- { id: 'c1', function: { name: 'readFile', arguments: '{"path": "x.ts"}' } },
312
- ];
313
- const result = parseOpenAIToolCalls(toolCalls);
314
- expect(result[0].tool).toBe('read_file');
315
- });
316
- it('should normalize executecommand (no separator) to execute_command', () => {
317
- const toolCalls = [
318
- { id: 'c1', function: { name: 'executecommand', arguments: '{"command": "ls"}' } },
319
- ];
320
- const result = parseOpenAIToolCalls(toolCalls);
321
- expect(result[0].tool).toBe('execute_command');
322
- });
323
- it('should normalize names with hyphens to underscores', () => {
324
- const toolCalls = [
325
- { id: 'c1', function: { name: 'read-file', arguments: '{"path": "a.ts"}' } },
326
- ];
327
- const result = parseOpenAIToolCalls(toolCalls);
328
- expect(result[0].tool).toBe('read_file');
329
- });
330
- it('should normalize all known tool name variants', () => {
331
- // Each variant needs arguments that pass its specific validation checks
332
- const variants = [
333
- ['readfile', 'read_file', '{"path": "."}'],
334
- ['writefile', 'write_file', '{"path": "a.ts", "content": "x"}'],
335
- ['editfile', 'edit_file', '{"path": "a.ts", "old_text": "a", "new_text": "b"}'],
336
- ['deletefile', 'delete_file', '{"path": "."}'],
337
- ['listfiles', 'list_files', '{"path": "."}'],
338
- ['searchcode', 'search_code', '{"pattern": "x"}'],
339
- ['createdirectory', 'create_directory', '{"path": "dir"}'],
340
- ['findfiles', 'find_files', '{"pattern": "*.ts"}'],
341
- ['fetchurl', 'fetch_url', '{"url": "https://example.com"}'],
342
- ];
343
- for (const [input, expected, args] of variants) {
344
- const result = parseOpenAIToolCalls([
345
- { id: 'x', function: { name: input, arguments: args } },
346
- ]);
347
- expect(result[0].tool).toBe(expected);
348
- }
349
- });
350
- // ── Validation: skip calls missing required params ──
351
- it('should skip read_file calls without a path', () => {
352
- const toolCalls = [
353
- { id: 'c1', function: { name: 'read_file', arguments: '{}' } },
354
- ];
355
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
356
- });
357
- it('should skip write_file calls without a path', () => {
358
- const toolCalls = [
359
- { id: 'c1', function: { name: 'write_file', arguments: '{"content": "hello"}' } },
360
- ];
361
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
362
- });
363
- it('should skip edit_file calls missing old_text or new_text', () => {
364
- const toolCalls = [
365
- { id: 'c1', function: { name: 'edit_file', arguments: '{"path": "a.ts"}' } },
366
- ];
367
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
368
- });
369
- it('should allow edit_file with empty string old_text and new_text', () => {
370
- const toolCalls = [
371
- {
372
- id: 'c1',
373
- function: {
374
- name: 'edit_file',
375
- arguments: '{"path": "a.ts", "old_text": "", "new_text": "new"}',
376
- },
377
- },
378
- ];
379
- const result = parseOpenAIToolCalls(toolCalls);
380
- expect(result).toHaveLength(1);
381
- expect(result[0].parameters).toEqual({ path: 'a.ts', old_text: '', new_text: 'new' });
382
- });
383
- // ── Malformed JSON arguments ──
384
- it('should skip calls with completely unparseable arguments and no extractable params', () => {
385
- const toolCalls = [
386
- { id: 'c1', function: { name: 'search_code', arguments: 'not json at all' } },
387
- ];
388
- // search_code doesn't have a partial extraction path, so it should be skipped
389
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
390
- });
391
- it('should skip entries with empty function name', () => {
392
- const toolCalls = [
393
- { id: 'c1', function: { name: '', arguments: '{"path": "a.ts"}' } },
394
- ];
395
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
396
- });
397
- it('should handle missing function property gracefully', () => {
398
- const toolCalls = [{ id: 'c1' }];
399
- // normalizeToolName('') returns '', which is falsy, so it gets skipped
400
- expect(parseOpenAIToolCalls(toolCalls)).toHaveLength(0);
401
- });
402
- it('should attempt partial extraction for truncated write_file JSON', () => {
403
- // Simulate a truncated response where JSON.parse fails
404
- const truncatedArgs = '{"path": "src/app.ts", "content": "const x = 1;\\nconst y = 2;';
405
- const toolCalls = [
406
- { id: 'c1', function: { name: 'write_file', arguments: truncatedArgs } },
407
- ];
408
- const result = parseOpenAIToolCalls(toolCalls);
409
- expect(result).toHaveLength(1);
410
- expect(result[0].tool).toBe('write_file');
411
- expect(result[0].parameters).toHaveProperty('path', 'src/app.ts');
412
- // Content should be extracted even if truncated
413
- expect(result[0].parameters).toHaveProperty('content');
414
- });
415
- it('should attempt partial extraction for truncated read_file JSON', () => {
416
- const truncatedArgs = '{"path": "src/utils.ts"';
417
- const toolCalls = [
418
- { id: 'c1', function: { name: 'read_file', arguments: truncatedArgs } },
419
- ];
420
- const result = parseOpenAIToolCalls(toolCalls);
421
- expect(result).toHaveLength(1);
422
- expect(result[0].tool).toBe('read_file');
423
- expect(result[0].parameters).toEqual({ path: 'src/utils.ts' });
424
- });
425
- it('should attempt partial extraction for truncated execute_command JSON', () => {
426
- const truncatedArgs = '{"command": "npm", "args": ["install", "lodash"]}extra';
427
- // This will fail JSON.parse because of trailing chars, but extractPartialToolParams handles it
428
- const toolCalls = [
429
- { id: 'c1', function: { name: 'execute_command', arguments: truncatedArgs } },
430
- ];
431
- const result = parseOpenAIToolCalls(toolCalls);
432
- // It should extract command and args from the partial
433
- expect(result).toHaveLength(1);
434
- expect(result[0].tool).toBe('execute_command');
435
- expect(result[0].parameters).toHaveProperty('command', 'npm');
436
- });
437
- });
438
- // ─── parseAnthropicToolCalls ─────────────────────────────────────────────────
439
- describe('parseAnthropicToolCalls', () => {
440
- it('should return empty array for null input', () => {
441
- expect(parseAnthropicToolCalls(null)).toEqual([]);
442
- });
443
- it('should return empty array for undefined input', () => {
444
- expect(parseAnthropicToolCalls(undefined)).toEqual([]);
445
- });
446
- it('should return empty array for empty array', () => {
447
- expect(parseAnthropicToolCalls([])).toEqual([]);
448
- });
449
- it('should parse a single tool_use block', () => {
450
- const content = [
451
- {
452
- type: 'tool_use',
453
- id: 'toolu_abc',
454
- name: 'read_file',
455
- input: { path: 'src/index.ts' },
456
- },
457
- ];
458
- const result = parseAnthropicToolCalls(content);
459
- expect(result).toHaveLength(1);
460
- expect(result[0].tool).toBe('read_file');
461
- expect(result[0].parameters).toEqual({ path: 'src/index.ts' });
462
- expect(result[0].id).toBe('toolu_abc');
463
- });
464
- it('should parse multiple tool_use blocks', () => {
465
- const content = [
466
- { type: 'tool_use', id: 'toolu_1', name: 'read_file', input: { path: 'a.ts' } },
467
- { type: 'tool_use', id: 'toolu_2', name: 'list_files', input: { path: '.' } },
468
- ];
469
- const result = parseAnthropicToolCalls(content);
470
- expect(result).toHaveLength(2);
471
- expect(result[0].tool).toBe('read_file');
472
- expect(result[1].tool).toBe('list_files');
473
- });
474
- it('should filter out non-tool_use blocks', () => {
475
- const content = [
476
- { type: 'text', text: 'Let me read the file.' },
477
- { type: 'tool_use', id: 'toolu_1', name: 'read_file', input: { path: 'a.ts' } },
478
- { type: 'text', text: 'Done.' },
479
- ];
480
- const result = parseAnthropicToolCalls(content);
481
- expect(result).toHaveLength(1);
482
- expect(result[0].tool).toBe('read_file');
483
- });
484
- it('should handle content with only text blocks', () => {
485
- const content = [
486
- { type: 'text', text: 'I cannot do that.' },
487
- ];
488
- expect(parseAnthropicToolCalls(content)).toEqual([]);
489
- });
490
- it('should normalize tool names', () => {
491
- const content = [
492
- { type: 'tool_use', id: 'toolu_1', name: 'readFile', input: { path: 'a.ts' } },
493
- ];
494
- const result = parseAnthropicToolCalls(content);
495
- expect(result[0].tool).toBe('read_file');
496
- });
497
- it('should use empty object for missing input', () => {
498
- const content = [
499
- { type: 'tool_use', id: 'toolu_1', name: 'list_files' },
500
- ];
501
- const result = parseAnthropicToolCalls(content);
502
- expect(result).toHaveLength(1);
503
- expect(result[0].parameters).toEqual({});
504
- });
505
- it('should skip blocks with empty name after normalization', () => {
506
- const content = [
507
- { type: 'tool_use', id: 'toolu_1', name: '', input: {} },
508
- ];
509
- const result = parseAnthropicToolCalls(content);
510
- expect(result).toHaveLength(0);
511
- });
512
- it('should skip blocks with no name property', () => {
513
- const content = [
514
- { type: 'tool_use', id: 'toolu_1', input: { path: 'a.ts' } },
515
- ];
516
- const result = parseAnthropicToolCalls(content);
517
- expect(result).toHaveLength(0);
518
- });
519
- });
520
- // ─── createActionLog ─────────────────────────────────────────────────────────
521
- describe('createActionLog', () => {
522
- // Helper to build a minimal ToolResult
523
- function makeResult(overrides = {}) {
524
- return {
525
- success: true,
526
- output: 'OK',
527
- tool: 'read_file',
528
- parameters: {},
529
- ...overrides,
530
- };
531
- }
532
- // ── Type mapping ──
533
- it('should map read_file to type "read"', () => {
534
- const tc = { tool: 'read_file', parameters: { path: 'a.ts' } };
535
- const log = createActionLog(tc, makeResult());
536
- expect(log.type).toBe('read');
537
- });
538
- it('should map write_file to type "write"', () => {
539
- const tc = { tool: 'write_file', parameters: { path: 'b.ts', content: 'x' } };
540
- const log = createActionLog(tc, makeResult({ tool: 'write_file' }));
541
- expect(log.type).toBe('write');
542
- });
543
- it('should map edit_file to type "edit"', () => {
544
- const tc = { tool: 'edit_file', parameters: { path: 'c.ts', old_text: 'a', new_text: 'b' } };
545
- const log = createActionLog(tc, makeResult({ tool: 'edit_file' }));
546
- expect(log.type).toBe('edit');
547
- });
548
- it('should map delete_file to type "delete"', () => {
549
- const tc = { tool: 'delete_file', parameters: { path: 'd.ts' } };
550
- const log = createActionLog(tc, makeResult({ tool: 'delete_file' }));
551
- expect(log.type).toBe('delete');
552
- });
553
- it('should map execute_command to type "command"', () => {
554
- const tc = { tool: 'execute_command', parameters: { command: 'npm test' } };
555
- const log = createActionLog(tc, makeResult({ tool: 'execute_command' }));
556
- expect(log.type).toBe('command');
557
- });
558
- it('should map search_code to type "search"', () => {
559
- const tc = { tool: 'search_code', parameters: { pattern: 'TODO' } };
560
- const log = createActionLog(tc, makeResult({ tool: 'search_code' }));
561
- expect(log.type).toBe('search');
562
- });
563
- it('should map list_files to type "list"', () => {
564
- const tc = { tool: 'list_files', parameters: { path: '.' } };
565
- const log = createActionLog(tc, makeResult({ tool: 'list_files' }));
566
- expect(log.type).toBe('list');
567
- });
568
- it('should map create_directory to type "mkdir"', () => {
569
- const tc = { tool: 'create_directory', parameters: { path: 'new-dir' } };
570
- const log = createActionLog(tc, makeResult({ tool: 'create_directory' }));
571
- expect(log.type).toBe('mkdir');
572
- });
573
- it('should map find_files to type "search"', () => {
574
- const tc = { tool: 'find_files', parameters: { pattern: '*.ts' } };
575
- const log = createActionLog(tc, makeResult({ tool: 'find_files' }));
576
- expect(log.type).toBe('search');
577
- });
578
- it('should map fetch_url to type "fetch"', () => {
579
- const tc = { tool: 'fetch_url', parameters: { url: 'https://example.com' } };
580
- const log = createActionLog(tc, makeResult({ tool: 'fetch_url' }));
581
- expect(log.type).toBe('fetch');
582
- });
583
- it('should map minimax_web_search to type "fetch"', () => {
584
- const tc = { tool: 'minimax_web_search', parameters: { query: 'test' } };
585
- const log = createActionLog(tc, makeResult({ tool: 'minimax_web_search' }));
586
- expect(log.type).toBe('fetch');
587
- });
588
- it('should map minimax_understand_image to type "fetch"', () => {
589
- const tc = { tool: 'minimax_understand_image', parameters: { prompt: 'describe', image_url: 'https://example.com/img.png' } };
590
- const log = createActionLog(tc, makeResult({ tool: 'minimax_understand_image' }));
591
- expect(log.type).toBe('fetch');
592
- });
593
- it('should default to type "command" for unknown tools', () => {
594
- const tc = { tool: 'unknown_tool', parameters: {} };
595
- const log = createActionLog(tc, makeResult({ tool: 'unknown_tool' }));
596
- expect(log.type).toBe('command');
597
- });
598
- it('should normalize tool name variants for type mapping', () => {
599
- const tc = { tool: 'readFile', parameters: { path: 'x.ts' } };
600
- const log = createActionLog(tc, makeResult());
601
- expect(log.type).toBe('read');
602
- });
603
- // ── Target extraction ──
604
- it('should extract path as target', () => {
605
- const tc = { tool: 'read_file', parameters: { path: 'src/index.ts' } };
606
- const log = createActionLog(tc, makeResult());
607
- expect(log.target).toBe('src/index.ts');
608
- });
609
- it('should extract command as target when no path', () => {
610
- const tc = { tool: 'execute_command', parameters: { command: 'npm test' } };
611
- const log = createActionLog(tc, makeResult({ tool: 'execute_command' }));
612
- expect(log.target).toBe('npm test');
613
- });
614
- it('should extract pattern as target when no path or command', () => {
615
- const tc = { tool: 'search_code', parameters: { pattern: 'TODO' } };
616
- const log = createActionLog(tc, makeResult({ tool: 'search_code' }));
617
- expect(log.target).toBe('TODO');
618
- });
619
- it('should extract url as target for fetch_url', () => {
620
- const tc = { tool: 'fetch_url', parameters: { url: 'https://docs.example.com' } };
621
- const log = createActionLog(tc, makeResult({ tool: 'fetch_url' }));
622
- expect(log.target).toBe('https://docs.example.com');
623
- });
624
- it('should use "unknown" when no recognized target parameter exists', () => {
625
- const tc = { tool: 'read_file', parameters: {} };
626
- const log = createActionLog(tc, makeResult());
627
- expect(log.target).toBe('unknown');
628
- });
629
- // ── Result status ──
630
- it('should set result to "success" for successful tool results', () => {
631
- const tc = { tool: 'read_file', parameters: { path: 'a.ts' } };
632
- const log = createActionLog(tc, makeResult({ success: true }));
633
- expect(log.result).toBe('success');
634
- });
635
- it('should set result to "error" for failed tool results', () => {
636
- const tc = { tool: 'read_file', parameters: { path: 'missing.ts' } };
637
- const log = createActionLog(tc, makeResult({ success: false, error: 'File not found' }));
638
- expect(log.result).toBe('error');
639
- });
640
- // ── Details / truncation ──
641
- it('should include full content in details for write_file on success', () => {
642
- const content = 'const x = 1;\nconst y = 2;\n';
643
- const tc = { tool: 'write_file', parameters: { path: 'a.ts', content } };
644
- const log = createActionLog(tc, makeResult({ tool: 'write_file' }));
645
- expect(log.details).toBe(content);
646
- });
647
- it('should include full new_text in details for edit_file on success', () => {
648
- const newText = 'const updated = true;';
649
- const tc = { tool: 'edit_file', parameters: { path: 'a.ts', old_text: 'old', new_text: newText } };
650
- const log = createActionLog(tc, makeResult({ tool: 'edit_file' }));
651
- expect(log.details).toBe(newText);
652
- });
653
- it('should truncate command output to 1000 chars for execute_command', () => {
654
- const longOutput = 'x'.repeat(2000);
655
- const tc = { tool: 'execute_command', parameters: { command: 'cat big.log' } };
656
- const log = createActionLog(tc, makeResult({ tool: 'execute_command', output: longOutput }));
657
- expect(log.details).toHaveLength(1000);
658
- expect(log.details).toBe(longOutput.slice(0, 1000));
659
- });
660
- it('should truncate general output to 500 chars for other tools', () => {
661
- const longOutput = 'y'.repeat(1000);
662
- const tc = { tool: 'read_file', parameters: { path: 'a.ts' } };
663
- const log = createActionLog(tc, makeResult({ output: longOutput }));
664
- expect(log.details).toHaveLength(500);
665
- expect(log.details).toBe(longOutput.slice(0, 500));
666
- });
667
- it('should use error message as details when result is failure', () => {
668
- const tc = { tool: 'read_file', parameters: { path: 'missing.ts' } };
669
- const log = createActionLog(tc, makeResult({ success: false, error: 'File not found: missing.ts' }));
670
- expect(log.details).toBe('File not found: missing.ts');
671
- });
672
- // ── Timestamp ──
673
- it('should include a timestamp close to now', () => {
674
- const before = Date.now();
675
- const tc = { tool: 'read_file', parameters: { path: 'a.ts' } };
676
- const log = createActionLog(tc, makeResult());
677
- const after = Date.now();
678
- expect(log.timestamp).toBeGreaterThanOrEqual(before);
679
- expect(log.timestamp).toBeLessThanOrEqual(after);
680
- });
681
- });