test(timer): expose pause-over-midnight duration bug

Pause is tracked as pausedAt (TimeOfDay, ms since local midnight) and
paused duration is derived via the naive `clock - pausedAt`. When a pause
spans midnight the clock has wrapped to a small value while pausedAt is
still large, so the subtraction goes negative and every paused-duration
result is corrupted (runtimeState.start resume accumulation, and
getExpectedFinish/getCurrent/getRuntimeOffset in timerUtils).

Add two currently-failing tests that reproduce this:
- runtimeState: full start/pause/resume cycle where the pause crosses
  midnight, asserting pausedDuration and elapsed exclude the pause.
- timerUtils.getRuntimeOffset: over-midnight variant of the paused-offset
  case (the site carrying the "brakes when crossing midnight" TODO).

Both fail today (report ~ -86,100,000 instead of the real 5-minute pause)
and will pass once the pause math adopts the wrap-aware primitives
(timeCore.elapsedTime / epoch-based tracking).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0136N3FnyuUmLJbMNJiZd6YX
This commit is contained in:
Claude
2026-07-14 10:41:50 +00:00
parent daa032e92c
commit 7b4ebbf7f5
2 changed files with 77 additions and 0 deletions
@@ -975,6 +975,36 @@ describe('getRuntimeOffset()', () => {
expect(absolute).toBe(25);
});
it('paused time is delayed time when the pause spans midnight', () => {
const state = {
eventNow: {
id: '1',
timeStart: 23 * MILLIS_PER_HOUR, // 23:00
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
dayOffset: 0,
},
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
timer: {
startedAt: 23 * MILLIS_PER_HOUR, // started on time at 23:00
current: 25, // still counting down
addedTime: 0,
},
_timer: {
pausedAt: 23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE, // 23:58, before midnight
},
rundown: {
actualStart: 23 * MILLIS_PER_HOUR,
plannedStart: 23 * MILLIS_PER_HOUR,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
// paused from 23:58 to 00:03 -> 5 minutes, regardless of the midnight wrap
const { absolute } = getRuntimeOffset(state);
expect(absolute).toBe(5 * MILLIS_PER_MINUTE);
});
it('offset doesnt exist if we havent started', () => {
const state = {
clock: 78480789,
@@ -248,6 +248,53 @@ describe('mutation on runtimeState', () => {
state = getState();
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
});
test('elapsed excludes a pause that spans midnight', async () => {
clearState();
// an event that runs over midnight (23:00 -> 01:00)
const event = {
...mockEvent,
id: 'elapsed-pause-midnight',
timeStart: 23 * MILLIS_PER_HOUR,
timeEnd: 1 * MILLIS_PER_HOUR,
duration: 2 * MILLIS_PER_HOUR,
};
const mockRundown = makeRundown({
entries: { [event.id]: event },
order: [event.id],
});
await initRundown(mockRundown, {});
vi.runAllTimers();
const { metadata, rundown } = rundownCache.get();
// start before midnight
vi.setSystemTime('jan 1 23:50');
load(event, rundown, metadata);
start();
// 8 minutes of active running before we pause
vi.setSystemTime('jan 1 23:58');
update();
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
pause();
// resume 5 minutes later, having crossed midnight (23:58 -> 00:03)
vi.setSystemTime('jan 2 00:03');
start();
let state = getState();
// the pause lasted 5 minutes, regardless of the midnight wrap
expect(state._timer.pausedDuration).toBe(5 * MILLIS_PER_MINUTE);
// elapsed must still exclude the paused time -> only the 8 active minutes
expect(state.timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
// 2 more active minutes after resume -> 10 minutes elapsed
vi.setSystemTime('jan 2 00:05');
update();
state = getState();
expect(state.timer.elapsed).toBe(10 * MILLIS_PER_MINUTE);
});
});
test('runtime offset', async () => {