fix(usage): handle missing token plan reset times

This commit is contained in:
sonicg
2026-08-06 09:12:41 +08:00
parent 80bdcb83f6
commit 752a79e442
3 changed files with 85 additions and 26 deletions
@@ -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<string, unknown>);
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`);
@@ -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 () => {
@@ -12,17 +12,22 @@ afterEach(() => {
vi.restoreAllMocks();
});
function makeUsageResponse(percentage: number): Record<string, unknown> {
function makeUsageResponse(
per5HourPercentage: number,
per1WeekPercentage = per5HourPercentage,
): Record<string, unknown> {
const usage: Record<string, number> = {
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}/);
});
});