import { describe, expect, it } from 'vitest'; import type { VoiceRecord } from '../src/core/record'; import { claimable, due, isStaleSent, staleOwn, type Located } from '../src/core/schedule'; const NOW = new Date('2026-09-27T11:15:03.000Z'); const ago = (ms: number) => new Date(NOW.getTime() - ms).toISOString(); const later = (ms: number) => new Date(NOW.getTime() + ms).toISOString(); function item(id: string, over: Partial = {}): Located { return { path: `voice/inbox/${id}.md`, record: { id, status: 'saved', device: 'me', audio: `voice/source/${id}.m4a`, mime: 'audio/mp4', attempts: 0, ...over }, }; } const ids = (xs: Located[]) => xs.map((x) => x.record.id); describe('due', () => { it('takes own saved and retry whose time has come, oldest id first', () => { const items = [ item('c', { status: 'retry', nextAttempt: NOW.toISOString() }), item('a'), item('b', { status: 'retry', nextAttempt: later(1) }), item('d', { device: 'other' }), item('e', { status: 'sent', sentAt: ago(0) }), item('f', { status: 'failed' }), item('g', { status: 'inserted' }), ]; expect(ids(due(items, NOW, 'me'))).toEqual(['a', 'c']); }); it('unparsable nextAttempt is due', () => { expect(ids(due([item('a', { status: 'retry', nextAttempt: 'завтра' })], NOW, 'me'))).toEqual(['a']); }); }); describe('isStaleSent', () => { it.each([ ['exactly 10 min', { status: 'sent' as const, sentAt: ago(600_000) }, false], ['10 min + 1 ms', { status: 'sent' as const, sentAt: ago(600_001) }, true], ['2 min', { status: 'sent' as const, sentAt: ago(120_000) }, false], ['no sentAt', { status: 'sent' as const }, true], ['garbage sentAt', { status: 'sent' as const, sentAt: 'x' }, true], ['not sent', { status: 'retry' as const, sentAt: ago(3_600_000) }, false], ])('%s → %s', (_name, over, expected) => { expect(isStaleSent(item('a', over).record, NOW)).toBe(expected); }); }); describe('staleOwn', () => { it('only own stale sent', () => { const items = [ item('a', { status: 'sent', sentAt: ago(700_000) }), item('b', { status: 'sent', sentAt: ago(700_000), device: 'other' }), item('c', { status: 'sent', sentAt: ago(1_000) }), ]; expect(ids(staleOwn(items, NOW, 'me'))).toEqual(['a']); }); }); describe('claimable', () => { it("others' saved, retry and stale sent only", () => { const o = { device: 'other' }; const items = [ item('a', o), item('b', { ...o, status: 'retry', nextAttempt: later(1_200_000) }), item('c', { ...o, status: 'sent', sentAt: ago(700_000) }), item('d', { ...o, status: 'sent', sentAt: ago(1_000) }), item('e', { ...o, status: 'failed' }), item('f', { ...o, status: 'inserted' }), item('g'), ]; expect(ids(claimable(items, NOW, 'me'))).toEqual(['a', 'b', 'c']); }); });