Skip to content

Commit 752a79e

Browse files
author
sonicg
committed
fix(usage): handle missing token plan reset times
1 parent 80bdcb8 commit 752a79e

3 files changed

Lines changed: 85 additions & 26 deletions

File tree

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

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,41 @@ const PROGRESS_WIDTH = 32;
77

88
interface TokenPlanUsage {
99
per5HourPercentage: number;
10-
per5HourResetTime: number;
10+
per5HourResetTime?: number;
1111
per1WeekPercentage: number;
12-
per1WeekResetTime: number;
12+
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+
1727
const usage = {
1828
per5HourPercentage: response.per5HourPercentage,
1929
per5HourResetTime: response.per5HourResetTime,
2030
per1WeekPercentage: response.per1WeekPercentage,
2131
per1WeekResetTime: response.per1WeekResetTime,
2232
};
2333

24-
if (!Object.values(usage).every((value) => typeof value === "number" && Number.isFinite(value))) {
34+
const resetTimes = [
35+
[usage.per5HourPercentage, usage.per5HourResetTime],
36+
[usage.per1WeekPercentage, usage.per1WeekResetTime],
37+
];
38+
const hasValidResetTimes = resetTimes.every(
39+
([percentage, resetTime]) =>
40+
(percentage === 0 && resetTime === undefined) ||
41+
(typeof resetTime === "number" && Number.isFinite(resetTime)),
42+
);
43+
44+
if (!hasValidResetTimes) {
2545
throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL);
2646
}
2747

@@ -81,18 +101,22 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void {
81101
const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`));
82102
process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`);
83103
};
84-
const writeQuota = (label: string, percentage: number, resetTime: number) => {
104+
const writeQuota = (label: string, percentage: number, resetTime: number | undefined) => {
85105
const percentageText = formatPercentage(percentage);
86106
const bar = progressBar(percentage);
87107
const style = progressStyle(percentage, color.green, color.yellow, color.red);
88108
writeLine(color.bold(label), label);
89109
writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`);
90-
writeLine(
91-
color.dim(
92-
`Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`,
93-
),
94-
`Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`,
95-
);
110+
if (resetTime === undefined) {
111+
writeLine(
112+
color.dim("Resets: not applicable (no usage yet)"),
113+
"Resets: not applicable (no usage yet)",
114+
);
115+
return;
116+
}
117+
118+
const resetText = `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`;
119+
writeLine(color.dim(resetText), resetText);
96120
};
97121

98122
process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`);

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

Lines changed: 5 additions & 9 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);
@@ -66,15 +66,11 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
6666
per1WeekResetTime?: number;
6767
}>(result.stdout);
6868
expect(data.per5HourPercentage).toBeTypeOf("number");
69-
expect(data.per5HourResetTime).toBeTypeOf("number");
7069
expect(data.per1WeekPercentage).toBeTypeOf("number");
71-
expect(data.per1WeekResetTime).toBeTypeOf("number");
72-
expect(Object.keys(data).sort()).toEqual([
73-
"per1WeekPercentage",
74-
"per1WeekResetTime",
75-
"per5HourPercentage",
76-
"per5HourResetTime",
77-
]);
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");
7874
});
7975

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

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

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,22 @@ afterEach(() => {
1212
vi.restoreAllMocks();
1313
});
1414

15-
function makeUsageResponse(percentage: number): Record<string, unknown> {
15+
function makeUsageResponse(
16+
per5HourPercentage: number,
17+
per1WeekPercentage = per5HourPercentage,
18+
): 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;
25+
1626
return {
1727
data: {
1828
DataV2: {
1929
data: {
20-
data: {
21-
per5HourPercentage: percentage,
22-
per5HourResetTime: 1_786_000_000_000,
23-
per1WeekPercentage: percentage,
24-
per1WeekResetTime: 1_786_100_000_000,
25-
},
30+
data: usage,
2631
},
2732
},
2833
},
@@ -51,4 +56,38 @@ describe("usage token-plan view", () => {
5156

5257
expect(output.join("")).toContain(`\u001B[${colorCode}m[`);
5358
});
59+
60+
test("accepts missing reset times when the quota usage is zero", async () => {
61+
const output: string[] = [];
62+
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
63+
output.push(String(chunk));
64+
return true;
65+
});
66+
67+
await tokenPlanUsage.run({
68+
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0)) },
69+
flags: { json: false, view: true },
70+
settings: { dryRun: false },
71+
} as never);
72+
73+
expect(output.join("")).toContain("Resets: not applicable (no usage yet)");
74+
});
75+
76+
test("allows one unused quota window without masking another reset time", async () => {
77+
const output: string[] = [];
78+
vi.spyOn(process.stdout, "write").mockImplementation((chunk) => {
79+
output.push(String(chunk));
80+
return true;
81+
});
82+
83+
await tokenPlanUsage.run({
84+
client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0, 0.5)) },
85+
flags: { json: false, view: true },
86+
settings: { dryRun: false },
87+
} as never);
88+
89+
const renderedOutput = output.join("");
90+
expect(renderedOutput).toContain("Resets: not applicable (no usage yet)");
91+
expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/);
92+
});
5493
});

0 commit comments

Comments
 (0)