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'); }); });