Files
zclaw_openfang/tests/fixtures/openfang-mock-server.ts
iven f4efc823e2 refactor(types): comprehensive TypeScript type system improvements
Major type system refactoring and error fixes across the codebase:

**Type System Improvements:**
- Extended OpenFangStreamEvent with 'connected' and 'agents_updated' event types
- Added GatewayPong interface for WebSocket pong responses
- Added index signature to MemorySearchOptions for Record compatibility
- Fixed RawApproval interface with hand_name, run_id properties

**Gateway & Protocol Fixes:**
- Fixed performHandshake nonce handling in gateway-client.ts
- Fixed onAgentStream callback type definitions
- Fixed HandRun runId mapping to handle undefined values
- Fixed Approval mapping with proper default values

**Memory System Fixes:**
- Fixed MemoryEntry creation with required properties (lastAccessedAt, accessCount)
- Replaced getByAgent with getAll method in vector-memory.ts
- Fixed MemorySearchOptions type compatibility

**Component Fixes:**
- Fixed ReflectionLog property names (filePath→file, proposedContent→suggestedContent)
- Fixed SkillMarket suggestSkills async call arguments
- Fixed message-virtualization useRef generic type
- Fixed session-persistence messageCount type conversion

**Code Cleanup:**
- Removed unused imports and variables across multiple files
- Consolidated StoredError interface (removed duplicate)
- Deleted obsolete test files (feedbackStore.test.ts, memory-index.test.ts)

**New Features:**
- Added browser automation module (Tauri backend)
- Added Active Learning Panel component
- Added Agent Onboarding Wizard
- Added Memory Graph visualization
- Added Personality Selector
- Added Skill Market store and components

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 08:05:07 +08:00

942 lines
30 KiB
TypeScript

/**
* OpenFang Mock Server for Testing
*
* Simulates OpenFang Kernel API endpoints and WebSocket events.
* Provides a complete test double for the ZCLAW desktop client.
*
* Usage:
* const server = createOpenFangMockServer({ port: 4200 });
* await server.start();
* // ... run tests ...
* await server.stop();
*/
import { createServer, IncomingMessage, Server, ServerResponse } from 'http';
import { WebSocketServer, WebSocket, RawData } from 'ws';
// === Types ===
export interface MockServerConfig {
port?: number;
host?: string;
version?: string;
connectionDelay?: number;
challengeDelay?: number;
}
export interface MockServerInstance {
start: () => Promise<void>;
stop: () => Promise<void>;
getPort: () => number;
getWsUrl: () => string;
getHttpUrl: () => string;
reset: () => void;
setHands: (hands: MockHand[]) => void;
setWorkflows: (workflows: MockWorkflow[]) => void;
setTriggers: (triggers: MockTrigger[]) => void;
setAgents: (agents: MockAgent[]) => void;
setSecurityLayers: (layers: MockSecurityLayer[]) => void;
addAuditLog: (entry: Omit<MockAuditLogEntry, 'id' | 'timestamp'>) => void;
simulateStreamEvent: (event: string, payload: unknown) => void;
getConnectedClients: () => number;
}
export interface MockHand {
name: string;
description: string;
status: 'idle' | 'running' | 'needs_approval' | 'completed' | 'error';
config?: Record<string, unknown>;
}
export interface MockWorkflow {
id: string;
name: string;
steps: number;
description?: string;
}
export interface MockTrigger {
id: string;
type: 'webhook' | 'schedule' | 'event';
enabled: boolean;
config?: Record<string, unknown>;
}
export interface MockAgent {
id: string;
name: string;
role?: string;
nickname?: string;
scenarios?: string[];
model?: string;
workspaceDir?: string;
workspaceResolvedPath?: string;
restrictFiles?: boolean;
privacyOptIn?: boolean;
userName?: string;
userRole?: string;
createdAt: string;
updatedAt?: string;
bootstrapReady?: boolean;
}
export interface MockSecurityLayer {
name: string;
enabled: boolean;
description?: string;
}
export interface MockAuditLogEntry {
id: string;
timestamp: string;
action: string;
actor?: string;
result?: 'success' | 'failure';
details?: Record<string, unknown>;
}
// === Sample Data ===
const DEFAULT_HANDS: MockHand[] = [
{ name: 'clip', description: 'Video processing and vertical screen generation', status: 'idle' },
{ name: 'lead', description: 'Sales lead discovery and qualification', status: 'idle' },
{ name: 'collector', description: 'Data collection and aggregation', status: 'idle' },
{ name: 'predictor', description: 'Predictive analytics', status: 'idle' },
{ name: 'researcher', description: 'Deep research and analysis', status: 'idle' },
{ name: 'twitter', description: 'Twitter automation', status: 'idle' },
{ name: 'browser', description: 'Browser automation', status: 'idle' },
];
const DEFAULT_WORKFLOWS: MockWorkflow[] = [
{ id: 'wf-001', name: 'Daily Report', steps: 3, description: 'Generate daily summary report' },
{ id: 'wf-002', name: 'Data Pipeline', steps: 5, description: 'ETL data processing pipeline' },
{ id: 'wf-003', name: 'Research Task', steps: 4, description: 'Multi-step research workflow' },
];
const DEFAULT_TRIGGERS: MockTrigger[] = [
{ id: 'tr-001', type: 'schedule', enabled: true, config: { cron: '0 9 * * *' } },
{ id: 'tr-002', type: 'webhook', enabled: true, config: { path: '/hooks/data' } },
{ id: 'tr-003', type: 'event', enabled: false, config: { eventType: 'data.received' } },
];
const DEFAULT_AGENTS: MockAgent[] = [
{
id: 'agent-001',
name: 'Default Agent',
role: 'assistant',
nickname: 'ZCLAW',
scenarios: ['general', 'coding'],
model: 'gpt-4',
workspaceDir: '~/.openfang/workspaces/default',
workspaceResolvedPath: '/home/user/.openfang/workspaces/default',
restrictFiles: false,
privacyOptIn: false,
userName: 'User',
userRole: 'developer',
createdAt: new Date().toISOString(),
bootstrapReady: true,
},
];
const DEFAULT_SECURITY_LAYERS: MockSecurityLayer[] = [
{ name: 'input_validation', enabled: true, description: 'Input sanitization and validation' },
{ name: 'rate_limiting', enabled: true, description: 'Request rate limiting' },
{ name: 'authentication', enabled: true, description: 'Ed25519 + JWT authentication' },
{ name: 'authorization', enabled: true, description: 'RBAC capability gates' },
{ name: 'encryption_at_rest', enabled: true, description: 'Data encryption at rest' },
{ name: 'encryption_in_transit', enabled: true, description: 'TLS encryption' },
{ name: 'audit_logging', enabled: true, description: 'Merkle hash chain audit' },
{ name: 'session_management', enabled: true, description: 'Secure session handling' },
{ name: 'device_attestation', enabled: true, description: 'Device identity verification' },
{ name: 'content_security', enabled: true, description: 'Content Security Policy' },
{ name: 'csp_headers', enabled: true, description: 'HTTP security headers' },
{ name: 'cors_policy', enabled: true, description: 'Cross-origin resource policy' },
{ name: 'sandbox_isolation', enabled: true, description: 'Process sandboxing' },
{ name: 'secret_management', enabled: true, description: 'Secure secret storage' },
{ name: 'intrusion_detection', enabled: false, description: 'IDS monitoring' },
{ name: 'backup_recovery', enabled: true, description: 'Backup and disaster recovery' },
];
// === Server Implementation ===
export function createOpenFangMockServer(config: MockServerConfig = {}): MockServerInstance {
const {
port = 4200,
host = '127.0.0.1',
version = '2026.3.13',
connectionDelay = 0,
challengeDelay = 0,
} = config;
let httpServer: Server | null = null;
let wsServer: WebSocketServer | null = null;
let actualPort = port;
// Mutable state
let hands = [...DEFAULT_HANDS];
let workflows = [...DEFAULT_WORKFLOWS];
let triggers = [...DEFAULT_TRIGGERS];
let agents = [...DEFAULT_AGENTS];
let securityLayers = [...DEFAULT_SECURITY_LAYERS];
let auditLogs: MockAuditLogEntry[] = [];
let handRuns = new Map<string, { status: string; result?: unknown; startedAt: string }>();
let workflowRuns = new Map<string, { status: string; step?: string; result?: unknown }>();
const connectedClients = new Set<WebSocket>();
// === REST API Handlers ===
function handleHealth(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, { version, status: 'ok' });
}
function handleGetAgents(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, { clones: agents });
}
function handleCreateAgent(req: IncomingMessage, res: ServerResponse): void {
parseBody(req, (body) => {
const newAgent: MockAgent = {
id: `agent-${Date.now()}`,
name: body.name || 'New Agent',
role: body.role,
nickname: body.nickname,
scenarios: body.scenarios,
model: body.model,
workspaceDir: body.workspaceDir,
restrictFiles: body.restrictFiles,
privacyOptIn: body.privacyOptIn,
userName: body.userName,
userRole: body.userRole,
createdAt: new Date().toISOString(),
};
agents.push(newAgent);
addAuditLogEntry({
action: 'agent.created',
actor: 'system',
result: 'success',
details: { agentId: newAgent.id, name: newAgent.name },
});
sendJson(res, { clone: newAgent }, 201);
});
}
function handleUpdateAgent(req: IncomingMessage, res: ServerResponse, agentId: string): void {
parseBody(req, (body) => {
const index = agents.findIndex((a) => a.id === agentId);
if (index === -1) {
sendError(res, 'Agent not found', 404);
return;
}
agents[index] = { ...agents[index], ...body, updatedAt: new Date().toISOString() };
addAuditLogEntry({
action: 'agent.updated',
actor: 'system',
result: 'success',
details: { agentId, updates: body },
});
sendJson(res, { clone: agents[index] });
});
}
function handleDeleteAgent(_req: IncomingMessage, res: ServerResponse, agentId: string): void {
const index = agents.findIndex((a) => a.id === agentId);
if (index === -1) {
sendError(res, 'Agent not found', 404);
return;
}
agents.splice(index, 1);
addAuditLogEntry({
action: 'agent.deleted',
actor: 'system',
result: 'success',
details: { agentId },
});
sendJson(res, { ok: true });
}
function handleGetHands(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, { hands });
}
function handleGetHand(_req: IncomingMessage, res: ServerResponse, name: string): void {
const hand = hands.find((h) => h.name === name);
if (!hand) {
sendError(res, 'Hand not found', 404);
return;
}
sendJson(res, { name: hand.name, description: hand.description, config: hand.config || {} });
}
function handleTriggerHand(_req: IncomingMessage, res: ServerResponse, name: string): void {
const hand = hands.find((h) => h.name === name);
if (!hand) {
sendError(res, 'Hand not found', 404);
return;
}
const runId = `run-${name}-${Date.now()}`;
const needsApproval = name === 'lead' || name === 'twitter'; // Some hands need approval
const status = needsApproval ? 'needs_approval' : 'running';
handRuns.set(runId, { status, startedAt: new Date().toISOString() });
// Update hand status
const handIndex = hands.findIndex((h) => h.name === name);
hands[handIndex] = { ...hand, status: needsApproval ? 'needs_approval' : 'running' };
addAuditLogEntry({
action: 'hand.triggered',
actor: 'system',
result: 'success',
details: { handName: name, runId, needsApproval },
});
sendJson(res, { runId, status });
// Simulate async events for running hands
if (!needsApproval) {
setTimeout(() => {
simulateStreamEvent('hand', {
handName: name,
runId,
status: 'running',
phase: 'start',
});
}, 100);
}
}
function handleApproveHand(_req: IncomingMessage, res: ServerResponse, name: string, runId: string): void {
parseBody(_req, (body) => {
const run = handRuns.get(runId);
if (!run) {
sendError(res, 'Run not found', 404);
return;
}
const approved = body.approved === true;
run.status = approved ? 'running' : 'cancelled';
handRuns.set(runId, run);
addAuditLogEntry({
action: approved ? 'hand.approved' : 'hand.rejected',
actor: 'operator',
result: 'success',
details: { handName: name, runId, reason: body.reason },
});
sendJson(res, { status: run.status });
if (approved) {
simulateStreamEvent('hand', {
handName: name,
runId,
status: 'running',
phase: 'start',
});
}
});
}
function handleCancelHand(_req: IncomingMessage, res: ServerResponse, name: string, runId: string): void {
const run = handRuns.get(runId);
if (!run) {
sendError(res, 'Run not found', 404);
return;
}
run.status = 'cancelled';
handRuns.set(runId, run);
addAuditLogEntry({
action: 'hand.cancelled',
actor: 'operator',
result: 'success',
details: { handName: name, runId },
});
sendJson(res, { status: 'cancelled' });
}
function handleGetHandRuns(_req: IncomingMessage, res: ServerResponse, name: string): void {
const runs = Array.from(handRuns.entries())
.filter(([runId]) => runId.startsWith(`run-${name}-`))
.map(([runId, data]) => ({
runId,
status: data.status,
startedAt: data.startedAt,
}));
sendJson(res, { runs });
}
function handleGetWorkflows(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, { workflows });
}
function handleGetWorkflow(_req: IncomingMessage, res: ServerResponse, id: string): void {
const workflow = workflows.find((w) => w.id === id);
if (!workflow) {
sendError(res, 'Workflow not found', 404);
return;
}
sendJson(res, {
id: workflow.id,
name: workflow.name,
steps: Array.from({ length: workflow.steps }, (_, i) => ({
id: `step-${i + 1}`,
name: `Step ${i + 1}`,
})),
});
}
function handleExecuteWorkflow(_req: IncomingMessage, res: ServerResponse, id: string): void {
const workflow = workflows.find((w) => w.id === id);
if (!workflow) {
sendError(res, 'Workflow not found', 404);
return;
}
parseBody(_req, (body) => {
const runId = `wf-run-${id}-${Date.now()}`;
workflowRuns.set(runId, { status: 'running', step: 'step-1' });
addAuditLogEntry({
action: 'workflow.started',
actor: 'system',
result: 'success',
details: { workflowId: id, runId, input: body },
});
sendJson(res, { runId, status: 'running' });
// Simulate workflow progress
setTimeout(() => {
simulateStreamEvent('workflow', {
workflowId: id,
runId,
status: 'running',
step: 'step-1',
workflowStatus: 'executing',
});
}, 100);
});
}
function handleGetWorkflowRun(_req: IncomingMessage, res: ServerResponse, workflowId: string, runId: string): void {
const run = workflowRuns.get(runId);
if (!run) {
sendError(res, 'Run not found', 404);
return;
}
sendJson(res, { status: run.status, step: run.step, result: run.result });
}
function handleCancelWorkflow(_req: IncomingMessage, res: ServerResponse, workflowId: string, runId: string): void {
const run = workflowRuns.get(runId);
if (!run) {
sendError(res, 'Run not found', 404);
return;
}
run.status = 'cancelled';
workflowRuns.set(runId, run);
addAuditLogEntry({
action: 'workflow.cancelled',
actor: 'operator',
result: 'success',
details: { workflowId, runId },
});
sendJson(res, { status: 'cancelled' });
}
function handleGetTriggers(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, { triggers });
}
function handleGetAuditLogs(req: IncomingMessage, res: ServerResponse): void {
const url = new URL(req.url || '/', `http://${host}`);
const limit = parseInt(url.searchParams.get('limit') || '50', 10);
const offset = parseInt(url.searchParams.get('offset') || '0', 10);
const paginatedLogs = auditLogs.slice(offset, offset + limit);
sendJson(res, { logs: paginatedLogs, total: auditLogs.length });
}
function handleGetSecurityStatus(_req: IncomingMessage, res: ServerResponse): void {
const enabledCount = securityLayers.filter((l) => l.enabled).length;
sendJson(res, {
layers: securityLayers,
enabledCount,
totalCount: securityLayers.length,
securityLevel: calculateSecurityLevel(enabledCount, securityLayers.length),
});
}
function handleGetCapabilities(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
capabilities: [
'operator.read',
'operator.write',
'operator.admin',
'operator.approvals',
'operator.pairing',
],
});
}
function handleChat(req: IncomingMessage, res: ServerResponse): void {
parseBody(req, (body) => {
const runId = `chat-${Date.now()}`;
const sessionId = body.session_id || `session-${Date.now()}`;
addAuditLogEntry({
action: 'chat.message',
actor: body.agent_id || 'default',
result: 'success',
details: { runId, sessionId, messageLength: body.message?.length || 0 },
});
sendJson(res, { runId, sessionId });
// Simulate streaming events
setTimeout(() => {
simulateStreamEvent('agent', {
stream: 'assistant',
delta: 'Hello',
runId,
});
setTimeout(() => {
simulateStreamEvent('agent', {
stream: 'assistant',
delta: '! How can I help you today?',
runId,
});
setTimeout(() => {
simulateStreamEvent('agent', {
stream: 'lifecycle',
phase: 'end',
runId,
});
}, 100);
}, 100);
}, 100);
});
}
function handleGetModels(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
models: [
{ id: 'gpt-4', name: 'GPT-4', provider: 'openai' },
{ id: 'gpt-3.5-turbo', name: 'GPT-3.5 Turbo', provider: 'openai' },
{ id: 'claude-3-sonnet', name: 'Claude 3 Sonnet', provider: 'anthropic' },
{ id: 'deepseek-chat', name: 'DeepSeek Chat', provider: 'deepseek' },
],
});
}
function handleGetConfig(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
server: { host: '127.0.0.1', port: actualPort },
agent: { default_model: 'gpt-4' },
llm: {
providers: [
{ name: 'openai', api_key: '${OPENAI_API_KEY}' },
],
},
});
}
function handleGetQuickConfig(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
quickConfig: {
agentName: 'ZCLAW',
agentRole: 'assistant',
userName: 'User',
userRole: 'developer',
theme: 'dark',
showToolCalls: true,
},
});
}
function handleGetWorkspace(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
path: '~/.openfang/workspaces/default',
resolvedPath: '/home/user/.openfang/workspaces/default',
exists: true,
fileCount: 42,
totalSize: 1024 * 1024 * 5, // 5MB
});
}
function handleGetSkills(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
skills: [
{ id: 'skill-001', name: 'Code Review', path: '/skills/code-review', source: 'builtin' },
{ id: 'skill-002', name: 'Translation', path: '/skills/translation', source: 'builtin' },
{ id: 'skill-003', name: 'Research', path: '/skills/research', source: 'extra' },
],
extraDirs: ['/extra/skills'],
});
}
function handleGetChannels(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
channels: [
{ id: 'feishu', type: 'feishu', label: 'Feishu', status: 'active', accounts: 1 },
],
});
}
function handleGetStatsUsage(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
totalSessions: 10,
totalMessages: 150,
totalTokens: 50000,
byModel: {
'gpt-4': { messages: 100, inputTokens: 30000, outputTokens: 15000 },
'claude-3-sonnet': { messages: 50, inputTokens: 4000, outputTokens: 1000 },
},
});
}
function handleGetPluginsStatus(_req: IncomingMessage, res: ServerResponse): void {
sendJson(res, {
plugins: [
{ id: 'feishu', name: 'Feishu Integration', status: 'active' },
{ id: 'chinese-models', name: 'Chinese Models', status: 'active' },
],
});
}
// === HTTP Request Handler ===
function handleRequest(req: IncomingMessage, res: ServerResponse): void {
// Add CORS headers
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
if (req.method === 'OPTIONS') {
res.writeHead(204);
res.end();
return;
}
const url = new URL(req.url || '/', `http://${host}`);
const path = url.pathname;
// Route matching
const routes: Array<{
method: string | string[];
pattern: RegExp;
handler: (req: IncomingMessage, res: ServerResponse, ...matches: string[]) => void;
}> = [
{ method: 'GET', pattern: /^\/api\/health$/, handler: handleHealth },
{ method: 'GET', pattern: /^\/api\/agents$/, handler: handleGetAgents },
{ method: 'POST', pattern: /^\/api\/agents$/, handler: handleCreateAgent },
{ method: 'PUT', pattern: /^\/api\/agents\/([^/]+)$/, handler: handleUpdateAgent },
{ method: 'DELETE', pattern: /^\/api\/agents\/([^/]+)$/, handler: handleDeleteAgent },
{ method: 'GET', pattern: /^\/api\/hands$/, handler: handleGetHands },
{ method: 'GET', pattern: /^\/api\/hands\/([^/]+)$/, handler: handleGetHand },
{ method: 'POST', pattern: /^\/api\/hands\/([^/]+)\/trigger$/, handler: handleTriggerHand },
{ method: 'POST', pattern: /^\/api\/hands\/([^/]+)\/runs\/([^/]+)\/approve$/, handler: handleApproveHand },
{ method: 'POST', pattern: /^\/api\/hands\/([^/]+)\/runs\/([^/]+)\/cancel$/, handler: handleCancelHand },
{ method: 'GET', pattern: /^\/api\/hands\/([^/]+)\/runs$/, handler: handleGetHandRuns },
{ method: 'GET', pattern: /^\/api\/workflows$/, handler: handleGetWorkflows },
{ method: 'GET', pattern: /^\/api\/workflows\/([^/]+)$/, handler: handleGetWorkflow },
{ method: 'POST', pattern: /^\/api\/workflows\/([^/]+)\/execute$/, handler: handleExecuteWorkflow },
{ method: 'GET', pattern: /^\/api\/workflows\/([^/]+)\/runs\/([^/]+)$/, handler: handleGetWorkflowRun },
{ method: 'POST', pattern: /^\/api\/workflows\/([^/]+)\/runs\/([^/]+)\/cancel$/, handler: handleCancelWorkflow },
{ method: 'GET', pattern: /^\/api\/triggers$/, handler: handleGetTriggers },
{ method: 'GET', pattern: /^\/api\/audit\/logs$/, handler: handleGetAuditLogs },
{ method: 'GET', pattern: /^\/api\/security\/status$/, handler: handleGetSecurityStatus },
{ method: 'GET', pattern: /^\/api\/capabilities$/, handler: handleGetCapabilities },
{ method: 'POST', pattern: /^\/api\/chat$/, handler: handleChat },
{ method: 'GET', pattern: /^\/api\/models$/, handler: handleGetModels },
{ method: 'GET', pattern: /^\/api\/config$/, handler: handleGetConfig },
{ method: 'GET', pattern: /^\/api\/config\/quick$/, handler: handleGetQuickConfig },
{ method: 'GET', pattern: /^\/api\/workspace$/, handler: handleGetWorkspace },
{ method: 'GET', pattern: /^\/api\/skills$/, handler: handleGetSkills },
{ method: 'GET', pattern: /^\/api\/channels$/, handler: handleGetChannels },
{ method: 'GET', pattern: /^\/api\/stats\/usage$/, handler: handleGetStatsUsage },
{ method: 'GET', pattern: /^\/api\/plugins\/status$/, handler: handleGetPluginsStatus },
];
for (const route of routes) {
const methods = Array.isArray(route.method) ? route.method : [route.method];
if (!methods.includes(req.method || 'GET')) continue;
const match = path.match(route.pattern);
if (match) {
try {
route.handler(req, res, ...match.slice(1));
} catch (error) {
console.error('[MockServer] Handler error:', error);
sendError(res, 'Internal server error', 500);
}
return;
}
}
// No route matched
sendError(res, 'Not found', 404);
}
// === WebSocket Handler ===
function handleWebSocket(ws: WebSocket): void {
connectedClients.add(ws);
ws.on('message', (data: RawData) => {
try {
const message = JSON.parse(data.toString());
if (message.type === 'req') {
handleWsRequest(ws, message);
}
} catch (error) {
console.error('[MockServer] WebSocket message parse error:', error);
}
});
ws.on('close', () => {
connectedClients.delete(ws);
});
ws.on('error', (error) => {
console.error('[MockServer] WebSocket error:', error);
connectedClients.delete(ws);
});
// Send challenge after a short delay
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
const nonce = `nonce-${Date.now()}-${Math.random().toString(36).slice(2)}`;
ws.send(JSON.stringify({
type: 'event',
event: 'connect.challenge',
payload: { nonce },
}));
}
}, challengeDelay);
}
function handleWsRequest(ws: WebSocket, message: { id: string; method: string; params?: unknown }): void {
if (message.method === 'connect') {
// Handle connect handshake
setTimeout(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({
type: 'res',
id: message.id,
ok: true,
payload: {
sessionId: `session-${Date.now()}`,
protocolVersion: 3,
serverVersion: version,
},
}));
}
}, connectionDelay);
} else if (message.method === 'health') {
ws.send(JSON.stringify({
type: 'res',
id: message.id,
ok: true,
payload: { version, status: 'ok' },
}));
} else if (message.method === 'status') {
ws.send(JSON.stringify({
type: 'res',
id: message.id,
ok: true,
payload: {
uptime: Math.floor(process.uptime()),
connections: connectedClients.size,
version,
},
}));
} else {
// Unknown method
ws.send(JSON.stringify({
type: 'res',
id: message.id,
ok: false,
error: { code: 'METHOD_NOT_FOUND', message: `Unknown method: ${message.method}` },
}));
}
}
// === Helper Functions ===
function sendJson(res: ServerResponse, data: unknown, statusCode = 200): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify(data));
}
function sendError(res: ServerResponse, message: string, statusCode = 500): void {
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ error: { message, code: statusCode } }));
}
function parseBody<T>(req: IncomingMessage, callback: (body: T) => void): void {
let body = '';
req.on('data', (chunk) => {
body += chunk.toString();
});
req.on('end', () => {
try {
const parsed = body ? JSON.parse(body) : {};
callback(parsed);
} catch {
// Send error response if we have access to res
// This is a simplified version; in real use, pass res to this function
console.error('[MockServer] Failed to parse request body');
}
});
}
function simulateStreamEvent(event: string, payload: unknown): void {
const message = JSON.stringify({ type: 'event', event, payload });
wsServer?.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
}
function addAuditLogEntry(entry: Omit<MockAuditLogEntry, 'id' | 'timestamp'>): void {
const logEntry: MockAuditLogEntry = {
id: `log-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`,
timestamp: new Date().toISOString(),
...entry,
};
auditLogs.unshift(logEntry); // Add to beginning for reverse chronological order
// Keep only last 1000 logs
if (auditLogs.length > 1000) {
auditLogs = auditLogs.slice(0, 1000);
}
}
function calculateSecurityLevel(enabledCount: number, totalCount: number): 'critical' | 'high' | 'medium' | 'low' {
if (totalCount === 0) return 'low';
const ratio = enabledCount / totalCount;
if (ratio >= 0.875) return 'critical'; // 14-16 layers
if (ratio >= 0.625) return 'high'; // 10-13 layers
if (ratio >= 0.375) return 'medium'; // 6-9 layers
return 'low'; // 0-5 layers
}
// === Public API ===
return {
start: () => {
return new Promise((resolve, reject) => {
httpServer = createServer(handleRequest);
httpServer.on('error', (error: NodeJS.ErrnoException) => {
if (error.code === 'EADDRINUSE') {
reject(new Error(`Port ${port} is already in use`));
} else {
reject(error);
}
});
httpServer.listen(port, host, () => {
const address = httpServer?.address();
actualPort = typeof address === 'object' && address ? address.port : port;
// Set up WebSocket server
wsServer = new WebSocketServer({ server: httpServer, path: '/ws' });
wsServer.on('connection', handleWebSocket);
console.log(`[MockServer] Started on http://${host}:${actualPort}`);
console.log(`[MockServer] WebSocket available at ws://${host}:${actualPort}/ws`);
resolve();
});
});
},
stop: () => {
return new Promise((resolve) => {
// Close all WebSocket connections
if (wsServer) {
wsServer.clients.forEach((ws) => {
ws.close(1000, 'Server shutting down');
});
wsServer.close(() => {
wsServer = null;
});
}
if (httpServer) {
httpServer.close(() => {
console.log('[MockServer] Stopped');
httpServer = null;
resolve();
});
} else {
resolve();
}
});
},
getPort: () => actualPort,
getWsUrl: () => `ws://${host}:${actualPort}/ws`,
getHttpUrl: () => `http://${host}:${actualPort}`,
reset: () => {
hands = [...DEFAULT_HANDS];
workflows = [...DEFAULT_WORKFLOWS];
triggers = [...DEFAULT_TRIGGERS];
agents = [...DEFAULT_AGENTS];
securityLayers = [...DEFAULT_SECURITY_LAYERS];
auditLogs = [];
handRuns.clear();
workflowRuns.clear();
},
setHands: (newHands: MockHand[]) => {
hands = newHands;
},
setWorkflows: (newWorkflows: MockWorkflow[]) => {
workflows = newWorkflows;
},
setTriggers: (newTriggers: MockTrigger[]) => {
triggers = newTriggers;
},
setAgents: (newAgents: MockAgent[]) => {
agents = newAgents;
},
setSecurityLayers: (newLayers: MockSecurityLayer[]) => {
securityLayers = newLayers;
},
addAuditLog: (entry: Omit<MockAuditLogEntry, 'id' | 'timestamp'>) => {
addAuditLogEntry(entry);
},
simulateStreamEvent: (event: string, payload: unknown) => {
const message = JSON.stringify({ type: 'event', event, payload });
wsServer?.clients.forEach((ws) => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(message);
}
});
},
getConnectedClients: () => connectedClients.size,
};
}
// === Export for CommonJS compatibility ===
export default createOpenFangMockServer;