g2api / test /e2e-test.ts
LerinaOwO's picture
Upload 98 files
097fb32 verified
/**
* 端到端测试:向真实 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);