Files
rhythmcal/src/test/scheduler.test.ts
T

84 lines
3.7 KiB
TypeScript

import { describe, it } from 'node:test';
import assert from 'node:assert';
import { DEFAULT_PROFILE, getEnergyLevelAtHour, isRecoveryWindow, energyScore, blocksRecovery, getWindowLabel } from '../lib/energy.js';
import { findFreeSlots, suggestSlot, energyOverlayForDay } from '../lib/scheduler.js';
import type { CalendarEvent } from '../lib/calendar.js';
describe('energy', () => {
it('returns correct energy level at hour 10 (deep work peak)', () => {
assert.strictEqual(getEnergyLevelAtHour(DEFAULT_PROFILE, 10), 5);
});
it('returns recovery at hour 12 (lunch)', () => {
assert.strictEqual(isRecoveryWindow(DEFAULT_PROFILE, 12), true);
});
it('returns recovery at hour 3 (overnight sleep)', () => {
assert.strictEqual(isRecoveryWindow(DEFAULT_PROFILE, 3), true);
});
it('energyScore for 9-12 window is 5.0', () => {
assert.strictEqual(energyScore(DEFAULT_PROFILE, 9, 3), 5.0);
});
it('blocksRecovery for 60 min starting at 11:30 overlaps lunch recovery', () => {
assert.strictEqual(blocksRecovery(DEFAULT_PROFILE, 11, 60), true);
});
it('does not block recovery at 9am for 60 min', () => {
assert.strictEqual(blocksRecovery(DEFAULT_PROFILE, 9, 60), false);
});
it('getWindowLabel returns Deep Work Peak at 10', () => {
assert.strictEqual(getWindowLabel(DEFAULT_PROFILE, 10), 'Deep Work Peak');
});
});
describe('scheduler', () => {
const events: CalendarEvent[] = [
{ id: '1', summary: 'Meeting', start: { dateTime: '2024-06-15T09:00:00' }, end: { dateTime: '2024-06-15T10:00:00' } },
{ id: '2', summary: 'Lunch', start: { dateTime: '2024-06-15T12:00:00' }, end: { dateTime: '2024-06-15T13:00:00' } },
];
it('findFreeSlots returns slots between 6am and 9am, and 10am-12pm', () => {
const slots = findFreeSlots(events, DEFAULT_PROFILE, new Date('2024-06-15'), 30);
assert.ok(slots.length >= 2, `Expected at least 2 slots, got ${slots.length}`);
const firstSlot = slots[0];
assert.strictEqual(firstSlot.start.getHours(), 6);
assert.strictEqual(firstSlot.end.getHours(), 9);
});
it('suggestSlot picks highest energy non-recovery slot for 60 min', () => {
const suggestion = suggestSlot('Deep Work', 60, events, DEFAULT_PROFILE, new Date('2024-06-15'));
assert.ok(suggestion, 'Expected a suggestion');
if (!suggestion) return;
assert.ok(suggestion.energyScore >= 3, `Expected decent energy score, got ${suggestion.energyScore}`);
assert.ok(suggestion.suggestedStart.getHours() >= 6, 'Should start after 6am');
});
it('suggestSlot returns null when no free time exists', () => {
const allDay: CalendarEvent[] = [
{ id: 'x', summary: 'All Day', start: { dateTime: '2024-06-15T06:00:00' }, end: { dateTime: '2024-06-15T22:00:00' } },
];
const suggestion = suggestSlot('Task', 60, allDay, DEFAULT_PROFILE, new Date('2024-06-15'));
assert.strictEqual(suggestion, null);
});
it('energyOverlayForDay returns 24 items', () => {
const overlay = energyOverlayForDay(DEFAULT_PROFILE, new Date('2024-06-15'));
assert.strictEqual(overlay.length, 24);
assert.strictEqual(overlay[10].level, 5);
assert.strictEqual(overlay[12].isRecovery, true);
});
it('findFreeSlots respects minDuration', () => {
const shortEvents: CalendarEvent[] = [
{ id: '1', summary: 'A', start: { dateTime: '2024-06-15T06:00:00' }, end: { dateTime: '2024-06-15T06:45:00' } },
{ id: '2', summary: 'B', start: { dateTime: '2024-06-15T06:50:00' }, end: { dateTime: '2024-06-15T22:00:00' } },
];
const slots = findFreeSlots(shortEvents, DEFAULT_PROFILE, new Date('2024-06-15'), 30);
// 5 min gap between 6:45 and 6:50 is less than 30 min minDuration, so should be 0 slots
assert.strictEqual(slots.length, 0, 'Should not return sub-minDuration gap');
});
});