| ... | @@ -5,12 +5,16 @@ import { | ... | @@ -5,12 +5,16 @@ import { |
| 5 | type UsageBucket, | 5 | type UsageBucket, |
| 6 | type UsageDay, | 6 | type UsageDay, |
| 7 | type UsageProviderKind, | 7 | type UsageProviderKind, |
| 8 | type UsageSummary, | 8 | UsageSummary, |
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import { describe, expect, it } from "vite-plus/test"; | 10 | import { describe, expect, it } from "vite-plus/test"; |
| | 11 | import * as Schema from "effect/Schema"; |
| 11 | | 12 | |
| 12 | import { isModelCostUnknown, mergeUsage, type EnvironmentUsage } from "./usageMerge.ts"; | 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 | function bucket(overrides: Partial<UsageBucket> = {}): UsageBucket { | 18 | function bucket(overrides: Partial<UsageBucket> = {}): UsageBucket { |
| 15 | return { | 19 | return { |
| 16 | day: "2026-08-07" as UsageDay, | 20 | day: "2026-08-07" as UsageDay, |
| ... | @@ -374,6 +378,79 @@ describe("mergeUsage", () => { | ... | @@ -374,6 +378,79 @@ describe("mergeUsage", () => { |
| 374 | expect(merged.staleEnvironments).toEqual([]); | 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 | it("derives provider shares and cost quality", () => { | 454 | it("derives provider shares and cost quality", () => { |
| 378 | const merged = mergeUsage( | 455 | const merged = mergeUsage( |
| 379 | [ | 456 | [ |