| ... | ... | @@ -5,12 +5,16 @@ import { |
| 5 | 5 | type UsageBucket, |
| 6 | 6 | type UsageDay, |
| 7 | 7 | type UsageProviderKind, |
| 8 | | type UsageSummary, |
| 8 | UsageSummary, |
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | 10 | import { describe, expect, it } from "vite-plus/test"; |
| 11 | import * as Schema from "effect/Schema"; |
| 11 | 12 | |
| 12 | 13 | import { isModelCostUnknown, mergeUsage, type EnvironmentUsage } from "./usageMerge.ts"; |
| 13 | 14 | |
| 15 | const decodeSummary = Schema.decodeUnknownSync(UsageSummary); |
| 16 | const encodeSummary = Schema.encodeSync(UsageSummary); |
| 17 | |
| 14 | 18 | function bucket(overrides: Partial<UsageBucket> = {}): UsageBucket { |
| 15 | 19 | return { |
| 16 | 20 | day: "2026-08-07" as UsageDay, |
| ... | ... | @@ -374,6 +378,79 @@ describe("mergeUsage", () => { |
| 374 | 378 | expect(merged.staleEnvironments).toEqual([]); |
| 375 | 379 | }); |
| 376 | 380 | |
| 381 | it("keeps known usage when newer providers and bucket variants cannot be decoded", () => { |
| 382 | const known = summary([bucket()], [{ provider: "claude", hostId: "mac", homePath: "/a" }]); |
| 383 | const decoded = decodeSummary({ |
| 384 | ...known, |
| 385 | buckets: [ |
| 386 | ...known.buckets, |
| 387 | { ...bucket(), provider: "future-provider", costUsd: 100 }, |
| 388 | { ...bucket(), costSource: "future-pricing", costUsd: 200 }, |
| 389 | ], |
| 390 | sources: [ |
| 391 | ...known.sources, |
| 392 | { |
| 393 | ...known.sources[0], |
| 394 | fingerprint: { |
| 395 | ...known.sources[0]?.fingerprint, |
| 396 | provider: "future-provider", |
| 397 | }, |
| 398 | }, |
| 399 | ], |
| 400 | }); |
| 401 | expect(decoded).toEqual(known); |
| 402 | const merged = mergeUsage([environment("env-a", decoded)], USAGE_CONTRACT_VERSION); |
| 403 | expect(merged.costUsd).toBe(10); |
| 404 | expect(merged.totalTokens).toBe(1160); |
| 405 | expect(merged.staleEnvironments).toEqual([]); |
| 406 | }); |
| 407 | |
| 408 | it("normalizes model names during decoding before grouping usage", () => { |
| 409 | const decoded = decodeSummary( |
| 410 | summary( |
| 411 | [ |
| 412 | bucket({ provider: "codex", model: " gpt-5 " }), |
| 413 | bucket({ provider: "codex", model: "gpt-5" }), |
| 414 | ], |
| 415 | [{ provider: "codex", hostId: "mac", homePath: "/a" }], |
| 416 | ), |
| 417 | ); |
| 418 | |
| 419 | expect(decoded.buckets.map((entry) => entry.model)).toEqual(["gpt-5", "gpt-5"]); |
| 420 | const merged = mergeUsage([environment("env-a", decoded)], USAGE_CONTRACT_VERSION); |
| 421 | expect(merged.models).toHaveLength(1); |
| 422 | expect(merged.models[0]).toMatchObject({ model: "gpt-5", costUsd: 20, totalTokens: 2320 }); |
| 423 | }); |
| 424 | |
| 425 | it("keeps all supported providers when encoding a response", () => { |
| 426 | const current = summary( |
| 427 | [bucket(), bucket({ provider: "grok" })], |
| 428 | [ |
| 429 | { provider: "claude", hostId: "mac", homePath: "/a" }, |
| 430 | { provider: "grok", hostId: "mac", homePath: "/b" }, |
| 431 | ], |
| 432 | ); |
| 433 | expect(encodeSummary(current)).toEqual(current); |
| 434 | expect(decodeSummary(encodeSummary(current))).toEqual(current); |
| 435 | }); |
| 436 | |
| 437 | it("still rejects a malformed summary envelope", () => { |
| 438 | expect(() => decodeSummary({ ...summary([], []), buckets: null })).toThrow(); |
| 439 | }); |
| 440 | |
| 441 | it("excludes a future incompatible contract even when its buckets still decode", () => { |
| 442 | const decoded = decodeSummary( |
| 443 | summary( |
| 444 | [bucket()], |
| 445 | [{ provider: "claude", hostId: "mac", homePath: "/a" }], |
| 446 | USAGE_CONTRACT_VERSION + 1, |
| 447 | ), |
| 448 | ); |
| 449 | const merged = mergeUsage([environment("env-a", decoded)], USAGE_CONTRACT_VERSION); |
| 450 | expect(merged.costUsd).toBe(0); |
| 451 | expect(merged.staleEnvironments).toEqual(["env-a"]); |
| 452 | }); |
| 453 | |
| 377 | 454 | it("derives provider shares and cost quality", () => { |
| 378 | 455 | const merged = mergeUsage( |
| 379 | 456 | [ |