1import { ProviderDriverKind, ProviderInstanceId, type ServerProvider } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3import { deriveProviderInstanceEntries } from "../../providerInstances";
4import {
5 formatContextWindowCompactionMessage,
6 hasAvailableCompactionProvider,
7 hasDismissedResumeCompaction,
8 formatContextWindowCost,
9 resolveContextWindowModelDisplayName,
10 shouldOfferResumeCompaction,
11 shouldReserveContextWindowMeter,
12} from "./ContextWindowMeter.logic";
13
14function claudeProvider(input: {
15 instanceId: string;
16 continuationGroupKey: string;
17 enabled?: boolean;
18}): ServerProvider {
19 return {
20 instanceId: ProviderInstanceId.make(input.instanceId),
21 driver: ProviderDriverKind.make("claudeAgent"),
22 continuation: { groupKey: input.continuationGroupKey },
23 enabled: input.enabled ?? true,
24 installed: true,
25 version: null,
26 status: "ready",
27 auth: { status: "authenticated" },
28 checkedAt: "2026-08-24T12:00:00.000Z",
29 models: [],
30 slashCommands: [{ name: "compact", description: "" }],
31 skills: [],
32 };
33}
34
35describe("hasAvailableCompactionProvider", () => {
36 const originalInstanceId = ProviderInstanceId.make("claude_original");
37
38 it("rejects a fallback in a different locked continuation group", () => {
39 const providers = deriveProviderInstanceEntries([
40 claudeProvider({
41 instanceId: originalInstanceId,
42 continuationGroupKey: "claude:home:/original",
43 enabled: false,
44 }),
45 claudeProvider({
46 instanceId: "claude_other",
47 continuationGroupKey: "claude:home:/other",
48 }),
49 ]);
50
51 expect(
52 hasAvailableCompactionProvider({
53 providers,
54 driverKind: ProviderDriverKind.make("claudeAgent"),
55 instanceId: originalInstanceId,
56 lockedInstanceId: originalInstanceId,
57 }),
58 ).toBe(false);
59 });
60
61 it("accepts an enabled fallback in the locked continuation group", () => {
62 const providers = deriveProviderInstanceEntries([
63 claudeProvider({
64 instanceId: originalInstanceId,
65 continuationGroupKey: "claude:home:/original",
66 enabled: false,
67 }),
68 claudeProvider({
69 instanceId: "claude_fallback",
70 continuationGroupKey: "claude:home:/original",
71 }),
72 ]);
73
74 expect(
75 hasAvailableCompactionProvider({
76 providers,
77 driverKind: ProviderDriverKind.make("claudeAgent"),
78 instanceId: originalInstanceId,
79 lockedInstanceId: originalInstanceId,
80 }),
81 ).toBe(true);
82 });
83});
84
85describe("resolveContextWindowModelDisplayName", () => {
86 it("uses the selected model from the exact provider instance", () => {
87 const primaryInstanceId = ProviderInstanceId.make("codex");
88 const selectedInstanceId = ProviderInstanceId.make("codex-work");
89 const modelOptionsByInstance = new Map([
90 [
91 primaryInstanceId,
92 [{ slug: "gpt-5.6-sol", name: "Primary profile model", shortName: "Primary" }],
93 ],
94 [selectedInstanceId, [{ slug: "gpt-5.6-sol", name: "GPT-5.6 Sol", shortName: "5.6 Sol" }]],
95 ]);
96
97 expect(
98 resolveContextWindowModelDisplayName(
99 {
100 instanceId: selectedInstanceId,
101 model: "gpt-5.6-sol",
102 },
103 modelOptionsByInstance,
104 ),
105 ).toBe("5.6 Sol");
106 });
107
108 it("falls back to the selected model slug when model metadata is unavailable", () => {
109 const selectedInstanceId = ProviderInstanceId.make("codex-work");
110
111 expect(
112 resolveContextWindowModelDisplayName(
113 {
114 instanceId: selectedInstanceId,
115 model: "custom-model",
116 },
117 new Map(),
118 ),
119 ).toBe("custom-model");
120 });
121});
122
123describe("formatContextWindowCompactionMessage", () => {
124 it("describes compaction in terms of the selected model", () => {
125 expect(formatContextWindowCompactionMessage("GPT-5.6 Sol")).toBe(
126 "Context for GPT-5.6 Sol compacts automatically when needed.",
127 );
128 });
129
130 it("uses neutral copy when the model is unavailable", () => {
131 expect(formatContextWindowCompactionMessage(null)).toBe(
132 "Context compacts automatically when needed.",
133 );
134 });
135
136 it("shows the configured auto-compaction threshold", () => {
137 expect(formatContextWindowCompactionMessage("Claude Sonnet 5", 300_000)).toBe(
138 "Compacts automatically at 300,000 tokens.",
139 );
140 });
141});
142
143describe("shouldOfferResumeCompaction", () => {
144 const now = "2026-08-24T12:00:00.000Z";
145
146 it("matches Claude's old-session age and context thresholds", () => {
147 expect(
148 shouldOfferResumeCompaction({
149 provider: "claudeAgent",
150 usedTokens: 100_000,
151 updatedAt: "2026-08-24T10:50:00.000Z",
152 now,
153 }),
154 ).toBe(true);
155 });
156
157 it("does not prompt for recent or smaller sessions", () => {
158 expect(
159 shouldOfferResumeCompaction({
160 provider: "claudeAgent",
161 usedTokens: 99_999,
162 updatedAt: "2026-08-24T10:00:00.000Z",
163 now,
164 }),
165 ).toBe(false);
166 expect(
167 shouldOfferResumeCompaction({
168 provider: "claudeAgent",
169 usedTokens: 200_000,
170 updatedAt: "2026-08-24T10:51:00.000Z",
171 now,
172 }),
173 ).toBe(false);
174 });
175
176 it("uses the reported prompt cache TTL instead of the fallback", () => {
177 const offer = (updatedAt: string) =>
178 shouldOfferResumeCompaction({
179 provider: "claudeAgent",
180 usedTokens: 200_000,
181 updatedAt,
182 promptCacheTtlMs: 5 * 60_000,
183 now,
184 });
185 expect(offer("2026-08-24T11:55:00.000Z")).toBe(true);
186 expect(offer("2026-08-24T11:56:00.000Z")).toBe(false);
187 });
188
189 it("does not show Claude's resume prompt for another provider", () => {
190 expect(
191 shouldOfferResumeCompaction({
192 provider: "codex",
193 usedTokens: 300_000,
194 updatedAt: "2026-08-24T09:00:00.000Z",
195 now,
196 }),
197 ).toBe(false);
198 });
199});
200
201describe("hasDismissedResumeCompaction", () => {
202 it("recognizes the native resume dialog's permanent dismissal", () => {
203 expect(
204 hasDismissedResumeCompaction([
205 {
206 kind: "user-input.resolved",
207 payload: {
208 answers: {
209 "This session is 2h 0m old and uses 250,000 tokens. Compact it before continuing?":
210 "Don't ask again",
211 },
212 },
213 },
214 ]),
215 ).toBe(true);
216 });
217
218 it("ignores the same answer on an unrelated question", () => {
219 expect(
220 hasDismissedResumeCompaction([
221 {
222 kind: "user-input.resolved",
223 payload: { answers: { "Show this setup reminder?": "Don't ask again" } },
224 },
225 ]),
226 ).toBe(false);
227 });
228
229 it("ignores unrelated questions that end with Claude's compaction prompt", () => {
230 expect(
231 hasDismissedResumeCompaction([
232 {
233 kind: "user-input.resolved",
234 payload: {
235 answers: {
236 "The build cache is large. Compact it before continuing?": "Don't ask again",
237 },
238 },
239 },
240 ]),
241 ).toBe(false);
242 });
243
244 it("ignores pending questions and malformed resolved payloads", () => {
245 expect(
246 hasDismissedResumeCompaction([
247 { kind: "user-input.requested", payload: { answers: { question: "Don't ask again" } } },
248 { kind: "user-input.resolved", payload: null },
249 { kind: "user-input.resolved", payload: { answers: ["Don't ask again"] } },
250 ]),
251 ).toBe(false);
252 });
253});
254
255describe("shouldReserveContextWindowMeter", () => {
256 const loadingStartedThread = {
257 meterEnabled: true,
258 detailLoading: true,
259 threadStarted: true,
260 providerReportsContextWindow: true,
261 };
262
263 it("holds the meter's slot while a started thread's detail loads", () => {
264 expect(shouldReserveContextWindowMeter(loadingStartedThread)).toBe(true);
265 });
266
267 it("reserves nothing once the detail is in", () => {
268 expect(shouldReserveContextWindowMeter({ ...loadingStartedThread, detailLoading: false })).toBe(
269 false,
270 );
271 });
272
273 it("reserves nothing for a thread that never ran a turn", () => {
274 expect(shouldReserveContextWindowMeter({ ...loadingStartedThread, threadStarted: false })).toBe(
275 false,
276 );
277 });
278
279 it("reserves while the thread's provider is not in the catalog yet", () => {
280 expect(
281 shouldReserveContextWindowMeter({
282 ...loadingStartedThread,
283 providerReportsContextWindow: null,
284 }),
285 ).toBe(true);
286 });
287
288 it("reserves nothing for a provider that does not stream usage", () => {
289 expect(
290 shouldReserveContextWindowMeter({
291 ...loadingStartedThread,
292 providerReportsContextWindow: false,
293 }),
294 ).toBe(false);
295 });
296
297 it("reserves nothing while the meter is switched off", () => {
298 expect(shouldReserveContextWindowMeter({ ...loadingStartedThread, meterEnabled: false })).toBe(
299 false,
300 );
301 });
302});
303
304describe("formatContextWindowCost", () => {
305 it("keeps ordinary and sub-cent ACP costs readable", () => {
306 expect(formatContextWindowCost({ amount: 0.42, currency: "USD" })).toBe("USD 0.42");
307 expect(formatContextWindowCost({ amount: 0.0042, currency: "USD" })).toBe("USD 0.0042");
308 });
309});