Skip to content

Commit 719c0b6

Browse files
committed
Merge remote-tracking branch 'origin/main' into feat/runtime-risk-confirmation
2 parents 67ae182 + 3f78de2 commit 719c0b6

68 files changed

Lines changed: 668 additions & 213 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/agents/cli-e2e-tests.md

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33
## 架构分层
44

5-
| 层级 | 路径 | 测什么 |
6-
| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- |
7-
| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) |
8-
| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-runlive(gated);每用例最小路由 |
9-
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
10-
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help |
11-
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | `kscli/src/commands.ts` 推导 path/分组;identity(`--version``search --help` path) |
12-
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
5+
| 层级 | 路径 | 测什么 |
6+
| --------------- | ----------------------------------------------------- | --------------------------------------------------------------------------------------- |
7+
| **共享基建** | `packages/e2e` | gating、子进程 runner、registry help 捕获、output、globalSetup(`private`,不发布) |
8+
| **命令 E2E** | `packages/commands/tests/e2e` | 进程内 help、子进程缺参/dry-run/live(gated);每用例最小路由 |
9+
| **Journey E2E** | `packages/commands/tests/e2e/knowledge/journeys` | 用户旅程全链路(跨命令回路 + 标记词召回闭环),全部 live gated;见 `journeys/README.md` |
10+
| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path/分组的进程内 help根 help、鉴权域等代表性子进程冒烟 |
11+
| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | map 全部 path/分组的进程内 help;`--version``search --help` 等代表性子进程冒烟 |
12+
| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 |
1313

14-
**依赖边界**`e2e``core``commands/tests``e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
14+
**依赖边界**`e2e``core``commands/tests``e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src` + `runtime` 公共 API**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。
1515

1616
## 触发条件
1717

@@ -29,28 +29,34 @@
2929
### commands E2E
3030

3131
- 路径:`packages/commands/tests/e2e/<kebab-topic>.e2e.test.ts`;knowledge 领域集中在 `packages/commands/tests/e2e/knowledge/` 子目录(新增 knowledge 命令测试放这里)
32-
- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts``routes` 为本 topic 最小 path → export 映射)
32+
- help:`runCommandHelp(routes, [...path, "--help"])` from `./helpers.ts`(当前 Vitest worker 内用真实 command + `CommandRegistry` 渲染,不启动子进程)
33+
- 子进程:缺参、dry-run、live 使用 `runCommandE2e(routes, args)`(spawn `harness/main.ts``routes` 为本 topic 最小 path → export 映射)
3334
- fixtures:`packages/commands/tests/e2e/fixtures/`
3435
- 路由常量:`topic-routes.ts`(按 topic 维护,****全量产品 map)
3536

3637
### 产品 smoke
3738

3839
- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts`
3940
- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts`
41+
- 全量 leaf/group help 使用产品 `commands` 创建 `CommandRegistry`,先通过 `resolve([...path, "--help"])` 检查 help 路由,再用 `captureRegistryHelp` 检查完整 Usage;禁止在 `test.each(commandPaths/groupPaths)` 中逐条启动 `tsx` 子进程
42+
- 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
4043

4144
### 共享
4245

43-
- gating / output / runner:`e2e/gating``e2e/output``e2e/runner`
46+
- gating / output / runner:`e2e/gating``e2e/output``e2e/runner`;runner 使用 `node --import tsx` 执行 TypeScript 入口,不启动 tsx CLI IPC server
4447
- globalSetup:根 `vite.config.ts``packages/e2e/src/global-setup.ts`
4548
- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))`
4649
- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时
4750

4851
## 双层 describe(固定结构)
4952

5053
```ts
51-
// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
54+
// 1) 不 skip:进程内 --help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖)
5255
describe("e2e: <topic>", () => {
53-
test("<subcommand> --help 正常退出", ...);
56+
test("<subcommand> --help 正常退出", async () => {
57+
const result = await runCommandHelp(FOO_ROUTES, ["foo", "bar", "--help"]);
58+
expect(result.exitCode, result.stderr).toBe(0);
59+
});
5460
});
5561

5662
// 2) skipIf:缺参 / dry-run / 真实集成
@@ -74,7 +80,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
7480

7581
## 用例类型
7682

77-
1. **--help**`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags
83+
1. **--help**`runCommandHelp(ROUTES, [..., "--help"])` → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
7884
2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`
7985
3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本
8086
4. **真实集成**:放在 skip 块**末尾**
@@ -112,7 +118,7 @@ describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
112118
- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由
113119
- [ ] `packages/commands/tests/e2e/<topic>.e2e.test.ts`(新建或扩展)
114120
- [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新各 `skills/<skill>/reference/` 并提交
115-
- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖)
121+
- [ ] 子命令 `--help` 使用 `runCommandHelp`(分组 help 由 bl `registry.smoke` 覆盖)
116122
- [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条
117123
- [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
118124
- [ ] `vp test packages/commands/tests/e2e/<file>` 通过

packages/cli/tests/e2e/registry.smoke.e2e.test.ts

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
1-
import { describe, expect, test } from "vite-plus/test";
2-
import { deriveGroupPaths } from "e2e/registry-smoke";
1+
import { mkdtempSync, rmSync } from "fs";
2+
import { tmpdir } from "os";
3+
import { join } from "path";
4+
import { afterAll, describe, expect, test } from "vite-plus/test";
5+
import { captureRegistryHelp, deriveGroupPaths } from "e2e/registry-smoke";
6+
import { CommandRegistry, resolve } from "bailian-cli-runtime";
37
import { commands } from "../../src/commands.ts";
48
import { runCli } from "./helpers.ts";
59

610
const commandPaths = Object.keys(commands).sort();
711
const groupPaths = deriveGroupPaths(commandPaths);
12+
const registry = new CommandRegistry(commands, "bl");
13+
const isolatedConfigDir = mkdtempSync(join(tmpdir(), "bl-registry-smoke-"));
14+
15+
afterAll(() => rmSync(isolatedConfigDir, { recursive: true, force: true }));
16+
17+
function runCliSmoke(args: string[]) {
18+
return runCli(args, { BAILIAN_CONFIG_DIR: isolatedConfigDir });
19+
}
820

921
describe("e2e: bl registry smoke", () => {
1022
test("根帮助展示 bl、逐命令鉴权域与全局 flag", async () => {
11-
const { stderr, exitCode } = await runCli(["--help"]);
23+
const { stderr, exitCode } = await runCliSmoke(["--help"]);
1224
expect(exitCode, stderr).toBe(0);
1325
expect(stderr).toMatch(/\bbl\b/i);
1426
expect(stderr).not.toMatch(/COMMAND\s+AUTH\s+DESCRIPTION/);
@@ -24,7 +36,7 @@ describe("e2e: bl registry smoke", () => {
2436
});
2537

2638
test("分组帮助按叶子命令展示不同鉴权域", async () => {
27-
const { stderr, exitCode } = await runCli(["app", "--help"]);
39+
const { stderr, exitCode } = await runCliSmoke(["app", "--help"]);
2840
expect(exitCode, stderr).toBe(0);
2941
expect(stderr).toMatch(/app call\s+\[API Key\]\s+Call a Bailian application/);
3042
expect(stderr).toMatch(/app list\s+\[Console\]\s+List Bailian applications/);
@@ -36,13 +48,13 @@ describe("e2e: bl registry smoke", () => {
3648
[["token-plan", "list-seats"], "AK/SK"],
3749
[["config", "show"], "No Auth"],
3850
] as const)("%s --help 明确展示鉴权域 %s", async (commandPath, authLabel) => {
39-
const { stderr, exitCode } = await runCli([...commandPath, "--help"]);
51+
const { stderr, exitCode } = await runCliSmoke([...commandPath, "--help"]);
4052
expect(exitCode, stderr).toBe(0);
4153
expect(stderr).toContain(`Authentication: ${authLabel}`);
4254
});
4355

4456
test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => {
45-
const { stderr, exitCode } = await runCli(["quota", "check", "--help"]);
57+
const { stderr, exitCode } = await runCliSmoke(["quota", "check", "--help"]);
4658
expect(exitCode, stderr).toBe(0);
4759
expect(stderr).toMatch(/Global Flags:/);
4860
expect(stderr).toMatch(/--console-region <region>/);
@@ -52,13 +64,25 @@ describe("e2e: bl registry smoke", () => {
5264
expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/);
5365
});
5466

55-
test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => {
56-
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
57-
expect(exitCode, stderr).toBe(0);
67+
test.each(commandPaths)("已注册命令 %s --help 成功", (path) => {
68+
const commandPath = path.split(" ");
69+
70+
expect(resolve([...commandPath, "--help"], registry)).toEqual({
71+
kind: "help",
72+
path: commandPath,
73+
});
74+
expect(captureRegistryHelp(registry, commandPath)).toContain(`Usage: bl ${path}`);
5875
});
5976

60-
test.each(groupPaths)("命令分组 %s --help 成功", async (path) => {
61-
const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]);
62-
expect(exitCode, stderr).toBe(0);
77+
test.each(groupPaths)("命令分组 %s --help 成功", (path) => {
78+
const commandPath = path.split(" ");
79+
80+
expect(resolve([...commandPath, "--help"], registry)).toEqual({
81+
kind: "help",
82+
path: commandPath,
83+
});
84+
expect(captureRegistryHelp(registry, commandPath)).toContain(
85+
`Usage: bl ${path} <command> [flags]`,
86+
);
6387
});
6488
});

packages/commands/src/commands/dataset/delete.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, deleteDataset, type FlagsDef } from "bailian-cli-core";
2-
import { emitResult, emitBare } from "bailian-cli-runtime";
2+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
33

44
const DELETE_FLAGS = {
55
fileId: {
@@ -8,14 +8,28 @@ const DELETE_FLAGS = {
88
description: { "en-US": "Dataset file ID (required)", "zh-CN": "数据集文件 ID(必填)" },
99
required: true,
1010
},
11+
yes: {
12+
type: "switch",
13+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
14+
},
1115
} satisfies FlagsDef;
1216

1317
export default defineCommand({
1418
description: { "en-US": "Delete a dataset file by ID", "zh-CN": "通过 ID 删除数据集文件" },
1519
auth: "apiKey",
16-
usageArgs: "--file-id <id>",
20+
usageArgs: "--file-id <id> [--yes]",
1721
flags: DELETE_FLAGS,
18-
exampleArgs: ["--file-id file-id-xxx", "--file-id file-id-xxx --dry-run"],
22+
exampleArgs: [
23+
"--file-id file-id-xxx",
24+
"--file-id file-id-xxx --dry-run",
25+
"--file-id file-id-xxx --yes",
26+
],
27+
notes: [
28+
{
29+
"en-US": "Irreversible — the dataset file is permanently removed.",
30+
"zh-CN": "该操作不可撤销——数据集文件将被永久删除。",
31+
},
32+
],
1933
async run(ctx) {
2034
const { settings, flags } = ctx;
2135
const fileId = flags.fileId;
@@ -25,6 +39,11 @@ export default defineCommand({
2539
return;
2640
}
2741

42+
await confirmDangerousAction(
43+
`Delete dataset file ${fileId}.\nThe file is permanently removed. This cannot be undone.`,
44+
flags.yes ?? false,
45+
);
46+
2847
const response = await deleteDataset(ctx.client, fileId);
2948

3049
if (settings.quiet) {

packages/commands/src/commands/deploy/delete.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import {
66
ExitCode,
77
type FlagsDef,
88
} from "bailian-cli-core";
9-
import { emitResult, emitBare } from "bailian-cli-runtime";
9+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
1010

1111
const DELETE_FLAGS = {
1212
deployedModel: {
@@ -25,6 +25,10 @@ const DELETE_FLAGS = {
2525
"zh-CN": "跳过本地 STOPPED/FAILED 状态预检查",
2626
},
2727
},
28+
yes: {
29+
type: "switch",
30+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
31+
},
2832
} satisfies FlagsDef;
2933

3034
/**
@@ -40,9 +44,19 @@ export default defineCommand({
4044
"zh-CN": "删除模型部署(状态必须为 STOPPED 或 FAILED)",
4145
},
4246
auth: "apiKey",
43-
usageArgs: "--deployed-model <id> [--skip-precheck]",
47+
usageArgs: "--deployed-model <id> [--skip-precheck] [--yes]",
4448
flags: DELETE_FLAGS,
45-
exampleArgs: ["--deployed-model dep-...", "--deployed-model dep-... --dry-run"],
49+
notes: [
50+
{
51+
"en-US": "Irreversible — the deployment is permanently destroyed.",
52+
"zh-CN": "该操作不可撤销——部署将被永久销毁。",
53+
},
54+
],
55+
exampleArgs: [
56+
"--deployed-model dep-...",
57+
"--deployed-model dep-... --dry-run",
58+
"--deployed-model dep-... --yes",
59+
],
4660
async run(ctx) {
4761
const { settings, flags } = ctx;
4862
const deployedModel = flags.deployedModel;
@@ -73,6 +87,11 @@ export default defineCommand({
7387
}
7488
}
7589

90+
await confirmDangerousAction(
91+
`Delete deployment ${deployedModel}.\nThe deployment is permanently destroyed. This cannot be undone.`,
92+
flags.yes ?? false,
93+
);
94+
7695
const response = await deleteDeployment(ctx.client, deployedModel);
7796

7897
if (settings.quiet) {

packages/commands/src/commands/finetune/delete.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, deleteFineTune, type FlagsDef } from "bailian-cli-core";
2-
import { emitResult, emitBare } from "bailian-cli-runtime";
2+
import { emitResult, emitBare, confirmDangerousAction } from "bailian-cli-runtime";
33

44
const DELETE_FLAGS = {
55
jobId: {
@@ -8,15 +8,23 @@ const DELETE_FLAGS = {
88
description: { "en-US": "Fine-tune job ID (required)", "zh-CN": "微调任务 ID(必填)" },
99
required: true,
1010
},
11+
yes: {
12+
type: "switch",
13+
description: { "en-US": "Skip the confirmation prompt", "zh-CN": "跳过确认提示" },
14+
},
1115
} satisfies FlagsDef;
1216

1317
export default defineCommand({
1418
description: { "en-US": "Delete a fine-tune job record", "zh-CN": "删除微调任务记录" },
1519
auth: "apiKey",
16-
usageArgs: "--job-id <id>",
20+
usageArgs: "--job-id <id> [--yes]",
1721
flags: DELETE_FLAGS,
18-
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run"],
22+
exampleArgs: ["--job-id ft-xxx", "--job-id ft-xxx --dry-run", "--job-id ft-xxx --yes"],
1923
notes: [
24+
{
25+
"en-US": "Irreversible — the job record is permanently removed.",
26+
"zh-CN": "该操作不可撤销——任务记录将被永久删除。",
27+
},
2028
{
2129
"en-US":
2230
"Cancel a RUNNING job first via `finetune cancel` — the platform refuses to delete jobs that are still in flight.",
@@ -32,6 +40,11 @@ export default defineCommand({
3240
return;
3341
}
3442

43+
await confirmDangerousAction(
44+
`Delete fine-tune job record ${jobId}.\nThe job record is permanently removed. This cannot be undone.`,
45+
flags.yes ?? false,
46+
);
47+
3548
const response = await deleteFineTune(ctx.client, jobId);
3649

3750
if (settings.quiet) {

packages/commands/src/commands/quota/update.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { defineCommand, detectOutputFormat, modelsLimitsPath } from "bailian-cli-core";
2-
import { emitResult } from "bailian-cli-runtime";
2+
import { emitResult, confirmDangerousAction } from "bailian-cli-runtime";
33
import { formatNumber } from "../shared/format.ts";
44

55
const MINUTE_SECONDS = 60;
@@ -10,7 +10,7 @@ export default defineCommand({
1010
"zh-CN": "更新模型限流配置(QPM/TPM),或使用 --delete 清除配置",
1111
},
1212
auth: "apiKey",
13-
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete]",
13+
usageArgs: "--model <model> [--rpm <n>] [--tpm <n>] [--delete] [--yes]",
1414
flags: {
1515
model: {
1616
type: "string",
@@ -41,11 +41,19 @@ export default defineCommand({
4141
"zh-CN": "清除该模型的所有自定义限流配置",
4242
},
4343
},
44+
yes: {
45+
type: "switch",
46+
description: {
47+
"en-US": "Skip the confirmation prompt for --delete",
48+
"zh-CN": "使用 --delete 时跳过确认提示",
49+
},
50+
},
4451
},
4552
exampleArgs: [
4653
"--model qwen-plus --rpm 60 --tpm 100000",
4754
"--model qwen3-max --tpm 500000",
4855
"--model qwen-plus --delete",
56+
"--model qwen-plus --delete --yes",
4957
"--model qwen-plus --rpm 60 --output json",
5058
],
5159
notes: [
@@ -55,6 +63,10 @@ export default defineCommand({
5563
"zh-CN":
5664
"未指定的字段将保留当前值(服务端 OVERLAY 合并);--delete 会清除所有自定义限流配置。",
5765
},
66+
{
67+
"en-US": "--delete requires confirmation; pass --yes to skip the prompt in scripts.",
68+
"zh-CN": "--delete 需要确认;脚本中可加 --yes 跳过交互提示。",
69+
},
5870
{
5971
"en-US":
6072
"Setting TPM without an existing QPM limit is rejected server-side — pass --rpm first or together.",
@@ -98,6 +110,13 @@ export default defineCommand({
98110
return;
99111
}
100112

113+
if (flags.delete) {
114+
await confirmDangerousAction(
115+
`Clear all custom rate limits for model ${modelName}.\nYour custom QPM/TPM configuration will be removed.`,
116+
flags.yes ?? false,
117+
);
118+
}
119+
101120
const result = await ctx.client.requestJson<{ request_id?: string }>({
102121
path: modelsLimitsPath(),
103122
method: "POST",

packages/commands/tests/e2e/advisor-recommend.e2e.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import { describe, expect, test } from "vite-plus/test";
2-
import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts";
2+
import { isDashScopeE2EReady, parseStdoutJson, runCommandHelp, runCommandE2e } from "./helpers.ts";
33
import { ADVISOR_ROUTES } from "./topic-routes.ts";
44

55
describe("e2e: advisor recommend", () => {
66
test("advisor recommend --help exits successfully", async () => {
7-
const { stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [
7+
const { stderr, exitCode } = await runCommandHelp(ADVISOR_ROUTES, [
88
"advisor",
99
"recommend",
1010
"--help",

0 commit comments

Comments
 (0)