| |
| |
| |
| |
| |
| |
| |
| |
|
|
| const SERVER_PORT = 19876 |
| const BASE_URL = `http://127.0.0.1:${SERVER_PORT}` |
| const WS_URL = `ws://127.0.0.1:${SERVER_PORT}` |
|
|
| |
| function uuid(): string { |
| return crypto.randomUUID() |
| } |
|
|
| async function sleep(ms: number) { |
| return new Promise((resolve) => setTimeout(resolve, ms)) |
| } |
|
|
| |
|
|
| type ServerMsg = { |
| type: string |
| [key: string]: any |
| } |
|
|
| function createWebSocket(sessionId: string): Promise<{ |
| ws: WebSocket |
| messages: ServerMsg[] |
| waitForType: (type: string, timeoutMs?: number) => Promise<ServerMsg> |
| waitForAny: (types: string[], timeoutMs?: number) => Promise<ServerMsg> |
| close: () => void |
| }> { |
| return new Promise((resolve, reject) => { |
| const messages: ServerMsg[] = [] |
| const waiters: Array<{ |
| types: string[] |
| resolve: (msg: ServerMsg) => void |
| reject: (err: Error) => void |
| }> = [] |
|
|
| const ws = new WebSocket(`${WS_URL}/ws/${sessionId}`) |
|
|
| ws.onmessage = (event) => { |
| try { |
| const msg = JSON.parse(event.data as string) as ServerMsg |
| messages.push(msg) |
| |
| for (let i = waiters.length - 1; i >= 0; i--) { |
| if (waiters[i].types.includes(msg.type)) { |
| waiters[i].resolve(msg) |
| waiters.splice(i, 1) |
| } |
| } |
| } catch (e) { |
| console.error('Failed to parse WS message:', event.data) |
| } |
| } |
|
|
| ws.onerror = (event) => { |
| reject(new Error(`WebSocket error`)) |
| } |
|
|
| ws.onopen = () => { |
| resolve({ |
| ws, |
| messages, |
| waitForType(type: string, timeoutMs = 60000) { |
| |
| const existing = messages.find((m) => m.type === type) |
| if (existing) return Promise.resolve(existing) |
|
|
| return new Promise((res, rej) => { |
| const timer = setTimeout(() => { |
| rej( |
| new Error( |
| `Timeout waiting for message type "${type}" after ${timeoutMs}ms. Got: ${messages.map((m) => m.type).join(', ')}` |
| ) |
| ) |
| }, timeoutMs) |
| waiters.push({ |
| types: [type], |
| resolve: (msg) => { |
| clearTimeout(timer) |
| res(msg) |
| }, |
| reject: rej, |
| }) |
| }) |
| }, |
| waitForAny(types: string[], timeoutMs = 60000) { |
| const existing = messages.find((m) => types.includes(m.type)) |
| if (existing) return Promise.resolve(existing) |
|
|
| return new Promise((res, rej) => { |
| const timer = setTimeout(() => { |
| rej( |
| new Error( |
| `Timeout waiting for any of [${types.join(', ')}] after ${timeoutMs}ms. Got: ${messages.map((m) => m.type).join(', ')}` |
| ) |
| ) |
| }, timeoutMs) |
| waiters.push({ |
| types, |
| resolve: (msg) => { |
| clearTimeout(timer) |
| res(msg) |
| }, |
| reject: rej, |
| }) |
| }) |
| }, |
| close() { |
| ws.close() |
| }, |
| }) |
| } |
| }) |
| } |
|
|
| |
|
|
| let server: ReturnType<typeof import('../index.js').startServer> | null = null |
|
|
| async function startTestServer() { |
| const { startServer } = await import('../index.js') |
| server = startServer(SERVER_PORT, '127.0.0.1') |
| await sleep(500) |
| console.log(`\n✅ Server started on port ${SERVER_PORT}`) |
| } |
|
|
| async function stopTestServer() { |
| if (server) { |
| server.stop(true) |
| server = null |
| await sleep(200) |
| console.log('✅ Server stopped') |
| } |
| } |
|
|
| |
|
|
| async function testHealthCheck() { |
| console.log('\n── Test 1: Health Check ──') |
| const res = await fetch(`${BASE_URL}/health`) |
| const body = await res.json() |
| if (res.status !== 200 || body.status !== 'ok') { |
| throw new Error(`Health check failed: ${JSON.stringify(body)}`) |
| } |
| console.log('✅ Health check passed') |
| } |
|
|
| |
|
|
| async function testSessionsApi() { |
| console.log('\n── Test 2: Sessions API ──') |
| const res = await fetch(`${BASE_URL}/api/sessions`) |
| if (res.status !== 200) { |
| throw new Error(`Sessions API failed: ${res.status}`) |
| } |
| const body = await res.json() |
| console.log(`✅ Sessions API returned ${body.sessions?.length ?? 0} sessions`) |
| } |
|
|
| |
|
|
| async function testSettingsApi() { |
| console.log('\n── Test 3: Settings API ──') |
| const res = await fetch(`${BASE_URL}/api/settings`) |
| if (res.status !== 200) { |
| throw new Error(`Settings API failed: ${res.status}`) |
| } |
| const body = await res.json() |
| console.log(`✅ Settings API returned:`, Object.keys(body.settings || body)) |
| } |
|
|
| |
|
|
| async function testModelsApi() { |
| console.log('\n── Test 4: Models API ──') |
| const res = await fetch(`${BASE_URL}/api/models`) |
| if (res.status !== 200) { |
| throw new Error(`Models API failed: ${res.status}`) |
| } |
| const body = await res.json() |
| console.log(`✅ Models API returned:`, body) |
| } |
|
|
| |
|
|
| async function testWebSocketConnect() { |
| console.log('\n── Test 5: WebSocket Connect ──') |
| const sessionId = uuid() |
| const client = await createWebSocket(sessionId) |
| const connMsg = await client.waitForType('connected', 5000) |
| if (connMsg.sessionId !== sessionId) { |
| throw new Error(`Session ID mismatch: ${connMsg.sessionId} !== ${sessionId}`) |
| } |
| client.close() |
| await sleep(200) |
| console.log('✅ WebSocket connected and received session ID') |
| } |
|
|
| |
|
|
| async function testWebSocketPing() { |
| console.log('\n── Test 6: WebSocket Ping/Pong ──') |
| const sessionId = uuid() |
| const client = await createWebSocket(sessionId) |
| await client.waitForType('connected', 5000) |
|
|
| client.ws.send(JSON.stringify({ type: 'ping' })) |
| const pong = await client.waitForType('pong', 5000) |
| if (pong.type !== 'pong') { |
| throw new Error('Pong not received') |
| } |
| client.close() |
| await sleep(200) |
| console.log('✅ Ping/Pong works') |
| } |
|
|
| |
|
|
| async function testRealLLMChat() { |
| console.log('\n── Test 7: Real LLM Chat (MiniMax API) ──') |
| console.log(' This will spawn a CLI subprocess and call the real API...') |
|
|
| const sessionId = uuid() |
| const client = await createWebSocket(sessionId) |
| await client.waitForType('connected', 5000) |
| console.log(` Session: ${sessionId}`) |
|
|
| |
| client.ws.send( |
| JSON.stringify({ |
| type: 'user_message', |
| content: 'Say "hello world" and nothing else. Keep it short.', |
| }) |
| ) |
|
|
| console.log(' Message sent, waiting for response...') |
|
|
| |
| const statusMsg = await client.waitForType('status', 10000) |
| console.log(` Got status: ${statusMsg.state} ${statusMsg.verb || ''}`) |
|
|
| |
| const responseMsg = await client.waitForAny( |
| ['content_delta', 'content_start', 'error', 'message_complete'], |
| 120000 |
| ) |
|
|
| console.log(` Got response type: ${responseMsg.type}`) |
|
|
| if (responseMsg.type === 'error') { |
| console.log(` ❌ Error: ${responseMsg.message} (code: ${responseMsg.code})`) |
| throw new Error(`LLM returned error: ${responseMsg.message}`) |
| } |
|
|
| |
| let fullText = '' |
| if (responseMsg.type === 'content_delta' && responseMsg.text) { |
| fullText += responseMsg.text |
| } |
|
|
| |
| try { |
| const complete = await client.waitForType('message_complete', 120000) |
| console.log(` Usage: input=${complete.usage?.input_tokens}, output=${complete.usage?.output_tokens}`) |
| } catch { |
| console.log(' Warning: message_complete not received within timeout') |
| } |
|
|
| |
| for (const msg of client.messages) { |
| if (msg.type === 'content_delta' && msg.text) { |
| if (!fullText.includes(msg.text)) { |
| fullText += msg.text |
| } |
| } |
| } |
|
|
| console.log(` Response text: "${fullText.substring(0, 200)}"`) |
|
|
| if (fullText.length === 0) { |
| |
| console.log(' All messages received:') |
| for (const msg of client.messages) { |
| console.log(` ${msg.type}: ${JSON.stringify(msg).substring(0, 150)}`) |
| } |
| throw new Error('No content received from LLM') |
| } |
|
|
| client.close() |
| await sleep(500) |
| console.log('✅ Real LLM chat works! Got response from MiniMax API') |
| } |
|
|
| |
|
|
| async function testScheduledTasks() { |
| console.log('\n── Test 8: Scheduled Tasks CRUD ──') |
|
|
| |
| const createRes = await fetch(`${BASE_URL}/api/scheduled-tasks`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ |
| name: 'Test Task', |
| prompt: 'Test prompt', |
| cron: '0 9 * * *', |
| }), |
| }) |
| if (createRes.status !== 201) { |
| const body = await createRes.text() |
| throw new Error(`Create scheduled task failed: ${createRes.status} ${body}`) |
| } |
| const { task } = await createRes.json() |
| console.log(` Created task: ${task.id}`) |
|
|
| |
| const listRes = await fetch(`${BASE_URL}/api/scheduled-tasks`) |
| const listBody = await listRes.json() |
| const found = listBody.tasks?.find((t: any) => t.id === task.id) |
| if (!found) throw new Error('Created task not found in list') |
| console.log(` Listed ${listBody.tasks.length} tasks`) |
|
|
| |
| const updateRes = await fetch(`${BASE_URL}/api/scheduled-tasks/${task.id}`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ name: 'Updated Task' }), |
| }) |
| if (updateRes.status !== 200) throw new Error(`Update failed: ${updateRes.status}`) |
|
|
| |
| const deleteRes = await fetch(`${BASE_URL}/api/scheduled-tasks/${task.id}`, { |
| method: 'DELETE', |
| }) |
| if (deleteRes.status !== 200) throw new Error(`Delete failed: ${deleteRes.status}`) |
|
|
| console.log('✅ Scheduled Tasks CRUD works') |
| } |
|
|
| |
|
|
| async function testSettingsReadWrite() { |
| console.log('\n── Test 9: Settings Read/Write ──') |
|
|
| |
| const readRes = await fetch(`${BASE_URL}/api/settings`) |
| const original = await readRes.json() |
|
|
| |
| const updateRes = await fetch(`${BASE_URL}/api/settings/user`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ testKey_integration: true }), |
| }) |
| if (updateRes.status !== 200) throw new Error(`Settings update failed: ${updateRes.status}`) |
|
|
| |
| const readBack = await fetch(`${BASE_URL}/api/settings/user`) |
| const updated = await readBack.json() |
|
|
| |
| const cleanSettings = { ...updated } |
| delete cleanSettings.testKey_integration |
| await fetch(`${BASE_URL}/api/settings/user`, { |
| method: 'PUT', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify(cleanSettings), |
| }) |
|
|
| console.log('✅ Settings read/write works') |
| } |
|
|
| |
|
|
| async function testPermissionMode() { |
| console.log('\n── Test 10: Permission Mode ──') |
| const res = await fetch(`${BASE_URL}/api/permissions/mode`) |
| if (res.status !== 200) throw new Error(`Permissions failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Current mode: ${body.mode || body.permissionMode || JSON.stringify(body)}`) |
| console.log('✅ Permission mode API works') |
| } |
|
|
| |
|
|
| async function testSearchApi() { |
| console.log('\n── Test 11: Search API ──') |
| const res = await fetch(`${BASE_URL}/api/search/sessions`, { |
| method: 'POST', |
| headers: { 'Content-Type': 'application/json' }, |
| body: JSON.stringify({ query: 'test' }), |
| }) |
| if (res.status !== 200) throw new Error(`Search failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Search results: ${body.results?.length ?? 0}`) |
| console.log('✅ Search API works') |
| } |
|
|
| |
|
|
| async function testAgentsApi() { |
| console.log('\n── Test 12: Agents API ──') |
| const res = await fetch(`${BASE_URL}/api/agents`) |
| if (res.status !== 200) throw new Error(`Agents failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Active agents: ${body.activeAgents?.length ?? body.agents?.length ?? 0}`) |
| console.log('✅ Agents API works') |
| } |
|
|
| |
|
|
| async function testTeamsApi() { |
| console.log('\n── Test 13: Teams API ──') |
| const res = await fetch(`${BASE_URL}/api/teams`) |
| if (res.status !== 200) throw new Error(`Teams failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Teams: ${body.teams?.length ?? 0}`) |
| console.log('✅ Teams API works') |
| } |
|
|
| |
|
|
| async function testTasksApi() { |
| console.log('\n── Test 14: Tasks API ──') |
| const res = await fetch(`${BASE_URL}/api/tasks`) |
| if (res.status !== 200) throw new Error(`Tasks failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Tasks: ${body.tasks?.length ?? 0}`) |
| console.log('✅ Tasks API works') |
| } |
|
|
| |
|
|
| async function testStatusApi() { |
| console.log('\n── Test 15: Status API ──') |
| const res = await fetch(`${BASE_URL}/api/status`) |
| if (res.status !== 200) throw new Error(`Status failed: ${res.status}`) |
| const body = await res.json() |
| console.log(` Status: ${body.status || JSON.stringify(body).substring(0, 100)}`) |
| console.log('✅ Status API works') |
| } |
|
|
| |
|
|
| async function main() { |
| console.log('╔══════════════════════════════════════════════════════════╗') |
| console.log('║ Real LLM Integration Test — MiniMax API via CLI ║') |
| console.log('╚══════════════════════════════════════════════════════════╝') |
|
|
| const failures: string[] = [] |
|
|
| try { |
| await startTestServer() |
|
|
| |
| const restTests = [ |
| testHealthCheck, |
| testSessionsApi, |
| testSettingsApi, |
| testModelsApi, |
| testScheduledTasks, |
| testSettingsReadWrite, |
| testPermissionMode, |
| testSearchApi, |
| testAgentsApi, |
| testTeamsApi, |
| testTasksApi, |
| testStatusApi, |
| ] |
|
|
| for (const test of restTests) { |
| try { |
| await test() |
| } catch (err: any) { |
| console.log(`❌ ${test.name} FAILED: ${err.message}`) |
| failures.push(`${test.name}: ${err.message}`) |
| } |
| } |
|
|
| |
| const wsTests = [testWebSocketConnect, testWebSocketPing] |
|
|
| for (const test of wsTests) { |
| try { |
| await test() |
| } catch (err: any) { |
| console.log(`❌ ${test.name} FAILED: ${err.message}`) |
| failures.push(`${test.name}: ${err.message}`) |
| } |
| } |
|
|
| |
| try { |
| await testRealLLMChat() |
| } catch (err: any) { |
| console.log(`❌ testRealLLMChat FAILED: ${err.message}`) |
| failures.push(`testRealLLMChat: ${err.message}`) |
| } |
| } finally { |
| await stopTestServer() |
| } |
|
|
| |
| console.log('\n' + '═'.repeat(60)) |
| if (failures.length === 0) { |
| console.log('🎉 ALL TESTS PASSED!') |
| } else { |
| console.log(`⚠️ ${failures.length} TEST(S) FAILED:`) |
| for (const f of failures) { |
| console.log(` ❌ ${f}`) |
| } |
| } |
| console.log('═'.repeat(60)) |
|
|
| process.exit(failures.length > 0 ? 1 : 0) |
| } |
|
|
| main() |
|
|