Spaces:
Sleeping
Sleeping
| /** | |
| * test/unit-tool-fixer.mjs | |
| * | |
| * ๅๅ ๆต่ฏ๏ผtool-fixer ็ๅๅ่ฝ | |
| * ่ฟ่กๆนๅผ๏ผnode test/unit-tool-fixer.mjs | |
| */ | |
| // โโโ ๅ ่ๅฎ็ฐ๏ผไธ src/tool-fixer.ts ไฟๆๅๆญฅ๏ผ้ฟๅ ไพ่ต dist๏ผโโโโโโโโโโโโโโ | |
| const SMART_DOUBLE_QUOTES = new Set([ | |
| '\u00ab', '\u201c', '\u201d', '\u275e', | |
| '\u201f', '\u201e', '\u275d', '\u00bb', | |
| ]); | |
| const SMART_SINGLE_QUOTES = new Set([ | |
| '\u2018', '\u2019', '\u201a', '\u201b', | |
| ]); | |
| function normalizeToolArguments(args) { | |
| if (!args || typeof args !== 'object') return args; | |
| // Removed legacy file_path to path conversion | |
| return args; | |
| } | |
| function replaceSmartQuotes(text) { | |
| const chars = [...text]; | |
| return chars.map(ch => { | |
| if (SMART_DOUBLE_QUOTES.has(ch)) return '"'; | |
| if (SMART_SINGLE_QUOTES.has(ch)) return "'"; | |
| return ch; | |
| }).join(''); | |
| } | |
| function fixToolCallArguments(toolName, args) { | |
| args = normalizeToolArguments(args); | |
| // repairExactMatchToolArguments is skipped in unit test (needs file system) | |
| return args; | |
| } | |
| // โโโ ๆต่ฏๆกๆถ โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| let passed = 0; | |
| let failed = 0; | |
| function test(name, fn) { | |
| try { | |
| fn(); | |
| console.log(` โ ${name}`); | |
| passed++; | |
| } catch (e) { | |
| console.error(` โ ${name}`); | |
| console.error(` ${e.message}`); | |
| failed++; | |
| } | |
| } | |
| function assert(condition, msg) { | |
| if (!condition) throw new Error(msg || 'Assertion failed'); | |
| } | |
| function assertEqual(a, b, msg) { | |
| const as = JSON.stringify(a), bs = JSON.stringify(b); | |
| if (as !== bs) throw new Error(msg || `Expected ${bs}, got ${as}`); | |
| } | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 1. normalizeToolArguments โ ๅญๆฎตๅๆ ๅฐ | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [1] normalizeToolArguments โ ๅญๆฎตๅๆ ๅฐ\n'); | |
| test('file_pathไธๅ้ๅผ่ฝฌไธบpath', () => { | |
| const args = { file_path: 'src/index.ts', content: 'hello' }; | |
| const result = normalizeToolArguments(args); | |
| assertEqual(result.file_path, 'src/index.ts', 'ๅบไฟ็ๅๅง file_path'); | |
| assert(!('path' in result), 'ไธๅบ่ชๅจ็ๆ path'); | |
| assertEqual(result.content, 'hello'); | |
| }); | |
| test('ๅๆถๅญๅจๆถไฟๆไธๅ', () => { | |
| const args = { file_path: 'old.ts', path: 'new.ts' }; | |
| const result = normalizeToolArguments(args); | |
| assertEqual(result.path, 'new.ts'); | |
| assert('file_path' in result); | |
| }); | |
| test('ๆ file_path ๆถไธๅฝฑๅ', () => { | |
| const args = { path: 'foo.ts', content: 'bar' }; | |
| const result = normalizeToolArguments(args); | |
| assertEqual(result.path, 'foo.ts'); | |
| assertEqual(result.content, 'bar'); | |
| }); | |
| test('null/undefined ่พๅ ฅๅฎๅ จ', () => { | |
| assertEqual(normalizeToolArguments(null), null); | |
| assertEqual(normalizeToolArguments(undefined), undefined); | |
| }); | |
| test('็ฉบๅฏน่ฑก', () => { | |
| const result = normalizeToolArguments({}); | |
| assertEqual(result, {}); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 2. replaceSmartQuotes โ ๆบ่ฝๅผๅทๆฟๆข | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [2] replaceSmartQuotes โ ๆบ่ฝๅผๅทๆฟๆข\n'); | |
| test('ไธญๆๅๅผๅท โ ๆฎ้ๅๅผๅท', () => { | |
| const input = '\u201cไฝ ๅฅฝ\u201d'; | |
| assertEqual(replaceSmartQuotes(input), '"ไฝ ๅฅฝ"'); | |
| }); | |
| test('ไธญๆๅๅผๅท โ ๆฎ้ๅๅผๅท', () => { | |
| const input = '\u2018hello\u2019'; | |
| assertEqual(replaceSmartQuotes(input), "'hello'"); | |
| }); | |
| test('ๆททๅๅผๅทๆฟๆข', () => { | |
| const input = '\u201cHello\u201d and \u2018World\u2019'; | |
| assertEqual(replaceSmartQuotes(input), '"Hello" and \'World\''); | |
| }); | |
| test('ๆ ๆบ่ฝๅผๅทๆถๅๆ ท่ฟๅ', () => { | |
| const input = '"normal" and \'single\''; | |
| assertEqual(replaceSmartQuotes(input), input); | |
| }); | |
| test('็ฉบๅญ็ฌฆไธฒ', () => { | |
| assertEqual(replaceSmartQuotes(''), ''); | |
| }); | |
| test('ๆณๆๅผๅท ยซ ยป', () => { | |
| const input = '\u00abBonjour\u00bb'; | |
| assertEqual(replaceSmartQuotes(input), '"Bonjour"'); | |
| }); | |
| test('ไปฃ็ ไธญ็ๆบ่ฝๅผๅทไฟฎๅค', () => { | |
| const input = 'const name = \u201cClaude\u201d;'; | |
| assertEqual(replaceSmartQuotes(input), 'const name = "Claude";'); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 3. fixToolCallArguments โ ็ปผๅไฟฎๅค | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [3] fixToolCallArguments โ ็ปผๅไฟฎๅค\n'); | |
| test('Read ๅทฅๅ ท: file_path ไฟๆ file_path', () => { | |
| const args = { file_path: 'src/main.ts' }; | |
| const result = fixToolCallArguments('Read', args); | |
| assertEqual(result.file_path, 'src/main.ts'); | |
| assert(!('path' in result)); | |
| }); | |
| test('Write ๅทฅๅ ท: file_path + content ไฟๆไธ่ขซๆชๆญ', () => { | |
| const args = { file_path: 'test.ts', content: 'console.log("hello")' }; | |
| const result = fixToolCallArguments('Write', args); | |
| assertEqual(result.file_path, 'test.ts'); | |
| assertEqual(result.content, 'console.log("hello")'); | |
| }); | |
| test('Bash ๅทฅๅ ท: ๆ ๆ ๅฐ้่ฆ', () => { | |
| const args = { command: 'ls -la' }; | |
| const result = fixToolCallArguments('Bash', args); | |
| assertEqual(result.command, 'ls -la'); | |
| }); | |
| test('้ๅฏน่ฑกๅๆฐๅฎๅ จๅค็', () => { | |
| assertEqual(fixToolCallArguments('Read', null), null); | |
| assertEqual(fixToolCallArguments('Read', undefined), undefined); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 4. parseToolCalls with fixToolCallArguments โ ้ๆๆต่ฏ | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [4] parseToolCalls + fixToolCallArguments ้ๆ\n'); | |
| function tolerantParse(jsonStr) { | |
| try { return JSON.parse(jsonStr); } catch { /* pass */ } | |
| let inString = false, escaped = false, fixed = ''; | |
| const bracketStack = []; | |
| 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 { } } | |
| throw _e2; | |
| } | |
| } | |
| function parseToolCallsWithFix(responseText) { | |
| const toolCalls = []; | |
| let cleanText = responseText; | |
| const fullBlockRegex = /```json(?:\s+action)?\s*([\s\S]*?)\s*```/g; | |
| let match; | |
| while ((match = fullBlockRegex.exec(responseText)) !== null) { | |
| let isToolCall = false; | |
| try { | |
| const parsed = tolerantParse(match[1]); | |
| if (parsed.tool || parsed.name) { | |
| const name = parsed.tool || parsed.name; | |
| let args = parsed.parameters || parsed.arguments || parsed.input || {}; | |
| args = fixToolCallArguments(name, args); | |
| toolCalls.push({ name, arguments: args }); | |
| isToolCall = true; | |
| } | |
| } catch (e) { /* skip */ } | |
| if (isToolCall) cleanText = cleanText.replace(match[0], ''); | |
| } | |
| return { toolCalls, cleanText: cleanText.trim() }; | |
| } | |
| test('่งฃๆๅซ file_path ็ๅทฅๅ ท่ฐ็จ โ ไฟๆไธบ file_path', () => { | |
| const text = `I'll read the file now. | |
| \`\`\`json action | |
| { | |
| "tool": "Read", | |
| "parameters": { | |
| "file_path": "src/index.ts" | |
| } | |
| } | |
| \`\`\``; | |
| const { toolCalls } = parseToolCallsWithFix(text); | |
| assertEqual(toolCalls.length, 1); | |
| assertEqual(toolCalls[0].name, 'Read'); | |
| assertEqual(toolCalls[0].arguments.file_path, 'src/index.ts'); | |
| assert(!('path' in toolCalls[0].arguments), 'ไธๅบ็ๆ path'); | |
| }); | |
| test('ๅคไธชๅทฅๅ ท่ฐ็จไธๅๅผบ่ฝฌ', () => { | |
| const text = `\`\`\`json action | |
| {"tool":"Read","parameters":{"file_path":"a.ts"}} | |
| \`\`\` | |
| \`\`\`json action | |
| {"tool":"Write","parameters":{"file_path":"b.ts","content":"hello"}} | |
| \`\`\``; | |
| const { toolCalls } = parseToolCallsWithFix(text); | |
| assertEqual(toolCalls.length, 2); | |
| assertEqual(toolCalls[0].arguments.file_path, 'a.ts'); | |
| assertEqual(toolCalls[1].arguments.file_path, 'b.ts'); | |
| assertEqual(toolCalls[1].arguments.content, 'hello'); | |
| }); | |
| test('ๆ ้ไฟฎๅค็ๅทฅๅ ท่ฐ็จไฟๆไธๅ', () => { | |
| const text = `\`\`\`json action | |
| {"tool":"Bash","parameters":{"command":"npm run build"}} | |
| \`\`\``; | |
| const { toolCalls } = parseToolCallsWithFix(text); | |
| assertEqual(toolCalls.length, 1); | |
| assertEqual(toolCalls[0].arguments.command, 'npm run build'); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // ๆฑๆป | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n' + 'โ'.repeat(55)); | |
| console.log(` ็ปๆ: ${passed} ้่ฟ / ${failed} ๅคฑ่ดฅ / ${passed + failed} ๆป่ฎก`); | |
| console.log('โ'.repeat(55) + '\n'); | |
| if (failed > 0) process.exit(1); | |