diff --git a/packages/commands/src/commands/usage/token-plan.ts b/packages/commands/src/commands/usage/token-plan.ts index b3ab83d..76c4876 100644 --- a/packages/commands/src/commands/usage/token-plan.ts +++ b/packages/commands/src/commands/usage/token-plan.ts @@ -6,24 +6,14 @@ const BOX_WIDTH = 76; const PROGRESS_WIDTH = 32; interface TokenPlanUsage { - per5HourPercentage: number; + per5HourPercentage?: number; per5HourResetTime?: number; - per1WeekPercentage: number; + per1WeekPercentage?: 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, @@ -31,17 +21,20 @@ function readUsage(result: unknown): TokenPlanUsage { per1WeekResetTime: response.per1WeekResetTime, }; - const resetTimes = [ + const quotas = [ [usage.per5HourPercentage, usage.per5HourResetTime], [usage.per1WeekPercentage, usage.per1WeekResetTime], ]; - const hasValidResetTimes = resetTimes.every( + const hasValidQuotas = quotas.every( ([percentage, resetTime]) => - (percentage === 0 && resetTime === undefined) || - (typeof resetTime === "number" && Number.isFinite(resetTime)), + (percentage === undefined && resetTime === undefined) || + (typeof percentage === "number" && + Number.isFinite(percentage) && + ((percentage === 0 && resetTime === undefined) || + (typeof resetTime === "number" && Number.isFinite(resetTime)))), ); - if (!hasValidResetTimes) { + if (!hasValidQuotas) { throw new BailianError("Token Plan usage response has an unexpected format.", ExitCode.GENERAL); } @@ -101,11 +94,21 @@ 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 | undefined) => { + const writeQuota = ( + label: string, + unlimitedMessage: string, + percentage: number | undefined, + resetTime: number | undefined, + ) => { + writeLine(color.bold(label), label); + if (percentage === undefined) { + writeLine(color.dim(unlimitedMessage), unlimitedMessage); + return; + } + 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}`); if (resetTime === undefined) { writeLine( @@ -124,9 +127,19 @@ function printView(usage: TokenPlanUsage, generatedAt: number): void { const generatedAtText = `Generated at: ${formatDateTime(generatedAt)} (local time)`; writeLine(color.dim(generatedAtText), generatedAtText); process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`); - writeQuota("5-hour quota", usage.per5HourPercentage, usage.per5HourResetTime); + writeQuota( + "5-hour quota", + "5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。", + usage.per5HourPercentage, + usage.per5HourResetTime, + ); process.stdout.write(`├${"─".repeat(BOX_WIDTH)}┤\n`); - writeQuota("1-week quota", usage.per1WeekPercentage, usage.per1WeekResetTime); + writeQuota( + "1-week quota", + "1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。", + usage.per1WeekPercentage, + usage.per1WeekResetTime, + ); 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 ffa4822..1acf052 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); @@ -65,12 +65,18 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () = per1WeekPercentage?: number; per1WeekResetTime?: number; }>(result.stdout); - expect(data.per5HourPercentage).toBeTypeOf("number"); - expect(data.per1WeekPercentage).toBeTypeOf("number"); - 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"); + const quotas = [ + [data.per5HourPercentage, data.per5HourResetTime], + [data.per1WeekPercentage, data.per1WeekResetTime], + ]; + for (const [percentage, resetTime] of quotas) { + if (percentage === undefined) expect(resetTime).toBeUndefined(); + else if (percentage === 0) expect(resetTime).toBeUndefined(); + else { + expect(percentage).toBeTypeOf("number"); + expect(resetTime).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 a35db67..9a1596c 100644 --- a/packages/commands/tests/token-plan-usage.test.ts +++ b/packages/commands/tests/token-plan-usage.test.ts @@ -13,15 +13,18 @@ afterEach(() => { }); function makeUsageResponse( - per5HourPercentage: number, + 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; + const usage: Record = {}; + if (per5HourPercentage !== undefined) { + usage.per5HourPercentage = per5HourPercentage; + if (per5HourPercentage !== 0) usage.per5HourResetTime = 1_786_000_000_000; + } + if (per1WeekPercentage !== undefined) { + usage.per1WeekPercentage = per1WeekPercentage; + if (per1WeekPercentage !== 0) usage.per1WeekResetTime = 1_786_100_000_000; + } return { data: { @@ -90,4 +93,57 @@ describe("usage token-plan view", () => { 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}/); }); + + test("renders missing quota windows as possibly unlimited", 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()) }, + flags: { json: false, view: true }, + settings: { dryRun: false }, + } as never); + + const renderedOutput = output.join(""); + expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。"); + expect(renderedOutput).toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。"); + }); + + test("renders only the missing quota window as possibly unlimited", 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(undefined, 0.5)) }, + flags: { json: false, view: true }, + settings: { dryRun: false }, + } as never); + + const renderedOutput = output.join(""); + expect(renderedOutput).toContain("5小时限额当前可能无限制,请到百炼 Token Plan 控制台核实。"); + expect(renderedOutput).not.toContain("1周限额当前可能无限制,请到百炼 Token Plan 控制台核实。"); + expect(renderedOutput).toMatch(/Resets: \d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}/); + }); + + test("returns an empty JSON object when no quota fields are available", 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()) }, + flags: { json: true, view: false }, + settings: { dryRun: false }, + } as never); + + expect(output.join("").trim()).toBe("{}"); + }); });