csa / test /unit-tolerant-parse.mjs
ricebug's picture
Upload 76 files
ca8ab2d verified
/**
* 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);