#!/usr/bin/env node /** * ZCLAW Backend API Connection Test Script * * Tests all API endpoints used by the ZCLAW desktop client against * the ZCLAW Kernel backend. * * Usage: * node desktop/scripts/test-api-connection.mjs [options] * * Options: * --url=URL Base URL for ZCLAW API (default: http://127.0.0.1:50051) * --verbose Show detailed output * --json Output results as JSON * --timeout=MS Request timeout in milliseconds (default: 5000) */ import { WebSocket } from 'ws'; // Configuration const DEFAULT_BASE_URL = 'http://127.0.0.1:50051'; const DEFAULT_TIMEOUT = 5000; // Parse command line arguments const args = process.argv.slice(2); const config = { baseUrl: DEFAULT_BASE_URL, verbose: false, json: false, timeout: DEFAULT_TIMEOUT, }; for (const arg of args) { if (arg.startsWith('--url=')) { config.baseUrl = arg.slice(6); } else if (arg === '--verbose' || arg === '-v') { config.verbose = true; } else if (arg === '--json') { config.json = true; } else if (arg.startsWith('--timeout=')) { config.timeout = parseInt(arg.slice(10), 10); } else if (arg === '--help' || arg === '-h') { console.log(` ZCLAW API Connection Tester Usage: node test-api-connection.mjs [options] Options: --url=URL Base URL for ZCLAW API (default: ${DEFAULT_BASE_URL}) --verbose Show detailed output including response bodies --json Output results as JSON for programmatic processing --timeout=MS Request timeout in milliseconds (default: ${DEFAULT_TIMEOUT}) --help, -h Show this help message `); process.exit(0); } } // Test result tracking const results = { timestamp: new Date().toISOString(), baseUrl: config.baseUrl, summary: { total: 0, passed: 0, failed: 0, skipped: 0, }, categories: {}, errors: [], }; /** * Make an HTTP request with timeout */ async function makeRequest(method, path, body = null, expectedStatus = null) { const url = `${config.baseUrl}${path}`; const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), config.timeout); const startTime = Date.now(); try { const fetchOptions = { method, signal: controller.signal, headers: { 'Content-Type': 'application/json', }, }; if (body && (method === 'POST' || method === 'PUT')) { fetchOptions.body = JSON.stringify(body); } const response = await fetch(url, fetchOptions); clearTimeout(timeoutId); const duration = Date.now() - startTime; let responseBody = null; try { responseBody = await response.json(); } catch { responseBody = await response.text().catch(() => null); } const statusMatch = expectedStatus ? response.status === expectedStatus : response.status < 500; return { success: statusMatch, status: response.status, statusText: response.statusText, duration, body: responseBody, error: null, }; } catch (error) { clearTimeout(timeoutId); const duration = Date.now() - startTime; return { success: false, status: 0, statusText: 'Network Error', duration, body: null, error: error.name === 'AbortError' ? 'Timeout' : error.message, }; } } /** * Test a single endpoint and record results */ async function testEndpoint(category, method, path, testOptions = {}) { const { body = null, expectedStatus = null, description = '' } = testOptions; results.summary.total++; if (!results.categories[category]) { results.categories[category] = { total: 0, passed: 0, failed: 0, tests: [] }; } results.categories[category].total++; const result = await makeRequest(method, path, body, expectedStatus); const testResult = { method, path, description, status: result.status, statusText: result.statusText, duration: result.duration, success: result.success, error: result.error, }; if (config.verbose && result.body) { testResult.responseBody = result.body; } results.categories[category].tests.push(testResult); if (result.success) { results.summary.passed++; results.categories[category].passed++; } else { results.summary.failed++; results.categories[category].failed++; if (result.error) { results.errors.push({ category, method, path, error: result.error, status: result.status, }); } } // Print progress if (!config.json) { const statusIcon = result.success ? '[PASS]' : '[FAIL]'; const statusColor = result.success ? '\x1b[32m' : '\x1b[31m'; const reset = '\x1b[0m'; const dim = '\x1b[2m'; console.log( `${statusColor}${statusIcon}${reset} ${method.padEnd(6)} ${path} ${dim}${result.status} ${result.duration}ms${reset}` ); if (config.verbose && result.body) { const bodyStr = JSON.stringify(result.body, null, 2); const indented = bodyStr.split('\n').join('\n '); console.log(` Response: ${indented}`); } if (!result.success && result.error) { console.log(` Error: ${result.error}`); } } return result; } /** * Test WebSocket connection */ async function testWebSocketConnection(url) { return new Promise((resolve) => { const startTime = Date.now(); let resolved = false; const ws = new WebSocket(url); const timeout = setTimeout(() => { if (!resolved) { resolved = true; ws.close(); resolve({ method: 'CONNECT', path: '/ws', description: 'WebSocket connection test', status: 0, statusText: 'Timeout', duration: Date.now() - startTime, success: false, error: 'Connection timeout', }); } }, config.timeout); ws.on('open', () => { if (!resolved) { resolved = true; clearTimeout(timeout); ws.close(); resolve({ method: 'CONNECT', path: '/ws', description: 'WebSocket connection test', status: 101, statusText: 'Switching Protocols', duration: Date.now() - startTime, success: true, error: null, }); } }); ws.on('error', (error) => { if (!resolved) { resolved = true; clearTimeout(timeout); resolve({ method: 'CONNECT', path: '/ws', description: 'WebSocket connection test', status: 0, statusText: 'Connection Failed', duration: Date.now() - startTime, success: false, error: error.message, }); } }); }); } /** * Print test summary */ function printSummary() { console.log(`\n`); console.log(`=== Test Summary ===`); console.log(`\n`); const categories = Object.entries(results.categories); const maxCategoryLen = Math.max(...categories.map(([name]) => name.length)); for (const [name, data] of categories) { const passRate = data.total > 0 ? ((data.passed / data.total) * 100).toFixed(0) : '0'; const statusIcon = data.failed === 0 ? '\x1b[32m' : '\x1b[31m'; const reset = '\x1b[0m'; console.log( `${statusIcon}${name.padEnd(maxCategoryLen + 1)}${reset} ` + `${data.passed}/${data.total} passed (${passRate}%)` ); } console.log(`\n`); const totalPassRate = results.summary.total > 0 ? ((results.summary.passed / results.summary.total) * 100).toFixed(0) : '0'; console.log( `Total: ${results.summary.passed}/${results.summary.total} passed ` + `(${totalPassRate}%)` ); if (results.errors.length > 0) { console.log(`\n\x1b[31m=== Errors ===\x1b[0m\n`); for (const error of results.errors) { console.log(` [${error.category}] ${error.method} ${error.path}`); console.log(` Status: ${error.status || 'N/A'}`); console.log(` Error: ${error.error}`); console.log(); } } // JSON output if requested if (config.json) { console.log(`\n=== JSON Output ===\n`); console.log(JSON.stringify(results, null, 2)); } // Exit with appropriate code process.exit(results.summary.failed > 0 ? 1 : 0); } /** * Run all API tests */ async function runAllTests() { console.log(`\n=== ZCLAW API Connection Test ===`); console.log(`Base URL: ${config.baseUrl}`); console.log(`Timeout: ${config.timeout}ms`); console.log(`\n`); // ========================================= // Health & System Endpoints // ========================================= console.log(`\n--- Health & System ---`); await testEndpoint('System', 'GET', '/api/health', { expectedStatus: 200, description: 'Health check endpoint', }); await testEndpoint('System', 'GET', '/api/version', { description: 'Version information', }); // ========================================= // Agent Endpoints // ========================================= console.log(`\n--- Agent API ---`); await testEndpoint('Agents', 'GET', '/api/agents', { expectedStatus: 200, description: 'List all agents', }); // Test agent creation (will fail if not authenticated) await testEndpoint('Agents', 'POST', '/api/agents', { body: { name: 'test-agent', role: 'assistant', }, description: 'Create agent (expect 401 without auth)', }); // ========================================= // Team API Endpoints // ========================================= console.log(`\n--- Team API ---`); await testEndpoint('Teams', 'GET', '/api/teams', { description: 'List all teams', }); await testEndpoint('Teams', 'POST', '/api/teams', { body: { name: 'Test Team', pattern: 'sequential', memberAgents: [], }, description: 'Create team (expect 401 without auth)', }); await testEndpoint('Teams', 'GET', '/api/teams/test-team-id', { description: 'Get team by ID', }); await testEndpoint('Teams', 'PUT', '/api/teams/test-team-id', { body: { name: 'Updated Team' }, description: 'Update team', }); await testEndpoint('Teams', 'DELETE', '/api/teams/test-team-id', { description: 'Delete team', }); // Team member endpoints await testEndpoint('Teams', 'POST', '/api/teams/test-team-id/members', { body: { agentId: 'test-agent', role: 'developer' }, description: 'Add team member', }); await testEndpoint('Teams', 'DELETE', '/api/teams/test-team-id/members/test-member-id', { description: 'Remove team member', }); // Team task endpoints await testEndpoint('Teams', 'GET', '/api/teams/test-team-id/metrics', { description: 'Get team metrics', }); await testEndpoint('Teams', 'GET', '/api/teams/test-team-id/events', { description: 'Get team events', }); // ========================================= // Config API Endpoints // ========================================= console.log(`\n--- Config API ---`); await testEndpoint('Config', 'GET', '/api/config', { description: 'Get full configuration', }); await testEndpoint('Config', 'GET', '/api/config/quick', { description: 'Get quick configuration', }); await testEndpoint('Config', 'PUT', '/api/config/quick', { body: { default_model: 'gpt-4' }, description: 'Update quick configuration', }); // ========================================= // Trigger API Endpoints // ========================================= console.log(`\n--- Trigger API ---`); await testEndpoint('Triggers', 'GET', '/api/triggers', { description: 'List all triggers', }); await testEndpoint('Triggers', 'POST', '/api/triggers', { body: { type: 'schedule', enabled: true, schedule: '0 * * * *', }, description: 'Create trigger', }); await testEndpoint('Triggers', 'GET', '/api/triggers/test-trigger-id', { description: 'Get trigger by ID', }); await testEndpoint('Triggers', 'PUT', '/api/triggers/test-trigger-id', { body: { enabled: false }, description: 'Update trigger', }); await testEndpoint('Triggers', 'DELETE', '/api/triggers/test-trigger-id', { description: 'Delete trigger', }); // ========================================= // Audit API Endpoints // ========================================= console.log(`\n--- Audit API ---`); await testEndpoint('Audit', 'GET', '/api/audit/logs', { description: 'Get audit logs', }); await testEndpoint('Audit', 'GET', '/api/audit/logs?limit=10', { description: 'Get audit logs with limit', }); await testEndpoint('Audit', 'GET', '/api/audit/verify/test-log-id', { description: 'Verify audit chain for log ID', }); // ========================================= // Skills & Plugins API Endpoints // ========================================= console.log(`\n--- Skills & Plugins ---`); await testEndpoint('Skills', 'GET', '/api/skills', { description: 'List all skills', }); await testEndpoint('Plugins', 'GET', '/api/plugins/status', { description: 'Get plugin status', }); // ========================================= // Hands API Endpoints // ========================================= console.log(`\n--- Hands API ---`); await testEndpoint('Hands', 'GET', '/api/hands', { description: 'List all hands', }); await testEndpoint('Hands', 'POST', '/api/hands/researcher/trigger', { body: { query: 'test query' }, description: 'Trigger researcher hand', }); // ========================================= // Workflow API Endpoints // ========================================= console.log(`\n--- Workflow API ---`); await testEndpoint('Workflows', 'GET', '/api/workflows', { description: 'List all workflows', }); await testEndpoint('Workflows', 'POST', '/api/workflows', { body: { name: 'Test Workflow', steps: [], }, description: 'Create workflow', }); await testEndpoint('Workflows', 'GET', '/api/workflows/test-workflow-id', { description: 'Get workflow by ID', }); // ========================================= // Stats API Endpoints // ========================================= console.log(`\n--- Stats API ---`); await testEndpoint('Stats', 'GET', '/api/stats/usage', { description: 'Get usage statistics', }); await testEndpoint('Stats', 'GET', '/api/stats/sessions', { description: 'Get session statistics', }); // ========================================= // Channel API Endpoints // ========================================= console.log(`\n--- Channels API ---`); await testEndpoint('Channels', 'GET', '/api/channels', { description: 'List all channels', }); // ========================================= // WebSocket Endpoint // ========================================= console.log(`\n--- WebSocket ---`); // WebSocket test is different - we need to check if the endpoint exists const wsUrl = `${config.baseUrl.replace(/^http/, 'ws')}/ws`; const wsResult = await testWebSocketConnection(wsUrl); results.categories['WebSocket'] = { total: 1, passed: wsResult.success ? 1 : 0, failed: wsResult.success ? 0 : 1, tests: [wsResult], }; results.summary.total++; if (wsResult.success) { results.summary.passed++; if (!config.json) { console.log(`\x1b[32m[PASS]\x1b[0m CONNECT /ws \x1b[2m${wsResult.status} ${wsResult.duration}ms\x1b[0m`); } } else { results.summary.failed++; results.errors.push({ category: 'WebSocket', method: 'CONNECT', path: '/ws', error: wsResult.error, status: wsResult.status, }); if (!config.json) { console.log(`\x1b[31m[FAIL]\x1b[0m CONNECT /ws \x1b[2m${wsResult.status} ${wsResult.duration}ms\x1b[0m`); console.log(` Error: ${wsResult.error}`); } } // Print summary printSummary(); } // Run tests runAllTests().catch((error) => { console.error('Failed to run tests:', error.message); process.exit(1); });