@ai-devkit/agent-manager 0.35.0 → 0.36.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 (92) hide show
  1. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
  2. package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
  3. package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
  4. package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
  5. package/dist/__tests__/capacity/claude.test.js +485 -0
  6. package/dist/__tests__/capacity/claude.test.js.map +1 -0
  7. package/dist/__tests__/capacity/openai.test.js +672 -0
  8. package/dist/__tests__/capacity/openai.test.js.map +1 -0
  9. package/dist/__tests__/capacity/wham.test.js +144 -0
  10. package/dist/__tests__/capacity/wham.test.js.map +1 -0
  11. package/dist/__tests__/utils/agents.test.js +6 -0
  12. package/dist/__tests__/utils/agents.test.js.map +1 -1
  13. package/dist/adapters/AgentAdapter.d.ts +1 -1
  14. package/dist/adapters/AgentAdapter.d.ts.map +1 -1
  15. package/dist/adapters/AgentAdapter.js.map +1 -1
  16. package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
  17. package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
  18. package/dist/adapters/AntigravityCliAdapter.js +273 -0
  19. package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
  20. package/dist/adapters/KiroAdapter.d.ts +46 -0
  21. package/dist/adapters/KiroAdapter.d.ts.map +1 -0
  22. package/dist/adapters/KiroAdapter.js +344 -0
  23. package/dist/adapters/KiroAdapter.js.map +1 -0
  24. package/dist/adapters/index.d.ts +2 -0
  25. package/dist/adapters/index.d.ts.map +1 -1
  26. package/dist/adapters/index.js +2 -0
  27. package/dist/adapters/index.js.map +1 -1
  28. package/dist/capacity/claude.d.ts +17 -0
  29. package/dist/capacity/claude.d.ts.map +1 -0
  30. package/dist/capacity/claude.js +271 -0
  31. package/dist/capacity/claude.js.map +1 -0
  32. package/dist/capacity/codex.d.ts +1 -2
  33. package/dist/capacity/codex.d.ts.map +1 -1
  34. package/dist/capacity/codex.js +5 -47
  35. package/dist/capacity/codex.js.map +1 -1
  36. package/dist/capacity/index.d.ts +11 -0
  37. package/dist/capacity/index.d.ts.map +1 -1
  38. package/dist/capacity/index.js +17 -0
  39. package/dist/capacity/index.js.map +1 -1
  40. package/dist/capacity/openai.d.ts +48 -0
  41. package/dist/capacity/openai.d.ts.map +1 -0
  42. package/dist/capacity/openai.js +312 -0
  43. package/dist/capacity/openai.js.map +1 -0
  44. package/dist/capacity/wham.d.ts +16 -0
  45. package/dist/capacity/wham.d.ts.map +1 -0
  46. package/dist/capacity/wham.js +66 -0
  47. package/dist/capacity/wham.js.map +1 -0
  48. package/dist/index.d.ts +4 -2
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +3 -1
  51. package/dist/index.js.map +1 -1
  52. package/dist/readiness/AgentReadiness.d.ts +1 -1
  53. package/dist/readiness/AgentReadiness.d.ts.map +1 -1
  54. package/dist/readiness/AgentReadiness.js.map +1 -1
  55. package/dist/runtime/ManagedAgentRuntime.js +3 -1
  56. package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
  57. package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
  58. package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
  59. package/dist/utils/agents.d.ts +1 -1
  60. package/dist/utils/agents.d.ts.map +1 -1
  61. package/dist/utils/agents.js +11 -0
  62. package/dist/utils/agents.js.map +1 -1
  63. package/package.json +1 -1
  64. package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
  65. package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
  66. package/src/__tests__/capacity/claude.test.ts +467 -0
  67. package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
  68. package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
  69. package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
  70. package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
  71. package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
  72. package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
  73. package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
  74. package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
  75. package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
  76. package/src/__tests__/capacity/openai.test.ts +570 -0
  77. package/src/__tests__/capacity/wham.test.ts +104 -0
  78. package/src/__tests__/utils/agents.test.ts +7 -0
  79. package/src/adapters/AgentAdapter.ts +2 -0
  80. package/src/adapters/AntigravityCliAdapter.ts +356 -0
  81. package/src/adapters/KiroAdapter.ts +456 -0
  82. package/src/adapters/index.ts +2 -0
  83. package/src/capacity/claude.ts +313 -0
  84. package/src/capacity/codex.ts +25 -134
  85. package/src/capacity/index.ts +33 -4
  86. package/src/capacity/openai.ts +361 -0
  87. package/src/capacity/wham.ts +84 -0
  88. package/src/index.ts +10 -1
  89. package/src/readiness/AgentReadiness.ts +4 -1
  90. package/src/runtime/ManagedAgentRuntime.ts +2 -0
  91. package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
  92. package/src/utils/agents.ts +11 -1
@@ -0,0 +1,672 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { readFileSync } from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ import { describe, expect, it, vi } from "vitest";
5
+ import { buildOpenAiReport, parseOpenAiUsage, probeOpenAiCapacity, resolveOpenAiApiKey, resolveOpenAiCredential } from "../../capacity/openai.js";
6
+ const checkedAt = "2026-09-26T10:00:00.000Z";
7
+ const fixture = (name)=>readFile(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
8
+ const fixtureString = (name)=>readFileSync(fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)), "utf8");
9
+ describe("OpenAI credential resolution", ()=>{
10
+ it("prefers OPENAI_API_KEY without reading Pi auth", async ()=>{
11
+ const readAuth = vi.fn(async ()=>fixture("openai-auth.json"));
12
+ await expect(resolveOpenAiApiKey({
13
+ env: {
14
+ OPENAI_API_KEY: "env-openai-test-key"
15
+ },
16
+ readFile: readAuth
17
+ })).resolves.toBe("env-openai-test-key");
18
+ expect(readAuth).not.toHaveBeenCalled();
19
+ });
20
+ it("reads the Pi openai api_key credential from ~/.pi/agent/auth.json", async ()=>{
21
+ const readAuth = vi.fn(async ()=>fixture("openai-auth.json"));
22
+ await expect(resolveOpenAiApiKey({
23
+ env: {
24
+ HOME: "/users/test"
25
+ },
26
+ readFile: readAuth
27
+ })).resolves.toBe("pi-openai-test-key");
28
+ expect(readAuth).toHaveBeenCalledWith("/users/test/.pi/agent/auth.json", "utf8");
29
+ });
30
+ it.each([
31
+ [
32
+ "missing",
33
+ async ()=>Promise.reject(Object.assign(new Error("missing"), {
34
+ code: "ENOENT"
35
+ }))
36
+ ],
37
+ [
38
+ "invalid JSON",
39
+ async ()=>"{"
40
+ ],
41
+ [
42
+ "wrong credential type",
43
+ async ()=>JSON.stringify({
44
+ openai: {
45
+ type: "oauth",
46
+ key: "x"
47
+ }
48
+ })
49
+ ],
50
+ [
51
+ "missing key",
52
+ async ()=>JSON.stringify({
53
+ openai: {
54
+ type: "api_key"
55
+ }
56
+ })
57
+ ]
58
+ ])("fails clearly for %s credentials without exposing file content", async (_name, readAuth)=>{
59
+ await expect(resolveOpenAiApiKey({
60
+ env: {
61
+ HOME: "/users/test"
62
+ },
63
+ readFile: readAuth
64
+ })).rejects.toThrow(/OpenAI credentials not found|OpenAI Pi auth file/);
65
+ });
66
+ });
67
+ describe("OpenAI tiered credential resolution", ()=>{
68
+ const oauthFixture = ()=>fixture("openai-codex-auth.json");
69
+ const bothCredentials = JSON.stringify({
70
+ openai: {
71
+ type: "api_key",
72
+ key: "pi-openai-test-key"
73
+ },
74
+ "openai-codex": {
75
+ type: "oauth",
76
+ access: "fixture-header.eyJleHAiOjE3OTMwMDAwMDB9.fixture-signature",
77
+ refresh: "fixture-refresh-token",
78
+ expires: 1793000000000,
79
+ accountId: "fixture-chatgpt-account-id"
80
+ }
81
+ });
82
+ it("prefers the Pi openai platform credential over the openai-codex OAuth login", async ()=>{
83
+ await expect(resolveOpenAiCredential({
84
+ env: {
85
+ HOME: "/users/test"
86
+ },
87
+ readFile: async ()=>bothCredentials
88
+ })).resolves.toEqual({
89
+ kind: "platform",
90
+ key: "pi-openai-test-key"
91
+ });
92
+ });
93
+ it("resolves the Pi openai-codex OAuth login when no platform credential exists", async ()=>{
94
+ await expect(resolveOpenAiCredential({
95
+ env: {
96
+ HOME: "/users/test"
97
+ },
98
+ readFile: async ()=>oauthFixture()
99
+ })).resolves.toEqual({
100
+ kind: "oauth",
101
+ access: "fixture-header.eyJleHAiOjE3OTMwMDAwMDB9.fixture-signature",
102
+ accountId: "fixture-chatgpt-account-id",
103
+ expiresMs: 1793000000000
104
+ });
105
+ });
106
+ it("normalizes second-based expires values to epoch milliseconds", async ()=>{
107
+ const auth = JSON.stringify({
108
+ "openai-codex": {
109
+ type: "oauth",
110
+ access: "fixture-access",
111
+ refresh: "fixture-refresh",
112
+ expires: 1793000000,
113
+ accountId: "fixture-chatgpt-account-id"
114
+ }
115
+ });
116
+ await expect(resolveOpenAiCredential({
117
+ env: {
118
+ HOME: "/users/test"
119
+ },
120
+ readFile: async ()=>auth
121
+ })).resolves.toMatchObject({
122
+ kind: "oauth",
123
+ expiresMs: 1793000000000
124
+ });
125
+ });
126
+ it("keeps a null expiry for OAuth logins without usable expires metadata", async ()=>{
127
+ const auth = JSON.stringify({
128
+ "openai-codex": {
129
+ type: "oauth",
130
+ access: "fixture-access",
131
+ accountId: "fixture-chatgpt-account-id"
132
+ }
133
+ });
134
+ await expect(resolveOpenAiCredential({
135
+ env: {
136
+ HOME: "/users/test"
137
+ },
138
+ readFile: async ()=>auth
139
+ })).resolves.toMatchObject({
140
+ kind: "oauth",
141
+ expiresMs: null
142
+ });
143
+ });
144
+ it.each([
145
+ [
146
+ "wrong credential type",
147
+ JSON.stringify({
148
+ "openai-codex": {
149
+ type: "api_key",
150
+ key: "x"
151
+ }
152
+ })
153
+ ],
154
+ [
155
+ "missing access token",
156
+ JSON.stringify({
157
+ "openai-codex": {
158
+ type: "oauth",
159
+ accountId: "a"
160
+ }
161
+ })
162
+ ],
163
+ [
164
+ "missing account id",
165
+ JSON.stringify({
166
+ "openai-codex": {
167
+ type: "oauth",
168
+ access: "t"
169
+ }
170
+ })
171
+ ]
172
+ ])("skips a malformed openai-codex credential (%s) with a sanitized error", async (_name, auth)=>{
173
+ await expect(resolveOpenAiCredential({
174
+ env: {
175
+ HOME: "/users/test"
176
+ },
177
+ readFile: async ()=>auth
178
+ })).rejects.toThrow("OpenAI credentials not found");
179
+ });
180
+ it("still throws for OAuth-only machines on the legacy platform-key resolver", async ()=>{
181
+ await expect(resolveOpenAiApiKey({
182
+ env: {
183
+ HOME: "/users/test"
184
+ },
185
+ readFile: async ()=>oauthFixture()
186
+ })).rejects.toThrow("OpenAI credentials not found");
187
+ });
188
+ });
189
+ describe("OpenAI OAuth capacity probe", ()=>{
190
+ const freshOauth = ()=>fixture("openai-codex-auth.json");
191
+ const whamResponse = ()=>new Response(fixtureString("wham-usage.json"), {
192
+ status: 200
193
+ });
194
+ it("reports wham usage windows for a fresh Pi OAuth login", async ()=>{
195
+ const fetch = vi.fn(async ()=>whamResponse());
196
+ const report = await probeOpenAiCapacity({
197
+ checkedAt,
198
+ env: {
199
+ HOME: "/users/test"
200
+ },
201
+ readFile: async ()=>freshOauth(),
202
+ fetch
203
+ });
204
+ expect(fetch).toHaveBeenCalledWith("https://chatgpt.com/backend-api/wham/usage", expect.objectContaining({
205
+ method: "GET",
206
+ headers: {
207
+ Authorization: "Bearer fixture-header.eyJleHAiOjE3OTMwMDAwMDB9.fixture-signature",
208
+ "ChatGPT-Account-Id": "fixture-chatgpt-account-id"
209
+ }
210
+ }));
211
+ expect(report).toMatchObject({
212
+ harness: "pi",
213
+ provider: "openai",
214
+ generatedAt: checkedAt,
215
+ authenticated: true,
216
+ available: "yes",
217
+ creditsRemaining: 4.5
218
+ });
219
+ expect(report.windows.map((window)=>window.id)).toEqual([
220
+ "session",
221
+ "weekly"
222
+ ]);
223
+ expect(JSON.stringify(report)).not.toContain("fixture-signature");
224
+ });
225
+ it("does not fetch and reports an authenticated-but-stale state for expired logins", async ()=>{
226
+ const staleAuth = JSON.stringify({
227
+ "openai-codex": {
228
+ type: "oauth",
229
+ access: "stale-fixture-access",
230
+ expires: 1790000000000,
231
+ accountId: "fixture-chatgpt-account-id"
232
+ }
233
+ });
234
+ const fetch = vi.fn();
235
+ const report = await probeOpenAiCapacity({
236
+ checkedAt,
237
+ env: {
238
+ HOME: "/users/test"
239
+ },
240
+ readFile: async ()=>staleAuth,
241
+ fetch
242
+ });
243
+ expect(fetch).not.toHaveBeenCalled();
244
+ expect(report).toEqual({
245
+ harness: "pi",
246
+ provider: "openai",
247
+ generatedAt: checkedAt,
248
+ authenticated: true,
249
+ available: "unknown",
250
+ windows: [],
251
+ creditsRemaining: null
252
+ });
253
+ });
254
+ it("treats second-based expiry metadata as stale the same way", async ()=>{
255
+ const staleAuth = JSON.stringify({
256
+ "openai-codex": {
257
+ type: "oauth",
258
+ access: "stale-fixture-access",
259
+ expires: 1790000000,
260
+ accountId: "fixture-chatgpt-account-id"
261
+ }
262
+ });
263
+ const fetch = vi.fn();
264
+ const report = await probeOpenAiCapacity({
265
+ checkedAt,
266
+ env: {
267
+ HOME: "/users/test"
268
+ },
269
+ readFile: async ()=>staleAuth,
270
+ fetch
271
+ });
272
+ expect(fetch).not.toHaveBeenCalled();
273
+ expect(report.available).toBe("unknown");
274
+ });
275
+ it("falls back to the JWT exp when expires metadata is missing", async ()=>{
276
+ const expiredJwtAuth = JSON.stringify({
277
+ "openai-codex": {
278
+ type: "oauth",
279
+ access: "h.eyJleHAiOjE3ODkwMDAwMDB9.s",
280
+ accountId: "fixture-chatgpt-account-id"
281
+ }
282
+ });
283
+ const noFetch = vi.fn();
284
+ await expect(probeOpenAiCapacity({
285
+ checkedAt,
286
+ env: {
287
+ HOME: "/users/test"
288
+ },
289
+ readFile: async ()=>expiredJwtAuth,
290
+ fetch: noFetch
291
+ })).resolves.toMatchObject({
292
+ authenticated: true,
293
+ available: "unknown"
294
+ });
295
+ expect(noFetch).not.toHaveBeenCalled();
296
+ const futureJwtAuth = JSON.stringify({
297
+ "openai-codex": {
298
+ type: "oauth",
299
+ access: "h.eyJleHAiOjE3OTMwMDAwMDB9.s",
300
+ accountId: "fixture-chatgpt-account-id"
301
+ }
302
+ });
303
+ const fetch = vi.fn(async ()=>whamResponse());
304
+ await expect(probeOpenAiCapacity({
305
+ checkedAt,
306
+ env: {
307
+ HOME: "/users/test"
308
+ },
309
+ readFile: async ()=>futureJwtAuth,
310
+ fetch
311
+ })).resolves.toMatchObject({
312
+ authenticated: true,
313
+ available: "yes"
314
+ });
315
+ expect(fetch).toHaveBeenCalledOnce();
316
+ });
317
+ it("treats unparseable JWT payloads as unknown expiry and probes anyway", async ()=>{
318
+ const auth = JSON.stringify({
319
+ "openai-codex": {
320
+ type: "oauth",
321
+ access: "fixture-header.bm90LWpzb24.fixture-signature",
322
+ accountId: "fixture-chatgpt-account-id"
323
+ }
324
+ });
325
+ const fetch = vi.fn(async ()=>whamResponse());
326
+ await expect(probeOpenAiCapacity({
327
+ checkedAt,
328
+ env: {
329
+ HOME: "/users/test"
330
+ },
331
+ readFile: async ()=>auth,
332
+ fetch
333
+ })).resolves.toMatchObject({
334
+ authenticated: true,
335
+ available: "yes"
336
+ });
337
+ expect(fetch).toHaveBeenCalledOnce();
338
+ });
339
+ it("treats non-JWT and exp-less access tokens as unknown expiry and probes anyway", async ()=>{
340
+ for (const access of [
341
+ "opaque-fixture-access",
342
+ "h.eyJzdWIiOiJ4In0.s"
343
+ ]){
344
+ const auth = JSON.stringify({
345
+ "openai-codex": {
346
+ type: "oauth",
347
+ access,
348
+ accountId: "fixture-chatgpt-account-id"
349
+ }
350
+ });
351
+ const fetch = vi.fn(async ()=>whamResponse());
352
+ await expect(probeOpenAiCapacity({
353
+ checkedAt,
354
+ env: {
355
+ HOME: "/users/test"
356
+ },
357
+ readFile: async ()=>auth,
358
+ fetch
359
+ })).resolves.toMatchObject({
360
+ authenticated: true,
361
+ available: "yes"
362
+ });
363
+ expect(fetch).toHaveBeenCalledOnce();
364
+ }
365
+ });
366
+ it("reports unknown availability when wham returns no usable windows", async ()=>{
367
+ const report = await probeOpenAiCapacity({
368
+ checkedAt,
369
+ env: {
370
+ HOME: "/users/test"
371
+ },
372
+ readFile: async ()=>freshOauth(),
373
+ fetch: vi.fn(async ()=>new Response("{}", {
374
+ status: 200
375
+ }))
376
+ });
377
+ expect(report).toMatchObject({
378
+ authenticated: true,
379
+ available: "unknown",
380
+ windows: []
381
+ });
382
+ });
383
+ it("reports unauthenticated when wham rejects the OAuth token", async ()=>{
384
+ for (const status of [
385
+ 401,
386
+ 403
387
+ ]){
388
+ const report = await probeOpenAiCapacity({
389
+ checkedAt,
390
+ env: {
391
+ HOME: "/users/test"
392
+ },
393
+ readFile: async ()=>freshOauth(),
394
+ fetch: vi.fn(async ()=>new Response("denied", {
395
+ status
396
+ }))
397
+ });
398
+ expect(report).toMatchObject({
399
+ authenticated: false,
400
+ available: "unknown",
401
+ windows: [],
402
+ creditsRemaining: null
403
+ });
404
+ }
405
+ });
406
+ it("throws sanitized errors for transport failures without leaking tokens", async ()=>{
407
+ const probe = (fetch)=>probeOpenAiCapacity({
408
+ checkedAt,
409
+ env: {
410
+ HOME: "/users/test"
411
+ },
412
+ readFile: async ()=>freshOauth(),
413
+ fetch: fetch,
414
+ timeoutMs: 10
415
+ });
416
+ await expect(probe((_url, init)=>new Promise((_resolve, reject)=>init.signal?.addEventListener("abort", ()=>reject(new Error("aborted")))))).rejects.toThrow("OpenAI usage request failed");
417
+ await expect(probe(vi.fn(async ()=>new Response("boom", {
418
+ status: 503
419
+ })))).rejects.toThrow("OpenAI usage request failed: HTTP 503");
420
+ await expect(probe(vi.fn(async ()=>new Response("not-json", {
421
+ status: 200
422
+ })))).rejects.toThrow("OpenAI usage response is not valid JSON");
423
+ try {
424
+ await probe(vi.fn(async ()=>new Response("boom", {
425
+ status: 503
426
+ })));
427
+ } catch (error) {
428
+ expect(String(error)).not.toContain("fixture-signature");
429
+ expect(String(error)).not.toContain("fixture-chatgpt-account-id");
430
+ }
431
+ });
432
+ });
433
+ describe("OpenAI usage mapping", ()=>{
434
+ it("sums per-model input and output tokens into UTC day buckets", async ()=>{
435
+ const buckets = parseOpenAiUsage(JSON.parse(await fixture("openai-usage-completions.json")));
436
+ expect(buckets).toEqual([
437
+ {
438
+ startMs: 1790380800000,
439
+ tokens: 2000
440
+ },
441
+ {
442
+ startMs: 1790294400000,
443
+ tokens: 5000
444
+ },
445
+ {
446
+ startMs: 1789862400000,
447
+ tokens: 1000
448
+ },
449
+ {
450
+ startMs: 1789689600000,
451
+ tokens: 19998
452
+ }
453
+ ]);
454
+ });
455
+ it("keeps empty buckets and skips entries it cannot place in time", ()=>{
456
+ expect(parseOpenAiUsage({
457
+ object: "list",
458
+ data: [
459
+ null,
460
+ 42,
461
+ {
462
+ aggregation_timestamp: 1790380800
463
+ },
464
+ {
465
+ aggregation_timestamp: "yesterday",
466
+ results: []
467
+ },
468
+ {
469
+ aggregation_timestamp: 1790294400,
470
+ results: null
471
+ }
472
+ ]
473
+ })).toEqual([
474
+ {
475
+ startMs: 1790380800000,
476
+ tokens: 0
477
+ },
478
+ {
479
+ startMs: 1790294400000,
480
+ tokens: 0
481
+ }
482
+ ]);
483
+ });
484
+ it("rejects responses without a usage data array", ()=>{
485
+ expect(()=>parseOpenAiUsage({
486
+ object: "list"
487
+ })).toThrow("Invalid OpenAI usage response");
488
+ expect(()=>parseOpenAiUsage({
489
+ data: {}
490
+ })).toThrow("Invalid OpenAI usage response");
491
+ });
492
+ it("builds today and 7-day windows without faked limits", async ()=>{
493
+ const buckets = [
494
+ ...parseOpenAiUsage(JSON.parse(await fixture("openai-usage-completions.json"))),
495
+ ...parseOpenAiUsage(JSON.parse(await fixture("openai-usage-responses.json")))
496
+ ];
497
+ const report = buildOpenAiReport(buckets, checkedAt);
498
+ expect(report).toMatchObject({
499
+ harness: "pi",
500
+ provider: "openai",
501
+ generatedAt: checkedAt,
502
+ authenticated: true,
503
+ available: "yes",
504
+ creditsRemaining: null
505
+ });
506
+ expect(report.windows).toEqual([
507
+ {
508
+ id: "openai:tokens:today",
509
+ label: "Tokens · today (UTC)",
510
+ limitType: "TOKENS_LIMIT",
511
+ durationMinutes: 1440,
512
+ usedPercent: null,
513
+ resetsAt: "2026-09-27T00:00:00.000Z",
514
+ total: null,
515
+ current: 2000,
516
+ remaining: null
517
+ },
518
+ {
519
+ id: "openai:tokens:week",
520
+ label: "Tokens · 7 days",
521
+ limitType: "TOKENS_LIMIT",
522
+ durationMinutes: 10080,
523
+ usedPercent: null,
524
+ resetsAt: null,
525
+ total: null,
526
+ current: 13000,
527
+ remaining: null
528
+ }
529
+ ]);
530
+ });
531
+ it("reports zero usage for days without buckets", ()=>{
532
+ const report = buildOpenAiReport([], checkedAt);
533
+ expect(report.windows.map((window)=>window.current)).toEqual([
534
+ 0,
535
+ 0
536
+ ]);
537
+ expect(report.windows[0].resetsAt).toBe("2026-09-27T00:00:00.000Z");
538
+ });
539
+ });
540
+ describe("OpenAI request", ()=>{
541
+ it("GETs both usage endpoints for the trailing week with only Bearer authentication", async ()=>{
542
+ const fetch = vi.fn(async (url)=>url.includes("/responses?") ? new Response(await fixture("openai-usage-responses.json"), {
543
+ status: 200
544
+ }) : new Response(await fixture("openai-usage-completions.json"), {
545
+ status: 200
546
+ }));
547
+ const report = await probeOpenAiCapacity({
548
+ checkedAt,
549
+ env: {
550
+ OPENAI_API_KEY: "request-test-key"
551
+ },
552
+ fetch
553
+ });
554
+ const query = "start_time=1789812000&end_time=1790416800";
555
+ expect(fetch).toHaveBeenCalledTimes(2);
556
+ expect(fetch).toHaveBeenCalledWith(`https://api.openai.com/v1/organization/usage/completions?${query}`, expect.objectContaining({
557
+ method: "GET",
558
+ headers: {
559
+ Authorization: "Bearer request-test-key"
560
+ }
561
+ }));
562
+ expect(fetch).toHaveBeenCalledWith(`https://api.openai.com/v1/organization/usage/responses?${query}`, expect.objectContaining({
563
+ headers: {
564
+ Authorization: "Bearer request-test-key"
565
+ }
566
+ }));
567
+ expect(report.windows.map((window)=>window.current)).toEqual([
568
+ 2000,
569
+ 13000
570
+ ]);
571
+ expect(JSON.stringify(report)).not.toContain("request-test-key");
572
+ });
573
+ it("reports unauthenticated for rejected keys", async ()=>{
574
+ const report = await probeOpenAiCapacity({
575
+ checkedAt,
576
+ env: {
577
+ OPENAI_API_KEY: "rejected-key"
578
+ },
579
+ fetch: vi.fn(async ()=>new Response("denied", {
580
+ status: 401
581
+ }))
582
+ });
583
+ expect(report).toMatchObject({
584
+ authenticated: false,
585
+ available: "unknown",
586
+ windows: [],
587
+ creditsRemaining: null
588
+ });
589
+ });
590
+ it("degrades to authentication-only when usage requires an admin key", async ()=>{
591
+ const fetch = vi.fn(async (url)=>url.endsWith("/models") ? new Response("{}", {
592
+ status: 200
593
+ }) : new Response("forbidden", {
594
+ status: 403
595
+ }));
596
+ const report = await probeOpenAiCapacity({
597
+ checkedAt,
598
+ env: {
599
+ OPENAI_API_KEY: "member-key"
600
+ },
601
+ fetch
602
+ });
603
+ expect(fetch).toHaveBeenCalledWith("https://api.openai.com/v1/models", expect.objectContaining({
604
+ headers: {
605
+ Authorization: "Bearer member-key"
606
+ }
607
+ }));
608
+ expect(report).toMatchObject({
609
+ authenticated: true,
610
+ available: "unknown",
611
+ windows: []
612
+ });
613
+ });
614
+ it("falls back to unauthenticated when the degraded check rejects the key", async ()=>{
615
+ const fetch = vi.fn(async (url)=>url.endsWith("/models") ? new Response("denied", {
616
+ status: 401
617
+ }) : new Response("forbidden", {
618
+ status: 403
619
+ }));
620
+ const report = await probeOpenAiCapacity({
621
+ checkedAt,
622
+ env: {
623
+ OPENAI_API_KEY: "revoked-key"
624
+ },
625
+ fetch
626
+ });
627
+ expect(report).toMatchObject({
628
+ authenticated: false,
629
+ windows: []
630
+ });
631
+ });
632
+ it("sanitizes network failures on the platform key path", async ()=>{
633
+ await expect(probeOpenAiCapacity({
634
+ checkedAt,
635
+ env: {
636
+ OPENAI_API_KEY: "network-key"
637
+ },
638
+ fetch: vi.fn(async ()=>Promise.reject(new Error("ECONNRESET secret")))
639
+ })).rejects.toThrow("OpenAI usage request failed");
640
+ await expect(probeOpenAiCapacity({
641
+ checkedAt,
642
+ env: {
643
+ OPENAI_API_KEY: "network-key"
644
+ },
645
+ fetch: vi.fn(async (url)=>url.endsWith("/models") ? Promise.reject(new Error("ECONNRESET secret")) : new Response("forbidden", {
646
+ status: 403
647
+ }))
648
+ })).rejects.toThrow("OpenAI usage request failed");
649
+ });
650
+ it("rejects other statuses and malformed JSON responses with sanitized errors", async ()=>{
651
+ await expect(probeOpenAiCapacity({
652
+ checkedAt,
653
+ env: {
654
+ OPENAI_API_KEY: "never-expose-this-key"
655
+ },
656
+ fetch: vi.fn(async ()=>new Response("private body", {
657
+ status: 503
658
+ }))
659
+ })).rejects.toThrow("OpenAI usage request failed: HTTP 503");
660
+ await expect(probeOpenAiCapacity({
661
+ checkedAt,
662
+ env: {
663
+ OPENAI_API_KEY: "never-expose-this-key"
664
+ },
665
+ fetch: vi.fn(async ()=>new Response("not-json", {
666
+ status: 200
667
+ }))
668
+ })).rejects.toThrow("OpenAI usage response is not valid JSON");
669
+ });
670
+ });
671
+
672
+ //# sourceMappingURL=openai.test.js.map