Skip to content

Commit 24092b4

Browse files
author
sonicg
committed
fix(usage): handle unavailable token plan quotas
1 parent 752a79e commit 24092b4

3 files changed

Lines changed: 110 additions & 35 deletions

File tree

packages/commands/src/commands/usage/token-plan.ts

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -6,42 +6,35 @@ const BOX_WIDTH = 76;
66
const PROGRESS_WIDTH = 32;
77

88
interface TokenPlanUsage {
9-
per5HourPercentage: number;
9+
per5HourPercentage?: number;
1010
per5HourResetTime?: number;
11-
per1WeekPercentage: number;
11+
per1WeekPercentage?: number;
1212
per1WeekResetTime?: number;
1313
}
1414

1515
function readUsage(result: unknown): TokenPlanUsage {
1616
const response = unwrapResponse(result as Record<string, unknown>);
17-
const percentages = [response.per5HourPercentage, response.per1WeekPercentage];
18-
19-
if (
20-
!percentages.every(
21-
(percentage) => typeof percentage === "number" && Number.isFinite(percentage),
22-
)
23-
) {
24-
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
25-
}
26-
2717
const usage = {
2818
per5HourPercentage: response.per5HourPercentage,
2919
per5HourResetTime: response.per5HourResetTime,
3020
per1WeekPercentage: response.per1WeekPercentage,
3121
per1WeekResetTime: response.per1WeekResetTime,
3222
};
3323

34-
const resetTimes = [
24+
const quotas = [
3525
[usage.per5HourPercentage, usage.per5HourResetTime],
3626
[usage.per1WeekPercentage, usage.per1WeekResetTime],
3727
];
38-
const hasValidResetTimes = resetTimes.every(
28+
const hasValidQuotas = quotas.every(
3929
([percentage, resetTime]) =>
40-
(percentage === 0 && resetTime === undefined) ||
41-
(typeof resetTime === "number" && Number.isFinite(resetTime)),
30+
(percentage === undefined && resetTime === undefined) ||
31+
(typeof percentage === "number" &&
32+
Number.isFinite(percentage) &&
33+
((percentage === 0 && resetTime === undefined) ||
34+
(typeof resetTime === "number" && Number.isFinite(resetTime)))),
4235
);
4336

44-
if (!hasValidResetTimes) {
37+
if (!hasValidQuotas) {
4538
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
4639
}
4740

@@ -101,11 +94,21 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void {
10194
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`));
10295
process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`);
10396
};
104-
const writeQuota = (label: string, percentage: number, resetTime: number | undefined) => {
97+
const writeQuota = (
98+
label: string,
99+
unlimitedMessage: string,
100+
percentage: number | undefined,
101+
resetTime: number | undefined,
102+
) => {
103+
writeLine(color.bold(label), label);
104+
if (percentage === undefined) {
105+
writeLine(color.dim(unlimitedMessage), unlimitedMessage);
106+
return;
107+
}
108+
105109
const percentageText = formatPercentage(percentage);
106110
const bar = progressBar(percentage);
107111
const style = progressStyle(percentage, color.green, color.yellow, color.red);
108-
writeLine(color.bold(label), label);
109112
writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`);
110113
if (resetTime === undefined) {
111114
writeLine(
@@ -124,9 +127,19 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void {
124127
const generatedAtText = `Generated at: ${formatDateTime(generatedAt)} (local time)`;
125128
writeLine(color.dim(generatedAtText), generatedAtText);
126129
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
127-
writeQuota("5-hour quota", usage.per5HourPercentage, usage.per5HourResetTime);
130+
writeQuota(
131+
"5-hour quota",
132+
"5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
133+
usage.per5HourPercentage,
134+
usage.per5HourResetTime,
135+
);
128136
process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`);
129-
writeQuota("1-week quota", usage.per1WeekPercentage, usage.per1WeekResetTime);
137+
writeQuota(
138+
"1-week quota",
139+
"1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。",
140+
usage.per1WeekPercentage,
141+
usage.per1WeekResetTime,
142+
);
130143
process.stdout.write(`└${"─".repeat(BOX_WIDTH)}┘\n`);
131144
}
132145

packages/commands/tests/e2e/usage-token-plan.e2e.test.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
5555
expect(data.data).toEqual({});
5656
});
5757

58-
test("usage token-plan --json 返回百分比与可用的重置时间", async () => {
58+
test("usage token-plan --json 返回可用的额度字段", async () => {
5959
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--json"]);
6060
if (isConsoleAuthFailure(result)) return;
6161
expect(result.exitCode, result.stderr).toBe(0);
@@ -65,12 +65,18 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
6565
per1WeekPercentage?: number;
6666
per1WeekResetTime?: number;
6767
}>(result.stdout);
68-
expect(data.per5HourPercentage).toBeTypeOf("number");
69-
expect(data.per1WeekPercentage).toBeTypeOf("number");
70-
if (data.per5HourPercentage === 0) expect(data.per5HourResetTime).toBeUndefined();
71-
else expect(data.per5HourResetTime).toBeTypeOf("number");
72-
if (data.per1WeekPercentage === 0) expect(data.per1WeekResetTime).toBeUndefined();
73-
else expect(data.per1WeekResetTime).toBeTypeOf("number");
68+
const quotas = [
69+
[data.per5HourPercentage, data.per5HourResetTime],
70+
[data.per1WeekPercentage, data.per1WeekResetTime],
71+
];
72+
for (const [percentage, resetTime] of quotas) {
73+
if (percentage === undefined) expect(resetTime).toBeUndefined();
74+
else if (percentage === 0) expect(resetTime).toBeUndefined();
75+
else {
76+
expect(percentage).toBeTypeOf("number");
77+
expect(resetTime).toBeTypeOf("number");
78+
}
79+
}
7480
});
7581

7682
test("usage token-plan --view 渲染生成时间与两个额度窗口", async () => {

packages/commands/tests/token-plan-usage.test.ts

Lines changed: 63 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,15 +13,18 @@ afterEach(() => {
1313
});
1414

1515
function makeUsageResponse(
16-
per5HourPercentage: number,
16+
per5HourPercentage?: number,
1717
per1WeekPercentage = per5HourPercentage,
1818
): Record<string, unknown> {
19-
const usage: Record<string, number> = {
20-
per5HourPercentage,
21-
per1WeekPercentage,
22-
};
23-
if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000;
24-
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
19+
const usage: Record<string, number> = {};
20+
if (per5HourPercentage !== undefined) {
21+
usage.per5HourPercentage = per5HourPercentage;
22+
if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000;
23+
}
24+
if (per1WeekPercentage !== undefined) {
25+
usage.per1WeekPercentage = per1WeekPercentage;
26+
if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000;
27+
}
2528

2629
return {
2730
data: {
@@ -90,4 +93,57 @@ describe("usage token-plan view", () => {
9093
expect(renderedOutput).toContain("Resets: not applicable (no usage yet)");
9194
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
9295
});
96+
97+
test("renders missing quota windows as possibly unlimited", async () => {
98+
const output: string[] = [];
99+
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
100+
output.push(String(chunk));
101+
return true;
102+
});
103+
104+
await tokenPlanUsage.run({
105+
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
106+
flags: { json: false, view: true },
107+
settings: { dryRun: false },
108+
} as never);
109+
110+
const renderedOutput = output.join("");
111+
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
112+
expect(renderedOutput).toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
113+
});
114+
115+
test("renders only the missing quota window as possibly unlimited", async () => {
116+
const output: string[] = [];
117+
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
118+
output.push(String(chunk));
119+
return true;
120+
});
121+
122+
await tokenPlanUsage.run({
123+
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(undefined, 0.5)) },
124+
flags: { json: false, view: true },
125+
settings: { dryRun: false },
126+
} as never);
127+
128+
const renderedOutput = output.join("");
129+
expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
130+
expect(renderedOutput).not.toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。");
131+
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
132+
});
133+
134+
test("returns an empty JSON object when no quota fields are available", async () => {
135+
const output: string[] = [];
136+
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
137+
output.push(String(chunk));
138+
return true;
139+
});
140+
141+
await tokenPlanUsage.run({
142+
client: { console: vi.fn().mockResolvedValue(makeUsageResponse()) },
143+
flags: { json: true, view: false },
144+
settings: { dryRun: false },
145+
} as never);
146+
147+
expect(output.join("").trim()).toBe("{}");
148+
});
93149
});

0 commit comments

Comments
 (0)