ci / module boundaries / boundaries (push) Successful in 6s
ci / api / api (push) Successful in 47s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m2s
ci / db / db (push) Successful in 1m4s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 1m30s
ci / ts / ts (packages + web) (push) Successful in 2m54s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 1m14s
77 lines
3.0 KiB
TypeScript
77 lines
3.0 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest';
|
|
import { WaveCoordinator, type WaveFeedback } from './wave';
|
|
|
|
function deferred<T>() {
|
|
let resolve!: (v: T) => void;
|
|
let reject!: (e: Error) => void;
|
|
const promise = new Promise<T>((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<number>({ 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<number>();
|
|
const next = vi.fn().mockReturnValueOnce(old.promise).mockResolvedValueOnce(2);
|
|
const wave = new WaveCoordinator<number>({ 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<number>();
|
|
const next = vi.fn().mockReturnValueOnce(request.promise).mockResolvedValueOnce(2);
|
|
const wave = new WaveCoordinator<number>({ 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<number>();
|
|
const fresh = deferred<number>();
|
|
const next = vi.fn().mockReturnValueOnce(old.promise).mockReturnValueOnce(fresh.promise);
|
|
const wave = new WaveCoordinator<number>({ 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;
|
|
});
|
|
});
|