openusage 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/bin/openusage +91 -0
  2. package/package.json +33 -0
  3. package/plugins/amp/icon.svg +6 -0
  4. package/plugins/amp/plugin.js +175 -0
  5. package/plugins/amp/plugin.json +20 -0
  6. package/plugins/amp/plugin.test.js +365 -0
  7. package/plugins/antigravity/icon.svg +3 -0
  8. package/plugins/antigravity/plugin.js +484 -0
  9. package/plugins/antigravity/plugin.json +17 -0
  10. package/plugins/antigravity/plugin.test.js +1356 -0
  11. package/plugins/claude/icon.svg +3 -0
  12. package/plugins/claude/plugin.js +565 -0
  13. package/plugins/claude/plugin.json +28 -0
  14. package/plugins/claude/plugin.test.js +1012 -0
  15. package/plugins/codex/icon.svg +3 -0
  16. package/plugins/codex/plugin.js +673 -0
  17. package/plugins/codex/plugin.json +30 -0
  18. package/plugins/codex/plugin.test.js +1071 -0
  19. package/plugins/copilot/icon.svg +3 -0
  20. package/plugins/copilot/plugin.js +264 -0
  21. package/plugins/copilot/plugin.json +20 -0
  22. package/plugins/copilot/plugin.test.js +529 -0
  23. package/plugins/cursor/icon.svg +3 -0
  24. package/plugins/cursor/plugin.js +526 -0
  25. package/plugins/cursor/plugin.json +24 -0
  26. package/plugins/cursor/plugin.test.js +1168 -0
  27. package/plugins/factory/icon.svg +1 -0
  28. package/plugins/factory/plugin.js +407 -0
  29. package/plugins/factory/plugin.json +19 -0
  30. package/plugins/factory/plugin.test.js +833 -0
  31. package/plugins/gemini/icon.svg +4 -0
  32. package/plugins/gemini/plugin.js +413 -0
  33. package/plugins/gemini/plugin.json +20 -0
  34. package/plugins/gemini/plugin.test.js +735 -0
  35. package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
  36. package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
  37. package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
  38. package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
  39. package/plugins/kimi/icon.svg +3 -0
  40. package/plugins/kimi/plugin.js +358 -0
  41. package/plugins/kimi/plugin.json +19 -0
  42. package/plugins/kimi/plugin.test.js +619 -0
  43. package/plugins/minimax/icon.svg +4 -0
  44. package/plugins/minimax/plugin.js +388 -0
  45. package/plugins/minimax/plugin.json +17 -0
  46. package/plugins/minimax/plugin.test.js +943 -0
  47. package/plugins/perplexity/icon.svg +1 -0
  48. package/plugins/perplexity/plugin.js +378 -0
  49. package/plugins/perplexity/plugin.json +15 -0
  50. package/plugins/perplexity/plugin.test.js +602 -0
  51. package/plugins/windsurf/icon.svg +3 -0
  52. package/plugins/windsurf/plugin.js +218 -0
  53. package/plugins/windsurf/plugin.json +16 -0
  54. package/plugins/windsurf/plugin.test.js +455 -0
  55. package/plugins/zai/icon.svg +5 -0
  56. package/plugins/zai/plugin.js +156 -0
  57. package/plugins/zai/plugin.json +18 -0
  58. package/plugins/zai/plugin.test.js +396 -0
@@ -0,0 +1,1356 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { makeCtx } from "../test-helpers.js"
3
+
4
+ const loadPlugin = async () => {
5
+ await import("./plugin.js")
6
+ return globalThis.__openusage_plugin
7
+ }
8
+
9
+ // --- Fixtures ---
10
+
11
+ function makeDiscovery(overrides) {
12
+ return Object.assign(
13
+ { pid: 12345, csrf: "test-csrf-token", ports: [42001, 42002], extensionPort: null },
14
+ overrides
15
+ )
16
+ }
17
+
18
+ function makeUserStatusResponse(overrides) {
19
+ var base = {
20
+ userStatus: {
21
+ planStatus: {
22
+ planInfo: {
23
+ planName: "Pro",
24
+ monthlyPromptCredits: 50000,
25
+ monthlyFlowCredits: 150000,
26
+ monthlyFlexCreditPurchaseAmount: 25000,
27
+ },
28
+ availablePromptCredits: 500,
29
+ availableFlowCredits: 100,
30
+ usedFlexCredits: 5000,
31
+ },
32
+ cascadeModelConfigData: {
33
+ clientModelConfigs: [
34
+ {
35
+ label: "Gemini 3.1 Pro (High)",
36
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M37" },
37
+ quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T09:10:56Z" },
38
+ },
39
+ {
40
+ label: "Gemini 3.1 Pro (Low)",
41
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M36" },
42
+ quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T09:10:56Z" },
43
+ },
44
+ {
45
+ label: "Gemini 3 Flash",
46
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M18" },
47
+ quotaInfo: { remainingFraction: 1.0, resetTime: "2026-02-08T09:10:56Z" },
48
+ },
49
+ {
50
+ label: "Claude Sonnet 4.6 (Thinking)",
51
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M35" },
52
+ quotaInfo: { resetTime: "2026-02-26T15:23:41Z" },
53
+ },
54
+ {
55
+ label: "Claude Opus 4.6 (Thinking)",
56
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M26" },
57
+ quotaInfo: { resetTime: "2026-02-26T15:23:41Z" },
58
+ },
59
+ {
60
+ label: "GPT-OSS 120B (Medium)",
61
+ modelOrAlias: { model: "MODEL_OPENAI_GPT_OSS_120B_MEDIUM" },
62
+ quotaInfo: { resetTime: "2026-02-26T15:23:41Z" },
63
+ },
64
+ ],
65
+ },
66
+ },
67
+ }
68
+ if (overrides) {
69
+ if (overrides.planName !== undefined) base.userStatus.planStatus.planInfo.planName = overrides.planName
70
+ if (overrides.configs !== undefined) base.userStatus.cascadeModelConfigData.clientModelConfigs = overrides.configs
71
+ if (overrides.planStatus !== undefined) base.userStatus.planStatus = overrides.planStatus
72
+ }
73
+ return base
74
+ }
75
+
76
+ function makeCloudCodeResponse(overrides) {
77
+ return Object.assign(
78
+ {
79
+ models: {
80
+ "gemini-3-pro": {
81
+ displayName: "Gemini 3 Pro",
82
+ model: "gemini-3-pro",
83
+ quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T10:00:00Z" },
84
+ },
85
+ "claude-sonnet-4.5": {
86
+ displayName: "Claude Sonnet 4.5",
87
+ model: "claude-sonnet-4.5",
88
+ quotaInfo: { remainingFraction: 0.6, resetTime: "2026-02-08T10:00:00Z" },
89
+ },
90
+ },
91
+ },
92
+ overrides
93
+ )
94
+ }
95
+
96
+ function makeAuthStatusJson(overrides) {
97
+ return JSON.stringify(
98
+ Object.assign({ apiKey: "test-api-key-123", email: "user@example.com", name: "Test User" }, overrides)
99
+ )
100
+ }
101
+
102
+ function setupLsMock(ctx, discovery, responseBody) {
103
+ ctx.host.ls.discover.mockReturnValue(discovery)
104
+ ctx.host.http.request.mockImplementation((opts) => {
105
+ if (String(opts.url).includes("GetUnleashData")) {
106
+ return { status: 200, bodyText: "{}" }
107
+ }
108
+ return { status: 200, bodyText: JSON.stringify(responseBody) }
109
+ })
110
+ }
111
+
112
+ function setupSqliteMock(ctx, authJson, protoBase64) {
113
+ ctx.host.sqlite.query.mockImplementation((db, sql) => {
114
+ if (sql.includes("agentManagerInitState") && protoBase64) {
115
+ return JSON.stringify([{ value: protoBase64 }])
116
+ }
117
+ if (sql.includes("antigravityAuthStatus") && authJson) {
118
+ return JSON.stringify([{ value: authJson }])
119
+ }
120
+ return "[]"
121
+ })
122
+ }
123
+
124
+ function makeProtobufBase64(ctx, accessToken, refreshToken, expirySeconds) {
125
+ function encodeVarint(n) {
126
+ var bytes = ""
127
+ while (n > 0x7f) {
128
+ bytes += String.fromCharCode((n & 0x7f) | 0x80)
129
+ n = Math.floor(n / 128)
130
+ }
131
+ bytes += String.fromCharCode(n & 0x7f)
132
+ return bytes
133
+ }
134
+ function encodeField(fieldNum, wireType, data) {
135
+ var tag = encodeVarint(fieldNum * 8 + wireType)
136
+ if (wireType === 2) return tag + encodeVarint(data.length) + data
137
+ if (wireType === 0) return tag + encodeVarint(data)
138
+ return ""
139
+ }
140
+ var inner = ""
141
+ if (accessToken) inner += encodeField(1, 2, accessToken)
142
+ if (refreshToken) inner += encodeField(3, 2, refreshToken)
143
+ if (expirySeconds !== null && expirySeconds !== undefined) {
144
+ var tsMsg = encodeField(1, 0, expirySeconds)
145
+ inner += encodeField(4, 2, tsMsg)
146
+ }
147
+ var outer = encodeField(6, 2, inner)
148
+ return ctx.base64.encode(outer)
149
+ }
150
+
151
+ // --- Tests ---
152
+
153
+ describe("antigravity plugin", () => {
154
+ beforeEach(() => {
155
+ delete globalThis.__openusage_plugin
156
+ vi.resetModules()
157
+ })
158
+
159
+ it("throws when LS not found and no DB credentials", async () => {
160
+ const ctx = makeCtx()
161
+ ctx.host.ls.discover.mockReturnValue(null)
162
+ const plugin = await loadPlugin()
163
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
164
+ })
165
+
166
+ it("throws when no working port found and no DB credentials", async () => {
167
+ const ctx = makeCtx()
168
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery())
169
+ ctx.host.http.request.mockImplementation(() => {
170
+ throw new Error("connection refused")
171
+ })
172
+ const plugin = await loadPlugin()
173
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
174
+ })
175
+
176
+ it("throws when both GetUserStatus and GetCommandModelConfigs fail", async () => {
177
+ const ctx = makeCtx()
178
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery())
179
+ ctx.host.http.request.mockImplementation((opts) => {
180
+ if (String(opts.url).includes("GetUnleashData")) {
181
+ return { status: 200, bodyText: "{}" }
182
+ }
183
+ return { status: 500, bodyText: "" }
184
+ })
185
+ const plugin = await loadPlugin()
186
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
187
+ })
188
+
189
+ it("returns models + plan from GetUserStatus", async () => {
190
+ const ctx = makeCtx()
191
+ const discovery = makeDiscovery()
192
+ const response = makeUserStatusResponse()
193
+ setupLsMock(ctx, discovery, response)
194
+
195
+ const plugin = await loadPlugin()
196
+ const result = plugin.probe(ctx)
197
+
198
+ expect(result.plan).toBe("Pro")
199
+
200
+ // Model lines exist — 3 pool lines
201
+ const labels = result.lines.map((l) => l.label)
202
+ expect(labels).toEqual(["Gemini Pro", "Gemini Flash", "Claude"])
203
+ })
204
+
205
+ it("deduplicates models by normalized label (keeps worst-case fraction)", async () => {
206
+ const ctx = makeCtx()
207
+ const discovery = makeDiscovery()
208
+ const response = makeUserStatusResponse()
209
+ setupLsMock(ctx, discovery, response)
210
+
211
+ const plugin = await loadPlugin()
212
+ const result = plugin.probe(ctx)
213
+
214
+ // Both Gemini 3.1 Pro variants have frac=0.8 → used = 20%
215
+ const pro = result.lines.find((l) => l.label === "Gemini Pro")
216
+ expect(pro).toBeTruthy()
217
+ expect(pro.used).toBe(20) // (1 - 0.8) * 100
218
+ })
219
+
220
+ it("orders: Gemini (Pro, Flash), Claude (Opus, Sonnet), then others", async () => {
221
+ const ctx = makeCtx()
222
+ const discovery = makeDiscovery()
223
+ const response = makeUserStatusResponse()
224
+ setupLsMock(ctx, discovery, response)
225
+
226
+ const plugin = await loadPlugin()
227
+ const result = plugin.probe(ctx)
228
+
229
+ const labels = result.lines.map((l) => l.label)
230
+
231
+ expect(labels).toEqual(["Gemini Pro", "Gemini Flash", "Claude"])
232
+ })
233
+
234
+ it("falls back to GetCommandModelConfigs when GetUserStatus fails", async () => {
235
+ const ctx = makeCtx()
236
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery())
237
+ ctx.host.http.request.mockImplementation((opts) => {
238
+ if (String(opts.url).includes("GetUnleashData")) {
239
+ return { status: 200, bodyText: "{}" }
240
+ }
241
+ if (String(opts.url).includes("GetUserStatus")) {
242
+ return { status: 500, bodyText: "" }
243
+ }
244
+ if (String(opts.url).includes("GetCommandModelConfigs")) {
245
+ return {
246
+ status: 200,
247
+ bodyText: JSON.stringify({
248
+ clientModelConfigs: [
249
+ {
250
+ label: "Gemini 3 Pro (High)",
251
+ modelOrAlias: { model: "M7" },
252
+ quotaInfo: { remainingFraction: 0.6, resetTime: "2026-02-08T09:10:56Z" },
253
+ },
254
+ ],
255
+ }),
256
+ }
257
+ }
258
+ return { status: 500, bodyText: "" }
259
+ })
260
+
261
+ const plugin = await loadPlugin()
262
+ const result = plugin.probe(ctx)
263
+
264
+ expect(result.plan).toBeNull()
265
+
266
+ // Model lines present
267
+ const pro = result.lines.find((l) => l.label === "Gemini Pro")
268
+ expect(pro).toBeTruthy()
269
+ expect(pro.used).toBe(40) // (1 - 0.6) * 100
270
+ })
271
+
272
+ it("uses extension port as fallback when all ports fail probing", async () => {
273
+ const ctx = makeCtx()
274
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery({ ports: [99999], extensionPort: 42010 }))
275
+
276
+ let usedPort = null
277
+ ctx.host.http.request.mockImplementation((opts) => {
278
+ const url = String(opts.url)
279
+ if (url.includes("GetUnleashData") && url.includes("99999")) {
280
+ throw new Error("refused")
281
+ }
282
+ if (url.includes("GetUserStatus")) {
283
+ usedPort = parseInt(url.match(/:(\d+)\//)[1])
284
+ return {
285
+ status: 200,
286
+ bodyText: JSON.stringify(makeUserStatusResponse()),
287
+ }
288
+ }
289
+ return { status: 200, bodyText: "{}" }
290
+ })
291
+
292
+ const plugin = await loadPlugin()
293
+ const result = plugin.probe(ctx)
294
+ expect(usedPort).toBe(42010)
295
+ expect(result.lines.length).toBeGreaterThan(0)
296
+ })
297
+
298
+ it("treats models with no quotaInfo as depleted (100% used)", async () => {
299
+ const ctx = makeCtx()
300
+ const discovery = makeDiscovery()
301
+ const response = makeUserStatusResponse({
302
+ configs: [
303
+ { label: "Gemini 3 Pro (High)", modelOrAlias: { model: "M7" }, quotaInfo: { remainingFraction: 0.5, resetTime: "2026-02-08T09:10:56Z" } },
304
+ { label: "Claude Opus 4.6 (Thinking)", modelOrAlias: { model: "M26" } },
305
+ ],
306
+ })
307
+ setupLsMock(ctx, discovery, response)
308
+
309
+ const plugin = await loadPlugin()
310
+ const result = plugin.probe(ctx)
311
+ const claude = result.lines.find((l) => l.label === "Claude")
312
+ expect(claude).toBeTruthy()
313
+ expect(claude.used).toBe(100)
314
+ expect(claude.limit).toBe(100)
315
+ expect(claude.resetsAt).toBeUndefined()
316
+ expect(result.lines.find((l) => l.label === "Gemini Pro")).toBeTruthy()
317
+ })
318
+
319
+ it("dedup picks depleted variant (no quotaInfo) over non-depleted sibling", async () => {
320
+ const ctx = makeCtx()
321
+ const discovery = makeDiscovery()
322
+ const response = makeUserStatusResponse({
323
+ configs: [
324
+ { label: "Gemini 3 Pro (High)", modelOrAlias: { model: "M7" }, quotaInfo: { remainingFraction: 0.75, resetTime: "2026-02-08T09:10:56Z" } },
325
+ { label: "Gemini 3 Pro (Low)", modelOrAlias: { model: "M8" } },
326
+ ],
327
+ })
328
+ setupLsMock(ctx, discovery, response)
329
+
330
+ const plugin = await loadPlugin()
331
+ const result = plugin.probe(ctx)
332
+ const pro = result.lines.find((l) => l.label === "Gemini Pro")
333
+ expect(pro).toBeTruthy()
334
+ expect(pro.used).toBe(100)
335
+ expect(pro.resetsAt).toBeUndefined()
336
+ })
337
+
338
+ it("returns lines when all models are depleted (no quotaInfo)", async () => {
339
+ const ctx = makeCtx()
340
+ const discovery = makeDiscovery()
341
+ const response = makeUserStatusResponse({
342
+ configs: [
343
+ { label: "Gemini 3 Pro (High)", modelOrAlias: { model: "M7" } },
344
+ { label: "Claude Opus 4.6 (Thinking)", modelOrAlias: { model: "M26" } },
345
+ ],
346
+ })
347
+ setupLsMock(ctx, discovery, response)
348
+
349
+ const plugin = await loadPlugin()
350
+ const result = plugin.probe(ctx)
351
+ expect(result).toBeTruthy()
352
+ const labels = result.lines.map((l) => l.label)
353
+ expect(labels).toEqual(["Gemini Pro", "Claude"])
354
+ expect(result.lines.every((l) => l.used === 100)).toBe(true)
355
+ })
356
+
357
+ it("skips configs with missing or empty labels", async () => {
358
+ const ctx = makeCtx()
359
+ const discovery = makeDiscovery()
360
+ const response = makeUserStatusResponse({
361
+ configs: [
362
+ { label: "Gemini 3 Pro (High)", modelOrAlias: { model: "M7" }, quotaInfo: { remainingFraction: 0.5, resetTime: "2026-02-08T09:10:56Z" } },
363
+ { label: "", modelOrAlias: { model: "M99" }, quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T09:10:56Z" } },
364
+ { modelOrAlias: { model: "M100" }, quotaInfo: { remainingFraction: 0.9, resetTime: "2026-02-08T09:10:56Z" } },
365
+ ],
366
+ })
367
+ setupLsMock(ctx, discovery, response)
368
+
369
+ const plugin = await loadPlugin()
370
+ const result = plugin.probe(ctx)
371
+ expect(result.lines.length).toBe(1)
372
+ expect(result.lines[0].label).toBe("Gemini Pro")
373
+ })
374
+
375
+ it("includes resetsAt on model lines", async () => {
376
+ const ctx = makeCtx()
377
+ const discovery = makeDiscovery()
378
+ const response = makeUserStatusResponse()
379
+ setupLsMock(ctx, discovery, response)
380
+
381
+ const plugin = await loadPlugin()
382
+ const result = plugin.probe(ctx)
383
+ const pro = result.lines.find((l) => l.label === "Gemini Pro")
384
+ expect(pro.resetsAt).toBe("2026-02-08T09:10:56Z")
385
+ })
386
+
387
+ it("clamps remainingFraction outside 0-1 range", async () => {
388
+ const ctx = makeCtx()
389
+ const discovery = makeDiscovery()
390
+ const response = makeUserStatusResponse({
391
+ configs: [
392
+ { label: "Gemini Pro (Over)", modelOrAlias: { model: "M1" }, quotaInfo: { remainingFraction: 1.5, resetTime: "2026-02-08T09:10:56Z" } },
393
+ { label: "Gemini Flash (Neg)", modelOrAlias: { model: "M2" }, quotaInfo: { remainingFraction: -0.3, resetTime: "2026-02-08T09:10:56Z" } },
394
+ ],
395
+ })
396
+ setupLsMock(ctx, discovery, response)
397
+
398
+ const plugin = await loadPlugin()
399
+ const result = plugin.probe(ctx)
400
+ const over = result.lines.find((l) => l.label === "Gemini Pro")
401
+ const neg = result.lines.find((l) => l.label === "Gemini Flash")
402
+ expect(over.used).toBe(0) // clamped to 1.0 → 0% used
403
+ expect(neg.used).toBe(100) // clamped to 0.0 → 100% used
404
+ })
405
+
406
+ it("handles missing resetTime gracefully", async () => {
407
+ const ctx = makeCtx()
408
+ const discovery = makeDiscovery()
409
+ const response = makeUserStatusResponse({
410
+ configs: [
411
+ { label: "Gemini Pro (No Reset)", modelOrAlias: { model: "M1" }, quotaInfo: { remainingFraction: 0.5 } },
412
+ ],
413
+ })
414
+ setupLsMock(ctx, discovery, response)
415
+
416
+ const plugin = await loadPlugin()
417
+ const result = plugin.probe(ctx)
418
+ const line = result.lines.find((l) => l.label === "Gemini Pro")
419
+ expect(line).toBeTruthy()
420
+ expect(line.used).toBe(50)
421
+ expect(line.resetsAt).toBeUndefined()
422
+ })
423
+
424
+ it("probes ports with HTTPS first, then HTTP, picks first success", async () => {
425
+ const ctx = makeCtx()
426
+ ctx.host.ls.discover.mockReturnValue(makeDiscovery({ ports: [10001, 10002] }))
427
+
428
+ const probed = []
429
+ ctx.host.http.request.mockImplementation((opts) => {
430
+ const url = String(opts.url)
431
+ if (url.includes("GetUnleashData")) {
432
+ const port = parseInt(url.match(/:(\d+)\//)[1])
433
+ const scheme = url.startsWith("https") ? "https" : "http"
434
+ probed.push({ port, scheme })
435
+ // Port 10001 refuses both, port 10002 accepts HTTPS
436
+ if (port === 10002 && scheme === "https") return { status: 200, bodyText: "{}" }
437
+ throw new Error("refused")
438
+ }
439
+ return { status: 200, bodyText: JSON.stringify(makeUserStatusResponse()) }
440
+ })
441
+
442
+ const plugin = await loadPlugin()
443
+ plugin.probe(ctx)
444
+ // Should try HTTPS then HTTP on 10001 (both fail), then HTTPS on 10002 (success)
445
+ expect(probed).toEqual([
446
+ { port: 10001, scheme: "https" },
447
+ { port: 10001, scheme: "http" },
448
+ { port: 10002, scheme: "https" },
449
+ ])
450
+ })
451
+
452
+ it("includes apiKey in LS metadata when DB has credentials", async () => {
453
+ const ctx = makeCtx()
454
+ setupSqliteMock(ctx, makeAuthStatusJson())
455
+ const discovery = makeDiscovery()
456
+ ctx.host.ls.discover.mockReturnValue(discovery)
457
+
458
+ let capturedMetadata = null
459
+ ctx.host.http.request.mockImplementation((opts) => {
460
+ const url = String(opts.url)
461
+ if (url.includes("GetUnleashData")) {
462
+ return { status: 200, bodyText: "{}" }
463
+ }
464
+ if (url.includes("GetUserStatus")) {
465
+ const body = JSON.parse(opts.bodyText)
466
+ capturedMetadata = body.metadata
467
+ return { status: 200, bodyText: JSON.stringify(makeUserStatusResponse()) }
468
+ }
469
+ return { status: 200, bodyText: "{}" }
470
+ })
471
+
472
+ const plugin = await loadPlugin()
473
+ plugin.probe(ctx)
474
+
475
+ expect(capturedMetadata).toBeTruthy()
476
+ expect(capturedMetadata.apiKey).toBe("test-api-key-123")
477
+ expect(capturedMetadata.ideName).toBe("antigravity")
478
+ })
479
+
480
+ it("works without apiKey when SQLite returns empty", async () => {
481
+ const ctx = makeCtx()
482
+ const discovery = makeDiscovery()
483
+ const response = makeUserStatusResponse()
484
+ setupLsMock(ctx, discovery, response)
485
+
486
+ let capturedMetadata = null
487
+ ctx.host.http.request.mockImplementation((opts) => {
488
+ const url = String(opts.url)
489
+ if (url.includes("GetUnleashData")) {
490
+ return { status: 200, bodyText: "{}" }
491
+ }
492
+ if (url.includes("GetUserStatus")) {
493
+ const body = JSON.parse(opts.bodyText)
494
+ capturedMetadata = body.metadata
495
+ return { status: 200, bodyText: JSON.stringify(response) }
496
+ }
497
+ return { status: 200, bodyText: "{}" }
498
+ })
499
+
500
+ const plugin = await loadPlugin()
501
+ const result = plugin.probe(ctx)
502
+
503
+ expect(result.lines.length).toBeGreaterThan(0)
504
+ expect(capturedMetadata).toBeTruthy()
505
+ expect(capturedMetadata.apiKey).toBeUndefined()
506
+ })
507
+
508
+ it("falls back to Cloud Code API when LS is not available", async () => {
509
+ const ctx = makeCtx()
510
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
511
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
512
+ ctx.host.ls.discover.mockReturnValue(null)
513
+
514
+ ctx.host.http.request.mockImplementation((opts) => {
515
+ const url = String(opts.url)
516
+ if (url.includes("fetchAvailableModels")) {
517
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
518
+ }
519
+ return { status: 500, bodyText: "" }
520
+ })
521
+
522
+ const plugin = await loadPlugin()
523
+ const result = plugin.probe(ctx)
524
+
525
+ expect(result.plan).toBeNull()
526
+ const labels = result.lines.map((l) => l.label)
527
+ expect(labels).toContain("Gemini Pro")
528
+ expect(labels).toContain("Claude")
529
+ })
530
+
531
+ it("Cloud Code sends correct Authorization header with proto token", async () => {
532
+ const ctx = makeCtx()
533
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
534
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.proto-token", "1//refresh", futureExpiry))
535
+ ctx.host.ls.discover.mockReturnValue(null)
536
+
537
+ let capturedHeaders = null
538
+ ctx.host.http.request.mockImplementation((opts) => {
539
+ const url = String(opts.url)
540
+ if (url.includes("fetchAvailableModels")) {
541
+ capturedHeaders = opts.headers
542
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
543
+ }
544
+ return { status: 500, bodyText: "" }
545
+ })
546
+
547
+ const plugin = await loadPlugin()
548
+ plugin.probe(ctx)
549
+
550
+ expect(capturedHeaders).toBeTruthy()
551
+ expect(capturedHeaders.Authorization).toBe("Bearer ya29.proto-token")
552
+ expect(capturedHeaders["User-Agent"]).toBe("antigravity")
553
+ })
554
+
555
+ it("Cloud Code returns null on 401/403 (invalid token, no refresh)", async () => {
556
+ const ctx = makeCtx()
557
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
558
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.bad-token", null, futureExpiry))
559
+ ctx.host.ls.discover.mockReturnValue(null)
560
+
561
+ ctx.host.http.request.mockImplementation((opts) => {
562
+ const url = String(opts.url)
563
+ if (url.includes("fetchAvailableModels")) {
564
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
565
+ }
566
+ return { status: 500, bodyText: "" }
567
+ })
568
+
569
+ const plugin = await loadPlugin()
570
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
571
+ })
572
+
573
+ it("Cloud Code tries multiple base URLs", async () => {
574
+ const ctx = makeCtx()
575
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
576
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
577
+ ctx.host.ls.discover.mockReturnValue(null)
578
+
579
+ const calledUrls = []
580
+ ctx.host.http.request.mockImplementation((opts) => {
581
+ const url = String(opts.url)
582
+ if (url.includes("fetchAvailableModels")) {
583
+ calledUrls.push(url)
584
+ if (url.includes("daily-cloudcode")) {
585
+ throw new Error("network error")
586
+ }
587
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
588
+ }
589
+ return { status: 500, bodyText: "" }
590
+ })
591
+
592
+ const plugin = await loadPlugin()
593
+ const result = plugin.probe(ctx)
594
+
595
+ expect(calledUrls.length).toBe(2)
596
+ expect(calledUrls[0]).toContain("daily-cloudcode-pa.googleapis.com")
597
+ expect(calledUrls[1]).toContain("cloudcode-pa.googleapis.com")
598
+ expect(result.lines.length).toBeGreaterThan(0)
599
+ })
600
+
601
+ it("Cloud Code correctly parses model quota response", async () => {
602
+ const ctx = makeCtx()
603
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
604
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
605
+ ctx.host.ls.discover.mockReturnValue(null)
606
+
607
+ ctx.host.http.request.mockImplementation((opts) => {
608
+ if (String(opts.url).includes("fetchAvailableModels")) {
609
+ return {
610
+ status: 200,
611
+ bodyText: JSON.stringify({
612
+ models: {
613
+ "gemini-3-pro-high": {
614
+ displayName: "Gemini 3 Pro (High)",
615
+ quotaInfo: { remainingFraction: 0.7, resetTime: "2026-02-08T12:00:00Z" },
616
+ },
617
+ "gemini-3-pro-low": {
618
+ displayName: "Gemini 3 Pro (Low)",
619
+ quotaInfo: { remainingFraction: 0.9, resetTime: "2026-02-08T12:00:00Z" },
620
+ },
621
+ },
622
+ }),
623
+ }
624
+ }
625
+ return { status: 500, bodyText: "" }
626
+ })
627
+
628
+ const plugin = await loadPlugin()
629
+ const result = plugin.probe(ctx)
630
+
631
+ const pro = result.lines.find((l) => l.label === "Gemini Pro")
632
+ expect(pro).toBeTruthy()
633
+ expect(pro.used).toBe(30)
634
+ })
635
+
636
+ it("skips Cloud Code when no credentials available", async () => {
637
+ const ctx = makeCtx()
638
+ ctx.host.ls.discover.mockReturnValue(null)
639
+
640
+ const plugin = await loadPlugin()
641
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
642
+ expect(ctx.host.http.request).not.toHaveBeenCalled()
643
+ })
644
+
645
+ it("LS takes priority over Cloud Code when both available", async () => {
646
+ const ctx = makeCtx()
647
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
648
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
649
+ const discovery = makeDiscovery()
650
+ const response = makeUserStatusResponse()
651
+ setupLsMock(ctx, discovery, response)
652
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
653
+
654
+ const plugin = await loadPlugin()
655
+ const result = plugin.probe(ctx)
656
+
657
+ expect(result.plan).toBe("Pro")
658
+ const calls = ctx.host.http.request.mock.calls.map((c) => String(c[0].url))
659
+ const ccCalls = calls.filter((u) => u.includes("fetchAvailableModels"))
660
+ expect(ccCalls.length).toBe(0)
661
+ })
662
+
663
+ it("Cloud Code treats models without quotaInfo as depleted (100% used)", async () => {
664
+ const ctx = makeCtx()
665
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
666
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
667
+ ctx.host.ls.discover.mockReturnValue(null)
668
+
669
+ ctx.host.http.request.mockImplementation((opts) => {
670
+ if (String(opts.url).includes("fetchAvailableModels")) {
671
+ return {
672
+ status: 200,
673
+ bodyText: JSON.stringify({
674
+ models: {
675
+ "valid-model": {
676
+ displayName: "Gemini 3 Pro",
677
+ quotaInfo: { remainingFraction: 0.5, resetTime: "2026-02-08T12:00:00Z" },
678
+ },
679
+ "no-quota": {
680
+ displayName: "Gemini Flash (No Quota)",
681
+ },
682
+ },
683
+ }),
684
+ }
685
+ }
686
+ return { status: 500, bodyText: "" }
687
+ })
688
+
689
+ const plugin = await loadPlugin()
690
+ const result = plugin.probe(ctx)
691
+
692
+ const noQuota = result.lines.find((l) => l.label === "Gemini Flash")
693
+ expect(noQuota).toBeTruthy()
694
+ expect(noQuota.used).toBe(100)
695
+ expect(noQuota.limit).toBe(100)
696
+ expect(noQuota.resetsAt).toBeUndefined()
697
+ expect(result.lines.find((l) => l.label === "Gemini Pro")).toBeTruthy()
698
+ })
699
+
700
+ it("decodes protobuf tokens from SQLite", async () => {
701
+ const ctx = makeCtx()
702
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
703
+ const protoB64 = makeProtobufBase64(ctx, "ya29.test-access", "1//refresh-token", futureExpiry)
704
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
705
+ ctx.host.ls.discover.mockReturnValue(null)
706
+
707
+ let capturedAuth = null
708
+ ctx.host.http.request.mockImplementation((opts) => {
709
+ const url = String(opts.url)
710
+ if (url.includes("fetchAvailableModels")) {
711
+ capturedAuth = opts.headers.Authorization
712
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
713
+ }
714
+ return { status: 500, bodyText: "" }
715
+ })
716
+
717
+ const plugin = await loadPlugin()
718
+ const result = plugin.probe(ctx)
719
+
720
+ expect(capturedAuth).toBe("Bearer ya29.test-access")
721
+ expect(result.lines.length).toBeGreaterThan(0)
722
+ })
723
+
724
+ it("handles missing protobuf data gracefully (falls back to apiKey)", async () => {
725
+ const ctx = makeCtx()
726
+ setupSqliteMock(ctx, makeAuthStatusJson())
727
+ ctx.host.ls.discover.mockReturnValue(null)
728
+
729
+ let capturedAuth = null
730
+ ctx.host.http.request.mockImplementation((opts) => {
731
+ if (String(opts.url).includes("fetchAvailableModels")) {
732
+ capturedAuth = opts.headers.Authorization
733
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
734
+ }
735
+ return { status: 500, bodyText: "" }
736
+ })
737
+
738
+ const plugin = await loadPlugin()
739
+ const result = plugin.probe(ctx)
740
+
741
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
742
+ expect(result.lines.length).toBeGreaterThan(0)
743
+ })
744
+
745
+ it("handles corrupt protobuf base64 gracefully (falls back to apiKey)", async () => {
746
+ const ctx = makeCtx()
747
+ setupSqliteMock(ctx, makeAuthStatusJson(), "not-valid-protobuf!!!")
748
+ ctx.host.ls.discover.mockReturnValue(null)
749
+
750
+ let capturedAuth = null
751
+ ctx.host.http.request.mockImplementation((opts) => {
752
+ if (String(opts.url).includes("fetchAvailableModels")) {
753
+ capturedAuth = opts.headers.Authorization
754
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
755
+ }
756
+ return { status: 500, bodyText: "" }
757
+ })
758
+
759
+ const plugin = await loadPlugin()
760
+ const result = plugin.probe(ctx)
761
+
762
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
763
+ expect(result.lines.length).toBeGreaterThan(0)
764
+ })
765
+
766
+ it("handles protobuf with no refresh_token or expiry", async () => {
767
+ const ctx = makeCtx()
768
+ const protoB64 = makeProtobufBase64(ctx, "ya29.access-only", null, null)
769
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
770
+ ctx.host.ls.discover.mockReturnValue(null)
771
+
772
+ let capturedAuth = null
773
+ ctx.host.http.request.mockImplementation((opts) => {
774
+ if (String(opts.url).includes("fetchAvailableModels")) {
775
+ capturedAuth = opts.headers.Authorization
776
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
777
+ }
778
+ return { status: 500, bodyText: "" }
779
+ })
780
+
781
+ const plugin = await loadPlugin()
782
+ plugin.probe(ctx)
783
+
784
+ expect(capturedAuth).toBe("Bearer ya29.access-only")
785
+ })
786
+
787
+ it("sends correct form-urlencoded POST to Google OAuth", async () => {
788
+ const ctx = makeCtx()
789
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
790
+ const protoB64 = makeProtobufBase64(ctx, "ya29.expired", "1//my-refresh", futureExpiry)
791
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
792
+ ctx.host.ls.discover.mockReturnValue(null)
793
+
794
+ let oauthBody = null
795
+ let oauthHeaders = null
796
+ ctx.host.http.request.mockImplementation((opts) => {
797
+ const url = String(opts.url)
798
+ if (url.includes("oauth2.googleapis.com")) {
799
+ oauthBody = opts.bodyText
800
+ oauthHeaders = opts.headers
801
+ return { status: 200, bodyText: JSON.stringify({ access_token: "ya29.refreshed-token" }) }
802
+ }
803
+ if (url.includes("fetchAvailableModels")) {
804
+ if (opts.headers.Authorization === "Bearer ya29.refreshed-token") {
805
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
806
+ }
807
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
808
+ }
809
+ return { status: 500, bodyText: "" }
810
+ })
811
+
812
+ const plugin = await loadPlugin()
813
+ plugin.probe(ctx)
814
+
815
+ expect(oauthHeaders["Content-Type"]).toBe("application/x-www-form-urlencoded")
816
+ expect(oauthBody).toContain("client_id=")
817
+ expect(oauthBody).toContain("client_secret=")
818
+ expect(oauthBody).toContain("refresh_token=" + encodeURIComponent("1//my-refresh"))
819
+ expect(oauthBody).toContain("grant_type=refresh_token")
820
+ })
821
+
822
+ it("throws when all tokens fail and refresh returns invalid_grant", async () => {
823
+ const ctx = makeCtx()
824
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
825
+ const protoB64 = makeProtobufBase64(ctx, "ya29.expired", "1//bad-refresh", futureExpiry)
826
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
827
+ ctx.host.ls.discover.mockReturnValue(null)
828
+
829
+ ctx.host.http.request.mockImplementation((opts) => {
830
+ const url = String(opts.url)
831
+ if (url.includes("oauth2.googleapis.com")) {
832
+ return { status: 400, bodyText: '{"error":"invalid_grant"}' }
833
+ }
834
+ if (url.includes("fetchAvailableModels")) {
835
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
836
+ }
837
+ return { status: 500, bodyText: "" }
838
+ })
839
+
840
+ const plugin = await loadPlugin()
841
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
842
+ })
843
+
844
+ it("tries proto token first, then apiKey on auth failure", async () => {
845
+ const ctx = makeCtx()
846
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
847
+ const protoB64 = makeProtobufBase64(ctx, "ya29.proto-first", "1//refresh", futureExpiry)
848
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
849
+ ctx.host.ls.discover.mockReturnValue(null)
850
+
851
+ const capturedTokens = []
852
+ ctx.host.http.request.mockImplementation((opts) => {
853
+ const url = String(opts.url)
854
+ if (url.includes("fetchAvailableModels")) {
855
+ capturedTokens.push(opts.headers.Authorization)
856
+ if (opts.headers.Authorization === "Bearer ya29.proto-first") {
857
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
858
+ }
859
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
860
+ }
861
+ return { status: 500, bodyText: "" }
862
+ })
863
+
864
+ const plugin = await loadPlugin()
865
+ const result = plugin.probe(ctx)
866
+
867
+ expect(capturedTokens[0]).toBe("Bearer ya29.proto-first")
868
+ expect(capturedTokens[capturedTokens.length - 1]).toBe("Bearer test-api-key-123")
869
+ expect(result.lines.length).toBeGreaterThan(0)
870
+ })
871
+
872
+ it("tries both tokens before refreshing", async () => {
873
+ const ctx = makeCtx()
874
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
875
+ const protoB64 = makeProtobufBase64(ctx, "ya29.both-fail", "1//refresh", futureExpiry)
876
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
877
+ ctx.host.ls.discover.mockReturnValue(null)
878
+
879
+ const capturedTokens = []
880
+ let refreshCalled = false
881
+ ctx.host.http.request.mockImplementation((opts) => {
882
+ const url = String(opts.url)
883
+ if (url.includes("oauth2.googleapis.com")) {
884
+ refreshCalled = true
885
+ return { status: 200, bodyText: JSON.stringify({ access_token: "ya29.refreshed" }) }
886
+ }
887
+ if (url.includes("fetchAvailableModels")) {
888
+ capturedTokens.push(opts.headers.Authorization)
889
+ if (opts.headers.Authorization === "Bearer ya29.refreshed") {
890
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
891
+ }
892
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
893
+ }
894
+ return { status: 500, bodyText: "" }
895
+ })
896
+
897
+ const plugin = await loadPlugin()
898
+ const result = plugin.probe(ctx)
899
+
900
+ expect(refreshCalled).toBe(true)
901
+ expect(capturedTokens.filter((t) => t === "Bearer ya29.both-fail").length).toBeGreaterThan(0)
902
+ expect(capturedTokens.filter((t) => t === "Bearer test-api-key-123").length).toBeGreaterThan(0)
903
+ expect(capturedTokens[capturedTokens.length - 1]).toBe("Bearer ya29.refreshed")
904
+ expect(result.lines.length).toBeGreaterThan(0)
905
+ })
906
+
907
+ it("deduplicates identical tokens", async () => {
908
+ const ctx = makeCtx()
909
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
910
+ const protoB64 = makeProtobufBase64(ctx, "ya29.same-token", "1//refresh", futureExpiry)
911
+ setupSqliteMock(ctx, makeAuthStatusJson({ apiKey: "ya29.same-token" }), protoB64)
912
+ ctx.host.ls.discover.mockReturnValue(null)
913
+
914
+ const capturedTokens = []
915
+ ctx.host.http.request.mockImplementation((opts) => {
916
+ const url = String(opts.url)
917
+ if (url.includes("oauth2.googleapis.com")) {
918
+ return { status: 200, bodyText: JSON.stringify({ access_token: "ya29.refreshed" }) }
919
+ }
920
+ if (url.includes("fetchAvailableModels")) {
921
+ capturedTokens.push(opts.headers.Authorization)
922
+ if (opts.headers.Authorization === "Bearer ya29.same-token") {
923
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
924
+ }
925
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
926
+ }
927
+ return { status: 500, bodyText: "" }
928
+ })
929
+
930
+ const plugin = await loadPlugin()
931
+ const result = plugin.probe(ctx)
932
+
933
+ const sameTokenCalls = capturedTokens.filter((t) => t === "Bearer ya29.same-token")
934
+ expect(sameTokenCalls.length).toBe(1)
935
+ expect(result.lines.length).toBeGreaterThan(0)
936
+ })
937
+
938
+ it("uses apiKey as only token when proto data unavailable", async () => {
939
+ const ctx = makeCtx()
940
+ setupSqliteMock(ctx, makeAuthStatusJson())
941
+ ctx.host.ls.discover.mockReturnValue(null)
942
+
943
+ let capturedAuth = null
944
+ ctx.host.http.request.mockImplementation((opts) => {
945
+ if (String(opts.url).includes("fetchAvailableModels")) {
946
+ capturedAuth = opts.headers.Authorization
947
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
948
+ }
949
+ return { status: 500, bodyText: "" }
950
+ })
951
+
952
+ const plugin = await loadPlugin()
953
+ plugin.probe(ctx)
954
+
955
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
956
+ })
957
+
958
+ it("caches refreshed token to pluginDataDir", async () => {
959
+ const ctx = makeCtx()
960
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
961
+ const protoB64 = makeProtobufBase64(ctx, "ya29.will-fail", "1//refresh", futureExpiry)
962
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
963
+ ctx.host.ls.discover.mockReturnValue(null)
964
+
965
+ ctx.host.http.request.mockImplementation((opts) => {
966
+ const url = String(opts.url)
967
+ if (url.includes("oauth2.googleapis.com")) {
968
+ return { status: 200, bodyText: JSON.stringify({ access_token: "ya29.refreshed", expires_in: 3599 }) }
969
+ }
970
+ if (url.includes("fetchAvailableModels")) {
971
+ if (opts.headers.Authorization === "Bearer ya29.refreshed") {
972
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
973
+ }
974
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
975
+ }
976
+ return { status: 500, bodyText: "" }
977
+ })
978
+
979
+ const plugin = await loadPlugin()
980
+ plugin.probe(ctx)
981
+
982
+ const cachePath = ctx.app.pluginDataDir + "/auth.json"
983
+ expect(ctx.host.fs.writeText).toHaveBeenCalledWith(cachePath, expect.any(String))
984
+ const cached = JSON.parse(ctx.host.fs.writeText.mock.calls.find((c) => c[0] === cachePath)[1])
985
+ expect(cached.accessToken).toBe("ya29.refreshed")
986
+ expect(cached.expiresAtMs).toBeGreaterThan(Date.now())
987
+ })
988
+
989
+ it("uses cached token before falling back to apiKey", async () => {
990
+ const ctx = makeCtx()
991
+ setupSqliteMock(ctx, makeAuthStatusJson())
992
+ ctx.host.ls.discover.mockReturnValue(null)
993
+
994
+ const cachePath = ctx.app.pluginDataDir + "/auth.json"
995
+ ctx.host.fs.writeText(cachePath, JSON.stringify({
996
+ accessToken: "ya29.cached-token",
997
+ expiresAtMs: Date.now() + 3600000,
998
+ }))
999
+
1000
+ const capturedTokens = []
1001
+ ctx.host.http.request.mockImplementation((opts) => {
1002
+ if (String(opts.url).includes("fetchAvailableModels")) {
1003
+ capturedTokens.push(opts.headers.Authorization)
1004
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
1005
+ }
1006
+ return { status: 500, bodyText: "" }
1007
+ })
1008
+
1009
+ const plugin = await loadPlugin()
1010
+ plugin.probe(ctx)
1011
+
1012
+ expect(capturedTokens[0]).toBe("Bearer ya29.cached-token")
1013
+ })
1014
+
1015
+ it("skips expired cached token", async () => {
1016
+ const ctx = makeCtx()
1017
+ setupSqliteMock(ctx, makeAuthStatusJson())
1018
+ ctx.host.ls.discover.mockReturnValue(null)
1019
+
1020
+ const cachePath = ctx.app.pluginDataDir + "/auth.json"
1021
+ ctx.host.fs.writeText(cachePath, JSON.stringify({
1022
+ accessToken: "ya29.expired-cache",
1023
+ expiresAtMs: Date.now() - 1000,
1024
+ }))
1025
+
1026
+ let capturedAuth = null
1027
+ ctx.host.http.request.mockImplementation((opts) => {
1028
+ if (String(opts.url).includes("fetchAvailableModels")) {
1029
+ capturedAuth = opts.headers.Authorization
1030
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
1031
+ }
1032
+ return { status: 500, bodyText: "" }
1033
+ })
1034
+
1035
+ const plugin = await loadPlugin()
1036
+ plugin.probe(ctx)
1037
+
1038
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
1039
+ })
1040
+
1041
+ it("skips expired proto token and falls back to next token", async () => {
1042
+ const ctx = makeCtx()
1043
+ const pastExpiry = Math.floor(Date.now() / 1000) - 3600
1044
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.expired-proto-token", "1//refresh", pastExpiry))
1045
+ ctx.host.ls.discover.mockReturnValue(null)
1046
+
1047
+ let capturedAuth = null
1048
+ ctx.host.http.request.mockImplementation((opts) => {
1049
+ if (String(opts.url).includes("fetchAvailableModels")) {
1050
+ capturedAuth = opts.headers.Authorization
1051
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
1052
+ }
1053
+ return { status: 500, bodyText: "" }
1054
+ })
1055
+
1056
+ const plugin = await loadPlugin()
1057
+ plugin.probe(ctx)
1058
+
1059
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
1060
+ })
1061
+
1062
+ it("handles missing/corrupt cache file gracefully", async () => {
1063
+ const ctx = makeCtx()
1064
+ setupSqliteMock(ctx, makeAuthStatusJson())
1065
+ ctx.host.ls.discover.mockReturnValue(null)
1066
+
1067
+ const cachePath = ctx.app.pluginDataDir + "/auth.json"
1068
+ ctx.host.fs.writeText(cachePath, "{bad json")
1069
+
1070
+ let capturedAuth = null
1071
+ ctx.host.http.request.mockImplementation((opts) => {
1072
+ if (String(opts.url).includes("fetchAvailableModels")) {
1073
+ capturedAuth = opts.headers.Authorization
1074
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
1075
+ }
1076
+ return { status: 500, bodyText: "" }
1077
+ })
1078
+
1079
+ const plugin = await loadPlugin()
1080
+ plugin.probe(ctx)
1081
+
1082
+ expect(capturedAuth).toBe("Bearer test-api-key-123")
1083
+ })
1084
+
1085
+ it("Cloud Code skips models with isInternal flag", async () => {
1086
+ const ctx = makeCtx()
1087
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1088
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test", "1//r", futureExpiry))
1089
+ ctx.host.ls.discover.mockReturnValue(null)
1090
+
1091
+ ctx.host.http.request.mockImplementation((opts) => {
1092
+ if (String(opts.url).includes("fetchAvailableModels")) {
1093
+ return {
1094
+ status: 200,
1095
+ bodyText: JSON.stringify({
1096
+ models: {
1097
+ "chat_20706": {
1098
+ model: "MODEL_CHAT_20706",
1099
+ isInternal: true,
1100
+ quotaInfo: { remainingFraction: 1, resetTime: "2026-02-08T10:00:00Z" },
1101
+ },
1102
+ "gemini-3-flash": {
1103
+ displayName: "Gemini 3 Flash",
1104
+ model: "MODEL_PLACEHOLDER_M18",
1105
+ quotaInfo: { remainingFraction: 0.9, resetTime: "2026-02-08T10:00:00Z" },
1106
+ },
1107
+ },
1108
+ }),
1109
+ }
1110
+ }
1111
+ return { status: 500, bodyText: "" }
1112
+ })
1113
+
1114
+ const plugin = await loadPlugin()
1115
+ const result = plugin.probe(ctx)
1116
+
1117
+ const labels = result.lines.map((l) => l.label)
1118
+ expect(labels).toContain("Gemini Flash")
1119
+ expect(labels).not.toContain("chat_20706")
1120
+ expect(labels).not.toContain("MODEL_CHAT_20706")
1121
+ })
1122
+
1123
+ it("Cloud Code skips models with empty or missing displayName", async () => {
1124
+ const ctx = makeCtx()
1125
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1126
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test", "1//r", futureExpiry))
1127
+ ctx.host.ls.discover.mockReturnValue(null)
1128
+
1129
+ ctx.host.http.request.mockImplementation((opts) => {
1130
+ if (String(opts.url).includes("fetchAvailableModels")) {
1131
+ return {
1132
+ status: 200,
1133
+ bodyText: JSON.stringify({
1134
+ models: {
1135
+ "tab_flash_lite": {
1136
+ displayName: "",
1137
+ model: "SOME_MODEL",
1138
+ quotaInfo: { remainingFraction: 1, resetTime: "2026-02-08T10:00:00Z" },
1139
+ },
1140
+ "no_display_name": {
1141
+ model: "ANOTHER_MODEL",
1142
+ quotaInfo: { remainingFraction: 1, resetTime: "2026-02-08T10:00:00Z" },
1143
+ },
1144
+ "gemini-3-pro": {
1145
+ displayName: "Gemini 3 Pro",
1146
+ model: "MODEL_PLACEHOLDER_M8",
1147
+ quotaInfo: { remainingFraction: 0.5, resetTime: "2026-02-08T10:00:00Z" },
1148
+ },
1149
+ },
1150
+ }),
1151
+ }
1152
+ }
1153
+ return { status: 500, bodyText: "" }
1154
+ })
1155
+
1156
+ const plugin = await loadPlugin()
1157
+ const result = plugin.probe(ctx)
1158
+
1159
+ const labels = result.lines.map((l) => l.label)
1160
+ expect(labels).toEqual(["Gemini Pro"])
1161
+ })
1162
+
1163
+ it("Cloud Code skips blacklisted model IDs", async () => {
1164
+ const ctx = makeCtx()
1165
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1166
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test", "1//r", futureExpiry))
1167
+ ctx.host.ls.discover.mockReturnValue(null)
1168
+
1169
+ ctx.host.http.request.mockImplementation((opts) => {
1170
+ if (String(opts.url).includes("fetchAvailableModels")) {
1171
+ return {
1172
+ status: 200,
1173
+ bodyText: JSON.stringify({
1174
+ models: {
1175
+ "gemini-2.5-flash": {
1176
+ displayName: "Gemini 2.5 Flash",
1177
+ model: "MODEL_GOOGLE_GEMINI_2_5_FLASH",
1178
+ quotaInfo: { remainingFraction: 1, resetTime: "2026-02-08T10:00:00Z" },
1179
+ },
1180
+ "gemini-2.5-pro": {
1181
+ displayName: "Gemini 2.5 Pro",
1182
+ model: "MODEL_GOOGLE_GEMINI_2_5_PRO",
1183
+ quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T10:00:00Z" },
1184
+ },
1185
+ "claude-sonnet-4.5": {
1186
+ displayName: "Claude Sonnet 4.5",
1187
+ model: "MODEL_CLAUDE_4_5_SONNET",
1188
+ quotaInfo: { remainingFraction: 0.6, resetTime: "2026-02-08T10:00:00Z" },
1189
+ },
1190
+ },
1191
+ }),
1192
+ }
1193
+ }
1194
+ return { status: 500, bodyText: "" }
1195
+ })
1196
+
1197
+ const plugin = await loadPlugin()
1198
+ const result = plugin.probe(ctx)
1199
+
1200
+ const labels = result.lines.map((l) => l.label)
1201
+ expect(labels).toEqual(["Claude"])
1202
+ })
1203
+
1204
+ it("Cloud Code keeps non-blacklisted models with valid displayName", async () => {
1205
+ const ctx = makeCtx()
1206
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1207
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test", "1//r", futureExpiry))
1208
+ ctx.host.ls.discover.mockReturnValue(null)
1209
+
1210
+ ctx.host.http.request.mockImplementation((opts) => {
1211
+ if (String(opts.url).includes("fetchAvailableModels")) {
1212
+ return {
1213
+ status: 200,
1214
+ bodyText: JSON.stringify({
1215
+ models: {
1216
+ "gemini-3-pro-high": {
1217
+ displayName: "Gemini 3 Pro (High)",
1218
+ model: "MODEL_PLACEHOLDER_M8",
1219
+ quotaInfo: { remainingFraction: 0.7, resetTime: "2026-02-08T10:00:00Z" },
1220
+ },
1221
+ "claude-opus-4-6-thinking": {
1222
+ displayName: "Claude Opus 4.6 (Thinking)",
1223
+ model: "MODEL_PLACEHOLDER_M26",
1224
+ quotaInfo: { remainingFraction: 1, resetTime: "2026-02-08T10:00:00Z" },
1225
+ },
1226
+ "gpt-oss-120b": {
1227
+ displayName: "GPT-OSS 120B (Medium)",
1228
+ model: "MODEL_OPENAI_GPT_OSS_120B_MEDIUM",
1229
+ quotaInfo: { remainingFraction: 0.9, resetTime: "2026-02-08T10:00:00Z" },
1230
+ },
1231
+ },
1232
+ }),
1233
+ }
1234
+ }
1235
+ return { status: 500, bodyText: "" }
1236
+ })
1237
+
1238
+ const plugin = await loadPlugin()
1239
+ const result = plugin.probe(ctx)
1240
+
1241
+ const labels = result.lines.map((l) => l.label)
1242
+ expect(labels).toEqual(["Gemini Pro", "Claude"])
1243
+ })
1244
+
1245
+ it("LS filters out blacklisted model IDs (Claude Opus 4.5)", async () => {
1246
+ const ctx = makeCtx()
1247
+ const discovery = makeDiscovery()
1248
+ const response = makeUserStatusResponse({
1249
+ configs: [
1250
+ {
1251
+ label: "Gemini 3 Pro (High)",
1252
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M8" },
1253
+ quotaInfo: { remainingFraction: 0.75, resetTime: "2026-02-08T09:10:56Z" },
1254
+ },
1255
+ {
1256
+ label: "Claude Opus 4.5 (Thinking)",
1257
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M12" },
1258
+ quotaInfo: { remainingFraction: 0.8, resetTime: "2026-02-08T09:10:56Z" },
1259
+ },
1260
+ {
1261
+ label: "Claude Opus 4.6 (Thinking)",
1262
+ modelOrAlias: { model: "MODEL_PLACEHOLDER_M26" },
1263
+ quotaInfo: { remainingFraction: 0.6, resetTime: "2026-02-08T09:10:56Z" },
1264
+ },
1265
+ ],
1266
+ })
1267
+ setupLsMock(ctx, discovery, response)
1268
+
1269
+ const plugin = await loadPlugin()
1270
+ const result = plugin.probe(ctx)
1271
+
1272
+ const labels = result.lines.map((l) => l.label)
1273
+ expect(labels).toEqual(["Gemini Pro", "Claude"])
1274
+ })
1275
+
1276
+ it("LS still takes priority over Cloud Code with proto tokens (no regression)", async () => {
1277
+ const ctx = makeCtx()
1278
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1279
+ const protoB64 = makeProtobufBase64(ctx, "ya29.proto-token", "1//refresh", futureExpiry)
1280
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
1281
+ const discovery = makeDiscovery()
1282
+ const response = makeUserStatusResponse()
1283
+ setupLsMock(ctx, discovery, response)
1284
+ setupSqliteMock(ctx, makeAuthStatusJson(), protoB64)
1285
+
1286
+ const plugin = await loadPlugin()
1287
+ const result = plugin.probe(ctx)
1288
+
1289
+ expect(result.plan).toBe("Pro")
1290
+ const calls = ctx.host.http.request.mock.calls.map((c) => String(c[0].url))
1291
+ expect(calls.filter((u) => u.includes("fetchAvailableModels")).length).toBe(0)
1292
+ expect(calls.filter((u) => u.includes("oauth2.googleapis.com")).length).toBe(0)
1293
+ })
1294
+
1295
+ it("throws when Cloud Code returns no models", async () => {
1296
+ const ctx = makeCtx()
1297
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1298
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
1299
+ ctx.host.ls.discover.mockReturnValue(null)
1300
+ ctx.host.http.request.mockImplementation((opts) => {
1301
+ if (String(opts.url).includes("fetchAvailableModels")) {
1302
+ return { status: 200, bodyText: JSON.stringify({}) }
1303
+ }
1304
+ return { status: 500, bodyText: "" }
1305
+ })
1306
+
1307
+ const plugin = await loadPlugin()
1308
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
1309
+ })
1310
+
1311
+ it("handles refresh response missing access_token", async () => {
1312
+ const ctx = makeCtx()
1313
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1314
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.will-fail", "1//refresh", futureExpiry))
1315
+ ctx.host.ls.discover.mockReturnValue(null)
1316
+
1317
+ let oauthCalls = 0
1318
+ ctx.host.http.request.mockImplementation((opts) => {
1319
+ const url = String(opts.url)
1320
+ if (url.includes("oauth2.googleapis.com")) {
1321
+ oauthCalls += 1
1322
+ return { status: 200, bodyText: JSON.stringify({ expires_in: 3600 }) }
1323
+ }
1324
+ if (url.includes("fetchAvailableModels")) {
1325
+ return { status: 401, bodyText: '{"error":"unauthorized"}' }
1326
+ }
1327
+ return { status: 500, bodyText: "" }
1328
+ })
1329
+
1330
+ const plugin = await loadPlugin()
1331
+ expect(() => plugin.probe(ctx)).toThrow("Start Antigravity and try again.")
1332
+ expect(oauthCalls).toBe(1)
1333
+ })
1334
+
1335
+ it("continues to next Cloud Code base URL after non-2xx response", async () => {
1336
+ const ctx = makeCtx()
1337
+ const futureExpiry = Math.floor(Date.now() / 1000) + 3600
1338
+ setupSqliteMock(ctx, makeAuthStatusJson(), makeProtobufBase64(ctx, "ya29.test-token", "1//refresh", futureExpiry))
1339
+ ctx.host.ls.discover.mockReturnValue(null)
1340
+
1341
+ let ccCalls = 0
1342
+ ctx.host.http.request.mockImplementation((opts) => {
1343
+ if (String(opts.url).includes("fetchAvailableModels")) {
1344
+ ccCalls += 1
1345
+ if (ccCalls === 1) return { status: 500, bodyText: "{}" }
1346
+ return { status: 200, bodyText: JSON.stringify(makeCloudCodeResponse()) }
1347
+ }
1348
+ return { status: 500, bodyText: "" }
1349
+ })
1350
+
1351
+ const plugin = await loadPlugin()
1352
+ const result = plugin.probe(ctx)
1353
+ expect(result.lines.length).toBeGreaterThan(0)
1354
+ expect(ccCalls).toBe(2)
1355
+ })
1356
+ })