Spaces:
Paused
Paused
| /** | |
| * Tests for UsageStatsStore β snapshot recording, delta computation, aggregation. | |
| */ | |
| import { describe, it, expect, vi, beforeEach } from "vitest"; | |
| vi.mock("../../paths.js", () => ({ | |
| getDataDir: vi.fn(() => "/tmp/test-data"), | |
| })); | |
| import { UsageStatsStore, type UsageStatsPersistence, type UsageSnapshot } from "../usage-stats.js"; | |
| import type { AccountPool } from "../account-pool.js"; | |
| function createMockPersistence(initial: UsageSnapshot[] = []): UsageStatsPersistence & { saved: UsageSnapshot[] } { | |
| const store = { | |
| saved: initial, | |
| load: () => ({ version: 1 as const, snapshots: [...initial] }), | |
| save: vi.fn((data: { version: 1; snapshots: UsageSnapshot[] }) => { | |
| store.saved = data.snapshots; | |
| }), | |
| }; | |
| return store; | |
| } | |
| function createMockPool(entries: Array<{ | |
| status: string; | |
| input_tokens: number; | |
| output_tokens: number; | |
| request_count: number; | |
| }>): AccountPool { | |
| return { | |
| getAllEntries: () => | |
| entries.map((e, i) => ({ | |
| id: `entry-${i}`, | |
| status: e.status, | |
| usage: { | |
| input_tokens: e.input_tokens, | |
| output_tokens: e.output_tokens, | |
| request_count: e.request_count, | |
| }, | |
| })), | |
| } as unknown as AccountPool; | |
| } | |
| describe("UsageStatsStore", () => { | |
| let persistence: ReturnType<typeof createMockPersistence>; | |
| let store: UsageStatsStore; | |
| beforeEach(() => { | |
| persistence = createMockPersistence(); | |
| store = new UsageStatsStore(persistence); | |
| }); | |
| describe("recordSnapshot", () => { | |
| it("records cumulative totals from all accounts", () => { | |
| const pool = createMockPool([ | |
| { status: "active", input_tokens: 1000, output_tokens: 200, request_count: 5 }, | |
| { status: "active", input_tokens: 500, output_tokens: 100, request_count: 3 }, | |
| { status: "expired", input_tokens: 300, output_tokens: 50, request_count: 2 }, | |
| ]); | |
| store.recordSnapshot(pool); | |
| expect(store.snapshotCount).toBe(1); | |
| expect(persistence.save).toHaveBeenCalledTimes(1); | |
| const saved = persistence.saved; | |
| expect(saved).toHaveLength(1); | |
| expect(saved[0].totals).toEqual({ | |
| input_tokens: 1800, | |
| output_tokens: 350, | |
| request_count: 10, | |
| active_accounts: 2, | |
| }); | |
| }); | |
| it("handles empty pool", () => { | |
| const pool = createMockPool([]); | |
| store.recordSnapshot(pool); | |
| expect(store.snapshotCount).toBe(1); | |
| expect(persistence.saved[0].totals).toEqual({ | |
| input_tokens: 0, | |
| output_tokens: 0, | |
| request_count: 0, | |
| active_accounts: 0, | |
| }); | |
| }); | |
| }); | |
| describe("getSummary", () => { | |
| it("returns live totals from pool", () => { | |
| const pool = createMockPool([ | |
| { status: "active", input_tokens: 1000, output_tokens: 200, request_count: 5 }, | |
| { status: "disabled", input_tokens: 500, output_tokens: 100, request_count: 3 }, | |
| ]); | |
| const summary = store.getSummary(pool); | |
| expect(summary).toEqual({ | |
| total_input_tokens: 1500, | |
| total_output_tokens: 300, | |
| total_request_count: 8, | |
| total_accounts: 2, | |
| active_accounts: 1, | |
| }); | |
| }); | |
| }); | |
| describe("getHistory", () => { | |
| it("returns empty for less than 2 snapshots", () => { | |
| expect(store.getHistory(24, "hourly")).toEqual([]); | |
| }); | |
| it("computes deltas between consecutive snapshots", () => { | |
| const now = Date.now(); | |
| const snapshots: UsageSnapshot[] = [ | |
| { | |
| timestamp: new Date(now - 3600_000).toISOString(), | |
| totals: { input_tokens: 100, output_tokens: 20, request_count: 2, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(now - 1800_000).toISOString(), | |
| totals: { input_tokens: 300, output_tokens: 50, request_count: 5, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(now).toISOString(), | |
| totals: { input_tokens: 600, output_tokens: 100, request_count: 10, active_accounts: 1 }, | |
| }, | |
| ]; | |
| persistence = createMockPersistence(snapshots); | |
| store = new UsageStatsStore(persistence); | |
| const raw = store.getHistory(2, "raw"); | |
| expect(raw).toHaveLength(2); | |
| expect(raw[0].input_tokens).toBe(200); | |
| expect(raw[0].output_tokens).toBe(30); | |
| expect(raw[0].request_count).toBe(3); | |
| expect(raw[1].input_tokens).toBe(300); | |
| expect(raw[1].output_tokens).toBe(50); | |
| expect(raw[1].request_count).toBe(5); | |
| }); | |
| it("clamps negative deltas to zero (account removal)", () => { | |
| const now = Date.now(); | |
| const snapshots: UsageSnapshot[] = [ | |
| { | |
| timestamp: new Date(now - 3600_000).toISOString(), | |
| totals: { input_tokens: 1000, output_tokens: 200, request_count: 10, active_accounts: 2 }, | |
| }, | |
| { | |
| timestamp: new Date(now).toISOString(), | |
| totals: { input_tokens: 500, output_tokens: 100, request_count: 5, active_accounts: 1 }, | |
| }, | |
| ]; | |
| persistence = createMockPersistence(snapshots); | |
| store = new UsageStatsStore(persistence); | |
| const raw = store.getHistory(2, "raw"); | |
| expect(raw).toHaveLength(1); | |
| expect(raw[0].input_tokens).toBe(0); | |
| expect(raw[0].output_tokens).toBe(0); | |
| expect(raw[0].request_count).toBe(0); | |
| }); | |
| it("aggregates into hourly buckets", () => { | |
| const now = Date.now(); | |
| const hourStart = Math.floor(now / 3600_000) * 3600_000; | |
| const snapshots: UsageSnapshot[] = [ | |
| { | |
| timestamp: new Date(hourStart - 1800_000).toISOString(), | |
| totals: { input_tokens: 0, output_tokens: 0, request_count: 0, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(hourStart - 900_000).toISOString(), | |
| totals: { input_tokens: 100, output_tokens: 10, request_count: 1, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(hourStart + 100_000).toISOString(), | |
| totals: { input_tokens: 300, output_tokens: 30, request_count: 3, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(hourStart + 200_000).toISOString(), | |
| totals: { input_tokens: 500, output_tokens: 50, request_count: 5, active_accounts: 1 }, | |
| }, | |
| ]; | |
| persistence = createMockPersistence(snapshots); | |
| store = new UsageStatsStore(persistence); | |
| const hourly = store.getHistory(2, "hourly"); | |
| // Two buckets: one before hourStart, one at/after hourStart | |
| expect(hourly).toHaveLength(2); | |
| // Previous hour bucket: delta 0β100 = 100 | |
| expect(hourly[0].input_tokens).toBe(100); | |
| // Current hour bucket: delta 100β300 + 300β500 = 200 + 200 = 400 | |
| expect(hourly[1].input_tokens).toBe(400); | |
| }); | |
| it("filters by time range", () => { | |
| const now = Date.now(); | |
| const snapshots: UsageSnapshot[] = [ | |
| { | |
| timestamp: new Date(now - 48 * 3600_000).toISOString(), // 48h ago | |
| totals: { input_tokens: 100, output_tokens: 10, request_count: 1, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(now - 12 * 3600_000).toISOString(), // 12h ago | |
| totals: { input_tokens: 500, output_tokens: 50, request_count: 5, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(now).toISOString(), | |
| totals: { input_tokens: 1000, output_tokens: 100, request_count: 10, active_accounts: 1 }, | |
| }, | |
| ]; | |
| persistence = createMockPersistence(snapshots); | |
| store = new UsageStatsStore(persistence); | |
| // Only last 24h β only the last two snapshots qualify β 1 delta | |
| const raw = store.getHistory(24, "raw"); | |
| expect(raw).toHaveLength(1); | |
| expect(raw[0].input_tokens).toBe(500); | |
| }); | |
| }); | |
| describe("retention", () => { | |
| it("prunes snapshots older than 7 days on record", () => { | |
| const now = Date.now(); | |
| const old: UsageSnapshot[] = [ | |
| { | |
| timestamp: new Date(now - 8 * 24 * 3600_000).toISOString(), // 8 days ago | |
| totals: { input_tokens: 100, output_tokens: 10, request_count: 1, active_accounts: 1 }, | |
| }, | |
| { | |
| timestamp: new Date(now - 1 * 3600_000).toISOString(), // 1h ago | |
| totals: { input_tokens: 500, output_tokens: 50, request_count: 5, active_accounts: 1 }, | |
| }, | |
| ]; | |
| persistence = createMockPersistence(old); | |
| store = new UsageStatsStore(persistence); | |
| const pool = createMockPool([ | |
| { status: "active", input_tokens: 1000, output_tokens: 100, request_count: 10 }, | |
| ]); | |
| store.recordSnapshot(pool); | |
| // Old snapshot pruned, recent + new remain | |
| expect(store.snapshotCount).toBe(2); | |
| }); | |
| }); | |
| }); | |