P0: Rust engine, wire daemon and audio-reactive descent surface
This commit is contained in:
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
echo "· rust fmt"
|
||||
cargo fmt --all -- --check
|
||||
echo "· clippy"
|
||||
cargo clippy --workspace --all-targets -- -D warnings
|
||||
echo "· rust tests"
|
||||
cargo test --workspace
|
||||
echo "· svelte check"
|
||||
npm --prefix ui run check
|
||||
echo "· surface build"
|
||||
npm --prefix ui run build
|
||||
echo
|
||||
echo "all checks passed"
|
||||
Executable
+19
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
cleanup() {
|
||||
trap - EXIT INT TERM
|
||||
kill 0 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT INT TERM
|
||||
|
||||
echo "ZHILA"
|
||||
echo " daemon ws://127.0.0.1:4317 (token: ${ZHILA_TOKEN:-dev})"
|
||||
echo " surface http://127.0.0.1:5173"
|
||||
echo
|
||||
|
||||
cargo run -p zhila-daemon -- --port 4317 &
|
||||
npm --prefix ui run dev &
|
||||
|
||||
wait
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/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));
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user