'use strict'; /** * Unit tests for the PCO Plan module's pure service-timing logic. * * node modules/pco-plan/service-timing.test.js * * Plain Node, no dependencies; exits non-zero on the first failure. Times are * built for a two-service Sunday (9:00 and 11:00, 75 min each, 30 min * pre-roll) so the expectations read like the day would. */ const assert = require('assert'); const { similarity, isCue, MATCH_THRESHOLD, TRIGGER_THRESHOLD } = require('./matching'); const { MINUTE, LAST_SERVICE_OVERRUN_MS, serviceWindows, selectService, freshService, advance, } = require('./service-timing'); const DAY = new Date(2026, 8, 13, 0, 0, 0, 0).getTime(); // a local midnight const at = (h, m = 0) => DAY + (h * 60 + m) * MINUTE; const hm = (ms) => new Date(ms).toLocaleTimeString([], { hour: 'numeric', minute: '2-digit' }); const PLAN_TIMES = [ { id: 'r', name: 'Rehearsal', timeType: 'rehearsal', startsAt: at(8, 0), endsAt: at(8, 45) }, { id: 's1', name: 'First Service', timeType: 'service', startsAt: at(9, 0), endsAt: at(10, 15) }, { id: 's2', name: 'Second Service', timeType: 'service', startsAt: at(11, 0), endsAt: at(12, 15) }, ]; const PRE_ROLL = 30 * MINUTE; const OPTS = { preRollMs: PRE_ROLL }; let passed = 0; function test(name, fn) { try { fn(); passed += 1; console.log(` ok ${name}`); } catch (err) { console.log(` FAIL ${name}\n ${err.message}`); process.exitCode = 1; throw err; } } console.log('service windows'); test('rehearsal is not a service; windows are contiguous and ordered', () => { const w = serviceWindows(PLAN_TIMES, OPTS); assert.deepStrictEqual(w.map((x) => x.id), ['s1', 's2']); assert.strictEqual(w[0].windowFrom, -Infinity); assert.strictEqual(w[0].windowUntil, w[1].windowFrom); assert.strictEqual(w[1].windowUntil, Infinity); assert.strictEqual(w[0].count, 2); }); test('the boundary is 30 min before the next start once the first was due to end', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); // 10:15 scheduled end, 11:00 − 30 = 10:30 → boundary 10:30 assert.strictEqual(hm(w1.windowUntil), hm(at(10, 30))); assert.strictEqual(w1.failsafeEndAt, w1.windowUntil, 'first service failsafe-stops at the boundary'); }); test('a pre-roll larger than the gap waits for the scheduled end, never passes the next start', () => { const [w1] = serviceWindows(PLAN_TIMES, { preRollMs: 60 * MINUTE }); assert.strictEqual(hm(w1.windowUntil), hm(at(10, 15)), 'max(10:00, 10:15)'); const [tight] = serviceWindows( [ { id: 'a', timeType: 'service', startsAt: at(9, 0), endsAt: at(10, 30) }, { id: 'b', timeType: 'service', startsAt: at(10, 0), endsAt: at(11, 0) }, ], OPTS ); assert.strictEqual(hm(tight.windowUntil), hm(at(10, 0)), 'capped at the next start'); }); test('an early actual end lets the next window open at pre-roll', () => { const [w1] = serviceWindows(PLAN_TIMES, { preRollMs: 60 * MINUTE, endedAt: { s1: at(10, 5) } }); assert.strictEqual(hm(w1.windowUntil), hm(at(10, 5)), 'ended at 10:05 inside the 10:00 pre-roll → flips at 10:05'); const [w1b] = serviceWindows(PLAN_TIMES, { preRollMs: PRE_ROLL, endedAt: { s1: at(10, 5) } }); assert.strictEqual(hm(w1b.windowUntil), hm(at(10, 30)), 'ended at 10:05 before the 10:30 pre-roll → flips at 10:30'); }); test('a service without ends_at gets the planned length, else 90 min', () => { const [w] = serviceWindows([{ id: 'x', timeType: 'service', startsAt: at(9, 0), endsAt: 0 }], { plannedMs: 70 * MINUTE }); assert.strictEqual(hm(w.endsAt), hm(at(10, 10))); const [w90] = serviceWindows([{ id: 'x', timeType: 'service', startsAt: at(9, 0), endsAt: 0 }], {}); assert.strictEqual(hm(w90.endsAt), hm(at(10, 30))); assert.strictEqual(w90.failsafeEndAt, w90.endsAt + LAST_SERVICE_OVERRUN_MS); }); test('selectService: 8:50 → 9:00, 9:30 → 9:00, 10:55 → 11:00, 11:05 → 11:00', () => { const w = serviceWindows(PLAN_TIMES, OPTS); assert.strictEqual(selectService(w, at(8, 50)).id, 's1'); assert.strictEqual(selectService(w, at(9, 30)).id, 's1'); assert.strictEqual(selectService(w, at(10, 29)).id, 's1', 'still the first service until the 10:30 boundary'); assert.strictEqual(selectService(w, at(10, 55)).id, 's2'); assert.strictEqual(selectService(w, at(11, 5)).id, 's2'); assert.strictEqual(selectService(w, at(6, 0)).id, 's1', 'the morning belongs to the first service'); assert.strictEqual(selectService(w, at(15, 0)).id, 's2', 'the afternoon stays with the last service'); assert.strictEqual(selectService([], at(9, 0)), null); }); console.log('service start'); const SCHEDULE = { startMode: 'schedule', startDocument: '', endDocument: '', startGraceMs: 10 * MINUTE }; const DOCUMENT = { startMode: 'document', startDocument: 'Welcome & Announcements', endDocument: 'Post Service Loop', startGraceMs: 10 * MINUTE, }; test('schedule mode: idle before 9:00, running from 9:00 counting from 9:00; documents ignored', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); let s = freshService(w1); assert.strictEqual(advance(s, w1, SCHEDULE, at(8, 59), 'Welcome').changed, false, 'a document is not a cue in schedule mode'); assert.strictEqual(advance(s, w1, SCHEDULE, at(8, 59)).changed, false); const out = advance(s, w1, SCHEDULE, at(9, 0, 30)); assert.strictEqual(out.changed, true); s = out.svc; assert.strictEqual(s.phase, 'running'); assert.strictEqual(s.startedAt, at(9, 0), 'counts from the scheduled start, not the tick that noticed'); assert.strictEqual(s.startTrigger, 'schedule'); }); test('document mode: the start document going live starts the service at that moment', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); const s = freshService(w1); assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 0, 30)).changed, false, 'the scheduled time alone does not start it'); const out = advance(s, w1, DOCUMENT, at(8, 58), 'Welcome and Announcements [Full]'); assert.strictEqual(out.changed, true, 'a renamed / tagged start document still counts'); assert.strictEqual(out.svc.phase, 'running'); assert.strictEqual(out.svc.startedAt, at(8, 58), 'an early start is the real start'); assert.strictEqual(out.svc.startTrigger, 'document'); assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 1), 'Post Service Loop').changed, false, 'the end document does not start an idle service'); assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 1), 'Goodness of God').changed, false, 'an unrelated document does nothing'); }); test('document mode: the start document is only a cue from pre-roll before the start', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); const s = freshService(w1); assert.strictEqual(advance(s, w1, DOCUMENT, at(8, 0), 'Welcome & Announcements').changed, false, 'rehearsal at 8:00 shows it — not the service'); assert.strictEqual(advance(s, w1, DOCUMENT, at(8, 30), 'Welcome & Announcements').changed, true, 'from 8:30 it counts'); }); test('document mode: failsafe at start + grace counts from the scheduled start', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); const s = freshService(w1); assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 9, 59)).changed, false); const out = advance(s, w1, DOCUMENT, at(9, 10)); assert.strictEqual(out.changed, true); assert.strictEqual(out.svc.phase, 'running'); assert.strictEqual(out.svc.startedAt, at(9, 0)); assert.strictEqual(out.svc.startTrigger, 'failsafe'); assert.match(out.event, /failsafe/); }); test('document mode: a cue and the failsafe in the same tick — the cue wins', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); const out = advance(freshService(w1), w1, DOCUMENT, at(9, 12), 'Welcome & Announcements'); assert.strictEqual(out.svc.startTrigger, 'document'); assert.strictEqual(out.svc.startedAt, at(9, 12)); }); console.log('service end'); test('the end document freezes a running service; a second start document is ignored', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); let s = advance(freshService(w1), w1, DOCUMENT, at(9, 0), 'Welcome & Announcements').svc; assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 40), 'Welcome & Announcements').changed, false, 'shown again mid-service: nothing'); assert.strictEqual(advance(s, w1, DOCUMENT, at(9, 40), 'Pre Service Loop').changed, false, 'the PRE-service loop must not end it (0.67 < 0.75)'); const out = advance(s, w1, DOCUMENT, at(10, 12), 'Post-Service Loop'); assert.strictEqual(out.changed, true, 'a hyphenated end document still counts'); s = out.svc; assert.strictEqual(s.phase, 'stopped'); assert.strictEqual(s.endedAt, at(10, 12)); assert.strictEqual(s.endTrigger, 'document'); assert.strictEqual(advance(s, w1, DOCUMENT, at(10, 20), 'Welcome & Announcements').changed, false, 'stopped stays stopped'); assert.strictEqual(advance(s, w1, DOCUMENT, at(10, 45)).changed, false); }); test('failsafe: a service still running when the next window opens stops at the boundary', () => { const [w1] = serviceWindows(PLAN_TIMES, OPTS); const s = advance(freshService(w1), w1, SCHEDULE, at(9, 0)).svc; assert.strictEqual(advance(s, w1, SCHEDULE, at(10, 29)).changed, false, 'running past its 10:15 end is allowed'); const out = advance(s, w1, SCHEDULE, at(10, 31)); assert.strictEqual(out.changed, true); assert.strictEqual(out.svc.phase, 'stopped'); assert.strictEqual(hm(out.svc.endedAt), hm(at(10, 30)), 'frozen at the boundary, not at the tick'); assert.strictEqual(out.svc.endTrigger, 'next-service'); }); test('failsafe: the last service stops an hour after its scheduled end', () => { const [, w2] = serviceWindows(PLAN_TIMES, OPTS); const s = advance(freshService(w2), w2, SCHEDULE, at(11, 0)).svc; assert.strictEqual(advance(s, w2, SCHEDULE, at(13, 14)).changed, false); const out = advance(s, w2, SCHEDULE, at(13, 15)); assert.strictEqual(out.changed, true); assert.strictEqual(out.svc.phase, 'stopped'); assert.strictEqual(hm(out.svc.endedAt), hm(at(13, 15))); assert.strictEqual(out.svc.endTrigger, 'plan-end'); }); test('the end document does not stop a service in the wrong window', () => { const w = serviceWindows(PLAN_TIMES, OPTS); const s1 = advance(freshService(w[0]), w[0], DOCUMENT, at(9, 0), 'Welcome & Announcements').svc; assert.strictEqual(advance(s1, w[1], DOCUMENT, at(10, 40), 'Post Service Loop').changed, false, 'state and window must belong together'); }); console.log('matching'); test('plan items: 0.6 — two of three words; cues: 0.75 — pre/post loops stay apart', () => { assert.strictEqual(MATCH_THRESHOLD, 0.6); assert.strictEqual(TRIGGER_THRESHOLD, 0.75); assert.strictEqual(similarity('Post-Service Loop', 'Post Service Loop'), 1); assert.strictEqual(similarity('Post Service Loop [Full]', 'Post Service Loop'), 1); assert.strictEqual(similarity('Post Service', 'Post Service Loop'), 0.85); assert.ok(Math.abs(similarity('Pre Service Loop', 'Post Service Loop') - 2 / 3) < 0.001); assert.strictEqual(isCue('Pre Service Loop', 'Post Service Loop'), false); assert.strictEqual(isCue('Post-Service Loop', 'Post Service Loop'), true); assert.strictEqual(isCue('Welcome', 'Welcome & Announcements'), true); assert.strictEqual(isCue('Anything', ''), false); }); console.log(`\n${passed} tests passed`);