/** * Minimal WebSocket agent for Playwright E2E against a live miner-server. * Mirrors server/internal/api/integration_test.go connectAgentViaRouter flow. */ export const E2E_STUB_AGENT_ID = 'e2e-crucible-agent'; export const E2E_STUB_AGENT_HOSTNAME = 'E2E-Crucible-Host'; export const E2E_WHOAMI_RESPONSE = 'e2e-whoami-ok'; /** Active LOTL tier sent in stub stats — maps to "LOTL In-Process" in Crucible. */ export const E2E_STUB_LOTL_TIER = 'inprocess'; export const E2E_STUB_LOTL_BADGE = 'LOTL In-Process'; const E2E_STUB_STATS_INTERVAL_MS = 1_000; type HubMessage = { type: string; payload: string | Record; }; function parsePayload(payload: HubMessage['payload']): Record { if (typeof payload === 'string') { return JSON.parse(payload) as Record; } return payload; } function wsAgentUrl(baseUrl: string): string { const trimmed = baseUrl.replace(/\/$/, ''); return trimmed.replace(/^http/i, 'ws') + '/ws/agent'; } function send(ws: WebSocket, type: string, payload: Record): void { ws.send(JSON.stringify({ type, payload })); } function sendStubStats(ws: WebSocket): void { send(ws, 'stats', { hashrate_15s: 42, hashrate_1m: 42, hashrate_15m: 42, shares_submitted: 0, shares_accepted: 0, cpu_usage_pct: 5, memory_usage_pct: 40, uptime_seconds: 120, active_method: 'inprocess', mining_hashrate: 42, lotl_tier: E2E_STUB_LOTL_TIER, lotl_attempts: [ { tier: 'container', ok: false, error: 'e2e-no-docker', duration_ms: 100 }, { tier: 'inprocess', ok: true, duration_ms: 200 }, ], }); } function replyCommand(ws: WebSocket, action: string, command: string): void { let message = 'e2e-stub-ok'; if (action === 'resume') { message = 'mining resumed'; } else if (command.trim().toLowerCase() === 'whoami') { message = E2E_WHOAMI_RESPONSE; } else if (command.trim().toLowerCase().startsWith('echo ')) { message = command.trim().slice(5); } send(ws, 'command_result', { action, success: true, message }); } /** * Connect a stub agent that answers exec/powershell commands on the live server. * Returns a cleanup function that closes the socket. */ export async function connectStubAgent( baseUrl: string, fleetSecret = '', ): Promise<() => void> { const ws = new WebSocket(wsAgentUrl(baseUrl)); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('stub agent ws open timeout')), 10_000); ws.addEventListener('open', () => { clearTimeout(timer); resolve(); }, { once: true }); ws.addEventListener('error', () => { clearTimeout(timer); reject(new Error('stub agent ws connection failed')); }, { once: true }); }); send(ws, 'auth', { agent_id: E2E_STUB_AGENT_ID, fleet_secret: fleetSecret, hostname: E2E_STUB_AGENT_HOSTNAME, version: '1.0.0-e2e', platform: 'windows', arch: 'amd64', cpu_cores: 4, memory_gb: 8, }); await new Promise((resolve, reject) => { const timer = setTimeout(() => reject(new Error('stub agent auth timeout')), 10_000); ws.addEventListener('message', (ev) => { const msg = JSON.parse(String(ev.data)) as HubMessage; if (msg.type !== 'auth_response') return; clearTimeout(timer); const body = parsePayload(msg.payload); if (body.success !== true) { reject(new Error(`stub agent auth rejected: ${JSON.stringify(body)}`)); return; } resolve(); }, { once: true }); }); sendStubStats(ws); const statsTimer = setInterval(() => sendStubStats(ws), E2E_STUB_STATS_INTERVAL_MS); ws.addEventListener('message', (ev) => { let msg: HubMessage; try { msg = JSON.parse(String(ev.data)) as HubMessage; } catch { return; } if (msg.type !== 'command') return; const payload = parsePayload(msg.payload); const action = String(payload.action ?? ''); const command = String(payload.command ?? ''); replyCommand(ws, action, command); }); return () => { clearInterval(statsTimer); ws.close(); }; }