Files
zclaw_openfang/desktop/scripts/test-api-connection.mjs
iven 3e81bd3e50 feat(ui): Phase 8 UI/UX optimization and system documentation update
## Sidebar Enhancement
- Change tabs to icon + small label layout for better space utilization
- Add Teams tab with team collaboration entry point

## Settings Page Improvements
- Connect theme toggle to gatewayStore.saveQuickConfig for persistence
- Remove OpenFang backend download section, simplify UI
- Add time range filter to UsageStats (7d/30d/all)
- Add stat cards with icons (sessions, messages, input/output tokens)
- Add token usage overview bar chart
- Add 8 ZCLAW system skill definitions with categories

## Bug Fixes
- Fix ChannelList duplicate content with deduplication logic
- Integrate CreateTriggerModal in TriggersPanel
- Add independent SecurityStatusPanel with 12 default enabled layers
- Change workflow view to use SchedulerPanel as unified entry

## New Components
- CreateTriggerModal: Event trigger creation modal
- HandApprovalModal: Hand approval workflow dialog
- HandParamsForm: Enhanced Hand parameter form
- SecurityLayersPanel: 16-layer security status display

## Architecture
- Add TOML config parsing support (toml-utils.ts, config-parser.ts)
- Add request timeout and retry mechanism (request-helper.ts)
- Add secure token storage (secure-storage.ts, secure_storage.rs)

## Tests
- Add unit tests for config-parser, toml-utils, request-helper
- Add team-client and teamStore tests

## Documentation
- Update SYSTEM_ANALYSIS.md with Phase 8 completion
- UI completion: 100% (30/30 components)
- API coverage: 93% (63/68 endpoints)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-15 14:12:11 +08:00

596 lines
16 KiB
JavaScript

#!/usr/bin/env node
/**
* OpenFang Backend API Connection Test Script
*
* Tests all API endpoints used by the ZCLAW desktop client against
* the OpenFang Kernel backend.
*
* Usage:
* node desktop/scripts/test-api-connection.mjs [options]
*
* Options:
* --url=URL Base URL for OpenFang 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(`
OpenFang API Connection Tester
Usage: node test-api-connection.mjs [options]
Options:
--url=URL Base URL for OpenFang 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=== OpenFang 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);
});