feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,182 @@
|
||||
/**
|
||||
* AAO Gateway Phase 2b — gateway_key_usage repository tests.
|
||||
*
|
||||
* Coverage:
|
||||
* - getGatewayKeyUsage returns null on miss, row on hit
|
||||
* - incrementGatewayKeyUsage UPSERTs first call, accumulates on second
|
||||
* - incrementGatewayKeyUsage clamps negative deltas to zero
|
||||
* - listGatewayKeyUsagesByKey orders by period_start DESC
|
||||
* - Cascade delete removes usage rows when key is deleted
|
||||
* - tokens_budget / rate_limit_rpm fields persist & round-trip
|
||||
* - updateGatewayVirtualKey patches fields independently and supports null reset
|
||||
*/
|
||||
import { describe, expect, it, beforeEach } from 'vitest';
|
||||
import { Repository } from './repository.js';
|
||||
import { hashKey } from '../gateway/key-format.js';
|
||||
|
||||
function makeRepo(): Repository {
|
||||
// ':memory:' triggers Repository.initSchema (creates all tables fresh).
|
||||
return new Repository(':memory:');
|
||||
}
|
||||
|
||||
function seedKey(repo: Repository, raw: string, team = 'alpha'): string {
|
||||
return repo.createGatewayVirtualKey({
|
||||
keyHash: hashKey(raw),
|
||||
keyPrefix: raw.slice(0, 14),
|
||||
team,
|
||||
}).id;
|
||||
}
|
||||
|
||||
describe('gateway_key_usage repository', () => {
|
||||
let repo: Repository;
|
||||
beforeEach(() => {
|
||||
repo = makeRepo();
|
||||
});
|
||||
|
||||
it('getGatewayKeyUsage returns null when no row exists', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-1');
|
||||
expect(repo.getGatewayKeyUsage(id, '2026-05')).toBeNull();
|
||||
});
|
||||
|
||||
it('incrementGatewayKeyUsage creates row on first call, accumulates on second', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-2');
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: 100, tokensOut: 50, requests: 1 });
|
||||
let usage = repo.getGatewayKeyUsage(id, '2026-05');
|
||||
expect(usage).not.toBeNull();
|
||||
expect(usage!.tokensIn).toBe(100);
|
||||
expect(usage!.tokensOut).toBe(50);
|
||||
expect(usage!.requests).toBe(1);
|
||||
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: 200, tokensOut: 80, requests: 1 });
|
||||
usage = repo.getGatewayKeyUsage(id, '2026-05');
|
||||
expect(usage!.tokensIn).toBe(300);
|
||||
expect(usage!.tokensOut).toBe(130);
|
||||
expect(usage!.requests).toBe(2);
|
||||
});
|
||||
|
||||
it('different periods get separate rows', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-3');
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-04', tokensIn: 10, requests: 1 });
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: 20, requests: 1 });
|
||||
expect(repo.getGatewayKeyUsage(id, '2026-04')!.tokensIn).toBe(10);
|
||||
expect(repo.getGatewayKeyUsage(id, '2026-05')!.tokensIn).toBe(20);
|
||||
});
|
||||
|
||||
it('clamps negative deltas to zero', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-4');
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: 100 });
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: -500, tokensOut: -10, requests: -1 });
|
||||
const usage = repo.getGatewayKeyUsage(id, '2026-05')!;
|
||||
expect(usage.tokensIn).toBe(100); // unchanged
|
||||
expect(usage.tokensOut).toBe(0);
|
||||
expect(usage.requests).toBe(0);
|
||||
});
|
||||
|
||||
it('listGatewayKeyUsagesByKey returns rows newest period first', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-5');
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-03', requests: 1 });
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', requests: 1 });
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-04', requests: 1 });
|
||||
const list = repo.listGatewayKeyUsagesByKey(id);
|
||||
expect(list.map(u => u.periodStart)).toEqual(['2026-05', '2026-04', '2026-03']);
|
||||
});
|
||||
|
||||
it('listGatewayKeyUsagesByKey honors limit', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-6');
|
||||
for (const m of ['2026-01', '2026-02', '2026-03', '2026-04']) {
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: m, requests: 1 });
|
||||
}
|
||||
const list = repo.listGatewayKeyUsagesByKey(id, { limit: 2 });
|
||||
expect(list).toHaveLength(2);
|
||||
expect(list[0]!.periodStart).toBe('2026-04');
|
||||
});
|
||||
|
||||
it('cascade-deletes usage rows when key is hard-deleted', () => {
|
||||
const id = seedKey(repo, 'sk-aao-test-7');
|
||||
repo.incrementGatewayKeyUsage({ keyId: id, period: '2026-05', tokensIn: 1 });
|
||||
expect(repo.getGatewayKeyUsage(id, '2026-05')).not.toBeNull();
|
||||
|
||||
// The Repository's defense-in-depth guard refuses to delete
|
||||
// config-import rows. Seeded above as 'admin' so this is legal.
|
||||
repo.deleteGatewayVirtualKey(id);
|
||||
expect(repo.getGatewayKeyUsage(id, '2026-05')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('gateway_virtual_keys budget/rate fields', () => {
|
||||
let repo: Repository;
|
||||
beforeEach(() => {
|
||||
repo = makeRepo();
|
||||
});
|
||||
|
||||
it('round-trips tokensBudget and rateLimitRpm on create', () => {
|
||||
const created = repo.createGatewayVirtualKey({
|
||||
keyHash: hashKey('sk-aao-create-budget'),
|
||||
keyPrefix: 'sk-aao-create',
|
||||
team: 'alpha',
|
||||
tokensBudget: 1_000_000,
|
||||
rateLimitRpm: 60,
|
||||
});
|
||||
expect(created.tokensBudget).toBe(1_000_000);
|
||||
expect(created.rateLimitRpm).toBe(60);
|
||||
|
||||
const refreshed = repo.findGatewayVirtualKeyById(created.id)!;
|
||||
expect(refreshed.tokensBudget).toBe(1_000_000);
|
||||
expect(refreshed.rateLimitRpm).toBe(60);
|
||||
});
|
||||
|
||||
it('defaults to null when budget/rate omitted', () => {
|
||||
const created = repo.createGatewayVirtualKey({
|
||||
keyHash: hashKey('sk-aao-no-limits'),
|
||||
keyPrefix: 'sk-aao-no-lim',
|
||||
team: 'beta',
|
||||
});
|
||||
expect(created.tokensBudget).toBeNull();
|
||||
expect(created.rateLimitRpm).toBeNull();
|
||||
});
|
||||
|
||||
it('coerces zero or negative limits to null (defensive)', () => {
|
||||
const created = repo.createGatewayVirtualKey({
|
||||
keyHash: hashKey('sk-aao-bad-limits'),
|
||||
keyPrefix: 'sk-aao-bad',
|
||||
team: 'gamma',
|
||||
tokensBudget: 0,
|
||||
rateLimitRpm: -5,
|
||||
});
|
||||
expect(created.tokensBudget).toBeNull();
|
||||
expect(created.rateLimitRpm).toBeNull();
|
||||
});
|
||||
|
||||
it('updateGatewayVirtualKey patches only specified fields', () => {
|
||||
const created = repo.createGatewayVirtualKey({
|
||||
keyHash: hashKey('sk-aao-patch'),
|
||||
keyPrefix: 'sk-aao-patch',
|
||||
team: 'alpha',
|
||||
tokensBudget: 1000,
|
||||
rateLimitRpm: 60,
|
||||
allowedModels: ['qwen3:8b'],
|
||||
});
|
||||
// Patch only tokensBudget — other fields untouched
|
||||
const after1 = repo.updateGatewayVirtualKey(created.id, { tokensBudget: 5000 });
|
||||
expect(after1.tokensBudget).toBe(5000);
|
||||
expect(after1.rateLimitRpm).toBe(60);
|
||||
expect(after1.allowedModels).toEqual(['qwen3:8b']);
|
||||
|
||||
// Patch allowedModels alone
|
||||
const after2 = repo.updateGatewayVirtualKey(created.id, { allowedModels: ['qwen3:14b'] });
|
||||
expect(after2.allowedModels).toEqual(['qwen3:14b']);
|
||||
expect(after2.tokensBudget).toBe(5000);
|
||||
|
||||
// Reset rate limit to null (unlimited)
|
||||
const after3 = repo.updateGatewayVirtualKey(created.id, { rateLimitRpm: null });
|
||||
expect(after3.rateLimitRpm).toBeNull();
|
||||
|
||||
// Reset allowedModels to null (no allowlist)
|
||||
const after4 = repo.updateGatewayVirtualKey(created.id, { allowedModels: null });
|
||||
expect(after4.allowedModels).toBeNull();
|
||||
});
|
||||
|
||||
it('updateGatewayVirtualKey throws for unknown id', () => {
|
||||
expect(() => repo.updateGatewayVirtualKey('does-not-exist', { tokensBudget: 1 })).toThrow();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user