csa / test /unit-openai-stream-truncation.mjs
ricebug's picture
Upload 76 files
ca8ab2d verified
import { autoContinueCursorToolResponseStream } from '../dist/handler.js';
import { parseToolCalls } from '../dist/converter.js';
let passed = 0;
let failed = 0;
function test(name, fn) {
Promise.resolve()
.then(fn)
.then(() => {
console.log(` ✅ ${name}`);
passed++;
})
.catch((error) => {
const message = error instanceof Error ? error.message : String(error);
console.error(` ❌ ${name}`);
console.error(` ${message}`);
failed++;
});
}
function assert(condition, message) {
if (!condition) throw new Error(message || 'Assertion failed');
}
function assertEqual(actual, expected, message) {
const a = JSON.stringify(actual);
const b = JSON.stringify(expected);
if (a !== b) {
throw new Error(message || `Expected ${b}, got ${a}`);
}
}
function buildCursorReq() {
return {
model: 'claude-sonnet-4-5',
id: 'req_test',
trigger: 'user',
messages: [
{
id: 'msg_user',
role: 'user',
parts: [{ type: 'text', text: 'Write a long file.' }],
},
],
};
}
function createSseResponse(deltas) {
const encoder = new TextEncoder();
const stream = new ReadableStream({
start(controller) {
for (const delta of deltas) {
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: 'text-delta', delta })}\n\n`));
}
controller.close();
},
});
return new Response(stream, {
status: 200,
headers: { 'Content-Type': 'text/event-stream' },
});
}
const pending = [];
console.log('\n📦 OpenAI 流式截断回归\n');
pending.push((async () => {
const originalFetch = global.fetch;
const fetchCalls = [];
try {
global.fetch = async (url, init) => {
fetchCalls.push({ url: String(url), body: init?.body ? JSON.parse(String(init.body)) : null });
return createSseResponse([
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
'BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB',
'"\n }\n}\n```',
]);
};
const initialResponse = [
'准备写入文件。',
'',
'```json action',
'{',
' "tool": "Write",',
' "parameters": {',
' "file_path": "/tmp/long.txt",',
' "content": "AAAA' + 'A'.repeat(1800),
].join('\n');
const fullResponse = await autoContinueCursorToolResponseStream(buildCursorReq(), initialResponse, true);
const parsed = parseToolCalls(fullResponse);
assertEqual(fetchCalls.length, 1, '长 Write 截断应触发一次续写请求');
assertEqual(parsed.toolCalls.length, 1, '续写后应恢复出一个工具调用');
assertEqual(parsed.toolCalls[0].name, 'Write');
assert(typeof fetchCalls[0].body?.messages?.at(-1)?.parts?.[0]?.text === 'string', '续写请求应包含 user 引导消息');
assert(fetchCalls[0].body.messages.at(-1).parts[0].text.includes('Continue EXACTLY from where you stopped'), '续写提示词应正确注入');
const content = String(parsed.toolCalls[0].arguments.content || '');
assert(content.startsWith('AAAA'), '应保留原始截断前缀');
assert(content.includes('BBBB'), '应拼接续写补全内容');
const argsStr = JSON.stringify(parsed.toolCalls[0].arguments);
const CHUNK_SIZE = 128;
const chunks = [];
for (let j = 0; j < argsStr.length; j += CHUNK_SIZE) {
chunks.push(argsStr.slice(j, j + CHUNK_SIZE));
}
assert(chunks.length > 1, '长 Write 参数在 OpenAI 流式中应拆成多帧 tool_calls');
assertEqual(chunks.join(''), argsStr, '分块后重新拼接应等于原始 arguments');
console.log(' ✅ 长 Write 截断后续写并恢复为多帧 tool_calls');
passed++;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(' ❌ 长 Write 截断后续写并恢复为多帧 tool_calls');
console.error(` ${message}`);
failed++;
} finally {
global.fetch = originalFetch;
}
})());
pending.push((async () => {
const originalFetch = global.fetch;
let fetchCount = 0;
try {
global.fetch = async () => {
fetchCount++;
throw new Error('短参数工具不应触发续写请求');
};
const initialResponse = [
'```json action',
'{',
' "tool": "Read",',
' "parameters": {',
' "file_path": "/tmp/config.yaml"',
' }',
].join('\n');
const fullResponse = await autoContinueCursorToolResponseStream(buildCursorReq(), initialResponse, true);
const parsed = parseToolCalls(fullResponse);
assertEqual(fetchCount, 0, '短参数 Read 不应进入续写');
assertEqual(parsed.toolCalls.length, 1, '即使未闭合也应直接恢复短参数工具');
assertEqual(parsed.toolCalls[0].name, 'Read');
console.log(' ✅ 短参数 Read 不会在 OpenAI 流式路径中误续写');
passed++;
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error(' ❌ 短参数 Read 不会在 OpenAI 流式路径中误续写');
console.error(` ${message}`);
failed++;
} finally {
global.fetch = originalFetch;
}
})());
await Promise.all(pending);
console.log(`\n结果: ${passed} 通过 / ${failed} 失败 / ${passed + failed} 总计\n`);
if (failed > 0) process.exit(1);