Files
TREK/server/tests/unit/plugins/plugin-host-state.test.ts
T

70 lines
4.1 KiB
TypeScript

/**
* HS1 (`plugin-host-state.ts#budgetFor`'s seed read) — Plan 3j Task 3. Converted
* from a raw `better-sqlite3` connection to `PluginCapabilityAuditRepository`;
* this covers the seed's grouping (ai.complete + ai.extract vs notify.send),
* the UTC-day floor, and the `code = 'ok'` filter (a denied call never counts
* against the budget).
*/
import { describe, it, expect, beforeAll, beforeEach, afterAll, afterEach, vi } from 'vitest';
import { createSnapshotTestDb } from '../../helpers/db-mock';
import { resetTestDb } from '../../helpers/test-db';
import { createTestOrm, type TestOrm } from '../../helpers/test-orm';
import { PluginCapabilityAudit } from '../../../src/db/entities/PluginCapabilityAudit.entity';
import type { PluginCapabilityAuditRepository } from '../../../src/db/repositories/PluginCapabilityAudit.repository';
import { appendAudit } from '../../../src/nest/plugins/host/plugin-audit';
import { budgetFor, pluginBudgetUsage } from '../../../src/nest/plugins/host/plugin-host-state';
const testDb = createSnapshotTestDb();
let t: TestOrm;
let audit: PluginCapabilityAuditRepository;
beforeAll(async () => {
t = await createTestOrm(testDb);
audit = t.repo(PluginCapabilityAudit);
});
beforeEach(() => { resetTestDb(testDb); t.clear(); });
afterEach(() => { vi.restoreAllMocks(); });
afterAll(async () => { await t.close(); testDb.close(); });
// `budgetFor`'s `budgets` Map is deliberately module-level (see plugin-host-state.ts's
// class docstring) — it is NOT reset by `resetTestDb`/`t.clear()`, so each test below
// uses its own plugin id to avoid one test's seed being served (cached) to the next.
describe('budgetFor / pluginBudgetUsage seed read', () => {
it('HS-001: seeds ai (ai.complete + ai.extract) and notify counts separately from today\'s already-audited rows', async () => {
await appendAudit(audit, { pluginId: 'hs1', actingUserId: 1, method: 'ai.complete', resource: null, code: 'ok' });
await appendAudit(audit, { pluginId: 'hs1', actingUserId: 1, method: 'ai.extract', resource: null, code: 'ok' });
await appendAudit(audit, { pluginId: 'hs1', actingUserId: 1, method: 'notify.send', resource: null, code: 'ok' });
// A denied call must not count against the budget.
await appendAudit(audit, { pluginId: 'hs1', actingUserId: 1, method: 'ai.complete', resource: null, code: 'RESOURCE_FORBIDDEN' });
// A different plugin's calls must not bleed into this one's budget.
await appendAudit(audit, { pluginId: 'hs1-other', actingUserId: 1, method: 'ai.complete', resource: null, code: 'ok' });
const usage = await pluginBudgetUsage('hs1', audit);
expect(usage.ai).toBe(2);
expect(usage.notify).toBe(1);
});
it('HS-002: seeded only once per plugin per process lifetime — a later append is NOT re-counted on a second read', async () => {
await appendAudit(audit, { pluginId: 'hs2', actingUserId: 1, method: 'ai.complete', resource: null, code: 'ok' });
const first = await budgetFor('hs2', audit);
expect(first.used(Date.now()).ai).toBe(1);
// A direct .take() call (what a real dispatch does) advances the in-memory
// counter; the seed read itself is not re-run.
await appendAudit(audit, { pluginId: 'hs2', actingUserId: 1, method: 'ai.complete', resource: null, code: 'ok' });
const second = await budgetFor('hs2', audit);
expect(second).toBe(first); // same cached DailyBudget instance — no re-seed
expect(second.used(Date.now()).ai).toBe(1); // still 1: the second append was never taken through the budget
});
it("HS-003: ignores yesterday's rows — only today's UTC window seeds the count", async () => {
await appendAudit(audit, { pluginId: 'hs3', actingUserId: 1, method: 'ai.complete', resource: null, code: 'ok' });
// Backdate the row to yesterday, bypassing the repository (simulating a
// real prior day's audit history already on disk).
const yesterday = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
testDb.prepare("UPDATE plugin_capability_audit SET ts = ? WHERE plugin_id = 'hs3'").run(yesterday);
const usage = await pluginBudgetUsage('hs3', audit);
expect(usage.ai).toBe(0);
});
});