Files
cursor2api/test/unit-tool-fixer.mjs

267 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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);