import { describe, it, expect } from 'vitest'; import { TICK_MS, MAX_OFFLINE_MS, drainTicks } from './tick'; import type { SimState } from '../state'; // Build a minimal SimState fixture inline; Wave-1 plans flesh out tile/plant // shapes and will replace this with a richer factory. function makeState(overrides: Partial = {}): SimState { return { garden: { tiles: [] }, plants: [], harvestedFragmentIds: [], lastTickAt: 0, tickCount: 0, unlockedPlantTypes: [], luraBeatProgress: { arrived: false, mid: false, farewell: false, pending: null }, offlineEvents: null, settings: { musicVolume: 0.7, ambientVolume: 0.5, sfxVolume: 0.8, persistenceToastShown: false, }, ...overrides, }; } // A no-op `simulate` is the Wave-0 placeholder. Wave 1 replaces this with // the real simulate from src/sim/garden/. const noopSim = (state: SimState): SimState => state; // A counting `simulate` lets us verify exact tick application counts. function makeCountingSim(): { sim: (state: SimState, dtMs: number, silent: boolean) => SimState; count: () => number; } { let calls = 0; return { sim: (state) => { calls += 1; return state; }, count: () => calls, }; } describe('TICK_MS / MAX_OFFLINE_MS constants', () => { it('TICK_MS is 200 (5Hz per RESEARCH Pattern 1)', () => { expect(TICK_MS).toBe(200); }); it('MAX_OFFLINE_MS is 24 hours', () => { expect(MAX_OFFLINE_MS).toBe(24 * 3600 * 1000); }); }); describe('drainTicks', () => { it('CORE-11: refuses negative accumulatorMs (state unchanged, 0 ticks applied)', () => { const s = makeState(); const result = drainTicks(s, -1, noopSim); expect(result.state).toBe(s); expect(result.ticksApplied).toBe(0); expect(result.remainderMs).toBe(0); }); it('CORE-03: clamps at MAX_OFFLINE_MS (25h input → 432000 ticks)', () => { const s = makeState(); const expectedTicks = Math.floor(MAX_OFFLINE_MS / TICK_MS); expect(expectedTicks).toBe(432000); const counting = makeCountingSim(); const result = drainTicks(s, 25 * 3600 * 1000, counting.sim); expect(result.ticksApplied).toBe(expectedTicks); expect(counting.count()).toBe(expectedTicks); }); it('exact-tick boundary: 1000ms with TICK_MS=200 calls sim 5 times, remainderMs=0', () => { const s = makeState(); const counting = makeCountingSim(); const result = drainTicks(s, 1000, counting.sim); expect(result.ticksApplied).toBe(5); expect(result.remainderMs).toBe(0); expect(counting.count()).toBe(5); }); it('partial-tick boundary: 1100ms calls sim 5 times, remainderMs=100', () => { const s = makeState(); const counting = makeCountingSim(); const result = drainTicks(s, 1100, counting.sim); expect(result.ticksApplied).toBe(5); expect(result.remainderMs).toBe(100); expect(counting.count()).toBe(5); }); it('benchmark: 432000 ticks complete within 500ms wall time (soft expect)', () => { const s = makeState(); const t0 = performance.now(); drainTicks(s, MAX_OFFLINE_MS, noopSim); const elapsed = performance.now() - t0; // RESEARCH Assumption A3: log + soft expect on the benchmark to avoid // CI flakes on slow runners. The hard guarantee is ticksApplied // correctness; the speed guarantee is a watchdog. expect.soft(elapsed).toBeLessThan(500); }); });