mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
38926aeb22
- finetune/deploy/dataset delete: add --yes flag + confirmDangerousAction - quota update --delete: confirm before clearing rate limits - non-TTY runs without --yes fail with a USAGE error - add e2e coverage and regenerate skill references
253 lines
8.1 KiB
TypeScript
253 lines
8.1 KiB
TypeScript
import { describe, expect, test } from "vite-plus/test";
|
||
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
|
||
import { DEPLOY_ROUTES } from "./topic-routes.ts";
|
||
|
||
/**
|
||
* Deploy E2E.
|
||
*
|
||
* The suite exercises command discovery, help text, and the `--dry-run`
|
||
* structured-output path (arg parsing + body construction) with no network
|
||
* dependency. Because `ensureApiKey` runs before every command (see main.ts),
|
||
* these cases are gated by isDashScopeE2EReady() — they are skipped when no
|
||
* DashScope credential is present (e.g. on CI) and run offline when one is.
|
||
* The remote list test is also gated and tolerates both empty accounts and
|
||
* auth/permission failures (see the test comment).
|
||
*/
|
||
|
||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => {
|
||
test("deploy 列出子命令", async () => {
|
||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, ["deploy", "--help"]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
expect(stderr).toMatch(/create|list|get|delete|update|scale|models/);
|
||
});
|
||
|
||
test("deploy create --help 正常退出并展示必填项", async () => {
|
||
const { stderr, exitCode } = await runCommandHelp(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"text",
|
||
"create",
|
||
"--help",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
expect(stderr).toMatch(/--model-name|--display-name/i);
|
||
});
|
||
|
||
test("deploy create --dry-run 构造 lora 部署请求体", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"text",
|
||
"create",
|
||
"--model-name",
|
||
"qwen-plus-2025-12-01",
|
||
"--display-name",
|
||
"my-qwen-plus",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{
|
||
action: string;
|
||
body: {
|
||
model_name: string;
|
||
name: string;
|
||
plan: string;
|
||
capacity: number;
|
||
};
|
||
}>(stdout);
|
||
expect(data.action).toBe("deploy.create");
|
||
expect(data.body.model_name).toBe("qwen-plus-2025-12-01");
|
||
expect(data.body.name).toBe("my-qwen-plus");
|
||
expect(data.body.plan).toBe("lora");
|
||
expect(data.body.capacity).toBe(1);
|
||
});
|
||
|
||
test("deploy create --plan mu --deploy-spec --dry-run 透传 deploy_spec", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"text",
|
||
"create",
|
||
"--model-name",
|
||
"qwen3-8b",
|
||
"--display-name",
|
||
"my-qwen3-mu",
|
||
"--plan",
|
||
"mu",
|
||
"--deploy-spec",
|
||
"MU1",
|
||
"--capacity",
|
||
"2",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{
|
||
action: string;
|
||
body: { plan: string; deploy_spec?: string; capacity?: number };
|
||
}>(stdout);
|
||
expect(data.action).toBe("deploy.create");
|
||
expect(data.body.plan).toBe("mu");
|
||
expect(data.body.deploy_spec).toBe("MU1");
|
||
expect(data.body.capacity).toBe(2);
|
||
});
|
||
|
||
test("deploy audio create --dry-run 默认 plan=mu(CosyVoice 部署契约)", async () => {
|
||
// Audio (CosyVoice TTS) outputs deploy model-unit-billed: the modality fixes
|
||
// the default plan to `mu` (text/image stay `lora`). In dry-run the mu
|
||
// strategy skips the catalog lookup, so deploy_spec is omitted and capacity
|
||
// falls back to 1 with billing_method POST_PAY.
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"audio",
|
||
"create",
|
||
"--model-name",
|
||
"my-cosyvoice-ft",
|
||
"--display-name",
|
||
"my-tts",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{
|
||
action: string;
|
||
body: { plan: string; name: string; billing_method?: string; capacity?: number };
|
||
}>(stdout);
|
||
expect(data.action).toBe("deploy.create");
|
||
expect(data.body.plan).toBe("mu");
|
||
expect(data.body.name).toBe("my-tts");
|
||
expect(data.body.billing_method).toBe("POST_PAY");
|
||
expect(data.body.capacity).toBe(1);
|
||
});
|
||
|
||
test("deploy scale --dry-run 转发 capacity", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"scale",
|
||
"--deployed-model",
|
||
"dep-xxx",
|
||
"--capacity",
|
||
"8",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{
|
||
action: string;
|
||
deployed_model: string;
|
||
body: { capacity: number };
|
||
}>(stdout);
|
||
expect(data.action).toBe("deploy.scale");
|
||
expect(data.deployed_model).toBe("dep-xxx");
|
||
expect(data.body.capacity).toBe(8);
|
||
});
|
||
|
||
test("deploy update --dry-run 转发 rate limits", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"update",
|
||
"--deployed-model",
|
||
"dep-xxx",
|
||
"--rpm-limit",
|
||
"1000",
|
||
"--tpm-limit",
|
||
"200000",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{
|
||
action: string;
|
||
body: { rpm_limit: number; tpm_limit: number };
|
||
}>(stdout);
|
||
expect(data.action).toBe("deploy.update");
|
||
expect(data.body.rpm_limit).toBe(1000);
|
||
expect(data.body.tpm_limit).toBe(200000);
|
||
});
|
||
|
||
test("deploy scale --dry-run 缺少 capacity/input-tpm/output-tpm 时报错", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"scale",
|
||
"--deployed-model",
|
||
"dep-xxx",
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).not.toBe(0);
|
||
// Nothing useful emitted to stdout on a usage error.
|
||
expect(stdout.trim()).toBe("");
|
||
});
|
||
|
||
test.each([
|
||
["list", ["--status", "RUNNING"]],
|
||
["get", ["--deployed-model", "dep-xxx"]],
|
||
["models", ["--source", "custom"]],
|
||
["delete", ["--deployed-model", "dep-xxx"]],
|
||
])("deploy %s --dry-run 发出结构化动作", async (sub, extra) => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
sub,
|
||
...extra,
|
||
"--dry-run",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
const data = parseStdoutJson<{ action: string }>(stdout);
|
||
expect(data.action).toBe(`deploy.${sub}`);
|
||
});
|
||
|
||
test("deploy delete --help 展示 --yes", async () => {
|
||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "delete", "--help"]);
|
||
expect(exitCode, stderr).toBe(0);
|
||
expect(stderr).toMatch(/--yes/i);
|
||
});
|
||
|
||
test("deploy delete 非 TTY 无 --yes 报 USAGE (2)", async () => {
|
||
// --skip-precheck 保证确认门在发任何网络请求前触发
|
||
const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"delete",
|
||
"--deployed-model",
|
||
"dep-xxx",
|
||
"--skip-precheck",
|
||
]);
|
||
expect(exitCode).toBe(2);
|
||
expect(stderr).toMatch(/--yes/);
|
||
});
|
||
});
|
||
|
||
describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => {
|
||
/**
|
||
* 不同开发者的 key 状态不一:可能鉴权失败、可能账号下没有任何部署记录、
|
||
* 也可能受区域/权限限制。因此本用例不假设"有数据"或"调用成功":
|
||
* - 成功(exit 0):响应必须可解析;deployments 可能为空数组或不存在。
|
||
* - 失败(非零退出):只要 CLI 把服务端/鉴权错误优雅上抛(stderr 有内容、
|
||
* 而非进程崩溃),即视为通过。
|
||
*/
|
||
test("deploy list --output json 优雅返回(空账号或鉴权失败均通过)", async () => {
|
||
const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [
|
||
"deploy",
|
||
"list",
|
||
"--page-size",
|
||
"5",
|
||
"--output",
|
||
"json",
|
||
]);
|
||
if (exitCode === 0) {
|
||
const data = parseStdoutJson<{ data?: { deployments?: unknown[] } }>(stdout);
|
||
expect(data).toBeTruthy();
|
||
if (data.data?.deployments) {
|
||
expect(Array.isArray(data.data.deployments)).toBe(true);
|
||
}
|
||
} else {
|
||
expect(stderr.length).toBeGreaterThan(0);
|
||
}
|
||
}, 60_000);
|
||
});
|