Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
101 lines
6.4 KiB
TypeScript
101 lines
6.4 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { describe, expect, test } from "vitest";
|
|
import { DatabaseConnection } from "../../src/main/database/connection";
|
|
import { OrganizationService } from "../../src/main/services/organization-service";
|
|
import { buildUsageStats } from "../../src/main/services/usage-stats";
|
|
|
|
describe("usage statistics", () => {
|
|
test("periods cover every record of each org, beyond the snapshot's recent-list caps", () => {
|
|
const connection = new DatabaseConnection(join(tmpdir(), `ac-usage-${randomUUID()}.db`));
|
|
const organization = new OrganizationService(connection.db);
|
|
const orgs: string[] = [];
|
|
connection.transaction(() => {
|
|
for (const name of ["Acme", "Beta"]) orgs.push(organization.createOrganization({ organizationName: name, employeeName: "Dana", mission: "m", goal: "g" }));
|
|
});
|
|
const now = new Date();
|
|
const insert = connection.db.prepare(`
|
|
insert into provider_requests(id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256, state, input_char_count, output_char_count,
|
|
execution_mode, policy_decision, stream_event_count, created_at, input_tokens, output_tokens, cost_cents)
|
|
values (?, ?, ?, 'openai', null, 'm', 'sha', ?, 1, 1, 'foreground', 'allowed', 0, ?, ?, ?, ?)
|
|
`);
|
|
connection.transaction(() => {
|
|
for (const [index, orgId] of orgs.entries()) {
|
|
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
|
|
for (let call = 0; call < 150; call += 1) {
|
|
const daysAgo = call % 20;
|
|
const at = new Date(now.getTime() - daysAgo * 86_400_000).toISOString();
|
|
insert.run(`preq_${index}_${call}`, orgId, providerId, call % 10 === 0 ? "failed" : "succeeded", at, call % 3 === 0 ? null : 100, call % 3 === 0 ? null : 10, 1);
|
|
}
|
|
}
|
|
});
|
|
const stats = buildUsageStats(connection.db, now);
|
|
expect(stats.map((item) => item.orgId).sort()).toEqual([...orgs].sort());
|
|
const acme = stats.find((item) => item.orgId === orgs[0])!;
|
|
expect(acme.month).toMatchObject({ requests: 150, failures: 15, callsWithoutUsage: 50, inputTokens: 100 * 100 });
|
|
expect(acme.days).toHaveLength(7);
|
|
const weekCalls = Array.from({ length: 150 }, (_, call) => call % 20).filter((daysAgo) => daysAgo < 7).length;
|
|
expect(acme.days.reduce((sum, day) => sum + day.callsWithoutUsage, 0) + acme.days.reduce((sum, day) => sum + day.tokens, 0) / 110).toBe(weekCalls);
|
|
expect(acme.providerActivity).toEqual([{ providerId: expect.any(String), requests: 150, failures: 15 }]);
|
|
connection.close();
|
|
});
|
|
|
|
test("day buckets follow calendar days across a daylight-saving change, and 30 days is a rolling window", () => {
|
|
const previous = process.env.TZ;
|
|
process.env.TZ = "America/New_York";
|
|
const connection = new DatabaseConnection(join(tmpdir(), `ac-usage-dst-${randomUUID()}.db`));
|
|
let orgId = "";
|
|
connection.transaction(() => {
|
|
orgId = new OrganizationService(connection.db).createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
|
|
});
|
|
const providerId = (connection.db.prepare("select id from providers where org_id = ? and provider_key = 'openai'").get(orgId) as { id: string }).id;
|
|
const insert = connection.db.prepare(`
|
|
insert into provider_requests(id, org_id, provider_id, provider_key, workspace_id, model, prompt_sha256, state, input_char_count, output_char_count,
|
|
execution_mode, policy_decision, stream_event_count, created_at, input_tokens, output_tokens)
|
|
values (?, ?, ?, 'openai', null, 'm', 'sha', 'succeeded', 1, 1, 'foreground', 'allowed', 0, ?, 1, 0)
|
|
`);
|
|
// 2026-03-09 00:30 New York is right after the spring-forward night; 29 days 23 hours before "now" is still inside 30 days.
|
|
const now = new Date("2026-03-12T12:00:00-04:00");
|
|
insert.run("preq_dst", orgId, providerId, new Date("2026-03-09T00:30:00-04:00").toISOString());
|
|
insert.run("preq_edge", orgId, providerId, new Date(now.getTime() - (30 * 24 - 1) * 3_600_000).toISOString());
|
|
const stats = buildUsageStats(connection.db, now).find((item) => item.orgId === orgId)!;
|
|
expect(stats.days.reduce((sum, day) => sum + day.tokens, 0)).toBe(1);
|
|
expect(stats.month.requests).toBe(2);
|
|
connection.close();
|
|
process.env.TZ = previous;
|
|
});
|
|
});
|
|
|
|
describe("accepted work", () => {
|
|
test("counts plain tasks reaching done and work sent back, not sessions that merely ended", () => {
|
|
const connection = new DatabaseConnection(join(tmpdir(), `ac-acceptance-${randomUUID()}.db`));
|
|
const organization = new OrganizationService(connection.db);
|
|
let orgId = "";
|
|
const ids: string[] = [];
|
|
connection.transaction(() => {
|
|
orgId = organization.createOrganization({ organizationName: "Acme", employeeName: "Dana", mission: "m", goal: "g" });
|
|
for (const title of ["Ship", "Spec", "Chat reply"]) {
|
|
ids.push(organization.createTask({
|
|
orgId, title, description: "d", status: "backlog", assigneeAgentId: null, risk: "low", projectId: null, goalId: null,
|
|
workspaceId: null, priority: "normal", requiresFilesystem: false, ...(title === "Chat reply" ? { origin: "chat_wake" as const } : {}),
|
|
}));
|
|
}
|
|
});
|
|
const [ship, spec, reply] = ids;
|
|
const move = (taskId: string, status: string) => connection.transaction(() => organization.updateTaskStatus(orgId, taskId, status));
|
|
move(ship, "in_progress"); move(ship, "review"); move(ship, "done");
|
|
move(spec, "in_progress"); move(spec, "review"); move(spec, "in_progress");
|
|
move(reply, "done");
|
|
const week = buildUsageStats(connection.db, new Date()).find((item) => item.orgId === orgId)!.days;
|
|
const total = (pick: (day: typeof week[number]) => number) => week.reduce((sum, day) => sum + pick(day), 0);
|
|
expect([total((day) => day.acceptedTasks), total((day) => day.returnedTasks)]).toEqual([1, 1]);
|
|
// Marking done twice is one acceptance; deleting the task keeps the history.
|
|
move(ship, "done");
|
|
connection.transaction(() => organization.deleteTask({ orgId, taskId: ship }));
|
|
const after = buildUsageStats(connection.db, new Date()).find((item) => item.orgId === orgId)!.days;
|
|
expect([after.reduce((sum, day) => sum + day.acceptedTasks, 0), after.reduce((sum, day) => sum + day.returnedTasks, 0)]).toEqual([1, 1]);
|
|
connection.close();
|
|
});
|
|
});
|