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,735 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest"
2
+ import { makeCtx } from "../test-helpers.js"
3
+
4
+ const SETTINGS_PATH = "~/.gemini/settings.json"
5
+ const CREDS_PATH = "~/.gemini/oauth_creds.json"
6
+ const OAUTH2_PATH = "~/.bun/install/global/node_modules/@google/gemini-cli-core/dist/src/code_assist/oauth2.js"
7
+
8
+ const LOAD_CODE_ASSIST_URL = "https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist"
9
+ const QUOTA_URL = "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota"
10
+ const PROJECTS_URL = "https://cloudresourcemanager.googleapis.com/v1/projects"
11
+ const TOKEN_URL = "https://oauth2.googleapis.com/token"
12
+
13
+ const loadPlugin = async () => {
14
+ await import("./plugin.js")
15
+ return globalThis.__openusage_plugin
16
+ }
17
+
18
+ function makeJwt(payload) {
19
+ const header = Buffer.from(JSON.stringify({ alg: "none", typ: "JWT" }), "utf8").toString("base64url")
20
+ const body = Buffer.from(JSON.stringify(payload), "utf8").toString("base64url")
21
+ return `${header}.${body}.sig`
22
+ }
23
+
24
+ describe("gemini plugin", () => {
25
+ beforeEach(() => {
26
+ delete globalThis.__openusage_plugin
27
+ vi.resetModules()
28
+ })
29
+
30
+ it("throws when auth type is api-key", async () => {
31
+ const ctx = makeCtx()
32
+ ctx.host.fs.writeText(SETTINGS_PATH, JSON.stringify({ authType: "api-key" }))
33
+ const plugin = await loadPlugin()
34
+ expect(() => plugin.probe(ctx)).toThrow("api-key")
35
+ })
36
+
37
+ it("throws when auth type is unsupported", async () => {
38
+ const ctx = makeCtx()
39
+ ctx.host.fs.writeText(SETTINGS_PATH, JSON.stringify({ authType: "unknown-mode" }))
40
+ const plugin = await loadPlugin()
41
+ expect(() => plugin.probe(ctx)).toThrow("unsupported auth type")
42
+ })
43
+
44
+ it("throws when creds are missing", async () => {
45
+ const ctx = makeCtx()
46
+ const plugin = await loadPlugin()
47
+ expect(() => plugin.probe(ctx)).toThrow("Not logged in")
48
+ })
49
+
50
+ it("refreshes token, parses plan, and returns pro + flash usage", async () => {
51
+ const ctx = makeCtx()
52
+ const nowMs = 1_700_000_000_000
53
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
54
+
55
+ ctx.host.fs.writeText(
56
+ CREDS_PATH,
57
+ JSON.stringify({
58
+ access_token: "old-token",
59
+ refresh_token: "refresh-token",
60
+ id_token: makeJwt({ email: "me@example.com" }),
61
+ expiry_date: nowMs - 1000,
62
+ })
63
+ )
64
+ ctx.host.fs.writeText(
65
+ OAUTH2_PATH,
66
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
67
+ )
68
+
69
+ ctx.host.http.request.mockImplementation((opts) => {
70
+ const url = String(opts.url)
71
+ if (url === TOKEN_URL) {
72
+ return { status: 200, bodyText: JSON.stringify({ access_token: "new-token", expires_in: 3600 }) }
73
+ }
74
+ if (url === LOAD_CODE_ASSIST_URL) {
75
+ return {
76
+ status: 200,
77
+ bodyText: JSON.stringify({ tier: "standard-tier", cloudaicompanionProject: "gen-lang-client-123" }),
78
+ }
79
+ }
80
+ if (url === QUOTA_URL) {
81
+ expect(opts.headers.Authorization).toBe("Bearer new-token")
82
+ expect(opts.bodyText).toContain("gen-lang-client-123")
83
+ return {
84
+ status: 200,
85
+ bodyText: JSON.stringify({
86
+ quotaBuckets: [
87
+ { modelId: "gemini-2.5-pro", remainingFraction: 0.2, resetTime: "2099-01-01T00:00:00Z" },
88
+ { modelId: "gemini-2.5-pro", remainingFraction: 0.4, resetTime: "2099-01-01T00:00:00Z" },
89
+ { modelId: "gemini-2.0-flash", remainingFraction: 0.6, resetTime: "2099-01-02T00:00:00Z" },
90
+ ],
91
+ }),
92
+ }
93
+ }
94
+ throw new Error("unexpected url: " + url)
95
+ })
96
+
97
+ const plugin = await loadPlugin()
98
+ const result = plugin.probe(ctx)
99
+ expect(result.plan).toBe("Paid")
100
+
101
+ const pro = result.lines.find((line) => line.label === "Pro")
102
+ const flash = result.lines.find((line) => line.label === "Flash")
103
+ const account = result.lines.find((line) => line.label === "Account")
104
+ expect(pro && pro.used).toBe(80)
105
+ expect(flash && flash.used).toBe(40)
106
+ expect(account && account.value).toBe("me@example.com")
107
+
108
+ const persisted = JSON.parse(ctx.host.fs.readText(CREDS_PATH))
109
+ expect(persisted.access_token).toBe("new-token")
110
+ })
111
+
112
+ it("uses project fallback and maps workspace tier", async () => {
113
+ const ctx = makeCtx()
114
+ const nowMs = 1_700_000_000_000
115
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
116
+
117
+ ctx.host.fs.writeText(
118
+ CREDS_PATH,
119
+ JSON.stringify({
120
+ access_token: "token",
121
+ refresh_token: "refresh-token",
122
+ id_token: makeJwt({ email: "corp@example.com", hd: "example.com" }),
123
+ expiry_date: nowMs + 3600_000,
124
+ })
125
+ )
126
+
127
+ ctx.host.http.request.mockImplementation((opts) => {
128
+ const url = String(opts.url)
129
+ if (url === LOAD_CODE_ASSIST_URL) {
130
+ return { status: 200, bodyText: JSON.stringify({ tier: "free-tier" }) }
131
+ }
132
+ if (url === PROJECTS_URL) {
133
+ return {
134
+ status: 200,
135
+ bodyText: JSON.stringify({ projects: [{ projectId: "other-project" }, { projectId: "gen-lang-client-456" }] }),
136
+ }
137
+ }
138
+ if (url === QUOTA_URL) {
139
+ expect(opts.bodyText).toContain("gen-lang-client-456")
140
+ return {
141
+ status: 200,
142
+ bodyText: JSON.stringify({
143
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.75, resetTime: "2099-01-01T00:00:00Z" }],
144
+ }),
145
+ }
146
+ }
147
+ throw new Error("unexpected url: " + url)
148
+ })
149
+
150
+ const plugin = await loadPlugin()
151
+ const result = plugin.probe(ctx)
152
+ expect(result.plan).toBe("Workspace")
153
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
154
+ })
155
+
156
+ it("retries loadCodeAssist on 401 and continues", async () => {
157
+ const ctx = makeCtx()
158
+ const nowMs = 1_700_000_000_000
159
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
160
+
161
+ ctx.host.fs.writeText(
162
+ CREDS_PATH,
163
+ JSON.stringify({
164
+ access_token: "stale-token",
165
+ refresh_token: "refresh-token",
166
+ id_token: makeJwt({ email: "me@example.com" }),
167
+ expiry_date: nowMs + 3600_000,
168
+ })
169
+ )
170
+ ctx.host.fs.writeText(
171
+ OAUTH2_PATH,
172
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
173
+ )
174
+
175
+ let loadCodeAssistCalls = 0
176
+ ctx.host.http.request.mockImplementation((opts) => {
177
+ const url = String(opts.url)
178
+ if (url === LOAD_CODE_ASSIST_URL) {
179
+ loadCodeAssistCalls += 1
180
+ if (loadCodeAssistCalls === 1) return { status: 401, bodyText: "" }
181
+ return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
182
+ }
183
+ if (url === TOKEN_URL) {
184
+ return { status: 200, bodyText: JSON.stringify({ access_token: "new-token", expires_in: 3600 }) }
185
+ }
186
+ if (url === QUOTA_URL) {
187
+ expect(opts.headers.Authorization).toBe("Bearer new-token")
188
+ return {
189
+ status: 200,
190
+ bodyText: JSON.stringify({
191
+ quotaBuckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.2, resetTime: "2099-01-01T00:00:00Z" }],
192
+ }),
193
+ }
194
+ }
195
+ throw new Error("unexpected url: " + url)
196
+ })
197
+
198
+ const plugin = await loadPlugin()
199
+ const result = plugin.probe(ctx)
200
+ expect(loadCodeAssistCalls).toBe(2)
201
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
202
+ })
203
+
204
+ it("throws session expired when loadCodeAssist keeps returning 401", async () => {
205
+ const ctx = makeCtx()
206
+ const nowMs = 1_700_000_000_000
207
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
208
+
209
+ ctx.host.fs.writeText(
210
+ CREDS_PATH,
211
+ JSON.stringify({
212
+ access_token: "token",
213
+ refresh_token: "refresh-token",
214
+ id_token: makeJwt({ email: "me@example.com" }),
215
+ expiry_date: nowMs + 3600_000,
216
+ })
217
+ )
218
+ ctx.host.fs.writeText(
219
+ OAUTH2_PATH,
220
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
221
+ )
222
+
223
+ ctx.host.http.request.mockImplementation((opts) => {
224
+ const url = String(opts.url)
225
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 401, bodyText: "" }
226
+ if (url === TOKEN_URL) {
227
+ return { status: 200, bodyText: JSON.stringify({ access_token: "new-token", expires_in: 3600 }) }
228
+ }
229
+ return { status: 404, bodyText: "" }
230
+ })
231
+
232
+ const plugin = await loadPlugin()
233
+ expect(() => plugin.probe(ctx)).toThrow("session expired")
234
+ })
235
+
236
+ it("throws when auth type is vertex-ai", async () => {
237
+ const ctx = makeCtx()
238
+ ctx.host.fs.writeText(SETTINGS_PATH, JSON.stringify({ authType: "vertex-ai" }))
239
+ const plugin = await loadPlugin()
240
+ expect(() => plugin.probe(ctx)).toThrow("vertex-ai")
241
+ })
242
+
243
+ it("falls back when settings cannot be read", async () => {
244
+ const ctx = makeCtx()
245
+ const readText = vi.fn((path) => {
246
+ if (path === SETTINGS_PATH) throw new Error("boom")
247
+ return null
248
+ })
249
+ ctx.host.fs.exists = (path) => path === SETTINGS_PATH
250
+ ctx.host.fs.readText = readText
251
+
252
+ const plugin = await loadPlugin()
253
+ expect(() => plugin.probe(ctx)).toThrow("Not logged in")
254
+ })
255
+
256
+ it("treats creds without tokens as not logged in", async () => {
257
+ const ctx = makeCtx()
258
+ ctx.host.fs.writeText(CREDS_PATH, JSON.stringify({ user: "me" }))
259
+ const plugin = await loadPlugin()
260
+ expect(() => plugin.probe(ctx)).toThrow("Not logged in")
261
+ })
262
+
263
+ it("treats non-object creds payload as not logged in", async () => {
264
+ const ctx = makeCtx()
265
+ ctx.host.fs.writeText(CREDS_PATH, JSON.stringify("bad-shape"))
266
+ const plugin = await loadPlugin()
267
+ expect(() => plugin.probe(ctx)).toThrow("Not logged in")
268
+ })
269
+
270
+ it("throws not logged in when refresh is needed but cannot be performed", async () => {
271
+ const ctx = makeCtx()
272
+ const nowMs = 1_700_000_000_000
273
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
274
+ ctx.host.fs.writeText(
275
+ CREDS_PATH,
276
+ JSON.stringify({
277
+ refresh_token: "refresh-token",
278
+ expiry_date: nowMs - 1000,
279
+ })
280
+ )
281
+
282
+ const plugin = await loadPlugin()
283
+ expect(() => plugin.probe(ctx)).toThrow("Not logged in")
284
+ })
285
+
286
+ it("continues with existing token when refresh token is missing", async () => {
287
+ const ctx = makeCtx()
288
+ const nowMs = 1_700_000_000_000
289
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
290
+ ctx.host.fs.writeText(
291
+ CREDS_PATH,
292
+ JSON.stringify({
293
+ access_token: "existing-token",
294
+ expiry_date: nowMs - 1000,
295
+ })
296
+ )
297
+ ctx.host.http.request.mockImplementation((opts) => {
298
+ const url = String(opts.url)
299
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
300
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-888" }] }) }
301
+ if (url === QUOTA_URL) {
302
+ return {
303
+ status: 200,
304
+ bodyText: JSON.stringify({
305
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.6, resetTime: "2099-01-01T00:00:00Z" }],
306
+ }),
307
+ }
308
+ }
309
+ throw new Error("unexpected url: " + url)
310
+ })
311
+
312
+ const plugin = await loadPlugin()
313
+ const result = plugin.probe(ctx)
314
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
315
+ })
316
+
317
+ it("continues with existing access token when oauth client creds are missing", async () => {
318
+ const ctx = makeCtx()
319
+ const nowMs = 1_700_000_000_000
320
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
321
+ ctx.host.fs.writeText(
322
+ CREDS_PATH,
323
+ JSON.stringify({
324
+ access_token: "existing-token",
325
+ refresh_token: "refresh-token",
326
+ expiry_date: nowMs - 1000,
327
+ })
328
+ )
329
+ ctx.host.fs.writeText(OAUTH2_PATH, "not oauth creds")
330
+ ctx.host.http.request.mockImplementation((opts) => {
331
+ const url = String(opts.url)
332
+ if (url === LOAD_CODE_ASSIST_URL) {
333
+ return { status: 200, bodyText: JSON.stringify({ tier: "legacy-tier" }) }
334
+ }
335
+ if (url === PROJECTS_URL) {
336
+ return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-123" }] }) }
337
+ }
338
+ if (url === QUOTA_URL) {
339
+ expect(opts.headers.Authorization).toBe("Bearer existing-token")
340
+ return {
341
+ status: 200,
342
+ bodyText: JSON.stringify({
343
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.3, resetTime: "2099-01-01T00:00:00Z" }],
344
+ }),
345
+ }
346
+ }
347
+ throw new Error("unexpected url: " + url)
348
+ })
349
+
350
+ const plugin = await loadPlugin()
351
+ const result = plugin.probe(ctx)
352
+ expect(result.plan).toBe("Legacy")
353
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
354
+ })
355
+
356
+ it("continues when refresh request throws and an access token already exists", async () => {
357
+ const ctx = makeCtx()
358
+ const nowMs = 1_700_000_000_000
359
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
360
+ ctx.host.fs.writeText(
361
+ CREDS_PATH,
362
+ JSON.stringify({
363
+ access_token: "existing-token",
364
+ refresh_token: "refresh-token",
365
+ expiry_date: nowMs - 1000,
366
+ })
367
+ )
368
+ ctx.host.fs.writeText(
369
+ OAUTH2_PATH,
370
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
371
+ )
372
+ ctx.host.http.request.mockImplementation((opts) => {
373
+ const url = String(opts.url)
374
+ if (url === TOKEN_URL) throw new Error("network")
375
+ if (url === LOAD_CODE_ASSIST_URL) {
376
+ return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
377
+ }
378
+ if (url === PROJECTS_URL) {
379
+ return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-456" }] }) }
380
+ }
381
+ if (url === QUOTA_URL) {
382
+ expect(opts.headers.Authorization).toBe("Bearer existing-token")
383
+ return {
384
+ status: 200,
385
+ bodyText: JSON.stringify({
386
+ quotaBuckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.5, resetTime: "2099-01-01T00:00:00Z" }],
387
+ }),
388
+ }
389
+ }
390
+ throw new Error("unexpected url: " + url)
391
+ })
392
+
393
+ const plugin = await loadPlugin()
394
+ const result = plugin.probe(ctx)
395
+ expect(result.plan).toBe("Paid")
396
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
397
+ })
398
+
399
+ it("continues with existing token when refresh returns non-2xx", async () => {
400
+ const ctx = makeCtx()
401
+ const nowMs = 1_700_000_000_000
402
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
403
+ ctx.host.fs.writeText(
404
+ CREDS_PATH,
405
+ JSON.stringify({
406
+ access_token: "existing-token",
407
+ refresh_token: "refresh-token",
408
+ expiry_date: nowMs - 1000,
409
+ })
410
+ )
411
+ ctx.host.fs.writeText(
412
+ OAUTH2_PATH,
413
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
414
+ )
415
+ ctx.host.http.request.mockImplementation((opts) => {
416
+ const url = String(opts.url)
417
+ if (url === TOKEN_URL) return { status: 500, bodyText: "{}" }
418
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
419
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-777" }] }) }
420
+ if (url === QUOTA_URL) {
421
+ return {
422
+ status: 200,
423
+ bodyText: JSON.stringify({
424
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.5, resetTime: "2099-01-01T00:00:00Z" }],
425
+ }),
426
+ }
427
+ }
428
+ throw new Error("unexpected url: " + url)
429
+ })
430
+
431
+ const plugin = await loadPlugin()
432
+ const result = plugin.probe(ctx)
433
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
434
+ })
435
+
436
+ it("ignores non-string oauth2.js payload and continues", async () => {
437
+ const ctx = makeCtx()
438
+ const nowMs = 1_700_000_000_000
439
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
440
+ ctx.host.fs.writeText(
441
+ CREDS_PATH,
442
+ JSON.stringify({
443
+ access_token: "existing-token",
444
+ refresh_token: "refresh-token",
445
+ expiry_date: nowMs - 1000,
446
+ })
447
+ )
448
+ ctx.host.fs.exists = (path) => path === CREDS_PATH || path === OAUTH2_PATH
449
+ ctx.host.fs.readText = vi.fn((path) => {
450
+ if (path === CREDS_PATH) {
451
+ return JSON.stringify({
452
+ access_token: "existing-token",
453
+ refresh_token: "refresh-token",
454
+ expiry_date: nowMs - 1000,
455
+ })
456
+ }
457
+ if (path === OAUTH2_PATH) return null
458
+ return null
459
+ })
460
+ ctx.host.http.request.mockImplementation((opts) => {
461
+ const url = String(opts.url)
462
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
463
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-666" }] }) }
464
+ if (url === QUOTA_URL) {
465
+ return {
466
+ status: 200,
467
+ bodyText: JSON.stringify({
468
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.4, resetTime: "2099-01-01T00:00:00Z" }],
469
+ }),
470
+ }
471
+ }
472
+ throw new Error("unexpected url: " + url)
473
+ })
474
+
475
+ const plugin = await loadPlugin()
476
+ const result = plugin.probe(ctx)
477
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
478
+ })
479
+
480
+ it("skips proactive refresh when expiry_date is non-numeric", async () => {
481
+ const ctx = makeCtx()
482
+ ctx.host.fs.writeText(
483
+ CREDS_PATH,
484
+ JSON.stringify({
485
+ access_token: "token",
486
+ refresh_token: "refresh-token",
487
+ expiry_date: "not-a-number",
488
+ })
489
+ )
490
+ ctx.host.http.request.mockImplementation((opts) => {
491
+ const url = String(opts.url)
492
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
493
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-555" }] }) }
494
+ if (url === QUOTA_URL) {
495
+ return {
496
+ status: 200,
497
+ bodyText: JSON.stringify({
498
+ buckets: [{ modelId: "gemini-2.5-pro", remainingFraction: 0.5, resetTime: "2099-01-01T00:00:00Z" }],
499
+ }),
500
+ }
501
+ }
502
+ throw new Error("unexpected url: " + url)
503
+ })
504
+
505
+ const plugin = await loadPlugin()
506
+ const result = plugin.probe(ctx)
507
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
508
+ expect(
509
+ ctx.host.http.request.mock.calls.some((call) => String(call[0]?.url) === TOKEN_URL)
510
+ ).toBe(false)
511
+ })
512
+
513
+ it("throws session expired when proactive refresh is unauthorized", async () => {
514
+ const ctx = makeCtx()
515
+ const nowMs = 1_700_000_000_000
516
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
517
+ ctx.host.fs.writeText(
518
+ CREDS_PATH,
519
+ JSON.stringify({
520
+ access_token: "token",
521
+ refresh_token: "refresh-token",
522
+ expiry_date: nowMs - 1000,
523
+ })
524
+ )
525
+ ctx.host.fs.writeText(
526
+ OAUTH2_PATH,
527
+ "const OAUTH_CLIENT_ID='client-id'; const OAUTH_CLIENT_SECRET='client-secret';"
528
+ )
529
+ ctx.host.http.request.mockImplementation((opts) => {
530
+ const url = String(opts.url)
531
+ if (url === TOKEN_URL) return { status: 401, bodyText: JSON.stringify({ error: "unauthorized" }) }
532
+ return { status: 500, bodyText: "{}" }
533
+ })
534
+
535
+ const plugin = await loadPlugin()
536
+ expect(() => plugin.probe(ctx)).toThrow("Gemini session expired")
537
+ })
538
+
539
+ it("returns free plan and status badge when quota has no recognized buckets", async () => {
540
+ const ctx = makeCtx()
541
+ const nowMs = 1_700_000_000_000
542
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
543
+ ctx.host.fs.writeText(
544
+ CREDS_PATH,
545
+ JSON.stringify({
546
+ access_token: "token",
547
+ refresh_token: "refresh-token",
548
+ expiry_date: nowMs + 3600_000,
549
+ })
550
+ )
551
+ ctx.host.http.request.mockImplementation((opts) => {
552
+ const url = String(opts.url)
553
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "free-tier" }) }
554
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [] }) }
555
+ if (url === QUOTA_URL) return { status: 200, bodyText: JSON.stringify({ buckets: [{ modelId: "other-model", remainingFraction: 0.2 }] }) }
556
+ throw new Error("unexpected url: " + url)
557
+ })
558
+
559
+ const plugin = await loadPlugin()
560
+ const result = plugin.probe(ctx)
561
+ expect(result.plan).toBe("Free")
562
+ const status = result.lines.find((line) => line.label === "Status")
563
+ expect(status && status.text).toBe("No usage data")
564
+ })
565
+
566
+ it("throws session expired when loadCodeAssist returns auth and refresh cannot recover", async () => {
567
+ const ctx = makeCtx()
568
+ const nowMs = 1_700_000_000_000
569
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
570
+ ctx.host.fs.writeText(
571
+ CREDS_PATH,
572
+ JSON.stringify({
573
+ access_token: "token",
574
+ refresh_token: "refresh-token",
575
+ expiry_date: nowMs + 3600_000,
576
+ })
577
+ )
578
+ ctx.host.http.request.mockImplementation((opts) => {
579
+ const url = String(opts.url)
580
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 401, bodyText: "" }
581
+ return { status: 404, bodyText: "" }
582
+ })
583
+
584
+ const plugin = await loadPlugin()
585
+ expect(() => plugin.probe(ctx)).toThrow("session expired")
586
+ })
587
+
588
+ it("throws when quota request is unauthorized and refresh cannot recover", async () => {
589
+ const ctx = makeCtx()
590
+ const nowMs = 1_700_000_000_000
591
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
592
+ ctx.host.fs.writeText(
593
+ CREDS_PATH,
594
+ JSON.stringify({
595
+ access_token: "token",
596
+ refresh_token: "refresh-token",
597
+ expiry_date: nowMs + 3600_000,
598
+ })
599
+ )
600
+ ctx.host.http.request.mockImplementation((opts) => {
601
+ const url = String(opts.url)
602
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
603
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-789" }] }) }
604
+ if (url === QUOTA_URL) return { status: 401, bodyText: "" }
605
+ return { status: 404, bodyText: "" }
606
+ })
607
+
608
+ const plugin = await loadPlugin()
609
+ expect(() => plugin.probe(ctx)).toThrow("session expired")
610
+ })
611
+
612
+ it("throws when quota request returns non-2xx", async () => {
613
+ const ctx = makeCtx()
614
+ const nowMs = 1_700_000_000_000
615
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
616
+ ctx.host.fs.writeText(
617
+ CREDS_PATH,
618
+ JSON.stringify({
619
+ access_token: "token",
620
+ refresh_token: "refresh-token",
621
+ expiry_date: nowMs + 3600_000,
622
+ })
623
+ )
624
+ ctx.host.http.request.mockImplementation((opts) => {
625
+ const url = String(opts.url)
626
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
627
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-321" }] }) }
628
+ if (url === QUOTA_URL) return { status: 500, bodyText: "" }
629
+ return { status: 404, bodyText: "" }
630
+ })
631
+
632
+ const plugin = await loadPlugin()
633
+ expect(() => plugin.probe(ctx)).toThrow("quota request failed")
634
+ })
635
+
636
+ it("throws when quota response JSON is invalid", async () => {
637
+ const ctx = makeCtx()
638
+ const nowMs = 1_700_000_000_000
639
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
640
+ ctx.host.fs.writeText(
641
+ CREDS_PATH,
642
+ JSON.stringify({
643
+ access_token: "token",
644
+ refresh_token: "refresh-token",
645
+ expiry_date: nowMs + 3600_000,
646
+ })
647
+ )
648
+ ctx.host.http.request.mockImplementation((opts) => {
649
+ const url = String(opts.url)
650
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
651
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-111" }] }) }
652
+ if (url === QUOTA_URL) return { status: 200, bodyText: "bad-json" }
653
+ return { status: 404, bodyText: "" }
654
+ })
655
+
656
+ const plugin = await loadPlugin()
657
+ expect(() => plugin.probe(ctx)).toThrow("quota response invalid")
658
+ })
659
+
660
+ it("reads project id from labels when loadCodeAssist does not provide one", async () => {
661
+ const ctx = makeCtx()
662
+ const nowMs = 1_700_000_000_000
663
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
664
+ ctx.host.fs.writeText(
665
+ CREDS_PATH,
666
+ JSON.stringify({
667
+ access_token: "token",
668
+ refresh_token: "refresh-token",
669
+ expiry_date: nowMs + 3600_000,
670
+ })
671
+ )
672
+ ctx.host.http.request.mockImplementation((opts) => {
673
+ const url = String(opts.url)
674
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 500, bodyText: "{}" }
675
+ if (url === PROJECTS_URL) {
676
+ return {
677
+ status: 200,
678
+ bodyText: JSON.stringify({ projects: [{ projectId: "labeled-project", labels: { "generative-language": "1" } }] }),
679
+ }
680
+ }
681
+ if (url === QUOTA_URL) {
682
+ expect(opts.bodyText).toContain("labeled-project")
683
+ return {
684
+ status: 200,
685
+ bodyText: JSON.stringify({
686
+ quotaBuckets: [{ modelId: "gemini-2.5-flash", remainingFraction: 0.4, resetTime: "2099-01-02T00:00:00Z" }],
687
+ }),
688
+ }
689
+ }
690
+ throw new Error("unexpected url: " + url)
691
+ })
692
+
693
+ const plugin = await loadPlugin()
694
+ const result = plugin.probe(ctx)
695
+ expect(result.lines.find((line) => line.label === "Flash")).toBeTruthy()
696
+ })
697
+
698
+ it("uses snake_case quota fields and still renders lines", async () => {
699
+ const ctx = makeCtx()
700
+ const nowMs = 1_700_000_000_000
701
+ vi.spyOn(Date, "now").mockReturnValue(nowMs)
702
+ ctx.host.fs.writeText(
703
+ CREDS_PATH,
704
+ JSON.stringify({
705
+ access_token: "token",
706
+ refresh_token: "refresh-token",
707
+ expiry_date: nowMs + 3600_000,
708
+ })
709
+ )
710
+ ctx.host.http.request.mockImplementation((opts) => {
711
+ const url = String(opts.url)
712
+ if (url === LOAD_CODE_ASSIST_URL) return { status: 200, bodyText: JSON.stringify({ tier: "standard-tier" }) }
713
+ if (url === PROJECTS_URL) return { status: 200, bodyText: JSON.stringify({ projects: [{ projectId: "gen-lang-client-909" }] }) }
714
+ if (url === QUOTA_URL) {
715
+ return {
716
+ status: 200,
717
+ bodyText: JSON.stringify({
718
+ data: {
719
+ items: [
720
+ { model_id: "gemini-2.5-pro", remainingFraction: 0.1, reset_time: "2099-01-01T00:00:00Z" },
721
+ { modelId: "gemini-2.0-flash", remainingFraction: 0.8, resetTime: "2099-01-02T00:00:00Z" },
722
+ ],
723
+ },
724
+ }),
725
+ }
726
+ }
727
+ throw new Error("unexpected url: " + url)
728
+ })
729
+
730
+ const plugin = await loadPlugin()
731
+ const result = plugin.probe(ctx)
732
+ expect(result.lines.find((line) => line.label === "Pro")).toBeTruthy()
733
+ expect(result.lines.find((line) => line.label === "Flash")).toBeTruthy()
734
+ })
735
+ })