Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled
重构所有代码和文档中的项目名称,将OpenFang统一更新为ZCLAW。包括: - 配置文件中的项目名称 - 代码注释和文档引用 - 环境变量和路径 - 类型定义和接口名称 - 测试用例和模拟数据 同时优化部分代码结构,移除未使用的模块,并更新相关依赖项。
596 lines
16 KiB
JavaScript
596 lines
16 KiB
JavaScript
#!/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);
|
|
});
|