import { describe, it, expect } from 'vitest'; import { calculateCascadeWarnings, getNextWarning, checkWarnings, getCascadeIntervals, formatMinutesBefore, CASCADE_PRESETS, } from './cascade'; describe('cascade', () => { const ONE_MIN = 60 * 1000; const ONE_HOUR = 60 * ONE_MIN; describe('CASCADE_PRESETS', () => { it('aggressive has 9 intervals', () => { expect(CASCADE_PRESETS.aggressive).toHaveLength(9); }); it('standard has 5 intervals', () => { expect(CASCADE_PRESETS.standard).toHaveLength(5); }); it('none has 0 intervals', () => { expect(CASCADE_PRESETS.none).toHaveLength(0); }); }); describe('calculateCascadeWarnings', () => { it('creates warnings for all intervals', () => { const targetTime = Date.now() + 3 * ONE_HOUR; const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30, 15, 5]); expect(warnings).toHaveLength(5); }); it('marks past warnings as fired', () => { const now = Date.now(); const targetTime = now + 10 * ONE_MIN; // 10 min from now const warnings = calculateCascadeWarnings(targetTime, [30, 15, 5], now); // 30m and 15m warnings are in the past (target is only 10m away) const firedCount = warnings.filter((w) => w.fired).length; expect(firedCount).toBe(2); }); it('returns empty array for empty intervals', () => { const warnings = calculateCascadeWarnings(Date.now() + ONE_HOUR, []); expect(warnings).toHaveLength(0); }); it('sorts warnings by minutesBefore descending (earliest first)', () => { const targetTime = Date.now() + 5 * ONE_HOUR; const warnings = calculateCascadeWarnings(targetTime, [5, 60, 15, 120]); expect(warnings[0].minutesBefore).toBe(120); expect(warnings[3].minutesBefore).toBe(5); }); it('all warnings are in the future when target is far away', () => { const targetTime = Date.now() + 24 * ONE_HOUR; const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30]); expect(warnings.every((w) => !w.fired)).toBe(true); }); }); describe('getNextWarning', () => { it('returns first unfired warning', () => { const targetTime = Date.now() + 2 * ONE_HOUR; const warnings = calculateCascadeWarnings(targetTime, [120, 60, 30, 15, 5]); const next = getNextWarning(warnings); expect(next).not.toBeNull(); expect(next!.fired).toBe(false); }); it('returns null when all warnings are fired', () => { const targetTime = Date.now() - ONE_MIN; // in the past const warnings = calculateCascadeWarnings(targetTime, [5, 1]); const next = getNextWarning(warnings); expect(next).toBeNull(); }); }); describe('checkWarnings', () => { it('fires warnings whose time has come', () => { const now = Date.now(); const targetTime = now + 20 * ONE_MIN; const warnings = calculateCascadeWarnings(targetTime, [30, 15, 5], now); // At now, 30m warning is past (target - 30m < now) // Advance time to 10m before target (5m warning should fire) const laterNow = targetTime - 10 * ONE_MIN; const fired = checkWarnings(warnings, laterNow); expect(fired.length).toBeGreaterThan(0); }); it('does not re-fire already fired warnings', () => { const now = Date.now(); const targetTime = now + 2 * ONE_MIN; const warnings = calculateCascadeWarnings(targetTime, [5], now); // 5m warning is already past since target is 2m away const fired1 = checkWarnings(warnings, now); const fired2 = checkWarnings(warnings, now + 1000); // Second check should not re-fire expect(fired2).toHaveLength(0); expect(fired1.length).toBeGreaterThanOrEqual(0); }); }); describe('getCascadeIntervals', () => { it('returns preset intervals for standard', () => { const intervals = getCascadeIntervals({ preset: 'standard', intervals: [] }); expect(intervals).toEqual(CASCADE_PRESETS.standard); }); it('returns custom intervals sorted descending', () => { const intervals = getCascadeIntervals({ preset: 'custom', intervals: [5, 30, 10] }); expect(intervals).toEqual([30, 10, 5]); }); }); describe('formatMinutesBefore', () => { it('formats minutes under 60', () => { expect(formatMinutesBefore(5)).toBe('5m'); expect(formatMinutesBefore(15)).toBe('15m'); }); it('formats exact hours', () => { expect(formatMinutesBefore(60)).toBe('1h'); expect(formatMinutesBefore(120)).toBe('2h'); }); it('formats hours and minutes', () => { expect(formatMinutesBefore(90)).toBe('1h 30m'); expect(formatMinutesBefore(150)).toBe('2h 30m'); }); }); });