Files
modelstudioai__cli/packages/commands/tests/e2e/deploy.e2e.test.ts
T
故璃 38926aeb22 feat: require --yes confirmation for destructive operations
- 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
2026-08-31 19:26:31 +08:00

253 lines
8.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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=muCosyVoice 部署契约)", 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);
});