From 752a79e4422e95a86dcfe512bb83b1b0a7c1bb91 Mon Sep 17 00:00:00 2001 From: sonicg <85807784@qq.com> Date: Thu, 6 Aug 2026 09:12:41 +0800 Subject: [PATCH] fix(usage): handle missing token plan reset times --- .../commands/src/commands/usage/token-plan.ts | 44 +++++++++++---- .../tests/e2e/usage-token-plan.e2e.test.ts | 14 ++--- .../commands/tests/token-plan-usage.test.ts | 53 ++++++++++++++++--- 3 files changed, 85 insertions(+), 26 deletions(-) diff --git a/packages/commands/src/commands/usage/token-plan.ts b/packages/commands/src/commands/usage/token-plan.ts index 968b270..b3ab83d 100644 --- a/packages/commands/src/commands/usage/token-plan.ts +++ b/packages/commands/src/commands/usage/token-plan.ts @@ -7,13 +7,23 @@ const PROGRESS_WIDTH = 32; interface TokenPlanUsage { per5HourPercentage: number; - per5HourResetTime: number; + per5HourResetTime?: number; per1WeekPercentage: number; - per1WeekResetTime: number; + per1WeekResetTime?: number; } function readUsage(result: unknown): TokenPlanUsage { const response = unwrapResponse(result as Record); + const percentages = [response.per5HourPercentage, response.per1WeekPercentage]; + + if ( + !percentages.every( + (percentage) => typeof percentage === "number" && Number.isFinite(percentage), + ) + ) { + throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL); + } + const usage = { per5HourPercentage: response.per5HourPercentage, per5HourResetTime: response.per5HourResetTime, @@ -21,7 +31,17 @@ function readUsage(result: unknown): TokenPlanUsage { per1WeekResetTime: response.per1WeekResetTime, }; - if (!Object.values(usage).every((value) => typeof value === "number" && Number.isFinite(value))) { + const resetTimes = [ + [usage.per5HourPercentage, usage.per5HourResetTime], + [usage.per1WeekPercentage, usage.per1WeekResetTime], + ]; + const hasValidResetTimes = resetTimes.every( + ([percentage, resetTime]) => + (percentage === 0 && resetTime === undefined) || + (typeof resetTime === "number" && Number.isFinite(resetTime)), + ); + + if (!hasValidResetTimes) { throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL); } @@ -81,18 +101,22 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void { const padding = Math.max(0, BOX_WIDTH - displayWidth(` ${visibleContent}`)); process.stdout.write(`│ ${content}${" ".repeat(padding)}│\n`); }; - const writeQuota = (label: string, percentage: number, resetTime: number) => { + const writeQuota = (label: string, percentage: number, resetTime: number | undefined) => { const percentageText = formatPercentage(percentage); const bar = progressBar(percentage); const style = progressStyle(percentage, color.green, color.yellow, color.red); writeLine(color.bold(label), label); writeLine(`${percentageText} used ${style(bar)}`, `${percentageText} used ${bar}`); - writeLine( - color.dim( - `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`, - ), - `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`, - ); + if (resetTime === undefined) { + writeLine( + color.dim("Resets: not applicable (no usage yet)"), + "Resets: not applicable (no usage yet)", + ); + return; + } + + const resetText = `Resets: ${formatDateTime(resetTime)} (in ${formatRemainingTime(resetTime, generatedAt)})`; + writeLine(color.dim(resetText), resetText); }; process.stdout.write(`┌${"─".repeat(BOX_WIDTH)}┐\n`); diff --git a/packages/commands/tests/e2e/usage-token-plan.e2e.test.ts b/packages/commands/tests/e2e/usage-token-plan.e2e.test.ts index 3fb961d..ffa4822 100644 --- a/packages/commands/tests/e2e/usage-token-plan.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-token-plan.e2e.test.ts @@ -55,7 +55,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () = expect(data.data).toEqual({}); }); - test("usage token-plan --json 返回四个核心字段", async () => { + test("usage token-plan --json 返回百分比与可用的重置时间", async () => { const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--json"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); @@ -66,15 +66,11 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () = per1WeekResetTime?: number; }>(result.stdout); expect(data.per5HourPercentage).toBeTypeOf("number"); - expect(data.per5HourResetTime).toBeTypeOf("number"); expect(data.per1WeekPercentage).toBeTypeOf("number"); - expect(data.per1WeekResetTime).toBeTypeOf("number"); - expect(Object.keys(data).sort()).toEqual([ - "per1WeekPercentage", - "per1WeekResetTime", - "per5HourPercentage", - "per5HourResetTime", - ]); + if (data.per5HourPercentage === 0) expect(data.per5HourResetTime).toBeUndefined(); + else expect(data.per5HourResetTime).toBeTypeOf("number"); + if (data.per1WeekPercentage === 0) expect(data.per1WeekResetTime).toBeUndefined(); + else expect(data.per1WeekResetTime).toBeTypeOf("number"); }); test("usage token-plan --view 渲染生成时间与两个额度窗口", async () => { diff --git a/packages/commands/tests/token-plan-usage.test.ts b/packages/commands/tests/token-plan-usage.test.ts index 2366983..a35db67 100644 --- a/packages/commands/tests/token-plan-usage.test.ts +++ b/packages/commands/tests/token-plan-usage.test.ts @@ -12,17 +12,22 @@ afterEach(() => { vi.restoreAllMocks(); }); -function makeUsageResponse(percentage: number): Record { +function makeUsageResponse( + per5HourPercentage: number, + per1WeekPercentage = per5HourPercentage, +): Record { + const usage: Record = { + per5HourPercentage, + per1WeekPercentage, + }; + if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000; + if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000; + return { data: { DataV2: { data: { - data: { - per5HourPercentage: percentage, - per5HourResetTime: 1_786_000_000_000, - per1WeekPercentage: percentage, - per1WeekResetTime: 1_786_100_000_000, - }, + data: usage, }, }, }, @@ -51,4 +56,38 @@ describe("usage token-plan view", () => { expect(output.join("")).toContain(`\u001B[${colorCode}m[`); }); + + test("accepts missing reset times when the quota usage is zero", async () => { + const output: string[] = []; + vi.spyOn(process.stdout, "write").mockImplementation((chunk) => { + output.push(String(chunk)); + return true; + }); + + await tokenPlanUsage.run({ + client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0)) }, + flags: { json: false, view: true }, + settings: { dryRun: false }, + } as never); + + expect(output.join("")).toContain("Resets: not applicable (no usage yet)"); + }); + + test("allows one unused quota window without masking another reset time", async () => { + const output: string[] = []; + vi.spyOn(process.stdout, "write").mockImplementation((chunk) => { + output.push(String(chunk)); + return true; + }); + + await tokenPlanUsage.run({ + client: { console: vi.fn().mockResolvedValue(makeUsageResponse(0, 0.5)) }, + flags: { json: false, view: true }, + settings: { dryRun: false }, + } as never); + + const renderedOutput = output.join(""); + expect(renderedOutput).toContain("Resets: not applicable (no usage yet)"); + expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/); + }); });