mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
feat: add usage/quota/workspace cli command
This commit is contained in:
@@ -0,0 +1,349 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
import { readConfigFile } from "bailian-cli-core";
|
||||
|
||||
function isConsoleE2EReady(): boolean {
|
||||
if (!isBailianE2EEnabled()) return false;
|
||||
if (process.env.DASHSCOPE_ACCESS_TOKEN?.trim()) return true;
|
||||
try {
|
||||
const config = readConfigFile();
|
||||
return typeof config.access_token === "string" && config.access_token.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe("e2e: quota", () => {
|
||||
test("quota list --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--all");
|
||||
});
|
||||
|
||||
test("quota list --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "list", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl quota list");
|
||||
expect(stderr).toContain("bl quota list --model qwen3.6-plus");
|
||||
expect(stderr).toContain("bl quota list --all");
|
||||
});
|
||||
|
||||
test("quota request --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "request", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--tpm");
|
||||
expect(stderr).toContain("--yes");
|
||||
});
|
||||
|
||||
test("quota history --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "history", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--page");
|
||||
expect(stderr).toContain("--model");
|
||||
});
|
||||
|
||||
test("quota check --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--model");
|
||||
expect(stderr).toContain("--period");
|
||||
expect(stderr).toContain("bl quota check");
|
||||
});
|
||||
|
||||
test("quota check --period 0 报错最小值", async () => {
|
||||
const { stderr, exitCode } = await runCli(["quota", "check", "--period", "0.5"]);
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("at least 1 minute");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => {
|
||||
test("quota list --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"list",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: {
|
||||
input?: { queryQpmInfo?: boolean; supports?: { selfServiceLimitIncrease?: boolean } };
|
||||
};
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("listFoundationModels");
|
||||
expect(data.data?.input?.queryQpmInfo).toBe(true);
|
||||
expect(data.data?.input?.supports?.selfServiceLimitIncrease).toBe(true);
|
||||
});
|
||||
|
||||
test("quota list --dry-run --all 不传 supports 过滤", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"list",
|
||||
"--all",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { input?: { supports?: unknown } };
|
||||
}>(stdout);
|
||||
expect(data.data?.input?.supports).toBeUndefined();
|
||||
});
|
||||
|
||||
test("quota list 文本输出包含双行表头", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"list",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("模型");
|
||||
expect(stdout).toContain("Model");
|
||||
expect(stdout).toContain("RPM");
|
||||
expect(stdout).toContain("TPM");
|
||||
expect(stdout).toContain("可设上限 TPM");
|
||||
expect(stdout).toContain("Max TPM");
|
||||
});
|
||||
|
||||
test("quota list --model 指定模型返回结果", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"list",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3.6-plus");
|
||||
expect(stdout).toMatch(/共 1 个模型/);
|
||||
});
|
||||
|
||||
test("quota list --model 不存在的模型报错", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"list",
|
||||
"--model",
|
||||
"nonexistent-model-xyz-99999",
|
||||
"--output",
|
||||
"text",
|
||||
]);
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("no matching models found");
|
||||
});
|
||||
|
||||
test("quota list JSON 输出包含 qpmInfo", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["quota", "list", "--output", "json"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<Array<{ model?: string; qpmInfo?: unknown }>>(stdout);
|
||||
expect(Array.isArray(data)).toBe(true);
|
||||
expect(data.length).toBeGreaterThan(0);
|
||||
expect(data[0].model).toBeTypeOf("string");
|
||||
expect(data[0].qpmInfo).toBeDefined();
|
||||
});
|
||||
|
||||
test("quota request --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"request",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--tpm",
|
||||
"6000000",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { input?: { model?: string; limit?: { usage_limit?: number } } };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("updateFoundationModelLimits");
|
||||
expect(data.data?.input?.model).toBe("qwen3.6-plus");
|
||||
expect(data.data?.input?.limit?.usage_limit).toBeTypeOf("number");
|
||||
});
|
||||
|
||||
test("quota request TPM 超范围报错", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"request",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--tpm",
|
||||
"999",
|
||||
]);
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("out of range");
|
||||
expect(stderr).toContain("Current");
|
||||
expect(stderr).toContain("Range");
|
||||
});
|
||||
|
||||
test("quota request 不支持提额的模型报错", async () => {
|
||||
const { stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"request",
|
||||
"--model",
|
||||
"nonexistent-model-xyz-99999",
|
||||
"--tpm",
|
||||
"100000",
|
||||
]);
|
||||
expect(exitCode).toBe(1);
|
||||
expect(stderr).toContain("not found");
|
||||
});
|
||||
|
||||
test("quota history --dry-run 输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"history",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { input?: { pageNo?: number; pageSize?: number } };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("listModelLimitApplications");
|
||||
expect(data.data?.input?.pageNo).toBe(1);
|
||||
expect(data.data?.input?.pageSize).toBe(10);
|
||||
});
|
||||
|
||||
test("quota check --dry-run 输出 API 信息", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ apis?: string[] }>(stdout);
|
||||
expect(data.apis).toContain(
|
||||
"zeldaHttp.dashscopeModel./zelda/api/v1/modelCenter/listFoundationModels",
|
||||
);
|
||||
expect(data.apis).toContain("zeldaEasy.bailian-telemetry.monitor.getMonitorData");
|
||||
});
|
||||
|
||||
test("quota check 文本输出包含双行表头", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("模型");
|
||||
expect(stdout).toContain("Model");
|
||||
expect(stdout).toContain("RPM 用量/限额");
|
||||
expect(stdout).toContain("RPM Usage/Limit");
|
||||
expect(stdout).toContain("TPM 用量/限额");
|
||||
expect(stdout).toContain("状态");
|
||||
});
|
||||
|
||||
test("quota check --model 指定单模型", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3.6-plus");
|
||||
expect(stdout).toMatch(/共 1 个模型/);
|
||||
});
|
||||
|
||||
test("quota check --model 逗号分隔多模型", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--model",
|
||||
"qwen3.6-plus,qwen-plus",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3.6-plus");
|
||||
expect(stdout).toContain("qwen-plus");
|
||||
expect(stdout).toMatch(/共 2 个模型/);
|
||||
});
|
||||
|
||||
test("quota check JSON 输出包含用量和限额字段", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<
|
||||
Array<{
|
||||
model?: string;
|
||||
rpmUsage?: number;
|
||||
rpmLimit?: number;
|
||||
tpmUsage?: number;
|
||||
tpmLimit?: number;
|
||||
}>
|
||||
>(stdout);
|
||||
expect(Array.isArray(data)).toBe(true);
|
||||
expect(data.length).toBe(1);
|
||||
expect(data[0].model).toBe("qwen3.6-plus");
|
||||
expect(data[0].rpmUsage).toBeTypeOf("number");
|
||||
expect(data[0].rpmLimit).toBeTypeOf("number");
|
||||
expect(data[0].tpmUsage).toBeTypeOf("number");
|
||||
expect(data[0].tpmLimit).toBeTypeOf("number");
|
||||
});
|
||||
|
||||
test("quota check 状态列显示正常/接近限流/已限流之一", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"check",
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const hasStatus =
|
||||
stdout.includes("正常") || stdout.includes("接近限流") || stdout.includes("已限流");
|
||||
expect(hasStatus).toBe(true);
|
||||
});
|
||||
|
||||
test("quota history --dry-run --page 2 --page-size 20", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"quota",
|
||||
"history",
|
||||
"--page",
|
||||
"2",
|
||||
"--page-size",
|
||||
"20",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { input?: { pageNo?: number; pageSize?: number } };
|
||||
}>(stdout);
|
||||
expect(data.data?.input?.pageNo).toBe(2);
|
||||
expect(data.data?.input?.pageSize).toBe(20);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,282 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
import { readConfigFile } from "bailian-cli-core";
|
||||
|
||||
function isConsoleE2EReady(): boolean {
|
||||
if (!isBailianE2EEnabled()) return false;
|
||||
if (process.env.DASHSCOPE_ACCESS_TOKEN?.trim()) return true;
|
||||
try {
|
||||
const config = readConfigFile();
|
||||
return typeof config.access_token === "string" && config.access_token.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
describe("e2e: usage free", () => {
|
||||
test("usage 分组展示子命令帮助且退出码为 0", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli(["usage"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const out = `${stdout}\n${stderr}`;
|
||||
expect(out).toMatch(/usage|free|freetier/i);
|
||||
});
|
||||
|
||||
test("usage free --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|quota|free-tier/i);
|
||||
});
|
||||
|
||||
test("usage free --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCli(["usage", "free", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage free");
|
||||
expect(stderr).toContain("bl usage free --model qwen3-max");
|
||||
expect(stderr).toContain("bl usage free --model qwen3-max,qwen-turbo");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => {
|
||||
test("usage free --dry-run --model 输出请求参数不发起调用", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { queryFreeTierQuotaRequest?: { models?: string[] } };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("queryFreeTierQuota");
|
||||
expect(data.data?.queryFreeTierQuotaRequest?.models).toEqual(["qwen3-max"]);
|
||||
});
|
||||
|
||||
test("usage free --dry-run --model 逗号分隔多个模型", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"qwen3-max,qwen-turbo",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { queryFreeTierQuotaRequest?: { models?: string[] } };
|
||||
}>(stdout);
|
||||
expect(data.data?.queryFreeTierQuotaRequest?.models).toEqual(["qwen3-max", "qwen-turbo"]);
|
||||
});
|
||||
|
||||
test("usage free --dry-run --model 重复模型名自动去重", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"qwen3-max,qwen3-max,qwen-turbo",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { queryFreeTierQuotaRequest?: { models?: string[] } };
|
||||
}>(stdout);
|
||||
expect(data.data?.queryFreeTierQuotaRequest?.models).toEqual(["qwen3-max", "qwen-turbo"]);
|
||||
});
|
||||
|
||||
test("usage free --dry-run --model 逗号间有空格也能正确解析", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"qwen3-max, qwen-turbo",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { queryFreeTierQuotaRequest?: { models?: string[] } };
|
||||
}>(stdout);
|
||||
expect(data.data?.queryFreeTierQuotaRequest?.models).toEqual(["qwen3-max", "qwen-turbo"]);
|
||||
});
|
||||
|
||||
test("usage free --dry-run 不指定 --model 传全量模型列表", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { queryFreeTierQuotaRequest?: { models?: string[] } };
|
||||
}>(stdout);
|
||||
const models = data.data?.queryFreeTierQuotaRequest?.models ?? [];
|
||||
expect(models.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("usage free --model 单模型查询返回 JSON 结果", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
code?: string;
|
||||
successResponse?: boolean;
|
||||
}>(stdout);
|
||||
expect(data.code).toBe("200");
|
||||
expect(data.successResponse).toBe(true);
|
||||
});
|
||||
|
||||
test("usage free --model 单模型文本输出包含表头", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("Model");
|
||||
expect(stdout).toContain("Type");
|
||||
expect(stdout).toContain("Remaining/Total");
|
||||
expect(stdout).toContain("Usage");
|
||||
expect(stdout).toContain("Expires");
|
||||
expect(stdout).toContain("Auto-Stop");
|
||||
});
|
||||
|
||||
test("usage free --model 文本输出包含模型名", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3-max");
|
||||
});
|
||||
|
||||
test("usage free --model 逗号分隔多模型文本输出包含所有模型", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max,qwen-turbo",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3-max");
|
||||
expect(stdout).toContain("qwen-turbo");
|
||||
});
|
||||
|
||||
test("usage free --model 文本输出包含正确的 Type 列", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("Text");
|
||||
});
|
||||
|
||||
test("usage free --model quotaStatus 为 UNKNOWN 时 Auto-Stop 显示 Unsupported", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"wan2.7-image",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("Unsupported");
|
||||
});
|
||||
|
||||
test("usage free --model quotaStatus 为 UNKNOWN 时额度显示为 -", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"wan2.7-image",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const lines = stdout.split("\n").filter((line) => line.includes("wan2.7-image"));
|
||||
expect(lines.length).toBe(1);
|
||||
expect(lines[0]).toContain("Vision");
|
||||
expect(lines[0]).toContain("Unsupported");
|
||||
});
|
||||
|
||||
test("usage free --model 不存在的模型仍返回表格行", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"nonexistent-model-xyz-12345",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("nonexistent-model-xyz-12345");
|
||||
});
|
||||
|
||||
test("usage free --model Auto-Stop 显示 ON、OFF 或 Unsupported", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const hasAutoStop =
|
||||
stdout.includes("ON") || stdout.includes("OFF") || stdout.includes("Unsupported");
|
||||
expect(hasAutoStop).toBe(true);
|
||||
});
|
||||
|
||||
test("usage free --model --region cn-beijing 指定区域查询", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"free",
|
||||
"--model",
|
||||
"qwen3-max",
|
||||
"--region",
|
||||
"cn-beijing",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ code?: string }>(stdout);
|
||||
expect(data.code).toBe("200");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,299 @@
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts";
|
||||
import { readConfigFile } from "bailian-cli-core";
|
||||
|
||||
function isConsoleE2EReady(): boolean {
|
||||
if (!isBailianE2EEnabled()) return false;
|
||||
if (process.env.DASHSCOPE_ACCESS_TOKEN?.trim()) return true;
|
||||
try {
|
||||
const config = readConfigFile();
|
||||
return typeof config.access_token === "string" && config.access_token.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function getStaticWorkspaceId(): string | undefined {
|
||||
if (process.env.BAILIAN_WORKSPACE_ID?.trim()) return process.env.BAILIAN_WORKSPACE_ID.trim();
|
||||
try {
|
||||
const config = readConfigFile();
|
||||
if (config.workspace_id) return config.workspace_id;
|
||||
} catch {}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
async function fetchDefaultWorkspaceId(): Promise<string> {
|
||||
const staticId = getStaticWorkspaceId();
|
||||
if (staticId) return staticId;
|
||||
|
||||
const { stdout } = await runCli(["workspace", "list", "--output", "json"]);
|
||||
const result = JSON.parse(stdout);
|
||||
const data = result?.data?.DataV2?.data?.data?.data ?? [];
|
||||
const defaultWs = data.find((ws: { defaultAgent?: boolean }) => ws.defaultAgent);
|
||||
if (defaultWs?.workspaceId) return defaultWs.workspaceId;
|
||||
if (data.length > 0 && data[0].workspaceId) return data[0].workspaceId;
|
||||
throw new Error("No workspace found for e2e tests");
|
||||
}
|
||||
|
||||
describe("e2e: usage stats", () => {
|
||||
test("usage stats --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/--model|--days|stats/i);
|
||||
});
|
||||
|
||||
test("usage stats --help 包含所有示例", async () => {
|
||||
const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("bl usage stats");
|
||||
expect(stderr).toContain("bl usage stats --model qwen-turbo");
|
||||
expect(stderr).toContain("bl usage stats --days 30");
|
||||
});
|
||||
|
||||
test("usage stats --help 包含 --workspace-id 选项", async () => {
|
||||
const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toContain("--workspace-id");
|
||||
});
|
||||
});
|
||||
|
||||
describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => {
|
||||
let wsId: string;
|
||||
|
||||
test("获取默认 workspace-id", async () => {
|
||||
wsId = await fetchDefaultWorkspaceId();
|
||||
expect(wsId).toBeTypeOf("string");
|
||||
expect(wsId.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
test("usage stats --dry-run 概览模式输出请求参数", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--dry-run",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: {
|
||||
reqDTO?: {
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
modelCallSource?: string;
|
||||
filterWorkspaceId?: string;
|
||||
};
|
||||
};
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("getModelUsageStatistic");
|
||||
expect(data.data?.reqDTO?.modelCallSource).toBe("Online");
|
||||
expect(data.data?.reqDTO?.startTime).toBeTypeOf("number");
|
||||
expect(data.data?.reqDTO?.endTime).toBeTypeOf("number");
|
||||
expect(data.data?.reqDTO?.filterWorkspaceId).toBe(wsId);
|
||||
});
|
||||
|
||||
test("usage stats --dry-run --days 30 时间跨度约 30 天", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--dry-run",
|
||||
"--days",
|
||||
"30",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { reqDTO?: { startTime?: number; endTime?: number } };
|
||||
}>(stdout);
|
||||
const span = (data.data?.reqDTO?.endTime ?? 0) - (data.data?.reqDTO?.startTime ?? 0);
|
||||
const thirtyDaysMs = 30 * 24 * 60 * 60 * 1000;
|
||||
expect(span).toBeGreaterThan(thirtyDaysMs - 5000);
|
||||
expect(span).toBeLessThan(thirtyDaysMs + 5000);
|
||||
});
|
||||
|
||||
test("usage stats --dry-run --model 指定模型使用 list API", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--dry-run",
|
||||
"--model",
|
||||
"qwen-turbo",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
api?: string;
|
||||
data?: { reqDTO?: { model?: string; filterWorkspaceId?: string } };
|
||||
}>(stdout);
|
||||
expect(data.api).toContain("listModelUsageStatisticData");
|
||||
expect(data.data?.reqDTO?.model).toBe("qwen-turbo");
|
||||
expect(data.data?.reqDTO?.filterWorkspaceId).toBe(wsId);
|
||||
});
|
||||
|
||||
test("usage stats --dry-run --type Text 传递 obsModelType", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--dry-run",
|
||||
"--type",
|
||||
"Text",
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
data?: { reqDTO?: { obsModelType?: string } };
|
||||
}>(stdout);
|
||||
expect(data.data?.reqDTO?.obsModelType).toBe("Text");
|
||||
});
|
||||
|
||||
test("usage stats 概览模式返回 JSON 结果", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--output",
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{
|
||||
code?: string;
|
||||
successResponse?: boolean;
|
||||
}>(stdout);
|
||||
expect(data.code).toBe("200");
|
||||
expect(data.successResponse).toBe(true);
|
||||
});
|
||||
|
||||
test("usage stats 概览文本输出包含中英文表头", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("时间范围 Period:");
|
||||
expect(stdout).toContain("调用模型数");
|
||||
expect(stdout).toContain("Models Called");
|
||||
expect(stdout).toContain("调用成功次数");
|
||||
expect(stdout).toContain("Successful Calls");
|
||||
});
|
||||
|
||||
test("usage stats 概览文本输出包含 Token 用量", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("总 Token");
|
||||
expect(stdout).toContain("Total Tokens");
|
||||
expect(stdout).toContain("[tokens]");
|
||||
});
|
||||
|
||||
test("usage stats --model 单模型文本输出包含双行表头", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--model",
|
||||
"qwen3.6-plus",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("模型");
|
||||
expect(stdout).toContain("Model");
|
||||
expect(stdout).toContain("调用次数");
|
||||
expect(stdout).toContain("Calls");
|
||||
expect(stdout).toContain("qwen3.6-plus");
|
||||
});
|
||||
|
||||
test("usage stats --model 逗号分隔多模型返回多行", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--model",
|
||||
"qwen3.6-plus,deepseek-v4-pro",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("qwen3.6-plus");
|
||||
expect(stdout).toContain("deepseek-v4-pro");
|
||||
expect(stdout).toMatch(/共 2 个模型/);
|
||||
});
|
||||
|
||||
test("usage stats --model 不存在的模型返回空表格", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--model",
|
||||
"nonexistent-model-xyz-99999",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("No usage data found");
|
||||
});
|
||||
|
||||
test("usage stats --days 1 短时间范围正常返回", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--days",
|
||||
"1",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("(1 天)");
|
||||
expect(stdout).toContain("调用模型数");
|
||||
});
|
||||
|
||||
test("usage stats --type Vision 按类型过滤", async () => {
|
||||
const { stdout, stderr, exitCode } = await runCli([
|
||||
"usage",
|
||||
"stats",
|
||||
"--workspace-id",
|
||||
wsId,
|
||||
"--type",
|
||||
"Vision",
|
||||
"--output",
|
||||
"text",
|
||||
"--no-color",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stdout).toContain("时间范围 Period:");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user