c8e8305107
Matches the backend split: usage attribution no longer shares EVOSCIENTIST_DEPLOYMENT_ID with workspace scope partitioning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
268 lines
9.1 KiB
TypeScript
268 lines
9.1 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|
import type { UsageEventV1, UsageHeartbeatV1 } from "../usageTypes";
|
|
|
|
vi.mock("server-only", () => ({}));
|
|
|
|
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "evosci-usage-store-"));
|
|
process.env.EVOSCIENTIST_DATA_DIR = dataDir;
|
|
process.env.EVOSCIENTIST_USAGE_DEPLOYMENT_ID = "11111111-1111-4111-8111-111111111111";
|
|
process.env.EVOSCIENTIST_WORKSPACE_ID = "ws1_fixture";
|
|
|
|
const store = await import("./usageStore");
|
|
const fixtures = path.join(process.cwd(), "docs", "schemas", "fixtures");
|
|
|
|
function fixture(relative: string): UsageEventV1 {
|
|
return JSON.parse(fs.readFileSync(path.join(fixtures, relative), "utf8"));
|
|
}
|
|
|
|
function eventFrom(
|
|
base: UsageEventV1,
|
|
modelCallId: string,
|
|
patch: Partial<UsageEventV1> = {}
|
|
): UsageEventV1 {
|
|
const deploymentId = patch.deployment_id ?? base.deployment_id;
|
|
return {
|
|
...base,
|
|
...patch,
|
|
deployment_id: deploymentId,
|
|
model_call_id: modelCallId,
|
|
event_id: `${deploymentId}:${modelCallId}:callback_final:1`,
|
|
};
|
|
}
|
|
|
|
function range(filters: Record<string, string> = {}): URLSearchParams {
|
|
return new URLSearchParams({
|
|
from: "2026-07-01T00:00:00.000Z",
|
|
to: "2026-07-31T00:00:00.000Z",
|
|
...filters,
|
|
});
|
|
}
|
|
|
|
describe("usage store", () => {
|
|
const confirmed = fixture("accepted/confirmed.json");
|
|
const unknown = fixture("accepted/unknown.json");
|
|
|
|
beforeAll(() => {
|
|
expect(store.ingestUsageEvent(confirmed).status).toBe("accepted");
|
|
expect(store.ingestUsageEvent(unknown).status).toBe("accepted");
|
|
});
|
|
|
|
afterAll(() => {
|
|
store.closeUsageDb();
|
|
fs.rmSync(dataDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it("handles duplicate and conflicting observations without changing projection", () => {
|
|
expect(store.ingestUsageEvent(confirmed).status).toBe("duplicate");
|
|
const conflict = fixture("projection/conflicting-confirmed.json");
|
|
expect(store.ingestUsageEvent(conflict).status).toBe("conflict");
|
|
const summary = store.usageSummary(range({ thread_id: "thread-fixture" }));
|
|
expect(summary.total_tokens).toBe("1200");
|
|
expect(summary.confirmed_call_count).toBe("1");
|
|
expect(summary.unknown_call_count).toBe("1");
|
|
});
|
|
|
|
it("includes async child calls through source_session_id", () => {
|
|
const child = eventFrom(confirmed, "44444444-4444-4444-8444-444444444444", {
|
|
thread_id: "worker-thread",
|
|
source_session_id: "thread-fixture",
|
|
scope: "async_subagent",
|
|
input_tokens: 50,
|
|
output_tokens: 5,
|
|
provider_total_tokens: 55,
|
|
});
|
|
expect(store.ingestUsageEvent(child).status).toBe("accepted");
|
|
const summary = store.usageSummary(
|
|
range({
|
|
deployment_id: confirmed.deployment_id,
|
|
workspace_id: confirmed.workspace_id,
|
|
thread_id: "thread-fixture",
|
|
})
|
|
);
|
|
expect(summary.total_tokens).toBe("1255");
|
|
expect(summary.confirmed_call_count).toBe("2");
|
|
expect(summary.by_scope).toEqual(
|
|
expect.arrayContaining([
|
|
expect.objectContaining({ key: "async_subagent", total_tokens: "55" }),
|
|
])
|
|
);
|
|
});
|
|
|
|
it("isolates identical call ids across deployments", () => {
|
|
const other = eventFrom(confirmed, confirmed.model_call_id, {
|
|
deployment_id: "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa",
|
|
workspace_id: "ws1_other",
|
|
});
|
|
expect(store.ingestUsageEvent(other).status).toBe("accepted");
|
|
const current = store.usageSummary(
|
|
range({
|
|
deployment_id: confirmed.deployment_id,
|
|
thread_id: "thread-fixture",
|
|
})
|
|
);
|
|
expect(current.confirmed_call_count).toBe("2");
|
|
});
|
|
|
|
it("returns exact aggregate values above the JavaScript safe integer", () => {
|
|
for (const id of [
|
|
"55555555-5555-4555-8555-555555555555",
|
|
"66666666-6666-4666-8666-666666666666",
|
|
]) {
|
|
expect(
|
|
store.ingestUsageEvent(
|
|
eventFrom(confirmed, id, {
|
|
thread_id: "bigint-thread",
|
|
turn_id: "bigint-turn",
|
|
input_tokens: Number.MAX_SAFE_INTEGER,
|
|
output_tokens: 0,
|
|
provider_total_tokens: null,
|
|
})
|
|
).status
|
|
).toBe("accepted");
|
|
}
|
|
const summary = store.usageSummary(range({ thread_id: "bigint-thread" }));
|
|
expect(summary.total_tokens).toBe("18014398509481982");
|
|
});
|
|
|
|
it("uses a total-order cursor across deployments", () => {
|
|
const first = store.usageCalls(range(), 1, null);
|
|
expect(first.next_cursor).not.toBeNull();
|
|
const decoded = JSON.parse(
|
|
Buffer.from(first.next_cursor!, "base64url").toString("utf8")
|
|
);
|
|
expect(decoded).toEqual({
|
|
at: expect.any(String),
|
|
deployment: expect.any(String),
|
|
id: expect.any(String),
|
|
});
|
|
const second = store.usageCalls(range(), 1, decoded);
|
|
expect(second.items[0]).not.toEqual(first.items[0]);
|
|
});
|
|
|
|
it("defaults status to the integrated launcher deployment", () => {
|
|
const heartbeat = (
|
|
deployment_id: string,
|
|
workspace_id: string
|
|
): UsageHeartbeatV1 => ({
|
|
deployment_id,
|
|
workspace_id,
|
|
emitter_version: "2.0",
|
|
schema_version: 1,
|
|
sender_status: "healthy",
|
|
spool_pending: 0,
|
|
spool_inflight: 0,
|
|
spool_quarantined: 0,
|
|
spool_bytes: 0,
|
|
first_loss_at: null,
|
|
tracking_degraded_reason: null,
|
|
last_error_code: null,
|
|
sent_at: new Date().toISOString(),
|
|
});
|
|
store.recordHeartbeat(
|
|
heartbeat(confirmed.deployment_id, confirmed.workspace_id)
|
|
);
|
|
store.recordHeartbeat(
|
|
heartbeat("bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb", "ws1_latest")
|
|
);
|
|
const status = store.usageStatus(new URLSearchParams());
|
|
expect(status.deployment_id).toBe(confirmed.deployment_id);
|
|
expect(status.workspace_id).toBe(confirmed.workspace_id);
|
|
});
|
|
|
|
it("expires inbox data while retaining idempotency tombstones", () => {
|
|
const db = store.usageDb();
|
|
db.prepare(
|
|
"UPDATE usage_event_inbox SET received_at=? WHERE event_id=?"
|
|
).run("2020-01-01T00:00:00.000Z", confirmed.event_id);
|
|
db.prepare(
|
|
"UPDATE usage_event_conflicts SET received_at=? WHERE event_id=?"
|
|
).run("2020-01-01T00:00:00.000Z", confirmed.event_id);
|
|
const result = store.runUsageMaintenance(
|
|
Date.parse("2026-07-16T00:00:00.000Z"),
|
|
db
|
|
);
|
|
expect(result.inbox_deleted).not.toBe("0");
|
|
expect(result.conflicts_deleted).not.toBe("0");
|
|
expect(
|
|
db
|
|
.prepare(
|
|
"SELECT event_id FROM usage_event_idempotency WHERE event_id=?"
|
|
)
|
|
.get(confirmed.event_id)
|
|
).toBeDefined();
|
|
expect(store.ingestUsageEvent(confirmed).status).toBe("duplicate");
|
|
});
|
|
|
|
it("creates a restorable online SQLite backup", async () => {
|
|
const destination = path.join(dataDir, "backups", "usage.db");
|
|
await store.backupUsageDatabase(destination);
|
|
expect(fs.statSync(destination).size).toBeGreaterThan(0);
|
|
const Database = (await import("better-sqlite3")).default;
|
|
const backup = new Database(destination, { readonly: true });
|
|
backup.defaultSafeIntegers(true);
|
|
try {
|
|
const row = backup
|
|
.prepare("SELECT COUNT(*) count FROM model_usage")
|
|
.get() as {
|
|
count: bigint;
|
|
};
|
|
expect(row.count).toBeGreaterThan(BigInt(0));
|
|
} finally {
|
|
backup.close();
|
|
}
|
|
});
|
|
|
|
it("stores aggregate-only provider reconciliation without rewriting calls", () => {
|
|
const aggregateEvent = eventFrom(
|
|
confirmed,
|
|
"88888888-8888-4888-8888-888888888888",
|
|
{
|
|
provider_profile_id: "aggregate-provider",
|
|
upstream_model_id: "aggregate-model",
|
|
model_alias: "aggregate-model",
|
|
input_tokens: 20,
|
|
output_tokens: 4,
|
|
provider_total_tokens: 24,
|
|
}
|
|
);
|
|
expect(store.ingestUsageEvent(aggregateEvent).status).toBe("accepted");
|
|
const observation = {
|
|
aggregate_id: "provider-window-1",
|
|
deployment_id: confirmed.deployment_id,
|
|
provider_profile_id: "aggregate-provider",
|
|
upstream_model_id: "aggregate-model",
|
|
window_start: "2026-07-16T00:00:00.000Z",
|
|
window_end: "2026-07-17T00:00:00.000Z",
|
|
input_tokens: "25",
|
|
output_tokens: "5",
|
|
provider_total_tokens: "30",
|
|
request_count: "1",
|
|
source_name: "provider-usage-api",
|
|
source_payload: { bucket: "fixture" },
|
|
observed_at: "2026-07-17T00:05:00.000Z",
|
|
};
|
|
expect(store.recordProviderAggregate(observation).status).toBe("accepted");
|
|
expect(store.recordProviderAggregate(observation).status).toBe("duplicate");
|
|
expect(
|
|
store.recordProviderAggregate({ ...observation, input_tokens: "26" })
|
|
.status
|
|
).toBe("conflict");
|
|
expect(store.providerAggregateDifference(observation.aggregate_id)).toEqual(
|
|
expect.objectContaining({
|
|
reconciliation_mode: "aggregate_only",
|
|
projection_updated: false,
|
|
local: expect.objectContaining({ total_tokens: "24" }),
|
|
provider: expect.objectContaining({ total_tokens: "30" }),
|
|
difference: expect.objectContaining({ total_tokens: "6" }),
|
|
})
|
|
);
|
|
const projection = store.usageSummary(
|
|
range({ provider_profile_id: "aggregate-provider" })
|
|
);
|
|
expect(projection.total_tokens).toBe("24");
|
|
});
|
|
});
|