Files
AetherForge/server/web/e2e/stub-agent.ts
AetherForge 6122c3ebfc
Some checks failed
CI Docker Mining Proof / Linux agent hashrate proof (push) Has been cancelled
Stabilize Playwright phase 8 with page smokes and flake fixes.
Direct-navigate Calibrate, harden stub agent timing, and add Emberwake/Seer/Oath smokes so the full 32-spec E2E sweep passes reliably.
2026-06-07 12:03:44 -07:00

147 lines
4.3 KiB
TypeScript

/**
* 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<string, unknown>;
};
function parsePayload(payload: HubMessage['payload']): Record<string, unknown> {
if (typeof payload === 'string') {
return JSON.parse(payload) as Record<string, unknown>;
}
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<string, unknown>): 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<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('stub agent ws open timeout')), 20_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<void>((resolve, reject) => {
const timer = setTimeout(() => reject(new Error('stub agent auth timeout')), 30_000);
const onMessage = (ev: MessageEvent) => {
let msg: HubMessage;
try {
msg = JSON.parse(String(ev.data)) as HubMessage;
} catch {
return;
}
if (msg.type !== 'auth_response') return;
clearTimeout(timer);
ws.removeEventListener('message', onMessage);
const body = parsePayload(msg.payload);
if (body.success !== true) {
reject(new Error(`stub agent auth rejected: ${JSON.stringify(body)}`));
return;
}
resolve();
};
ws.addEventListener('message', onMessage);
});
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);
if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) {
ws.close(1000, 'e2e-teardown');
}
};
}