learning_ai_clock/web/src/lib/cascade.test.ts

135 lines
4.7 KiB
TypeScript

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