| author | |
| committer | |
| log | d4d5d12e8ba086cfbf79ca3adeb4156b46ead665 |
| tree | 93c52ee87ffd140b5e46e236079f6d6709358018 |
| parent | 1ab2dfb5a7bd2996f79407b5d02cae6132a7626c |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: shivam <91240327+shivamhwp@users.noreply.github.com>2 files changed, 28 insertions(+), 4 deletions(-)
packages/shared/src/usageMerge.test.ts+20| ... | ... | @@ -146,6 +146,26 @@ describe("mergeUsage", () => { |
| 146 | 146 | ).toEqual({ claude: 1, codex: 1 }); |
| 147 | 147 | }); |
| 148 | 148 | |
| 149 | it("uses the newest scan when environments share the same transcript directory", () => { | |
| 150 | const source = { provider: "claude" as const, hostId: "mac", homePath: "/home/theo/.claude" }; | |
| 151 | const environments = [ | |
| 152 | environment("env-a", summary([bucket({ costUsd: 4, records: 2 })], [source])), | |
| 153 | environment("env-b", { | |
| 154 | ...summary([bucket()], [source]), | |
| 155 | readAt: "2026-08-07T01:00:00.000Z", | |
| 156 | }), | |
| 157 | ]; | |
| 158 | ||
| 159 | for (const ordered of [environments, environments.toReversed()]) { | |
| 160 | const merged = mergeUsage(ordered, USAGE_CONTRACT_VERSION); | |
| 161 | expect(merged.costUsd).toBe(10); | |
| 162 | expect(merged.records).toBe(5); | |
| 163 | expect(merged.sessions).toBe(1); | |
| 164 | expect(merged.contributingEnvironments).toEqual(["env-b"]); | |
| 165 | expect(merged.duplicateSources).toEqual(["env-a: /home/theo/.claude"]); | |
| 166 | } | |
| 167 | }); | |
| 168 | ||
| 149 | 169 | it("excludes an environment reporting an older contract version", () => { |
| 150 | 170 | const merged = mergeUsage( |
| 151 | 171 | [ |
packages/shared/src/usageMerge.ts+8-4| ... | ... | @@ -118,9 +118,9 @@ function fingerprintKey(fingerprint: UsageSourceFingerprint): string { |
| 118 | 118 | * |
| 119 | 119 | * Several environments on one machine (worktree servers, for instance) resolve |
| 120 | 120 | * the same provider home and would otherwise double count every token. The |
| 121 | * first environment in a stable order claims a fingerprint; the rest have that | |
| 122 | * provider's buckets dropped. Environments are sorted by id so the winner does | |
| 123 | * not change between renders. | |
| 121 | * most recently read summary claims a fingerprint; the rest have that provider's | |
| 122 | * buckets dropped. Environment ids break ties so the winner is stable when | |
| 123 | * summaries have the same read time. | |
| 124 | 124 | */ |
| 125 | 125 | function claimSources(environments: readonly EnvironmentUsage[]): { |
| 126 | 126 | readonly ownerByFingerprint: ReadonlyMap<string, EnvironmentId>; |
| ... | ... | @@ -129,7 +129,11 @@ function claimSources(environments: readonly EnvironmentUsage[]): { |
| 129 | 129 | const ownerByFingerprint = new Map<string, EnvironmentId>(); |
| 130 | 130 | const duplicates: string[] = []; |
| 131 | 131 | |
| 132 | const ordered = [...environments].sort((a, b) => a.environmentId.localeCompare(b.environmentId)); | |
| 132 | const ordered = [...environments].sort( | |
| 133 | (a, b) => | |
| 134 | (Date.parse(b.summary.readAt) || 0) - (Date.parse(a.summary.readAt) || 0) || | |
| 135 | a.environmentId.localeCompare(b.environmentId), | |
| 136 | ); | |
| 133 | 137 | |
| 134 | 138 | for (const environment of ordered) { |
| 135 | 139 | for (const source of environment.summary.sources) { |