Sessions 5-7a: 955 tests, deployment ready
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
const { createLimiter, createCircuitBreaker, API_BUDGETS } = require('../../src/utils/rateLimiter');
|
||||
|
||||
describe('createLimiter', () => {
|
||||
test('first N tokens within capacity resolve immediately', async () => {
|
||||
const limiter = createLimiter({ tokensPerInterval: 3, interval: 60_000 });
|
||||
const start = Date.now();
|
||||
expect(await limiter.waitForToken()).toBe(true);
|
||||
expect(await limiter.waitForToken()).toBe(true);
|
||||
expect(await limiter.waitForToken()).toBe(true);
|
||||
expect(Date.now() - start).toBeLessThan(100);
|
||||
});
|
||||
|
||||
test('fourth token in a small bucket waits for refill', async () => {
|
||||
const limiter = createLimiter({ tokensPerInterval: 2, interval: 200 });
|
||||
expect(await limiter.waitForToken()).toBe(true);
|
||||
expect(await limiter.waitForToken()).toBe(true);
|
||||
const start = Date.now();
|
||||
expect(await limiter.waitForToken(2_000)).toBe(true);
|
||||
const elapsed = Date.now() - start;
|
||||
// Should take roughly interval/tokens = 100ms before refill yields one.
|
||||
expect(elapsed).toBeGreaterThan(50);
|
||||
expect(elapsed).toBeLessThan(500);
|
||||
});
|
||||
|
||||
test('timeout returns false (does not throw) so caller can proceed', async () => {
|
||||
const limiter = createLimiter({ tokensPerInterval: 1, interval: 60_000 });
|
||||
expect(await limiter.waitForToken()).toBe(true); // consume the one token
|
||||
const result = await limiter.waitForToken(120); // wait briefly, then bail
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
test('throws on invalid config', () => {
|
||||
expect(() => createLimiter({ tokensPerInterval: 0, interval: 1000 })).toThrow();
|
||||
expect(() => createLimiter({ tokensPerInterval: 1, interval: 0 })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('createCircuitBreaker', () => {
|
||||
test('closed initially; success resets failure count', async () => {
|
||||
const cb = createCircuitBreaker({ failureThreshold: 2, resetTimeout: 60_000 });
|
||||
const result = await cb.call(async () => 'ok');
|
||||
expect(result).toBe('ok');
|
||||
expect(cb.snapshot().state).toBe('closed');
|
||||
});
|
||||
|
||||
test('opens after threshold failures and rejects further calls', async () => {
|
||||
const cb = createCircuitBreaker({ failureThreshold: 2, resetTimeout: 60_000 });
|
||||
await expect(cb.call(async () => { throw new Error('boom'); })).rejects.toThrow('boom');
|
||||
await expect(cb.call(async () => { throw new Error('boom'); })).rejects.toThrow('boom');
|
||||
expect(cb.snapshot().state).toBe('open');
|
||||
await expect(cb.call(async () => 'should never run')).rejects.toMatchObject({ code: 'CIRCUIT_OPEN' });
|
||||
});
|
||||
|
||||
test('transitions to half-open after resetTimeout, closes on success', async () => {
|
||||
const cb = createCircuitBreaker({ failureThreshold: 1, resetTimeout: 50 });
|
||||
await expect(cb.call(async () => { throw new Error('x'); })).rejects.toThrow();
|
||||
expect(cb.snapshot().state).toBe('open');
|
||||
await new Promise((r) => setTimeout(r, 80));
|
||||
// Reading the snapshot triggers the transition check.
|
||||
expect(cb.snapshot().state).toBe('half_open');
|
||||
const result = await cb.call(async () => 'recovered');
|
||||
expect(result).toBe('recovered');
|
||||
expect(cb.snapshot().state).toBe('closed');
|
||||
});
|
||||
|
||||
test('half-open failure re-opens the circuit immediately', async () => {
|
||||
const cb = createCircuitBreaker({ failureThreshold: 1, resetTimeout: 30 });
|
||||
await expect(cb.call(async () => { throw new Error('first'); })).rejects.toThrow();
|
||||
await new Promise((r) => setTimeout(r, 60));
|
||||
await expect(cb.call(async () => { throw new Error('second'); })).rejects.toThrow('second');
|
||||
expect(cb.snapshot().state).toBe('open');
|
||||
});
|
||||
});
|
||||
|
||||
describe('API_BUDGETS', () => {
|
||||
test('contains the five named upstreams', () => {
|
||||
expect(API_BUDGETS).toMatchObject({
|
||||
sharpApi: { tokensPerInterval: 10, interval: 60_000 },
|
||||
espn: { tokensPerInterval: 2, interval: 60_000 },
|
||||
mlbStats: { tokensPerInterval: 2, interval: 60_000 },
|
||||
oddsPapi: { tokensPerInterval: 5, interval: 60_000 },
|
||||
openRouter: { tokensPerInterval: 15, interval: 60_000 },
|
||||
});
|
||||
});
|
||||
|
||||
test('is frozen — adapters cannot mutate it accidentally', () => {
|
||||
expect(Object.isFrozen(API_BUDGETS)).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user