Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
98 changes: 70 additions & 28 deletions packages/commands/src/commands/usage/token-plan.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,82 @@
import { defineCommand, detectOutputFormat, unwrapResponse } from "bailian-cli-core";
import { emitResult } from "bailian-cli-runtime";
import { printQuotaBox, readNumber } from "./quota-box.ts";
import { printQuotaBox, readNumber, type QuotaSection } from "./quota-box.ts";

const TOKEN_PLAN_USAGE_API = "zeldaHttp.apikeyMgr./tokenplan/personal/api/v2/usage";

interface TokenPlanUsage {
per1MonthPercentage?: number;
per1MonthResetTime?: number;
/** Quota window fields look like per5HourPercentage / per1WeekResetTime / per1MonthPercentage. */
const WINDOW_FIELD_PATTERN = /^per(\d+)(Hour|Day|Week|Month)(Percentage|ResetTime)$/;

/** Personal plans are metered monthly; render an empty monthly window when the API returns none. */
const DEFAULT_WINDOW_KEY = "per1Month";

const UNIT_HOURS: Record<string, number> = { Hour: 1, Day: 24, Week: 24 * 7, Month: 24 * 30 };

interface TokenPlanWindow {
/** Field prefix, e.g. per1Month. */
key: string;
count: number;
unit: string;
percentage?: number;
resetTime?: number;
}

function readUsage(result: unknown): TokenPlanUsage {
const response = unwrapResponse(result as Record<string, unknown>);
const usage: TokenPlanUsage = {};
/**
* JSON output: keeps the API's per* quota fields as-is; hasQuotaData tells whether any usage
* ratio was read, so consumers can tell "no data" apart from a normal result.
*/
type TokenPlanUsage = Record<string, number | boolean>;

const per1MonthPercentage = readNumber(response.per1MonthPercentage);
if (per1MonthPercentage !== undefined) usage.per1MonthPercentage = per1MonthPercentage;
const per1MonthResetTime = readNumber(response.per1MonthResetTime);
if (per1MonthResetTime !== undefined) usage.per1MonthResetTime = per1MonthResetTime;
/** Discover windows from the per* fields actually present, so metering changes don't break parsing. */
function readWindows(response: Record<string, unknown>): TokenPlanWindow[] {
const windows = new Map<string, TokenPlanWindow>();
for (const [field, value] of Object.entries(response)) {
const match = WINDOW_FIELD_PATTERN.exec(field);
if (!match) continue;
const numberValue = readNumber(value);
if (numberValue === undefined) continue;

const [, count, unit, kind] = match;
const key = `per${count}${unit}`;
const window = windows.get(key) ?? { key, count: Number(count), unit };
if (kind === "Percentage") window.percentage = numberValue;
else window.resetTime = numberValue;
windows.set(key, window);
}
return [...windows.values()].sort(
(left, right) => left.count * UNIT_HOURS[left.unit] - right.count * UNIT_HOURS[right.unit],
);
}

function toJson(windows: TokenPlanWindow[]): TokenPlanUsage {
const usage: TokenPlanUsage = {};
for (const window of windows) {
if (window.percentage !== undefined) usage[`${window.key}Percentage`] = window.percentage;
if (window.resetTime !== undefined) usage[`${window.key}ResetTime`] = window.resetTime;
}
usage.hasQuotaData = windows.some((window) => window.percentage !== undefined);
return usage;
}

function printView(usage: TokenPlanUsage, generatedAt: number): void {
printQuotaBox(
"Token Plan Usage",
[
{
label: "Monthly quota",
emptyMessage:
"The monthly limit may be unlimited; verify in the Bailian Token Plan console.",
percentage: usage.per1MonthPercentage,
resetTime: usage.per1MonthResetTime,
},
],
generatedAt,
);
function windowName(window: Pick<TokenPlanWindow, "count" | "unit">): string {
if (window.count === 1 && window.unit === "Month") return "monthly";
return `${window.count}-${window.unit.toLowerCase()}`;
}

function toSection(window: TokenPlanWindow): QuotaSection {
const name = windowName(window);
return {
label: `${name.charAt(0).toUpperCase()}${name.slice(1)} quota`,
emptyMessage: `The ${name} limit may be unlimited; verify in the Bailian Token Plan console.`,
percentage: window.percentage,
resetTime: window.resetTime,
};
}

function printView(windows: TokenPlanWindow[], generatedAt: number): void {
const visibleWindows =
windows.length > 0 ? windows : [{ key: DEFAULT_WINDOW_KEY, count: 1, unit: "Month" }];
printQuotaBox("Token Plan Usage", visibleWindows.map(toSection), generatedAt);
}

export default defineCommand({
Expand All @@ -55,13 +97,13 @@ export default defineCommand({
}

const result = await ctx.client.console(TOKEN_PLAN_USAGE_API, {});
const usage = readUsage(result);
const windows = readWindows(unwrapResponse(result as Record<string, unknown>));

if (format === "json") {
emitResult(usage, format);
emitResult(toJson(windows), format);
return;
}

printView(usage, Date.now());
printView(windows, Date.now());
},
});
15 changes: 6 additions & 9 deletions packages/commands/tests/e2e/usage-token-plan.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,21 +49,18 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage token-plan(Console)", () =
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan", "--output", "json"]);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
const data = parseStdoutJson<{
per1MonthPercentage?: number;
per1MonthResetTime?: number;
}>(result.stdout);
const fields = [data.per1MonthPercentage, data.per1MonthResetTime];
for (const field of fields) {
if (field !== undefined) expect(field).toBeTypeOf("number");
const data = parseStdoutJson<Record<string, unknown>>(result.stdout);
expect(data.hasQuotaData).toBeTypeOf("boolean");
for (const [field, value] of Object.entries(data)) {
if (field.startsWith("per")) expect(value).toBeTypeOf("number");
}
});

test("usage token-plan 默认渲染生成时间与月额度窗口", async () => {
test("usage token-plan 默认渲染生成时间与额度窗口", async () => {
const result = await runCommandE2e(USAGE_ROUTES, ["usage", "token-plan"]);
if (isConsoleAuthFailure(result)) return;
expect(result.exitCode, result.stderr).toBe(0);
expect(result.stdout).toContain("Generated at:");
expect(result.stdout).toContain("Monthly quota");
expect(result.stdout).toMatch(/(Monthly|\d+-(hour|day|week|month)) quota/);
});
});
55 changes: 52 additions & 3 deletions packages/commands/tests/token-plan-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,28 @@ describe("usage token-plan view", () => {
"The monthly limit may be unlimited; verify in the Bailian Token Plan console.",
);
});

test("renders every quota window returned by the API, ordered by duration", async () => {
process.env.NO_COLOR = "1";
const output = captureStdout();

await runTokenPlan(
wrapResponse({
per1WeekPercentage: 0.13,
per1WeekResetTime: 1_786_417_680_000,
per5HourPercentage: 0.43,
per5HourResetTime: 1_785_868_560_000,
}),
);

const renderedOutput = output.join("");
expect(renderedOutput).toContain("5-hour quota");
expect(renderedOutput).toContain("1-week quota");
expect(renderedOutput).not.toContain("Monthly quota");
expect(renderedOutput.indexOf("5-hour quota")).toBeLessThan(
renderedOutput.indexOf("1-week quota"),
);
});
});

describe("usage token-plan json", () => {
Expand All @@ -135,15 +157,39 @@ describe("usage token-plan json", () => {
expect(JSON.parse(output.join(""))).toEqual({
per1MonthPercentage: 0.5,
per1MonthResetTime: 1_786_000_000_000,
hasQuotaData: true,
});
});

test("returns an empty JSON object when no quota fields are available", async () => {
test("flags the absence of quota data instead of returning a bare empty object", async () => {
const output = captureStdout();

await runTokenPlan(makeUsageResponse(), "json");

expect(output.join("").trim()).toBe("{}");
expect(JSON.parse(output.join(""))).toEqual({ hasQuotaData: false });
});

test("outputs every per* quota window returned by the API", async () => {
const output = captureStdout();

await runTokenPlan(
wrapResponse({
per5HourPercentage: 0.43,
per5HourResetTime: 1_785_868_560_000,
per1WeekPercentage: 0.13,
per1WeekResetTime: 1_786_417_680_000,
requestId: "ignored",
}),
"json",
);

expect(JSON.parse(output.join(""))).toEqual({
per5HourPercentage: 0.43,
per5HourResetTime: 1_785_868_560_000,
per1WeekPercentage: 0.13,
per1WeekResetTime: 1_786_417_680_000,
hasQuotaData: true,
});
});

test("omits non-numeric quota fields from the JSON output", async () => {
Expand All @@ -154,6 +200,9 @@ describe("usage token-plan json", () => {
"json",
);

expect(JSON.parse(output.join(""))).toEqual({ per1MonthResetTime: 1_786_000_000_000 });
expect(JSON.parse(output.join(""))).toEqual({
per1MonthResetTime: 1_786_000_000_000,
hasQuotaData: false,
});
});
});
Loading