From ecf4fa82ee386e2fb7ae11ddafb46932d4f3321b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=B5=B7?= <7836246@qq.com> Date: Wed, 11 Mar 2026 09:55:15 +0800 Subject: [PATCH] { "message": "fix: stop renaming file_path to path implicitly, and remove compression tests" } --- src/tool-fixer.ts | 7 +- test/compression-test.ts | 186 ------------------------- test/integration-compress-test.ts | 221 ------------------------------ test/unit-tool-fixer.mjs | 41 +++--- 4 files changed, 22 insertions(+), 433 deletions(-) delete mode 100644 test/compression-test.ts delete mode 100644 test/integration-compress-test.ts diff --git a/src/tool-fixer.ts b/src/tool-fixer.ts index df399d0..fea2aeb 100644 --- a/src/tool-fixer.ts +++ b/src/tool-fixer.ts @@ -25,10 +25,9 @@ const SMART_SINGLE_QUOTES = new Set([ export function normalizeToolArguments(args: Record): Record { if (!args || typeof args !== 'object') return args; - if ('file_path' in args && !('path' in args)) { - args.path = args.file_path; - delete args.file_path; - } + // Removed legacy mapping that forcefully converted 'file_path' to 'path'. + // Claude Code 2.1.71 tools like 'Read' legitimately require 'file_path' as per their schema, + // and this legacy mapping causes infinite loop failures. return args; } diff --git a/test/compression-test.ts b/test/compression-test.ts deleted file mode 100644 index 95be57d..0000000 --- a/test/compression-test.ts +++ /dev/null @@ -1,186 +0,0 @@ -/** - * 快速测试:上下文压缩 + tolerantParse 增强 - */ - -// ==================== 1. tolerantParse 测试 ==================== - -// 内联一个简化版 tolerantParse 进行测试 -function tolerantParse(jsonStr: string): any { - try { return JSON.parse(jsonStr); } catch {} - - let inString = false, escaped = false, fixed = ''; - const bracketStack: string[] = []; - for (let i = 0; i < jsonStr.length; i++) { - const char = jsonStr[i]; - if (char === '\\' && !escaped) { escaped = true; fixed += char; } - else if (char === '"' && !escaped) { inString = !inString; fixed += char; escaped = false; } - else { - if (inString) { - if (char === '\n') fixed += '\\n'; - else if (char === '\r') fixed += '\\r'; - else if (char === '\t') fixed += '\\t'; - else fixed += char; - } else { - if (char === '{' || char === '[') bracketStack.push(char === '{' ? '}' : ']'); - else if (char === '}' || char === ']') { if (bracketStack.length > 0) bracketStack.pop(); } - fixed += char; - } - escaped = false; - } - } - if (inString) fixed += '"'; - while (bracketStack.length > 0) fixed += bracketStack.pop(); - fixed = fixed.replace(/,\s*([}\]])/g, '$1'); - - try { return JSON.parse(fixed); } catch (_e2) { - const lastBrace = fixed.lastIndexOf('}'); - if (lastBrace > 0) { try { return JSON.parse(fixed.substring(0, lastBrace + 1)); } catch {} } - - // 第四层:正则兜底 - try { - const toolMatch = jsonStr.match(/"(?:tool|name)"\s*:\s*"([^"]+)"/); - if (toolMatch) { - const toolName = toolMatch[1]; - const paramsMatch = jsonStr.match(/"(?:parameters|arguments|input)"\s*:\s*(\{[\s\S]*)/); - let params: Record = {}; - if (paramsMatch) { - const paramsStr = paramsMatch[1]; - let depth = 0, end = -1, pInString = false, pEscaped = false; - for (let i = 0; i < paramsStr.length; i++) { - const c = paramsStr[i]; - if (c === '\\' && !pEscaped) { pEscaped = true; continue; } - if (c === '"' && !pEscaped) { pInString = !pInString; } - if (!pInString) { - if (c === '{') depth++; - if (c === '}') { depth--; if (depth === 0) { end = i; break; } } - } - pEscaped = false; - } - if (end > 0) { - const rawParams = paramsStr.substring(0, end + 1); - try { params = JSON.parse(rawParams); } catch { - const fieldRegex = /"([^"]+)"\s*:\s*"((?:[^"\\]|\\.)*)"/g; - let fm; - while ((fm = fieldRegex.exec(rawParams)) !== null) { - params[fm[1]] = fm[2].replace(/\\n/g, '\n').replace(/\\t/g, '\t'); - } - } - } - } - return { tool: toolName, parameters: params }; - } - } catch {} - throw _e2; - } -} - -// ==================== 2. compressMessage 测试 ==================== -const COMPRESS_CONTENT_MAX = 200; - -function compressMessage(role: string, text: string): string { - if (text.length <= COMPRESS_CONTENT_MAX) return text; - if (role === 'user') { - const actionMatch = text.match(/^Action output:\n([\s\S]*?)(?:\n\nBased on the output above|$)/); - if (actionMatch) { - const output = actionMatch[1]; - const firstLine = output.split('\n')[0]?.trim() || ''; - const lineCount = output.split('\n').length; - return `Action output: [${output.length} chars, ${lineCount} lines] ${firstLine.substring(0, 80)}...`; - } - const errorMatch = text.match(/^The action encountered an error:\n([\s\S]*?)(?:\n\nBased on the output above|$)/); - if (errorMatch) { - return `Action error: ${errorMatch[1].substring(0, 150)}...`; - } - return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars total]`; - } - if (role === 'assistant') { - const toolBlocks = text.match(/```json action\s*\n([\s\S]*?)```/g); - if (toolBlocks && toolBlocks.length > 0) { - const summaries: string[] = []; - for (const block of toolBlocks) { - try { - const jsonMatch = block.match(/```json action\s*\n([\s\S]*?)```/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[1]); - const toolName = parsed.tool || parsed.name || 'unknown'; - const paramKeys = parsed.parameters ? Object.keys(parsed.parameters) : []; - summaries.push(`[Called ${toolName}(${paramKeys.join(', ')})]`); - } - } catch { summaries.push('[Called action]'); } - } - const cleanText = text.replace(/```json action\s*\n[\s\S]*?```/g, '').trim(); - const briefText = cleanText.length > 100 ? cleanText.substring(0, 100) + '...' : cleanText; - return (briefText ? briefText + '\n' : '') + summaries.join('\n'); - } - return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars]`; - } - return text.substring(0, COMPRESS_CONTENT_MAX) + '...'; -} - -// ==================== 运行测试 ==================== - -let passed = 0, failed = 0; -function assert(name: string, condition: boolean, detail?: string) { - if (condition) { passed++; console.log(` ✅ ${name}`); } - else { failed++; console.log(` ❌ ${name}${detail ? ': ' + detail : ''}`); } -} - -console.log('\n=== tolerantParse 测试 ==='); - -// 正常 JSON -const t1 = tolerantParse('{"tool":"read_file","parameters":{"file_path":"src/index.ts"}}'); -assert('正常 JSON', t1.tool === 'read_file' && t1.parameters.file_path === 'src/index.ts'); - -// 带裸换行符 -const t2 = tolerantParse('{"tool":"write_file","parameters":{"content":"line1\nline2"}}'); -assert('裸换行修复', t2.tool === 'write_file'); - -// 截断 JSON(未闭合) -const t3 = tolerantParse('{"tool":"bash","parameters":{"command":"ls -la'); -assert('截断兜底', t3.tool === 'bash'); - -// 含未转义引号的代码内容(最重要的场景) -const badJson = `{ - "tool": "write_file", - "parameters": { - "file_path": "test.ts", - "content": "const x = "hello"; console.log(x);" - } -}`; -const t4 = tolerantParse(badJson); -assert('未转义引号 - 提取 tool 名', t4.tool === 'write_file'); -assert('未转义引号 - 提取参数', Object.keys(t4.parameters).length > 0, `keys=${JSON.stringify(Object.keys(t4.parameters))}`); - -// 尾部逗号 -const t5 = tolerantParse('{"tool":"list_dir","parameters":{"path":"./",},}'); -assert('尾部逗号修复', t5.tool === 'list_dir'); - -console.log('\n=== compressMessage 测试 ==='); - -// 短消息不压缩 -assert('短消息保留', compressMessage('user', 'hello world') === 'hello world'); - -// 长工具结果压缩 -const longResult = 'Action output:\n' + 'x'.repeat(5000) + '\n\nBased on the output above, continue...'; -const c1 = compressMessage('user', longResult); -assert('工具结果压缩', c1.length < 200, `压缩到 ${c1.length} chars`); -assert('工具结果保留信息', c1.includes('5000 chars') && c1.includes('Action output')); - -// 错误结果压缩 -const errorResult = 'The action encountered an error:\nPermission denied: cannot access /root/secret\n\nBased on the output above, continue...'; -const c2 = compressMessage('user', errorResult.padEnd(300, ' detail')); -assert('错误结果压缩', c2.startsWith('Action error:')); - -// 助手消息(工具调用)压缩 -const assistantMsg = `Let me check the file structure first.\n\n\`\`\`json action\n{"tool":"read_file","parameters":{"file_path":"src/index.ts"}}\n\`\`\`\n\nAnd then more text here to pad the message beyond the threshold limit. ${'x'.repeat(200)}`; -const c3 = compressMessage('assistant', assistantMsg); -assert('助手消息压缩保留工具名', c3.includes('[Called read_file(file_path)]'), c3); -assert('助手消息压缩', c3.length < assistantMsg.length, `${c3.length} < ${assistantMsg.length}`); - -// 普通长用户消息 -const longUser = 'Please help me with '.padEnd(500, 'this task '); -const c4 = compressMessage('user', longUser); -assert('普通用户消息截短', c4.length < 300 && c4.includes('chars total')); - -console.log(`\n=== 结果: ${passed} 通过, ${failed} 失败 ===\n`); -process.exit(failed > 0 ? 1 : 0); diff --git a/test/integration-compress-test.ts b/test/integration-compress-test.ts deleted file mode 100644 index 6b85839..0000000 --- a/test/integration-compress-test.ts +++ /dev/null @@ -1,221 +0,0 @@ -/** - * 集成测试:模拟长对话,验证上下文压缩流程 - * - * 测试场景:30+ 条消息的工具模式对话,验证: - * 1. 压缩触发条件(消息数 > 30 或字符数 > 60000) - * 2. few-shot 头部不被压缩 - * 3. 最近 6 条消息保持原文 - * 4. 中间老消息被压缩 - * 5. 消息数量不变(压缩不丢弃) - * 6. 总字符数显著减少 - */ - -// 模拟 CursorMessage 类型 -interface CursorMessage { - parts: { type: string; text?: string }[]; - id: string; - role: string; -} - -// 从 converter.ts 复制常量 -const FEWSHOT_COUNT = 2; -const MAX_CURSOR_MESSAGES = 30; -const MAX_CONTEXT_CHARS = 60000; -const KEEP_RECENT_MESSAGES = 6; -const COMPRESS_CONTENT_MAX = 200; - -// 从 converter.ts 复制 compressMessage -function compressMessage(role: string, text: string): string { - if (text.length <= COMPRESS_CONTENT_MAX) return text; - if (role === 'user') { - const actionMatch = text.match(/^Action output:\n([\s\S]*?)(?:\n\nBased on the output above|$)/); - if (actionMatch) { - const output = actionMatch[1]; - const firstLine = output.split('\n')[0]?.trim() || ''; - const lineCount = output.split('\n').length; - return `Action output: [${output.length} chars, ${lineCount} lines] ${firstLine.substring(0, 80)}...`; - } - const errorMatch = text.match(/^The action encountered an error:\n([\s\S]*?)(?:\n\nBased on the output above|$)/); - if (errorMatch) { - return `Action error: ${errorMatch[1].substring(0, 150)}...`; - } - return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars total]`; - } - if (role === 'assistant') { - const toolBlocks = text.match(/```json action\s*\n([\s\S]*?)```/g); - if (toolBlocks && toolBlocks.length > 0) { - const summaries: string[] = []; - for (const block of toolBlocks) { - try { - const jsonMatch = block.match(/```json action\s*\n([\s\S]*?)```/); - if (jsonMatch) { - const parsed = JSON.parse(jsonMatch[1]); - const toolName = parsed.tool || parsed.name || 'unknown'; - const paramKeys = parsed.parameters ? Object.keys(parsed.parameters) : []; - summaries.push(`[Called ${toolName}(${paramKeys.join(', ')})]`); - } - } catch { summaries.push('[Called action]'); } - } - const cleanText = text.replace(/```json action\s*\n[\s\S]*?```/g, '').trim(); - const briefText = cleanText.length > 100 ? cleanText.substring(0, 100) + '...' : cleanText; - return (briefText ? briefText + '\n' : '') + summaries.join('\n'); - } - return text.substring(0, COMPRESS_CONTENT_MAX) + `... [${text.length} chars]`; - } - return text.substring(0, COMPRESS_CONTENT_MAX) + '...'; -} - -// 模拟 converter.ts 中的压缩流程 -function applyCompression(messages: CursorMessage[], hasTools: boolean): { compressedCount: number; charsBefore: number; charsAfter: number } { - let compressedCount = 0; - const charsBefore = messages.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0); - - if (hasTools && messages.length > FEWSHOT_COUNT) { - if (charsBefore > MAX_CONTEXT_CHARS || messages.length > MAX_CURSOR_MESSAGES) { - const keepRecentCount = Math.min(KEEP_RECENT_MESSAGES, messages.length - FEWSHOT_COUNT); - const compressBoundary = messages.length - keepRecentCount; - - for (let i = FEWSHOT_COUNT; i < compressBoundary; i++) { - const original = messages[i].parts.map(p => p.text ?? '').join(''); - const compressed = compressMessage(messages[i].role, original); - if (compressed.length < original.length) { - messages[i] = { ...messages[i], parts: [{ type: 'text', text: compressed }] }; - compressedCount++; - } - } - } - } - - const charsAfter = messages.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0); - return { compressedCount, charsBefore, charsAfter }; -} - -// ==================== 构造测试数据 ==================== - -function buildLongConversation(turnCount: number): CursorMessage[] { - const messages: CursorMessage[] = []; - - // few-shot 头部 (2 条) - messages.push({ - parts: [{ type: 'text', text: 'You are a coding assistant. Use tools with ```json action``` format...' }], - id: 'fs1', role: 'user' - }); - messages.push({ - parts: [{ type: 'text', text: 'Understood. I\'ll use the structured format for actions.' }], - id: 'fs2', role: 'assistant' - }); - - // 模拟 N 轮工具交互 - for (let i = 0; i < turnCount; i++) { - // 用户请求或工具结果 - if (i === 0) { - messages.push({ - parts: [{ type: 'text', text: `Please read the file src/module${i}.ts and analyze its structure.\n\nRespond with the appropriate action using the structured format.` }], - id: `u${i}`, role: 'user' - }); - } else { - // 工具结果:模拟真实大小的文件内容 - const fileContent = `import { something } from './utils';\n\nexport class Module${i} {\n` + - Array.from({ length: 50 }, (_, j) => ` public method${j}(): void { /* implementation line ${j} */ }`).join('\n') + - `\n}\n`; - messages.push({ - parts: [{ type: 'text', text: `Action output:\n${fileContent}\n\nBased on the output above, continue with the next appropriate action using the structured format.` }], - id: `u${i}`, role: 'user' - }); - } - - // 助手的工具调用 - messages.push({ - parts: [{ - type: 'text', - text: `Let me examine the structure of module${i}.\n\n\`\`\`json action\n{"tool": "read_file", "parameters": {"file_path": "src/module${i}.ts"}}\n\`\`\`` - }], - id: `a${i}`, role: 'assistant' - }); - } - - return messages; -} - -// ==================== 运行测试 ==================== - -let passed = 0, failed = 0; -function assert(name: string, condition: boolean, detail?: string) { - if (condition) { passed++; console.log(` ✅ ${name}`); } - else { failed++; console.log(` ❌ ${name}${detail ? ': ' + detail : ''}`); } -} - -console.log('\n=== 场景 1:短对话(不触发压缩)==='); -{ - const msgs = buildLongConversation(3); // 2 few-shot + 6 实际 = 8 条 - const originalCount = msgs.length; - const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true); - assert('短对话不压缩', compressedCount === 0, `compressed=${compressedCount}`); - assert('消息数不变', msgs.length === originalCount); - assert('字符数不变', charsBefore === charsAfter); - console.log(` 📊 ${msgs.length} 条消息, ${charsBefore} chars`); -} - -console.log('\n=== 场景 2:长对话(触发按条数压缩)==='); -{ - const msgs = buildLongConversation(20); // 2 + 40 = 42 条消息 - const originalCount = msgs.length; - const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true); - assert('压缩触发', compressedCount > 0, `compressed=${compressedCount}`); - assert('消息数不变(压缩不丢弃)', msgs.length === originalCount, `${msgs.length} vs ${originalCount}`); - assert('总字符减少', charsAfter < charsBefore, `${charsAfter} < ${charsBefore}`); - assert('压缩率 >50%', charsAfter < charsBefore * 0.5, `ratio=${(charsAfter / charsBefore * 100).toFixed(1)}%`); - console.log(` 📊 ${msgs.length} 条, ${charsBefore} → ${charsAfter} chars (${(100 - charsAfter / charsBefore * 100).toFixed(1)}% 减少)`); - - // 验证 few-shot 头部不被压缩 - assert('few-shot[0] 保持原文', msgs[0].parts[0].text!.includes('You are a coding assistant')); - assert('few-shot[1] 保持原文', msgs[1].parts[0].text!.includes('Understood')); - - // 验证最近 6 条保持完整 - const lastSix = msgs.slice(-6); - for (const m of lastSix) { - const text = m.parts[0].text!; - const isOriginal = text.includes('Action output:\n') || text.includes('```json action'); - assert(`最近消息保持原文 (role=${m.role}, ${text.length} chars)`, isOriginal || text.length <= COMPRESS_CONTENT_MAX); - } - - // 验证中间消息被压缩 - const midMsg = msgs[4]; // 第 5 条消息(应在压缩区) - assert('中间消息已压缩', midMsg.parts[0].text!.length < 300, `len=${midMsg.parts[0].text!.length}`); -} - -console.log('\n=== 场景 3:非工具模式(不压缩)==='); -{ - const msgs = buildLongConversation(20); - const { compressedCount } = applyCompression(msgs, false); - assert('非工具模式不压缩', compressedCount === 0); -} - -console.log('\n=== 场景 4:大字符数但少消息(按字符数触发)==='); -{ - const msgs: CursorMessage[] = [ - { parts: [{ type: 'text', text: 'few-shot user' }], id: 'fs1', role: 'user' }, - { parts: [{ type: 'text', text: 'few-shot assistant' }], id: 'fs2', role: 'assistant' }, - ]; - // 10 轮,但每条都很大 - for (let i = 0; i < 10; i++) { - msgs.push({ - parts: [{ type: 'text', text: `Action output:\n${'x'.repeat(8000)}\n\nBased on the output above, continue with the next appropriate action using the structured format.` }], - id: `u${i}`, role: 'user' - }); - msgs.push({ - parts: [{ type: 'text', text: `Analysis done.\n\n\`\`\`json action\n{"tool": "write_file", "parameters": {"file_path": "out${i}.ts", "content": "${'y'.repeat(3000)}"}}\n\`\`\`` }], - id: `a${i}`, role: 'assistant' - }); - } - const originalChars = msgs.reduce((s, m) => s + m.parts.reduce((a, p) => a + (p.text?.length ?? 0), 0), 0); - assert('超过字符阈值', originalChars > MAX_CONTEXT_CHARS, `${originalChars} > ${MAX_CONTEXT_CHARS}`); - - const { compressedCount, charsBefore, charsAfter } = applyCompression(msgs, true); - assert('按字符数触发压缩', compressedCount > 0); - assert('字符数大幅减少', charsAfter < charsBefore * 0.5, `${charsAfter} < ${charsBefore * 0.5}`); - console.log(` 📊 ${msgs.length} 条, ${charsBefore} → ${charsAfter} chars (${(100 - charsAfter / charsBefore * 100).toFixed(1)}% 减少)`); -} - -console.log(`\n=== 总结果: ${passed} 通过, ${failed} 失败 ===\n`); -process.exit(failed > 0 ? 1 : 0); diff --git a/test/unit-tool-fixer.mjs b/test/unit-tool-fixer.mjs index 1ed49bb..15eb0c9 100644 --- a/test/unit-tool-fixer.mjs +++ b/test/unit-tool-fixer.mjs @@ -17,10 +17,7 @@ const SMART_SINGLE_QUOTES = new Set([ function normalizeToolArguments(args) { if (!args || typeof args !== 'object') return args; - if ('file_path' in args && !('path' in args)) { - args.path = args.file_path; - delete args.file_path; - } + // Removed legacy file_path to path conversion return args; } @@ -69,19 +66,19 @@ function assertEqual(a, b, msg) { // ════════════════════════════════════════════════════════════════════ console.log('\n📦 [1] normalizeToolArguments — 字段名映射\n'); -test('file_path → path 映射', () => { +test('file_path不再隐式转为path', () => { const args = { file_path: 'src/index.ts', content: 'hello' }; const result = normalizeToolArguments(args); - assertEqual(result.path, 'src/index.ts'); - assert(!('file_path' in result), 'file_path 应被删除'); - assertEqual(result.content, 'hello', 'content 不应被修改'); + assertEqual(result.file_path, 'src/index.ts', '应保留原始 file_path'); + assert(!('path' in result), '不应自动生成 path'); + assertEqual(result.content, 'hello'); }); -test('已有 path 字段时不覆盖', () => { +test('同时存在时保持不变', () => { const args = { file_path: 'old.ts', path: 'new.ts' }; const result = normalizeToolArguments(args); - assertEqual(result.path, 'new.ts', '应保留原始 path'); - assert('file_path' in result, 'file_path 应保留'); + assertEqual(result.path, 'new.ts'); + assert('file_path' in result); }); test('无 file_path 时不影响', () => { @@ -145,17 +142,17 @@ test('代码中的智能引号修复', () => { // ════════════════════════════════════════════════════════════════════ console.log('\n📦 [3] fixToolCallArguments — 综合修复\n'); -test('Read 工具: file_path → path', () => { +test('Read 工具: file_path 保持 file_path', () => { const args = { file_path: 'src/main.ts' }; const result = fixToolCallArguments('Read', args); - assertEqual(result.path, 'src/main.ts'); - assert(!('file_path' in result)); + assertEqual(result.file_path, 'src/main.ts'); + assert(!('path' in result)); }); -test('Write 工具: file_path + content 完整修复', () => { +test('Write 工具: file_path + content 保持不被截断', () => { const args = { file_path: 'test.ts', content: 'console.log("hello")' }; const result = fixToolCallArguments('Write', args); - assertEqual(result.path, 'test.ts'); + assertEqual(result.file_path, 'test.ts'); assertEqual(result.content, 'console.log("hello")'); }); @@ -217,7 +214,7 @@ function parseToolCallsWithFix(responseText) { return { toolCalls, cleanText: cleanText.trim() }; } -test('解析含 file_path 的工具调用 → 自动修复为 path', () => { +test('解析含 file_path 的工具调用 → 保持为 file_path', () => { const text = `I'll read the file now. \`\`\`json action @@ -231,11 +228,11 @@ test('解析含 file_path 的工具调用 → 自动修复为 path', () => { const { toolCalls } = parseToolCallsWithFix(text); assertEqual(toolCalls.length, 1); assertEqual(toolCalls[0].name, 'Read'); - assertEqual(toolCalls[0].arguments.path, 'src/index.ts'); - assert(!('file_path' in toolCalls[0].arguments), 'file_path 应被删除'); + assertEqual(toolCalls[0].arguments.file_path, 'src/index.ts'); + assert(!('path' in toolCalls[0].arguments), '不应生成 path'); }); -test('多个工具调用全部修复', () => { +test('多个工具调用不再强转', () => { const text = `\`\`\`json action {"tool":"Read","parameters":{"file_path":"a.ts"}} \`\`\` @@ -245,8 +242,8 @@ test('多个工具调用全部修复', () => { \`\`\``; const { toolCalls } = parseToolCallsWithFix(text); assertEqual(toolCalls.length, 2); - assertEqual(toolCalls[0].arguments.path, 'a.ts'); - assertEqual(toolCalls[1].arguments.path, 'b.ts'); + assertEqual(toolCalls[0].arguments.file_path, 'a.ts'); + assertEqual(toolCalls[1].arguments.file_path, 'b.ts'); assertEqual(toolCalls[1].arguments.content, 'hello'); });