#!/usr/bin/env node // End-to-end wire check against a running zhila-daemon. // Usage: node scripts/wire-smoke.mjs const URL = process.env.ZHILA_WS ?? 'ws://127.0.0.1:4317'; const TOKEN = process.env.ZHILA_TOKEN ?? 'dev'; const ws = new WebSocket(URL); ws.binaryType = 'arraybuffer'; let hello = null; let snapshot = null; let frames = 0; let maxMaster = 0; const pending = new Map(); const events = []; let failure = null; const timeout = setTimeout(() => fail(new Error('wire smoke timed out after 12s')), 12_000); function fail(error) { if (failure) return; failure = error; clearTimeout(timeout); try { ws.close(); } catch {} console.error('FAIL', error.message); process.exit(1); } function done() { clearTimeout(timeout); try { ws.close(); } catch {} console.log(JSON.stringify({ protocol: hello?.protocol, session: hello?.session, audio: snapshot?.audio, patternRows: snapshot?.pattern?.rows?.length, steps: snapshot?.pattern?.steps, frames, maxMaster: +maxMaster.toFixed(3), events: events.map(e => e.kind ?? e.type), tempo: snapshot?.transport?.bpm, revision: snapshot?.revision }, null, 2)); process.exit(0); } function send(id, command) { return new Promise((resolve, reject) => { pending.set(id, { resolve, reject }); ws.send(JSON.stringify({ type: 'command', id, command })); }); } ws.onopen = () => { ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: TOKEN, client: 'wire-smoke' })); }; ws.onerror = () => fail(new Error('websocket error (is the daemon running?)')); ws.onmessage = async (message) => { if (failure) return; if (message.data instanceof ArrayBuffer) { const view = new DataView(message.data); if (view.byteLength !== 31 || view.getUint8(0) !== 1) return; frames++; const master = view.getFloat32(7, true); if (master > maxMaster) maxMaster = master; return; } let parsed; try { parsed = JSON.parse(message.data); } catch { return; } if (parsed.type === 'hello') { hello = parsed; return; } if (parsed.type === 'snapshot') { snapshot = parsed.state; try { await send(1, { type: 'play' }); await new Promise(r => setTimeout(r, 1600)); await send(2, { type: 'strike', vein: 'bass', velocity: 0.9 }); await new Promise(r => setTimeout(r, 700)); await send(3, { type: 'set_tempo', bpm: 132 }); await new Promise(r => setTimeout(r, 300)); await send(4, { type: 'stop' }); if (!frames) throw new Error('no meter frames received'); if (!(maxMaster > 0)) throw new Error('meter never left zero — engine may be silent'); if (!snapshot.audio?.running) throw new Error('audio reported offline'); if (snapshot.pattern?.rows?.length !== 5) throw new Error('pattern is incomplete'); done(); } catch (error) { fail(error); } return; } if (parsed.type === 'event') { events.push(parsed.event); return; } if (parsed.type === 'result') { const entry = pending.get(parsed.id); if (!entry) return; pending.delete(parsed.id); if (parsed.result?.state) snapshot = parsed.result.state; entry.resolve(parsed.result); return; } if (parsed.type === 'error') { const entry = parsed.id != null ? pending.get(parsed.id) : null; if (entry) { pending.delete(parsed.id); entry.reject(new Error(parsed.message)); } else fail(new Error(parsed.message)); } };