src/writeBudget/window.test.ts
v0.2.1 · 4.1 KB
import { describe, expect, it } from 'vitest';
import {
createWriteWindow,
EXCHANGE_WRITE_LIMIT,
seedWriteWindow,
} from './window.js';
describe('constants', () => {
it('exposes only the exchange limit', () => {
expect(EXCHANGE_WRITE_LIMIT).toBe(40);
});
});
describe('createWriteWindow', () => {
it('gives ordinary writes limit - reserve and critical writes the whole limit', () => {
const w = createWriteWindow({ limit: 5, reserve: 2, now: () => 0 });
let ordinary = 0;
while (w.tryTake(false).ok) ordinary++;
expect(ordinary).toBe(3);
expect(w.budget()).toMatchObject({ used: 3, freeOrdinary: 0, freeCritical: 2 });
let critical = 0;
while (w.tryTake(true).ok) critical++;
expect(critical).toBe(2);
expect(w.budget()).toMatchObject({ used: 5, freeOrdinary: 0, freeCritical: 0 });
const refused = w.tryTake(true);
expect(refused.ok).toBe(false);
if (!refused.ok) expect(refused.rateLimited).toBe(true);
});
it('is a sliding window by timestamps, not a per-tick counter', () => {
const w = createWriteWindow({ limit: 4, reserve: 0, windowMs: 60_000 });
expect(w.tryTake(false, 0).ok).toBe(true);
expect(w.tryTake(false, 15_000).ok).toBe(true);
expect(w.tryTake(false, 30_000).ok).toBe(true);
expect(w.tryTake(false, 45_000).ok).toBe(true);
expect(w.tryTake(false, 59_999).ok).toBe(false); // four ticks, one window
expect(w.tryTake(false, 60_000).ok).toBe(true); // the write at t=0 left the window
expect(w.budget(60_000).used).toBe(4);
});
it('takes the slot before the request and does not give it back', () => {
const w = createWriteWindow({ limit: 1, reserve: 0, now: () => 0 });
expect(w.tryTake(false).ok).toBe(true);
// a failed request cannot "return" the slot: there is no API for it
expect(w.tryTake(false).ok).toBe(false);
});
it('explains a refusal with numbers', () => {
const w = createWriteWindow({ limit: 3, reserve: 1, now: () => 0 });
w.tryTake(false);
w.tryTake(false);
const r = w.tryTake(false);
expect(r.ok).toBe(false);
if (!r.ok)
expect(r.error).toBe('write budget 2/3 per 60s exhausted (1 reserved for cancels and reduceOnly orders)');
});
it('seedWriteWindow spreads a full budget evenly over the past window', () => {
const stamps = seedWriteWindow(5, 60_000);
expect(stamps).toHaveLength(5);
expect(stamps[0]).toBeGreaterThan(0);
expect(stamps[4]).toBeLessThan(60_000);
expect(stamps.every((t, i, a) => i === 0 || t > (a[i - 1] as number))).toBe(true);
expect(seedWriteWindow(0, 1)).toEqual([]);
});
it('seedAsExhausted blocks at start and frees gradually', () => {
const w = createWriteWindow({ limit: 5, reserve: 2, windowMs: 60_000 });
w.seedAsExhausted(100_000);
expect(w.budget(100_000).used).toBe(5);
expect(w.tryTake(true, 100_000).ok).toBe(false);
expect(w.tryTake(true, 111_000).ok).toBe(true);
expect(w.tryTake(false, 121_000).ok).toBe(false);
expect(w.budget(160_001).used).toBeLessThanOrEqual(1);
});
it('seedFromRecentWrites replays a journal, ignoring stale and future stamps', () => {
const w = createWriteWindow({ limit: 5, reserve: 0, windowMs: 60_000 });
w.seedFromRecentWrites([10_000, 50_000, 70_000, 200_000, Number.NaN], 100_000);
expect(w.stamps(100_000)).toEqual([50_000, 70_000]);
expect(w.budget(100_000).used).toBe(2);
});
it('validates options', () => {
expect(() => (createWriteWindow as unknown as () => unknown)()).toThrow(
'createWriteWindow requires explicit limit and reserve',
);
expect(() => createWriteWindow({ limit: 0, reserve: 0 })).toThrow(RangeError);
expect(() => createWriteWindow({ limit: 41, reserve: 0 })).toThrow(RangeError);
expect(() => createWriteWindow({ limit: 5, reserve: 6 })).toThrow(RangeError);
expect(() => createWriteWindow({ limit: 5, reserve: 0, windowMs: 0 })).toThrow(RangeError);
});
it('reset clears the window', () => {
const w = createWriteWindow({ limit: 1, reserve: 0, now: () => 0 });
w.tryTake(false);
w.reset();
expect(w.tryTake(false).ok).toBe(true);
});
});