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