mirror of
https://github.com/7836246/cursor2api.git
synced 2026-09-03 07:20:02 +08:00
- mark degraded requests in logs and viewer when execution quality drops - record Anthropic toolCallsDetected correctly in summaries - continue semantically incomplete long Write/Edit tool payloads - restore truncated OpenAI stream long Write tool_calls - update release docs, sample config, docker notes, and README guidance
152 lines
5.4 KiB
JavaScript
152 lines
5.4 KiB
JavaScript
import { autoContinueCursorToolResponseStream } from '../dist/handler.js';
|
|
import { parseToolCalls } from '../dist/converter.js';
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
|
|
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' },
|
|
});
|
|
}
|
|
|
|
async function runTest(name, fn) {
|
|
try {
|
|
await fn();
|
|
console.log(` OK ${name}`);
|
|
passed++;
|
|
} catch (error) {
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
console.error(` FAIL ${name}`);
|
|
console.error(` ${message}`);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
console.log('\nOpenAI stream truncation regression\n');
|
|
|
|
await runTest('long Write triggers continuation and restores multi-frame tool_calls', 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 = [
|
|
'Preparing file write.',
|
|
'',
|
|
'```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, 'long Write truncation should trigger one continuation request');
|
|
assertEqual(parsed.toolCalls.length, 1, 'continuation should restore one tool call');
|
|
assertEqual(parsed.toolCalls[0].name, 'Write');
|
|
assert(typeof fetchCalls[0].body?.messages?.at(-1)?.parts?.[0]?.text === 'string', 'continuation request should include a user guidance message');
|
|
assert(fetchCalls[0].body.messages.at(-1).parts[0].text.includes('Continue EXACTLY from where you stopped'), 'continuation prompt should be injected');
|
|
|
|
const content = String(parsed.toolCalls[0].arguments.content || '');
|
|
assert(content.startsWith('AAAA'), 'should preserve original prefix before truncation');
|
|
assert(content.includes('BBBB'), 'should append continuation content');
|
|
|
|
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, 'long Write arguments should split into multiple tool_call frames in OpenAI stream mode');
|
|
assertEqual(chunks.join(''), argsStr, 'rejoined chunks should equal original arguments');
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
await runTest('short Read does not trigger continuation in OpenAI stream mode', async () => {
|
|
const originalFetch = global.fetch;
|
|
let fetchCount = 0;
|
|
|
|
try {
|
|
global.fetch = async () => {
|
|
fetchCount++;
|
|
throw new Error('short-argument tools should not trigger continuation');
|
|
};
|
|
|
|
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, 'short Read should not enter continuation');
|
|
assertEqual(parsed.toolCalls.length, 1, 'short-argument tools should still be recovered directly');
|
|
assertEqual(parsed.toolCalls[0].name, 'Read');
|
|
} finally {
|
|
global.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
console.log(`\nresult: ${passed} passed / ${failed} failed / ${passed + failed} total\n`);
|
|
|
|
if (failed > 0) process.exit(1);
|