2026-10-11 11:15:45 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Tests for Vista Cues.
|
|
|
|
|
*
|
|
|
|
|
* node modules/vista-cues/vista.test.js
|
|
|
|
|
*
|
|
|
|
|
* Plain Node, no dependencies; exits non-zero on the first failure. Covers
|
|
|
|
|
* the cue-sheet parser, RTP-MIDI packet decoding, and a real session over
|
|
|
|
|
* loopback UDP: a fake sender invites the module's session, plays notes, and
|
|
|
|
|
* the server entry's state follows.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
const assert = require('assert');
|
|
|
|
|
const dgram = require('dgram');
|
|
|
|
|
const http = require('http');
|
|
|
|
|
const { parseMapping, match, noteFromName } = require('./mapping');
|
|
|
|
|
const { parseRtpMidi, describe, Session } = require('./rtpmidi');
|
|
|
|
|
|
|
|
|
|
let passed = 0;
|
|
|
|
|
async function test(name, fn) {
|
|
|
|
|
await fn();
|
|
|
|
|
passed++;
|
|
|
|
|
console.log(` ok ${name}`);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const wait = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
|
async function until(fn, ms = 3000) {
|
|
|
|
|
const end = Date.now() + ms;
|
|
|
|
|
while (Date.now() < end) {
|
|
|
|
|
if (fn()) return;
|
|
|
|
|
await wait(20);
|
|
|
|
|
}
|
|
|
|
|
throw new Error('timed out waiting');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/* ── a minimal independent sender ─────────────────────────────────────── */
|
|
|
|
|
|
|
|
|
|
function sessionPkt(cmd, token, ssrc, name) {
|
|
|
|
|
const n = name == null ? Buffer.alloc(0) : Buffer.from(`${name}\0`);
|
|
|
|
|
const b = Buffer.alloc(16 + n.length);
|
|
|
|
|
b.writeUInt16BE(0xffff, 0);
|
|
|
|
|
b.write(cmd, 2, 'ascii');
|
|
|
|
|
b.writeUInt32BE(2, 4);
|
|
|
|
|
b.writeUInt32BE(token, 8);
|
|
|
|
|
b.writeUInt32BE(ssrc, 12);
|
|
|
|
|
n.copy(b, 16);
|
|
|
|
|
return b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function rtp(ssrc, seq, midiBytes, flags = 0) {
|
|
|
|
|
const midi = Buffer.from(midiBytes);
|
|
|
|
|
const b = Buffer.alloc(13 + midi.length);
|
|
|
|
|
b[0] = 0x80;
|
|
|
|
|
b[1] = 0x61;
|
|
|
|
|
b.writeUInt16BE(seq, 2);
|
|
|
|
|
b.writeUInt32BE(1234, 4);
|
|
|
|
|
b.writeUInt32BE(ssrc, 8);
|
|
|
|
|
b[12] = flags | midi.length;
|
|
|
|
|
midi.copy(b, 13);
|
|
|
|
|
return b;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** A sender that accepts invitations (like a Mac session) and can play notes. */
|
|
|
|
|
async function fakeSender(port, name) {
|
|
|
|
|
const ssrc = 0x0badf00d;
|
|
|
|
|
const control = dgram.createSocket('udp4');
|
|
|
|
|
const data = dgram.createSocket('udp4');
|
|
|
|
|
const peers = [];
|
|
|
|
|
const syncs = [];
|
|
|
|
|
let seq = 100;
|
|
|
|
|
const handle = (sock, which) => (buf, r) => {
|
|
|
|
|
const cmd = buf.toString('ascii', 2, 4);
|
|
|
|
|
if (cmd === 'IN') {
|
|
|
|
|
sock.send(sessionPkt('OK', buf.readUInt32BE(8), ssrc, name), r.port, r.address);
|
|
|
|
|
if (which === 'data') peers.push({ address: r.address, port: r.port });
|
|
|
|
|
} else if (cmd === 'CK') {
|
|
|
|
|
syncs.push(buf[8]);
|
|
|
|
|
if (buf[8] === 0) {
|
|
|
|
|
const out = Buffer.from(buf);
|
|
|
|
|
out.writeUInt32BE(ssrc, 4);
|
|
|
|
|
out[8] = 1;
|
|
|
|
|
data.send(out, r.port, r.address);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
control.on('message', handle(control, 'control'));
|
|
|
|
|
data.on('message', handle(data, 'data'));
|
|
|
|
|
await new Promise((r) => control.bind(port, '127.0.0.1', r));
|
|
|
|
|
await new Promise((r) => data.bind(port + 1, '127.0.0.1', r));
|
|
|
|
|
return {
|
|
|
|
|
peers,
|
|
|
|
|
syncs,
|
|
|
|
|
note(n, ch = 1) {
|
|
|
|
|
for (const p of peers) data.send(rtp(ssrc, seq++, [0x90 | (ch - 1), n, 100, 0x00, n, 0]), p.port, p.address);
|
|
|
|
|
},
|
|
|
|
|
close() { control.close(); data.close(); },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function freePort() {
|
|
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
const s = dgram.createSocket('udp4');
|
|
|
|
|
s.bind(0, () => { const p = s.address().port; s.close(() => resolve(p)); });
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Two adjacent free UDP ports, as a session needs. */
|
|
|
|
|
async function freePair() {
|
|
|
|
|
for (let i = 0; i < 20; i++) {
|
|
|
|
|
const p = await freePort();
|
|
|
|
|
if (p > 65000) continue;
|
|
|
|
|
const ok = await new Promise((resolve) => {
|
|
|
|
|
const s = dgram.createSocket('udp4');
|
|
|
|
|
s.on('error', () => resolve(false));
|
|
|
|
|
s.bind(p + 1, () => s.close(() => resolve(true)));
|
|
|
|
|
});
|
|
|
|
|
if (ok) return p;
|
|
|
|
|
}
|
|
|
|
|
throw new Error('no free port pair');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
(async () => {
|
|
|
|
|
console.log('mapping');
|
|
|
|
|
|
|
|
|
|
await test('reads the cue sheet forms', () => {
|
|
|
|
|
const { rules, errors } = parseMapping([
|
|
|
|
|
'# comment',
|
|
|
|
|
'60 = Walk-in',
|
|
|
|
|
'C3 - Worship 1',
|
|
|
|
|
'61 release Walk-in',
|
|
|
|
|
'62\tPause\tWorship 1',
|
|
|
|
|
'127 release all',
|
|
|
|
|
'ch2:64 = Message # only on channel 2',
|
|
|
|
|
'pc5 = Baptism',
|
|
|
|
|
'note 66: Go: Song #2',
|
|
|
|
|
'67 = "Back Lights"',
|
|
|
|
|
'F#2 = Hazer',
|
|
|
|
|
].join('\n'));
|
|
|
|
|
assert.deepStrictEqual(errors, []);
|
|
|
|
|
const brief = rules.map((r) => [r.channel, r.kind, r.number, r.action, r.list]);
|
|
|
|
|
assert.deepStrictEqual(brief, [
|
|
|
|
|
[null, 'note', 60, 'go', 'Walk-in'],
|
|
|
|
|
[null, 'note', 60, 'go', 'Worship 1'],
|
|
|
|
|
[null, 'note', 61, 'release', 'Walk-in'],
|
|
|
|
|
[null, 'note', 62, 'pause', 'Worship 1'],
|
|
|
|
|
[null, 'note', 127, 'releaseAll', ''],
|
|
|
|
|
[2, 'note', 64, 'go', 'Message'],
|
|
|
|
|
[null, 'program', 5, 'go', 'Baptism'],
|
|
|
|
|
[null, 'note', 66, 'go', 'Song #2'],
|
|
|
|
|
[null, 'note', 67, 'go', 'Back Lights'],
|
|
|
|
|
[null, 'note', 54, 'go', 'Hazer'],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('middle C naming', () => {
|
|
|
|
|
assert.strictEqual(noteFromName('C3', 'C3'), 60);
|
|
|
|
|
assert.strictEqual(noteFromName('C4', 'C4'), 60);
|
|
|
|
|
assert.strictEqual(noteFromName('C-2', 'C3'), 0);
|
|
|
|
|
assert.strictEqual(noteFromName('Bb3', 'C3'), 70);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('bad lines are reported, not fatal', () => {
|
|
|
|
|
const { rules, errors } = parseMapping('hello\n200 = Big\n61 release\nch17:3 = X\n60 = OK');
|
|
|
|
|
assert.strictEqual(rules.length, 1);
|
|
|
|
|
assert.deepStrictEqual(errors.map((e) => e.line), [1, 2, 3, 4]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('groups and flashes', () => {
|
|
|
|
|
const { rules, groups, errors } = parseMapping('5 = Loose\n[Scene]\n0 = Pre Service\n8 = Message\n[Color]\n23 = Blue\n30 flash Blinder Flash 1\n[]\n9 = After');
|
|
|
|
|
assert.deepStrictEqual(errors, []);
|
|
|
|
|
assert.deepStrictEqual(groups, ['Scene', 'Color']);
|
|
|
|
|
assert.deepStrictEqual(rules.map((r) => [r.number, r.group, r.action]), [
|
|
|
|
|
[5, '', 'go'], [0, 'Scene', 'go'], [8, 'Scene', 'go'], [23, 'Color', 'go'], [30, 'Color', 'flash'], [9, '', 'go'],
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('the NA cue sheet parses cleanly', () => {
|
|
|
|
|
const fs = require('fs');
|
|
|
|
|
const path = require('path');
|
|
|
|
|
const file = path.join(__dirname, '..', '..', '..', 'NA cue sheet.txt');
|
|
|
|
|
if (!fs.existsSync(file)) return;
|
|
|
|
|
const { rules, errors, groups } = parseMapping(fs.readFileSync(file, 'utf8'));
|
|
|
|
|
assert.deepStrictEqual(errors, []);
|
|
|
|
|
assert.strictEqual(rules.length, 53); // every note marked TRUE for NA in the sheet
|
|
|
|
|
assert.deepStrictEqual(groups, ['Main', 'Color', 'Blinders', 'Build', 'House', 'Lekos']);
|
|
|
|
|
assert.strictEqual(new Set(rules.map((r) => r.number)).size, rules.length);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('matching respects channel and kind', () => {
|
|
|
|
|
const { rules } = parseMapping('ch2:64 = Two\n64 = Any\npc64 = Program');
|
|
|
|
|
assert.strictEqual(match(rules, { type: 'noteon', channel: 2, note: 64 }).list, 'Two');
|
|
|
|
|
assert.strictEqual(match(rules, { type: 'noteon', channel: 1, note: 64 }).list, 'Any');
|
|
|
|
|
assert.strictEqual(match(rules, { type: 'program', channel: 1, program: 64 }).list, 'Program');
|
|
|
|
|
assert.strictEqual(match(rules, { type: 'noteoff', channel: 1, note: 64 }), null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('rtp-midi');
|
|
|
|
|
|
|
|
|
|
await test('note on + running-status note off', () => {
|
|
|
|
|
const p = parseRtpMidi(rtp(7, 9, [0x90, 60, 100, 0x00, 60, 0]));
|
|
|
|
|
assert.strictEqual(p.ssrc, 7);
|
|
|
|
|
assert.strictEqual(p.seq, 9);
|
|
|
|
|
assert.deepStrictEqual(p.messages.map(describe), [
|
|
|
|
|
{ type: 'noteon', channel: 1, note: 60, velocity: 100 },
|
|
|
|
|
{ type: 'noteoff', channel: 1, note: 60 },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('Z flag, multi-byte deltas, program change, sysex in between', () => {
|
|
|
|
|
const p = parseRtpMidi(rtp(7, 1, [0x05, 0xc2, 4, 0x81, 0x00, 0xf0, 0x7f, 0x01, 0xf7, 0x00, 0x93, 61, 9], 0x20));
|
|
|
|
|
assert.deepStrictEqual(p.messages.map(describe).filter(Boolean), [
|
|
|
|
|
{ type: 'program', channel: 3, program: 4 },
|
|
|
|
|
{ type: 'noteon', channel: 4, note: 61, velocity: 9 },
|
|
|
|
|
]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('journal bytes after the command section are ignored', () => {
|
|
|
|
|
const b = Buffer.concat([rtp(7, 1, [0x90, 62, 1], 0x40), Buffer.from([0x80, 0x00, 0x05, 0x01, 0x02])]);
|
|
|
|
|
assert.deepStrictEqual(parseRtpMidi(b).messages.map(describe), [{ type: 'noteon', channel: 1, note: 62, velocity: 1 }]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
console.log('session');
|
|
|
|
|
|
|
|
|
|
await test('invites a sender, keeps sync, and hears its notes', async () => {
|
|
|
|
|
const senderPort = await freePair();
|
|
|
|
|
const sender = await fakeSender(senderPort, 'Fake PP');
|
|
|
|
|
const port = await freePair();
|
|
|
|
|
const s = new Session({ name: 'Test', port });
|
|
|
|
|
assert.ok(await s.start());
|
|
|
|
|
const heard = [];
|
|
|
|
|
s.on('midi', ({ participant, event }) => heard.push([participant.label, event.type, event.note]));
|
|
|
|
|
s.invite('127.0.0.1', senderPort, 'ProPresenter');
|
|
|
|
|
await until(() => s.list().some((p) => p.connected));
|
|
|
|
|
await until(() => sender.syncs.includes(2)); // we answered its count-1 with a count-2
|
|
|
|
|
sender.note(60);
|
|
|
|
|
await until(() => heard.length >= 2);
|
|
|
|
|
assert.deepStrictEqual(heard, [['ProPresenter', 'noteon', 60], ['ProPresenter', 'noteoff', 60]]);
|
|
|
|
|
s.stop();
|
|
|
|
|
sender.close();
|
|
|
|
|
await wait(80);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('a config save re-inits on the same port straight away', async () => {
|
|
|
|
|
const senderPort = await freePair();
|
|
|
|
|
const sender = await fakeSender(senderPort, 'Fake PP');
|
|
|
|
|
const port = await freePair();
|
|
|
|
|
const mod = require('./server');
|
|
|
|
|
const config = { sessionPort: port, advertise: false, source1: { host: '127.0.0.1', port: senderPort } };
|
|
|
|
|
let handle = mod.init({ config, log: () => {} });
|
|
|
|
|
await until(() => handle.health().status === 'ok');
|
|
|
|
|
handle.stop();
|
|
|
|
|
handle = mod.init({ config, log: () => {} }); // what the shell does on save: stop(), then init()
|
|
|
|
|
await until(() => handle.health().status === 'ok');
|
|
|
|
|
handle.stop();
|
|
|
|
|
sender.close();
|
|
|
|
|
await wait(80);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('a port taken by someone else is reported, then taken once free', async () => {
|
|
|
|
|
const port = await freePair();
|
|
|
|
|
const squatter = dgram.createSocket('udp4');
|
|
|
|
|
await new Promise((r) => squatter.bind(port + 1, r));
|
|
|
|
|
const s = new Session({ name: 'Test', port });
|
|
|
|
|
let listening = false;
|
|
|
|
|
s.on('listening', () => { listening = true; });
|
|
|
|
|
assert.strictEqual(await s.start(), false);
|
|
|
|
|
assert.match(s.error, /in use/);
|
|
|
|
|
squatter.close();
|
|
|
|
|
await until(() => listening, 5000);
|
|
|
|
|
assert.strictEqual(s.error, '');
|
|
|
|
|
s.stop();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('server entry: notes become live cue lists, over HTTP', async () => {
|
|
|
|
|
const senderPort = await freePair();
|
|
|
|
|
const sender = await fakeSender(senderPort, 'Fake PP');
|
|
|
|
|
const port = await freePair();
|
|
|
|
|
const mod = require('./server');
|
|
|
|
|
const handle = mod.init({
|
|
|
|
|
config: {
|
|
|
|
|
sessionPort: port,
|
|
|
|
|
advertise: false,
|
|
|
|
|
source1Name: 'ProPresenter',
|
|
|
|
|
source1: { host: '127.0.0.1', port: senderPort },
|
|
|
|
|
cueLists: '60 = Walk-in\n61 = Worship 1\n70 release Walk-in\n127 release all',
|
|
|
|
|
},
|
|
|
|
|
log: () => {},
|
|
|
|
|
});
|
|
|
|
|
const routes = mod.routes({});
|
|
|
|
|
const server = http.createServer((req, res) => {
|
|
|
|
|
const r = routes[`${req.method} ${req.url}`];
|
|
|
|
|
if (r) r(req, res); else { res.writeHead(404); res.end(); }
|
|
|
|
|
});
|
|
|
|
|
await new Promise((r) => server.listen(0, '127.0.0.1', r));
|
|
|
|
|
const get = async (path, method = 'GET') => {
|
|
|
|
|
const res = await fetch(`http://127.0.0.1:${server.address().port}${path}`, { method });
|
|
|
|
|
return res.json();
|
|
|
|
|
};
|
|
|
|
|
const state = async () => (await get('/state')).state;
|
|
|
|
|
|
|
|
|
|
await until(() => sender.peers.length > 0);
|
|
|
|
|
await until(() => handle.health().status === 'ok');
|
|
|
|
|
sender.note(60);
|
|
|
|
|
await wait(100);
|
|
|
|
|
sender.note(61);
|
|
|
|
|
await wait(100);
|
|
|
|
|
let st = await state();
|
|
|
|
|
assert.strictEqual(st.current.name, 'Worship 1');
|
|
|
|
|
assert.deepStrictEqual(st.active.map((l) => l.name), ['Walk-in']);
|
|
|
|
|
assert.strictEqual(st.current.source, 'ProPresenter');
|
|
|
|
|
assert.strictEqual(st.sources[0].connected, true);
|
|
|
|
|
|
|
|
|
|
sender.note(70);
|
|
|
|
|
sender.note(99); // unmapped
|
|
|
|
|
await wait(150);
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.deepStrictEqual(st.active, []);
|
|
|
|
|
assert.strictEqual(st.log[0].mapped, false);
|
|
|
|
|
assert.match(st.log[0].trigger, /Note 99/);
|
|
|
|
|
|
|
|
|
|
sender.note(127);
|
|
|
|
|
await wait(100);
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.strictEqual(st.current, null);
|
|
|
|
|
|
|
|
|
|
sender.note(60);
|
|
|
|
|
await wait(100);
|
|
|
|
|
await get('/clear', 'POST');
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.strictEqual(st.current, null);
|
|
|
|
|
assert.strictEqual(st.log[0].action, 'cleared');
|
|
|
|
|
handle.stop();
|
|
|
|
|
|
|
|
|
|
// Groups: a Go replaces its group's live list; the first group is the headline; flashes stay out.
|
|
|
|
|
const h2 = mod.init({
|
|
|
|
|
config: {
|
|
|
|
|
sessionPort: port, advertise: false, source1: { host: '127.0.0.1', port: senderPort },
|
|
|
|
|
cueLists: '[Scene]\n0 = Pre Service\n8 = Message\n[Color]\n22 = Red\n23 = Blue\n30 flash Blinder Flash 1',
|
|
|
|
|
},
|
|
|
|
|
log: () => {},
|
|
|
|
|
});
|
|
|
|
|
await until(() => h2.health().status === 'ok');
|
|
|
|
|
for (const n of [0, 22, 23, 30]) { sender.note(n); await wait(60); }
|
|
|
|
|
await wait(100);
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.strictEqual(st.current.name, 'Pre Service');
|
|
|
|
|
assert.deepStrictEqual(st.active.map((l) => [l.group, l.name]), [['Color', 'Blue']]); // Blue replaced Red
|
|
|
|
|
assert.strictEqual(st.log[0].action, 'flash');
|
|
|
|
|
sender.note(8); // a new main scene takes over the look…
|
|
|
|
|
await wait(100);
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.strictEqual(st.current.name, 'Message');
|
|
|
|
|
assert.deepStrictEqual(st.active, []);
|
|
|
|
|
sender.note(22); // …and FX fired after it layer on top again
|
|
|
|
|
await wait(100);
|
|
|
|
|
st = await state();
|
|
|
|
|
assert.deepStrictEqual(st.active.map((l) => l.name), ['Red']);
|
|
|
|
|
h2.stop();
|
|
|
|
|
|
|
|
|
|
server.close();
|
|
|
|
|
sender.close();
|
|
|
|
|
await wait(80);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-20 13:14:44 -04:00
|
|
|
await test('wired network: addresses outside it, and names, are flagged', () => {
|
|
|
|
|
const mod = require('./server');
|
|
|
|
|
const off = mod._offNetwork;
|
|
|
|
|
assert.strictEqual(off('', '10.6.9.193'), ''); // no rule: any network
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16', '10.1.3.227'), '');
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16', '10.6.9.193'), 'not wired'); // Wi-Fi
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16', '169.254.12.3'), 'not wired'); // Dante
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16', 'NA-Tracks.local'), 'by name');
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16', '127.0.0.1'), ''); // Companion on this machine
|
|
|
|
|
assert.strictEqual(off('10.1.0.0/16, 10.6.0.0/20', '10.6.9.193'), ''); // several
|
|
|
|
|
assert.strictEqual(off('10.1.3.227', '10.1.3.227'), ''); // a single address
|
|
|
|
|
assert.throws(() => off('wifi', '10.1.3.227'), /not a network like 10\.1\.0\.0\/16/);
|
|
|
|
|
assert.throws(() => off('10.1.0.0/40', '10.1.3.227'), /not a network/);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
await test('server entry: a sender off the wired network still joins, and says so', async () => {
|
|
|
|
|
const senderPort = await freePair();
|
|
|
|
|
const sender = await fakeSender(senderPort, 'Fake Tracks');
|
|
|
|
|
const port = await freePair();
|
|
|
|
|
const mod = require('./server');
|
|
|
|
|
assert.throws(() => mod.init({ config: { sessionPort: port, advertise: false, wiredNetwork: '10.1/16' }, log: () => {} }), /Wired network/);
|
|
|
|
|
const handle = mod.init({
|
|
|
|
|
config: {
|
|
|
|
|
sessionPort: port,
|
|
|
|
|
advertise: false,
|
|
|
|
|
wiredNetwork: '10.1.0.0/16',
|
|
|
|
|
source2Name: 'Tracks',
|
|
|
|
|
source2: { host: 'localhost', port: senderPort }, // a name: it may resolve to Wi-Fi next time
|
|
|
|
|
cueLists: '60 = Walk-in',
|
|
|
|
|
},
|
|
|
|
|
log: () => {},
|
|
|
|
|
});
|
|
|
|
|
await until(() => sender.peers.length > 0); // joined all the same
|
|
|
|
|
const h = handle.health();
|
|
|
|
|
assert.strictEqual(h.status, 'error');
|
|
|
|
|
assert.match(h.message, /^Tracks is set by name \(localhost\): use its address on 10\.1\.0\.0\/16$/);
|
|
|
|
|
const routes = mod.routes({});
|
|
|
|
|
let body = '';
|
|
|
|
|
await new Promise((r) => routes['GET /state']({}, { writeHead() {}, end(b) { body = b; r(); } }));
|
|
|
|
|
const st = JSON.parse(body).state;
|
|
|
|
|
assert.strictEqual(st.wiredNetwork, '10.1.0.0/16');
|
|
|
|
|
assert.strictEqual(st.sources[0].offNetwork, 'by name');
|
|
|
|
|
handle.stop();
|
|
|
|
|
sender.close();
|
|
|
|
|
await wait(80);
|
|
|
|
|
});
|
|
|
|
|
|
2026-10-11 11:15:45 -04:00
|
|
|
console.log(`\n${passed} passed`);
|
|
|
|
|
process.exit(0);
|
|
|
|
|
})().catch((e) => {
|
|
|
|
|
console.error(e);
|
|
|
|
|
process.exit(1);
|
|
|
|
|
});
|