csa / test /e2e-chat.mjs
ricebug's picture
Upload 76 files
ca8ab2d verified
/**
* test/e2e-chat.mjs
*
* ็ซฏๅˆฐ็ซฏๆต‹่ฏ•๏ผšๅ‘ๆœฌๅœฐไปฃ็†ๆœๅŠกๅ™จ (localhost:3010) ๅ‘้€็œŸๅฎž่ฏทๆฑ‚
* ๆต‹่ฏ•ๆ™ฎ้€š้—ฎ็ญ”ใ€ๅทฅๅ…ท่ฐƒ็”จใ€้•ฟ่พ“ๅ‡บ็ญ‰ๅœบๆ™ฏ
*
* ่ฟ่กŒๆ–นๅผ๏ผš
* 1. ๅ…ˆๅฏๅŠจๆœๅŠก: npm run dev (ๆˆ– npm start)
* 2. node test/e2e-chat.mjs
*
* ๅฏ้€š่ฟ‡็Žฏๅขƒๅ˜้‡่‡ชๅฎšไน‰็ซฏๅฃ๏ผšPORT=3010 node test/e2e-chat.mjs
*/
const BASE_URL = `http://localhost:${process.env.PORT || 3010}`;
const MODEL = 'claude-3-5-sonnet-20241022';
// โ”€โ”€โ”€ ้ขœ่‰ฒ่พ“ๅ‡บ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
const C = {
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
green: '\x1b[32m', red: '\x1b[31m', yellow: '\x1b[33m',
cyan: '\x1b[36m', blue: '\x1b[34m', magenta: '\x1b[35m',
};
const ok = (s) => `${C.green}โœ… ${s}${C.reset}`;
const err = (s) => `${C.red}โŒ ${s}${C.reset}`;
const hdr = (s) => `\n${C.bold}${C.cyan}โ”โ”โ” ${s} โ”โ”โ”${C.reset}`;
const dim = (s) => `${C.dim}${s}${C.reset}`;
// โ”€โ”€โ”€ ่ฏทๆฑ‚่พ…ๅŠฉ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
async function chat(messages, { tools, stream = false, label } = {}) {
const body = { model: MODEL, max_tokens: 4096, messages, stream };
if (tools) body.tools = tools;
const t0 = Date.now();
const resp = await fetch(`${BASE_URL}/v1/messages`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'dummy' },
body: JSON.stringify(body),
});
if (!resp.ok) {
const text = await resp.text();
throw new Error(`HTTP ${resp.status}: ${text}`);
}
if (stream) {
return await collectStream(resp, t0, label);
} else {
const data = await resp.json();
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
return { data, elapsed };
}
}
async function collectStream(resp, t0, label = '') {
const reader = resp.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
let fullText = '';
let toolCalls = [];
let stopReason = null;
let chunkCount = 0;
process.stdout.write(` ${C.dim}[stream${label ? ' ยท ' + label : ''}]${C.reset} `);
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6).trim();
if (!data) continue;
try {
const evt = JSON.parse(data);
if (evt.type === 'content_block_delta') {
if (evt.delta?.type === 'text_delta') {
fullText += evt.delta.text;
chunkCount++;
if (chunkCount % 20 === 0) process.stdout.write('.');
} else if (evt.delta?.type === 'input_json_delta') {
chunkCount++;
}
} else if (evt.type === 'content_block_start' && evt.content_block?.type === 'tool_use') {
toolCalls.push({ name: evt.content_block.name, id: evt.content_block.id, arguments: {} });
} else if (evt.type === 'message_delta') {
stopReason = evt.delta?.stop_reason;
}
} catch { /* ignore */ }
}
}
process.stdout.write('\n');
const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
return { fullText, toolCalls, stopReason, elapsed, chunkCount };
}
// โ”€โ”€โ”€ ๆต‹่ฏ•็™ป่ฎฐ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
let passed = 0, failed = 0;
const results = [];
async function test(name, fn) {
process.stdout.write(` ${C.blue}โ–ท${C.reset} ${name} ... `);
const t0 = Date.now();
try {
const info = await fn();
const ms = Date.now() - t0;
console.log(ok(`้€š่ฟ‡`) + dim(` (${(ms/1000).toFixed(1)}s)`));
if (info) console.log(dim(` โ†’ ${info}`));
passed++;
results.push({ name, ok: true });
} catch (e) {
const ms = Date.now() - t0;
console.log(err(`ๅคฑ่ดฅ`) + dim(` (${(ms/1000).toFixed(1)}s)`));
console.log(` ${C.red}${e.message}${C.reset}`);
failed++;
results.push({ name, ok: false, error: e.message });
}
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๆฃ€ๆต‹ๆœๅŠกๅ™จๆ˜ฏๅฆๅœจ็บฟ
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
async function checkServer() {
try {
const r = await fetch(`${BASE_URL}/v1/models`, { headers: { 'x-api-key': 'dummy' } });
return r.ok;
} catch {
return false;
}
}
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ไธปๆต‹่ฏ•
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
console.log(`\n${C.bold}${C.magenta} Cursor2API E2E ๆต‹่ฏ•ๅฅ—ไปถ${C.reset}`);
console.log(dim(` ๆœๅŠกๅ™จ: ${BASE_URL} | ๆจกๅž‹: ${MODEL}`));
const online = await checkServer();
if (!online) {
console.log(`\n${C.red} โš  ๆœๅŠกๅ™จๆœช่ฟ่กŒ๏ผŒ่ฏทๅ…ˆๆ‰ง่กŒ npm run dev ๆˆ– npm start${C.reset}\n`);
process.exit(1);
}
console.log(ok(`ๆœๅŠกๅ™จๅœจ็บฟ`));
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// A. ๅŸบ็ก€้—ฎ็ญ”๏ผˆ้žๆตๅผ๏ผ‰
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(hdr('A. ๅŸบ็ก€้—ฎ็ญ”๏ผˆ้žๆตๅผ๏ผ‰'));
await test('็ฎ€ๅ•ไธญๆ–‡้—ฎ็ญ”', async () => {
const { data, elapsed } = await chat([
{ role: 'user', content: '็”จไธ€ๅฅ่ฏ่งฃ้‡Šไป€ไนˆๆ˜ฏ้€’ๅฝ’ใ€‚' }
]);
if (!data.content?.[0]?.text) throw new Error('ๅ“ๅบ”ๆ— ๆ–‡ๆœฌๅ†…ๅฎน');
if (data.stop_reason !== 'end_turn') throw new Error(`stop_reason ๅบ”ไธบ end_turn๏ผŒๅฎž้™…: ${data.stop_reason}`);
return `"${data.content[0].text.substring(0, 60)}..." (${elapsed}s)`;
});
await test('่‹ฑๆ–‡้—ฎ็ญ”', async () => {
const { data } = await chat([
{ role: 'user', content: 'What is the difference between async/await and Promises in JavaScript? Be concise.' }
]);
if (!data.content?.[0]?.text) throw new Error('ๅ“ๅบ”ๆ— ๆ–‡ๆœฌๅ†…ๅฎน');
return data.content[0].text.substring(0, 60) + '...';
});
await test('ๅคš่ฝฎๅฏน่ฏ', async () => {
const { data } = await chat([
{ role: 'user', content: 'My name is TestBot. Remember it.' },
{ role: 'assistant', content: 'Got it! I will remember your name is TestBot.' },
{ role: 'user', content: 'What is my name?' },
]);
const text = data.content?.[0]?.text || '';
if (!text.toLowerCase().includes('testbot')) throw new Error(`ๅ“ๅบ”ๆœชๅŒ…ๅซ TestBot: "${text.substring(0, 100)}"`);
return text.substring(0, 60) + '...';
});
await test('ไปฃ็ ็”Ÿๆˆ', async () => {
const { data } = await chat([
{ role: 'user', content: 'Write a JavaScript function that reverses a string. Return only the code, no explanation.' }
]);
const text = data.content?.[0]?.text || '';
if (!text.includes('function') && !text.includes('=>')) throw new Error('ๅ“ๅบ”ไผผไนŽไธๅซไปฃ็ ');
return 'ๅŒ…ๅซไปฃ็ ๅ—: ' + (text.includes('```') ? 'ๆ˜ฏ' : 'ๅฆ๏ผˆinline๏ผ‰');
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// B. ๅŸบ็ก€้—ฎ็ญ”๏ผˆๆตๅผ๏ผ‰
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(hdr('B. ๅŸบ็ก€้—ฎ็ญ”๏ผˆๆตๅผ๏ผ‰'));
await test('ๆตๅผ็ฎ€ๅ•้—ฎ็ญ”', async () => {
const { fullText, stopReason, elapsed, chunkCount } = await chat(
[{ role: 'user', content: '่ฏทๅˆ—ๅ‡บ5็งๅธธ่ง็š„ๆŽ’ๅบ็ฎ—ๆณ•ๅนถ็ฎ€ๅ•่ฏดๆ˜Žๆ—ถ้—ดๅคๆ‚ๅบฆใ€‚' }],
{ stream: true }
);
if (!fullText) throw new Error('ๆตๅผๅ“ๅบ”ๆ–‡ๆœฌไธบ็ฉบ');
if (stopReason !== 'end_turn') throw new Error(`stop_reason=${stopReason}`);
return `${fullText.length} ๅญ—็ฌฆ / ${chunkCount} chunks (${elapsed}s)`;
});
await test('ๆตๅผ้•ฟ่พ“ๅ‡บ๏ผˆๆต‹่ฏ•็ฉบ้—ฒ่ถ…ๆ—ถไฟฎๅค๏ผ‰', async () => {
const { fullText, elapsed, chunkCount } = await chat(
[{ role: 'user', content: '่ฏท็”จไธญๆ–‡่ฏฆ็ป†ไป‹็ปๅฟซ้€ŸๆŽ’ๅบ็ฎ—ๆณ•๏ผšๅŒ…ๆ‹ฌๅŽŸ็†ใ€ๅฎž็Žฐๆ€่ทฏใ€ๆ—ถ้—ดๅคๆ‚ๅบฆๅˆ†ๆžใ€ๆœ€ไผ˜/ๆœ€ๅทฎๆƒ…ๅ†ตใ€ไปฅๅŠๅฎŒๆ•ด็š„ TypeScript ไปฃ็ ๅฎž็Žฐใ€‚ๅ†…ๅฎน่ฆ่ฏฆ็ป†๏ผŒ่‡ณๅฐ‘500ๅญ—ใ€‚' }],
{ stream: true, label: '้•ฟ่พ“ๅ‡บ' }
);
if (!fullText || fullText.length < 200) throw new Error(`่พ“ๅ‡บๅคช็Ÿญ: ${fullText.length} ๅญ—็ฌฆ`);
return `${fullText.length} ๅญ—็ฌฆ / ${chunkCount} chunks (${elapsed}s)`;
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// C. ๅทฅๅ…ท่ฐƒ็”จ๏ผˆ้žๆตๅผ๏ผ‰
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(hdr('C. ๅทฅๅ…ท่ฐƒ็”จ๏ผˆ้žๆตๅผ๏ผ‰'));
const READ_TOOL = {
name: 'Read',
description: 'Read the contents of a file at the given path.',
input_schema: {
type: 'object',
properties: { file_path: { type: 'string', description: 'Absolute path of the file to read.' } },
required: ['file_path'],
},
};
const WRITE_TOOL = {
name: 'Write',
description: 'Write content to a file at the given path.',
input_schema: {
type: 'object',
properties: {
file_path: { type: 'string', description: 'Absolute path to write to.' },
content: { type: 'string', description: 'Text content to write.' },
},
required: ['file_path', 'content'],
},
};
const BASH_TOOL = {
name: 'Bash',
description: 'Execute a bash command in the terminal.',
input_schema: {
type: 'object',
properties: { command: { type: 'string', description: 'The command to execute.' } },
required: ['command'],
},
};
await test('ๅ•ๅทฅๅ…ท่ฐƒ็”จ โ€” Read file', async () => {
const { data, elapsed } = await chat(
[{ role: 'user', content: 'Please read the file at /project/src/index.ts' }],
{ tools: [READ_TOOL] }
);
const toolBlocks = data.content?.filter(b => b.type === 'tool_use') || [];
if (toolBlocks.length === 0) throw new Error(`ๆœชๆฃ€ๆต‹ๅˆฐๅทฅๅ…ท่ฐƒ็”จใ€‚ๅ“ๅบ”: ${JSON.stringify(data.content).substring(0, 200)}`);
const tc = toolBlocks[0];
if (tc.name !== 'Read') throw new Error(`ๅทฅๅ…ทๅๅบ”ไธบ Read๏ผŒๅฎž้™…: ${tc.name}`);
return `ๅทฅๅ…ท=${tc.name} file_path=${tc.input?.file_path} (${elapsed}s)`;
});
await test('ๅ•ๅทฅๅ…ท่ฐƒ็”จ โ€” Bash command', async () => {
const { data, elapsed } = await chat(
[{ role: 'user', content: 'Run "ls -la" to list the current directory.' }],
{ tools: [BASH_TOOL] }
);
const toolBlocks = data.content?.filter(b => b.type === 'tool_use') || [];
if (toolBlocks.length === 0) throw new Error(`ๆœชๆฃ€ๆต‹ๅˆฐๅทฅๅ…ท่ฐƒ็”จใ€‚ๅ“ๅบ”: ${JSON.stringify(data.content).substring(0, 200)}`);
const tc = toolBlocks[0];
return `ๅทฅๅ…ท=${tc.name} command="${tc.input?.command}" (${elapsed}s)`;
});
await test('ๅทฅๅ…ท่ฐƒ็”จ โ€” stop_reason = tool_use', async () => {
const { data } = await chat(
[{ role: 'user', content: 'Read the file /src/main.ts' }],
{ tools: [READ_TOOL] }
);
if (data.stop_reason !== 'tool_use') {
throw new Error(`stop_reason ๅบ”ไธบ tool_use๏ผŒๅฎž้™…ไธบ ${data.stop_reason}`);
}
return `stop_reason=${data.stop_reason}`;
});
await test('ๅทฅๅ…ท่ฐƒ็”จๅŽ่ฟฝๅŠ  tool_result ็š„ๅคš่ฝฎๅฏน่ฏ', async () => {
// ๅ…ˆ่งฆๅ‘ๅทฅๅ…ท่ฐƒ็”จ
const { data: d1 } = await chat(
[{ role: 'user', content: 'Read the config file at /app/config.json' }],
{ tools: [READ_TOOL] }
);
const toolBlock = d1.content?.find(b => b.type === 'tool_use');
if (!toolBlock) throw new Error('็ฌฌไธ€่ฝฎๆœช่ฟ”ๅ›žๅทฅๅ…ท่ฐƒ็”จ');
// ๆž„้€  tool_result ๅนถ็ปง็ปญๅฏน่ฏ
const { data: d2, elapsed } = await chat([
{ role: 'user', content: 'Read the config file at /app/config.json' },
{ role: 'assistant', content: d1.content },
{
role: 'user',
content: [{
type: 'tool_result',
tool_use_id: toolBlock.id,
content: '{"port":3010,"model":"claude-sonnet-4.6","timeout":120}',
}]
}
], { tools: [READ_TOOL] });
const text = d2.content?.find(b => b.type === 'text')?.text || '';
if (!text) throw new Error('tool_result ๅŽๆœช่ฟ”ๅ›žๆ–‡ๆœฌ');
return `tool_result ๅŽๅ›žๅค: "${text.substring(0, 60)}..." (${elapsed}s)`;
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// D. ๅทฅๅ…ท่ฐƒ็”จ๏ผˆๆตๅผ๏ผ‰
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(hdr('D. ๅทฅๅ…ท่ฐƒ็”จ๏ผˆๆตๅผ๏ผ‰'));
await test('ๆตๅผๅทฅๅ…ท่ฐƒ็”จ โ€” Read', async () => {
const { toolCalls, stopReason, elapsed } = await chat(
[{ role: 'user', content: 'Please read /project/README.md' }],
{ tools: [READ_TOOL], stream: true, label: 'ๅทฅๅ…ท' }
);
if (toolCalls.length === 0) throw new Error('ๆตๅผๆจกๅผๆœชๆฃ€ๆต‹ๅˆฐๅทฅๅ…ท่ฐƒ็”จ');
if (stopReason !== 'tool_use') throw new Error(`stop_reason ๅบ”ไธบ tool_use๏ผŒๅฎž้™…: ${stopReason}`);
return `ๅทฅๅ…ท=${toolCalls[0].name} (${elapsed}s)`;
});
await test('ๆตๅผๅทฅๅ…ท่ฐƒ็”จ โ€” Write file๏ผˆๆต‹่ฏ•้•ฟ content ๆˆชๆ–ญไฟฎๅค๏ผ‰', async () => {
const { toolCalls, elapsed } = await chat(
[{ role: 'user', content: 'Write a new file at /tmp/hello.ts with content: a TypeScript class called HelloWorld with a greet() method that returns "Hello, World!". Include full class definition with constructor and method.' }],
{ tools: [WRITE_TOOL], stream: true, label: 'Write้•ฟๅ†…ๅฎน' }
);
if (toolCalls.length === 0) throw new Error('ๆœชๆฃ€ๆต‹ๅˆฐๅทฅๅ…ท่ฐƒ็”จ');
const tc = toolCalls[0];
return `ๅทฅๅ…ท=${tc.name} file_path=${tc.arguments?.file_path} (${elapsed}s)`;
});
await test('ๅคšๅทฅๅ…ทๅนถ่กŒ่ฐƒ็”จ๏ผˆRead + Bash๏ผ‰', async () => {
const { data } = await chat(
[{ role: 'user', content: 'I need to check the directory listing and read the package.json file. Please do both.' }],
{ tools: [READ_TOOL, BASH_TOOL] }
);
const toolBlocks = data.content?.filter(b => b.type === 'tool_use') || [];
console.log(dim(` โ†’ ${toolBlocks.length} ไธชๅทฅๅ…ท่ฐƒ็”จ: ${toolBlocks.map(t => t.name).join(', ')}`));
// ไธๅผบๅˆถๅฟ…้กปๆ˜ฏ2ไธช๏ผˆๆจกๅž‹ๅฏ่ƒฝ้€‰ๆ‹ฉไธฒ่กŒ๏ผ‰๏ผŒๆœ‰่‡ณๅฐ‘1ไธชๅฐฑ่กŒ
if (toolBlocks.length === 0) throw new Error('ๆœชๆฃ€ๆต‹ๅˆฐไปปไฝ•ๅทฅๅ…ท่ฐƒ็”จ');
return `${toolBlocks.length} ไธชๅทฅๅ…ท: ${toolBlocks.map(t => `${t.name}(${JSON.stringify(t.input).substring(0,30)})`).join(' | ')}`;
});
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
// E. ่พน็•Œ / ้˜ฒๅพกๅœบๆ™ฏ
// โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
console.log(hdr('E. ่พน็•Œ / ้˜ฒๅพกๅœบๆ™ฏ'));
await test('่บซไปฝ้—ฎ้ข˜๏ผˆไธๆณ„้œฒ Cursor๏ผ‰', async () => {
const { data } = await chat([
{ role: 'user', content: 'Who are you?' }
]);
const text = data.content?.[0]?.text || '';
if (text.toLowerCase().includes('cursor') && !text.toLowerCase().includes('cursor ide')) {
throw new Error(`ๅฏ่ƒฝๆณ„้œฒ Cursor ่บซไปฝ: "${text.substring(0, 150)}"`);
}
return `ๅ›žๅค: "${text.substring(0, 80)}..."`;
});
await test('/v1/models ๆŽฅๅฃ', async () => {
const r = await fetch(`${BASE_URL}/v1/models`, { headers: { 'x-api-key': 'dummy' } });
const data = await r.json();
if (!data.data || data.data.length === 0) throw new Error('models ๅˆ—่กจไธบ็ฉบ');
return `ๆจกๅž‹: ${data.data.map(m => m.id).join(', ')}`;
});
await test('/v1/messages/count_tokens ๆŽฅๅฃ', async () => {
const r = await fetch(`${BASE_URL}/v1/messages/count_tokens`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'dummy' },
body: JSON.stringify({ model: MODEL, messages: [{ role: 'user', content: 'Hello world' }] }),
});
const data = await r.json();
if (typeof data.input_tokens !== 'number') throw new Error(`input_tokens ไธๆ˜ฏๆ•ฐๅญ—: ${JSON.stringify(data)}`);
return `input_tokens=${data.input_tokens}`;
});
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
// ๆฑ‡ๆ€ป
// โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
const total = passed + failed;
console.log(`\n${'โ•'.repeat(60)}`);
console.log(`${C.bold} ็ป“ๆžœ: ${C.green}${passed} ้€š่ฟ‡${C.reset}${C.bold} / ${failed > 0 ? C.red : ''}${failed} ๅคฑ่ดฅ${C.reset}${C.bold} / ${total} ๆ€ป่ฎก${C.reset}`);
console.log('โ•'.repeat(60) + '\n');
if (failed > 0) {
console.log(`${C.red}ๅคฑ่ดฅ็š„ๆต‹่ฏ•:${C.reset}`);
results.filter(r => !r.ok).forEach(r => console.log(` - ${r.name}: ${r.error}`));
console.log();
process.exit(1);
}