Spaces:
Sleeping
Sleeping
| /** | |
| * test/unit-tolerant-parse.mjs | |
| * | |
| * ๅๅ ๆต่ฏ๏ผtolerantParse ๅ parseToolCalls ็ๅ็ง่พน็ๅบๆฏ | |
| * ่ฟ่กๆนๅผ๏ผnode test/unit-tolerant-parse.mjs | |
| * | |
| * ๆ ้ๆๅกๅจ๏ผๅฎๅ จ็ฆป็บฟ่ฟ่กใ | |
| */ | |
| // โโโ ไป dist/ ไธญ็ดๆฅๅผๅ ฅๅทฒ็ผ่ฏ็ converter๏ผ้่ฆๅ npm run build๏ผโโโโโโโโโโ | |
| // ๅฆๆๆฒกๆ dist๏ผไนๅฏไปฅๆ tolerantParse ็ๅฎ็ฐๅคๅถๅฐๆญคๅคๅๆต่ฏ | |
| // โโโ ๅ ่ tolerantParse๏ผไธ src/converter.ts ไฟๆๅๆญฅ๏ผโโโโโโโโโโโโโโโโโโโโโโ | |
| function tolerantParse(jsonStr) { | |
| try { | |
| return JSON.parse(jsonStr); | |
| } catch (_e1) { /* pass */ } | |
| let inString = false; | |
| let escaped = false; | |
| let 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 { /* ignore */ } | |
| } | |
| throw _e2; | |
| } | |
| } | |
| // โโโ ๅ ่ parseToolCalls๏ผไธ src/converter.ts ไฟๆๅๆญฅ๏ผโโโโโโโโโโโโโโโโโโโโ | |
| function parseToolCalls(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) { | |
| toolCalls.push({ | |
| name: parsed.tool || parsed.name, | |
| arguments: parsed.parameters || parsed.arguments || parsed.input || {} | |
| }); | |
| isToolCall = true; | |
| } | |
| } catch (e) { | |
| console.error(` โ tolerantParse ๅคฑ่ดฅ:`, e.message); | |
| } | |
| if (isToolCall) cleanText = cleanText.replace(match[0], ''); | |
| } | |
| return { toolCalls, cleanText: cleanText.trim() }; | |
| } | |
| // โโโ ๆต่ฏๆกๆถ๏ผๆ็ฎ๏ผโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| 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. tolerantParse โ ๆญฃๅธธ JSON | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [1] tolerantParse โ ๆญฃๅธธ JSON\n'); | |
| test('ๆ ๅ JSON ๅฏน่ฑก', () => { | |
| const r = tolerantParse('{"tool":"Read","parameters":{"path":"/foo"}}'); | |
| assertEqual(r.tool, 'Read'); | |
| assertEqual(r.parameters.path, '/foo'); | |
| }); | |
| test('ๅธฆๆข่ก็ผฉ่ฟ็ JSON', () => { | |
| const r = tolerantParse(`{ | |
| "tool": "Write", | |
| "parameters": { | |
| "file_path": "src/index.ts", | |
| "content": "hello world" | |
| } | |
| }`); | |
| assertEqual(r.tool, 'Write'); | |
| }); | |
| test('็ฉบๅฏน่ฑก', () => { | |
| const r = tolerantParse('{}'); | |
| assertEqual(r, {}); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 2. tolerantParse โ ๅญ็ฌฆไธฒๅ ๅซ่ฃธๆข่ก๏ผๆตๅผ่พๅบๅธธ่งๅบๆฏ๏ผ | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [2] tolerantParse โ ๅญ็ฌฆไธฒๅ ๅซ่ฃธๆข่ก\n'); | |
| test('value ไธญๅซ่ฃธ \\n', () => { | |
| // ๆจกๆ๏ผcontent ๅญๆฎตๅผ้ๆๅค่กๆๆฌ๏ผไฝ JSON ๆฒกๆ่ฝฌไนๆข่ก | |
| const raw = '{"tool":"Write","parameters":{"content":"line1\nline2\nline3"}}'; | |
| const r = tolerantParse(raw); | |
| assert(r.parameters.content.includes('\n') || r.parameters.content.includes('\\n'), | |
| 'content ๅบๅ ๅซๆข่กไฟกๆฏ'); | |
| }); | |
| test('value ไธญๅซ่ฃธ \\t', () => { | |
| const raw = '{"tool":"Bash","parameters":{"command":"echo\there"}}'; | |
| const r = tolerantParse(raw); | |
| assert(r.parameters.command !== undefined); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 3. tolerantParse โ ๆชๆญ JSON๏ผๆ ธๅฟไฟฎๅคๅบๆฏ๏ผ | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [3] tolerantParse โ ๆชๆญ JSON๏ผๆช้ญๅๅญ็ฌฆไธฒ / ๆฌๅท๏ผ\n'); | |
| test('ๅญ็ฌฆไธฒๅจๅผไธญ้ดๆชๆญ', () => { | |
| // ๆจกๆ๏ผ็ฝ็ปไธญๆญ๏ผ"content" ๅญๆฎตๅผๅชไผ ไบไธๅ | |
| const truncated = '{"tool":"Write","parameters":{"content":"# Accrual Backfill Start Date Implementation Plan\\n\\n> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.\\n\\n**Goal:** Add an optional `backfillStartDate` parameter to the company-level accrual recalculate feature, allowing admins to specify a'; | |
| const r = tolerantParse(truncated); | |
| // ่ฝ่งฃๆๅบๆฅๅฐฑ่ก๏ผcontent ๅฏ่ฝ่ขซๆชๆญไฝ tool ๅญๆฎตๅญๅจ | |
| assertEqual(r.tool, 'Write'); | |
| assert(r.parameters !== undefined); | |
| }); | |
| test('ๅช็ผบๅฐๆๅ็ }}', () => { | |
| const truncated = '{"tool":"Read","parameters":{"file_path":"/Users/rain/project/src/index.ts"'; | |
| const r = tolerantParse(truncated); | |
| assertEqual(r.tool, 'Read'); | |
| }); | |
| test('ๅช็ผบๅฐๆๅ็ }', () => { | |
| const truncated = '{"name":"Bash","input":{"command":"ls -la"}'; | |
| const r = tolerantParse(truncated); | |
| assertEqual(r.name, 'Bash'); | |
| }); | |
| test('ๅตๅฅๅฏน่ฑกๆชๆญ', () => { | |
| const truncated = '{"tool":"Write","parameters":{"path":"a.ts","content":"export function foo() {\n return 42;\n}'; | |
| const r = tolerantParse(truncated); | |
| assertEqual(r.tool, 'Write'); | |
| }); | |
| test('ๅธฆๅฐพ้จ้ๅท', () => { | |
| const withComma = '{"tool":"Read","parameters":{"path":"/foo",},}'; | |
| const r = tolerantParse(withComma); | |
| assertEqual(r.tool, 'Read'); | |
| }); | |
| test('ๆจกๆ issue #13 ๅๅง้่ฏฏ โ position 813 ๆชๆญ', () => { | |
| // ๆจกๆไธไธช็บฆ813ๅญ่็ content ๅญๆฎตๅจๅญ็ฌฆไธฒไธญ้ดๆชๆญ | |
| const longContent = 'A'.repeat(700); | |
| const truncated = `{"tool":"Write","parameters":{"file_path":"/docs/plan.md","content":"${longContent}`; | |
| const r = tolerantParse(truncated); | |
| assertEqual(r.tool, 'Write'); | |
| // content ๅญๆฎตๅผๅฏ่ฝ่ขซๆชๆญ๏ผไฝๆดไฝ JSON ๅบๅฝ่ฝ่งฃๆ | |
| assert(typeof r.parameters.content === 'string', 'content ๅบไธบๅญ็ฌฆไธฒ'); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 4. parseToolCalls โ ๅฎๆด ```json action ๅ | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [4] parseToolCalls โ ๅฎๆดไปฃ็ ๅ\n'); | |
| test('ๅไธชๅทฅๅ ท่ฐ็จๅ (tool ๅญๆฎต)', () => { | |
| const text = `I'll read the file now. | |
| \`\`\`json action | |
| { | |
| "tool": "Read", | |
| "parameters": { | |
| "file_path": "src/index.ts" | |
| } | |
| } | |
| \`\`\``; | |
| const { toolCalls, cleanText } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 1); | |
| assertEqual(toolCalls[0].name, 'Read'); | |
| assertEqual(toolCalls[0].arguments.file_path, 'src/index.ts'); | |
| assert(!cleanText.includes('```'), 'ไปฃ็ ๅๅบ่ขซ็งป้ค'); | |
| }); | |
| test('ๅไธชๅทฅๅ ท่ฐ็จๅ (name ๅญๆฎต)', () => { | |
| const text = `\`\`\`json action | |
| {"name":"Bash","input":{"command":"npm run build"}} | |
| \`\`\``; | |
| const { toolCalls } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 1); | |
| assertEqual(toolCalls[0].name, 'Bash'); | |
| assertEqual(toolCalls[0].arguments.command, 'npm run build'); | |
| }); | |
| 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 } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 2); | |
| assertEqual(toolCalls[0].name, 'Read'); | |
| assertEqual(toolCalls[1].name, 'Write'); | |
| }); | |
| test('ๅทฅๅ ท่ฐ็จๅๆ่งฃ้ๆๆฌ', () => { | |
| const text = `Let me first read the existing file to understand the structure. | |
| \`\`\`json action | |
| {"tool":"Read","parameters":{"file_path":"src/handler.ts"}} | |
| \`\`\``; | |
| const { toolCalls, cleanText } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 1); | |
| assert(cleanText.includes('Let me first read'), '่งฃ้ๆๆฌๅบไฟ็'); | |
| }); | |
| test('ไธๅซๅทฅๅ ท่ฐ็จ็็บฏๆๆฌ', () => { | |
| const text = 'Here is the answer: 42. No tool calls needed.'; | |
| const { toolCalls, cleanText } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 0); | |
| assertEqual(cleanText, text); | |
| }); | |
| test('json ๅไฝไธๆฏ tool call๏ผๆฎ้ json๏ผ', () => { | |
| const text = `Here is an example: | |
| \`\`\`json | |
| {"key":"value","count":42} | |
| \`\`\``; | |
| const { toolCalls } = parseToolCalls(text); | |
| assertEqual(toolCalls.length, 0, 'ๆ tool/name ๅญๆฎต็ JSON ไธๅบ่ขซ่ฏๅซไธบๅทฅๅ ท่ฐ็จ'); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // 5. ๆชๆญๅบๆฏไธ็ parseToolCalls | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n๐ฆ [5] parseToolCalls โ ๆชๆญๅบๆฏ\n'); | |
| test('ไปฃ็ ๅๅ ๅฎน่ขซๆตไธญๆญ๏ผblock ๅฎๆดไฝ JSON ๆชๆญ๏ผ', () => { | |
| // ๅฎๆด็ ``` ๅ ่ฃน๏ผไฝ JSON ๅ ๅฎน่ขซๆชๆญ | |
| const text = `\`\`\`json action | |
| {"tool":"Write","parameters":{"file_path":"/docs/plan.md","content":"# Plan\n\nThis is a very long document that got cut at position 813 in the strea | |
| \`\`\``; | |
| const { toolCalls } = parseToolCalls(text); | |
| // ๅบๅฝ่ฝ่งฃๆๅบๅทฅๅ ท่ฐ็จ๏ผๅณไฝฟ content ่ขซๆชๆญ๏ผ | |
| assertEqual(toolCalls.length, 1); | |
| assertEqual(toolCalls[0].name, 'Write'); | |
| console.log(` โ ่งฃๆๅบ็ content ๅ30ๅญ็ฌฆ: "${String(toolCalls[0].arguments.content).substring(0, 30)}..."`); | |
| }); | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| // ๆฑๆป | |
| // โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ | |
| console.log('\n' + 'โ'.repeat(55)); | |
| console.log(` ็ปๆ: ${passed} ้่ฟ / ${failed} ๅคฑ่ดฅ / ${passed + failed} ๆป่ฎก`); | |
| console.log('โ'.repeat(55) + '\n'); | |
| if (failed > 0) process.exit(1); | |