| 1 | // @effect-diagnostics nodeBuiltinImport:off - the suite seeds and grows real |
| 2 | // transcript trees on disk, outside the service's Effect FileSystem. |
| 3 | import * as NodeChildProcess from "node:child_process"; |
| 4 | import * as NodeFSP from "node:fs/promises"; |
| 5 | import * as NodeOS from "node:os"; |
| 6 | import * as NodePath from "node:path"; |
| 7 | import * as NodeSqlite from "node:sqlite"; |
| 8 | |
| 9 | import { assert, describe, it } from "@effect/vitest"; |
| 10 | import * as NodeServices from "@effect/platform-node/NodeServices"; |
| 11 | import * as HostProcess from "@t3tools/shared/HostProcess"; |
| 12 | import { mergeUsage } from "@t3tools/shared/usageMerge"; |
| 13 | import { |
| 14 | EnvironmentId, |
| 15 | ProviderDriverKind, |
| 16 | ProviderInstanceId, |
| 17 | UsageDay, |
| 18 | type UsageSource, |
| 19 | type UsageSummaryInput, |
| 20 | } from "@t3tools/contracts"; |
| 21 | import * as Duration from "effect/Duration"; |
| 22 | import * as Deferred from "effect/Deferred"; |
| 23 | import * as Effect from "effect/Effect"; |
| 24 | import * as Exit from "effect/Exit"; |
| 25 | import * as Fiber from "effect/Fiber"; |
| 26 | import * as FileSystem from "effect/FileSystem"; |
| 27 | import * as Context from "effect/Context"; |
| 28 | import * as Layer from "effect/Layer"; |
| 29 | import * as Scheduler from "effect/Scheduler"; |
| 30 | import * as Schema from "effect/Schema"; |
| 31 | import * as TestClock from "effect/testing/TestClock"; |
| 32 | import { HttpClient, HttpClientResponse } from "effect/http"; |
| 33 | |
| 34 | import * as ServerConfig from "../config.ts"; |
| 35 | import * as ServerSettings from "../serverSettings.ts"; |
| 36 | import * as CursorAccountReader from "@t3tools/provider-cursor/server/CursorAccountReader"; |
| 37 | import * as CursorKeychain from "@t3tools/provider-cursor/server/CursorKeychain"; |
| 38 | import * as CursorUsageAccounts from "@t3tools/provider-cursor/server/CursorUsageAccounts"; |
| 39 | import * as ServerSecretStore from "../auth/ServerSecretStore.ts"; |
| 40 | import * as BackgroundPolicy from "../background/BackgroundPolicy.ts"; |
| 41 | import * as AntigravityUsage from "../provider/Drivers/AntigravityUsage.ts"; |
| 42 | import * as ProviderHostLive from "../provider/ProviderHostLive.ts"; |
| 43 | import { totalTokens, type UsageRecord } from "@t3tools/provider-core/server/usage"; |
| 44 | import * as UsageService from "./UsageService.ts"; |
| 45 | |
| 46 | const encodeUnknownJson = Schema.encodeEffect(Schema.fromJsonString(Schema.Unknown)); |
| 47 | const encodeUnknownJsonString = Schema.encodeSync(Schema.fromJsonString(Schema.Unknown)); |
| 48 | const decodeUnknownJsonString = Schema.decodeSync(Schema.fromJsonString(Schema.Unknown)); |
| 49 | |
| 50 | function 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 | |
| 64 | const WINDOW: UsageSummaryInput = { |
| 65 | timeZone: "UTC", |
| 66 | sinceDay: UsageDay.make("2026-07-31"), |
| 67 | untilDay: UsageDay.make("2026-08-02"), |
| 68 | }; |
| 69 | |
| 70 | const 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 | |
| 97 | const 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 | */ |
| 105 | const 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 | |
| 118 | const 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. */ |
| 169 | const BEFORE_NARROW_WINDOW = Date.parse("2026-08-01T10:00:00Z") / 1000; |
| 170 | const 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 | */ |
| 181 | const 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 | */ |
| 193 | const 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. */ |
| 197 | const 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 | |
| 203 | function 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. */ |
| 208 | const CURSOR_NOW = Date.parse("2026-08-02T12:00:00Z"); |
| 209 | const HOUR_MS = 60 * 60 * 1000; |
| 210 | /** The service's cache retention, which the Cursor account cache always covers. */ |
| 211 | const 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 | */ |
| 218 | function 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 | |
| 261 | const 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 | |
| 268 | function cursorSource(summary: { readonly sources: readonly UsageSource[] }) { |
| 269 | return summary.sources.find((source) => source.fingerprint.provider === "cursor"); |
| 270 | } |
| 271 | |
| 272 | describe("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 | }); |