Resi Cues 1.0.1: the next cue's name keeps moving on a 30-second Resi

On a Resi that publishes every ~30s, the board could start a few seconds
before a publish and learn a 2s cadence from that partial first gap. Every
honest 30s gap after that was thrown out as a pause, so playback read as
paused 15s into every update: the countdown froze, and the next cue -- and
the name on the Timers card -- stopped moving on.

- The partial gap right after startup is no longer learned, and one outlier
  gap can no longer lock out the real cadence.
- Until a cadence is measured, a pause is only called after 45s, not 15s.
- The Timers card is titled with the cue's name; its status line now says
  "Next cue" / "Held" instead of repeating the name.
- server.test.js awaited nothing, so an async test raced the next one and
  failed on main; tests now run one at a time.

Same fix as the Resi -> ProPresenter bridge and the Companion module 0.1.2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-06 14:59:12 -04:00
co-authored by Claude Opus 5.5
parent 8ca88ba10b
commit 0f0adb4c60
5 changed files with 63 additions and 20 deletions
+15 -14
View File
@@ -18,8 +18,9 @@
const assert = require('assert');
let passed = 0;
function test(name, fn) {
fn();
// Awaited, so an async test finishes before the next one changes the world under it.
async function test(name, fn) {
await fn();
passed++;
console.log(` ok ${name}`);
}
@@ -101,7 +102,7 @@ function collect(route, req) {
const routes = server.routes({ config: {}, log: (m) => logged.push(m) });
test('signs in once and finds the configured decoder', () => {
await test('signs in once and finds the configured decoder', () => {
assert.strictEqual(world.logins, 1, 'should sign in exactly once');
const state = handle.state;
assert.strictEqual(state.status, 'ok', state.message);
@@ -110,20 +111,20 @@ function collect(route, req) {
assert.strictEqual(state.cues.length, 3);
});
test('works out the cue either side of the playhead', () => {
await test('works out the cue either side of the playhead', () => {
const state = handle.state;
assert.strictEqual(state.next.name, 'Campus Host');
assert.strictEqual(state.prev.name, 'Welcome');
assert.ok(Math.abs(state.estMs - 528000) < 2000, `playhead was ${state.estMs}`);
});
test('/state answers the same snapshot as the stream', () => {
await test('/state answers the same snapshot as the stream', () => {
const res = collect(routes['GET /state']);
assert.strictEqual(res.code, 200);
assert.deepStrictEqual(JSON.parse(res.body), handle.state);
});
test('/stream opens with an event so a new tile paints at once', () => {
await test('/stream opens with an event so a new tile paints at once', () => {
const res = collect(routes['GET /stream']);
assert.strictEqual(res.code, 200);
assert.strictEqual(res.headers['Content-Type'], 'text/event-stream; charset=utf-8');
@@ -131,7 +132,7 @@ function collect(route, req) {
assert.ok(res.body.includes('event: state'), 'should send an initial state event');
});
test('/timers offers the countdown and the playhead, shaped as the guide says', () => {
await test('/timers offers the countdown and the playhead, shaped as the guide says', () => {
const snap = JSON.parse(collect(routes['GET /timers']).body);
assert.strictEqual(snap.source, 'resi-cues');
assert.strictEqual(snap.label, 'Resi');
@@ -145,7 +146,7 @@ function collect(route, req) {
assert.ok(Math.abs(next.remainingMs - 222000) < 2000, `remaining was ${next.remainingMs}`);
// the countdown runs from the cue before it, so a consumer can colour progress
assert.strictEqual(next.targetMs, 450000);
assert.strictEqual(next.status.text, 'Campus Host');
assert.strictEqual(next.status.text, 'Next cue'); // the name is the label; not repeated
assert.ok(next.startedAt <= snap.updatedAt, 'startedAt must be in the past');
assert.strictEqual(playhead.id, 'playhead');
@@ -154,7 +155,7 @@ function collect(route, req) {
assert.ok(Math.abs(playhead.elapsedMs - 528000) < 2000);
});
test('a cue fired at the host moves the countdown on the next poll', async () => {
await test('a cue fired at the host moves the countdown on the next poll', async () => {
world.position = '00:12:40.000'; // jumped past Campus Host
tickFn();
await settle();
@@ -162,10 +163,10 @@ function collect(route, req) {
assert.strictEqual(state.next.name, 'Message');
assert.strictEqual(state.prev.name, 'Campus Host');
const snap = JSON.parse(collect(routes['GET /timers']).body);
assert.strictEqual(snap.timers[0].label, 'Message');
assert.strictEqual(snap.timers[0].label, 'Message'); // the card's name follows the cue
});
test('a decoder with no event reports nothing playing rather than stale cues', async () => {
await test('a decoder with no event reports nothing playing rather than stale cues', async () => {
world.eventId = '';
tickFn();
await settle();
@@ -174,13 +175,13 @@ function collect(route, req) {
assert.strictEqual(handle.state.event, '');
});
test('health speaks about the upstream, not about itself', () => {
await test('health speaks about the upstream, not about itself', () => {
const h = handle.health();
assert.ok(['ok', 'connecting', 'error'].includes(h.status), h.status);
assert.ok(h.message.length > 0, 'health should say something useful');
});
test('an unconfigured module idles instead of hammering Resi', () => {
await test('an unconfigured module idles instead of hammering Resi', () => {
const before = world.logins;
const idle = server.init({ config: {}, log: () => {} });
assert.strictEqual(world.logins, before, 'must not sign in with no credentials');
@@ -189,7 +190,7 @@ function collect(route, req) {
idle.stop();
});
test('stop() leaves nothing running', () => {
await test('stop() leaves nothing running', () => {
handle.stop();
const res = collect(routes['GET /state']);
assert.strictEqual(JSON.parse(res.body).status, 'error');