mirror of
https://github.com/screentinker/screentinker.git
synced 2026-08-13 22:03:13 -06:00
The calendar is the operator's only view of what is scheduled, and it disagreed with the engine in
both directions for the two most-used repeat presets.
The expansion stepped by the recurrence unit from the schedule's original start:
- WEEKLY advanced a whole week at a time, so dayOfWeek never changed and a
FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR rule could only ever match its start day. Created on a Monday
it drew one event a week; created on a Saturday it drew nothing at all.
- The walk began at the original start under a 366-iteration cap, so a schedule begun more than a
year ago never reached the current week and drew nothing.
The engine evaluates day-of-week directly, so those schedules were running Mon-Fri the whole time.
Screens switched content the calendar said was not scheduled.
The expansion now walks the visible range day by day and applies the same rule the engine does, so
the drawing follows what actually happens. Cost is bounded by the window being displayed rather than
by how long ago the schedule was created, and the loop re-anchors the time of day on each step so a
DST boundary does not drift the instances.
Overlap is left to resolve as it already does: a shorter, higher-priority schedule takes over while
it is active and the recurring one resumes underneath when it ends. Nothing here changes what fires
— only what is shown — so this cannot alter live screens.
8 tests: five events for a Mon-Fri rule whichever day it was created on, a two-year-old daily
schedule drawing again, WEEKLY-without-byDay still meaning the start's weekday, INTERVAL honoured,
recurrence_end stopping the drawing, one-offs unaffected, and durations preserved.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Uaeo9MvzKoyXuN6ZsbhtkL
92 lines
4 KiB
JavaScript
92 lines
4 KiB
JavaScript
'use strict';
|
|
|
|
// The calendar is the operator's only view of what is scheduled, and it disagreed with the engine
|
|
// in both directions for the two most-used repeat presets.
|
|
//
|
|
// The old expansion stepped by the recurrence unit from the schedule's original start:
|
|
// - WEEKLY advanced a whole week at a time, so dayOfWeek never changed and a
|
|
// FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR rule matched only its start day. Created on a Monday it drew
|
|
// one event a week; created on a Saturday it drew nothing at all.
|
|
// - The walk began at the original start under a 366-iteration cap, so a schedule begun more than
|
|
// a year ago never reached the current week and drew nothing.
|
|
// Meanwhile the engine evaluates day-of-week directly, so those schedules ran Mon-Fri the whole
|
|
// time. Screens were switching content that the calendar said was not scheduled.
|
|
//
|
|
// The invariant: the calendar draws an event on every day the schedule actually fires.
|
|
|
|
const { test } = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const path = require('path');
|
|
const fs = require('fs');
|
|
const os = require('os');
|
|
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'st-cal-'));
|
|
process.env.DATA_DIR = tmp;
|
|
process.env.JWT_SECRET = 'test-secret-calendar';
|
|
|
|
const { expandSchedule } = require('../routes/schedules');
|
|
|
|
// A Monday-to-Sunday window well clear of the schedules' start dates.
|
|
const WEEK_START = new Date('2026-08-03T00:00:00'); // Monday
|
|
const WEEK_END = new Date('2026-08-09T23:59:59'); // Sunday
|
|
|
|
const mk = (recurrence, startISO, recurrenceEnd = null) => ({
|
|
id: 's', recurrence, recurrence_end: recurrenceEnd,
|
|
start_time: startISO,
|
|
end_time: new Date(new Date(startISO).getTime() + 60 * 60 * 1000).toISOString(),
|
|
});
|
|
const weekdaysOf = (events) => events.map(e => new Date(e.instance_start).getDay()).sort();
|
|
|
|
test('THE BUG: a Mon-Fri rule draws five events, not one', () => {
|
|
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-07-27T09:00:00'), WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 5);
|
|
assert.deepEqual(weekdaysOf(ev), [1, 2, 3, 4, 5], 'Mon..Fri');
|
|
});
|
|
|
|
test('...and it does not matter which day the rule was created on', () => {
|
|
// Created on a Saturday, the old code drew nothing whatsoever.
|
|
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-08-01T09:00:00'), WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 5);
|
|
assert.deepEqual(weekdaysOf(ev), [1, 2, 3, 4, 5]);
|
|
});
|
|
|
|
test('a DAILY schedule older than a year still draws', () => {
|
|
// The 366-iteration cap meant the walk never reached the visible window.
|
|
const ev = expandSchedule(mk('FREQ=DAILY', '2024-05-01T09:00:00'), WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 7, 'every day of the week');
|
|
});
|
|
|
|
test('WEEKLY without byDay still means "the same weekday as the start"', () => {
|
|
const ev = expandSchedule(mk('FREQ=WEEKLY', '2026-07-27T09:00:00'), WEEK_START, WEEK_END); // a Monday
|
|
assert.equal(ev.length, 1);
|
|
assert.deepEqual(weekdaysOf(ev), [1]);
|
|
});
|
|
|
|
test('a DAILY interval is honoured rather than drawn every day', () => {
|
|
const ev = expandSchedule(mk('FREQ=DAILY;INTERVAL=2', '2026-08-03T09:00:00'), WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 4, 'Mon, Wed, Fri, Sun');
|
|
});
|
|
|
|
test('recurrence_end stops the drawing', () => {
|
|
const ev = expandSchedule(
|
|
mk('FREQ=DAILY', '2026-07-27T09:00:00', '2026-08-05T23:59:59'), WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 3, 'Mon, Tue, Wed then it ends');
|
|
});
|
|
|
|
test('a one-off schedule is unaffected', () => {
|
|
const ev = expandSchedule(
|
|
{ id: 's', recurrence: null, start_time: '2026-08-05T09:00:00', end_time: '2026-08-05T10:00:00' },
|
|
WEEK_START, WEEK_END);
|
|
assert.equal(ev.length, 1);
|
|
});
|
|
|
|
test('each drawn event keeps the schedule duration', () => {
|
|
const ev = expandSchedule(mk('FREQ=WEEKLY;BYDAY=MO,TU,WE,TH,FR', '2026-07-27T09:00:00'), WEEK_START, WEEK_END);
|
|
for (const e of ev) {
|
|
const mins = (new Date(e.instance_end) - new Date(e.instance_start)) / 60000;
|
|
assert.equal(mins, 60);
|
|
}
|
|
});
|
|
|
|
test.after(() => { try { fs.rmSync(tmp, { recursive: true, force: true }); } catch (_) {} });
|