122 lines
5.4 KiB
JavaScript
122 lines
5.4 KiB
JavaScript
#!/usr/bin/env node
|
|
// Restart the daemon and verify the core, pattern and tempo survived.
|
|
import { spawn } from 'node:child_process';
|
|
import { mkdtempSync, readFileSync, rmSync, existsSync } from 'node:fs';
|
|
import { tmpdir } from 'node:os';
|
|
import { join } from 'node:path';
|
|
|
|
const PORT = 4318;
|
|
const DAEMON = './target/debug/zhila-daemon';
|
|
const project = mkdtempSync(join(tmpdir(), 'zhila-persist-'));
|
|
const fixture = readFileSync('fixtures/clicks-01.wav');
|
|
let child = null;
|
|
|
|
function start() {
|
|
return new Promise((resolve, reject) => {
|
|
child = spawn(DAEMON, ['--port', String(PORT), '--project', project, '--no-audio'], {
|
|
stdio: ['ignore', 'ignore', 'pipe']
|
|
});
|
|
child.stderr.on('data', (data) => process.stderr.write(data));
|
|
child.on('exit', (code) => { if (code && code !== 0) console.error('daemon exited', code); });
|
|
const attempt = () => {
|
|
const socket = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
|
socket.onopen = () => { socket.close(); resolve(); };
|
|
socket.onerror = () => setTimeout(attempt, 150);
|
|
};
|
|
setTimeout(attempt, 200);
|
|
setTimeout(() => reject(new Error('daemon did not start')), 10_000);
|
|
});
|
|
}
|
|
|
|
async function connect() {
|
|
const ws = new WebSocket(`ws://127.0.0.1:${PORT}`);
|
|
ws.binaryType = 'arraybuffer';
|
|
const pending = new Map();
|
|
let snapshot = null;
|
|
let imported = null;
|
|
const waiters = [];
|
|
const waitFor = (predicate) => new Promise((resolve) => waiters.push({ predicate, resolve }));
|
|
ws.onopen = () => ws.send(JSON.stringify({ type: 'hello', protocol: 1, token: 'dev', client: 'persistence-smoke' }));
|
|
ws.onmessage = (message) => {
|
|
if (typeof message.data !== 'string') return;
|
|
const parsed = JSON.parse(message.data);
|
|
if (parsed.type === 'snapshot') {
|
|
snapshot = parsed.state;
|
|
for (const waiter of [...waiters]) if (waiter.predicate(parsed)) waiters.splice(waiters.indexOf(waiter), 1), waiter.resolve(parsed);
|
|
} else if (parsed.type === 'imported') {
|
|
imported = parsed;
|
|
for (const waiter of [...waiters]) if (waiter.predicate(parsed)) waiters.splice(waiters.indexOf(waiter), 1), waiter.resolve(parsed);
|
|
} else if (parsed.type === 'result') {
|
|
const entry = pending.get(parsed.id);
|
|
if (entry) { pending.delete(parsed.id); if (parsed.result.state) snapshot = parsed.result.state; entry.resolve(parsed.result); }
|
|
} else 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)); }
|
|
}
|
|
};
|
|
await waitFor((message) => message.type === 'snapshot');
|
|
const command = (id, command) => new Promise((resolve, reject) => {
|
|
pending.set(id, { resolve, reject });
|
|
ws.send(JSON.stringify({ type: 'command', id, command }));
|
|
});
|
|
const importFixture = async () => {
|
|
ws.send(JSON.stringify({ type: 'import_core', id: 10, name: 'clicks-01.wav', seal: 'found' }));
|
|
ws.send(fixture);
|
|
await waitFor((message) => message.type === 'imported');
|
|
return imported.core;
|
|
};
|
|
return { ws, command, importFixture, get snapshot() { return snapshot; } };
|
|
}
|
|
|
|
try {
|
|
await start();
|
|
let client = await connect();
|
|
const first = client.snapshot;
|
|
if (!first.project_name) throw new Error('no project name in the snapshot');
|
|
const core = await client.importFixture();
|
|
if (core.faces.length !== 16) throw new Error('no sixteen faces');
|
|
const top = core.faces.find((face) => face.rank === 0);
|
|
await client.command(20, { type: 'set_pattern_face', row: 0, core: core.id, face: top.index });
|
|
await client.command(21, { type: 'undo' });
|
|
if (client.snapshot.pattern.rows[0].face) throw new Error('undo did not revert the face');
|
|
await client.command(22, { type: 'redo' });
|
|
const assigned = client.snapshot.pattern.rows[0].face;
|
|
if (!assigned) throw new Error('redo did not restore the face');
|
|
await client.command(23, { type: 'set_pattern_step', row: 0, step: 0, velocity: 96 });
|
|
await client.command(24, { type: 'set_tempo', bpm: 132 });
|
|
client.ws.close();
|
|
await new Promise((resolve) => { child.once('exit', resolve); child.kill('SIGTERM'); });
|
|
|
|
await start();
|
|
client = await connect();
|
|
const second = client.snapshot;
|
|
const restored = second.pattern.rows[0];
|
|
if (second.cores.length !== 1) throw new Error('core did not survive the restart');
|
|
if (second.cores[0].name !== 'clicks-01') throw new Error('core name changed');
|
|
if (!restored.face || restored.face.face !== assigned.face) throw new Error('pattern face did not survive');
|
|
if (restored.velocities[0] !== 96) throw new Error('step did not survive');
|
|
if (second.transport.bpm !== 132) throw new Error('tempo did not survive');
|
|
if (second.project_name !== first.project_name) throw new Error('project name changed');
|
|
console.log(JSON.stringify({
|
|
project: second.project_name,
|
|
path: project,
|
|
cores: second.cores.length,
|
|
faces: second.cores[0].faces.length,
|
|
face: restored.face,
|
|
step: restored.velocities[0],
|
|
bpm: second.transport.bpm,
|
|
journal: second.journal_len,
|
|
db: existsSync(join(project, 'project.db'))
|
|
}, null, 2));
|
|
client.ws.close();
|
|
child.kill('SIGTERM');
|
|
rmSync(project, { recursive: true, force: true });
|
|
console.log('persistence smoke passed');
|
|
process.exit(0);
|
|
} catch (error) {
|
|
if (child) child.kill('SIGTERM');
|
|
rmSync(project, { recursive: true, force: true });
|
|
console.error('FAIL', error.message);
|
|
process.exit(1);
|
|
}
|