/** * 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; stop: () => Promise; 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) => 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; } export interface MockWorkflow { id: string; name: string; steps: number; description?: string; } export interface MockTrigger { id: string; type: 'webhook' | 'schedule' | 'event'; enabled: boolean; config?: Record; } 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; } // === 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(); let workflowRuns = new Map(); const connectedClients = new Set(); // === 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(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): 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) => { 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;