import { describe, expect, it, vi } from 'vitest'; import { WaveCoordinator, type WaveFeedback } from './wave'; function deferred() { let resolve!: (v: T) => void; let reject!: (e: Error) => void; const promise = new Promise((yes, no) => { resolve = yes; reject = no; }); return { promise, resolve, reject }; } describe('wave coordination', () => { it('preserves unsent feedback when changing context in the same session', async () => { const next = vi.fn().mockResolvedValue(1); const wave = new WaveCoordinator({ next }); wave.activate('a'); wave.note('track', 'like'); wave.changeContext('a'); await wave.refill(0, true, () => {}); expect(next).toHaveBeenCalledWith('a', [{ track_id: 'track', outcome: 'like' }]); }); it('ignores late results and allows the new session to refill immediately', async () => { const old = deferred(); const next = vi.fn().mockReturnValueOnce(old.promise).mockResolvedValueOnce(2); const wave = new WaveCoordinator({ next }); const apply = vi.fn(); wave.activate('old'); const first = wave.refill(0, false, apply); wave.activate('new'); await wave.refill(0, false, apply); old.resolve(1); await first; expect(apply.mock.calls.map(([value]) => value)).toEqual([2]); }); it('retries feedback in order and respects threshold, refresh and single in-flight request', async () => { const request = deferred(); const next = vi.fn().mockReturnValueOnce(request.promise).mockResolvedValueOnce(2); const wave = new WaveCoordinator({ next }); const apply = vi.fn(); wave.activate('a'); wave.note('one', 'skip_early'); await wave.refill(3, false, apply); expect(next).not.toHaveBeenCalled(); const first = wave.refill(3, true, apply); wave.note('two', 'like'); await wave.refill(0, false, apply); expect(next).toHaveBeenCalledTimes(1); request.reject(new Error('offline')); await first; await wave.refill(2, false, apply); expect(next.mock.calls[1]).toEqual(['a', [ { track_id: 'one', outcome: 'skip_early' }, { track_id: 'two', outcome: 'like' }, ] satisfies WaveFeedback[]]); expect(apply).toHaveBeenCalledTimes(1); }); it('does not restore old feedback or clear a newer in-flight request after a stale failure', async () => { const old = deferred(); const fresh = deferred(); const next = vi.fn().mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise); const wave = new WaveCoordinator({ next }); wave.activate('a'); wave.note('old', 'dislike'); const first = wave.refill(0, false, () => {}); wave.activate('a'); // same session, new context wave.note('new', 'like'); const second = wave.refill(0, false, () => {}); old.reject(new Error('late failure')); await first; await wave.refill(0, false, () => {}); expect(next).toHaveBeenCalledTimes(2); expect(next.mock.calls[1]?.[1]).toEqual([{ track_id: 'new', outcome: 'like' }]); fresh.resolve(2); await second; }); });