@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.
- package/dist/__tests__/adapters/AntigravityCliAdapter.test.js +381 -0
- package/dist/__tests__/adapters/AntigravityCliAdapter.test.js.map +1 -0
- package/dist/__tests__/adapters/KiroAdapter.test.js +607 -0
- package/dist/__tests__/adapters/KiroAdapter.test.js.map +1 -0
- package/dist/__tests__/capacity/claude.test.js +485 -0
- package/dist/__tests__/capacity/claude.test.js.map +1 -0
- package/dist/__tests__/capacity/openai.test.js +672 -0
- package/dist/__tests__/capacity/openai.test.js.map +1 -0
- package/dist/__tests__/capacity/wham.test.js +144 -0
- package/dist/__tests__/capacity/wham.test.js.map +1 -0
- package/dist/__tests__/utils/agents.test.js +6 -0
- package/dist/__tests__/utils/agents.test.js.map +1 -1
- package/dist/adapters/AgentAdapter.d.ts +1 -1
- package/dist/adapters/AgentAdapter.d.ts.map +1 -1
- package/dist/adapters/AgentAdapter.js.map +1 -1
- package/dist/adapters/AntigravityCliAdapter.d.ts +57 -0
- package/dist/adapters/AntigravityCliAdapter.d.ts.map +1 -0
- package/dist/adapters/AntigravityCliAdapter.js +273 -0
- package/dist/adapters/AntigravityCliAdapter.js.map +1 -0
- package/dist/adapters/KiroAdapter.d.ts +46 -0
- package/dist/adapters/KiroAdapter.d.ts.map +1 -0
- package/dist/adapters/KiroAdapter.js +344 -0
- package/dist/adapters/KiroAdapter.js.map +1 -0
- package/dist/adapters/index.d.ts +2 -0
- package/dist/adapters/index.d.ts.map +1 -1
- package/dist/adapters/index.js +2 -0
- package/dist/adapters/index.js.map +1 -1
- package/dist/capacity/claude.d.ts +17 -0
- package/dist/capacity/claude.d.ts.map +1 -0
- package/dist/capacity/claude.js +271 -0
- package/dist/capacity/claude.js.map +1 -0
- package/dist/capacity/codex.d.ts +1 -2
- package/dist/capacity/codex.d.ts.map +1 -1
- package/dist/capacity/codex.js +5 -47
- package/dist/capacity/codex.js.map +1 -1
- package/dist/capacity/index.d.ts +11 -0
- package/dist/capacity/index.d.ts.map +1 -1
- package/dist/capacity/index.js +17 -0
- package/dist/capacity/index.js.map +1 -1
- package/dist/capacity/openai.d.ts +48 -0
- package/dist/capacity/openai.d.ts.map +1 -0
- package/dist/capacity/openai.js +312 -0
- package/dist/capacity/openai.js.map +1 -0
- package/dist/capacity/wham.d.ts +16 -0
- package/dist/capacity/wham.d.ts.map +1 -0
- package/dist/capacity/wham.js +66 -0
- package/dist/capacity/wham.js.map +1 -0
- package/dist/index.d.ts +4 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/readiness/AgentReadiness.d.ts +1 -1
- package/dist/readiness/AgentReadiness.d.ts.map +1 -1
- package/dist/readiness/AgentReadiness.js.map +1 -1
- package/dist/runtime/ManagedAgentRuntime.js +3 -1
- package/dist/runtime/ManagedAgentRuntime.js.map +1 -1
- package/dist/runtime/herdr/HerdrAgentDiscovery.js +10 -0
- package/dist/runtime/herdr/HerdrAgentDiscovery.js.map +1 -1
- package/dist/utils/agents.d.ts +1 -1
- package/dist/utils/agents.d.ts.map +1 -1
- package/dist/utils/agents.js +11 -0
- package/dist/utils/agents.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/adapters/AntigravityCliAdapter.test.ts +324 -0
- package/src/__tests__/adapters/KiroAdapter.test.ts +534 -0
- package/src/__tests__/capacity/claude.test.ts +467 -0
- package/src/__tests__/capacity/fixtures/claude-credentials.json +7 -0
- package/src/__tests__/capacity/fixtures/claude-usage-malformed.json +1 -0
- package/src/__tests__/capacity/fixtures/claude-usage-partial.json +15 -0
- package/src/__tests__/capacity/fixtures/claude-usage.json +63 -0
- package/src/__tests__/capacity/fixtures/openai-auth.json +6 -0
- package/src/__tests__/capacity/fixtures/openai-codex-auth.json +9 -0
- package/src/__tests__/capacity/fixtures/openai-usage-completions.json +67 -0
- package/src/__tests__/capacity/fixtures/openai-usage-responses.json +20 -0
- package/src/__tests__/capacity/fixtures/wham-usage.json +32 -0
- package/src/__tests__/capacity/openai.test.ts +570 -0
- package/src/__tests__/capacity/wham.test.ts +104 -0
- package/src/__tests__/utils/agents.test.ts +7 -0
- package/src/adapters/AgentAdapter.ts +2 -0
- package/src/adapters/AntigravityCliAdapter.ts +356 -0
- package/src/adapters/KiroAdapter.ts +456 -0
- package/src/adapters/index.ts +2 -0
- package/src/capacity/claude.ts +313 -0
- package/src/capacity/codex.ts +25 -134
- package/src/capacity/index.ts +33 -4
- package/src/capacity/openai.ts +361 -0
- package/src/capacity/wham.ts +84 -0
- package/src/index.ts +10 -1
- package/src/readiness/AgentReadiness.ts +4 -1
- package/src/runtime/ManagedAgentRuntime.ts +2 -0
- package/src/runtime/herdr/HerdrAgentDiscovery.ts +5 -1
- 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
|