diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index 3a07f20..749329c 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -2,13 +2,13 @@ ## 架构分层 -| 层级 | 路径 | 测什么 | -| --------------- | ----------------------------------------------------- | --------------------------------------------------------------- | -| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) | -| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);harness `binName: "bl"` | -| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help | -| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 6 条 flat path `--help`;`search --help` 与 harness 一致 | -| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 | +| 层级 | 路径 | 测什么 | +| --------------- | ----------------------------------------------------- | ---------------------------------------------------------------------------------------- | +| **共享基建** | `packages/e2e` | gating、子进程 runner、output、globalSetup(`private`,不发布) | +| **命令 E2E** | `packages/commands/tests/e2e` | help、缺参、dry-run、live(gated);每用例最小路由 | +| **bl smoke** | `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` | 产品 map 全部 path `--help`、分组 help、根 help | +| **kscli smoke** | `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` | 从 `kscli/src/commands.ts` 推导 path/分组;identity(`--version`、`search --help` path) | +| **runtime** | `packages/runtime/tests` | `proxy.e2e`、console 跨域 flag 拒绝 | **依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。 @@ -17,18 +17,20 @@ - 新增/修改 `packages/commands/src/commands` 下的 command 实现 - 新增/修改 `packages/cli/src/commands.ts` 的 `bl` 命令路径 map - 新建或扩展 `packages/commands/tests/e2e/.e2e.test.ts` -- 新增 bl 产品 path → 同步 `registry.smoke`(自动覆盖 leaf path)与 `harness/e2e-command-map.ts` +- 新增 bl 产品 path → `registry.smoke` 自动覆盖 leaf path;commands topic 测试在 `topic-routes.ts` 补最小路由 跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。 +> **规则**:共享 command 行为在 `commands/tests/e2e`;产品 map、identity、CLI-only 命令留在对应产品 `tests/e2e`。 + ## 文件与工具 ### commands E2E - 路径:`packages/commands/tests/e2e/.e2e.test.ts` -- 子进程:`runCommandE2e` from `./helpers.ts`(spawn `harness/main.ts`) +- 子进程:`runCommandE2e(routes, args)` from `./helpers.ts`(spawn `harness/main.ts`,`routes` 为本 topic 最小 path → export 映射) - fixtures:`packages/commands/tests/e2e/fixtures/` -- map:`harness/e2e-command-map.ts`(自维护,初始从 `cli/commands.ts` 复制) +- 路由常量:`topic-routes.ts`(按 topic 维护,**非**全量产品 map) ### 产品 smoke @@ -45,13 +47,12 @@ ## 双层 describe(固定结构) ```ts -// 1) 不 skip:分组 + --help,无密钥、无真实 API +// 1) 不 skip:--help,无密钥、无真实 API(分组 help 由 bl registry.smoke 覆盖) describe("e2e: ", () => { - test(" 分组展示子命令帮助且成功退出", ...); test(" --help 正常退出", ...); }); -// 2) skipIf:缺参 / dry-run / 真实集成;原有集成用例放最后、勿改逻辑 +// 2) skipIf:缺参 / dry-run / 真实集成 describe.skipIf()("e2e: (DashScope …)", () => { test("缺少 -- 时退出为用法错误 (2)", ...); test(" --dry-run ...", ...); // 若适用 @@ -71,18 +72,15 @@ describe.skipIf()("e2e: (DashScope …)", () => { ## 用例类型 -1. **分组 help**:`runCommandE2e(["image"])` → `exitCode === 0`,stdout+stderr 含子命令名 -2. **--help**:`runCommandE2e([..., "--help"])` → stderr 含主要 flags -3. **缺参**:带一个无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2`,stderr 匹配 `--flag|Missing required argument` -4. **--dry-run**:仅当实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本,不入网 -5. **真实集成**:保留既有用例名称与断言;放在 skip 块**末尾** +1. **--help**:`runCommandE2e(ROUTES, [..., "--help"])` → stderr 含主要 flags +2. **缺参**:带无害全局 flag(如 `--quiet`)且不传 required flag → `exitCode === 2` +3. **--dry-run**:实现在联网/上传/写盘**之前**返回;断言 stdout JSON/文本 +4. **真实集成**:放在 skip 块**末尾** -## 契约与防漂移 +## 增删命令同步 -- `harness/e2e-command-map.contract.test.ts`:path 唯一、合法 export、白名单、测试 argv 前缀在 map 中 -- Advisory(不阻塞 CI):`node tools/compare-e2e-command-map.ts` 对比 `cli/commands.ts` 与 harness map - -增删命令须同步三处:**实现 export** + **`cli/commands.ts`** + **`e2e-command-map.ts`**(见 [command-add-remove.md](command-add-remove.md))。 +- **commands export** + **topic 路由**(`topic-routes.ts` 或测试文件内 `ROUTES`)+ **产品 map**(`cli/commands.ts` / `kscli/commands.ts`) +- 分组 help 由产品 `registry.smoke` 负责,无需在 commands 重复 ## 安全与例外 @@ -93,10 +91,10 @@ describe.skipIf()("e2e: (DashScope …)", () => { ## 新增 command 检查清单 -- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `harness/e2e-command-map.ts` 登记 +- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `topic-routes.ts` 补最小路由 - [ ] `packages/commands/tests/e2e/.e2e.test.ts`(新建或扩展) - [ ] 若改了 `usageArgs` / `flags` / `exampleArgs`,跑 `pnpm --filter bailian-cli run generate:reference` 更新 `skills/bailian-cli/reference/` 并提交 -- [ ] 顶层:分组 help + 子命令 `--help`(多子命令则各一条 help) +- [ ] 子命令 `--help`(分组 help 由 bl `registry.smoke` 覆盖) - [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条 - [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序 - [ ] `vp test packages/commands/tests/e2e/` 通过 @@ -108,20 +106,21 @@ pnpm --filter bailian-cli-commands exec vp test packages/commands/tests/e2e/text pnpm --filter bailian-cli exec vp test packages/cli/tests/e2e/registry.smoke.e2e.test.ts pnpm --filter knowledge-studio-cli exec vp test packages/kscli/tests/e2e/registry.smoke.e2e.test.ts pnpm --filter bailian-cli-runtime exec vp test packages/runtime/tests/proxy.e2e.test.ts -node tools/compare-e2e-command-map.ts ``` ## 示例片段 ```ts +import { FOO_ROUTES } from "./topic-routes.ts"; + test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["foo", "bar", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(FOO_ROUTES, ["foo", "bar", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--prompt|Missing required argument/i); }); test("foo bar --dry-run 仅输出计划", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FOO_ROUTES, [ "foo", "bar", "--dry-run", diff --git a/docs/agents/command-add-remove.md b/docs/agents/command-add-remove.md index 4bd189a..fb333c1 100644 --- a/docs/agents/command-add-remove.md +++ b/docs/agents/command-add-remove.md @@ -93,17 +93,16 @@ packages/commands/src/index.ts ### D. 测试层 - [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/commands/tests/e2e/.e2e.test.ts` -- [ ] 同步 `packages/commands/tests/e2e/harness/e2e-command-map.ts` 与 `E2E_COMMAND_EXPORTS` -- [ ] `packages/cli/src/commands.ts` 增删 path 后跑 `node tools/compare-e2e-command-map.ts`(advisory)确认与 harness 一致 -- [ ] bl 产品 path 变更由 `packages/cli/tests/e2e/registry.smoke.e2e.test.ts` 覆盖;kscli 变更同步 `packages/kscli/tests/e2e/registry.smoke.e2e.test.ts` -- [ ] 删除命令时一并删对应 commands e2e / README 示例 / reference 生成结果 / harness map 条目 +- [ ] 同步 `packages/commands/tests/e2e/topic-routes.ts`(该 topic 的最小 path → export 映射) +- [ ] bl 产品 path 变更由 `registry.smoke` 自动覆盖;kscli 变更同步 `kscli/src/commands.ts` 与 `registry.smoke` +- [ ] 删除命令时一并删对应 commands e2e / README 示例 / reference / topic 路由条目 - [ ] 如果 shared command 在不同入口路径下复用,至少确保 commands e2e 覆盖 `bl` path;`kscli` 入口改动需补对应 smoke 或说明不测 flat path live ### E. 重命名特殊处理 - [ ] 全仓 grep **旧命令名字符串**,确保以下位置全部更新: - `packages/cli/src/commands.ts` map key - - `packages/kscli/src/main.ts` map key(如适用) + - `packages/kscli/src/commands.ts` map key(如适用) - 用户可见 hint / README / tests - `skills/bailian-cli/reference/`(重建后检查并提交) - [ ] 检查 `usageArgs` / `exampleArgs` 没有硬编码旧的 `bl ` 前缀 @@ -114,8 +113,7 @@ packages/commands/src/index.ts pnpm run sync:skill-assets pnpm -F bailian-cli exec tsx src/main.ts --help pnpm -F bailian-cli exec tsx src/main.ts -vp test packages/cli/tests/e2e/.e2e.test.ts -node tools/compare-e2e-command-map.ts +vp test packages/commands/tests/e2e/.e2e.test.ts ``` 如改了 `kscli` 入口: diff --git a/packages/cli/tests/e2e/registry.smoke.e2e.test.ts b/packages/cli/tests/e2e/registry.smoke.e2e.test.ts index 8d48c26..abcb2b0 100644 --- a/packages/cli/tests/e2e/registry.smoke.e2e.test.ts +++ b/packages/cli/tests/e2e/registry.smoke.e2e.test.ts @@ -1,23 +1,8 @@ import { describe, expect, test } from "vite-plus/test"; +import { deriveGroupPaths } from "e2e/registry-smoke"; import { commands } from "../../src/commands.ts"; import { runCli } from "./helpers.ts"; -/** 从命令 map 推导有子命令的分组路径 */ -function deriveGroupPaths(commandPaths: string[]): string[] { - const groups = new Set(); - for (const path of commandPaths) { - const parts = path.split(" "); - for (let i = 1; i < parts.length; i++) { - const prefix = parts.slice(0, i).join(" "); - const hasChildren = commandPaths.some( - (candidate) => candidate.startsWith(`${prefix} `) && candidate !== prefix, - ); - if (hasChildren) groups.add(prefix); - } - } - return [...groups].sort(); -} - const commandPaths = Object.keys(commands).sort(); const groupPaths = deriveGroupPaths(commandPaths); diff --git a/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts b/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts index 5f01005..ec2fb2b 100644 --- a/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts +++ b/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts @@ -1,15 +1,14 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { ADVISOR_ROUTES } from "./topic-routes.ts"; describe("e2e: advisor recommend", () => { - test("advisor shows subcommand groups and exits successfully", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["advisor"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/advisor|recommend/i); - }); - test("advisor recommend --help exits successfully", async () => { - const { stderr, exitCode } = await runCommandE2e(["advisor", "recommend", "--help"]); + const { stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ + "advisor", + "recommend", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recommend|--message|dry-run/i); }); @@ -17,13 +16,17 @@ describe("e2e: advisor recommend", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () => { test("advisor recommend without --message errors as usage error (2)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["advisor", "recommend", "--quiet"]); + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ + "advisor", + "recommend", + "--quiet", + ]); expect(exitCode).toBe(2); expect(`${stdout}\n${stderr}`).toMatch(/--message|Usage:/i); }); test("advisor recommend --dry-run outputs intent analysis and candidates", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--dry-run", @@ -63,7 +66,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () }, 60_000); test("advisor recommend full flow returns results", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--message", @@ -96,7 +99,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () // ---- Model preference: positive cases ---- test("scoped preference — intent contains modelPreference.mode=scoped when family is specified", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--dry-run", @@ -116,7 +119,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () }, 60_000); test("comparison preference — intent contains modelPreference.mode=comparison when comparing models", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--dry-run", @@ -136,7 +139,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () }, 60_000); test("excludes preference — intent detects modelPreference when excluding models", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--dry-run", @@ -166,7 +169,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () // ---- Model preference: negative cases ---- test("no preference — intent has no modelPreference or mode=unconstrained for generic queries", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(ADVISOR_ROUTES, [ "advisor", "recommend", "--dry-run", diff --git a/packages/commands/tests/e2e/auth.e2e.test.ts b/packages/commands/tests/e2e/auth.e2e.test.ts index 8cc340b..a4f5425 100644 --- a/packages/commands/tests/e2e/auth.e2e.test.ts +++ b/packages/commands/tests/e2e/auth.e2e.test.ts @@ -1,45 +1,39 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { AUTH_ROUTES } from "./topic-routes.ts"; /** * Auth 相关 E2E:只验证 CLI 进程能正常解析参数并退出。 */ describe("e2e: auth", () => { - test("auth 分组展示子命令帮助且退出码为 0", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["auth"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/auth|Authentication|login|logout|status/i); - }); - test("auth login --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "login", "--help"]); + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/login|api-key/i); expect(stderr).toMatch(/--console-site/); }); test("auth logout --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "logout", "--help"]); + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/logout|dry-run|yes/i); }); test("auth status --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "status", "--help"]); + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/status|output/i); }); test("auth login 缺少 --api-key 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "login", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--api-key|Usage:/i); }); test("auth login --dry-run --api-key 不发起校验与落盘", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "login", "--dry-run", @@ -51,7 +45,7 @@ describe("e2e: auth", () => { }); test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "login", "--dry-run", @@ -67,7 +61,12 @@ describe("e2e: auth", () => { }); test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "login", "--output", "json"]); + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--output", + "json", + ]); expect(exitCode).toBe(2); const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } }; expect(err.error?.code).toBe(2); @@ -75,14 +74,18 @@ describe("e2e: auth", () => { }); test("auth logout --dry-run 不写入配置", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["auth", "logout", "--dry-run"]); + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "logout", + "--dry-run", + ]); expect(exitCode, stderr).toBe(0); expect(stdout).toContain("No changes made."); expect(stderr).not.toContain("Cleared api_key"); }); test("auth logout --dry-run --quiet", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "logout", "--dry-run", @@ -93,7 +96,7 @@ describe("e2e: auth", () => { }); test("auth logout --dry-run --output json(不清除密钥)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "logout", "--dry-run", @@ -106,7 +109,7 @@ describe("e2e: auth", () => { }); test.skipIf(!isDashScopeE2EReady())("auth status 文本输出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "status", "--output", @@ -119,7 +122,7 @@ describe("e2e: auth", () => { }); test.skipIf(!isDashScopeE2EReady())("auth status --output json", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "status", "--output", @@ -138,6 +141,7 @@ describe("e2e: auth", () => { "auth status --output json --quiet(base_url 经 env 指定;凭证域 flag 对 status 不可见)", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + AUTH_ROUTES, ["auth", "status", "--output", "json", "--quiet"], { DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com" }, ); @@ -149,7 +153,7 @@ describe("e2e: auth", () => { ); test("auth status 不接受凭证域覆盖 flag(--base-url 报 Unknown flag)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ "auth", "status", "--base-url", diff --git a/packages/commands/tests/e2e/config.e2e.test.ts b/packages/commands/tests/e2e/config.e2e.test.ts index 1b6b1af..66c7122 100644 --- a/packages/commands/tests/e2e/config.e2e.test.ts +++ b/packages/commands/tests/e2e/config.e2e.test.ts @@ -1,32 +1,26 @@ import { describe, expect, test } from "vite-plus/test"; import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { CONFIG_ROUTES } from "./topic-routes.ts"; /** * Config 相关 E2E */ describe("e2e: config", () => { - test("config 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["config"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/config|show|set/i); - }); - test("config show --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["config", "show", "--help"]); + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "show", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/show|config/i); }); test("config set --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["config", "set", "--help"]); + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/set|--key|--value/i); }); test("config show --output json", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "show", "--output", @@ -44,7 +38,7 @@ describe("e2e: config", () => { }); test("config show --output text", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "show", "--output", @@ -55,13 +49,13 @@ describe("e2e: config", () => { }); test("config set 缺少 --key / --value 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["config", "set", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--key|--value|Usage:/i); }); test("config set 非法 key 时退出为用法错误", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--key", @@ -74,7 +68,7 @@ describe("e2e: config", () => { }); test("config set 非法 output", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--key", @@ -87,7 +81,7 @@ describe("e2e: config", () => { }); test("config set 非法 timeout", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--key", @@ -100,7 +94,7 @@ describe("e2e: config", () => { }); test("config set --dry-run 不落盘(仅输出 would_set)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--dry-run", @@ -117,7 +111,7 @@ describe("e2e: config", () => { }); test("config set --dry-run 支持连字符别名 key", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--dry-run", @@ -134,7 +128,7 @@ describe("e2e: config", () => { }); test("config set --dry-run 支持 AccessKey 短字段别名", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--dry-run", @@ -151,7 +145,7 @@ describe("e2e: config", () => { }); test("config set 不接受旧 OpenAPI AccessKey 字段名", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [ "config", "set", "--key", diff --git a/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts b/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts index f0d1c8f..4fafea5 100644 --- a/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts +++ b/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { CONSOLE_FLAGS_DRY_RUN_ROUTES } from "./topic-routes.ts"; type ConsoleDryRunMeta = { consoleRegion?: string; @@ -9,7 +10,11 @@ type ConsoleDryRunMeta = { describe("e2e: console global flags (dry-run)", () => { test("console call --help 不暴露命令级 region/site,示例使用 --console-region", async () => { - const { stderr, exitCode } = await runCommandE2e(["console", "call", "--help"]); + const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ + "console", + "call", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api /); expect(stderr).toMatch(/--data /); @@ -19,7 +24,11 @@ describe("e2e: console global flags (dry-run)", () => { }); test("auth login --help:自有 flag 含 --console-site,不含其余 console 域 flag", async () => { - const { stderr, exitCode } = await runCommandE2e(["auth", "login", "--help"]); + const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ + "auth", + "login", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api-key /); expect(stderr).toMatch(/--base-url /); @@ -29,7 +38,7 @@ describe("e2e: console global flags (dry-run)", () => { }); test("console call --dry-run 默认 consoleRegion 为 cn-beijing", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ "console", "call", "--api", @@ -47,7 +56,7 @@ describe("e2e: console global flags (dry-run)", () => { }); test("console call --dry-run --console-region / --console-site / --console-switch-agent 透传", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ "console", "call", "--api", @@ -72,7 +81,7 @@ describe("e2e: console global flags (dry-run)", () => { }); test("console call 拒绝未知全局 flag --region", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ "console", "call", "--api", @@ -88,7 +97,7 @@ describe("e2e: console global flags (dry-run)", () => { }); test("mcp list --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ "mcp", "list", "--dry-run", @@ -103,7 +112,7 @@ describe("e2e: console global flags (dry-run)", () => { }); test("quota check --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(CONSOLE_FLAGS_DRY_RUN_ROUTES, [ "quota", "check", "--dry-run", diff --git a/packages/commands/tests/e2e/dataset.e2e.test.ts b/packages/commands/tests/e2e/dataset.e2e.test.ts index 6dd19e2..7b68822 100644 --- a/packages/commands/tests/e2e/dataset.e2e.test.ts +++ b/packages/commands/tests/e2e/dataset.e2e.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vite-plus/test"; import { join } from "path"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; +import { DATASET_ROUTES } from "./topic-routes.ts"; /** * Dataset (fine-tune file) E2E. @@ -16,22 +17,19 @@ import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } f */ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { - test("dataset --help 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["dataset"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/upload|list|get|delete|validate/); - }); - test("dataset upload --help 正常退出并展示 --file", async () => { - const { stderr, exitCode } = await runCommandE2e(["dataset", "upload", "--help"]); + const { stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ + "dataset", + "upload", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--file|jsonl/i); }); test("dataset validate 通过合法 JSONL", async () => { const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -47,7 +45,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate 拒绝 pretty-printed JSON 并以非零码退出", async () => { const file = join(e2eFixturesDir, ".dataset-invalid.jsonl"); - const { stdout, exitCode } = await runCommandE2e([ + const { stdout, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -66,7 +64,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset upload --no-validate --dry-run 跳过本地校验", async () => { const file = join(e2eFixturesDir, ".dataset-invalid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "upload", "--file", @@ -86,7 +84,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { // No --schema: a record carrying chosen/rejected is auto-detected as DPO // and the valid fixture passes. const file = join(e2eFixturesDir, ".dataset-dpo-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -104,7 +102,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { // No --schema: a record carrying `text` (and no `messages`) is auto-detected // as CPT and the valid fixture passes. const file = join(e2eFixturesDir, ".dataset-cpt-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -120,7 +118,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate --schema cpt 拒绝缺失 text 的记录", async () => { const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); // SFT {messages}, no text - const { stdout, exitCode } = await runCommandE2e([ + const { stdout, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -140,7 +138,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate --schema dpo 拒绝缺失 rejected 的记录", async () => { const file = join(e2eFixturesDir, ".dataset-dpo-invalid.jsonl"); - const { stdout, exitCode } = await runCommandE2e([ + const { stdout, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -161,7 +159,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate --schema chatml 忽略 chosen/rejected(不报 DPO 错误)", async () => { // Same invalid-DPO file, but --schema chatml must not run DPO checks. const file = join(e2eFixturesDir, ".dataset-dpo-invalid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -179,7 +177,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate --schema 以非零码退出", async () => { const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "validate", "--file", @@ -195,7 +193,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset upload --dry-run 转发 --schema", async () => { const file = join(e2eFixturesDir, ".dataset-dpo-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "upload", "--file", @@ -215,7 +213,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => { test("dataset list --output json 返回结构化结果", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DATASET_ROUTES, [ "dataset", "list", "--page-size", diff --git a/packages/commands/tests/e2e/deploy.e2e.test.ts b/packages/commands/tests/e2e/deploy.e2e.test.ts index b2b4891..6e64344 100644 --- a/packages/commands/tests/e2e/deploy.e2e.test.ts +++ b/packages/commands/tests/e2e/deploy.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { DEPLOY_ROUTES } from "./topic-routes.ts"; /** * Deploy E2E. @@ -14,21 +15,14 @@ import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.t */ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { - test("deploy 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["deploy"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/create|list|get|delete|update|scale|models/); - }); - test("deploy create --help 正常退出并展示必填项", async () => { - const { stderr, exitCode } = await runCommandE2e(["deploy", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, ["deploy", "create", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--model|--name/i); }); test("deploy create --dry-run 构造 lora 部署请求体", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", "create", "--model", @@ -57,7 +51,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy scale --dry-run 转发 capacity", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", "scale", "--deployed-model", @@ -80,7 +74,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy update --dry-run 转发 rate limits", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", "update", "--deployed-model", @@ -104,7 +98,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy scale --dry-run 缺少 capacity/input-tpm/output-tpm 时报错", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", "scale", "--deployed-model", @@ -124,7 +118,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { ["models", ["--source", "custom"]], ["delete", ["--deployed-model", "dep-xxx"]], ])("deploy %s --dry-run 发出结构化动作", async (sub, extra) => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", sub, ...extra, @@ -147,7 +141,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => { * 而非进程崩溃),即视为通过。 */ test("deploy list --output json 优雅返回(空账号或鉴权失败均通过)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(DEPLOY_ROUTES, [ "deploy", "list", "--page-size", diff --git a/packages/commands/tests/e2e/file-upload.e2e.test.ts b/packages/commands/tests/e2e/file-upload.e2e.test.ts index f49c6d2..5b7912c 100644 --- a/packages/commands/tests/e2e/file-upload.e2e.test.ts +++ b/packages/commands/tests/e2e/file-upload.e2e.test.ts @@ -1,21 +1,19 @@ import { describe, expect, test } from "vite-plus/test"; import { join } from "path"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; +import { FILE_UPLOAD_ROUTES } from "./topic-routes.ts"; /** * File upload E2E */ describe("e2e: file upload", () => { - test("file 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["file"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/file|upload/i); - }); - test("file upload --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["file", "upload", "--help"]); + const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [ + "file", + "upload", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/upload|--file|--model/i); }); @@ -23,7 +21,7 @@ describe("e2e: file upload", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: file upload(DashScope)", () => { test("file upload 缺少 --file 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [ "file", "upload", "--model", @@ -35,14 +33,19 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: file upload(DashScope)", () => test("file upload 缺少 --model 时报用法错误并退出 (2)", async () => { const testFile = join(e2eFixturesDir, ".smoke-32.png"); - const { stderr, exitCode } = await runCommandE2e(["file", "upload", "--file", testFile]); + const { stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [ + "file", + "upload", + "--file", + testFile, + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--model|Usage:/i); }); test("上传文件成功返回oss临时 URL", async () => { const testFile = join(e2eFixturesDir, ".smoke-32.png"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FILE_UPLOAD_ROUTES, [ "file", "upload", "--file", diff --git a/packages/commands/tests/e2e/finetune.e2e.test.ts b/packages/commands/tests/e2e/finetune.e2e.test.ts index c5b32c1..53869f1 100644 --- a/packages/commands/tests/e2e/finetune.e2e.test.ts +++ b/packages/commands/tests/e2e/finetune.e2e.test.ts @@ -1,6 +1,7 @@ import { describe, expect, test } from "vite-plus/test"; import { join } from "path"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; +import { FINETUNE_ROUTES } from "./topic-routes.ts"; /** * Fine-tune E2E. @@ -15,21 +16,18 @@ import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } f */ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { - test("finetune 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["finetune"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/create|list|get|cancel|delete|logs|checkpoints|export|watch|capability/); - }); - test("finetune create --help 正常退出并展示必填项", async () => { - const { stderr, exitCode } = await runCommandE2e(["finetune", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ + "finetune", + "create", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--model|--datasets/i); }); test("finetune create --dry-run 构造 SFT 默认请求体", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -62,7 +60,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 转发训练类型与超参", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -115,7 +113,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --training-type 拒绝不支持的训练类型值", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -133,7 +131,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune create --dry-run 把本地路径标记为 pending 上传且不发起网络请求", async () => { const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -163,7 +161,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --datasets 为空时拒绝", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -183,7 +181,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { // so this is fully offline (no key, no network) — the proof is that the // error is the gate message AND no "Uploaded …" line ever appears. const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -204,7 +202,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { // Even with --batch-size 1 (server clamps to 8), 3 samples <= 8 still trips // the gate — confirms the gate uses the clamped/effective batch, not the raw. const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -231,7 +229,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { ["watch", ["--job-id", "ft-xxx"]], ["capability", ["--model", "qwen3-8b"]], ])("finetune %s --dry-run 发出结构化动作", async (sub, extra) => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", sub, ...extra, @@ -245,7 +243,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 解析多 datasets 中的空白", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "create", "--model", @@ -273,7 +271,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (DashScope)", () => { * 而非进程崩溃),即视为通过。 */ test("finetune list --output json 优雅返回(空账号或鉴权失败均通过)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(FINETUNE_ROUTES, [ "finetune", "list", "--page-size", diff --git a/packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts b/packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts deleted file mode 100644 index e384570..0000000 --- a/packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { readdirSync, readFileSync } from "fs"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -import { describe, expect, test } from "vite-plus/test"; -import * as commandExports from "bailian-cli-commands"; -import { E2E_COMMAND_EXPORTS, e2eCommandMap } from "./e2e-command-map.ts"; - -const e2eDir = dirname(fileURLToPath(import.meta.url)); - -const exportedCommands = new Set( - Object.values(commandExports).filter( - (value) => typeof value === "object" && value !== null && "run" in value, - ), -); - -/** 从 runCommandE2e([...]) 调用中提取命令 path 前缀 */ -function extractCommandPathsFromSource(source: string): string[] { - const paths: string[] = []; - const callPattern = /runCommandE2e\(\[([\s\S]*?)\]/g; - - for (const match of source.matchAll(callPattern)) { - const arrayBody = match[1] ?? ""; - const tokens: string[] = []; - const stringPattern = /"([^"\\]*(?:\\.[^"\\]*)*)"|'([^'\\]*(?:\\.[^'\\]*)*)'/g; - for (const tokenMatch of arrayBody.matchAll(stringPattern)) { - const token = tokenMatch[1] ?? tokenMatch[2] ?? ""; - if (token.startsWith("--")) break; - tokens.push(token); - } - if (tokens.length > 0) paths.push(tokens.join(" ")); - } - - return paths; -} - -describe("e2e-command-map contract", () => { - test("path 唯一", () => { - const paths = Object.keys(e2eCommandMap); - expect(new Set(paths).size).toBe(paths.length); - }); - - test("value 均为 bailian-cli-commands 合法 export", () => { - for (const [path, command] of Object.entries(e2eCommandMap)) { - expect(exportedCommands.has(command), `invalid export for path "${path}"`).toBe(true); - } - }); - - test("E2E_COMMAND_EXPORTS 白名单 ⊆ map values", () => { - const mapValues = new Set(Object.values(e2eCommandMap)); - for (const command of E2E_COMMAND_EXPORTS) { - expect(mapValues.has(command)).toBe(true); - } - }); - - test("各 e2e 测试 argv 前缀在 map 中有对应 path", () => { - const mapPaths = new Set(Object.keys(e2eCommandMap)); - const testFiles = readdirSync(e2eDir).filter( - (name) => name.endsWith(".e2e.test.ts") && name !== "proxy.e2e.test.ts", - ); - - const missing: string[] = []; - for (const file of testFiles) { - const source = readFileSync(join(e2eDir, file), "utf8"); - for (const path of extractCommandPathsFromSource(source)) { - if (!mapPaths.has(path)) missing.push(`${file}: ${path}`); - } - } - - expect(missing).toEqual([]); - }); -}); diff --git a/packages/commands/tests/e2e/harness/e2e-command-map.ts b/packages/commands/tests/e2e/harness/e2e-command-map.ts deleted file mode 100644 index bc976b5..0000000 --- a/packages/commands/tests/e2e/harness/e2e-command-map.ts +++ /dev/null @@ -1,233 +0,0 @@ -import type { AnyCommand } from "bailian-cli-core"; -import { - authLogin, - authStatus, - authLogout, - textChat, - textOmni, - imageGenerate, - imageEdit, - videoGenerate, - videoEdit, - videoRef, - videoTaskGet, - videoDownload, - visionDescribe, - configShow, - configSet, - update, - appCall, - appList, - memoryAdd, - memorySearch, - memoryList, - memoryUpdate, - memoryDelete, - memoryProfileCreate, - memoryProfileGet, - knowledgeRetrieve, - knowledgeSearch, - knowledgeChat, - mcpCall, - mcpList, - mcpTools, - searchWeb, - speechSynthesize, - speechRecognize, - fileUpload, - consoleCall, - usageFree, - usageFreetier, - usageStats, - pipelineRun, - pipelineValidate, - advisorRecommend, - workspaceList, - quotaList, - quotaRequest, - quotaHistory, - quotaCheck, - datasetUpload, - datasetList, - datasetGet, - datasetDelete, - datasetValidate, - finetuneCreate, - finetuneList, - finetuneGet, - finetuneCancel, - finetuneDelete, - finetuneLogs, - finetuneCheckpoints, - finetuneExport, - finetuneWatch, - finetuneCapability, - deployCreate, - deployList, - deployGet, - deployModels, - deployScale, - deployUpdate, - deployDelete, - tokenPlanListSeats, - tokenPlanCreateKey, - tokenPlanAssignSeats, - tokenPlanAddMember, -} from "bailian-cli-commands"; - -/** - * E2E 专用全量路由表(与产品 map 独立维护)。 - * 新增/删改命令时须同步:commands 实现、产品 map、本文件。 - */ -export const e2eCommandMap: Record = { - "auth login": authLogin, - "auth status": authStatus, - "auth logout": authLogout, - "text chat": textChat, - omni: textOmni, - "image generate": imageGenerate, - "image edit": imageEdit, - "video generate": videoGenerate, - "video edit": videoEdit, - "video ref": videoRef, - "video task get": videoTaskGet, - "video download": videoDownload, - "vision describe": visionDescribe, - "config show": configShow, - "config set": configSet, - update, - "app call": appCall, - "app list": appList, - "memory add": memoryAdd, - "memory search": memorySearch, - "memory list": memoryList, - "memory update": memoryUpdate, - "memory delete": memoryDelete, - "memory profile create": memoryProfileCreate, - "memory profile get": memoryProfileGet, - "knowledge retrieve": knowledgeRetrieve, - "knowledge search": knowledgeSearch, - "knowledge chat": knowledgeChat, - "mcp call": mcpCall, - "mcp list": mcpList, - "mcp tools": mcpTools, - "search web": searchWeb, - "speech synthesize": speechSynthesize, - "speech recognize": speechRecognize, - "file upload": fileUpload, - "console call": consoleCall, - "usage free": usageFree, - "usage freetier": usageFreetier, - "usage stats": usageStats, - "pipeline run": pipelineRun, - "pipeline validate": pipelineValidate, - "advisor recommend": advisorRecommend, - "workspace list": workspaceList, - "quota list": quotaList, - "quota request": quotaRequest, - "quota history": quotaHistory, - "quota check": quotaCheck, - "dataset upload": datasetUpload, - "dataset list": datasetList, - "dataset get": datasetGet, - "dataset delete": datasetDelete, - "dataset validate": datasetValidate, - "finetune create": finetuneCreate, - "finetune list": finetuneList, - "finetune get": finetuneGet, - "finetune cancel": finetuneCancel, - "finetune delete": finetuneDelete, - "finetune logs": finetuneLogs, - "finetune checkpoints": finetuneCheckpoints, - "finetune export": finetuneExport, - "finetune watch": finetuneWatch, - "finetune capability": finetuneCapability, - "deploy create": deployCreate, - "deploy list": deployList, - "deploy get": deployGet, - "deploy models": deployModels, - "deploy scale": deployScale, - "deploy update": deployUpdate, - "deploy delete": deployDelete, - "token-plan list-seats": tokenPlanListSeats, - "token-plan create-key": tokenPlanCreateKey, - "token-plan assign-seats": tokenPlanAssignSeats, - "token-plan add-member": tokenPlanAddMember, -}; - -/** 须在 e2eCommandMap 中出现至少一次的 commands export(契约测试用) */ -export const E2E_COMMAND_EXPORTS = [ - authLogin, - authStatus, - authLogout, - textChat, - textOmni, - imageGenerate, - imageEdit, - videoGenerate, - videoEdit, - videoRef, - videoTaskGet, - videoDownload, - visionDescribe, - configShow, - configSet, - update, - appCall, - appList, - memoryAdd, - memorySearch, - memoryList, - memoryUpdate, - memoryDelete, - memoryProfileCreate, - memoryProfileGet, - knowledgeRetrieve, - knowledgeSearch, - knowledgeChat, - mcpCall, - mcpList, - mcpTools, - searchWeb, - speechSynthesize, - speechRecognize, - fileUpload, - consoleCall, - usageFree, - usageFreetier, - usageStats, - pipelineRun, - pipelineValidate, - advisorRecommend, - workspaceList, - quotaList, - quotaRequest, - quotaHistory, - quotaCheck, - datasetUpload, - datasetList, - datasetGet, - datasetDelete, - datasetValidate, - finetuneCreate, - finetuneList, - finetuneGet, - finetuneCancel, - finetuneDelete, - finetuneLogs, - finetuneCheckpoints, - finetuneExport, - finetuneWatch, - finetuneCapability, - deployCreate, - deployList, - deployGet, - deployModels, - deployScale, - deployUpdate, - deployDelete, - tokenPlanListSeats, - tokenPlanCreateKey, - tokenPlanAssignSeats, - tokenPlanAddMember, -] as const; diff --git a/packages/commands/tests/e2e/harness/identity.ts b/packages/commands/tests/e2e/harness/identity.ts new file mode 100644 index 0000000..ff6d5f1 --- /dev/null +++ b/packages/commands/tests/e2e/harness/identity.ts @@ -0,0 +1,13 @@ +import pkg from "../../../package.json" with { type: "json" }; + +/** + * commands E2E harness 产品身份。 + * version 取自 commands 包(与 monorepo 同步);npmPackage 使用非发布名以阻断 + * versionCheckStage 的 registry 查询与 CI 中的 auto-update。 + */ +export const E2E_HARNESS_IDENTITY = { + binName: "bl", + version: pkg.version, + clientName: "commands-e2e", + npmPackage: "bailian-cli-commands-e2e-harness", +} as const; diff --git a/packages/commands/tests/e2e/harness/main.ts b/packages/commands/tests/e2e/harness/main.ts index 0b8941e..a6c65d6 100644 --- a/packages/commands/tests/e2e/harness/main.ts +++ b/packages/commands/tests/e2e/harness/main.ts @@ -1,10 +1,29 @@ +import * as cmd from "bailian-cli-commands"; +import type { AnyCommand } from "bailian-cli-core"; import { createCli } from "bailian-cli-runtime"; -import pkg from "../../../../cli/package.json" with { type: "json" }; -import { e2eCommandMap } from "./e2e-command-map.ts"; +import { E2E_HARNESS_IDENTITY } from "./identity.ts"; -void createCli(e2eCommandMap, { - binName: "bl", - version: pkg.version, - clientName: "commands-e2e", - npmPackage: "bailian-cli-commands", -}).run(); +interface RouteSpec { + path: string; + export: string; +} + +function buildRoutesFromEnv(): Record { + const raw = process.env.BAILIAN_E2E_ROUTES; + if (!raw?.trim()) { + throw new Error("BAILIAN_E2E_ROUTES is required for commands E2E harness"); + } + const spec = JSON.parse(raw) as RouteSpec[]; + const routes: Record = {}; + const lib = cmd as Record; + for (const { path, export: exportName } of spec) { + const command = lib[exportName]; + if (typeof command !== "object" || command === null || !("run" in command)) { + throw new Error(`Unknown bailian-cli-commands export: ${exportName}`); + } + routes[path] = command as AnyCommand; + } + return routes; +} + +void createCli(buildRoutesFromEnv(), E2E_HARNESS_IDENTITY).run(); diff --git a/packages/commands/tests/e2e/helpers.ts b/packages/commands/tests/e2e/helpers.ts index 4082a70..0ee7375 100644 --- a/packages/commands/tests/e2e/helpers.ts +++ b/packages/commands/tests/e2e/helpers.ts @@ -9,6 +9,7 @@ import { parseStdoutJson, } from "e2e/output"; import { runNodeMain, type RunCliResult } from "e2e/runner"; +import type { E2eRouteExports } from "./topic-routes.ts"; export { cliTimeoutPrefix, @@ -39,13 +40,26 @@ export const commandsPackageRoot = join(e2eDir, "..", ".."); /** E2E fixtures 目录 */ export const e2eFixturesDir = join(e2eDir, "fixtures"); -/** 通过 harness 子进程执行 E2E 命令 */ +function serializeRoutes(routes: E2eRouteExports): string { + return JSON.stringify( + Object.entries(routes).map(([path, exportName]) => ({ path, export: exportName })), + ); +} + +/** + * 通过 harness 子进程执行命令 E2E。 + * `routes` 为本用例所需的最小 path → export 映射,不维护全量产品 map。 + */ export async function runCommandE2e( + routes: E2eRouteExports, args: string[], envOverrides: NodeJS.ProcessEnv = {}, ): Promise { return runNodeMain(harnessMainTs, args, { cwd: commandsPackageRoot, - env: envOverrides, + env: { + BAILIAN_E2E_ROUTES: serializeRoutes(routes), + ...envOverrides, + }, }); } diff --git a/packages/commands/tests/e2e/image-edit.e2e.test.ts b/packages/commands/tests/e2e/image-edit.e2e.test.ts index 3fd3272..486bbca 100644 --- a/packages/commands/tests/e2e/image-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/image-edit.e2e.test.ts @@ -9,27 +9,21 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { IMAGE_ROUTES } from "./topic-routes.ts"; /** * Image edit E2E */ describe("e2e: image edit", () => { - test("image 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["image"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/image|generate|edit/i); - }); - test("image edit --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["image", "edit", "--help"]); + const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "edit", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/edit|--image|--prompt|--async|--concurrent/i); }); test("image edit --dry-run 接受 async 模型的 --async 与 --concurrent", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [ "image", "edit", "--dry-run", @@ -56,21 +50,31 @@ describe("e2e: image edit", () => { describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: image edit", () => { test("image edit 缺少 --image 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["image", "edit", "--prompt", "仅提示词"]); + const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [ + "image", + "edit", + "--prompt", + "仅提示词", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--image|Usage:/i); }); test("image edit 缺少 --prompt 时报用法错误并退出 (2)", async () => { const testPng = join(e2eFixturesDir, ".smoke-32.png"); - const { stderr, exitCode } = await runCommandE2e(["image", "edit", "--image", testPng]); + const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [ + "image", + "edit", + "--image", + testPng, + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--prompt|Usage:/i); }); test("【qwen-image-2.0】图片编辑", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const gen = await runCommandE2e([ + const gen = await runCommandE2e(IMAGE_ROUTES, [ "image", "generate", "--model", @@ -90,7 +94,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: ima expect(imagePath).toBeTruthy(); - const ed = await runCommandE2e([ + const ed = await runCommandE2e(IMAGE_ROUTES, [ "image", "edit", "--model", diff --git a/packages/commands/tests/e2e/image-generate.e2e.test.ts b/packages/commands/tests/e2e/image-generate.e2e.test.ts index 7bee37e..b987536 100644 --- a/packages/commands/tests/e2e/image-generate.e2e.test.ts +++ b/packages/commands/tests/e2e/image-generate.e2e.test.ts @@ -7,6 +7,7 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { IMAGE_ROUTES } from "./topic-routes.ts"; /** * Image generate:先做 help / 分组等常规检测(不依赖密钥、不调生成接口)。 @@ -15,15 +16,8 @@ import { */ describe("e2e: image generate", () => { - test("image 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["image"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/image|generate|edit/i); - }); - test("image generate --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["image", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, ["image", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--model/i); }); @@ -33,7 +27,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: image generate", () => { test("image generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [ "image", "generate", "--model", @@ -45,7 +39,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( test("【qwen-image-2.0】图片生成", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(IMAGE_ROUTES, [ "image", "generate", "--model", diff --git a/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts b/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts index 93796f3..5ee66f5 100644 --- a/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isChatE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { KNOWLEDGE_CHAT_ROUTES } from "./topic-routes.ts"; interface ContentPart { type: string; @@ -24,7 +25,11 @@ interface DryRunBody { describe("e2e: knowledge chat", () => { test("knowledge chat --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "chat", "--help"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ + "knowledge", + "chat", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--message/i); expect(stderr).toMatch(/--agent-id/i); @@ -32,7 +37,7 @@ describe("e2e: knowledge chat", () => { }); test("缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--agent-id", @@ -43,13 +48,19 @@ describe("e2e: knowledge chat", () => { }); test("缺少 --agent-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "chat", "--message", "Hello"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ + "knowledge", + "chat", + "--message", + "Hello", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--agent-id|Usage:/i); }); test("缺少 --workspace-id 时非零退出并提示", async () => { const { stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_CHAT_ROUTES, // 假 key + 隔离配置目录:避免本机 config 的 workspace_id/api_key 漏入 [ "knowledge", @@ -70,7 +81,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run 输出 endpoint 和 request body", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--dry-run", @@ -93,7 +104,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run 多轮消息解析 role:content 前缀", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--dry-run", @@ -123,7 +134,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run + --image 输出多模态 content 数组", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--dry-run", @@ -152,7 +163,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run + --image 无 --message 自动创建空 user message", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--dry-run", @@ -194,7 +205,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; test("chat (JSON mode) returns answer", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", @@ -215,7 +226,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { }); test("chat (text mode) returns plain text", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", @@ -233,7 +244,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { }); test("chat (stream, JSON mode) collects and returns answer", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", @@ -254,7 +265,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { }); test("chat (stream, text mode) outputs streaming text", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", @@ -272,7 +283,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { }); test("chat with multi-turn messages returns context-aware answer", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", @@ -296,7 +307,7 @@ describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { }); test("chat with invalid agent_id fails gracefully", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_CHAT_ROUTES, [ "knowledge", "chat", "--message", diff --git a/packages/commands/tests/e2e/knowledge-search.e2e.test.ts b/packages/commands/tests/e2e/knowledge-search.e2e.test.ts index 9ff1963..fdbdf47 100644 --- a/packages/commands/tests/e2e/knowledge-search.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge-search.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isSearchE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { KNOWLEDGE_SEARCH_ROUTES } from "./topic-routes.ts"; interface DryRunBody { endpoint?: string; @@ -13,7 +14,11 @@ interface DryRunBody { describe("e2e: knowledge search", () => { test("knowledge search --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "search", "--help"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ + "knowledge", + "search", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--query/i); expect(stderr).toMatch(/--agent-id/i); @@ -23,7 +28,7 @@ describe("e2e: knowledge search", () => { }); test("缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--agent-id", @@ -34,13 +39,19 @@ describe("e2e: knowledge search", () => { }); test("缺少 --agent-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "search", "--query", "test"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ + "knowledge", + "search", + "--query", + "test", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--agent-id|Usage:/i); }); test("缺少 --workspace-id 时非零退出并提示", async () => { const { stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_SEARCH_ROUTES, // 假 key + 隔离配置目录:避免本机 config 的 workspace_id/api_key 漏入 [ "knowledge", @@ -61,7 +72,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run 输出 endpoint 和 request body", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--dry-run", @@ -83,7 +94,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --image 输出 images", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--dry-run", @@ -109,7 +120,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --query-history 输出用户对话历史", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--dry-run", @@ -133,7 +144,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --query-history 无效 JSON 非零退出", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--dry-run", @@ -173,7 +184,7 @@ describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; test("search returns results in JSON mode", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--query", @@ -201,7 +212,7 @@ describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { }); test("search returns results in text mode", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--query", @@ -219,7 +230,7 @@ describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { }); test("search with --query-history returns results", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--query", @@ -241,7 +252,7 @@ describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { }); test("search with invalid agent_id fails gracefully", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_SEARCH_ROUTES, [ "knowledge", "search", "--query", diff --git a/packages/commands/tests/e2e/knowledge.e2e.test.ts b/packages/commands/tests/e2e/knowledge.e2e.test.ts index 88be979..cfb58d4 100644 --- a/packages/commands/tests/e2e/knowledge.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { KNOWLEDGE_ROUTES } from "./topic-routes.ts"; // ---- Types ---- @@ -20,15 +21,12 @@ interface DryRunBody { // ---- Help & missing args (no credentials needed) ---- describe("e2e: knowledge retrieve", () => { - test("knowledge 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["knowledge"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/knowledge|retrieve/i); - }); - test("knowledge retrieve --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "retrieve", "--help"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [ + "knowledge", + "retrieve", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--index-id/i); expect(stderr).toMatch(/--query/i); @@ -37,13 +35,18 @@ describe("e2e: knowledge retrieve", () => { }); test("缺少 --index-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["knowledge", "retrieve", "--query", "test"]); + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [ + "knowledge", + "retrieve", + "--query", + "test", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--index-id|Usage:/i); }); test("缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(KNOWLEDGE_ROUTES, [ "knowledge", "retrieve", "--index-id", @@ -59,6 +62,7 @@ describe("e2e: knowledge retrieve", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: knowledge retrieve errors", () => { test("无任何凭证时提示缺少密钥并非零退出", async () => { const { stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_ROUTES, ["knowledge", "retrieve", "--index-id", "idx_test", "--query", "test", "--output", "json"], { DASHSCOPE_API_KEY: "", @@ -76,6 +80,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: knowledge retrieve errors", () => describe("e2e: knowledge retrieve dry-run", () => { test("--dry-run 输出 endpoint 和 snake_case body", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_ROUTES, [ "knowledge", "retrieve", @@ -98,6 +103,7 @@ describe("e2e: knowledge retrieve dry-run", () => { test("--dry-run + --top-k 转发到 rerank_top_n 并输出废弃警告", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_ROUTES, [ "knowledge", "retrieve", @@ -121,6 +127,7 @@ describe("e2e: knowledge retrieve dry-run", () => { test("--dry-run + --rerank-top-n 优先于 --top-k", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_ROUTES, [ "knowledge", "retrieve", @@ -145,6 +152,7 @@ describe("e2e: knowledge retrieve dry-run", () => { test("--dry-run + rerank 参数完整输出", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + KNOWLEDGE_ROUTES, [ "knowledge", "retrieve", diff --git a/packages/commands/tests/e2e/mcp.e2e.test.ts b/packages/commands/tests/e2e/mcp.e2e.test.ts index 71a9fef..ad5d06e 100644 --- a/packages/commands/tests/e2e/mcp.e2e.test.ts +++ b/packages/commands/tests/e2e/mcp.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { MCP_ROUTES } from "./topic-routes.ts"; /** * `bl mcp` E2E. @@ -16,40 +17,32 @@ import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.t */ describe("e2e: mcp", () => { - test("mcp 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["mcp"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/mcp/i); - expect(out).toMatch(/list|tools|call/i); - }); - test("mcp list --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/list|--name|--type|--page/i); }); test("mcp tools --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "tools", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "tools", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/tools|--server|--url/i); }); test("mcp call --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "call", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "call", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/call|--target|--arg|--json/i); }); test("mcp list --help 不暴露 --all 入口(市场全量已下线)", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).not.toMatch(/--all/); }); test("mcp list --dry-run 仅打印计划且固定 activated=1", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "list", "--dry-run", @@ -87,7 +80,7 @@ describe("e2e: mcp", () => { }); test("mcp list --dry-run 自定义 --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "list", "--dry-run", @@ -102,7 +95,7 @@ describe("e2e: mcp", () => { }); test("mcp tools --server --dry-run 输出 /api/v1/mcps//mcp 形态 URL", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "tools", "--server", @@ -121,7 +114,7 @@ describe("e2e: mcp", () => { }); test("mcp tools --url 覆盖 baseUrl 约定", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "tools", "--server", @@ -139,13 +132,13 @@ describe("e2e: mcp", () => { }); test("mcp tools 缺少 --server 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "tools", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "tools", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--server|Usage:/i); }); test("mcp call --target --dry-run 输出工具调用计划", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "call", "--target", @@ -171,7 +164,7 @@ describe("e2e: mcp", () => { }); test("mcp call --json 与 --arg 合并(arg 覆盖 json),--query 等价 arg.query", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "call", "--target", @@ -207,7 +200,7 @@ describe("e2e: mcp", () => { }); test("mcp call --target 缺少 . 时报错且非零退出", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "call", "--target", @@ -220,7 +213,7 @@ describe("e2e: mcp", () => { }); test("mcp call --arg 非 K=V 形式时报错且非零退出", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "call", "--target", @@ -235,7 +228,7 @@ describe("e2e: mcp", () => { }); test("mcp call --json 无效 JSON 报错且非零退出", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "call", "--target", @@ -250,7 +243,7 @@ describe("e2e: mcp", () => { }); test("mcp call 缺少 --target 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["mcp", "call", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(MCP_ROUTES, ["mcp", "call", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--target|Usage:/i); }); @@ -261,7 +254,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: mcp (live)", () => { // Regression: bailianMcpUrl previously added an `AliyunBailianMCP_` prefix, // which made every real call 500. This test asserts the convention-built URL // (no --url override) actually reaches a live MCP server end-to-end. - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MCP_ROUTES, [ "mcp", "tools", "--server", diff --git a/packages/commands/tests/e2e/memory.e2e.test.ts b/packages/commands/tests/e2e/memory.e2e.test.ts index e94bfe9..f81c6d7 100644 --- a/packages/commands/tests/e2e/memory.e2e.test.ts +++ b/packages/commands/tests/e2e/memory.e2e.test.ts @@ -5,6 +5,7 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { MEMORY_ROUTES } from "./topic-routes.ts"; interface MemoryAddBody { memory_ids?: string[]; @@ -36,33 +37,31 @@ function memoryLibraryCliArgs(): string[] { */ describe("e2e: memory", () => { - test("memory 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["memory"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/memory|add|list|search|update|delete|profile/i); - }); - test("memory add --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["memory", "add", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "add", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/add|--user-id|--content|messages/i); }); test("memory list --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["memory", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/list|--user-id|memory-library/i); }); test("memory search --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["memory", "search", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, ["memory", "search", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/search|--query|user-id/i); }); test("memory profile create --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["memory", "profile", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [ + "memory", + "profile", + "create", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/profile|create|user-id/i); }); @@ -75,7 +74,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( "e2e: memory CRUD + search", () => { test("memory add 缺少 --user-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [ "memory", "add", ...memoryLibraryCliArgs(), @@ -88,7 +87,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( test("memory add 缺少 --messages 与 --content 时报错正常退出", async () => { const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID; - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [ "memory", "add", ...memoryLibraryCliArgs(), @@ -101,7 +100,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( test("memory add --dry-run 仅输出计划且不入网", async () => { const userId = process.env.BAILIAN_E2E_MEMORY_USER_ID?.trim() || DEFAULT_E2E_MEMORY_USER_ID; - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(MEMORY_ROUTES, [ "memory", "add", "--dry-run", @@ -126,7 +125,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const contentA = "CLI vp test:记忆写入(可删)"; const contentB = "CLI vp test:记忆已更新"; - const addRes = await runCommandE2e([ + const addRes = await runCommandE2e(MEMORY_ROUTES, [ "memory", "add", ...memoryLibraryCliArgs(), @@ -141,7 +140,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const added = parseStdoutJson(addRes.stdout); expect(added.request_id?.length ?? 0, addRes.stdout + addRes.stderr).toBeGreaterThan(0); - const listRes = await runCommandE2e([ + const listRes = await runCommandE2e(MEMORY_ROUTES, [ "memory", "list", ...memoryLibraryCliArgs(), @@ -162,7 +161,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const nodeId = listed.memory_nodes![0]!.memory_node_id.trim(); expect(nodeId.length).toBeGreaterThan(0); - const searchRes = await runCommandE2e([ + const searchRes = await runCommandE2e(MEMORY_ROUTES, [ "memory", "search", ...memoryLibraryCliArgs(), @@ -179,7 +178,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const searched = parseStdoutJson(searchRes.stdout); expect(searched.memory_nodes?.length ?? 0).toBeGreaterThan(0); - const updRes = await runCommandE2e([ + const updRes = await runCommandE2e(MEMORY_ROUTES, [ "memory", "update", ...memoryLibraryCliArgs(), @@ -194,7 +193,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( ]); expect(updRes.exitCode, updRes.stderr).toBe(0); - const delRes = await runCommandE2e([ + const delRes = await runCommandE2e(MEMORY_ROUTES, [ "memory", "delete", ...memoryLibraryCliArgs(), diff --git a/packages/commands/tests/e2e/omni.e2e.test.ts b/packages/commands/tests/e2e/omni.e2e.test.ts index cdf83b6..93804d0 100644 --- a/packages/commands/tests/e2e/omni.e2e.test.ts +++ b/packages/commands/tests/e2e/omni.e2e.test.ts @@ -8,10 +8,11 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { OMNI_ROUTES } from "./topic-routes.ts"; describe("e2e: omni", () => { test("omni --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["omni", "--help"]); + const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, ["omni", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/omni|--message|--audio|text-only/i); }); @@ -21,7 +22,10 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: omni(DashScope 媒体)", () => { test("omni --list-voices 输出音色列表并退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["omni", "--list-voices"]); + const { stdout, stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, [ + "omni", + "--list-voices", + ]); expect(exitCode, stderr).toBe(0); expect(stdout).toMatch(/Omni output voices:/); expect(stdout).toMatch(/Tina/); @@ -30,13 +34,17 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("omni 缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["omni", "--model", "qwen3.5-omni-flash"]); + const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, [ + "omni", + "--model", + "qwen3.5-omni-flash", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--message|Usage:/i); }); test("omni --audio 无法识别扩展名时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, [ "omni", "--model", "qwen3.5-omni-flash", @@ -51,7 +59,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("omni --dry-run --audio 构造 input_audio 而非 audio_url", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(OMNI_ROUTES, [ "omni", "--dry-run", "--model", @@ -91,7 +99,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( const clipText = "端到端Omni音频测试"; const clipWav = join(outDir, "e2e-omni-input.wav"); - const syn = await runCommandE2e([ + const syn = await runCommandE2e(OMNI_ROUTES, [ "speech", "synthesize", "--model", @@ -109,7 +117,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( ]); expect(syn.exitCode, syn.stderr).toBe(0); - const omni = await runCommandE2e([ + const omni = await runCommandE2e(OMNI_ROUTES, [ "omni", "--model", "qwen3.5-omni-flash", diff --git a/packages/commands/tests/e2e/pipeline.e2e.test.ts b/packages/commands/tests/e2e/pipeline.e2e.test.ts index 8589a77..f6f01ca 100644 --- a/packages/commands/tests/e2e/pipeline.e2e.test.ts +++ b/packages/commands/tests/e2e/pipeline.e2e.test.ts @@ -3,6 +3,7 @@ import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { PIPELINE_ROUTES } from "./topic-routes.ts"; describe("e2e: pipeline", () => { let tempDir: string; @@ -63,30 +64,29 @@ describe("e2e: pipeline", () => { await rm(tempDir, { recursive: true, force: true }); }); - test("pipeline 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["pipeline"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/pipeline|run|validate/i); - expect(out).toMatch(/Minimal workflow\.yaml|text\/chat|bl pipeline run workflow\.yaml/i); - expect(out).toMatch(/Say hello in one short sentence|--dry-run --output json/i); - }); - test("pipeline run --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["pipeline", "run", "--help"]); + const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ + "pipeline", + "run", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/pipeline run|--input|--input-file|--events|--concurrency/i); expect(stderr).not.toMatch(/--session-(?:dir|id)/i); }); test("pipeline validate --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["pipeline", "validate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ + "pipeline", + "validate", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/pipeline validate|workflow\.json|output json/i); }); test("pipeline validate --output json 校验合法 workflow", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "validate", "--file", @@ -102,6 +102,7 @@ describe("e2e: pipeline", () => { test("pipeline validate 使用 config 输出格式", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + PIPELINE_ROUTES, ["pipeline", "validate", "--file", chatBasicPath], { DASHSCOPE_OUTPUT: "text", @@ -112,7 +113,7 @@ describe("e2e: pipeline", () => { }); test("pipeline validate 拒绝非法依赖 workflow", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "validate", "--file", @@ -128,13 +129,17 @@ describe("e2e: pipeline", () => { }); test("pipeline run 缺少 --file 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["pipeline", "run", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ + "pipeline", + "run", + "--quiet", + ]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/Usage: bl pipeline run --file |--file/i); }); test("pipeline run --dry-run --output json 仅输出计划", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "run", "--file", @@ -168,6 +173,7 @@ describe("e2e: pipeline", () => { test("pipeline run 使用 config 输出格式", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + PIPELINE_ROUTES, ["pipeline", "run", "--file", chatBasicPath, "--input", '{"message":"hello"}', "--dry-run"], { DASHSCOPE_OUTPUT: "text" }, ); @@ -177,7 +183,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run --verbose 打印总步数和当前步骤序号", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "run", "--file", @@ -193,7 +199,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run --events jsonl 在 dry-run 下输出生命周期事件", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "run", "--file", @@ -221,7 +227,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run 拒绝未知 events format", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(PIPELINE_ROUTES, [ "pipeline", "run", "--file", diff --git a/packages/commands/tests/e2e/quota.e2e.test.ts b/packages/commands/tests/e2e/quota.e2e.test.ts index d332b76..b0223d3 100644 --- a/packages/commands/tests/e2e/quota.e2e.test.ts +++ b/packages/commands/tests/e2e/quota.e2e.test.ts @@ -5,17 +5,18 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { QUOTA_ROUTES } from "./topic-routes.ts"; describe("e2e: quota", () => { test("quota list --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["quota", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["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 runCommandE2e(["quota", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl quota list"); expect(stderr).toContain("bl quota list --model qwen3.6-plus"); @@ -23,21 +24,21 @@ describe("e2e: quota", () => { }); test("quota request --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["quota", "request", "--help"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "request", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--model"); expect(stderr).toContain("--tpm"); }); test("quota history --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["quota", "history", "--help"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["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 runCommandE2e(["quota", "check", "--help"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, ["quota", "check", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--model"); expect(stderr).toContain("--period"); @@ -45,7 +46,12 @@ describe("e2e: quota", () => { }); test("quota check --period 0 报错最小值", async () => { - const { stderr, exitCode } = await runCommandE2e(["quota", "check", "--period", "0.5"]); + const { stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ + "quota", + "check", + "--period", + "0.5", + ]); expect(exitCode).toBe(2); expect(stderr).toContain("at least 1 minute"); }); @@ -53,7 +59,7 @@ describe("e2e: quota", () => { describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { test("quota list --dry-run 输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "list", "--dry-run", @@ -73,7 +79,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list --dry-run --all 不传 supports 过滤", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "list", "--all", @@ -89,13 +95,13 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list 文本输出包含英文表头", async () => { - const result = await runCommandE2e(["quota", "list", "--output", "text"]); + const result = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--output", "text"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota list --model 指定模型返回结果", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "list", "--model", @@ -108,7 +114,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list --model 不存在的模型报错", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "list", "--model", @@ -122,13 +128,13 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list JSON 输出包含 model/rpm/tpm/maxTPM", async () => { - const result = await runCommandE2e(["quota", "list", "--output", "json"]); + const result = await runCommandE2e(QUOTA_ROUTES, ["quota", "list", "--output", "json"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota request --dry-run 输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "request", "--model", @@ -150,7 +156,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota request TPM 超范围报错", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "request", "--model", @@ -166,7 +172,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota request 不支持提额的模型报错", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "request", "--model", @@ -180,7 +186,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota history --dry-run 输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "history", "--dry-run", @@ -198,7 +204,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check --dry-run 输出 API 信息", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "check", "--dry-run", @@ -215,7 +221,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "check", "--dry-run", @@ -230,13 +236,13 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check 文本输出包含英文表头", async () => { - const result = await runCommandE2e(["quota", "check", "--output", "text"]); + const result = await runCommandE2e(QUOTA_ROUTES, ["quota", "check", "--output", "text"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota check --model 指定单模型", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "check", "--model", @@ -249,7 +255,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check --model 逗号分隔多模型", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "check", "--model", @@ -262,7 +268,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check JSON 输出包含用量和限额字段", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(QUOTA_ROUTES, [ "quota", "check", "--model", @@ -275,7 +281,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota history --dry-run --page 2 --page-size 20", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(QUOTA_ROUTES, [ "quota", "history", "--page", diff --git a/packages/commands/tests/e2e/search-web.e2e.test.ts b/packages/commands/tests/e2e/search-web.e2e.test.ts index 9ae5c64..89e1b30 100644 --- a/packages/commands/tests/e2e/search-web.e2e.test.ts +++ b/packages/commands/tests/e2e/search-web.e2e.test.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { SEARCH_WEB_ROUTES } from "./topic-routes.ts"; function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> { const envelope = parseStdoutJson<{ content?: Array<{ type?: string; text?: string }> }>(stdout); @@ -15,21 +16,19 @@ function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: */ describe("e2e: search web", () => { - test("search 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["search"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/search|web/i); - }); - test("search web --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["search", "web", "--help"]); + const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/web|--query|list-tools|count/i); }); test("search web --dry-run --list-tools 无需 --query 也无需凭证即可干跑", async () => { const { stdout, stderr, exitCode } = await runCommandE2e( + SEARCH_WEB_ROUTES, ["search", "web", "--dry-run", "--list-tools", "--output", "json"], { DASHSCOPE_API_KEY: undefined, @@ -44,13 +43,17 @@ describe("e2e: search web", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { test("search web 缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["search", "web", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ + "search", + "web", + "--quiet", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--query|Usage:/i); }); test("search web --dry-run 仅输出计划且不调 MCP", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ "search", "web", "--dry-run", @@ -74,7 +77,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { }); test("联网搜索返回 JSON 且含搜索结果", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(SEARCH_WEB_ROUTES, [ "search", "web", "--query", diff --git a/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts index 057c020..df9dc55 100644 --- a/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts @@ -1,26 +1,28 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, runCommandE2e } from "./helpers.ts"; +import { SPEECH_ROUTES } from "./topic-routes.ts"; /** * Speech list-voices E2E */ describe("e2e: speech list-voices", () => { - test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["speech"]); - expect(exitCode, stderr).toBe(0); - const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/speech|synthesize|recognize/i); - }); - test("speech synthesize --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "synthesize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "synthesize", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/synthesize|--text|--voice|list-voices|model/i); }); test("speech recognize --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "recognize", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recognize|--url|audio|model/i); }); @@ -28,13 +30,17 @@ describe("e2e: speech list-voices", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: speech list-voices", () => { test("speech synthesize 缺少 --text 且非 --list-voices 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "synthesize", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "synthesize", + "--quiet", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--text|Usage:/i); }); test("【cosyvoice-v3-flash】获取音色列表", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ "speech", "synthesize", "--list-voices", diff --git a/packages/commands/tests/e2e/speech-recognize.e2e.test.ts b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts index a2209ed..926af0e 100644 --- a/packages/commands/tests/e2e/speech-recognize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts @@ -9,20 +9,19 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { SPEECH_ROUTES } from "./topic-routes.ts"; /** * Speech recognize:help / 分组不依赖密钥;识别流程需媒体 E2E + DashScope。 */ describe("e2e: speech recognize", () => { - test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["speech"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/speech|synthesize|recognize/i); - }); - test("speech recognize --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "recognize", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recognize|--url|model|audio/i); }); @@ -32,7 +31,11 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: speech recognize(DashScope 媒体)", () => { test("speech recognize 缺少 --url 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "recognize", + "--quiet", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--url|Usage:/i); }); @@ -40,7 +43,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( test("【fun-asr】语音识别", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const outMp3 = join(outDir, "e2e-tts.mp3"); - const syn = await runCommandE2e([ + const syn = await runCommandE2e(SPEECH_ROUTES, [ "speech", "synthesize", "--model", @@ -60,7 +63,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( expect(audioUrl?.startsWith("http")).toBe(true); const asrJson = join(outDir, "e2e-asr.json"); - const rec = await runCommandE2e([ + const rec = await runCommandE2e(SPEECH_ROUTES, [ "speech", "recognize", "--model", diff --git a/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts index 3f73472..4881bca 100644 --- a/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts @@ -8,20 +8,19 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { SPEECH_ROUTES } from "./topic-routes.ts"; /** * Speech synthesize:help / 分组不依赖密钥;合成本地需媒体 E2E + DashScope。 */ describe("e2e: speech synthesize", () => { - test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["speech"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/speech|synthesize|recognize/i); - }); - test("speech synthesize --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["speech", "synthesize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ + "speech", + "synthesize", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/synthesize|--text|--voice|model/i); }); @@ -31,7 +30,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: speech synthesize(DashScope 媒体)", () => { test("speech synthesize 缺少 --text 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ "speech", "synthesize", "--model", @@ -44,7 +43,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("speech synthesize --dry-run 仅输出 request 且不调 TTS", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ "speech", "synthesize", "--dry-run", @@ -68,7 +67,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( test("【cosyvoice-v3-flash】语音合成", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const outMp3 = join(outDir, "e2e-tts.mp3"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(SPEECH_ROUTES, [ "speech", "synthesize", "--model", diff --git a/packages/commands/tests/e2e/text-chat.e2e.test.ts b/packages/commands/tests/e2e/text-chat.e2e.test.ts index c8e10b0..a5f4d5e 100644 --- a/packages/commands/tests/e2e/text-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/text-chat.e2e.test.ts @@ -1,19 +1,14 @@ import { describe, expect, test } from "vite-plus/test"; import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { TEXT_CHAT_ROUTES } from "./topic-routes.ts"; /** * Text chat:help / 分组不依赖密钥;对话需 DashScope。 */ describe("e2e: text chat", () => { - test("text 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["text"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/text|chat/i); - }); - test("text chat --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["text", "chat", "--help"]); + const { stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, ["text", "chat", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/chat|--message|model|stream/i); }); @@ -21,13 +16,18 @@ describe("e2e: text chat", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => { test("text chat 缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e(["text", "chat", "--model", "qwen3.7-max"]); + const { stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, [ + "text", + "chat", + "--model", + "qwen3.7-max", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--message|Usage:/i); }); test("text chat --dry-run 仅输出 request 且不调对话接口", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, [ "text", "chat", "--dry-run", @@ -49,7 +49,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => { }); test("【qwen3.7-max】文本对话", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(TEXT_CHAT_ROUTES, [ "text", "chat", "--model", diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts new file mode 100644 index 0000000..8c5382e --- /dev/null +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -0,0 +1,129 @@ +/** + * 各 topic E2E 的最小路由(path → bailian-cli-commands export 名)。 + * 仅包含该 topic 测试会调用的 path,不维护全量产品 map。 + */ +export type E2eRouteExports = Record; + +export const AUTH_ROUTES: E2eRouteExports = { + "auth login": "authLogin", + "auth status": "authStatus", + "auth logout": "authLogout", +}; + +export const TEXT_CHAT_ROUTES: E2eRouteExports = { "text chat": "textChat" }; + +export const CONFIG_ROUTES: E2eRouteExports = { + "config show": "configShow", + "config set": "configSet", +}; + +export const MEMORY_ROUTES: E2eRouteExports = { + "memory add": "memoryAdd", + "memory search": "memorySearch", + "memory list": "memoryList", + "memory update": "memoryUpdate", + "memory delete": "memoryDelete", + "memory profile create": "memoryProfileCreate", + "memory profile get": "memoryProfileGet", +}; + +export const KNOWLEDGE_ROUTES: E2eRouteExports = { + "knowledge retrieve": "knowledgeRetrieve", + "knowledge search": "knowledgeSearch", + "knowledge chat": "knowledgeChat", +}; + +export const KNOWLEDGE_SEARCH_ROUTES: E2eRouteExports = { + "knowledge search": "knowledgeSearch", +}; + +export const KNOWLEDGE_CHAT_ROUTES: E2eRouteExports = { + "knowledge chat": "knowledgeChat", +}; + +export const IMAGE_ROUTES: E2eRouteExports = { + "image generate": "imageGenerate", + "image edit": "imageEdit", +}; + +export const VIDEO_ROUTES: E2eRouteExports = { + "video generate": "videoGenerate", + "video edit": "videoEdit", + "video ref": "videoRef", + "video task get": "videoTaskGet", + "video download": "videoDownload", +}; + +export const SPEECH_ROUTES: E2eRouteExports = { + "speech synthesize": "speechSynthesize", + "speech recognize": "speechRecognize", +}; + +export const MCP_ROUTES: E2eRouteExports = { + "mcp call": "mcpCall", + "mcp list": "mcpList", + "mcp tools": "mcpTools", +}; + +export const SEARCH_WEB_ROUTES: E2eRouteExports = { "search web": "searchWeb" }; + +export const PIPELINE_ROUTES: E2eRouteExports = { + "pipeline run": "pipelineRun", + "pipeline validate": "pipelineValidate", +}; + +export const OMNI_ROUTES: E2eRouteExports = { omni: "textOmni" }; + +export const FILE_UPLOAD_ROUTES: E2eRouteExports = { "file upload": "fileUpload" }; + +export const ADVISOR_ROUTES: E2eRouteExports = { "advisor recommend": "advisorRecommend" }; + +export const QUOTA_ROUTES: E2eRouteExports = { + "quota list": "quotaList", + "quota request": "quotaRequest", + "quota history": "quotaHistory", + "quota check": "quotaCheck", +}; + +export const USAGE_ROUTES: E2eRouteExports = { + "usage free": "usageFree", + "usage freetier": "usageFreetier", + "usage stats": "usageStats", +}; + +export const DEPLOY_ROUTES: E2eRouteExports = { + "deploy create": "deployCreate", + "deploy list": "deployList", + "deploy get": "deployGet", + "deploy models": "deployModels", + "deploy scale": "deployScale", + "deploy update": "deployUpdate", + "deploy delete": "deployDelete", +}; + +export const DATASET_ROUTES: E2eRouteExports = { + "dataset upload": "datasetUpload", + "dataset list": "datasetList", + "dataset get": "datasetGet", + "dataset delete": "datasetDelete", + "dataset validate": "datasetValidate", +}; + +export const FINETUNE_ROUTES: E2eRouteExports = { + "finetune create": "finetuneCreate", + "finetune list": "finetuneList", + "finetune get": "finetuneGet", + "finetune cancel": "finetuneCancel", + "finetune delete": "finetuneDelete", + "finetune logs": "finetuneLogs", + "finetune checkpoints": "finetuneCheckpoints", + "finetune export": "finetuneExport", + "finetune watch": "finetuneWatch", + "finetune capability": "finetuneCapability", +}; + +export const CONSOLE_FLAGS_DRY_RUN_ROUTES: E2eRouteExports = { + "console call": "consoleCall", + "mcp list": "mcpList", + "quota check": "quotaCheck", +}; diff --git a/packages/commands/tests/e2e/usage-free.e2e.test.ts b/packages/commands/tests/e2e/usage-free.e2e.test.ts index 7967b65..14eea08 100644 --- a/packages/commands/tests/e2e/usage-free.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-free.e2e.test.ts @@ -5,23 +5,17 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { USAGE_ROUTES } from "./topic-routes.ts"; describe("e2e: usage free", () => { - test("usage 分组展示子命令帮助且退出码为 0", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["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 runCommandE2e(["usage", "free", "--help"]); + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["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 runCommandE2e(["usage", "free", "--help"]); + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "free", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl usage free"); expect(stderr).toContain("bl usage free --model qwen3-max"); @@ -31,7 +25,7 @@ describe("e2e: usage free", () => { describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { test("usage free --dry-run --model 输出请求参数不发起调用", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--dry-run", @@ -50,7 +44,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 逗号分隔多个模型", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--dry-run", @@ -67,7 +61,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 重复模型名自动去重", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--dry-run", @@ -84,7 +78,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 逗号间有空格也能正确解析", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--dry-run", @@ -101,7 +95,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run 不指定 --model 传全量模型列表", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--dry-run", @@ -112,7 +106,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 单模型查询返回 JSON 结果", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -125,7 +119,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 单模型文本输出包含表头", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -138,7 +132,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 文本输出包含模型名", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -151,7 +145,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 逗号分隔多模型文本输出包含所有模型", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -164,7 +158,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 文本输出包含正确的 Type 列", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -177,7 +171,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model quotaStatus 为 UNKNOWN 时 Auto-Stop 显示 Unsupported", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -190,7 +184,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model quotaStatus 为 UNKNOWN 时额度显示为 -", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -203,7 +197,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 不存在的模型仍返回表格行", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -216,7 +210,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model Auto-Stop 显示 ON、OFF 或 Unsupported", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", @@ -229,7 +223,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model --console-region cn-beijing 指定区域查询", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "free", "--model", diff --git a/packages/commands/tests/e2e/usage-stats.e2e.test.ts b/packages/commands/tests/e2e/usage-stats.e2e.test.ts index b6e2320..150a6c8 100644 --- a/packages/commands/tests/e2e/usage-stats.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-stats.e2e.test.ts @@ -5,6 +5,7 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { USAGE_ROUTES } from "./topic-routes.ts"; import { readConfigFile } from "bailian-cli-core"; function getStaticWorkspaceId(): string | undefined { @@ -25,7 +26,7 @@ async function fetchDefaultWorkspaceId(): Promise { const staticId = getStaticWorkspaceId(); if (staticId) return staticId; - const result = await runCommandE2e(["workspace", "list", "--output", "json"]); + const result = await runCommandE2e(USAGE_ROUTES, ["workspace", "list", "--output", "json"]); if (isConsoleAuthFailure(result) || result.exitCode !== 0) return FALLBACK_WORKSPACE_ID; try { const parsed = JSON.parse(result.stdout); @@ -41,13 +42,13 @@ async function fetchDefaultWorkspaceId(): Promise { describe("e2e: usage stats", () => { test("usage stats --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--model|--days|stats/i); }); test("usage stats --help 包含所有示例", async () => { - const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl usage stats"); expect(stderr).toContain("bl usage stats --model qwen-turbo"); @@ -55,7 +56,7 @@ describe("e2e: usage stats", () => { }); test("usage stats --help 包含 --workspace-id 选项", async () => { - const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, ["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--workspace-id"); }); @@ -71,7 +72,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run 概览模式输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -100,7 +101,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --days 30 时间跨度约 30 天", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -122,7 +123,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --model 指定模型使用 list API", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -144,7 +145,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --type Text 传递 obsModelType", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -163,7 +164,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats 概览模式返回 JSON 结果", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -176,7 +177,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats 概览文本输出包含英文标签", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -189,7 +190,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats 概览文本输出包含 Token 用量", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -202,7 +203,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --model 单模型文本输出包含英文表头", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -217,7 +218,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --model 逗号分隔多模型返回多行", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -232,7 +233,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --model 不存在的模型返回空表格", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -247,7 +248,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --days 1 短时间范围正常返回", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", @@ -262,7 +263,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --type Vision 按类型过滤", async () => { - const result = await runCommandE2e([ + const result = await runCommandE2e(USAGE_ROUTES, [ "usage", "stats", "--workspace-id", diff --git a/packages/commands/tests/e2e/video-download.e2e.test.ts b/packages/commands/tests/e2e/video-download.e2e.test.ts index 46a6bd9..ece742d 100644 --- a/packages/commands/tests/e2e/video-download.e2e.test.ts +++ b/packages/commands/tests/e2e/video-download.e2e.test.ts @@ -10,6 +10,7 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; /** dry-run 占位 UUID */ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001"; @@ -20,14 +21,8 @@ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001"; */ describe("e2e: video download", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video download --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "download", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "download", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/download|--task-id|--out/i); }); @@ -37,7 +32,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video download(DashScope 视频)", () => { test("video download 缺少 --task-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "download", "--out", @@ -48,7 +43,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video download 缺少 --out 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "download", "--task-id", @@ -61,7 +56,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("video download --dry-run 仅输出计划且不下载", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const fakeOut = join(outDir, "e2e-dry-not-written.mp4"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "download", "--dry-run", @@ -83,7 +78,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const genMp4 = join(outDir, "e2e-gen-for-download.mp4"); - const gen = await runCommandE2e([ + const gen = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), @@ -111,7 +106,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( expect(existsSync(genMp4)).toBe(true); const downloadMp4 = join(outDir, "e2e-download.mp4"); - const dl = await runCommandE2e([ + const dl = await runCommandE2e(VIDEO_ROUTES, [ "video", "download", ...cliTimeoutPrefix(), diff --git a/packages/commands/tests/e2e/video-edit.e2e.test.ts b/packages/commands/tests/e2e/video-edit.e2e.test.ts index fd692eb..95e526a 100644 --- a/packages/commands/tests/e2e/video-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/video-edit.e2e.test.ts @@ -9,26 +9,21 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; /** * Video edit E2E */ describe("e2e: video edit", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video edit --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "edit", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "edit", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/edit|--video|--prompt|model|--async|--concurrent/i); }); test("video edit --dry-run 接受 --async 与 --concurrent", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "edit", "--dry-run", @@ -54,7 +49,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video edit(DashScope 视频)", () => { test("video edit 缺少 --video 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "edit", ...cliTimeoutPrefix(), @@ -71,7 +66,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const t2vPath = join(outDir, "e2e-video-t2v.mp4"); - const t2v = await runCommandE2e([ + const t2v = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), @@ -88,7 +83,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const t2vData = parseStdoutJson<{ status?: string; video_url?: string }>(t2v.stdout); expect(t2vData.status).toBe("SUCCEEDED"); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "edit", ...cliTimeoutPrefix(), diff --git a/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts index e0f3ecd..1363ed3 100644 --- a/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts @@ -9,20 +9,15 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; /** * Video generate (i2v):help / 分组不依赖密钥;长任务需视频 E2E + DashScope。 */ describe("e2e: video generate (i2v)", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video generate --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--image|model/i); }); @@ -32,7 +27,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video generate (i2v)(DashScope 视频)", () => { test("video generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), @@ -46,7 +41,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video generate --dry-run(无 --image)仅输出 request(t2v 路径不调上传)", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), @@ -69,7 +64,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("【happyhorse-1.1-i2v】图片生成视频", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const png = join(outDir, "e2e-gen.png"); - const gen = await runCommandE2e([ + const gen = await runCommandE2e(VIDEO_ROUTES, [ "image", "generate", "--model", @@ -87,7 +82,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const genData = parseStdoutJson<{ saved?: string[] }>(gen.stdout); const imagePath = genData.saved?.[0] ?? png; - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), diff --git a/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts index 86507dc..e1efdfb 100644 --- a/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts @@ -9,20 +9,15 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; /** * Video generate (t2v):help / 分组不依赖密钥;长任务需视频 E2E + DashScope。 */ describe("e2e: video generate (t2v)", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video generate --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--model|download|image/i); }); @@ -32,7 +27,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video generate (t2v)(DashScope 视频)", () => { test("video generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), @@ -44,7 +39,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video generate --dry-run(无 --image)仅输出 request 且不调生成接口", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", "--dry-run", @@ -66,7 +61,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("【happyhorse-1.1-t2v】文本生成视频", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "generate", ...cliTimeoutPrefix(), diff --git a/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts index b3839f6..9a3af7f 100644 --- a/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts @@ -9,26 +9,21 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; /** * Video ref (r2v):help / 分组不依赖密钥;参考生成需视频 E2E + DashScope。 */ describe("e2e: video ref (r2v)", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video ref --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "ref", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, ["video", "ref", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/ref|--prompt|--image|model|--async|--concurrent/i); }); test("video ref --dry-run 接受 --async 与 --concurrent", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "ref", "--dry-run", @@ -54,7 +49,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video ref (r2v)(DashScope 视频)", () => { test("video ref 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "ref", ...cliTimeoutPrefix(), @@ -68,7 +63,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video ref 缺少 --image 与 --ref-video 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "ref", ...cliTimeoutPrefix(), @@ -83,7 +78,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("【happyhorse-1.1-r2v】视频参考生成", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const gen = await runCommandE2e([ + const gen = await runCommandE2e(VIDEO_ROUTES, [ "image", "generate", "--model", @@ -102,7 +97,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const imagePath = genData.saved?.[0]; expect(imagePath).toBeTruthy(); - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "ref", ...cliTimeoutPrefix(), diff --git a/packages/commands/tests/e2e/video-task-get.e2e.test.ts b/packages/commands/tests/e2e/video-task-get.e2e.test.ts index ddd4162..eeeb0e4 100644 --- a/packages/commands/tests/e2e/video-task-get.e2e.test.ts +++ b/packages/commands/tests/e2e/video-task-get.e2e.test.ts @@ -5,6 +5,7 @@ import { parseStdoutJson, runCommandE2e, } from "./helpers.ts"; +import { VIDEO_ROUTES } from "./topic-routes.ts"; const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim(); @@ -13,14 +14,13 @@ const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim(); */ describe("e2e: video task get", () => { - test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e(["video"]); - expect(exitCode, stderr).toBe(0); - expect(`${stdout}\n${stderr}`).toMatch(/video|generate|edit|ref|task|download/i); - }); - test("video task get --help 正常退出", async () => { - const { stderr, exitCode } = await runCommandE2e(["video", "task", "get", "--help"]); + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ + "video", + "task", + "get", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/task|get|--task-id/i); }); @@ -30,7 +30,7 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( "e2e: video task get(DashScope)", () => { test("video task get 缺少 --task-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "task", "get", @@ -44,7 +44,7 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( }); test("video task get --dry-run 仅回显 task_id 且不调任务接口", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "task", "get", @@ -60,7 +60,7 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( }); test("根据 task_id 查询任务状态", async () => { - const { stdout, stderr, exitCode } = await runCommandE2e([ + const { stdout, stderr, exitCode } = await runCommandE2e(VIDEO_ROUTES, [ "video", "task", "get", diff --git a/packages/e2e/package.json b/packages/e2e/package.json index 9abcd17..f1d5540 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -9,7 +9,8 @@ "./runner": "./src/run-subprocess.ts", "./output": "./src/output.ts", "./global-setup": "./src/global-setup.ts", - "./monorepo-root": "./src/monorepo-root.ts" + "./monorepo-root": "./src/monorepo-root.ts", + "./registry-smoke": "./src/registry-smoke.ts" }, "dependencies": { "bailian-cli-core": "workspace:*" diff --git a/packages/e2e/src/registry-smoke.ts b/packages/e2e/src/registry-smoke.ts new file mode 100644 index 0000000..bed14cc --- /dev/null +++ b/packages/e2e/src/registry-smoke.ts @@ -0,0 +1,18 @@ +/** + * 从产品 commands map 的 path key 推导有子命令的分组前缀。 + * 供 cli / kscli registry smoke 共用。 + */ +export function deriveGroupPaths(commandPaths: string[]): string[] { + const groups = new Set(); + for (const path of commandPaths) { + const parts = path.split(" "); + for (let i = 1; i < parts.length; i++) { + const prefix = parts.slice(0, i).join(" "); + const hasChildren = commandPaths.some( + (candidate) => candidate.startsWith(`${prefix} `) && candidate !== prefix, + ); + if (hasChildren) groups.add(prefix); + } + } + return [...groups].sort(); +} diff --git a/packages/kscli/src/commands.ts b/packages/kscli/src/commands.ts new file mode 100644 index 0000000..ad49dff --- /dev/null +++ b/packages/kscli/src/commands.ts @@ -0,0 +1,23 @@ +import type { AnyCommand } from "bailian-cli-core"; +import { + configShow, + configSet, + update, + knowledgeRetrieve, + knowledgeSearch, + knowledgeChat, +} from "bailian-cli-commands"; + +// kscli (Knowledge Studio CLI): lightweight RAG product. Ships config/update +// plus the knowledge commands, remapped to flat paths. Routing is driven +// entirely by these keys, and usage/examples/errors render the path from the +// key — so the same shared command shows `kscli search` here and +// `bl knowledge search` in bl. +export const commands: Record = { + "config show": configShow, + "config set": configSet, + update, + retrieve: knowledgeRetrieve, + search: knowledgeSearch, + chat: knowledgeChat, +}; diff --git a/packages/kscli/src/main.ts b/packages/kscli/src/main.ts index f0bd202..fbd0f04 100644 --- a/packages/kscli/src/main.ts +++ b/packages/kscli/src/main.ts @@ -1,29 +1,7 @@ import { createCli } from "bailian-cli-runtime"; -import type { AnyCommand } from "bailian-cli-core"; -import { - configShow, - configSet, - update, - knowledgeRetrieve, - knowledgeSearch, - knowledgeChat, -} from "bailian-cli-commands"; +import { commands } from "./commands.ts"; import pkg from "../package.json" with { type: "json" }; -// kscli (Knowledge Studio CLI): lightweight RAG product. Ships config/update -// plus the knowledge commands, remapped to flat paths. Routing is driven -// entirely by these keys, and usage/examples/errors render the path from the -// key — so the same shared command shows `kscli search` here and -// `bl knowledge search` in bl. -const commands: Record = { - "config show": configShow, - "config set": configSet, - update, - retrieve: knowledgeRetrieve, - search: knowledgeSearch, - chat: knowledgeChat, -}; - void createCli(commands, { binName: "kscli", version: pkg.version, diff --git a/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts b/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts index 6cbe5be..892aa1f 100644 --- a/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts +++ b/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts @@ -1,45 +1,50 @@ -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; import { describe, expect, test } from "vite-plus/test"; -import { runNodeMain } from "e2e/runner"; +import { deriveGroupPaths } from "e2e/registry-smoke"; +import pkg from "../../package.json" with { type: "json" }; +import { commands } from "../../src/commands.ts"; import { runKscli } from "./helpers.ts"; -const kscliPaths = ["config show", "config set", "update", "retrieve", "search", "chat"] as const; - -const harnessMainTs = join( - dirname(fileURLToPath(import.meta.url)), - "../../../commands/tests/e2e/harness/main.ts", -); -const commandsPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "../../../commands"); - -/** 提取 Flags + Global Flags 段,忽略 Notes/Examples 中的产品 bin 差异 */ -function extractFlagSections(text: string): string { - const start = text.indexOf("Flags:"); - if (start < 0) return text.trim(); - const notesIdx = text.indexOf("\nNotes:", start); - const examplesIdx = text.indexOf("\nExamples:", start); - const endCandidates = [notesIdx, examplesIdx].filter((idx) => idx >= 0); - const end = endCandidates.length > 0 ? Math.min(...endCandidates) : text.length; - return text.slice(start, end).trim(); -} +const commandPaths = Object.keys(commands).sort(); +const groupPaths = deriveGroupPaths(commandPaths); describe("e2e: kscli registry smoke", () => { - test.each(kscliPaths)("已注册命令 %s --help 成功", async (path) => { + test("根帮助展示 kscli 与全局 flag", async () => { + const { stderr, exitCode } = await runKscli(["--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/\bkscli\b/i); + expect(stderr).toMatch(/--base-url/); + expect(stderr).not.toMatch(/^\s*--region\s/m); + }); + + test("--version 输出产品名与版本", async () => { + const { stdout, exitCode } = await runKscli(["--version"]); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe(`kscli ${pkg.version}`); + }); + + test("search --help 展示 kscli 路径与 knowledge 必填 flag", async () => { + const { stderr, exitCode } = await runKscli(["search", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/kscli search/i); + expect(stderr).toMatch(/--query/i); + expect(stderr).toMatch(/--agent-id/i); + expect(stderr).toMatch(/--workspace-id/i); + expect(stderr).not.toMatch(/bl knowledge search/i); + }); + + test("search 缺少 --query 时报用法错误 (2)", async () => { + const { stderr, exitCode } = await runKscli(["search", "--agent-id", "aid_test"]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/--query|Missing required/i); + }); + + test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => { const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]); expect(exitCode, stderr).toBe(0); }); - test("search --help flag 与 harness knowledge search --help 一致", async () => { - const [kscliHelp, harnessHelp] = await Promise.all([ - runKscli(["search", "--help"]), - runNodeMain(harnessMainTs, ["knowledge", "search", "--help"], { - cwd: commandsPackageRoot, - }), - ]); - - expect(kscliHelp.exitCode, kscliHelp.stderr).toBe(0); - expect(harnessHelp.exitCode, harnessHelp.stderr).toBe(0); - - expect(extractFlagSections(kscliHelp.stderr)).toBe(extractFlagSections(harnessHelp.stderr)); + test.each(groupPaths)("命令分组 %s --help 成功", async (path) => { + const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]); + expect(exitCode, stderr).toBe(0); }); }); diff --git a/packages/runtime/tests/console-flags.e2e.test.ts b/packages/runtime/tests/console-flags.e2e.test.ts index a03a3c3..66ee1e2 100644 --- a/packages/runtime/tests/console-flags.e2e.test.ts +++ b/packages/runtime/tests/console-flags.e2e.test.ts @@ -1,13 +1,21 @@ +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; import { describe, expect, test } from "vite-plus/test"; -import { runCommandE2e } from "../../commands/tests/e2e/helpers.ts"; +import { runNodeMain } from "e2e/runner"; + +const runtimeRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); +const harnessMainTs = join(runtimeRoot, "tests/harness/cross-domain-main.ts"); + +async function runRuntimeHarness(args: string[]) { + return runNodeMain(harnessMainTs, args, { cwd: runtimeRoot }); +} /** - * 跨域 flag 拒绝:runtime 凭证域与命令 flag 解析边界。 + * 跨域 flag 拒绝:runtime 凭证域与命令 flag 解析边界(synthetic command,不依赖 commands)。 */ - describe("e2e: console global flags (cross-domain rejection)", () => { test("跨域 flag 拒绝:model 命令传 --console-region 报 Unknown flag", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runRuntimeHarness([ "text", "chat", "--message", @@ -21,7 +29,7 @@ describe("e2e: console global flags (cross-domain rejection)", () => { }); test("跨域 flag 拒绝:console 命令传 --api-key 报 Unknown flag", async () => { - const { stderr, exitCode } = await runCommandE2e([ + const { stderr, exitCode } = await runRuntimeHarness([ "mcp", "list", "--api-key", diff --git a/packages/runtime/tests/harness/cross-domain-main.ts b/packages/runtime/tests/harness/cross-domain-main.ts new file mode 100644 index 0000000..c585486 --- /dev/null +++ b/packages/runtime/tests/harness/cross-domain-main.ts @@ -0,0 +1,33 @@ +import { defineCommand } from "bailian-cli-core"; +import { createCli } from "bailian-cli-runtime"; +import pkg from "../../package.json" with { type: "json" }; + +const noop = async () => {}; + +/** apiKey 域 stub:用于跨域 flag 拒绝测试 */ +const textChat = defineCommand({ + description: "runtime e2e stub", + auth: "apiKey", + flags: { + message: { type: "string", valueHint: "", description: "message" }, + }, + run: noop, +}); + +/** console 域 stub:用于跨域 flag 拒绝测试 */ +const mcpList = defineCommand({ + description: "runtime e2e stub", + auth: "console", + flags: {}, + run: noop, +}); + +void createCli( + { "text chat": textChat, "mcp list": mcpList }, + { + binName: "bl", + version: pkg.version, + clientName: "runtime-e2e", + npmPackage: "bailian-cli-runtime-e2e-harness", + }, +).run(); diff --git a/tools/compare-e2e-command-map.ts b/tools/compare-e2e-command-map.ts deleted file mode 100644 index 0a501ae..0000000 --- a/tools/compare-e2e-command-map.ts +++ /dev/null @@ -1,35 +0,0 @@ -#!/usr/bin/env node -/** - * Advisory:对比 bl 产品 map 与 E2E harness map 是否指向同一 command 实例。 - * 不一致时打印 warning,不阻塞测试。 - */ -import { commands as cliCommands } from "../packages/cli/src/commands.ts"; -import { e2eCommandMap } from "../packages/commands/tests/e2e/harness/e2e-command-map.ts"; - -const cliPaths = new Set(Object.keys(cliCommands)); -const e2ePaths = new Set(Object.keys(e2eCommandMap)); - -let hasWarning = false; - -for (const path of cliPaths) { - if (!e2ePaths.has(path)) { - console.warn(`[advisory] cli map path missing in e2e map: ${path}`); - hasWarning = true; - } else if (cliCommands[path] !== e2eCommandMap[path]) { - console.warn(`[advisory] command instance mismatch for path: ${path}`); - hasWarning = true; - } -} - -for (const path of e2ePaths) { - if (!cliPaths.has(path)) { - console.warn(`[advisory] e2e map path missing in cli map: ${path}`); - hasWarning = true; - } -} - -if (!hasWarning) { - console.log("[advisory] cli commands map and e2e harness map are aligned."); -} - -process.exit(0);