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.
- package/bin/openusage +91 -0
- package/package.json +33 -0
- package/plugins/amp/icon.svg +6 -0
- package/plugins/amp/plugin.js +175 -0
- package/plugins/amp/plugin.json +20 -0
- package/plugins/amp/plugin.test.js +365 -0
- package/plugins/antigravity/icon.svg +3 -0
- package/plugins/antigravity/plugin.js +484 -0
- package/plugins/antigravity/plugin.json +17 -0
- package/plugins/antigravity/plugin.test.js +1356 -0
- package/plugins/claude/icon.svg +3 -0
- package/plugins/claude/plugin.js +565 -0
- package/plugins/claude/plugin.json +28 -0
- package/plugins/claude/plugin.test.js +1012 -0
- package/plugins/codex/icon.svg +3 -0
- package/plugins/codex/plugin.js +673 -0
- package/plugins/codex/plugin.json +30 -0
- package/plugins/codex/plugin.test.js +1071 -0
- package/plugins/copilot/icon.svg +3 -0
- package/plugins/copilot/plugin.js +264 -0
- package/plugins/copilot/plugin.json +20 -0
- package/plugins/copilot/plugin.test.js +529 -0
- package/plugins/cursor/icon.svg +3 -0
- package/plugins/cursor/plugin.js +526 -0
- package/plugins/cursor/plugin.json +24 -0
- package/plugins/cursor/plugin.test.js +1168 -0
- package/plugins/factory/icon.svg +1 -0
- package/plugins/factory/plugin.js +407 -0
- package/plugins/factory/plugin.json +19 -0
- package/plugins/factory/plugin.test.js +833 -0
- package/plugins/gemini/icon.svg +4 -0
- package/plugins/gemini/plugin.js +413 -0
- package/plugins/gemini/plugin.json +20 -0
- package/plugins/gemini/plugin.test.js +735 -0
- package/plugins/jetbrains-ai-assistant/icon.svg +3 -0
- package/plugins/jetbrains-ai-assistant/plugin.js +357 -0
- package/plugins/jetbrains-ai-assistant/plugin.json +17 -0
- package/plugins/jetbrains-ai-assistant/plugin.test.js +338 -0
- package/plugins/kimi/icon.svg +3 -0
- package/plugins/kimi/plugin.js +358 -0
- package/plugins/kimi/plugin.json +19 -0
- package/plugins/kimi/plugin.test.js +619 -0
- package/plugins/minimax/icon.svg +4 -0
- package/plugins/minimax/plugin.js +388 -0
- package/plugins/minimax/plugin.json +17 -0
- package/plugins/minimax/plugin.test.js +943 -0
- package/plugins/perplexity/icon.svg +1 -0
- package/plugins/perplexity/plugin.js +378 -0
- package/plugins/perplexity/plugin.json +15 -0
- package/plugins/perplexity/plugin.test.js +602 -0
- package/plugins/windsurf/icon.svg +3 -0
- package/plugins/windsurf/plugin.js +218 -0
- package/plugins/windsurf/plugin.json +16 -0
- package/plugins/windsurf/plugin.test.js +455 -0
- package/plugins/zai/icon.svg +5 -0
- package/plugins/zai/plugin.js +156 -0
- package/plugins/zai/plugin.json +18 -0
- package/plugins/zai/plugin.test.js +396 -0
|
@@ -0,0 +1,833 @@
|
|
|
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
|
+
// Helper to create a valid JWT with configurable expiry
|
|
10
|
+
function makeJwt(expSeconds) {
|
|
11
|
+
const header = btoa(JSON.stringify({ alg: "HS256", typ: "JWT" }))
|
|
12
|
+
const payload = btoa(JSON.stringify({ exp: expSeconds, org_id: "org_123", email: "test@example.com" }))
|
|
13
|
+
const sig = "signature"
|
|
14
|
+
return `${header}.${payload}.${sig}`
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe("factory plugin", () => {
|
|
18
|
+
beforeEach(() => {
|
|
19
|
+
delete globalThis.__openusage_plugin
|
|
20
|
+
vi.resetModules()
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it("throws when auth missing", async () => {
|
|
24
|
+
const ctx = makeCtx()
|
|
25
|
+
const plugin = await loadPlugin()
|
|
26
|
+
expect(() => plugin.probe(ctx)).toThrow("Not logged in")
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it("throws when auth json is invalid", async () => {
|
|
30
|
+
const ctx = makeCtx()
|
|
31
|
+
ctx.host.fs.writeText("~/.factory/auth.json", "{bad")
|
|
32
|
+
const plugin = await loadPlugin()
|
|
33
|
+
expect(() => plugin.probe(ctx)).toThrow("Not logged in")
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
it("throws when auth lacks access_token", async () => {
|
|
37
|
+
const ctx = makeCtx()
|
|
38
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({ refresh_token: "refresh" }))
|
|
39
|
+
const plugin = await loadPlugin()
|
|
40
|
+
expect(() => plugin.probe(ctx)).toThrow("Invalid auth file")
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it("loads auth from auth.encrypted when auth.json is missing", async () => {
|
|
44
|
+
const ctx = makeCtx()
|
|
45
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
46
|
+
ctx.host.fs.writeText("~/.factory/auth.encrypted", JSON.stringify({
|
|
47
|
+
access_token: makeJwt(futureExp),
|
|
48
|
+
refresh_token: "refresh",
|
|
49
|
+
}))
|
|
50
|
+
ctx.host.http.request.mockReturnValue({
|
|
51
|
+
status: 200,
|
|
52
|
+
headers: {},
|
|
53
|
+
bodyText: JSON.stringify({
|
|
54
|
+
usage: {
|
|
55
|
+
startDate: 1770623326000,
|
|
56
|
+
endDate: 1772956800000,
|
|
57
|
+
standard: { orgTotalTokensUsed: 123, totalAllowance: 20000000 },
|
|
58
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
59
|
+
},
|
|
60
|
+
}),
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
const plugin = await loadPlugin()
|
|
64
|
+
const result = plugin.probe(ctx)
|
|
65
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
it("loads auth from keychain when auth files are missing", async () => {
|
|
69
|
+
const ctx = makeCtx()
|
|
70
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
71
|
+
ctx.host.keychain.readGenericPassword.mockImplementation((service) => {
|
|
72
|
+
if (service === "Factory Token") {
|
|
73
|
+
return JSON.stringify({
|
|
74
|
+
access_token: makeJwt(futureExp),
|
|
75
|
+
refresh_token: "refresh",
|
|
76
|
+
})
|
|
77
|
+
}
|
|
78
|
+
return null
|
|
79
|
+
})
|
|
80
|
+
ctx.host.http.request.mockReturnValue({
|
|
81
|
+
status: 200,
|
|
82
|
+
headers: {},
|
|
83
|
+
bodyText: JSON.stringify({
|
|
84
|
+
usage: {
|
|
85
|
+
startDate: 1770623326000,
|
|
86
|
+
endDate: 1772956800000,
|
|
87
|
+
standard: { orgTotalTokensUsed: 1, totalAllowance: 20000000 },
|
|
88
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
89
|
+
},
|
|
90
|
+
}),
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
const plugin = await loadPlugin()
|
|
94
|
+
const result = plugin.probe(ctx)
|
|
95
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
96
|
+
expect(ctx.host.keychain.readGenericPassword).toHaveBeenCalledWith("Factory Token")
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it("loads auth from keychain when payload is hex-encoded JSON", async () => {
|
|
100
|
+
const ctx = makeCtx()
|
|
101
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
102
|
+
const payload = JSON.stringify({
|
|
103
|
+
access_token: makeJwt(futureExp),
|
|
104
|
+
refresh_token: "refresh",
|
|
105
|
+
})
|
|
106
|
+
const hexPayload = Buffer.from(payload, "utf8").toString("hex")
|
|
107
|
+
ctx.host.keychain.readGenericPassword.mockImplementation((service) => {
|
|
108
|
+
if (service === "Factory Token") return hexPayload
|
|
109
|
+
return null
|
|
110
|
+
})
|
|
111
|
+
ctx.host.http.request.mockReturnValue({
|
|
112
|
+
status: 200,
|
|
113
|
+
headers: {},
|
|
114
|
+
bodyText: JSON.stringify({
|
|
115
|
+
usage: {
|
|
116
|
+
startDate: 1770623326000,
|
|
117
|
+
endDate: 1772956800000,
|
|
118
|
+
standard: { orgTotalTokensUsed: 9, totalAllowance: 20000000 },
|
|
119
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
120
|
+
},
|
|
121
|
+
}),
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
const plugin = await loadPlugin()
|
|
125
|
+
const result = plugin.probe(ctx)
|
|
126
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
it("skips invalid keychain payload and tries next service", async () => {
|
|
130
|
+
const ctx = makeCtx()
|
|
131
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
132
|
+
ctx.host.keychain.readGenericPassword.mockImplementation((service) => {
|
|
133
|
+
if (service === "Factory Token") return "not-json"
|
|
134
|
+
if (service === "Factory token") {
|
|
135
|
+
return JSON.stringify({
|
|
136
|
+
access_token: makeJwt(futureExp),
|
|
137
|
+
refresh_token: "refresh",
|
|
138
|
+
})
|
|
139
|
+
}
|
|
140
|
+
return null
|
|
141
|
+
})
|
|
142
|
+
ctx.host.http.request.mockReturnValue({
|
|
143
|
+
status: 200,
|
|
144
|
+
headers: {},
|
|
145
|
+
bodyText: JSON.stringify({
|
|
146
|
+
usage: {
|
|
147
|
+
startDate: 1770623326000,
|
|
148
|
+
endDate: 1772956800000,
|
|
149
|
+
standard: { orgTotalTokensUsed: 2, totalAllowance: 20000000 },
|
|
150
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
151
|
+
},
|
|
152
|
+
}),
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const plugin = await loadPlugin()
|
|
156
|
+
const result = plugin.probe(ctx)
|
|
157
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
158
|
+
expect(ctx.host.keychain.readGenericPassword).toHaveBeenCalledWith("Factory Token")
|
|
159
|
+
expect(ctx.host.keychain.readGenericPassword).toHaveBeenCalledWith("Factory token")
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it("refreshes keychain auth and writes back to keychain", async () => {
|
|
163
|
+
const ctx = makeCtx()
|
|
164
|
+
const nearExp = Math.floor(Date.now() / 1000) + 12 * 60 * 60 // force proactive refresh
|
|
165
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
166
|
+
ctx.host.keychain.readGenericPassword.mockImplementation((service) => {
|
|
167
|
+
if (service === "Factory Token") {
|
|
168
|
+
return JSON.stringify({
|
|
169
|
+
access_token: makeJwt(nearExp),
|
|
170
|
+
refresh_token: "refresh",
|
|
171
|
+
})
|
|
172
|
+
}
|
|
173
|
+
return null
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
177
|
+
if (String(opts.url).includes("workos.com")) {
|
|
178
|
+
return {
|
|
179
|
+
status: 200,
|
|
180
|
+
bodyText: JSON.stringify({
|
|
181
|
+
access_token: makeJwt(futureExp),
|
|
182
|
+
refresh_token: "new-refresh",
|
|
183
|
+
}),
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
status: 200,
|
|
188
|
+
headers: {},
|
|
189
|
+
bodyText: JSON.stringify({
|
|
190
|
+
usage: {
|
|
191
|
+
startDate: 1770623326000,
|
|
192
|
+
endDate: 1772956800000,
|
|
193
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
194
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
195
|
+
},
|
|
196
|
+
}),
|
|
197
|
+
}
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
const plugin = await loadPlugin()
|
|
201
|
+
plugin.probe(ctx)
|
|
202
|
+
expect(ctx.host.keychain.writeGenericPassword).toHaveBeenCalledTimes(1)
|
|
203
|
+
const [service, writtenPayload] = ctx.host.keychain.writeGenericPassword.mock.calls[0]
|
|
204
|
+
expect(service).toBe("Factory Token")
|
|
205
|
+
const parsed = JSON.parse(writtenPayload)
|
|
206
|
+
expect(parsed.refresh_token).toBe("new-refresh")
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it("fetches usage and formats standard tokens", async () => {
|
|
210
|
+
const ctx = makeCtx()
|
|
211
|
+
// Token expires in 7 days (no refresh needed)
|
|
212
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
213
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
214
|
+
access_token: makeJwt(futureExp),
|
|
215
|
+
refresh_token: "refresh",
|
|
216
|
+
}))
|
|
217
|
+
ctx.host.http.request.mockReturnValue({
|
|
218
|
+
status: 200,
|
|
219
|
+
headers: {},
|
|
220
|
+
bodyText: JSON.stringify({
|
|
221
|
+
usage: {
|
|
222
|
+
startDate: 1770623326000,
|
|
223
|
+
endDate: 1772956800000,
|
|
224
|
+
standard: {
|
|
225
|
+
orgTotalTokensUsed: 5000000,
|
|
226
|
+
totalAllowance: 20000000,
|
|
227
|
+
},
|
|
228
|
+
premium: {
|
|
229
|
+
orgTotalTokensUsed: 0,
|
|
230
|
+
totalAllowance: 0,
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
}),
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
const plugin = await loadPlugin()
|
|
237
|
+
const result = plugin.probe(ctx)
|
|
238
|
+
|
|
239
|
+
expect(result.plan).toBe("Pro")
|
|
240
|
+
const standardLine = result.lines.find((line) => line.label === "Standard")
|
|
241
|
+
expect(standardLine).toBeTruthy()
|
|
242
|
+
expect(standardLine.used).toBe(5000000)
|
|
243
|
+
expect(standardLine.limit).toBe(20000000)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("shows premium line when premium allowance > 0", async () => {
|
|
247
|
+
const ctx = makeCtx()
|
|
248
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
249
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
250
|
+
access_token: makeJwt(futureExp),
|
|
251
|
+
refresh_token: "refresh",
|
|
252
|
+
}))
|
|
253
|
+
ctx.host.http.request.mockReturnValue({
|
|
254
|
+
status: 200,
|
|
255
|
+
headers: {},
|
|
256
|
+
bodyText: JSON.stringify({
|
|
257
|
+
usage: {
|
|
258
|
+
startDate: 1770623326000,
|
|
259
|
+
endDate: 1772956800000,
|
|
260
|
+
standard: {
|
|
261
|
+
orgTotalTokensUsed: 10000000,
|
|
262
|
+
totalAllowance: 200000000,
|
|
263
|
+
},
|
|
264
|
+
premium: {
|
|
265
|
+
orgTotalTokensUsed: 1000000,
|
|
266
|
+
totalAllowance: 50000000,
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
}),
|
|
270
|
+
})
|
|
271
|
+
|
|
272
|
+
const plugin = await loadPlugin()
|
|
273
|
+
const result = plugin.probe(ctx)
|
|
274
|
+
|
|
275
|
+
expect(result.plan).toBe("Max")
|
|
276
|
+
const premiumLine = result.lines.find((line) => line.label === "Premium")
|
|
277
|
+
expect(premiumLine).toBeTruthy()
|
|
278
|
+
expect(premiumLine.used).toBe(1000000)
|
|
279
|
+
expect(premiumLine.limit).toBe(50000000)
|
|
280
|
+
})
|
|
281
|
+
|
|
282
|
+
it("omits premium line when premium allowance is 0", async () => {
|
|
283
|
+
const ctx = makeCtx()
|
|
284
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
285
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
286
|
+
access_token: makeJwt(futureExp),
|
|
287
|
+
refresh_token: "refresh",
|
|
288
|
+
}))
|
|
289
|
+
ctx.host.http.request.mockReturnValue({
|
|
290
|
+
status: 200,
|
|
291
|
+
headers: {},
|
|
292
|
+
bodyText: JSON.stringify({
|
|
293
|
+
usage: {
|
|
294
|
+
startDate: 1770623326000,
|
|
295
|
+
endDate: 1772956800000,
|
|
296
|
+
standard: {
|
|
297
|
+
orgTotalTokensUsed: 0,
|
|
298
|
+
totalAllowance: 20000000,
|
|
299
|
+
},
|
|
300
|
+
premium: {
|
|
301
|
+
orgTotalTokensUsed: 0,
|
|
302
|
+
totalAllowance: 0,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
}),
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
const plugin = await loadPlugin()
|
|
309
|
+
const result = plugin.probe(ctx)
|
|
310
|
+
|
|
311
|
+
const premiumLine = result.lines.find((line) => line.label === "Premium")
|
|
312
|
+
expect(premiumLine).toBeUndefined()
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
it("refreshes token when near expiry", async () => {
|
|
316
|
+
const ctx = makeCtx()
|
|
317
|
+
// Token expires in 12 hours (within 24h threshold, needs refresh)
|
|
318
|
+
const nearExp = Math.floor(Date.now() / 1000) + 12 * 60 * 60
|
|
319
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
320
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
321
|
+
access_token: makeJwt(nearExp),
|
|
322
|
+
refresh_token: "refresh",
|
|
323
|
+
}))
|
|
324
|
+
|
|
325
|
+
let refreshCalled = false
|
|
326
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
327
|
+
if (String(opts.url).includes("workos.com")) {
|
|
328
|
+
refreshCalled = true
|
|
329
|
+
return {
|
|
330
|
+
status: 200,
|
|
331
|
+
bodyText: JSON.stringify({
|
|
332
|
+
access_token: makeJwt(futureExp),
|
|
333
|
+
refresh_token: "new-refresh",
|
|
334
|
+
}),
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
// Usage request
|
|
338
|
+
expect(opts.headers.Authorization).toContain("Bearer ")
|
|
339
|
+
return {
|
|
340
|
+
status: 200,
|
|
341
|
+
headers: {},
|
|
342
|
+
bodyText: JSON.stringify({
|
|
343
|
+
usage: {
|
|
344
|
+
startDate: 1770623326000,
|
|
345
|
+
endDate: 1772956800000,
|
|
346
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
347
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
348
|
+
},
|
|
349
|
+
}),
|
|
350
|
+
}
|
|
351
|
+
})
|
|
352
|
+
|
|
353
|
+
const plugin = await loadPlugin()
|
|
354
|
+
plugin.probe(ctx)
|
|
355
|
+
|
|
356
|
+
expect(refreshCalled).toBe(true)
|
|
357
|
+
// Verify auth file was updated
|
|
358
|
+
expect(ctx.host.fs.writeText).toHaveBeenCalled()
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
it("falls back to existing token when proactive refresh throws", async () => {
|
|
362
|
+
const ctx = makeCtx()
|
|
363
|
+
const nearExp = Math.floor(Date.now() / 1000) + 12 * 60 * 60
|
|
364
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
365
|
+
access_token: makeJwt(nearExp),
|
|
366
|
+
refresh_token: "refresh",
|
|
367
|
+
}))
|
|
368
|
+
|
|
369
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
370
|
+
if (String(opts.url).includes("workos.com")) {
|
|
371
|
+
throw new Error("refresh transport error")
|
|
372
|
+
}
|
|
373
|
+
return {
|
|
374
|
+
status: 200,
|
|
375
|
+
headers: {},
|
|
376
|
+
bodyText: JSON.stringify({
|
|
377
|
+
usage: {
|
|
378
|
+
startDate: 1770623326000,
|
|
379
|
+
endDate: 1772956800000,
|
|
380
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
381
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
382
|
+
},
|
|
383
|
+
}),
|
|
384
|
+
}
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
const plugin = await loadPlugin()
|
|
388
|
+
const result = plugin.probe(ctx)
|
|
389
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it("throws session expired when refresh fails with 401", async () => {
|
|
393
|
+
const ctx = makeCtx()
|
|
394
|
+
// Token expired
|
|
395
|
+
const pastExp = Math.floor(Date.now() / 1000) - 1000
|
|
396
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
397
|
+
access_token: makeJwt(pastExp),
|
|
398
|
+
refresh_token: "refresh",
|
|
399
|
+
}))
|
|
400
|
+
ctx.host.http.request.mockReturnValue({ status: 401, headers: {}, bodyText: "{}" })
|
|
401
|
+
|
|
402
|
+
const plugin = await loadPlugin()
|
|
403
|
+
expect(() => plugin.probe(ctx)).toThrow("Session expired")
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
it("throws on http errors", async () => {
|
|
407
|
+
const ctx = makeCtx()
|
|
408
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
409
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
410
|
+
access_token: makeJwt(futureExp),
|
|
411
|
+
refresh_token: "refresh",
|
|
412
|
+
}))
|
|
413
|
+
ctx.host.http.request.mockReturnValue({ status: 500, headers: {}, bodyText: "" })
|
|
414
|
+
|
|
415
|
+
const plugin = await loadPlugin()
|
|
416
|
+
expect(() => plugin.probe(ctx)).toThrow("HTTP 500")
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
it("throws on invalid usage response", async () => {
|
|
420
|
+
const ctx = makeCtx()
|
|
421
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
422
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
423
|
+
access_token: makeJwt(futureExp),
|
|
424
|
+
refresh_token: "refresh",
|
|
425
|
+
}))
|
|
426
|
+
ctx.host.http.request.mockReturnValue({ status: 200, headers: {}, bodyText: "bad json" })
|
|
427
|
+
|
|
428
|
+
const plugin = await loadPlugin()
|
|
429
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage response invalid")
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it("throws when usage response missing usage object", async () => {
|
|
433
|
+
const ctx = makeCtx()
|
|
434
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
435
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
436
|
+
access_token: makeJwt(futureExp),
|
|
437
|
+
refresh_token: "refresh",
|
|
438
|
+
}))
|
|
439
|
+
ctx.host.http.request.mockReturnValue({ status: 200, headers: {}, bodyText: JSON.stringify({}) })
|
|
440
|
+
|
|
441
|
+
const plugin = await loadPlugin()
|
|
442
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage response missing data")
|
|
443
|
+
})
|
|
444
|
+
|
|
445
|
+
it("returns no usage data badge when standard is missing", async () => {
|
|
446
|
+
const ctx = makeCtx()
|
|
447
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
448
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
449
|
+
access_token: makeJwt(futureExp),
|
|
450
|
+
refresh_token: "refresh",
|
|
451
|
+
}))
|
|
452
|
+
ctx.host.http.request.mockReturnValue({
|
|
453
|
+
status: 200,
|
|
454
|
+
headers: {},
|
|
455
|
+
bodyText: JSON.stringify({
|
|
456
|
+
usage: {
|
|
457
|
+
startDate: 1770623326000,
|
|
458
|
+
endDate: 1772956800000,
|
|
459
|
+
},
|
|
460
|
+
}),
|
|
461
|
+
})
|
|
462
|
+
|
|
463
|
+
const plugin = await loadPlugin()
|
|
464
|
+
const result = plugin.probe(ctx)
|
|
465
|
+
|
|
466
|
+
expect(result.lines[0].label).toBe("Status")
|
|
467
|
+
expect(result.lines[0].text).toBe("No usage data")
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
it("throws on usage request failures", async () => {
|
|
471
|
+
const ctx = makeCtx()
|
|
472
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
473
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
474
|
+
access_token: makeJwt(futureExp),
|
|
475
|
+
refresh_token: "refresh",
|
|
476
|
+
}))
|
|
477
|
+
ctx.host.http.request.mockImplementation(() => {
|
|
478
|
+
throw new Error("network error")
|
|
479
|
+
})
|
|
480
|
+
|
|
481
|
+
const plugin = await loadPlugin()
|
|
482
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage request failed")
|
|
483
|
+
})
|
|
484
|
+
|
|
485
|
+
it("throws specific error when post-refresh usage request fails", async () => {
|
|
486
|
+
const ctx = makeCtx()
|
|
487
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
488
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
489
|
+
access_token: makeJwt(futureExp),
|
|
490
|
+
refresh_token: "refresh",
|
|
491
|
+
}))
|
|
492
|
+
|
|
493
|
+
let usageCalls = 0
|
|
494
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
495
|
+
if (String(opts.url).includes("workos.com")) {
|
|
496
|
+
return {
|
|
497
|
+
status: 200,
|
|
498
|
+
bodyText: JSON.stringify({
|
|
499
|
+
access_token: makeJwt(futureExp),
|
|
500
|
+
refresh_token: "new-refresh",
|
|
501
|
+
}),
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
usageCalls++
|
|
505
|
+
if (usageCalls === 1) return { status: 401, headers: {}, bodyText: "" }
|
|
506
|
+
throw new Error("network after refresh")
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
const plugin = await loadPlugin()
|
|
510
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage request failed after refresh")
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
it("throws generic usage request failure when retry helper throws", async () => {
|
|
514
|
+
const ctx = makeCtx()
|
|
515
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
516
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
517
|
+
access_token: makeJwt(futureExp),
|
|
518
|
+
refresh_token: "refresh",
|
|
519
|
+
}))
|
|
520
|
+
ctx.util.retryOnceOnAuth = () => {
|
|
521
|
+
throw new Error("unexpected retry helper error")
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const plugin = await loadPlugin()
|
|
525
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage request failed. Check your connection.")
|
|
526
|
+
})
|
|
527
|
+
|
|
528
|
+
it("retries on 401 and succeeds after refresh", async () => {
|
|
529
|
+
const ctx = makeCtx()
|
|
530
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
531
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
532
|
+
access_token: makeJwt(futureExp),
|
|
533
|
+
refresh_token: "refresh",
|
|
534
|
+
}))
|
|
535
|
+
|
|
536
|
+
let usageCalls = 0
|
|
537
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
538
|
+
if (String(opts.url).includes("workos.com")) {
|
|
539
|
+
return {
|
|
540
|
+
status: 200,
|
|
541
|
+
bodyText: JSON.stringify({
|
|
542
|
+
access_token: makeJwt(futureExp),
|
|
543
|
+
refresh_token: "new-refresh",
|
|
544
|
+
}),
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
usageCalls++
|
|
548
|
+
if (usageCalls === 1) {
|
|
549
|
+
return { status: 401, headers: {}, bodyText: "" }
|
|
550
|
+
}
|
|
551
|
+
return {
|
|
552
|
+
status: 200,
|
|
553
|
+
headers: {},
|
|
554
|
+
bodyText: JSON.stringify({
|
|
555
|
+
usage: {
|
|
556
|
+
startDate: 1770623326000,
|
|
557
|
+
endDate: 1772956800000,
|
|
558
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
559
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
560
|
+
},
|
|
561
|
+
}),
|
|
562
|
+
}
|
|
563
|
+
})
|
|
564
|
+
|
|
565
|
+
const plugin = await loadPlugin()
|
|
566
|
+
const result = plugin.probe(ctx)
|
|
567
|
+
|
|
568
|
+
expect(usageCalls).toBe(2)
|
|
569
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
570
|
+
})
|
|
571
|
+
|
|
572
|
+
it("throws token expired after retry still fails", async () => {
|
|
573
|
+
const ctx = makeCtx()
|
|
574
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
575
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
576
|
+
access_token: makeJwt(futureExp),
|
|
577
|
+
refresh_token: "refresh",
|
|
578
|
+
}))
|
|
579
|
+
|
|
580
|
+
let usageCalls = 0
|
|
581
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
582
|
+
if (String(opts.url).includes("workos.com")) {
|
|
583
|
+
return {
|
|
584
|
+
status: 200,
|
|
585
|
+
bodyText: JSON.stringify({
|
|
586
|
+
access_token: makeJwt(futureExp),
|
|
587
|
+
refresh_token: "new-refresh",
|
|
588
|
+
}),
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
usageCalls++
|
|
592
|
+
return { status: 403, headers: {}, bodyText: "" }
|
|
593
|
+
})
|
|
594
|
+
|
|
595
|
+
const plugin = await loadPlugin()
|
|
596
|
+
expect(() => plugin.probe(ctx)).toThrow("Token expired")
|
|
597
|
+
})
|
|
598
|
+
|
|
599
|
+
it("infers Basic plan from low allowance", async () => {
|
|
600
|
+
const ctx = makeCtx()
|
|
601
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
602
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
603
|
+
access_token: makeJwt(futureExp),
|
|
604
|
+
refresh_token: "refresh",
|
|
605
|
+
}))
|
|
606
|
+
ctx.host.http.request.mockReturnValue({
|
|
607
|
+
status: 200,
|
|
608
|
+
headers: {},
|
|
609
|
+
bodyText: JSON.stringify({
|
|
610
|
+
usage: {
|
|
611
|
+
startDate: 1770623326000,
|
|
612
|
+
endDate: 1772956800000,
|
|
613
|
+
standard: {
|
|
614
|
+
orgTotalTokensUsed: 0,
|
|
615
|
+
totalAllowance: 1000000,
|
|
616
|
+
},
|
|
617
|
+
premium: {
|
|
618
|
+
orgTotalTokensUsed: 0,
|
|
619
|
+
totalAllowance: 0,
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
}),
|
|
623
|
+
})
|
|
624
|
+
|
|
625
|
+
const plugin = await loadPlugin()
|
|
626
|
+
const result = plugin.probe(ctx)
|
|
627
|
+
|
|
628
|
+
expect(result.plan).toBe("Basic")
|
|
629
|
+
})
|
|
630
|
+
|
|
631
|
+
it("includes resetsAt and periodDurationMs from usage dates", async () => {
|
|
632
|
+
const ctx = makeCtx()
|
|
633
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
634
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
635
|
+
access_token: makeJwt(futureExp),
|
|
636
|
+
refresh_token: "refresh",
|
|
637
|
+
}))
|
|
638
|
+
const startDate = 1770623326000
|
|
639
|
+
const endDate = 1772956800000
|
|
640
|
+
ctx.host.http.request.mockReturnValue({
|
|
641
|
+
status: 200,
|
|
642
|
+
headers: {},
|
|
643
|
+
bodyText: JSON.stringify({
|
|
644
|
+
usage: {
|
|
645
|
+
startDate,
|
|
646
|
+
endDate,
|
|
647
|
+
standard: {
|
|
648
|
+
orgTotalTokensUsed: 0,
|
|
649
|
+
totalAllowance: 20000000,
|
|
650
|
+
},
|
|
651
|
+
premium: {
|
|
652
|
+
orgTotalTokensUsed: 0,
|
|
653
|
+
totalAllowance: 0,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
}),
|
|
657
|
+
})
|
|
658
|
+
|
|
659
|
+
const plugin = await loadPlugin()
|
|
660
|
+
const result = plugin.probe(ctx)
|
|
661
|
+
|
|
662
|
+
const standardLine = result.lines.find((line) => line.label === "Standard")
|
|
663
|
+
expect(standardLine.resetsAt).toBeTruthy()
|
|
664
|
+
expect(standardLine.periodDurationMs).toBe(endDate - startDate)
|
|
665
|
+
})
|
|
666
|
+
|
|
667
|
+
it("loads direct JWT auth payloads from plain text and quoted JSON strings", async () => {
|
|
668
|
+
const jwt = "header.payload.signature"
|
|
669
|
+
|
|
670
|
+
const runCase = async (rawAuth) => {
|
|
671
|
+
const ctx = makeCtx()
|
|
672
|
+
ctx.host.fs.writeText("~/.factory/auth.json", rawAuth)
|
|
673
|
+
ctx.host.http.request.mockReturnValue({
|
|
674
|
+
status: 200,
|
|
675
|
+
headers: {},
|
|
676
|
+
bodyText: JSON.stringify({
|
|
677
|
+
usage: {
|
|
678
|
+
startDate: 1770623326000,
|
|
679
|
+
endDate: 1772956800000,
|
|
680
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
681
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
682
|
+
},
|
|
683
|
+
}),
|
|
684
|
+
})
|
|
685
|
+
|
|
686
|
+
delete globalThis.__openusage_plugin
|
|
687
|
+
vi.resetModules()
|
|
688
|
+
const plugin = await loadPlugin()
|
|
689
|
+
const result = plugin.probe(ctx)
|
|
690
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
await runCase(jwt)
|
|
694
|
+
await runCase(JSON.stringify(jwt))
|
|
695
|
+
})
|
|
696
|
+
|
|
697
|
+
it("supports uppercase 0X-prefixed hex payload without TextDecoder", async () => {
|
|
698
|
+
const ctx = makeCtx()
|
|
699
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
700
|
+
const payload = JSON.stringify({ access_token: makeJwt(futureExp), refresh_token: "refresh" })
|
|
701
|
+
const hexPayload = "0X" + Buffer.from(payload, "utf8").toString("hex").toUpperCase()
|
|
702
|
+
ctx.host.keychain.readGenericPassword.mockImplementation((service) => {
|
|
703
|
+
if (service === "Factory Token") return hexPayload
|
|
704
|
+
return null
|
|
705
|
+
})
|
|
706
|
+
const originalTextDecoder = globalThis.TextDecoder
|
|
707
|
+
globalThis.TextDecoder = undefined
|
|
708
|
+
try {
|
|
709
|
+
ctx.host.http.request.mockReturnValue({
|
|
710
|
+
status: 200,
|
|
711
|
+
headers: {},
|
|
712
|
+
bodyText: JSON.stringify({
|
|
713
|
+
usage: {
|
|
714
|
+
startDate: 1770623326000,
|
|
715
|
+
endDate: 1772956800000,
|
|
716
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
717
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
718
|
+
},
|
|
719
|
+
}),
|
|
720
|
+
})
|
|
721
|
+
const plugin = await loadPlugin()
|
|
722
|
+
const result = plugin.probe(ctx)
|
|
723
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
724
|
+
} finally {
|
|
725
|
+
globalThis.TextDecoder = originalTextDecoder
|
|
726
|
+
}
|
|
727
|
+
})
|
|
728
|
+
|
|
729
|
+
it("throws when keychain API is unavailable and files are missing", async () => {
|
|
730
|
+
const ctx = makeCtx()
|
|
731
|
+
ctx.host.keychain = null
|
|
732
|
+
const plugin = await loadPlugin()
|
|
733
|
+
expect(() => plugin.probe(ctx)).toThrow("Not logged in")
|
|
734
|
+
})
|
|
735
|
+
|
|
736
|
+
it("continues with existing token when refresh cannot produce a new token", async () => {
|
|
737
|
+
const nearExp = Math.floor(Date.now() / 1000) + 12 * 60 * 60
|
|
738
|
+
const baseAuth = JSON.stringify({
|
|
739
|
+
access_token: makeJwt(nearExp),
|
|
740
|
+
refresh_token: "refresh",
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
const runCase = async (refreshResp) => {
|
|
744
|
+
const ctx = makeCtx()
|
|
745
|
+
ctx.host.fs.writeText("~/.factory/auth.json", baseAuth)
|
|
746
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
747
|
+
if (String(opts.url).includes("workos.com")) return refreshResp
|
|
748
|
+
return {
|
|
749
|
+
status: 200,
|
|
750
|
+
headers: {},
|
|
751
|
+
bodyText: JSON.stringify({
|
|
752
|
+
usage: {
|
|
753
|
+
startDate: 1770623326000,
|
|
754
|
+
endDate: 1772956800000,
|
|
755
|
+
standard: { orgTotalTokensUsed: 0, totalAllowance: 20000000 },
|
|
756
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
757
|
+
},
|
|
758
|
+
}),
|
|
759
|
+
}
|
|
760
|
+
})
|
|
761
|
+
|
|
762
|
+
delete globalThis.__openusage_plugin
|
|
763
|
+
vi.resetModules()
|
|
764
|
+
const plugin = await loadPlugin()
|
|
765
|
+
const result = plugin.probe(ctx)
|
|
766
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
await runCase({ status: 500, headers: {}, bodyText: "" })
|
|
770
|
+
await runCase({ status: 200, headers: {}, bodyText: "not-json" })
|
|
771
|
+
await runCase({ status: 200, headers: {}, bodyText: JSON.stringify({}) })
|
|
772
|
+
})
|
|
773
|
+
|
|
774
|
+
it("skips refresh when refresh token is missing and uses existing access token", async () => {
|
|
775
|
+
const ctx = makeCtx()
|
|
776
|
+
const nearExp = Math.floor(Date.now() / 1000) + 12 * 60 * 60
|
|
777
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
778
|
+
access_token: makeJwt(nearExp),
|
|
779
|
+
}))
|
|
780
|
+
ctx.host.http.request.mockReturnValue({
|
|
781
|
+
status: 200,
|
|
782
|
+
headers: {},
|
|
783
|
+
bodyText: JSON.stringify({
|
|
784
|
+
usage: {
|
|
785
|
+
startDate: 1770623326000,
|
|
786
|
+
endDate: 1772956800000,
|
|
787
|
+
standard: { orgTotalTokensUsed: 1, totalAllowance: 20000000 },
|
|
788
|
+
premium: { orgTotalTokensUsed: 0, totalAllowance: 0 },
|
|
789
|
+
},
|
|
790
|
+
}),
|
|
791
|
+
})
|
|
792
|
+
|
|
793
|
+
const plugin = await loadPlugin()
|
|
794
|
+
const result = plugin.probe(ctx)
|
|
795
|
+
expect(result.lines.find((line) => line.label === "Standard")).toBeTruthy()
|
|
796
|
+
})
|
|
797
|
+
|
|
798
|
+
it("handles usage dates and counters when optional values are missing", async () => {
|
|
799
|
+
const ctx = makeCtx()
|
|
800
|
+
const futureExp = Math.floor(Date.now() / 1000) + 7 * 24 * 60 * 60
|
|
801
|
+
ctx.host.fs.writeText("~/.factory/auth.json", JSON.stringify({
|
|
802
|
+
access_token: makeJwt(futureExp),
|
|
803
|
+
refresh_token: "refresh",
|
|
804
|
+
}))
|
|
805
|
+
ctx.host.http.request.mockReturnValue({
|
|
806
|
+
status: 200,
|
|
807
|
+
headers: {},
|
|
808
|
+
bodyText: JSON.stringify({
|
|
809
|
+
usage: {
|
|
810
|
+
startDate: "n/a",
|
|
811
|
+
endDate: "n/a",
|
|
812
|
+
standard: {
|
|
813
|
+
// Missing orgTotalTokensUsed should fall back to 0
|
|
814
|
+
totalAllowance: 0,
|
|
815
|
+
},
|
|
816
|
+
premium: {
|
|
817
|
+
orgTotalTokensUsed: 0,
|
|
818
|
+
totalAllowance: 0,
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
}),
|
|
822
|
+
})
|
|
823
|
+
|
|
824
|
+
const plugin = await loadPlugin()
|
|
825
|
+
const result = plugin.probe(ctx)
|
|
826
|
+
const standardLine = result.lines.find((line) => line.label === "Standard")
|
|
827
|
+
expect(standardLine).toBeTruthy()
|
|
828
|
+
expect(standardLine.used).toBe(0)
|
|
829
|
+
expect(standardLine.resetsAt).toBeUndefined()
|
|
830
|
+
expect(standardLine.periodDurationMs).toBeUndefined()
|
|
831
|
+
expect(result.plan).toBeNull()
|
|
832
|
+
})
|
|
833
|
+
})
|