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,156 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const BASE_URL = "https://api.z.ai"
|
|
3
|
+
const SUBSCRIPTION_URL = BASE_URL + "/api/biz/subscription/list"
|
|
4
|
+
const QUOTA_URL = BASE_URL + "/api/monitor/usage/quota/limit"
|
|
5
|
+
const PERIOD_MS = 5 * 60 * 60 * 1000
|
|
6
|
+
const MONTH_MS = 30 * 24 * 60 * 60 * 1000
|
|
7
|
+
|
|
8
|
+
function loadApiKey(ctx) {
|
|
9
|
+
const zai = ctx.host.env.get("ZAI_API_KEY")
|
|
10
|
+
if (typeof zai === "string" && zai.trim()) return zai.trim()
|
|
11
|
+
|
|
12
|
+
const glm = ctx.host.env.get("GLM_API_KEY")
|
|
13
|
+
if (typeof glm === "string" && glm.trim()) return glm.trim()
|
|
14
|
+
|
|
15
|
+
return null
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function fetchSubscription(ctx, apiKey) {
|
|
19
|
+
try {
|
|
20
|
+
const resp = ctx.util.request({
|
|
21
|
+
method: "GET",
|
|
22
|
+
url: SUBSCRIPTION_URL,
|
|
23
|
+
headers: {
|
|
24
|
+
Authorization: "Bearer " + apiKey,
|
|
25
|
+
Accept: "application/json",
|
|
26
|
+
},
|
|
27
|
+
timeoutMs: 10000,
|
|
28
|
+
})
|
|
29
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
30
|
+
ctx.host.log.warn("subscription request failed: HTTP " + resp.status)
|
|
31
|
+
return null
|
|
32
|
+
}
|
|
33
|
+
const data = ctx.util.tryParseJson(resp.bodyText)
|
|
34
|
+
if (!data) return null
|
|
35
|
+
const list = data.data
|
|
36
|
+
if (!Array.isArray(list) || list.length === 0) return null
|
|
37
|
+
return {
|
|
38
|
+
productName: list[0].productName || null,
|
|
39
|
+
nextRenewTime: list[0].nextRenewTime || null,
|
|
40
|
+
}
|
|
41
|
+
} catch (e) {
|
|
42
|
+
ctx.host.log.warn("subscription request exception: " + String(e))
|
|
43
|
+
return null
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function fetchQuota(ctx, apiKey) {
|
|
48
|
+
let resp
|
|
49
|
+
try {
|
|
50
|
+
resp = ctx.util.request({
|
|
51
|
+
method: "GET",
|
|
52
|
+
url: QUOTA_URL,
|
|
53
|
+
headers: {
|
|
54
|
+
Authorization: "Bearer " + apiKey,
|
|
55
|
+
Accept: "application/json",
|
|
56
|
+
},
|
|
57
|
+
timeoutMs: 10000,
|
|
58
|
+
})
|
|
59
|
+
} catch (e) {
|
|
60
|
+
ctx.host.log.error("usage request exception: " + String(e))
|
|
61
|
+
throw "Usage request failed. Check your connection."
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (ctx.util.isAuthStatus(resp.status)) {
|
|
65
|
+
throw "API key invalid. Check your Z.ai API key."
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (resp.status < 200 || resp.status >= 300) {
|
|
69
|
+
throw "Usage request failed (HTTP " + String(resp.status) + "). Try again later."
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const data = ctx.util.tryParseJson(resp.bodyText)
|
|
73
|
+
if (!data) {
|
|
74
|
+
throw "Usage response invalid. Try again later."
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return data
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findLimit(limits, type) {
|
|
81
|
+
for (let i = 0; i < limits.length; i++) {
|
|
82
|
+
if (limits[i].type === type || limits[i].name === type) return limits[i]
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function probe(ctx) {
|
|
88
|
+
const apiKey = loadApiKey(ctx)
|
|
89
|
+
if (!apiKey) {
|
|
90
|
+
throw "No ZAI_API_KEY found. Set up environment variable first."
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const sub = fetchSubscription(ctx, apiKey)
|
|
94
|
+
const plan = sub && sub.productName ? ctx.fmt.planLabel(sub.productName) : null
|
|
95
|
+
|
|
96
|
+
const quota = fetchQuota(ctx, apiKey)
|
|
97
|
+
const lines = []
|
|
98
|
+
|
|
99
|
+
const container = quota.data || quota
|
|
100
|
+
const limits = container.limits || container
|
|
101
|
+
if (!Array.isArray(limits) || limits.length === 0) {
|
|
102
|
+
lines.push(ctx.line.badge({ label: "Session", text: "No usage data", color: "#a3a3a3" }))
|
|
103
|
+
return { plan, lines }
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const tokenLimit = findLimit(limits, "TOKENS_LIMIT")
|
|
107
|
+
|
|
108
|
+
if (!tokenLimit) {
|
|
109
|
+
lines.push(ctx.line.badge({ label: "Session", text: "No usage data", color: "#a3a3a3" }))
|
|
110
|
+
return { plan, lines }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const used = typeof tokenLimit.percentage === "number" ? tokenLimit.percentage : 0
|
|
114
|
+
const resetsAt = tokenLimit.nextResetTime ? ctx.util.toIso(tokenLimit.nextResetTime) : undefined
|
|
115
|
+
|
|
116
|
+
const progressOpts = {
|
|
117
|
+
label: "Session",
|
|
118
|
+
used,
|
|
119
|
+
limit: 100,
|
|
120
|
+
format: { kind: "percent" },
|
|
121
|
+
periodDurationMs: PERIOD_MS,
|
|
122
|
+
}
|
|
123
|
+
if (resetsAt) {
|
|
124
|
+
progressOpts.resetsAt = resetsAt
|
|
125
|
+
}
|
|
126
|
+
lines.push(ctx.line.progress(progressOpts))
|
|
127
|
+
|
|
128
|
+
const timeLimit = findLimit(limits, "TIME_LIMIT")
|
|
129
|
+
|
|
130
|
+
if (timeLimit) {
|
|
131
|
+
const webUsed = typeof timeLimit.currentValue === "number" ? timeLimit.currentValue : 0
|
|
132
|
+
const webTotal = typeof timeLimit.usage === "number" ? timeLimit.usage : 0
|
|
133
|
+
const now = new Date()
|
|
134
|
+
const nextMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))
|
|
135
|
+
const webResetsAt = timeLimit.nextResetTime
|
|
136
|
+
? ctx.util.toIso(timeLimit.nextResetTime)
|
|
137
|
+
: nextMonth.toISOString()
|
|
138
|
+
|
|
139
|
+
const webOpts = {
|
|
140
|
+
label: "Web Searches",
|
|
141
|
+
used: webUsed,
|
|
142
|
+
limit: webTotal,
|
|
143
|
+
format: { kind: "count", suffix: "/ " + webTotal },
|
|
144
|
+
periodDurationMs: MONTH_MS,
|
|
145
|
+
}
|
|
146
|
+
if (webResetsAt) {
|
|
147
|
+
webOpts.resetsAt = webResetsAt
|
|
148
|
+
}
|
|
149
|
+
lines.push(ctx.line.progress(webOpts))
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return { plan, lines }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
globalThis.__openusage_plugin = { id: "zai", probe }
|
|
156
|
+
})()
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"schemaVersion": 1,
|
|
3
|
+
"id": "zai",
|
|
4
|
+
"name": "Z.ai",
|
|
5
|
+
"version": "0.0.1",
|
|
6
|
+
"entry": "plugin.js",
|
|
7
|
+
"icon": "icon.svg",
|
|
8
|
+
"brandColor": "#2D2D2D",
|
|
9
|
+
"cli": {
|
|
10
|
+
"category": "env",
|
|
11
|
+
"envVarNames": ["ZAI_API_KEY", "GLM_API_KEY"],
|
|
12
|
+
"envKeyLabel": "Z.ai API Key"
|
|
13
|
+
},
|
|
14
|
+
"lines": [
|
|
15
|
+
{ "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 },
|
|
16
|
+
{ "type": "progress", "label": "Web Searches", "scope": "overview" }
|
|
17
|
+
]
|
|
18
|
+
}
|
|
@@ -0,0 +1,396 @@
|
|
|
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
|
+
const mockEnvWithKey = (ctx, key, varName = "ZAI_API_KEY") => {
|
|
10
|
+
ctx.host.env.get.mockImplementation((name) => (name === varName ? key : null))
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const QUOTA_RESPONSE = {
|
|
14
|
+
code: 200,
|
|
15
|
+
data: {
|
|
16
|
+
limits: [
|
|
17
|
+
{
|
|
18
|
+
type: "TOKENS_LIMIT",
|
|
19
|
+
usage: 800000000,
|
|
20
|
+
currentValue: 1900000,
|
|
21
|
+
percentage: 10,
|
|
22
|
+
nextResetTime: 1738368000000,
|
|
23
|
+
unit: 3,
|
|
24
|
+
number: 5,
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
type: "TIME_LIMIT",
|
|
28
|
+
usage: 4000,
|
|
29
|
+
currentValue: 1095,
|
|
30
|
+
percentage: 27,
|
|
31
|
+
remaining: 2905,
|
|
32
|
+
usageDetails: [
|
|
33
|
+
{ modelCode: "search-prime", usage: 951 },
|
|
34
|
+
{ modelCode: "web-reader", usage: 211 },
|
|
35
|
+
{ modelCode: "zread", usage: 0 },
|
|
36
|
+
],
|
|
37
|
+
unit: 5,
|
|
38
|
+
number: 1,
|
|
39
|
+
},
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const QUOTA_RESPONSE_NO_TIME_LIMIT = {
|
|
45
|
+
code: 200,
|
|
46
|
+
data: {
|
|
47
|
+
limits: [
|
|
48
|
+
{
|
|
49
|
+
type: "TOKENS_LIMIT",
|
|
50
|
+
usage: 800000000,
|
|
51
|
+
currentValue: 1900000,
|
|
52
|
+
percentage: 10,
|
|
53
|
+
nextResetTime: 1738368000000,
|
|
54
|
+
unit: 3,
|
|
55
|
+
number: 5,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const SUBSCRIPTION_RESPONSE = {
|
|
62
|
+
data: [{ productName: "GLM Coding Max", nextRenewTime: "2026-03-12" }],
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const mockHttp = (ctx) => {
|
|
66
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
67
|
+
if (opts.url.includes("subscription")) {
|
|
68
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
69
|
+
}
|
|
70
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE) }
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
describe("zai plugin", () => {
|
|
75
|
+
beforeEach(() => {
|
|
76
|
+
delete globalThis.__openusage_plugin
|
|
77
|
+
vi.resetModules()
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it("throws when no env vars set", async () => {
|
|
81
|
+
const ctx = makeCtx()
|
|
82
|
+
const plugin = await loadPlugin()
|
|
83
|
+
expect(() => plugin.probe(ctx)).toThrow("No ZAI_API_KEY found. Set up environment variable first.")
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it("uses ZAI_API_KEY when set", async () => {
|
|
87
|
+
const ctx = makeCtx()
|
|
88
|
+
mockEnvWithKey(ctx, "test-key")
|
|
89
|
+
mockHttp(ctx)
|
|
90
|
+
|
|
91
|
+
const plugin = await loadPlugin()
|
|
92
|
+
const result = plugin.probe(ctx)
|
|
93
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it("falls back to GLM_API_KEY when ZAI_API_KEY is missing", async () => {
|
|
97
|
+
const ctx = makeCtx()
|
|
98
|
+
mockEnvWithKey(ctx, "glm-key", "GLM_API_KEY")
|
|
99
|
+
mockHttp(ctx)
|
|
100
|
+
|
|
101
|
+
const plugin = await loadPlugin()
|
|
102
|
+
const result = plugin.probe(ctx)
|
|
103
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it("prefers ZAI_API_KEY over GLM_API_KEY", async () => {
|
|
107
|
+
const ctx = makeCtx()
|
|
108
|
+
ctx.host.env.get.mockImplementation((name) => {
|
|
109
|
+
if (name === "ZAI_API_KEY") return "zai-key"
|
|
110
|
+
if (name === "GLM_API_KEY") return "glm-key"
|
|
111
|
+
return null
|
|
112
|
+
})
|
|
113
|
+
mockHttp(ctx)
|
|
114
|
+
|
|
115
|
+
const plugin = await loadPlugin()
|
|
116
|
+
const result = plugin.probe(ctx)
|
|
117
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
118
|
+
const authHeader = ctx.host.http.request.mock.calls[0][0].headers.Authorization
|
|
119
|
+
expect(authHeader).toBe("Bearer zai-key")
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it("renders session usage as percent from quota response", async () => {
|
|
123
|
+
const ctx = makeCtx()
|
|
124
|
+
mockEnvWithKey(ctx, "test-key")
|
|
125
|
+
mockHttp(ctx)
|
|
126
|
+
|
|
127
|
+
const plugin = await loadPlugin()
|
|
128
|
+
const result = plugin.probe(ctx)
|
|
129
|
+
const line = result.lines.find((l) => l.label === "Session")
|
|
130
|
+
expect(line).toBeTruthy()
|
|
131
|
+
expect(line.type).toBe("progress")
|
|
132
|
+
expect(line.used).toBe(10)
|
|
133
|
+
expect(line.limit).toBe(100)
|
|
134
|
+
expect(line.format).toEqual({ kind: "percent" })
|
|
135
|
+
expect(line.periodDurationMs).toBe(5 * 60 * 60 * 1000)
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
it("extracts plan name from subscription response", async () => {
|
|
139
|
+
const ctx = makeCtx()
|
|
140
|
+
mockEnvWithKey(ctx, "test-key")
|
|
141
|
+
mockHttp(ctx)
|
|
142
|
+
|
|
143
|
+
const plugin = await loadPlugin()
|
|
144
|
+
const result = plugin.probe(ctx)
|
|
145
|
+
expect(result.plan).toBe("GLM Coding Max")
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it("handles subscription fetch failure gracefully", async () => {
|
|
149
|
+
const ctx = makeCtx()
|
|
150
|
+
mockEnvWithKey(ctx, "test-key")
|
|
151
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
152
|
+
if (opts.url.includes("subscription")) {
|
|
153
|
+
return { status: 500, bodyText: "" }
|
|
154
|
+
}
|
|
155
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE) }
|
|
156
|
+
})
|
|
157
|
+
|
|
158
|
+
const plugin = await loadPlugin()
|
|
159
|
+
const result = plugin.probe(ctx)
|
|
160
|
+
expect(result.plan).toBeNull()
|
|
161
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it("throws on 401 from quota endpoint", async () => {
|
|
165
|
+
const ctx = makeCtx()
|
|
166
|
+
mockEnvWithKey(ctx, "test-key")
|
|
167
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
168
|
+
if (opts.url.includes("subscription")) {
|
|
169
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
170
|
+
}
|
|
171
|
+
return { status: 401, bodyText: "" }
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const plugin = await loadPlugin()
|
|
175
|
+
expect(() => plugin.probe(ctx)).toThrow("API key invalid")
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it("throws on HTTP 500 from quota endpoint", async () => {
|
|
179
|
+
const ctx = makeCtx()
|
|
180
|
+
mockEnvWithKey(ctx, "test-key")
|
|
181
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
182
|
+
if (opts.url.includes("subscription")) {
|
|
183
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
184
|
+
}
|
|
185
|
+
return { status: 500, bodyText: "" }
|
|
186
|
+
})
|
|
187
|
+
|
|
188
|
+
const plugin = await loadPlugin()
|
|
189
|
+
expect(() => plugin.probe(ctx)).toThrow("HTTP 500")
|
|
190
|
+
})
|
|
191
|
+
|
|
192
|
+
it("throws on network exception", async () => {
|
|
193
|
+
const ctx = makeCtx()
|
|
194
|
+
mockEnvWithKey(ctx, "test-key")
|
|
195
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
196
|
+
if (opts.url.includes("subscription")) {
|
|
197
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
198
|
+
}
|
|
199
|
+
throw new Error("ECONNREFUSED")
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const plugin = await loadPlugin()
|
|
203
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage request failed. Check your connection.")
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it("throws on invalid JSON from quota endpoint", async () => {
|
|
207
|
+
const ctx = makeCtx()
|
|
208
|
+
mockEnvWithKey(ctx, "test-key")
|
|
209
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
210
|
+
if (opts.url.includes("subscription")) {
|
|
211
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
212
|
+
}
|
|
213
|
+
return { status: 200, bodyText: "not-json" }
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
const plugin = await loadPlugin()
|
|
217
|
+
expect(() => plugin.probe(ctx)).toThrow("Usage response invalid")
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it("shows badge when limits array is empty", async () => {
|
|
221
|
+
const ctx = makeCtx()
|
|
222
|
+
mockEnvWithKey(ctx, "test-key")
|
|
223
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
224
|
+
if (opts.url.includes("subscription")) {
|
|
225
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
226
|
+
}
|
|
227
|
+
return { status: 200, bodyText: JSON.stringify({ data: { limits: [] } }) }
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
const plugin = await loadPlugin()
|
|
231
|
+
const result = plugin.probe(ctx)
|
|
232
|
+
expect(result.lines[0].text).toBe("No usage data")
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
it("passes resetsAt from nextResetTime (epoch ms to ISO)", async () => {
|
|
236
|
+
const ctx = makeCtx()
|
|
237
|
+
mockEnvWithKey(ctx, "test-key")
|
|
238
|
+
mockHttp(ctx)
|
|
239
|
+
|
|
240
|
+
const plugin = await loadPlugin()
|
|
241
|
+
const result = plugin.probe(ctx)
|
|
242
|
+
const line = result.lines.find((l) => l.label === "Session")
|
|
243
|
+
expect(line.resetsAt).toBe(new Date(1738368000000).toISOString())
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it("renders Web Searches line with count format and 1st-of-month reset", async () => {
|
|
247
|
+
const ctx = makeCtx()
|
|
248
|
+
mockEnvWithKey(ctx, "test-key")
|
|
249
|
+
mockHttp(ctx)
|
|
250
|
+
|
|
251
|
+
const plugin = await loadPlugin()
|
|
252
|
+
const result = plugin.probe(ctx)
|
|
253
|
+
const line = result.lines.find((l) => l.label === "Web Searches")
|
|
254
|
+
expect(line).toBeTruthy()
|
|
255
|
+
expect(line.type).toBe("progress")
|
|
256
|
+
expect(line.used).toBe(1095)
|
|
257
|
+
expect(line.limit).toBe(4000)
|
|
258
|
+
expect(line.format).toEqual({ kind: "count", suffix: "/ 4000" })
|
|
259
|
+
expect(line.periodDurationMs).toBe(30 * 24 * 60 * 60 * 1000)
|
|
260
|
+
const now = new Date()
|
|
261
|
+
const expected1st = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))
|
|
262
|
+
expect(line.resetsAt).toBe(expected1st.toISOString())
|
|
263
|
+
})
|
|
264
|
+
|
|
265
|
+
it("skips Web Searches when TIME_LIMIT is absent", async () => {
|
|
266
|
+
const ctx = makeCtx()
|
|
267
|
+
mockEnvWithKey(ctx, "test-key")
|
|
268
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
269
|
+
if (opts.url.includes("subscription")) {
|
|
270
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
271
|
+
}
|
|
272
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE_NO_TIME_LIMIT) }
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
const plugin = await loadPlugin()
|
|
276
|
+
const result = plugin.probe(ctx)
|
|
277
|
+
expect(result.lines.find((l) => l.label === "Web Searches")).toBeUndefined()
|
|
278
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it("Web Searches still has resetsAt (1st of month) even when subscription fails", async () => {
|
|
282
|
+
const ctx = makeCtx()
|
|
283
|
+
mockEnvWithKey(ctx, "test-key")
|
|
284
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
285
|
+
if (opts.url.includes("subscription")) {
|
|
286
|
+
return { status: 500, bodyText: "" }
|
|
287
|
+
}
|
|
288
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE) }
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
const plugin = await loadPlugin()
|
|
292
|
+
const result = plugin.probe(ctx)
|
|
293
|
+
const line = result.lines.find((l) => l.label === "Web Searches")
|
|
294
|
+
expect(line).toBeTruthy()
|
|
295
|
+
const now = new Date()
|
|
296
|
+
const expected1st = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() + 1, 1))
|
|
297
|
+
expect(line.resetsAt).toBe(expected1st.toISOString())
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it("handles missing nextResetTime gracefully", async () => {
|
|
301
|
+
const ctx = makeCtx()
|
|
302
|
+
mockEnvWithKey(ctx, "test-key")
|
|
303
|
+
const quotaNoReset = {
|
|
304
|
+
data: {
|
|
305
|
+
limits: [
|
|
306
|
+
{ type: "TOKENS_LIMIT", percentage: 10 },
|
|
307
|
+
],
|
|
308
|
+
},
|
|
309
|
+
}
|
|
310
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
311
|
+
if (opts.url.includes("subscription")) {
|
|
312
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
313
|
+
}
|
|
314
|
+
return { status: 200, bodyText: JSON.stringify(quotaNoReset) }
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
const plugin = await loadPlugin()
|
|
318
|
+
const result = plugin.probe(ctx)
|
|
319
|
+
const line = result.lines.find((l) => l.label === "Session")
|
|
320
|
+
expect(line).toBeTruthy()
|
|
321
|
+
expect(line.resetsAt).toBeUndefined()
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
it("handles invalid subscription JSON without failing quota rendering", async () => {
|
|
325
|
+
const ctx = makeCtx()
|
|
326
|
+
mockEnvWithKey(ctx, "test-key")
|
|
327
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
328
|
+
if (opts.url.includes("subscription")) {
|
|
329
|
+
return { status: 200, bodyText: "not-json" }
|
|
330
|
+
}
|
|
331
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE) }
|
|
332
|
+
})
|
|
333
|
+
|
|
334
|
+
const plugin = await loadPlugin()
|
|
335
|
+
const result = plugin.probe(ctx)
|
|
336
|
+
expect(result.plan).toBeNull()
|
|
337
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
338
|
+
})
|
|
339
|
+
|
|
340
|
+
it("handles subscription payload with empty list", async () => {
|
|
341
|
+
const ctx = makeCtx()
|
|
342
|
+
mockEnvWithKey(ctx, "test-key")
|
|
343
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
344
|
+
if (opts.url.includes("subscription")) {
|
|
345
|
+
return { status: 200, bodyText: JSON.stringify({ data: [] }) }
|
|
346
|
+
}
|
|
347
|
+
return { status: 200, bodyText: JSON.stringify(QUOTA_RESPONSE) }
|
|
348
|
+
})
|
|
349
|
+
|
|
350
|
+
const plugin = await loadPlugin()
|
|
351
|
+
const result = plugin.probe(ctx)
|
|
352
|
+
expect(result.plan).toBeNull()
|
|
353
|
+
expect(result.lines.find((l) => l.label === "Session")).toBeTruthy()
|
|
354
|
+
})
|
|
355
|
+
|
|
356
|
+
it("supports quota payloads where limits are top-level and optional fields are non-numeric", async () => {
|
|
357
|
+
const ctx = makeCtx()
|
|
358
|
+
mockEnvWithKey(ctx, "test-key")
|
|
359
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
360
|
+
if (opts.url.includes("subscription")) {
|
|
361
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
362
|
+
}
|
|
363
|
+
return {
|
|
364
|
+
status: 200,
|
|
365
|
+
bodyText: JSON.stringify([
|
|
366
|
+
{ type: "TOKENS_LIMIT", percentage: "10", nextResetTime: 1738368000000 },
|
|
367
|
+
{ type: "TIME_LIMIT", currentValue: "1095", usage: "4000" },
|
|
368
|
+
]),
|
|
369
|
+
}
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
const plugin = await loadPlugin()
|
|
373
|
+
const result = plugin.probe(ctx)
|
|
374
|
+
const session = result.lines.find((l) => l.label === "Session")
|
|
375
|
+
const web = result.lines.find((l) => l.label === "Web Searches")
|
|
376
|
+
expect(session.used).toBe(0)
|
|
377
|
+
expect(web.used).toBe(0)
|
|
378
|
+
expect(web.limit).toBe(0)
|
|
379
|
+
})
|
|
380
|
+
|
|
381
|
+
it("shows no-usage badge when token limit entry is missing", async () => {
|
|
382
|
+
const ctx = makeCtx()
|
|
383
|
+
mockEnvWithKey(ctx, "test-key")
|
|
384
|
+
ctx.host.http.request.mockImplementation((opts) => {
|
|
385
|
+
if (opts.url.includes("subscription")) {
|
|
386
|
+
return { status: 200, bodyText: JSON.stringify(SUBSCRIPTION_RESPONSE) }
|
|
387
|
+
}
|
|
388
|
+
return { status: 200, bodyText: JSON.stringify({ data: { limits: [{ type: "TIME_LIMIT", usage: 10 }] } }) }
|
|
389
|
+
})
|
|
390
|
+
|
|
391
|
+
const plugin = await loadPlugin()
|
|
392
|
+
const result = plugin.probe(ctx)
|
|
393
|
+
expect(result.lines).toHaveLength(1)
|
|
394
|
+
expect(result.lines[0].text).toBe("No usage data")
|
|
395
|
+
})
|
|
396
|
+
})
|