gentle-pi 3.2.0 → 3.2.1
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/assets/orchestrator-delegation.md +13 -8
- package/assets/orchestrator.md +2 -2
- package/docs/gentle-shell.md +15 -5
- package/docs/readme-reference.md +6 -6
- package/docs/review-integration.md +15 -6
- package/extensions/gentle-ai.ts +72 -7
- package/extensions/gentle-shell.ts +29 -7
- package/lib/model-routing-authority.ts +1 -1
- package/lib/native-review-cli.ts +9 -0
- package/lib/opaque-pi-reviewer-adapter.ts +130 -10
- package/lib/review-host-relay.ts +95 -2
- package/lib/shell-bar.ts +63 -9
- package/lib/shell-usage.ts +226 -10
- package/package.json +2 -1
- package/runtime/native-review-cli.mjs +9 -0
- package/scripts/gentle-ai-installer.mjs +10 -10
- package/scripts/mirror-odd-routing.mjs +242 -0
- package/scripts/verify-package-files.mjs +3 -3
- package/tests/gentle-ai-binary.test.ts +1 -1
- package/tests/gentle-ai-installer.test.ts +47 -47
- package/tests/gentle-shell.test.ts +109 -3
- package/tests/native-review-capability-contract.test.ts +14 -1
- package/tests/odd-routing-canonical-ratchet.test.ts +293 -0
- package/tests/odd-routing-contract.test.ts +57 -0
- package/tests/opaque-pi-reviewer-adapter.test.ts +153 -9
- package/tests/package-manifest.test.ts +6 -6
- package/tests/review-controller-native-routing.test.ts +60 -1
- package/tests/review-host-relay.test.ts +83 -14
- package/tests/review-relay-transport-agent.test.ts +86 -1
- package/tests/shell-bar.test.ts +153 -3
- package/tests/shell-usage-view.test.ts +3 -2
- package/tests/shell-usage.test.ts +254 -6
|
@@ -6,13 +6,17 @@ import {
|
|
|
6
6
|
formatReset,
|
|
7
7
|
parseAnthropicHeaders,
|
|
8
8
|
parseCodexHeaders,
|
|
9
|
+
parseNanQuota,
|
|
9
10
|
parseUsageHeaders,
|
|
10
11
|
parseCodexUsage,
|
|
12
|
+
providerNote,
|
|
11
13
|
renderUsageBar,
|
|
12
14
|
renderUsagePanel,
|
|
15
|
+
SUPPORTED_USAGE_PROVIDERS,
|
|
13
16
|
UsageStore,
|
|
14
17
|
windowLabel,
|
|
15
18
|
type ProviderUsage,
|
|
19
|
+
type UsageWindow,
|
|
16
20
|
} from "../lib/shell-usage.ts";
|
|
17
21
|
|
|
18
22
|
// Subscription usage: what each connected provider says about its windows.
|
|
@@ -134,11 +138,11 @@ test("renderUsagePanel lists each provider with meters, resets, and a stale mark
|
|
|
134
138
|
const lines = renderUsagePanel([usage], plainTheme, 70, NOW + 3 * 60_000);
|
|
135
139
|
for (const line of lines) assert.ok(visibleWidth(line) <= 70, `too wide: ${line}`);
|
|
136
140
|
assert.match(lines[0], /^openai-codex · pro · updated 3m ago$/);
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
assert.match(lines[
|
|
140
|
-
assert.match(lines[
|
|
141
|
-
assert.match(lines[
|
|
141
|
+
// One row per window: the name, its meter, its percentage and, when the window
|
|
142
|
+
// reports one, its reset, all on the same line.
|
|
143
|
+
assert.match(lines[1], /^ {2}codex week +[▰▱]{16} +40% · resets in 2d 1h$/);
|
|
144
|
+
assert.match(lines[2], /^ {2}codex_spark 5h +[▰▱]{16} +12% · resets in \d+h \d+m$/);
|
|
145
|
+
assert.match(lines[3], /^ {2}codex_spark week +[▰▱]{16} +3% · resets in \d+d \d+h$/);
|
|
142
146
|
assert.deepEqual(renderUsagePanel([], plainTheme, 120, NOW), ["No subscription usage yet. Usage arrives with the next response, or press r to fetch it."]);
|
|
143
147
|
});
|
|
144
148
|
|
|
@@ -148,7 +152,7 @@ test("renderUsagePanel puts the active provider first and explains missing data"
|
|
|
148
152
|
assert.ok(claude);
|
|
149
153
|
const both = renderUsagePanel([codex, claude], plainTheme, 100, NOW, { provider: "anthropic" });
|
|
150
154
|
assert.match(both[0], /^✿ anthropic · updated just now$/);
|
|
151
|
-
assert.match(both[1], /^ {2}claude
|
|
155
|
+
assert.match(both[1], /^ {2}claude 5h +[▰▱]{16} +20%$/);
|
|
152
156
|
assert.match(both.find((line) => line.startsWith("openai-codex")) ?? "", /^openai-codex · pro/);
|
|
153
157
|
|
|
154
158
|
const apiKey = renderUsagePanel([codex], plainTheme, 100, NOW, { provider: "openai" });
|
|
@@ -160,6 +164,250 @@ test("renderUsagePanel puts the active provider first and explains missing data"
|
|
|
160
164
|
assert.deepEqual(renderUsagePanel([], plainTheme, 100, NOW, { provider: "openai-codex" }), ["✿ openai-codex · no usage yet · r to fetch"]);
|
|
161
165
|
});
|
|
162
166
|
|
|
167
|
+
// NaN Cloud quota: per-model allowances for the billing period, plus the
|
|
168
|
+
// rolling window the model reports. Percentages are tokensUsed over cap, the
|
|
169
|
+
// same ratio the dashboard draws. The payload shape was read off the official
|
|
170
|
+
// dashboard bundle, not a published schema, so every field stays optional.
|
|
171
|
+
const NAN_QUOTA = {
|
|
172
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
173
|
+
models: [
|
|
174
|
+
{
|
|
175
|
+
model: "glm5.3",
|
|
176
|
+
cap: 3_000_000_000,
|
|
177
|
+
fullCap: 3_000_000_000,
|
|
178
|
+
tokensUsed: 820_000_000,
|
|
179
|
+
windowHours: 4,
|
|
180
|
+
windowTokens: 400_000_000,
|
|
181
|
+
windowTokensUsed: 120_000_000,
|
|
182
|
+
windowResetsAt: 1_788_620_161,
|
|
183
|
+
email: "someone@example.com",
|
|
184
|
+
},
|
|
185
|
+
{ model: "deepseek-v4-flash", cap: 1_500_000_000, tokensUsed: 150_000_000 },
|
|
186
|
+
{ model: "qwen3.8-flash", cap: 0, tokensUsed: 10 },
|
|
187
|
+
],
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// The billing-period window carries no label: the model id in front of it names
|
|
191
|
+
// the allowance, and the reset text says what the window is. Only a sub-window
|
|
192
|
+
// on top (a rolling `4h`) needs a name, so `windowText` spells the unlabeled one
|
|
193
|
+
// out for the assertions below.
|
|
194
|
+
function windowText(window: UsageWindow): string {
|
|
195
|
+
return `${window.label === "" ? "period" : window.label}:${window.usedPercent}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
test("parseNanQuota maps each model allowance and its rolling window", () => {
|
|
199
|
+
const usage = parseNanQuota(NAN_QUOTA, NOW);
|
|
200
|
+
assert.equal(usage.provider, "nan");
|
|
201
|
+
assert.equal(usage.plan, undefined);
|
|
202
|
+
assert.equal(usage.fetchedAt, NOW);
|
|
203
|
+
assert.deepEqual(usage.limits.map((limit) => limit.name), ["glm5.3", "deepseek-v4-flash"]);
|
|
204
|
+
|
|
205
|
+
const [glm, deepseek] = usage.limits;
|
|
206
|
+
assert.deepEqual(glm.windows.map((window) => window.label), ["", "4h"]);
|
|
207
|
+
assert.equal(glm.windows[0].usedPercent, (820_000_000 / 3_000_000_000) * 100);
|
|
208
|
+
assert.equal(glm.windows[0].windowSeconds, 2_212_800);
|
|
209
|
+
assert.equal(glm.windows[0].resetAt, 1_790_812_800_000);
|
|
210
|
+
assert.equal(glm.windows[1].usedPercent, 30);
|
|
211
|
+
assert.equal(glm.windows[1].windowSeconds, 14_400);
|
|
212
|
+
assert.equal(glm.windows[1].resetAt, 1_788_620_161_000);
|
|
213
|
+
assert.equal(glm.limitReached, false);
|
|
214
|
+
assert.deepEqual(deepseek.windows.map(windowText), ["period:10"]);
|
|
215
|
+
assert.equal(JSON.stringify(usage).includes("example.com"), false, "the quota parser must not keep unrelated account fields");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("parseNanQuota falls back to the top-level period end and defaults the window budget", () => {
|
|
219
|
+
const topLevel = parseNanQuota({ periodEnd: 1_790_812_800, models: [{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 0 }] }, NOW);
|
|
220
|
+
assert.equal(topLevel.limits[0].windows[0].resetAt, 1_790_812_800_000);
|
|
221
|
+
|
|
222
|
+
const defaulted = parseNanQuota({ models: [{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 0, windowTokensUsed: 100_000_000 }] }, NOW);
|
|
223
|
+
assert.deepEqual(defaulted.limits[0].windows.map(windowText), ["period:0", "4h:25"]);
|
|
224
|
+
|
|
225
|
+
const overCap = parseNanQuota({ models: [{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 3_000_000_000, windowHours: 12, windowTokensUsed: 60_000_000 }] }, NOW);
|
|
226
|
+
assert.deepEqual(overCap.limits[0].windows.map(windowText), ["period:100", "12h:15"]);
|
|
227
|
+
assert.equal(overCap.limits[0].limitReached, true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
test("parseNanQuota degrades to no data instead of throwing", () => {
|
|
231
|
+
assert.deepEqual(parseNanQuota({}, NOW).limits, []);
|
|
232
|
+
assert.deepEqual(parseNanQuota(undefined, NOW).limits, []);
|
|
233
|
+
assert.deepEqual(parseNanQuota({ models: "nope" }, NOW).limits, []);
|
|
234
|
+
assert.deepEqual(parseNanQuota({ models: [null, "glm5.3", 7] }, NOW).limits, []);
|
|
235
|
+
assert.deepEqual(parseNanQuota({ models: [{ model: "glm5.3", cap: "3000000000", tokensUsed: 1 }] }, NOW).limits, []);
|
|
236
|
+
assert.deepEqual(parseNanQuota({ models: [{ model: "glm5.3", cap: 3_000_000_000 }] }, NOW).limits, []);
|
|
237
|
+
assert.deepEqual(parseNanQuota({ models: [{ model: "", cap: 3_000_000_000, tokensUsed: 1 }] }, NOW).limits, []);
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("nan is a supported usage provider with its own pending note", () => {
|
|
241
|
+
assert.ok(SUPPORTED_USAGE_PROVIDERS.includes("nan"));
|
|
242
|
+
assert.equal(providerNote("nan"), "no usage yet · r to fetch");
|
|
243
|
+
assert.deepEqual(renderUsagePanel([], plainTheme, 100, NOW, { provider: "nan" }), ["✿ nan · no usage yet · r to fetch"]);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("renderUsagePanel lists each NaN model allowance with its reset on the same row", () => {
|
|
247
|
+
const usage = parseNanQuota(NAN_QUOTA, NOW);
|
|
248
|
+
const lines = renderUsagePanel([usage], plainTheme, 80, NOW, { provider: "nan" });
|
|
249
|
+
for (const line of lines) assert.ok(visibleWidth(line) <= 80, `too wide: ${line}`);
|
|
250
|
+
assert.match(lines[0], /^✿ nan · updated just now$/);
|
|
251
|
+
assert.match(lines[1], /^ {2}glm5\.3 +[▰▱]{16} +27% · resets in \d+d \d+h$/);
|
|
252
|
+
// The rolling window a model reports is a row of its own, with its own reset.
|
|
253
|
+
assert.match(lines[2], /^ {2}glm5\.3 4h +[▰▱]{16} +30% · resets in \d+h \d+m$/);
|
|
254
|
+
assert.match(lines[3], /^ {2}deepseek-v4-flash +[▰▱]{16} +10% · resets in \d+d \d+h$/);
|
|
255
|
+
assert.equal(lines.some((line) => line.includes("total")), false, "an aggregate row only costs space");
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
// The server picks the order of the per-model allowances, so drawing the first
|
|
259
|
+
// one showed DeepSeek's meter inside a GLM session. The bar follows the model
|
|
260
|
+
// the session actually uses, and falls back to the account total when that
|
|
261
|
+
// model holds no allowance of its own.
|
|
262
|
+
test("renderUsageBar prefers the active model allowance over the payload order", () => {
|
|
263
|
+
const usage = parseNanQuota(NAN_QUOTA, NOW);
|
|
264
|
+
assert.equal(renderUsageBar(usage, plainTheme, "deepseek-v4-flash"), "deepseek-v4-flash ▰▱▱▱▱▱▱▱ 10%");
|
|
265
|
+
assert.equal(renderUsageBar(usage, plainTheme, "glm5.3"), "glm5.3 ▰▰▱▱▱▱▱▱ 27% · 4h 30%");
|
|
266
|
+
assert.equal(renderUsageBar(usage, plainTheme), "glm5.3 ▰▰▱▱▱▱▱▱ 27% · 4h 30%", "without an active model the first limit still wins");
|
|
267
|
+
assert.equal(renderUsageBar(usage, plainTheme, "gemma4"), "nan total ▰▰▱▱▱▱▱▱ 22%", "an unmetered model reports the account, never another model");
|
|
268
|
+
assert.equal(renderUsageBar(usage, plainTheme, "qwen3.8-flash"), "nan total ▰▰▱▱▱▱▱▱ 22%", "a model the payload skips holds no allowance either");
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("renderUsageBar prefers the active model's family before the account total", () => {
|
|
272
|
+
const usage = parseNanQuota({
|
|
273
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
274
|
+
models: [
|
|
275
|
+
{ model: "glm5.3-flash", cap: 2_000_000_000, tokensUsed: 200_000_000 },
|
|
276
|
+
{ model: "deepseek-v4-flash", cap: 4_000_000_000, tokensUsed: 200_000_000 },
|
|
277
|
+
],
|
|
278
|
+
}, NOW);
|
|
279
|
+
// The session model holds no allowance of its own and its family has exactly
|
|
280
|
+
// one member: the family is still a closer name for the meter than the whole
|
|
281
|
+
// account, so the ladder visits it before the account rung.
|
|
282
|
+
assert.match(renderUsageBar(usage, plainTheme, "glm5.3-turbo") ?? "", /^glm total ▰▱▱▱▱▱▱▱ 10%$/);
|
|
283
|
+
assert.match(renderUsageBar(usage, plainTheme, "gemma4") ?? "", /^nan total ▰▱▱▱▱▱▱▱ 7%$/, "no member of the family means the account is the only honest name");
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("a single metered allowance still takes the family and account names", () => {
|
|
287
|
+
const usage = parseNanQuota({
|
|
288
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
289
|
+
models: [
|
|
290
|
+
{ model: "glm5.3-flash", cap: 2_000_000_000, tokensUsed: 400_000_000 },
|
|
291
|
+
{ model: "gemma4", cap: 0 },
|
|
292
|
+
],
|
|
293
|
+
}, NOW);
|
|
294
|
+
// One metered model is still a payload that carries raw allowances, so the bar
|
|
295
|
+
// names the allowance the session draws from instead of listing whichever
|
|
296
|
+
// model the payload happened to report.
|
|
297
|
+
assert.deepEqual(usage.limits.map((limit) => limit.name), ["glm5.3-flash"]);
|
|
298
|
+
assert.equal(renderUsageBar(usage, plainTheme, "glm5.3-flash"), "glm5.3-flash ▰▰▱▱▱▱▱▱ 20%");
|
|
299
|
+
assert.match(renderUsageBar(usage, plainTheme, "glm5.4") ?? "", /^glm total ▰▰▱▱▱▱▱▱ 20%$/);
|
|
300
|
+
assert.match(renderUsageBar(usage, plainTheme, "gemma4") ?? "", /^nan total ▰▰▱▱▱▱▱▱ 20%$/);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("renderUsageBar leaves providers without raw allowances on their first limit", () => {
|
|
304
|
+
assert.equal(renderUsageBar(parseCodexUsage(CODEX_PAYLOAD, NOW), plainTheme, "gpt-5.2-codex"), "codex week ▰▰▰▱▱▱▱▱ 40%");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("parseNanQuota weights the period window by the effective allowance", () => {
|
|
308
|
+
const usage = parseNanQuota({
|
|
309
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
310
|
+
models: [
|
|
311
|
+
{ model: "glm5.3-flash", cap: 1_500_000_000, fullCap: 2_000_000_000, tokensUsed: 200_000_000 },
|
|
312
|
+
{ model: "glm5.3", cap: 0, fullCap: 3_000_000_000, tokensUsed: 300_000_000 },
|
|
313
|
+
],
|
|
314
|
+
}, NOW);
|
|
315
|
+
// The dashboard divides by the full-period allowance, not by the prorated cap
|
|
316
|
+
// the current period reports, so the percentages keep matching it.
|
|
317
|
+
assert.deepEqual(usage.limits.map((limit) => limit.name), ["glm5.3-flash", "glm5.3"]);
|
|
318
|
+
const [prorated, noPeriodCap] = usage.limits;
|
|
319
|
+
assert.equal(prorated.windows[0].budget, 2_000_000_000, "fullCap is the denominator when it is positive");
|
|
320
|
+
assert.equal(prorated.windows[0].usedPercent, 10);
|
|
321
|
+
assert.equal(noPeriodCap.windows[0].budget, 3_000_000_000, "a period cap prorated to zero still reports a metered model");
|
|
322
|
+
assert.equal(noPeriodCap.windows[0].usedPercent, 10);
|
|
323
|
+
assert.equal(noPeriodCap.limitReached, false);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("parseNanQuota refuses a payload that hides a metered model's usage", () => {
|
|
327
|
+
// A sibling with a metered allowance whose usage cannot be read is drift, not
|
|
328
|
+
// a model to skip: a partial snapshot would understate every aggregate it
|
|
329
|
+
// feeds, so the read fails whole and the last valid snapshot survives.
|
|
330
|
+
const drifted = parseNanQuota({
|
|
331
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
332
|
+
models: [
|
|
333
|
+
{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 820_000_000 },
|
|
334
|
+
{ model: "deepseek-v4-flash", cap: 2_000_000_000 },
|
|
335
|
+
],
|
|
336
|
+
}, NOW);
|
|
337
|
+
assert.deepEqual(drifted.limits, []);
|
|
338
|
+
// No allowance at all is not drift: the dashboard draws nothing for these
|
|
339
|
+
// models either, and today's real payload carries them.
|
|
340
|
+
const unmetered = parseNanQuota({
|
|
341
|
+
models: [
|
|
342
|
+
{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 820_000_000 },
|
|
343
|
+
{ model: "gemma4", cap: 0 },
|
|
344
|
+
{ model: "qwen3.8-flash", tokensUsed: 10 },
|
|
345
|
+
],
|
|
346
|
+
}, NOW);
|
|
347
|
+
assert.deepEqual(unmetered.limits.map((limit) => limit.name), ["glm5.3"]);
|
|
348
|
+
assert.deepEqual(parseNanQuota({ models: "none" }, NOW).limits, []);
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("parseNanQuota keeps the raw numbers the aggregates are weighted by", () => {
|
|
352
|
+
const [glm] = parseNanQuota(NAN_QUOTA, NOW).limits;
|
|
353
|
+
assert.equal(glm.windows[0].used, 820_000_000);
|
|
354
|
+
assert.equal(glm.windows[0].budget, 3_000_000_000);
|
|
355
|
+
const [codex] = parseCodexUsage(CODEX_PAYLOAD, NOW).limits[0].windows;
|
|
356
|
+
assert.equal(codex.used, undefined, "only NaN reports raw allowance numbers, which is what gates the aggregates");
|
|
357
|
+
assert.equal(codex.budget, undefined);
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
// Grouping is a presentation decision: the panel adds the account total and one
|
|
361
|
+
// row per family with more than one metered model, and each of those rows is an
|
|
362
|
+
// ordinary limit block, the shape Codex already uses for its extra limits.
|
|
363
|
+
const GROUPED_NAN_QUOTA = {
|
|
364
|
+
periodEnd: "2026-10-01T00:00:00.000Z",
|
|
365
|
+
models: [
|
|
366
|
+
{ model: "glm5.3-flash", cap: 2_000_000_000, tokensUsed: 200_000_000 },
|
|
367
|
+
{ model: "glm5.3", cap: 3_000_000_000, tokensUsed: 0, periodEnd: "2026-10-17T05:53:20.000Z" },
|
|
368
|
+
{ model: "glm5.2", cap: 3_000_000_000, tokensUsed: 0, periodEnd: "2026-10-17T05:53:20.000Z" },
|
|
369
|
+
{ model: "deepseek-v4-flash", cap: 3_000_000_000, tokensUsed: 300_000_000 },
|
|
370
|
+
],
|
|
371
|
+
};
|
|
372
|
+
|
|
373
|
+
function panelNames(lines: string[]): string[] {
|
|
374
|
+
return lines.filter(isMeterRow).map((line) => line.trim().replace(/\s*[▰▱].*$/, ""));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// A meter row carries the row name and its gauge on one line; the reset, when the
|
|
378
|
+
// window reports one, is the line underneath.
|
|
379
|
+
function isMeterRow(line: string): boolean {
|
|
380
|
+
return /[▰▱]/.test(line);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// A row carries its own reset on the same line when the window reports one.
|
|
384
|
+
function panelResets(lines: string[]): string[] {
|
|
385
|
+
return lines.filter(isMeterRow).map((line) => /resets in .*$/.exec(line)?.[0] ?? "").filter((reset) => reset.length > 0);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function panelPercents(lines: string[]): number[] {
|
|
389
|
+
return lines.filter(isMeterRow).map((line) => Number.parseInt(line.trim().match(/(\d+)%/)![1] ?? "", 10));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
test("renderUsagePanel orders the NaN allowances by family and prints no totals", () => {
|
|
393
|
+
const lines = renderUsagePanel([parseNanQuota(GROUPED_NAN_QUOTA, NOW)], plainTheme, 80, NOW, { provider: "nan" }).map((line) => line.trimEnd());
|
|
394
|
+
assert.deepEqual(panelNames(lines), ["deepseek-v4-flash", "glm5.3-flash", "glm5.3", "glm5.2"]);
|
|
395
|
+
// 300M of 3B for DeepSeek first, then the GLM family by its own allowance.
|
|
396
|
+
assert.deepEqual(panelPercents(lines), [10, 10, 0, 0]);
|
|
397
|
+
// Every row keeps its own reset, inline: one line per window, never two.
|
|
398
|
+
const resets = panelResets(lines);
|
|
399
|
+
assert.equal(resets.length, 4);
|
|
400
|
+
assert.equal(resets[0], resets[1], "the two models on the 2026-10-01 period share their date");
|
|
401
|
+
assert.equal(resets[2], resets[3], "the two models on the 2026-10-17 period share theirs");
|
|
402
|
+
assert.notEqual(resets[1], resets[2], "each row carries its own window's reset, not its family's");
|
|
403
|
+
});
|
|
404
|
+
|
|
405
|
+
test("renderUsagePanel leaves providers without raw allowances ungrouped", () => {
|
|
406
|
+
const lines = renderUsagePanel([parseCodexUsage(CODEX_PAYLOAD, NOW)], plainTheme, 70, NOW);
|
|
407
|
+
assert.equal(lines.some((line) => line.includes("total")), false);
|
|
408
|
+
assert.match(lines[1], /^ {2}codex week /);
|
|
409
|
+
});
|
|
410
|
+
|
|
163
411
|
test("UsageStore keeps the latest snapshot per provider and lists them in order", () => {
|
|
164
412
|
const store = new UsageStore();
|
|
165
413
|
const first: ProviderUsage = { provider: "openai-codex", plan: "pro", limits: [], fetchedAt: 1 };
|