| 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); |
|
|