Files
cursor2api/test/e2e-test.ts
小海 5f0f9b7936 feat: v2.5.1 - 上下文智能压缩 + 截断检测 + tolerantParse 增强
🗜️ 智能压缩
- 长对话老消息压缩而非丢弃,保留因果链语义
- 工具结果压缩为摘要,助手消息保留工具名
- 压缩率 70-80%,解决 Cursor 上下文溢出问题

⚠️ 截断检测
- 代码块/XML 未闭合时返回 stop_reason=max_tokens
- Claude Code 自动继续,无需手动点击"继续"

🔧 tolerantParse
- 新增正则兜底层,处理未转义双引号的 JSON
- 解决 position 5384 等长参数解析崩溃

🛡️ 拒绝 fallback 优化
- 工具模式下返回极短引导文本
2026-03-10 17:29:49 +08:00

204 lines
7.4 KiB
TypeScript
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.
/**
* 端到端测试:向真实 Cursor2API 服务发送请求
*
* 测试场景:
* 1. 简单请求能正常返回
* 2. 带工具的多轮长对话触发压缩
* 3. 验证 stop_reason 正确
*/
const API_URL = 'http://localhost:3010/v1/messages';
interface TestResult {
name: string;
passed: boolean;
detail: string;
}
const results: TestResult[] = [];
function assert(name: string, condition: boolean, detail = '') {
results.push({ name, passed: condition, detail });
console.log(condition ? `${name}` : `${name}: ${detail}`);
}
// 构造一个模拟 Claude Code 的长对话请求(带很多轮工具交互历史)
function buildLongToolRequest(turnCount: number) {
const messages: any[] = [];
// 模拟多轮工具交互历史
for (let i = 0; i < turnCount; i++) {
if (i === 0) {
// 第一轮:用户发起请求
messages.push({
role: 'user',
content: 'Help me analyze the project structure. Read the main entry file first.'
});
} else {
// 工具结果
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: `tool_${i}`,
content: `File content of module${i}.ts:\n` +
`import { something } from './utils';\n\n` +
`export class Module${i} {\n` +
Array.from({length: 30}, (_, j) => ` method${j}() { return ${j}; }`).join('\n') +
`\n}\n`
}
]
});
}
// 助手的工具调用
messages.push({
role: 'assistant',
content: [
{ type: 'text', text: `Let me check module${i + 1}.` },
{
type: 'tool_use',
id: `tool_${i + 1}`,
name: 'Read',
input: { file_path: `src/module${i + 1}.ts` }
}
]
});
}
// 最后一轮工具结果
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: `tool_${turnCount}`,
content: 'File not found: src/module' + turnCount + '.ts'
}
]
});
return {
model: 'claude-sonnet-4-20250514',
max_tokens: 4096,
stream: false,
system: 'You are a helpful coding assistant.',
tools: [
{
name: 'Read',
description: 'Read a file from disk',
input_schema: {
type: 'object',
properties: {
file_path: { type: 'string', description: 'Path to the file' }
},
required: ['file_path']
}
},
{
name: 'Bash',
description: 'Execute a shell command',
input_schema: {
type: 'object',
properties: {
command: { type: 'string', description: 'The command to execute' }
},
required: ['command']
}
}
],
messages
};
}
async function runTests() {
console.log('\n=== 测试 1基本请求 ===');
try {
const resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
stream: false,
messages: [{ role: 'user', content: 'Say "hello" in one word.' }]
})
});
assert('服务器响应', resp.ok, `status=${resp.status}`);
const data = await resp.json();
assert('返回 message 类型', data.type === 'message', `type=${data.type}`);
assert('stop_reason 是 end_turn', data.stop_reason === 'end_turn', `stop_reason=${data.stop_reason}`);
assert('有 content', data.content?.length > 0, `content=${JSON.stringify(data.content)}`);
console.log(` 📝 响应: ${data.content?.[0]?.text?.substring(0, 100)}`);
} catch (e: any) {
assert('基本请求', false, e.message);
}
console.log('\n=== 测试 2长对话工具请求触发压缩===');
try {
const longReq = buildLongToolRequest(18); // 18 轮 → 37 条消息
console.log(` 📊 发送 ${longReq.messages.length} 条消息...`);
const resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
body: JSON.stringify(longReq)
});
assert('长对话服务器响应', resp.ok, `status=${resp.status}`);
const data = await resp.json();
assert('长对话返回 message', data.type === 'message', `type=${data.type}`);
assert('长对话有 content', data.content?.length > 0);
// 检查 stop_reason
const validStops = ['end_turn', 'tool_use', 'max_tokens'];
assert('stop_reason 合法', validStops.includes(data.stop_reason), `stop_reason=${data.stop_reason}`);
console.log(` 📝 stop_reason: ${data.stop_reason}`);
console.log(` 📝 content blocks: ${data.content?.length}`);
if (data.content?.[0]?.text) {
console.log(` 📝 响应片段: ${data.content[0].text.substring(0, 150)}...`);
}
} catch (e: any) {
assert('长对话请求', false, e.message);
}
console.log('\n=== 测试 3流式请求 ===');
try {
const resp = await fetch(API_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'test' },
body: JSON.stringify({
model: 'claude-sonnet-4-20250514',
max_tokens: 1024,
stream: true,
messages: [{ role: 'user', content: 'Say "world" in one word.' }]
})
});
assert('流式响应 200', resp.ok, `status=${resp.status}`);
assert('Content-Type 是 SSE', resp.headers.get('content-type')?.includes('text/event-stream') ?? false);
const body = await resp.text();
const events = body.split('\n').filter(l => l.startsWith('event:'));
assert('有 SSE 事件', events.length > 0, `events=${events.length}`);
assert('包含 message_start', body.includes('message_start'));
assert('包含 message_stop', body.includes('message_stop'));
// 检查 stop_reason
const deltaMatch = body.match(/"stop_reason"\s*:\s*"([^"]+)"/);
if (deltaMatch) {
assert('流式 stop_reason 合法', ['end_turn', 'tool_use', 'max_tokens'].includes(deltaMatch[1]), `stop_reason=${deltaMatch[1]}`);
}
console.log(` 📝 SSE 事件数: ${events.length}`);
} catch (e: any) {
assert('流式请求', false, e.message);
}
// 总结
const passed = results.filter(r => r.passed).length;
const failed = results.filter(r => !r.passed).length;
console.log(`\n=== 端到端结果: ${passed} 通过, ${failed} 失败 ===\n`);
}
runTests().catch(console.error);