| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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}`); |
| } |
|
|
| |
| 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); |
| 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); |
| |
| |
| 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')); |
| |
| |
| 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); |
|
|