Files
TREK/server/tests/unit/plugins/daily-budget.test.ts
T
2026-10-06 21:50:59 +02:00

68 lines
3.2 KiB
TypeScript

/**
* Per-plugin daily broker budget (#plugins hardening): a per-kind daily counter
* with a UTC-midnight rollover, clock injected so it tests without timers.
*/
import { DailyBudget, envCap } from '../../../src/nest/plugins/host/daily-budget';
import { describe, it, expect } from 'vitest';
const CFG = { aiPerDay: 3, notifyPerDay: 2 };
const t = (iso: string) => new Date(iso).getTime();
describe('DailyBudget', () => {
it('allows up to the per-kind daily cap, then refuses', () => {
const b = new DailyBudget(CFG, t('2026-07-10T09:00:00Z'));
expect(b.take('ai', t('2026-07-10T09:00:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T10:00:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T11:00:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T12:00:00Z'))).toBe(false); // ai cap 3 spent
// notify is a separate budget, still available
expect(b.take('notify', t('2026-07-10T12:00:00Z'))).toBe(true);
expect(b.take('notify', t('2026-07-10T12:00:00Z'))).toBe(true);
expect(b.take('notify', t('2026-07-10T12:00:00Z'))).toBe(false); // notify cap 2 spent
});
it('resets both counters at UTC midnight', () => {
const b = new DailyBudget(CFG, t('2026-07-10T23:00:00Z'));
expect(b.take('ai', t('2026-07-10T23:00:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T23:30:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T23:45:00Z'))).toBe(true);
expect(b.take('ai', t('2026-07-10T23:59:00Z'))).toBe(false); // spent for the 10th
expect(b.take('ai', t('2026-07-11T00:01:00Z'))).toBe(true); // new UTC day -> reset
});
it("seeds today's already-spent counts so a restart continues the same day", () => {
const b = new DailyBudget(CFG, t('2026-07-10T12:00:00Z'), { ai: 2, notify: 2 });
expect(b.take('ai', t('2026-07-10T12:00:00Z'))).toBe(true); // 3rd ai
expect(b.take('ai', t('2026-07-10T12:00:00Z'))).toBe(false); // cap already reached
expect(b.take('notify', t('2026-07-10T12:00:00Z'))).toBe(false); // seeded at cap
});
it('reports usage for the admin view', () => {
const b = new DailyBudget(CFG, t('2026-07-10T12:00:00Z'), { ai: 1 });
b.take('ai', t('2026-07-10T12:00:00Z'));
expect(b.used(t('2026-07-10T12:00:00Z'))).toMatchObject({ ai: 2, aiMax: 3, notify: 0, notifyMax: 2 });
});
it('a cap of 0 disables the broker entirely (take always refuses)', () => {
const b = new DailyBudget({ aiPerDay: 0, notifyPerDay: 5 }, t('2026-07-10T12:00:00Z'));
expect(b.take('ai', t('2026-07-10T12:00:00Z'))).toBe(false); // 0 means off, not unlimited
expect(b.take('notify', t('2026-07-10T12:00:00Z'))).toBe(true);
});
it('envCap honours a literal 0 and only falls back on absent/malformed values', () => {
const KEY = 'TREK_TEST_CAP_XYZ';
delete process.env[KEY];
expect(envCap(KEY, 200)).toBe(200); // absent -> default
process.env[KEY] = '0';
expect(envCap(KEY, 200)).toBe(0); // literal 0 honoured (would have been 200 with `|| def`)
process.env[KEY] = '50';
expect(envCap(KEY, 200)).toBe(50);
process.env[KEY] = 'nonsense';
expect(envCap(KEY, 200)).toBe(200); // malformed -> default
process.env[KEY] = '-5';
expect(envCap(KEY, 200)).toBe(200); // negative -> default
delete process.env[KEY];
});
});