1// @effect-diagnostics nodeBuiltinImport:off - the suite seeds and grows real
2// transcript trees on disk, outside the service's Effect FileSystem.
3import * as NodeChildProcess from "node:child_process";
4import * as NodeFSP from "node:fs/promises";
5import * as NodeOS from "node:os";
6import * as NodePath from "node:path";
7import * as NodeSqlite from "node:sqlite";
8
9import { assert, describe, it } from "@effect/vitest";
10import * as NodeServices from "@effect/platform-node/NodeServices";
11import * as HostProcess from "@t3tools/shared/HostProcess";
12import { mergeUsage } from "@t3tools/shared/usageMerge";
13import {
14 EnvironmentId,
15 ProviderDriverKind,
16 ProviderInstanceId,
17 UsageDay,
18 type UsageSource,
19 type UsageSummaryInput,
20} from "@t3tools/contracts";
21import * as Duration from "effect/Duration";
22import * as Deferred from "effect/Deferred";
23import * as Effect from "effect/Effect";
24import * as Exit from "effect/Exit";
25import * as Fiber from "effect/Fiber";
26import * as FileSystem from "effect/FileSystem";
27import * as Context from "effect/Context";
28import * as Layer from "effect/Layer";
29import * as Scheduler from "effect/Scheduler";
30import * as Schema from "effect/Schema";
31import * as TestClock from "effect/testing/TestClock";
32import { HttpClient, HttpClientResponse } from "effect/http";
33
34import * as ServerConfig from "../config.ts";
35import * as ServerSettings from "../serverSettings.ts";
36import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader";
37import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain";
38import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts";
39import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
40import * as BackgroundPolicy from "../background/BackgroundPolicy.ts";
41import * as AntigravityUsage from "../provider/Drivers/AntigravityUsage.ts";
42import * as ProviderHostLive from "../provider/ProviderHostLive.ts";
43import { totalTokens, type UsageRecord } from "@t3tools/provider-core/server/usage";
44import * as UsageService from "./UsageService.ts";
45
46const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown));
47const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown));
48const decodeUnknownJsonString = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown));
49
50function claudeLine(id: number, outputTokens: number, model = "claude-fable-5"): string {
51 return `${JSON.stringify({
52 type: "assistant",
53 timestamp: "2026-08-01T10:00:00Z",
54 requestId: `req_${id}`,
55 sessionId: "session-1",
56 message: {
57 id: `msg_${id}`,
58 model,
59 usage: { input_tokens: 10, output_tokens: outputTokens },
60 },
61 })}\n`;
62}
63
64const WINDOW: UsageSummaryInput = {
65 timeZone: "UTC",
66 sinceDay: UsageDay.make("2026-07-31"),
67 untilDay: UsageDay.make("2026-08-02"),
68};
69
70const setup = Effect.gen(function* () {
71 const home = yield* Effect.promise(() =>
72 NodeFSP.mkdtemp(NodePath.join(NodeOS.tmpdir(), "usage-service-test-")),
73 );
74 yield* Effect.addFinalizer(() =>
75 Effect.promise(() => NodeFSP.rm(home, { recursive: true, force: true })),
76 );
77 const transcriptDir = NodePath.join(home, "claude", "projects", "proj");
78 yield* Effect.promise(() => NodeFSP.mkdir(transcriptDir, { recursive: true }));
79 return {
80 home,
81 transcript: NodePath.join(transcriptDir, "session.jsonl"),
82 settings: {
83 providerInstances: {
84 [ProviderInstanceId.make("claudeAgent")]: {
85 driver: ProviderDriverKind.make("claudeAgent"),
86 config: { homePath: NodePath.join(home, "claude") },
87 },
88 [ProviderInstanceId.make("codex")]: {
89 driver: ProviderDriverKind.make("codex"),
90 config: { homePath: NodePath.join(home, "codex") },
91 },
92 },
93 },
94 };
95});
96
97const layerCursorUsageAccounts = <E, R>(
98 reader: Layer.Layer<CursorAccountReader.CursorAccountReader, E, R>,
99) => Layer.fresh(CursorUsageAccounts.layer).pipe(Layer.provide(reader));
100
101/**
102 * A service with its own Cursor account caches over `read`, as a fresh server
103 * process has. `awaitPersisted` also waits for the account cache writes.
104 */
105const makeWithCursor = (read: CursorAccountReader.CursorAccountReader["Service"]["read"]) =>
106 Effect.gen(function* () {
107 const context = yield* Layer.build(
108 layerCursorUsageAccounts(Layer.succeed(CursorAccountReader.CursorAccountReader, { read })),
109 );
110 const accounts = Context.get(context, CursorUsageAccounts.CursorUsageAccounts);
111 const service = yield* UsageService.make.pipe(Effect.provideContext(context));
112 return {
113 ...service,
114 awaitPersisted: Effect.andThen(service.awaitPersisted, accounts.awaitPersisted),
115 };
116 });
117
118const layerService = (input: {
119 readonly prefix: string;
120 readonly home: string;
121 readonly settings: Parameters<typeof ServerSettings.layerTest>[0];
122 readonly onRatesFetch?: () => void;
123 /** Defaults to an unparsable document so every scan retries the fetch. */
124 readonly ratesDocument?: unknown;
125 readonly environment?: NodeJS.ProcessEnv;
126 readonly platform?: NodeJS.Platform;
127}) =>
128 layerCursorUsageAccounts(
129 CursorAccountReader.layer.pipe(Layer.provide(CursorKeychain.layer)),
130 ).pipe(
131 Layer.provideMerge(AntigravityUsage.layer),
132 Layer.provideMerge(ProviderHostLive.layer),
133 Layer.provideMerge(Layer.mock(BackgroundPolicy.BackgroundPolicy)({})),
134 Layer.provideMerge(Layer.mock(ServerSecretStore.ServerSecretStore)({})),
135 Layer.provideMerge(
136 ServerConfig.layerTest(process.cwd(), NodePath.join(input.home, input.prefix)),
137 ),
138 Layer.provideMerge(NodeServices.layer),
139 Layer.provideMerge(Layer.succeed(HostProcess.Platform, input.platform ?? "linux")),
140 Layer.provideMerge(ServerSettings.layerTest(input.settings)),
141 Layer.provideMerge(
142 Layer.succeed(
143 HttpClient.HttpClient,
144 HttpClient.make((request) =>
145 Effect.sync(() => {
146 input.onRatesFetch?.();
147 // Unparsable rates: every scan retries the fetch, which makes the
148 // fetch count a boundary-level observation of how many scans ran.
149 return HttpClientResponse.fromWeb(request, Response.json(input.ratesDocument ?? {}));
150 }),
151 ),
152 ),
153 ),
154 Layer.provideMerge(
155 Layer.succeed(HostProcess.Environment, {
156 HOME: input.home,
157 GROK_HOME: NodePath.join(input.home, "grok"),
158 PI_CODING_AGENT_DIR: NodePath.join(input.home, "pi"),
159 OPENCODE_DATA_DIR: NodePath.join(input.home, "opencode"),
160 ANTIGRAVITY_DATA_DIR: NodePath.join(input.home, "antigravity"),
161 XDG_CONFIG_HOME: NodePath.join(input.home, "config"),
162 APPDATA: NodePath.join(input.home, "config"),
163 ...input.environment,
164 }),
165 ),
166 );
167
168/** Outside `NARROW_WINDOW`, inside `WINDOW`. Seconds, as `utimes` takes them. */
169const BEFORE_NARROW_WINDOW = Date.parse("2026-08-01T10:00:00Z") / 1000;
170const NARROW_WINDOW: UsageSummaryInput = {
171 timeZone: "UTC",
172 sinceDay: UsageDay.make("2026-09-01"),
173 untilDay: UsageDay.make("2026-09-02"),
174};
175
176/**
177 * A FIFO named like a transcript. A scan's read of it waits in `open` until
178 * `openGate`, then fails at once, so a gate holds that scan's directory reads
179 * in flight. Each waiting gate holds one libuv pool thread; keep at most three.
180 */
181const makeGate = (path: string, lastWriteSeconds?: number) =>
182 Effect.promise(async () => {
183 NodeChildProcess.execFileSync("mkfifo", [path]);
184 if (lastWriteSeconds !== undefined) {
185 await NodeFSP.utimes(path, lastWriteSeconds, lastWriteSeconds);
186 }
187 });
188
189/**
190 * Returns once a scan has opened the gate. A scan opens every file of a
191 * directory at once, so it then holds the directory's transcripts open too.
192 */
193const openGate = (path: string) =>
194 Effect.promise(async () => (await NodeFSP.open(path, "w")).close());
195
196/** Replaces a file by rename, so a scan holding the old one keeps reading it. */
197const replaceFile = (path: string, content: string) =>
198 Effect.promise(async () => {
199 await NodeFSP.writeFile(path + ".next", content);
200 await NodeFSP.rename(path + ".next", path);
201 });
202
203function totalOutputTokens(summary: { buckets: readonly { totals: { outputTokens: number } }[] }) {
204 return summary.buckets.reduce((sum, bucket) => sum + bucket.totals.outputTokens, 0);
205}
206
207/** Inside `WINDOW`: the time Cursor tests run at. */
208const CURSOR_NOW = Date.parse("2026-08-02T12:00:00Z");
209const HOUR_MS = 60 * 60 * 1000;
210/** The service's cache retention, which the Cursor account cache always covers. */
211const CURSOR_RETENTION_MS = 92 * 24 * HOUR_MS;
212
213/**
214 * Stands in for Cursor's dashboard API. Each read returns the account's events
215 * in its range, keyed like the real reader: occurrences count within one read.
216 * While `gate` is set, reads wait for it.
217 */
218function makeFakeCursor() {
219 const state = {
220 accountKey: "account-a",
221 events: [] as { readonly timestampMs: number; readonly outputTokens: number }[],
222 error: null as string | null,
223 gate: undefined as Deferred.Deferred<void> | undefined,
224 calls: [] as { readonly sinceMs: number; readonly untilMs: number }[],
225 };
226 const read = (_credential: unknown, sinceMs: number, untilMs: number) =>
227 Effect.gen(function* () {
228 state.calls.push({ sinceMs, untilMs });
229 if (state.gate !== undefined) yield* Deferred.await(state.gate);
230 const { accountKey, error } = state;
231 if (error !== null) return { accountKey, records: [], missing: false, error };
232 const occurrences = new Map<string, number>();
233 const records = state.events
234 .filter(({ timestampMs }) => timestampMs >= sinceMs && timestampMs <= untilMs)
235 .map(({ timestampMs, outputTokens }): UsageRecord => {
236 const key = `${timestampMs}:${outputTokens}`;
237 const occurrence = occurrences.get(key) ?? 0;
238 occurrences.set(key, occurrence + 1);
239 return {
240 provider: "cursor",
241 timestampMs,
242 model: "claude-fable-5",
243 sessionId: "conversation-1",
244 totals: {
245 uncachedInputTokens: 0,
246 cachedInputTokens: 0,
247 cacheCreationTokens: 0,
248 outputTokens,
249 reasoningTokens: 0,
250 },
251 reportedCostUsd: null,
252 speed: "standard",
253 dedupeKey: `cursor-account:${accountKey}:${key}:${occurrence}`,
254 };
255 });
256 return { accountKey, records, missing: false, error: null };
257 });
258 return { state, read };
259}
260
261const writeCursorLogin = (home: string) =>
262 Effect.promise(async () => {
263 const authPath = NodePath.join(home, "config", "cursor", "auth.json");
264 await NodeFSP.mkdir(NodePath.dirname(authPath), { recursive: true });
265 await NodeFSP.writeFile(authPath, "{}");
266 });
267
268function cursorSource(summary: { readonly sources: readonly UsageSource[] }) {
269 return summary.sources.find((source) => source.fingerprint.provider === "cursor");
270}
271
272describe("UsageService", () => {
273 it.live(
274 "reads Pi per-model usage and auxiliary costs once across forks and repeated entries",
275 () =>
276 Effect.gen(function* () {
277 const { home, settings } = yield* setup;
278 const dir = NodePath.join(home, "pi", "sessions", "project");
279 const usage = {
280 input: 10,
281 output: 5,
282 cacheRead: 20,
283 cacheWrite: 3,
284 reasoning: 2,
285 totalTokens: 38,
286 cost: { input: 0.1, output: 0.2, cacheRead: 0.3, cacheWrite: 0.4, total: 1 },
287 };
288 const parent = {
289 type: "message",
290 id: "a123abcd",
291 parentId: null,
292 timestamp: "2026-08-01T10:00:00Z",
293 message: {
294 role: "assistant",
295 provider: "anthropic",
296 model: "requested-model",
297 responseModel: "actual-model",
298 usage,
299 },
300 };
301 const auxiliary = [
302 {
303 type: "message",
304 id: "tool",
305 message: { role: "toolResult", toolName: "classify", usage },
306 },
307 {
308 type: "message",
309 id: "image",
310 message: {
311 role: "toolResult",
312 toolName: "image",
313 usage: { ...usage, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
314 },
315 },
316 { type: "compaction", id: "compact", usage },
317 { type: "branch_summary", id: "summary", usage },
318 ].map((entry) => ({ ...entry, timestamp: "2026-08-01T11:00:00Z" }));
319 yield* Effect.promise(async () => {
320 await NodeFSP.mkdir(dir, { recursive: true });
321 await NodeFSP.writeFile(
322 NodePath.join(dir, "parent.jsonl"),
323 [
324 { type: "session", id: "parent" },
325 parent,
326 ...auxiliary,
327 {
328 type: "usage",
329 id: "warm",
330 timestamp: "2026-08-01T12:00:00Z",
331 kind: "cache_warm",
332 provider: "openai",
333 model: "actual-model",
334 usage: { ...usage, cost: { ...usage.cost, total: 0 } },
335 },
336 ]
337 .map((line) => encodeUnknownJsonString(line))
338 .join("\n") + "\n",
339 );
340 await NodeFSP.writeFile(
341 NodePath.join(dir, "fork.jsonl"),
342 [
343 { type: "session", id: "fork", parentSession: NodePath.join(dir, "parent.jsonl") },
344 { ...parent, parentId: "relinked" },
345 parent,
346 ...auxiliary,
347 {
348 ...parent,
349 timestamp: "2026-08-01T13:00:00Z",
350 message: { ...parent.message, provider: "openai", responseModel: "another-model" },
351 },
352 { type: "get_session_stats", usage: { ...usage, cost: { total: 1000 } } },
353 ]
354 .map((line) => encodeUnknownJsonString(line))
355 .join("\n") + "\n",
356 );
357 });
358 yield* Effect.gen(function* () {
359 const service = yield* UsageService.make;
360 const first = yield* service.readSummary(WINDOW);
361 const pi = first.buckets.filter((bucket) => bucket.provider === "pi");
362 assert.strictEqual(
363 pi.reduce((total, bucket) => total + bucket.costUsd, 0),
364 44,
365 );
366 assert.strictEqual(
367 pi.reduce((total, bucket) => total + totalTokens(bucket.totals), 0),
368 228,
369 );
370 assert.deepStrictEqual(pi.map((bucket) => bucket.model).sort(), [
371 "Tools/summaries",
372 "anthropic/actual-model",
373 "openai/actual-model",
374 "openai/another-model",
375 ]);
376 const assistant = pi.find((bucket) => bucket.model === "anthropic/actual-model");
377 assert.deepStrictEqual(assistant?.totals, {
378 uncachedInputTokens: 10,
379 outputTokens: 5,
380 cachedInputTokens: 20,
381 cacheCreationTokens: 3,
382 reasoningTokens: 2,
383 });
384 assert.strictEqual(assistant?.costSource, "providerReported");
385 assert.approximately(assistant?.categoryCostUsd?.input ?? NaN, 10 / 38, 1e-6);
386 assert.approximately(assistant?.categoryCostUsd?.cacheRead ?? NaN, 2 / 38, 1e-6);
387 assert.approximately(assistant?.categoryCostUsd?.cacheWrite ?? NaN, 6 / 38, 1e-6);
388 assert.approximately(assistant?.categoryCostUsd?.output ?? NaN, 20 / 38, 1e-6);
389 assert.strictEqual(assistant?.cacheSavingsUsd, 18);
390 const warm = pi.find((bucket) => bucket.model === "openai/actual-model");
391 assert.strictEqual(warm?.costSource, "modelPriced");
392 assert.strictEqual(warm?.costUsd, 38);
393 assert.deepStrictEqual(warm?.categoryCostUsd, {
394 input: 10,
395 cacheRead: 2,
396 cacheWrite: 6,
397 output: 20,
398 });
399 assert.strictEqual(warm?.cacheSavingsUsd, 18);
400 assert.strictEqual(
401 first.sources.filter((source) => source.fingerprint.provider === "pi").length,
402 1,
403 );
404 // Warm scans retain header state and only charge appended entries.
405 yield* Effect.promise(() =>
406 NodeFSP.appendFile(
407 NodePath.join(dir, "fork.jsonl"),
408 encodeUnknownJsonString({
409 ...parent,
410 id: "new",
411 timestamp: "2026-08-01T14:00:00Z",
412 message: { ...parent.message, responseModel: "latest-model" },
413 }) + "\n",
414 ),
415 );
416 const grown = yield* service.readSummary(WINDOW);
417 assert.strictEqual(
418 grown.buckets
419 .filter((bucket) => bucket.provider === "pi")
420 .reduce((total, bucket) => total + bucket.costUsd, 0),
421 45,
422 );
423 yield* service.awaitPersisted;
424 const restarted = yield* UsageService.make;
425 assert.deepStrictEqual((yield* restarted.readSummary(WINDOW)).buckets, grown.buckets);
426 }).pipe(
427 Effect.scoped,
428 Effect.provide(
429 layerService({
430 prefix: "usage-pi-fork",
431 home,
432 ratesDocument: {
433 "actual-model": {
434 input_cost_per_token: 1,
435 output_cost_per_token: 4,
436 cache_read_input_token_cost: 0.1,
437 cache_creation_input_token_cost: 2,
438 },
439 },
440 settings: {
441 ...settings,
442 providerInstances: {
443 ...settings.providerInstances,
444 [ProviderInstanceId.make("pi-extra")]: {
445 driver: ProviderDriverKind.make("pi"),
446 enabled: false,
447 environment: [
448 {
449 name: "PI_CODING_AGENT_DIR",
450 value: NodePath.join(home, "pi"),
451 sensitive: false,
452 },
453 ],
454 },
455 },
456 },
457 }),
458 ),
459 );
460 }).pipe(Effect.scoped),
461 );
462
463 it.live("reports missing Pi history and skips malformed records without losing valid usage", () =>
464 Effect.gen(function* () {
465 const { home, settings } = yield* setup;
466 yield* Effect.gen(function* () {
467 const service = yield* UsageService.make;
468 const missing = yield* service.readSummary(WINDOW);
469 assert.strictEqual(
470 missing.sources.find((source) => source.fingerprint.provider === "pi")?.status,
471 "missing",
472 );
473 const dir = NodePath.join(home, "pi", "sessions");
474 yield* Effect.promise(async () => {
475 await NodeFSP.mkdir(dir, { recursive: true });
476 await NodeFSP.writeFile(
477 NodePath.join(dir, "session.jsonl"),
478 [
479 "{broken",
480 "null",
481 encodeUnknownJsonString({ type: "session", id: "valid" }),
482 encodeUnknownJsonString({
483 type: "message",
484 id: "bad",
485 timestamp: "invalid",
486 message: {
487 role: "assistant",
488 provider: "openai",
489 model: "model",
490 usage: { input: 100 },
491 },
492 }),
493 encodeUnknownJsonString({
494 type: "message",
495 id: "valid",
496 timestamp: "2026-08-01T10:00:00Z",
497 message: {
498 role: "assistant",
499 provider: "openai",
500 model: "model",
501 usage: {
502 input: -1,
503 cacheRead: "100",
504 output: 7,
505 reasoning: 50,
506 cost: { total: -1 },
507 },
508 },
509 }),
510 ].join("\n") + "\n",
511 );
512 });
513 const summary = yield* service.readSummary(WINDOW);
514 const pi = summary.buckets.filter((bucket) => bucket.provider === "pi");
515 assert.strictEqual(pi.length, 1);
516 assert.strictEqual(pi[0] && totalTokens(pi[0].totals), 7);
517 assert.strictEqual(pi[0]?.totals.reasoningTokens, 7);
518 assert.strictEqual(pi[0]?.costSource, "unpriced");
519 }).pipe(
520 Effect.scoped,
521 Effect.provide(layerService({ prefix: "usage-pi-malformed", home, settings })),
522 );
523 }).pipe(Effect.scoped),
524 );
525
526 it.live("discovers Pi history in its default home and directory overrides", () =>
527 Effect.gen(function* () {
528 const { home, settings } = yield* setup;
529 for (const [agentDir, sessionDir, relativeDir] of [
530 [undefined, undefined, ".pi/agent/sessions"],
531 ["~/custom-pi", undefined, "custom-pi/sessions"],
532 ["~/ignored-pi", "~/custom-sessions", "custom-sessions"],
533 ["~/ignored-pi", NodePath.join(home, "absolute-sessions"), "absolute-sessions"],
534 ] as const) {
535 const dir = NodePath.join(home, relativeDir);
536 yield* Effect.promise(async () => {
537 await NodeFSP.mkdir(dir, { recursive: true });
538 await NodeFSP.writeFile(
539 NodePath.join(dir, "session.jsonl"),
540 [
541 { type: "session", id: relativeDir },
542 {
543 type: "usage",
544 id: "warm",
545 timestamp: "2026-08-01T10:00:00Z",
546 provider: "openai",
547 model: "model",
548 usage: { input: 7 },
549 },
550 ]
551 .map((line) => encodeUnknownJsonString(line))
552 .join("\n") + "\n",
553 );
554 });
555 yield* Effect.gen(function* () {
556 const service = yield* UsageService.make;
557 const summary = yield* service.readSummary(WINDOW);
558 const pi = summary.buckets.filter((bucket) => bucket.provider === "pi");
559 assert.strictEqual(pi.length, 1);
560 assert.strictEqual(pi[0]?.totals.uncachedInputTokens, 7);
561 assert.strictEqual(
562 summary.sources.find((source) => source.fingerprint.provider === "pi")?.fingerprint
563 .resolvedHomePath,
564 dir,
565 );
566 }).pipe(
567 Effect.scoped,
568 Effect.provide(
569 layerService({
570 prefix: "usage-pi-" + relativeDir.replaceAll("/", "-"),
571 home,
572 settings,
573 environment: {
574 PI_CODING_AGENT_DIR: agentDir,
575 PI_CODING_AGENT_SESSION_DIR: sessionDir,
576 },
577 }),
578 ),
579 Effect.provideService(HostProcess.HomeDirectory, home),
580 );
581 }
582 }).pipe(Effect.scoped),
583 );
584
585 it.live("reads shared managed default and disabled extra account history once", () =>
586 Effect.gen(function* () {
587 const { home, settings } = yield* setup;
588 const summary = yield* Effect.gen(function* () {
589 for (const [id, output] of [
590 ["codex", 17],
591 ["codex-personal", 23],
592 ] as const) {
593 const sessions = NodePath.join(home, "shared-codex", "sessions");
594 yield* Effect.promise(async () => {
595 await NodeFSP.mkdir(sessions, { recursive: true });
596 await NodeFSP.writeFile(
597 NodePath.join(sessions, `${id}-rollout.jsonl`),
598 [
599 { type: "session_meta", payload: { id } },
600 { type: "turn_context", payload: { model: "gpt-5.6-sol" } },
601 {
602 type: "event_msg",
603 timestamp: "2026-08-01T10:00:00Z",
604 payload: {
605 type: "token_count",
606 info: { last_token_usage: { input_tokens: 10, output_tokens: output } },
607 },
608 },
609 ]
610 .map((line) => encodeUnknownJsonString(line))
611 .join("\n") + "\n",
612 );
613 });
614 }
615 const service = yield* UsageService.make;
616 return yield* service.readSummary(WINDOW);
617 }).pipe(
618 // Scoped inside the state directory, so pending cache writes land
619 // before it is removed.
620 Effect.scoped,
621 Effect.provide(
622 layerService({
623 prefix: "usage-managed-accounts",
624 home,
625 settings: {
626 ...settings,
627 providerInstances: {
628 ...settings.providerInstances,
629 [ProviderInstanceId.make("codex")]: {
630 driver: ProviderDriverKind.make("codex"),
631 config: {
632 setupMode: "managed",
633 homePath: NodePath.join(home, "shared-codex"),
634 },
635 },
636 [ProviderInstanceId.make("codex-personal")]: {
637 driver: ProviderDriverKind.make("codex"),
638 enabled: false,
639 config: {
640 setupMode: "managed",
641 homePath: NodePath.join(home, "shared-codex"),
642 shadowHomePath: NodePath.join(home, "personal-shadow"),
643 },
644 environment: [
645 {
646 name: "CODEX_HOME",
647 value: NodePath.join(home, "ignored-environment"),
648 sensitive: false,
649 },
650 ],
651 },
652 },
653 },
654 }),
655 ),
656 );
657 assert.strictEqual(totalOutputTokens(summary), 40);
658 assert.strictEqual(
659 summary.sources.filter(
660 (source) => source.fingerprint.provider === "codex" && source.status === "ok",
661 ).length,
662 1,
663 );
664 }).pipe(Effect.scoped),
665 );
666 it.live("omits Cursor account usage when no file login is saved", () =>
667 Effect.gen(function* () {
668 const { settings, home } = yield* setup;
669 for (const platform of ["linux", "win32", "darwin"] as const) {
670 const service = yield* UsageService.make.pipe(
671 Effect.provide(
672 layerService({
673 prefix: `usage-service-cursor-no-login-${platform}`,
674 home,
675 settings,
676 platform,
677 environment: { AGENT_CLI_CREDENTIAL_STORE: "file" },
678 }),
679 ),
680 );
681 const summary = yield* service.readSummary(WINDOW);
682 assert.isFalse(summary.sources.some((source) => source.fingerprint.provider === "cursor"));
683 }
684 }).pipe(Effect.scoped),
685 );
686
687 it.live("keeps Cursor credential errors visible when a saved login cannot be read", () =>
688 Effect.gen(function* () {
689 const { settings, home } = yield* setup;
690 const authPath = NodePath.join(home, "config", "cursor", "auth.json");
691 yield* Effect.promise(async () => {
692 await NodeFSP.mkdir(NodePath.dirname(authPath), { recursive: true });
693 await NodeFSP.writeFile(authPath, "invalid json");
694 });
695 const service = yield* UsageService.make.pipe(
696 Effect.provide(
697 layerService({ prefix: "usage-service-cursor-invalid-login", home, settings }),
698 ),
699 );
700 const summary = yield* service.readSummary({ ...WINDOW, awaitRefresh: true });
701 const cursor = summary.sources.find((source) => source.fingerprint.provider === "cursor");
702 assert.strictEqual(cursor?.message, "Cursor credentials could not be read.");
703 }).pipe(Effect.scoped),
704 );
705
706 it.live("answers Cursor from its cache while one shared refresh runs", () =>
707 Effect.gen(function* () {
708 const { settings, home } = yield* setup;
709 yield* writeCursorLogin(home);
710 yield* TestClock.setTime(CURSOR_NOW);
711 const cursor = makeFakeCursor();
712 cursor.state.events = [{ timestampMs: CURSOR_NOW - 2 * HOUR_MS, outputTokens: 5 }];
713 const gate = yield* Deferred.make<void>();
714 cursor.state.gate = gate;
715 yield* Effect.gen(function* () {
716 const service = yield* makeWithCursor(cursor.read);
717 // Cold: nothing cached yet, so Cursor answers empty while it refreshes.
718 const cold = yield* service.readSummary(WINDOW);
719 assert.strictEqual(cursorSource(cold)?.refreshing, true);
720 assert.strictEqual(cursorSource(cold)?.status, "ok");
721 assert.strictEqual(totalOutputTokens(cold), 0);
722 // A second reader joins the refresh in flight.
723 const joined = yield* service.readSummary(WINDOW);
724 assert.strictEqual(cursorSource(joined)?.refreshing, true);
725
726 const waited = yield* service
727 .readSummary({ ...WINDOW, awaitRefresh: true })
728 .pipe(Effect.forkChild);
729 yield* Deferred.succeed(gate, undefined);
730 const refreshed = yield* Fiber.join(waited);
731 assert.isFalse(refreshed.sources.some((source) => source.refreshing));
732 assert.strictEqual(cursorSource(refreshed)?.status, "ok");
733 assert.strictEqual(totalOutputTokens(refreshed), 5);
734 assert.strictEqual(cursor.state.calls.length, 1);
735
736 // Inside the TTL a plain read answers from the cache without refreshing.
737 yield* TestClock.adjust(Duration.seconds(30));
738 const cached = yield* service.readSummary(WINDOW);
739 assert.isUndefined(cursorSource(cached)?.refreshing);
740 assert.strictEqual(totalOutputTokens(cached), 5);
741 assert.strictEqual(cursor.state.calls.length, 1);
742
743 // Past it, a plain read answers from the stale cache and refreshes.
744 yield* TestClock.adjust(Duration.seconds(31));
745 cursor.state.events.push({ timestampMs: CURSOR_NOW, outputTokens: 7 });
746 const stale = yield* service.readSummary(WINDOW);
747 assert.strictEqual(cursorSource(stale)?.refreshing, true);
748 assert.strictEqual(totalOutputTokens(stale), 5);
749 const fresh = yield* service.readSummary({ ...WINDOW, awaitRefresh: true });
750 assert.isUndefined(cursorSource(fresh)?.refreshing);
751 assert.strictEqual(totalOutputTokens(fresh), 12);
752 assert.strictEqual(cursor.state.calls.length, 2);
753 }).pipe(Effect.provide(layerService({ prefix: "usage-service-cursor-swr", home, settings })));
754 }).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
755 );
756
757 it.live("caches Cursor's whole retention and then refetches only the newest edge", () =>
758 Effect.gen(function* () {
759 const { settings, home } = yield* setup;
760 yield* writeCursorLogin(home);
761 yield* TestClock.setTime(CURSOR_NOW);
762 const cursor = makeFakeCursor();
763 // Two identical billed rows inside the refetched hour must both survive it, once each.
764 cursor.state.events = [
765 { timestampMs: Date.parse("2026-07-10T10:00:00Z"), outputTokens: 17 },
766 { timestampMs: CURSOR_NOW - 24 * HOUR_MS, outputTokens: 5 },
767 { timestampMs: CURSOR_NOW - HOUR_MS / 2, outputTokens: 7 },
768 { timestampMs: CURSOR_NOW - HOUR_MS / 2, outputTokens: 7 },
769 ];
770 yield* Effect.gen(function* () {
771 const service = yield* makeWithCursor(cursor.read);
772 const read = (input: UsageSummaryInput) =>
773 service.readSummary({ ...input, awaitRefresh: true });
774 assert.strictEqual(totalOutputTokens(yield* read(WINDOW)), 19);
775 assert.deepStrictEqual(cursor.state.calls, [
776 { sinceMs: CURSOR_NOW - CURSOR_RETENTION_MS, untilMs: CURSOR_NOW },
777 ]);
778
779 // A wider window is answered from the same cache.
780 const wide = { ...WINDOW, sinceDay: UsageDay.make("2026-07-01") };
781 assert.strictEqual(totalOutputTokens(yield* read(wide)), 36);
782 assert.strictEqual(cursor.state.calls.length, 1);
783
784 // An event finalized late, inside the overlap, and a new one.
785 cursor.state.events.push(
786 { timestampMs: CURSOR_NOW - HOUR_MS / 6, outputTokens: 11 },
787 { timestampMs: CURSOR_NOW + HOUR_MS, outputTokens: 13 },
788 );
789 yield* TestClock.adjust(Duration.hours(2));
790 assert.strictEqual(totalOutputTokens(yield* read(WINDOW)), 43);
791 assert.deepStrictEqual(cursor.state.calls[1], {
792 sinceMs: CURSOR_NOW - HOUR_MS,
793 untilMs: CURSOR_NOW + 2 * HOUR_MS,
794 });
795
796 // Another login replaces the cached account's history instead of adding to it.
797 cursor.state.accountKey = "account-b";
798 yield* TestClock.adjust(Duration.minutes(2));
799 assert.strictEqual(totalOutputTokens(yield* read(wide)), 60);
800 assert.strictEqual(cursor.state.calls.length, 4);
801 assert.strictEqual(cursorSource(yield* read(wide))?.fingerprint.volumeId, "account-b");
802 }).pipe(
803 Effect.provide(
804 layerService({ prefix: "usage-service-cursor-incremental", home, settings }),
805 ),
806 );
807 }).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
808 );
809
810 it.live("reports a failed Cursor refresh from its cache without refreshing", () =>
811 Effect.gen(function* () {
812 const { settings, home } = yield* setup;
813 yield* writeCursorLogin(home);
814 yield* TestClock.setTime(CURSOR_NOW);
815 const cursor = makeFakeCursor();
816 cursor.state.events = [{ timestampMs: CURSOR_NOW - HOUR_MS * 3, outputTokens: 5 }];
817 yield* Effect.gen(function* () {
818 const service = yield* makeWithCursor(cursor.read);
819 yield* service.readSummary({ ...WINDOW, awaitRefresh: true });
820
821 yield* TestClock.adjust(Duration.minutes(2));
822 cursor.state.error = "Sign in to Cursor again to read account usage.";
823 const failed = yield* service.readSummary({ ...WINDOW, awaitRefresh: true });
824 assert.strictEqual(cursorSource(failed)?.status, "partial");
825 assert.strictEqual(cursorSource(failed)?.message, cursor.state.error);
826 assert.isUndefined(cursorSource(failed)?.refreshing);
827 assert.strictEqual(totalOutputTokens(failed), 5);
828
829 // The failure stands for the TTL; a plain read does not retry it.
830 const again = yield* service.readSummary(WINDOW);
831 assert.strictEqual(cursorSource(again)?.status, "partial");
832 assert.isUndefined(cursorSource(again)?.refreshing);
833 assert.strictEqual(cursor.state.calls.length, 2);
834
835 // A failing read of another login drops the previous account's history.
836 yield* TestClock.adjust(Duration.minutes(2));
837 cursor.state.accountKey = "account-b";
838 const switched = yield* service.readSummary({ ...WINDOW, awaitRefresh: true });
839 assert.strictEqual(cursorSource(switched)?.status, "missing");
840 assert.strictEqual(cursorSource(switched)?.message, cursor.state.error);
841 assert.strictEqual(totalOutputTokens(switched), 0);
842 }).pipe(
843 Effect.provide(layerService({ prefix: "usage-service-cursor-failure", home, settings })),
844 );
845 }).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
846 );
847
848 it.live("restores the Cursor account cache after a restart", () =>
849 Effect.gen(function* () {
850 const { settings, home } = yield* setup;
851 yield* writeCursorLogin(home);
852 yield* TestClock.setTime(CURSOR_NOW);
853 const before = makeFakeCursor();
854 before.state.events = [
855 { timestampMs: CURSOR_NOW - 30 * HOUR_MS, outputTokens: 5 },
856 { timestampMs: CURSOR_NOW - 3 * HOUR_MS, outputTokens: 7 },
857 ];
858 yield* Effect.gen(function* () {
859 const first = yield* makeWithCursor(before.read);
860 const original = yield* first.readSummary({ ...WINDOW, awaitRefresh: true });
861 yield* first.awaitPersisted;
862
863 const after = makeFakeCursor();
864 after.state.events = before.state.events;
865 const restarted = yield* makeWithCursor(after.read);
866 const restored = yield* restarted.readSummary(WINDOW);
867 assert.isUndefined(cursorSource(restored)?.refreshing);
868 assert.deepStrictEqual(restored.buckets, original.buckets);
869 assert.deepStrictEqual(cursorSource(restored), cursorSource(original));
870 assert.strictEqual(after.state.calls.length, 0);
871
872 // Once stale, the restored cache refreshes only its newest edge.
873 yield* TestClock.adjust(Duration.minutes(2));
874 const refreshed = yield* restarted.readSummary({ ...WINDOW, awaitRefresh: true });
875 assert.deepStrictEqual(refreshed.buckets, original.buckets);
876 assert.deepStrictEqual(after.state.calls, [
877 { sinceMs: CURSOR_NOW - HOUR_MS, untilMs: CURSOR_NOW + 2 * 60 * 1000 },
878 ]);
879 }).pipe(
880 Effect.provide(layerService({ prefix: "usage-service-cursor-restart", home, settings })),
881 );
882 }).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
883 );
884
885 it.live("does not read the macOS Cursor Keychain before account usage is enabled", () =>
886 Effect.gen(function* () {
887 const { settings, home } = yield* setup;
888 const service = yield* UsageService.make.pipe(
889 Effect.provide(
890 layerService({
891 prefix: "usage-service-cursor-keychain-disabled",
892 home,
893 settings,
894 platform: "darwin",
895 environment: {},
896 }),
897 ),
898 );
899 const summary = yield* service.readSummary(WINDOW);
900 const cursor = summary.sources.find((source) => source.fingerprint.provider === "cursor");
901 assert.strictEqual(cursor?.status, "missing");
902 assert.strictEqual(cursor?.action, "enableCursorKeychain");
903 }).pipe(Effect.scoped),
904 );
905
906 it.live("ignores stale Cursor file logins when the active credential store differs", () =>
907 Effect.gen(function* () {
908 const { settings, home } = yield* setup;
909 for (const [index, testCase] of [
910 {
911 platform: "darwin" as const,
912 environment: { AGENT_CLI_CREDENTIAL_STORE: "memory" },
913 authPath: [".cursor", "auth.json"],
914 },
915 {
916 platform: "linux" as const,
917 environment: { AGENT_CLI_CREDENTIAL_STORE: "memory" },
918 authPath: ["config", "cursor", "auth.json"],
919 },
920 {
921 platform: "linux" as const,
922 environment: { CURSOR_API_KEY: "different-account" },
923 authPath: ["config", "cursor", "auth.json"],
924 },
925 ].entries()) {
926 const authPath = NodePath.join(home, ...testCase.authPath);
927 yield* Effect.promise(async () => {
928 await NodeFSP.mkdir(NodePath.dirname(authPath), { recursive: true });
929 await NodeFSP.writeFile(
930 authPath,
931 encodeUnknownJsonString({ accessToken: "stale-token" }),
932 );
933 });
934 const service = yield* UsageService.make.pipe(
935 Effect.provide(
936 layerService({
937 prefix: `usage-service-cursor-store-${index}`,
938 home,
939 settings,
940 platform: testCase.platform,
941 environment: testCase.environment,
942 }),
943 ),
944 );
945 const summary = yield* service.readSummary(WINDOW);
946 const cursor = summary.sources.find((source) => source.fingerprint.provider === "cursor");
947 assert.strictEqual(cursor?.status, "missing");
948 assert.include(cursor?.message ?? "", "Cursor CLI login");
949 assert.isFalse(summary.buckets.some((bucket) => bucket.provider === "cursor"));
950 }
951 }).pipe(Effect.scoped),
952 );
953
954 it.live(
955 "includes OpenCode history but does not substitute desktop usage for an unavailable Cursor account",
956 () =>
957 Effect.gen(function* () {
958 const { settings, home } = yield* setup;
959 const root = NodePath.join(home, "opencode");
960 const message = yield* encodeUnknownJson({
961 id: "msg_1",
962 sessionID: "session-1",
963 role: "assistant",
964 modelID: "example-model",
965 time: { created: Date.parse("2026-08-01T10:00:00Z") },
966 tokens: { input: 10, output: 5, reasoning: 2, cache: { read: 20, write: 3 } },
967 });
968 const bubble = yield* encodeUnknownJson({
969 type: 2,
970 createdAt: "2026-08-01T10:00:00Z",
971 modelInfo: { modelName: "example-model" },
972 tokenCount: { inputTokens: 100, outputTokens: 20 },
973 });
974 yield* Effect.promise(async () => {
975 const directory = NodePath.join(root, "storage", "message", "session-1");
976 await NodeFSP.mkdir(directory, { recursive: true });
977 await NodeFSP.writeFile(NodePath.join(directory, "msg_1.json"), message);
978 const desktop = NodePath.join(home, "config", "Cursor", "User", "globalStorage");
979 await NodeFSP.mkdir(desktop, { recursive: true });
980 const db = new NodeSqlite.DatabaseSync(NodePath.join(desktop, "state.vscdb"));
981 try {
982 db.exec("CREATE TABLE cursorDiskKV (key TEXT, value TEXT)");
983 db.prepare("INSERT INTO cursorDiskKV VALUES (?, ?)").run(
984 "bubbleId:session:assistant",
985 bubble,
986 );
987 } finally {
988 db.close();
989 }
990 });
991 const service = yield* UsageService.make.pipe(
992 Effect.provide(layerService({ prefix: "usage-service-opencode", home, settings })),
993 );
994 const summary = yield* service.readSummary(WINDOW);
995 assert.strictEqual(summary.buckets[0]?.provider, "opencode");
996 assert.isFalse(summary.buckets.some((bucket) => bucket.provider === "cursor"));
997 assert.isFalse(summary.sources.some((source) => source.fingerprint.provider === "cursor"));
998 assert.strictEqual(
999 summary.buckets[0]?.sourcePath,
1000 yield* Effect.promise(() => NodeFSP.realpath(root)),
1001 );
1002 assert.strictEqual(summary.buckets[0]?.totals.outputTokens, 7);
1003 assert.strictEqual(
1004 summary.sources.find((source) => source.fingerprint.provider === "opencode")
1005 ?.distinctSessions,
1006 1,
1007 );
1008 }).pipe(Effect.scoped),
1009 );
1010
1011 it.live("counts aliased OpenCode and Antigravity directories once", () =>
1012 Effect.gen(function* () {
1013 const { settings, home } = yield* setup;
1014 const opencode = NodePath.join(home, "opencode-store");
1015 const opencodeAlias = NodePath.join(home, "opencode-alias");
1016 const conversations = NodePath.join(home, "antigravity-conversations");
1017 const antigravityA = NodePath.join(home, "antigravity-a");
1018 const antigravityB = NodePath.join(home, "antigravity-b");
1019 yield* Effect.promise(async () => {
1020 await NodeFSP.mkdir(opencode);
1021 await NodeFSP.symlink(opencode, opencodeAlias, "junction");
1022 await NodeFSP.mkdir(conversations);
1023 await NodeFSP.mkdir(antigravityA);
1024 await NodeFSP.mkdir(antigravityB);
1025 await NodeFSP.symlink(
1026 conversations,
1027 NodePath.join(antigravityA, "conversations"),
1028 "junction",
1029 );
1030 await NodeFSP.symlink(
1031 conversations,
1032 NodePath.join(antigravityB, "conversations"),
1033 "junction",
1034 );
1035 });
1036 const service = yield* UsageService.make.pipe(
1037 Effect.provide(
1038 layerService({
1039 prefix: "usage-service-aliased-roots-test",
1040 home,
1041 settings,
1042 environment: {
1043 OPENCODE_DATA_DIR: `${opencode},${opencodeAlias}`,
1044 ANTIGRAVITY_DATA_DIR: `${antigravityA},${antigravityB}`,
1045 },
1046 }),
1047 ),
1048 );
1049 const summary = yield* service.readSummary(WINDOW);
1050 const sourcesFor = (provider: "opencode" | "antigravity") =>
1051 summary.sources.filter((source) => source.fingerprint.provider === provider);
1052 assert.strictEqual(sourcesFor("opencode").length, 1);
1053 assert.strictEqual(sourcesFor("antigravity").length, 1);
1054 assert.strictEqual(
1055 sourcesFor("opencode")[0]?.fingerprint.resolvedHomePath,
1056 yield* Effect.promise(() => NodeFSP.realpath(opencode)),
1057 );
1058 assert.strictEqual(
1059 sourcesFor("antigravity")[0]?.fingerprint.resolvedHomePath,
1060 yield* Effect.promise(() => NodeFSP.realpath(conversations)),
1061 );
1062 }).pipe(Effect.scoped),
1063 );
1064
1065 it.live("reads configured and disabled accounts once across shared and aliased homes", () =>
1066 Effect.gen(function* () {
1067 const { transcript, settings, home } = yield* setup;
1068 const codexHome = NodePath.join(home, "codex-account");
1069 const alias = NodePath.join(home, "codex-alias");
1070 const claudeHome = NodePath.join(home, "claude-account");
1071 const grokHome = NodePath.join(home, "grok-account");
1072 yield* Effect.promise(async () => {
1073 await NodeFSP.writeFile(transcript, claudeLine(1, 5));
1074 await NodeFSP.mkdir(NodePath.join(claudeHome, "projects"), { recursive: true });
1075 await NodeFSP.writeFile(
1076 NodePath.join(claudeHome, "projects", "session.jsonl"),
1077 claudeLine(2, 7),
1078 );
1079 await NodeFSP.mkdir(NodePath.join(codexHome, "sessions"), { recursive: true });
1080 await NodeFSP.symlink(codexHome, alias, "junction");
1081 await NodeFSP.writeFile(
1082 NodePath.join(codexHome, "sessions", "rollout.jsonl"),
1083 [
1084 { type: "session_meta", payload: { id: "codex-account-session" } },
1085 { type: "turn_context", payload: { model: "gpt-5.6-sol" } },
1086 // A-B-A at one timestamp must preserve both equal A events.
1087 ...[11, 12, 11].map((outputTokens) => ({
1088 type: "event_msg",
1089 timestamp: "2026-08-01T10:00:00Z",
1090 payload: {
1091 type: "token_count",
1092 info: { last_token_usage: { input_tokens: 10, output_tokens: outputTokens } },
1093 },
1094 })),
1095 ]
1096 .map((line) => encodeUnknownJsonString(line))
1097 .join("\n") + "\n",
1098 );
1099 await NodeFSP.mkdir(NodePath.join(grokHome, "sessions", "session"), { recursive: true });
1100 await NodeFSP.writeFile(
1101 NodePath.join(grokHome, "sessions", "session", "updates.jsonl"),
1102 encodeUnknownJsonString({
1103 timestamp: Date.parse("2026-08-01T10:00:00Z") / 1000,
1104 method: "_x.ai/session/update",
1105 params: {
1106 sessionId: "grok-account-session",
1107 update: {
1108 sessionUpdate: "turn_completed",
1109 prompt_id: "prompt-1",
1110 usage: { inputTokens: 10, outputTokens: 13 },
1111 },
1112 },
1113 }) + "\n",
1114 );
1115 });
1116 const service = yield* UsageService.make.pipe(
1117 Effect.provide(
1118 layerService({
1119 prefix: "usage-service-accounts-test",
1120 home,
1121 settings: {
1122 ...settings,
1123 providerInstances: {
1124 ...settings.providerInstances,
1125 [ProviderInstanceId.make("claude-work")]: {
1126 driver: ProviderDriverKind.make("claudeAgent"),
1127 enabled: false,
1128 environment: [{ name: "CLAUDE_CONFIG_DIR", value: claudeHome, sensitive: false }],
1129 },
1130 [ProviderInstanceId.make("codex-work")]: {
1131 driver: ProviderDriverKind.make("codex"),
1132 environment: [{ name: "CODEX_HOME", value: codexHome, sensitive: false }],
1133 },
1134 [ProviderInstanceId.make("codex-alias")]: {
1135 driver: ProviderDriverKind.make("codex"),
1136 config: { homePath: alias },
1137 },
1138 [ProviderInstanceId.make("codex-shadow")]: {
1139 driver: ProviderDriverKind.make("codex"),
1140 config: { homePath: codexHome, shadowHomePath: NodePath.join(home, "shadow") },
1141 environment: [
1142 { name: "CODEX_HOME", value: NodePath.join(home, "ignored"), sensitive: false },
1143 ],
1144 },
1145 [ProviderInstanceId.make("grok-work")]: {
1146 driver: ProviderDriverKind.make("grok"),
1147 // An undecodable config must not hide history Grok reads by home alone.
1148 config: { customModels: "not-a-list" },
1149 environment: [{ name: "GROK_HOME", value: grokHome, sensitive: false }],
1150 },
1151 },
1152 },
1153 }),
1154 ),
1155 );
1156 const summary = yield* service.readSummary(WINDOW);
1157 assert.strictEqual(totalOutputTokens(summary), 59);
1158 yield* Effect.promise(() =>
1159 NodeFSP.rename(
1160 NodePath.join(codexHome, "sessions", "rollout.jsonl"),
1161 NodePath.join(codexHome, "sessions", "moved.jsonl"),
1162 ),
1163 );
1164 const moved = yield* service.readSummary(WINDOW);
1165 assert.deepStrictEqual(moved.buckets, summary.buckets);
1166 yield* Effect.promise(() =>
1167 NodeFSP.rm(NodePath.join(codexHome, "sessions"), { recursive: true }),
1168 );
1169 const removed = yield* service.readSummary(WINDOW);
1170 assert.deepStrictEqual(removed.buckets, summary.buckets);
1171
1172 const sources = summary.sources.filter((source) => source.status === "ok");
1173 assert.strictEqual(sources.length, 4);
1174 assert.strictEqual(
1175 sources.reduce((sum, source) => sum + source.scannedFiles, 0),
1176 4,
1177 );
1178 assert.strictEqual(
1179 sources.filter((source) => source.fingerprint.provider === "codex").length,
1180 1,
1181 );
1182 }).pipe(Effect.scoped),
1183 );
1184
1185 it.live(
1186 "uses explicit account settings before environment and default homes, then refreshes them",
1187 () =>
1188 Effect.gen(function* () {
1189 const { transcript, settings, home } = yield* setup;
1190 const configured = NodePath.join(home, "configured");
1191 const environmentHome = NodePath.join(home, "environment");
1192 yield* Effect.promise(async () => {
1193 await NodeFSP.writeFile(transcript, claudeLine(1, 100));
1194 for (const [index, root] of [configured, environmentHome].entries()) {
1195 await NodeFSP.mkdir(NodePath.join(root, "projects"), { recursive: true });
1196 await NodeFSP.writeFile(
1197 NodePath.join(root, "projects", "session.jsonl"),
1198 claudeLine(index + 2, index + 7),
1199 );
1200 }
1201 await NodeFSP.mkdir(NodePath.join(configured, ".claude", "projects"), {
1202 recursive: true,
1203 });
1204 await NodeFSP.writeFile(
1205 NodePath.join(configured, ".claude", "projects", "wrong.jsonl"),
1206 claudeLine(4, 1000),
1207 );
1208 });
1209 yield* Effect.gen(function* () {
1210 const settingsService = yield* ServerSettings.ServerSettingsService;
1211 const service = yield* UsageService.make;
1212 const first = yield* service.readSummary(WINDOW);
1213 assert.strictEqual(totalOutputTokens(first), 7);
1214 const configuredProjects = yield* Effect.promise(() =>
1215 NodeFSP.realpath(NodePath.join(configured, "projects")),
1216 );
1217 assert.include(
1218 first.sources.map((source) => source.fingerprint.resolvedHomePath),
1219 configuredProjects,
1220 );
1221 yield* settingsService.updateSettings({
1222 providerInstances: {
1223 ...settings.providerInstances,
1224 [ProviderInstanceId.make("claudeAgent")]: {
1225 driver: ProviderDriverKind.make("claudeAgent"),
1226 config: { homePath: "" },
1227 environment: [
1228 { name: "CLAUDE_CONFIG_DIR", value: environmentHome, sensitive: false },
1229 ],
1230 },
1231 },
1232 });
1233 const second = yield* service.readSummary(WINDOW);
1234 assert.strictEqual(totalOutputTokens(second), 8);
1235 const environmentProjects = yield* Effect.promise(() =>
1236 NodeFSP.realpath(NodePath.join(environmentHome, "projects")),
1237 );
1238 assert.include(
1239 second.sources.map((source) => source.fingerprint.resolvedHomePath),
1240 environmentProjects,
1241 );
1242 }).pipe(
1243 Effect.scoped,
1244 Effect.provide(
1245 layerService({
1246 prefix: "usage-service-home-refresh-test",
1247 home,
1248 environment: { CLAUDE_CONFIG_DIR: NodePath.join(home, "host-ignored") },
1249 settings: {
1250 ...settings,
1251 providerInstances: {
1252 ...settings.providerInstances,
1253 [ProviderInstanceId.make("claudeAgent")]: {
1254 driver: ProviderDriverKind.make("claudeAgent"),
1255 config: { homePath: configured },
1256 environment: [
1257 { name: "CLAUDE_CONFIG_DIR", value: environmentHome, sensitive: false },
1258 ],
1259 },
1260 },
1261 },
1262 }),
1263 ),
1264 );
1265 }).pipe(Effect.scoped),
1266 );
1267
1268 it.live(
1269 "uses inherited home variables when explicit default accounts have no home settings",
1270 () =>
1271 Effect.gen(function* () {
1272 const { transcript, settings, home } = yield* setup;
1273 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1274 const service = yield* UsageService.make.pipe(
1275 Effect.provide(
1276 layerService({
1277 prefix: "usage-service-inherited-homes-test",
1278 home,
1279 environment: {
1280 CODEX_HOME: NodePath.join(home, "inherited-codex"),
1281 CLAUDE_CONFIG_DIR: NodePath.join(home, "claude"),
1282 },
1283 settings: {
1284 ...settings,
1285 providerInstances: {
1286 [ProviderInstanceId.make("codex")]: {
1287 driver: ProviderDriverKind.make("codex"),
1288 config: {},
1289 },
1290 [ProviderInstanceId.make("claudeAgent")]: {
1291 driver: ProviderDriverKind.make("claudeAgent"),
1292 config: {},
1293 },
1294 },
1295 },
1296 }),
1297 ),
1298 );
1299 const summary = yield* service.readSummary(WINDOW);
1300 assert.strictEqual(totalOutputTokens(summary), 5);
1301 assert.strictEqual(
1302 summary.sources.find((source) => source.fingerprint.provider === "codex")?.fingerprint
1303 .resolvedHomePath,
1304 NodePath.join(home, "inherited-codex", "sessions"),
1305 );
1306 assert.strictEqual(
1307 summary.sources.find((source) => source.fingerprint.provider === "grok")?.fingerprint
1308 .resolvedHomePath,
1309 NodePath.join(home, "grok", "sessions"),
1310 );
1311 }).pipe(Effect.scoped),
1312 );
1313
1314 it.live.skipIf(HostProcess.Platform.defaultValue() === "win32" || process.getuid?.() === 0)(
1315 "reports unreadable transcripts as partial and recovers once they can be read",
1316 () =>
1317 Effect.gen(function* () {
1318 const { transcript, settings, home } = yield* setup;
1319 const unreadable = NodePath.join(NodePath.dirname(transcript), "other.jsonl");
1320 yield* Effect.promise(async () => {
1321 await NodeFSP.writeFile(transcript, claudeLine(1, 5));
1322 await NodeFSP.writeFile(unreadable, claudeLine(2, 7));
1323 await NodeFSP.chmod(unreadable, 0);
1324 });
1325 yield* Effect.gen(function* () {
1326 const service = yield* UsageService.make;
1327 const partial = yield* service.readSummary(WINDOW);
1328 assert.strictEqual(totalOutputTokens(partial), 5);
1329 assert.strictEqual(partial.sources[0]?.status, "partial");
1330
1331 yield* Effect.promise(() => NodeFSP.chmod(unreadable, 0o600));
1332 const healthy = yield* service.readSummary(WINDOW);
1333 assert.strictEqual(totalOutputTokens(healthy), 12);
1334 assert.strictEqual(healthy.sources[0]?.status, "ok");
1335 assert.isNull(healthy.sources[0]?.message);
1336 }).pipe(
1337 Effect.provide(layerService({ prefix: "usage-service-unreadable", home, settings })),
1338 );
1339 }).pipe(Effect.scoped),
1340 );
1341
1342 it.live("reprices unchanged transcripts when custom prices are added, edited, or removed", () =>
1343 Effect.gen(function* () {
1344 const { transcript, settings, home } = yield* setup;
1345 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5, "example-model")));
1346
1347 yield* Effect.gen(function* () {
1348 const settingsService = yield* ServerSettings.ServerSettingsService;
1349 const service = yield* UsageService.make;
1350
1351 const original = yield* service.readSummary(WINDOW);
1352 assert.strictEqual(original.buckets[0]?.costUsd, 0);
1353 assert.strictEqual(original.buckets[0]?.unpricedRecords, 1);
1354
1355 yield* settingsService.updateSettings({
1356 usagePriceOverrides: {
1357 "example-model": { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 },
1358 },
1359 });
1360 const overridden = yield* service.readSummary(WINDOW);
1361 assert.closeTo(overridden.buckets[0]?.costUsd ?? -1, 0.00006, 1e-12);
1362 assert.strictEqual(overridden.buckets[0]?.costSource, "modelPriced");
1363 assert.strictEqual(overridden.buckets[0]?.unpricedRecords, 0);
1364 assert.deepStrictEqual(overridden.buckets[0]?.totals, original.buckets[0]?.totals);
1365
1366 yield* settingsService.updateSettings({
1367 usagePriceOverrides: {
1368 "example-model": { inputCostPerMillionTokens: 4, outputCostPerMillionTokens: 16 },
1369 },
1370 });
1371 const edited = yield* service.readSummary(WINDOW);
1372 assert.closeTo(edited.buckets[0]?.costUsd ?? -1, 0.00012, 1e-12);
1373
1374 yield* settingsService.updateSettings({ usagePriceOverrides: { "example-model": null } });
1375 const restored = yield* service.readSummary(WINDOW);
1376 assert.deepStrictEqual(restored.buckets, original.buckets);
1377 }).pipe(
1378 Effect.scoped,
1379 Effect.provide(
1380 layerService({ prefix: "usage-service-price-overrides-test", home, settings }),
1381 ),
1382 );
1383 }).pipe(Effect.scoped),
1384 );
1385
1386 it.live("counts appended usage on a rescan of a grown transcript", () =>
1387 Effect.gen(function* () {
1388 const { transcript, settings, home } = yield* setup;
1389 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1390
1391 const service = yield* UsageService.make.pipe(
1392 Effect.provide(layerService({ prefix: "usage-service-grow-test", home, settings })),
1393 );
1394
1395 const first = yield* service.readSummary(WINDOW);
1396 assert.strictEqual(totalOutputTokens(first), 5);
1397
1398 yield* Effect.promise(() => NodeFSP.appendFile(transcript, claudeLine(2, 7)));
1399 const second = yield* service.readSummary(WINDOW);
1400 assert.strictEqual(totalOutputTokens(second), 12);
1401 }).pipe(Effect.scoped),
1402 );
1403
1404 it.live(
1405 "keeps large-record totals and costs exact through append, dedupe, restart and cleanup",
1406 () =>
1407 Effect.gen(function* () {
1408 const { transcript, settings, home } = yield* setup;
1409 const large = claudeLine(1, 9900).replace(
1410 '"message":',
1411 '"padding":' + encodeUnknownJsonString("x".repeat(9 * 1024 * 1024)) + ',"message":',
1412 );
1413 yield* Effect.promise(() => NodeFSP.writeFile(transcript, large));
1414 yield* Effect.gen(function* () {
1415 const service = yield* UsageService.make;
1416 const first = yield* service.readSummary(WINDOW);
1417 assert.strictEqual(totalOutputTokens(first), 9900);
1418 assert.closeTo(
1419 first.buckets.reduce((sum, bucket) => sum + bucket.costUsd, 0),
1420 0.4951,
1421 1e-12,
1422 );
1423 const warm = yield* service.readSummary(WINDOW);
1424 assert.deepStrictEqual(warm.buckets, first.buckets);
1425 // The repeated content block has the same message/request identity.
1426 yield* Effect.promise(() => NodeFSP.appendFile(transcript, large + claudeLine(2, 100)));
1427 const appended = yield* service.readSummary(WINDOW);
1428 assert.strictEqual(totalOutputTokens(appended), 10000);
1429 assert.strictEqual(
1430 appended.buckets.reduce((sum, bucket) => sum + bucket.totals.uncachedInputTokens, 0),
1431 20,
1432 );
1433 yield* service.awaitPersisted;
1434 const restarted = yield* UsageService.make;
1435 const restored = yield* restarted.readSummary(WINDOW);
1436 assert.deepStrictEqual(restored.buckets, appended.buckets);
1437 yield* restarted.awaitPersisted;
1438 yield* Effect.promise(() => NodeFSP.rm(transcript));
1439 const afterCleanup = yield* UsageService.make;
1440 assert.deepStrictEqual(
1441 (yield* afterCleanup.readSummary(WINDOW)).buckets,
1442 appended.buckets,
1443 );
1444 }).pipe(
1445 Effect.scoped,
1446 Effect.provide(
1447 layerService({
1448 prefix: "usage-service-large-record-test",
1449 home,
1450 settings,
1451 ratesDocument: {
1452 "claude-fable-5": { input_cost_per_token: 1e-5, output_cost_per_token: 5e-5 },
1453 },
1454 }),
1455 ),
1456 );
1457 }).pipe(Effect.scoped),
1458 );
1459
1460 it.live(
1461 "upgrades a v4 cache: reprices live Codex tiers, keeps deleted rollouts, leaves v4 intact",
1462 () =>
1463 Effect.gen(function* () {
1464 const { home, settings } = yield* setup;
1465 const sessions = NodePath.join(home, "codex", "sessions");
1466 const rollout = (sessionId: string, outputTokens: number) =>
1467 [
1468 { type: "session_meta", payload: { id: sessionId } },
1469 { type: "turn_context", payload: { model: "gpt-6-astra" } },
1470 {
1471 type: "event_msg",
1472 payload: {
1473 type: "thread_settings_applied",
1474 thread_settings: { service_tier: "ultrafast" },
1475 },
1476 },
1477 {
1478 type: "event_msg",
1479 timestamp: "2026-08-01T10:00:00Z",
1480 payload: {
1481 type: "token_count",
1482 info: { last_token_usage: { input_tokens: 0, output_tokens: outputTokens } },
1483 },
1484 },
1485 ]
1486 .map((line) => encodeUnknownJsonString(line))
1487 .join("\n") + "\n";
1488 const live = NodePath.join(sessions, "live.jsonl");
1489 const deleted = NodePath.join(sessions, "deleted.jsonl");
1490 yield* Effect.promise(async () => {
1491 await NodeFSP.mkdir(sessions, { recursive: true });
1492 await NodeFSP.writeFile(live, rollout("live", 10));
1493 await NodeFSP.writeFile(deleted, rollout("deleted", 20));
1494 });
1495
1496 yield* Effect.gen(function* () {
1497 const { stateDir } = yield* ServerConfig.ServerConfig;
1498 const cachePath = NodePath.join(stateDir, "usage-scan-cache-v5.json");
1499 const legacyPath = NodePath.join(stateDir, "usage-scan-cache.json");
1500 const first = yield* UsageService.make;
1501 yield* first.readSummary(WINDOW);
1502 yield* first.awaitPersisted;
1503
1504 // Rewrite the cache as a v4 server left it: every Codex record at
1505 // speed 0 (standard), and no tier in the reducer state.
1506 const legacy = yield* Effect.promise(async () => {
1507 const document = decodeUnknownJsonString(await NodeFSP.readFile(cachePath, "utf8")) as {
1508 files: Record<string, { r: unknown[][]; cs: { speed?: unknown } }>;
1509 };
1510 for (const file of Object.values(document.files)) {
1511 file.r = file.r.map((row) => [...row.slice(0, 10), 0]);
1512 delete file.cs.speed;
1513 }
1514 const text = encodeUnknownJsonString({ ...document, version: 4 });
1515 await NodeFSP.writeFile(legacyPath, text);
1516 await NodeFSP.rm(cachePath);
1517 await NodeFSP.rm(deleted);
1518 return text;
1519 });
1520
1521 const summary = yield* (yield* UsageService.make).readSummary(WINDOW);
1522 // The live rollout re-parses at the ultrafast rate (10 x 6); the
1523 // deleted one keeps its saved v4 usage at the standard rate (20 x 1).
1524 assert.strictEqual(totalOutputTokens(summary), 30);
1525 assert.strictEqual(
1526 summary.buckets.reduce((sum, bucket) => sum + bucket.costUsd, 0),
1527 80,
1528 );
1529 // A v4 server sharing this state directory still finds its own cache.
1530 assert.strictEqual(
1531 yield* Effect.promise(() => NodeFSP.readFile(legacyPath, "utf8")),
1532 legacy,
1533 );
1534 }).pipe(
1535 Effect.provide(
1536 layerService({
1537 prefix: "usage-service-v4-upgrade-test",
1538 home,
1539 settings,
1540 ratesDocument: {
1541 "gpt-6-astra": {
1542 input_cost_per_token: 0,
1543 output_cost_per_token: 1,
1544 input_cost_per_token_ultrafast: 0,
1545 output_cost_per_token_ultrafast: 6,
1546 },
1547 },
1548 }),
1549 ),
1550 );
1551 }).pipe(Effect.scoped),
1552 );
1553
1554 it.live("preserves saved tokens, costs and sessions after transcript cleanup and restart", () =>
1555 Effect.gen(function* () {
1556 const { transcript, settings, home } = yield* setup;
1557 const alias = NodePath.join(home, "claude-alias");
1558 yield* Effect.promise(() =>
1559 NodeFSP.symlink(NodePath.join(home, "claude"), alias, "junction"),
1560 );
1561 const content = claudeLine(1, 5);
1562 yield* Effect.promise(() => NodeFSP.writeFile(transcript, content));
1563 yield* Effect.gen(function* () {
1564 const service = yield* UsageService.make;
1565 const first = yield* service.readSummary(WINDOW);
1566 assert.strictEqual(totalOutputTokens(first), 5);
1567 assert.isAbove(first.buckets[0]?.costUsd ?? 0, 0);
1568
1569 yield* Effect.promise(() => NodeFSP.rm(transcript));
1570 const deleted = yield* service.readSummary(WINDOW);
1571 assert.deepStrictEqual(deleted.buckets, first.buckets);
1572 assert.deepStrictEqual(deleted.sources, first.sources);
1573
1574 yield* service.awaitPersisted;
1575 const restarted = yield* UsageService.make;
1576 const restored = yield* restarted.readSummary(WINDOW);
1577 assert.deepStrictEqual(restored.buckets, first.buckets);
1578 assert.deepStrictEqual(restored.sources, first.sources);
1579
1580 // A moved transcript must not count the saved usage twice.
1581 yield* Effect.promise(() => NodeFSP.writeFile(transcript + ".jsonl", content));
1582 const moved = yield* restarted.readSummary(WINDOW);
1583 assert.deepStrictEqual(moved.buckets, first.buckets);
1584 assert.strictEqual(moved.sources[0]?.distinctSessions, 1);
1585 yield* restarted.awaitPersisted;
1586
1587 const replacementProjects = NodePath.join(home, "replacement-projects");
1588 yield* Effect.promise(() => NodeFSP.mkdir(replacementProjects));
1589 yield* Effect.promise(() =>
1590 NodeFSP.rm(NodePath.join(home, "claude", "projects"), { recursive: true }),
1591 );
1592 const afterRootCleanup = yield* UsageService.make;
1593 const missingRoot = yield* afterRootCleanup.readSummary(WINDOW);
1594 assert.deepStrictEqual(missingRoot.buckets, first.buckets);
1595 assert.strictEqual(missingRoot.sources[0]?.distinctSessions, 1);
1596 assert.strictEqual(missingRoot.sources[0]?.status, "ok");
1597 assert.deepStrictEqual(missingRoot.sources[0]?.fingerprint, first.sources[0]?.fingerprint);
1598 yield* Effect.promise(async () => {
1599 const projects = NodePath.join(home, "claude", "projects");
1600 await NodeFSP.rename(replacementProjects, projects);
1601 await NodeFSP.writeFile(NodePath.join(projects, "new.jsonl"), claudeLine(2, 7));
1602 });
1603 const recreated = yield* afterRootCleanup.readSummary(WINDOW);
1604 assert.strictEqual(totalOutputTokens(recreated), 12);
1605 assert.deepStrictEqual(recreated.sources[0]?.fingerprint, first.sources[0]?.fingerprint);
1606
1607 const merged = mergeUsage(
1608 [
1609 {
1610 environmentId: EnvironmentId.make("cleanup-test"),
1611 label: "test",
1612 summary: recreated,
1613 },
1614 {
1615 environmentId: EnvironmentId.make("other-environment"),
1616 label: "before cleanup",
1617 summary: first,
1618 },
1619 ],
1620 missingRoot.contractVersion,
1621 );
1622 assert.strictEqual(merged.outputTokens, 12);
1623 assert.strictEqual(merged.sessions, 1);
1624 assert.strictEqual(merged.costUsd, recreated.buckets[0]?.costUsd);
1625
1626 const outsideWindow = yield* restarted.readSummary({
1627 ...WINDOW,
1628 sinceDay: UsageDay.make("2026-08-02"),
1629 });
1630 assert.deepStrictEqual(outsideWindow.buckets, []);
1631 assert.strictEqual(outsideWindow.sources[0]?.distinctSessions, 0);
1632 }).pipe(
1633 // Scoped inside the state directory, so pending cache writes land
1634 // before it is removed.
1635 Effect.scoped,
1636 Effect.provide(
1637 layerService({
1638 prefix: "usage-service-cleanup-test",
1639 home,
1640 settings: {
1641 providerInstances: {
1642 ...settings.providerInstances,
1643 [ProviderInstanceId.make("claudeAgent")]: {
1644 driver: ProviderDriverKind.make("claudeAgent"),
1645 config: { homePath: alias },
1646 },
1647 },
1648 },
1649 ratesDocument: {
1650 "claude-fable-5": { input_cost_per_token: 1e-5, output_cost_per_token: 5e-5 },
1651 },
1652 }),
1653 ),
1654 );
1655 }).pipe(Effect.scoped),
1656 );
1657
1658 it.live("credits the same copy of a duplicate after its transcripts are deleted", () =>
1659 Effect.gen(function* () {
1660 const { transcript, settings, home } = yield* setup;
1661 const dir = NodePath.dirname(transcript);
1662 // The walk-first file is the original, padded so it finishes parsing
1663 // after the small fork copy that repeats its record under a new session.
1664 const [first = "", second = ""] = yield* Effect.promise(async () => {
1665 await NodeFSP.writeFile(NodePath.join(dir, "a.jsonl"), "");
1666 await NodeFSP.writeFile(NodePath.join(dir, "b.jsonl"), "");
1667 return (await NodeFSP.readdir(dir)).map((name) => NodePath.join(dir, name));
1668 });
1669 const forked = (line: string) => line.replace('"session-1"', '"session-2"');
1670 yield* Effect.promise(async () => {
1671 await NodeFSP.writeFile(
1672 first,
1673 claudeLine(1, 5).replace(
1674 '"message":',
1675 '"padding":' + encodeUnknownJsonString("x".repeat(9 * 1024 * 1024)) + ',"message":',
1676 ),
1677 );
1678 await NodeFSP.writeFile(second, forked(claudeLine(1, 5)) + forked(claudeLine(2, 7)));
1679 });
1680 yield* Effect.gen(function* () {
1681 const service = yield* UsageService.make;
1682 const live = yield* service.readSummary(WINDOW);
1683 assert.strictEqual(live.buckets[0]?.sessions, 2);
1684
1685 yield* Effect.promise(() => Promise.all([NodeFSP.rm(first), NodeFSP.rm(second)]));
1686 const saved = yield* service.readSummary(WINDOW);
1687 assert.deepStrictEqual(saved.buckets, live.buckets);
1688 assert.deepStrictEqual(saved.sources, live.sources);
1689 yield* service.awaitPersisted;
1690 const restored = yield* (yield* UsageService.make).readSummary(WINDOW);
1691 assert.deepStrictEqual(restored.buckets, live.buckets);
1692 }).pipe(
1693 Effect.provide(layerService({ prefix: "usage-service-copy-order-test", home, settings })),
1694 );
1695 }).pipe(Effect.scoped),
1696 );
1697
1698 it.live.skipIf(HostProcess.Platform.defaultValue() === "win32")(
1699 "keeps a newer cached read when a slower scan of another window finishes later",
1700 () =>
1701 Effect.gen(function* () {
1702 const { transcript, settings, home } = yield* setup;
1703 const dir = NodePath.dirname(transcript);
1704 const probe = NodePath.join(dir, "probe.jsonl");
1705 const hold = NodePath.join(dir, "hold.jsonl");
1706 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1707 yield* makeGate(probe, BEFORE_NARROW_WINDOW);
1708 yield* makeGate(hold, BEFORE_NARROW_WINDOW);
1709 yield* Effect.gen(function* () {
1710 const service = yield* UsageService.make;
1711 const wide = yield* service.readSummary(WINDOW).pipe(Effect.forkChild);
1712 yield* openGate(probe);
1713 yield* replaceFile(transcript, claudeLine(1, 5) + claudeLine(2, 7));
1714 // The narrow scan caches the newer read while the wide one waits.
1715 yield* service.readSummary(NARROW_WINDOW);
1716 yield* openGate(hold);
1717 yield* Fiber.join(wide);
1718
1719 yield* Effect.promise(() =>
1720 Promise.all([transcript, probe, hold].map((path) => NodeFSP.rm(path))),
1721 );
1722 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);
1723 }).pipe(
1724 Effect.provide(layerService({ prefix: "usage-service-stale-read-test", home, settings })),
1725 );
1726 }).pipe(Effect.scoped),
1727 );
1728
1729 it.live.skipIf(HostProcess.Platform.defaultValue() === "win32")(
1730 "keeps the later read when a scan that read earlier finishes first",
1731 () =>
1732 Effect.gen(function* () {
1733 const { transcript, settings, home } = yield* setup;
1734 const dir = NodePath.dirname(transcript);
1735 const gate = (name: string) => NodePath.join(dir, `${name}.jsonl`);
1736 const wideProbe = gate("wide-probe");
1737 const wideHold = gate("wide-hold");
1738 const narrowProbe = gate("narrow-probe");
1739 const narrowHold = gate("narrow-hold");
1740 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1741 yield* makeGate(wideProbe, BEFORE_NARROW_WINDOW);
1742 yield* makeGate(wideHold, BEFORE_NARROW_WINDOW);
1743 yield* Effect.gen(function* () {
1744 const service = yield* UsageService.make;
1745 const wide = yield* service.readSummary(WINDOW).pipe(Effect.forkChild);
1746 yield* openGate(wideProbe);
1747 yield* replaceFile(transcript, claudeLine(1, 5) + claudeLine(2, 7));
1748 // Made after the wide scan's walk, so only the narrow scan waits on them.
1749 yield* makeGate(narrowProbe);
1750 yield* makeGate(narrowHold);
1751 const narrow = yield* service.readSummary(NARROW_WINDOW).pipe(Effect.forkChild);
1752 yield* openGate(narrowProbe);
1753 // Both scans started from an empty cache entry; the earlier read lands first.
1754 yield* openGate(wideHold);
1755 yield* Fiber.join(wide);
1756 yield* openGate(narrowHold);
1757 yield* Fiber.join(narrow);
1758
1759 yield* Effect.promise(() =>
1760 Promise.all(
1761 [transcript, wideProbe, wideHold, narrowProbe, narrowHold].map((path) =>
1762 NodeFSP.rm(path),
1763 ),
1764 ),
1765 );
1766 assert.strictEqual(totalOutputTokens(yield* service.readSummary(WINDOW)), 12);
1767 }).pipe(
1768 Effect.provide(layerService({ prefix: "usage-service-late-read-test", home, settings })),
1769 );
1770 }).pipe(Effect.scoped),
1771 );
1772
1773 it.live("reports saved usage of a removed directory only for windows it reaches", () =>
1774 Effect.gen(function* () {
1775 const { transcript, settings, home } = yield* setup;
1776 yield* Effect.promise(async () => {
1777 await NodeFSP.writeFile(transcript, claudeLine(1, 5));
1778 const lastWrite = Date.parse("2026-08-01T10:00:00Z") / 1000;
1779 await NodeFSP.utimes(transcript, lastWrite, lastWrite);
1780 });
1781 const service = yield* UsageService.make.pipe(
1782 Effect.provide(layerService({ prefix: "usage-service-saved-window-test", home, settings })),
1783 );
1784 const first = yield* service.readSummary(WINDOW);
1785 yield* Effect.promise(() =>
1786 NodeFSP.rm(NodePath.join(home, "claude", "projects"), { recursive: true }),
1787 );
1788
1789 const reached = yield* service.readSummary(WINDOW);
1790 assert.deepStrictEqual(reached.buckets, first.buckets);
1791 assert.strictEqual(reached.sources[0]?.status, "ok");
1792
1793 // A missing source cannot claim this directory from another environment
1794 // that still reads it, so it adds nothing to a window after its last write.
1795 const later = yield* service.readSummary({
1796 timeZone: "UTC",
1797 sinceDay: UsageDay.make("2026-08-10"),
1798 untilDay: UsageDay.make("2026-08-12"),
1799 });
1800 assert.deepStrictEqual(later.buckets, []);
1801 assert.strictEqual(later.sources[0]?.status, "missing");
1802 assert.strictEqual(later.sources[0]?.scannedFiles, 0);
1803 }).pipe(Effect.scoped),
1804 );
1805
1806 it.live("does not share an in-flight scan after custom prices change", () =>
1807 Effect.gen(function* () {
1808 const { transcript, settings, home } = yield* setup;
1809 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5, "example-model")));
1810 const transcriptDir = yield* Effect.promise(() =>
1811 NodeFSP.realpath(NodePath.join(home, "claude", "projects")),
1812 );
1813
1814 yield* Effect.gen(function* () {
1815 const settingsService = yield* ServerSettings.ServerSettingsService;
1816 const fileSystem = yield* FileSystem.FileSystem;
1817 const firstScanStarted = yield* Deferred.make<void>();
1818 const secondScanStarted = yield* Deferred.make<void>();
1819 const releaseRates = yield* Deferred.make<void>();
1820 let homeProbes = 0;
1821 const service = yield* UsageService.make.pipe(
1822 Effect.provideService(FileSystem.FileSystem, {
1823 ...fileSystem,
1824 exists: (path) =>
1825 fileSystem.exists(path).pipe(
1826 Effect.tap(() => {
1827 if (path !== transcriptDir) return Effect.void;
1828 homeProbes += 1;
1829 return Deferred.succeed(
1830 homeProbes === 1 ? firstScanStarted : secondScanStarted,
1831 undefined,
1832 );
1833 }),
1834 ),
1835 }),
1836 Effect.provideService(
1837 HttpClient.HttpClient,
1838 HttpClient.make((request) =>
1839 Deferred.await(releaseRates).pipe(
1840 Effect.as(HttpClientResponse.fromWeb(request, Response.json({}))),
1841 ),
1842 ),
1843 ),
1844 );
1845
1846 const first = yield* service.readSummary(WINDOW).pipe(Effect.forkChild);
1847 yield* Deferred.await(firstScanStarted);
1848 yield* settingsService.updateSettings({
1849 usagePriceOverrides: {
1850 "example-model": { inputCostPerMillionTokens: 2, outputCostPerMillionTokens: 8 },
1851 },
1852 });
1853 const second = yield* service.readSummary(WINDOW).pipe(Effect.forkChild);
1854 yield* Deferred.await(secondScanStarted);
1855 yield* Deferred.succeed(releaseRates, undefined);
1856
1857 const original = yield* Fiber.join(first);
1858 const updated = yield* Fiber.join(second);
1859 assert.strictEqual(original.buckets[0]?.costUsd, 0);
1860 assert.closeTo(updated.buckets[0]?.costUsd ?? -1, 0.00006, 1e-12);
1861 }).pipe(
1862 Effect.provide(layerService({ prefix: "usage-service-price-race-test", home, settings })),
1863 );
1864 }).pipe(Effect.scoped),
1865 );
1866
1867 it.live("shares one scan between concurrent identical requests", () =>
1868 Effect.gen(function* () {
1869 const { transcript, settings, home } = yield* setup;
1870 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1871
1872 let ratesFetches = 0;
1873 const service = yield* UsageService.make.pipe(
1874 Effect.provide(
1875 layerService({
1876 prefix: "usage-service-flight-test",
1877 home,
1878 settings,
1879 onRatesFetch: () => {
1880 ratesFetches += 1;
1881 },
1882 }),
1883 ),
1884 );
1885
1886 const [first, second] = yield* Effect.all(
1887 [service.readSummary(WINDOW), service.readSummary(WINDOW)],
1888 { concurrency: 2 },
1889 );
1890 assert.deepStrictEqual(first, second);
1891 assert.strictEqual(ratesFetches, 1);
1892
1893 // A later request is fresh work again, not a stale cached answer.
1894 yield* service.readSummary(WINDOW);
1895 assert.strictEqual(ratesFetches, 2);
1896 }).pipe(Effect.scoped),
1897 );
1898
1899 it.live("refetches a rate table inside its TTL only when the client asks", () =>
1900 Effect.gen(function* () {
1901 const { transcript, settings, home } = yield* setup;
1902 yield* Effect.promise(() => NodeFSP.writeFile(transcript, claudeLine(1, 5)));
1903
1904 let ratesFetches = 0;
1905 const service = yield* UsageService.make.pipe(
1906 Effect.provide(
1907 layerService({
1908 prefix: "usage-service-rates-refresh-test",
1909 home,
1910 settings,
1911 ratesDocument: {
1912 "claude-fable-5": { input_cost_per_token: 1e-5, output_cost_per_token: 5e-5 },
1913 },
1914 onRatesFetch: () => {
1915 ratesFetches += 1;
1916 },
1917 }),
1918 ),
1919 );
1920
1921 const first = yield* service.readSummary(WINDOW);
1922 assert.strictEqual(ratesFetches, 1);
1923 assert.strictEqual(first.pricing.status, "fresh");
1924
1925 // Inside the daily TTL a plain rescan keeps the cached table.
1926 yield* TestClock.adjust(Duration.minutes(2));
1927 yield* service.readSummary(WINDOW);
1928 assert.strictEqual(ratesFetches, 1);
1929
1930 // An explicit refresh fetches again so a newly listed model gets priced.
1931 // A burst of refreshes shares that one fetch.
1932 const [refreshed] = yield* Effect.all([service.refreshRates, service.refreshRates], {
1933 concurrency: 2,
1934 });
1935 assert.strictEqual(ratesFetches, 2);
1936 assert.strictEqual(refreshed.status, "fresh");
1937 assert.strictEqual(refreshed.knownModels, 1);
1938 }).pipe(Effect.scoped, Effect.provide(TestClock.layer())),
1939 );
1940
1941 it.live("does not orphan an in-flight scan when its first caller is interrupted", () =>
1942 Effect.gen(function* () {
1943 const { settings, home } = yield* setup;
1944 const service = yield* UsageService.make.pipe(
1945 Effect.provide(layerService({ prefix: "usage-service-interruption-test", home, settings })),
1946 );
1947
1948 let orphanedAt: number | undefined;
1949 for (let interruptAt = 1; interruptAt <= 31; interruptAt += 1) {
1950 const tasks: Array<() => void> = [];
1951 const dispatcher: Scheduler.SchedulerDispatcher = {
1952 scheduleTask: (task) => tasks.push(task),
1953 flush: () => {
1954 let task: (() => void) | undefined;
1955 while ((task = tasks.shift()) !== undefined) task();
1956 },
1957 };
1958
1959 let requestFiber: Fiber.Fiber<unknown, unknown> | undefined;
1960 let requestChecks = 0;
1961 const scheduler: Scheduler.Scheduler = {
1962 executionMode: "async",
1963 makeDispatcher: () => dispatcher,
1964 shouldYield: (fiber) => {
1965 if (fiber !== requestFiber) return false;
1966 requestChecks += 1;
1967 if (requestChecks !== interruptAt) return false;
1968 fiber.interruptUnsafe();
1969 return true;
1970 },
1971 };
1972
1973 // Each candidate needs a distinct key because the broken case leaves
1974 // its entry in the service's private in-flight map. The invalid window
1975 // keeps the real scan synchronous once its detached fiber starts.
1976 const input: UsageSummaryInput = {
1977 ...WINDOW,
1978 sinceDay: UsageDay.make("2026-09-01"),
1979 untilDay: UsageDay.make(`2026-08-${String(interruptAt).padStart(2, "0")}`),
1980 };
1981 const first = yield* service
1982 .readSummary(input)
1983 .pipe(
1984 Effect.exit,
1985 Effect.provideService(Scheduler.Scheduler, scheduler),
1986 Effect.forkChild,
1987 );
1988 requestFiber = first;
1989 yield* Effect.yieldNow;
1990 dispatcher.flush();
1991
1992 const second = yield* service.readSummary(input).pipe(
1993 Effect.match({
1994 onFailure: (error) => error.reason,
1995 onSuccess: () => "success" as const,
1996 }),
1997 Effect.provideService(Scheduler.Scheduler, scheduler),
1998 Effect.forkChild,
1999 );
2000 yield* Effect.yieldNow;
2001 dispatcher.flush();
2002 const secondExit = second.pollUnsafe();
2003 if (secondExit === undefined) {
2004 second.interruptUnsafe();
2005 orphanedAt = interruptAt;
2006 break;
2007 }
2008 if (Exit.isFailure(secondExit)) {
2009 assert.fail("the matching request fiber was interrupted");
2010 }
2011 assert.strictEqual(secondExit.value, "invalidWindow");
2012 }
2013
2014 assert.isUndefined(
2015 orphanedAt,
2016 `interruption left the next matching request pending at scheduler check ${orphanedAt}`,
2017 );
2018 }).pipe(Effect.scoped),
2019 );
2020});