import { describe, expect, it, vi } from 'vitest' import fc from 'fast-check' import { KeyRange } from '../../src/utils/key-range.js' import { KeyCounter } from '../../src/utils/key-counter.js' import { SerialQueue } from '../../src/utils/serial-queue.js' import { RetryTimer } from '../../src/utils/retry-timer.js' import { Keys } from '../../src/utils/keys.js' describe('KeyRange', () => { it('contains a key and its subtree, not lookalikes', () => { const range = new KeyRange('ab') expect(['ab', 'ab/c', 'ab/c/d'].every((k) => range.contains(k))).toBe(true) expect(['a', 'abc', 'ab0', 'ab~2D1', 'b'].some((k) => range.contains(k))).toBe(false) expect(new KeyRange('').contains('anything')).toBe(true) }) it('agrees with its scan bounds for escaped keys (property)', () => { const path = fc.array(fc.string({ unit: fc.constantFrom('a', 'b', '-', '/', '~', '0', '.'), minLength: 1, maxLength: 3 }), { minLength: 1, maxLength: 3 }) fc.assert( fc.property(path, path, (p, q) => { const range = new KeyRange(Keys.join(p)) const key = Keys.join(q) expect(range.contains(key)).toBe(key >= range.gte! && key < range.lt!) }), ) }) it('clamps and checks scan ranges', () => { const range = new KeyRange('ab') expect(range.clamp({ limit: 5 })).toEqual({ limit: 5, gte: 'ab', lt: 'ab0' }) expect(range.clamp({ gte: 'ab/x', lt: 'zz' })).toEqual({ gte: 'ab/x', lt: 'ab0' }) expect(range.clamp({ gte: 'b' })).toBeUndefined() expect(range.covers('ab/', 'ab0')).toBe(true) expect(range.covers('a', 'ab0')).toBe(false) expect(range.covers(undefined, 'ab0')).toBe(false) expect(new KeyRange('').covers(undefined, undefined)).toBe(true) }) }) describe('KeyCounter', () => { it('counts per key and in total', () => { const counter = new KeyCounter() counter.add(['a', 'a', 'b']) expect(counter.size).toBe(3) expect(counter.take('a')).toBe(true) expect(counter.has('a')).toBe(true) expect(counter.take('a')).toBe(true) expect(counter.has('a')).toBe(false) expect(counter.take('a')).toBe(false) expect(counter.size).toBe(1) counter.clear() expect(counter.size).toBe(0) }) }) describe('SerialQueue', () => { it('runs tasks in order per key, keys independently, and survives failures', async () => { const queue = new SerialQueue() const log: string[] = [] const task = (name: string, ms: number) => () => new Promise((r) => setTimeout(() => (log.push(name), r()), ms)) const a1 = queue.run(task('a1', 20), 'a') const failed = queue.run(() => Promise.reject(new Error('boom')), 'a') const a2 = queue.run(task('a2', 1), 'a') const b1 = queue.run(task('b1', 1), 'b') await expect(failed).rejects.toThrow('boom') await Promise.all([a1, a2, b1]) expect(log).toEqual(['b1', 'a1', 'a2']) await queue.idle('a') }) }) describe('RetryTimer', () => { it('replaces a pending retry and stops cleanly', () => { vi.useFakeTimers() const timer = new RetryTimer({ baseDelay: 10, maxDelay: 10 }) const first = vi.fn() const second = vi.fn() timer.schedule(first) timer.schedule(second) vi.advanceTimersByTime(20) expect(first).not.toHaveBeenCalled() expect(second).toHaveBeenCalledOnce() timer.schedule(first) timer.stop() vi.advanceTimersByTime(20) expect(first).not.toHaveBeenCalled() vi.useRealTimers() }) })