1import {
2 EnvironmentId,
3 UsageDay,
4 USAGE_CONTRACT_VERSION,
5 type UsageProviderKind,
6} from "@t3tools/contracts";
7import { act, useLayoutEffect } from "react";
8import { create, type ReactTestRenderer } from "react-test-renderer";
9import { afterEach, beforeEach, describe, expect, it, vi } from "vite-plus/test";
10
11import { useUsage, type EnvironmentUsageStatus, type UsageView } from "./usage";
12
13const testState = vi.hoisted(() => ({ environments: [] as EnvironmentUsageStatus[] }));
14vi.mock("@effect/atom-react", async (importOriginal) => ({
15 ...(await importOriginal<typeof import("@effect/atom-react")>()),
16 useAtomValue: () => testState.environments,
17}));
18
19const input = {
20 sinceDay: UsageDay.make("2026-09-04"),
21 untilDay: UsageDay.make("2026-09-04"),
22 timeZone: "UTC",
23};
24
25function environment(
26 id: string,
27 cost: number | null,
28 hostId = id,
29 provider: UsageProviderKind = "codex",
30): EnvironmentUsageStatus {
31 return {
32 environmentId: EnvironmentId.make(id),
33 label: id,
34 isPending: cost === null,
35 canReadDiagnostics: true,
36 isConnected: true,
37 error: null,
38 needsCursorKeychainAccess: false,
39 summary:
40 cost === null
41 ? null
42 : {
43 ...input,
44 contractVersion: USAGE_CONTRACT_VERSION,
45 readAt: "2026-09-04T12:00:00Z",
46 buckets: [
47 {
48 day: input.sinceDay,
49 provider,
50 model: id,
51 totals: {
52 uncachedInputTokens: 100,
53 cachedInputTokens: 0,
54 cacheCreationTokens: 0,
55 outputTokens: 50,
56 reasoningTokens: 0,
57 },
58 costUsd: cost,
59 cacheSavingsUsd: 0,
60 costSource: "modelPriced",
61 records: 1,
62 unpricedRecords: 0,
63 sessions: 1,
64 },
65 ],
66 sources: [
67 {
68 fingerprint: {
69 hostId,
70 provider,
71 resolvedHomePath: "/sessions",
72 volumeId: hostId,
73 },
74 status: "ok",
75 scannedFiles: 1,
76 skippedFiles: 0,
77 malformedRecords: 0,
78 distinctSessions: 1,
79 message: null,
80 },
81 ],
82 pricing: { status: "fresh", source: "test", fetchedAt: null, knownModels: 1 },
83 scanDurationMs: 1,
84 },
85 };
86}
87
88let renderer: ReactTestRenderer | undefined;
89let latest: UsageView;
90
91function Probe({
92 selected,
93 hidden,
94 window = input,
95}: {
96 selected: ReadonlySet<EnvironmentId> | null;
97 hidden?: ReadonlySet<UsageProviderKind>;
98 window?: typeof input;
99}) {
100 const usage = useUsage(window, selected, hidden);
101 useLayoutEffect(() => {
102 latest = usage;
103 }, [usage]);
104 return null;
105}
106
107async function select(...ids: string[]) {
108 await act(() => {
109 renderer?.update(<Probe selected={new Set(ids.map((id) => EnvironmentId.make(id)))} />);
110 });
111}
112
113beforeEach(async () => {
114 vi.stubGlobal("IS_REACT_ACT_ENVIRONMENT", true);
115 testState.environments = [environment("a", 10), environment("b", 20), environment("slow", null)];
116 await act(() => {
117 renderer = create(<Probe selected={null} />);
118 });
119});
120
121afterEach(async () => {
122 await act(() => renderer?.unmount());
123 vi.unstubAllGlobals();
124});
125
126describe("usage environment selection", () => {
127 it("starts with all environments and adds results as they arrive", async () => {
128 expect(latest.merged.costUsd).toBe(30);
129 expect(latest.isPending).toBe(false);
130 expect(latest.isPartial).toBe(true);
131
132 testState.environments = [...testState.environments.slice(0, 2), environment("slow", 40)];
133 await act(() => renderer?.update(<Probe selected={null} />));
134 expect(latest.merged.costUsd).toBe(70);
135 expect(latest.isPartial).toBe(false);
136 });
137
138 it("excludes unselected usage and pending environments, then restores all", async () => {
139 await select("b");
140 expect(latest.merged.costUsd).toBe(20);
141 expect(latest.merged.models.map((model) => model.model)).toEqual(["b"]);
142 expect(latest.isPending).toBe(false);
143 expect(latest.isPartial).toBe(false);
144 expect(latest.environments).toHaveLength(3);
145
146 await act(() => renderer?.update(<Probe selected={null} />));
147 expect(latest.merged.costUsd).toBe(30);
148 expect(latest.isPartial).toBe(true);
149 });
150
151 it("distinguishes a pending selection from an empty or failed selection", async () => {
152 await select("slow");
153 expect(latest.isPending).toBe(true);
154 expect(latest.merged.costUsd).toBe(0);
155
156 await select();
157 expect(latest.selectedEnvironments).toHaveLength(0);
158 expect(latest.isPending).toBe(false);
159 expect(latest.isPartial).toBe(false);
160
161 testState.environments = [{ ...environment("slow", null), isPending: false, error: "Offline" }];
162 await select("slow");
163 expect(latest.isPending).toBe(false);
164 expect(latest.isPartial).toBe(false);
165 });
166
167 it("deduplicates within the selection so an excluded owner cannot hide usage", async () => {
168 testState.environments = [environment("a", 10, "shared"), environment("b", 20, "shared")];
169 await act(() => renderer?.update(<Probe selected={null} />));
170 expect(latest.merged.costUsd).toBe(10);
171
172 await select("b");
173 expect(latest.merged.costUsd).toBe(20);
174 expect(latest.merged.duplicateSources).toEqual([]);
175 });
176
177 it("keeps selected cached results visible during a refresh", async () => {
178 testState.environments = [
179 { ...environment("a", 10), isPending: true },
180 environment("slow", null),
181 ];
182 await select("a");
183 expect(latest.merged.costUsd).toBe(10);
184 expect(latest.isPending).toBe(false);
185 expect(latest.isPartial).toBe(false);
186 });
187
188 it("shows the last answered usage until the next window answers, for the same selection", async () => {
189 const selected = new Set([EnvironmentId.make("a")]);
190 await act(() => renderer?.update(<Probe selected={selected} />));
191 expect(latest.shown?.merged.costUsd).toBe(10);
192
193 // A new window that nothing has answered yet.
194 const nextWindow = { ...input, sinceDay: UsageDay.make("2026-08-28") };
195 testState.environments = [environment("a", null)];
196 await act(() => renderer?.update(<Probe selected={selected} window={nextWindow} />));
197 expect(latest.isPending).toBe(true);
198 expect(latest.shown?.window).toBe(input);
199 expect(latest.shown?.merged.costUsd).toBe(10);
200
201 // A window that fails everywhere keeps it too.
202 testState.environments = [{ ...environment("a", null), isPending: false, error: "Offline" }];
203 await act(() => renderer?.update(<Probe selected={selected} window={nextWindow} />));
204 expect(latest.isPending).toBe(false);
205 expect(latest.shown?.window).toBe(input);
206 expect(latest.shown?.merged.costUsd).toBe(10);
207
208 // Once the new window answers, it replaces the kept one.
209 testState.environments = [environment("a", 30)];
210 await act(() => renderer?.update(<Probe selected={selected} window={nextWindow} />));
211 expect(latest.shown?.window).toBe(nextWindow);
212 expect(latest.shown?.merged.costUsd).toBe(30);
213
214 // A different provider filter does not reuse usage merged with the old one.
215 testState.environments = [environment("a", null)];
216 await act(() =>
217 renderer?.update(<Probe selected={selected} hidden={new Set(["claude"])} window={input} />),
218 );
219 expect(latest.shown).toBeNull();
220
221 // Another selection has nothing of its own to show.
222 testState.environments = [environment("a", null)];
223 await select("a");
224 expect(latest.shown).toBeNull();
225 });
226});
227
228describe("usage provider filter", () => {
229 it("drops hidden providers from totals and sessions, then restores them", async () => {
230 testState.environments = [environment("a", 10), environment("c", 5, "c", "claude")];
231 await act(() => renderer?.update(<Probe selected={null} hidden={new Set(["codex"])} />));
232 expect(latest.merged.costUsd).toBe(5);
233 expect(latest.merged.sessions).toBe(1);
234 expect(latest.merged.providers.map((entry) => entry.provider)).toEqual(["claude"]);
235
236 await act(() => renderer?.update(<Probe selected={null} />));
237 expect(latest.merged.costUsd).toBe(15);
238 expect(latest.merged.sessions).toBe(2);
239 });
240});