From 9602209113127a1a86469e08b017fdbe3dcb2e54 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 11:46:05 +0800 Subject: [PATCH 1/8] feat(e2e): integrate e2e package and update test configurations - Added e2e package as a workspace dependency in pnpm-lock.yaml. - Updated globalSetup path in vite.config.ts to point to the new e2e package location. - Enhanced documentation for CLI E2E testing, including architecture layers and triggering conditions. - Refactored test paths and commands in the documentation to align with the new structure. - Removed outdated dataset and auth E2E tests to streamline the test suite. - Updated command references and validation checks in the documentation to reflect recent changes. --- docs/agents/cli-e2e-tests.md | 84 +++++-- docs/agents/command-add-remove.md | 12 +- packages/cli/package.json | 1 + packages/cli/tests/e2e/helpers.ts | 238 ++++-------------- .../cli/tests/e2e/registry.smoke.e2e.test.ts | 56 +++++ packages/cli/vite.config.ts | 2 +- packages/commands/package.json | 1 + .../tests/e2e/advisor-recommend.e2e.test.ts | 20 +- .../tests/e2e/auth.e2e.test.ts | 52 ++-- .../tests/e2e/config.e2e.test.ts | 34 ++- .../e2e/console-flags-dry-run.e2e.test.ts} | 64 +---- .../tests/e2e/dataset.e2e.test.ts | 53 ++-- .../tests/e2e/deploy.e2e.test.ts | 18 +- .../tests/e2e/file-upload.e2e.test.ts | 26 +- .../tests/e2e/finetune.e2e.test.ts | 32 +-- .../e2e/fixtures}/.dataset-cpt-valid.jsonl | 0 .../e2e/fixtures}/.dataset-dpo-invalid.jsonl | 0 .../e2e/fixtures}/.dataset-dpo-valid.jsonl | 0 .../e2e/fixtures}/.dataset-invalid.jsonl | 0 .../tests/e2e/fixtures}/.dataset-valid.jsonl | 0 .../tests/e2e/fixtures}/.smoke-32.png | Bin .../harness/e2e-command-map.contract.test.ts | 71 ++++++ .../tests/e2e/harness/e2e-command-map.ts | 233 +++++++++++++++++ packages/commands/tests/e2e/harness/main.ts | 9 + packages/commands/tests/e2e/helpers.ts | 51 ++++ .../tests/e2e/image-edit.e2e.test.ts | 24 +- .../tests/e2e/image-generate.e2e.test.ts | 15 +- .../tests/e2e/knowledge-chat.e2e.test.ts | 153 ++++++++++- .../tests/e2e/knowledge-search.e2e.test.ts | 129 +++++++++- .../tests/e2e/knowledge.e2e.test.ts | 25 +- .../tests/e2e/mcp.e2e.test.ts | 36 +-- .../tests/e2e/memory.e2e.test.ts | 33 +-- .../tests/e2e/omni.e2e.test.ts | 16 +- .../tests/e2e/pipeline.e2e.test.ts | 26 +- .../tests/e2e/quota.e2e.test.ts | 81 ++++-- .../tests/e2e/search-web.e2e.test.ts | 14 +- .../tests/e2e/speech-list-voices.e2e.test.ts | 12 +- .../tests/e2e/speech-recognize.e2e.test.ts | 12 +- .../tests/e2e/speech-synthesize.e2e.test.ts | 12 +- .../tests/e2e/text-chat.e2e.test.ts | 12 +- .../tests/e2e/usage-free.e2e.test.ts | 98 ++++++-- .../tests/e2e/usage-stats.e2e.test.ts | 60 +++-- .../tests/e2e/video-download.e2e.test.ts | 16 +- .../tests/e2e/video-edit.e2e.test.ts | 14 +- .../tests/e2e/video-generate-i2v.e2e.test.ts | 14 +- .../tests/e2e/video-generate-t2v.e2e.test.ts | 12 +- .../tests/e2e/video-ref-r2v.e2e.test.ts | 16 +- .../tests/e2e/video-task-get.e2e.test.ts | 23 +- packages/commands/vite.config.ts | 5 + packages/e2e/package.json | 25 ++ packages/e2e/src/gating.ts | 56 +++++ .../tests/e2e => e2e/src}/global-setup.ts | 33 +-- packages/e2e/src/monorepo-root.ts | 15 ++ packages/e2e/src/output.ts | 77 ++++++ packages/e2e/src/run-subprocess.ts | 43 ++++ packages/e2e/vite.config.ts | 11 + packages/kscli/package.json | 1 + packages/kscli/tests/e2e/chat.e2e.test.ts | 131 ---------- packages/kscli/tests/e2e/global-setup.ts | 9 - packages/kscli/tests/e2e/helpers.ts | 147 +++-------- .../tests/e2e/registry.smoke.e2e.test.ts | 45 ++++ packages/kscli/tests/e2e/search.e2e.test.ts | 122 --------- packages/kscli/vite.config.ts | 2 +- packages/runtime/package.json | 2 + .../runtime/tests/console-flags.e2e.test.ts | 34 +++ .../e2e => runtime/tests}/proxy.e2e.test.ts | 28 +-- packages/runtime/vite.config.ts | 5 + pnpm-lock.yaml | 31 +++ tools/compare-e2e-command-map.ts | 35 +++ vite.config.ts | 2 +- 70 files changed, 1743 insertions(+), 1026 deletions(-) create mode 100644 packages/cli/tests/e2e/registry.smoke.e2e.test.ts rename packages/{cli => commands}/tests/e2e/advisor-recommend.e2e.test.ts (89%) rename packages/{cli => commands}/tests/e2e/auth.e2e.test.ts (73%) rename packages/{cli => commands}/tests/e2e/config.e2e.test.ts (75%) rename packages/{cli/tests/e2e/console-flags.e2e.test.ts => commands/tests/e2e/console-flags-dry-run.e2e.test.ts} (58%) rename packages/{cli => commands}/tests/e2e/dataset.e2e.test.ts (79%) rename packages/{cli => commands}/tests/e2e/deploy.e2e.test.ts (88%) rename packages/{cli => commands}/tests/e2e/file-upload.e2e.test.ts (66%) rename packages/{cli => commands}/tests/e2e/finetune.e2e.test.ts (89%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.dataset-cpt-valid.jsonl (100%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.dataset-dpo-invalid.jsonl (100%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.dataset-dpo-valid.jsonl (100%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.dataset-invalid.jsonl (100%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.dataset-valid.jsonl (100%) rename packages/{cli/tests/e2e => commands/tests/e2e/fixtures}/.smoke-32.png (100%) create mode 100644 packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts create mode 100644 packages/commands/tests/e2e/harness/e2e-command-map.ts create mode 100644 packages/commands/tests/e2e/harness/main.ts create mode 100644 packages/commands/tests/e2e/helpers.ts rename packages/{cli => commands}/tests/e2e/image-edit.e2e.test.ts (81%) rename packages/{cli => commands}/tests/e2e/image-generate.e2e.test.ts (82%) rename packages/{cli => commands}/tests/e2e/knowledge-chat.e2e.test.ts (54%) rename packages/{cli => commands}/tests/e2e/knowledge-search.e2e.test.ts (52%) rename packages/{cli => commands}/tests/e2e/knowledge.e2e.test.ts (85%) rename packages/{cli => commands}/tests/e2e/mcp.e2e.test.ts (86%) rename packages/{cli => commands}/tests/e2e/memory.e2e.test.ts (86%) rename packages/{cli => commands}/tests/e2e/omni.e2e.test.ts (88%) rename packages/{cli => commands}/tests/e2e/pipeline.e2e.test.ts (87%) rename packages/{cli => commands}/tests/e2e/quota.e2e.test.ts (77%) rename packages/{cli => commands}/tests/e2e/search-web.e2e.test.ts (83%) rename packages/{cli => commands}/tests/e2e/speech-list-voices.e2e.test.ts (72%) rename packages/{cli => commands}/tests/e2e/speech-recognize.e2e.test.ts (86%) rename packages/{cli => commands}/tests/e2e/speech-synthesize.e2e.test.ts (87%) rename packages/{cli => commands}/tests/e2e/text-chat.e2e.test.ts (80%) rename packages/{cli => commands}/tests/e2e/usage-free.e2e.test.ts (73%) rename packages/{cli => commands}/tests/e2e/usage-stats.e2e.test.ts (83%) rename packages/{cli => commands}/tests/e2e/video-download.e2e.test.ts (89%) rename packages/{cli => commands}/tests/e2e/video-edit.e2e.test.ts (88%) rename packages/{cli => commands}/tests/e2e/video-generate-i2v.e2e.test.ts (88%) rename packages/{cli => commands}/tests/e2e/video-generate-t2v.e2e.test.ts (87%) rename packages/{cli => commands}/tests/e2e/video-ref-r2v.e2e.test.ts (88%) rename packages/{cli => commands}/tests/e2e/video-task-get.e2e.test.ts (77%) create mode 100644 packages/e2e/package.json create mode 100644 packages/e2e/src/gating.ts rename packages/{cli/tests/e2e => e2e/src}/global-setup.ts (65%) create mode 100644 packages/e2e/src/monorepo-root.ts create mode 100644 packages/e2e/src/output.ts create mode 100644 packages/e2e/src/run-subprocess.ts create mode 100644 packages/e2e/vite.config.ts delete mode 100644 packages/kscli/tests/e2e/chat.e2e.test.ts delete mode 100644 packages/kscli/tests/e2e/global-setup.ts create mode 100644 packages/kscli/tests/e2e/registry.smoke.e2e.test.ts delete mode 100644 packages/kscli/tests/e2e/search.e2e.test.ts create mode 100644 packages/runtime/tests/console-flags.e2e.test.ts rename packages/{cli/tests/e2e => runtime/tests}/proxy.e2e.test.ts (70%) create mode 100644 tools/compare-e2e-command-map.ts diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index 055dd69..3a07f20 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -1,18 +1,44 @@ # CLI E2E 测试规范 +## 架构分层 + +| 层级 | 路径 | 测什么 | +| --------------- | ----------------------------------------------------- | --------------------------------------------------------------- | +| **共享基建** | `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 拒绝 | + +**依赖边界**:`e2e` → `core`;`commands/tests` → `e2e` + `commands/src`;产品 tests → `e2e` + 各自 `src`。**禁止**产品 import `commands/tests/**`(子进程 spawn harness 路径除外)。 + ## 触发条件 - 新增/修改 `packages/commands/src/commands` 下的 command 实现 - 新增/修改 `packages/cli/src/commands.ts` 的 `bl` 命令路径 map -- 新建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例 -- 为命令补 help / 缺参 / dry-run / 真实集成测试 +- 新建或扩展 `packages/commands/tests/e2e/.e2e.test.ts` +- 新增 bl 产品 path → 同步 `registry.smoke`(自动覆盖 leaf path)与 `harness/e2e-command-map.ts` -以上情况必须同步维护 `packages/cli/tests/e2e/.e2e.test.ts`。跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。 +跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。 ## 文件与工具 -- 路径:`packages/cli/tests/e2e/.e2e.test.ts` -- 框架:`vite-plus/test`;子进程跑 CLI:`runCli` from `./helpers.ts` +### commands E2E + +- 路径:`packages/commands/tests/e2e/.e2e.test.ts` +- 子进程:`runCommandE2e` from `./helpers.ts`(spawn `harness/main.ts`) +- fixtures:`packages/commands/tests/e2e/fixtures/` +- map:`harness/e2e-command-map.ts`(自维护,初始从 `cli/commands.ts` 复制) + +### 产品 smoke + +- bl:`runCli` from `packages/cli/tests/e2e/helpers.ts` +- kscli:`runKscli` from `packages/kscli/tests/e2e/helpers.ts` + +### 共享 + +- gating / output / runner:`e2e/gating`、`e2e/output`、`e2e/runner` +- globalSetup:根 `vite.config.ts` → `packages/e2e/src/global-setup.ts` - 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))` - 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时 @@ -33,24 +59,31 @@ describe.skipIf()("e2e: (DashScope …)", () => { }); ``` -## skip 条件(helpers.ts) +## skip 条件(`e2e/gating`,commands helpers re-export) -| 场景 | 条件 | -| ------------------- | ----------------------------------------------------- | -| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` | -| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` | -| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` | -| 知识库 | `isKnowledgeE2EReady()` | -| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` | +| 场景 | 条件 | +| ----------------------- | ---------------------------------------------------------------------------------------------------------- | +| 文本/搜索/记忆/配置 | `isDashScopeE2EReady()` | +| 图像/语音 | `isBailianE2EMediaEnabled() && isDashScopeE2EReady()` | +| 视频 | `isBailianE2EVideoEnabled() && isDashScopeE2EReady()` | +| 视频 download/task | 另需 `BAILIAN_E2E_VIDEO_TASK_ID` | +| 知识库 chat/search live | `isChatE2EReady()` / `isSearchE2EReady()`(`knowledge chat/search`,需 `BAILIAN_WORKSPACE_ID` + agent ID) | ## 用例类型 -1. **分组 help**:`runCli(["image"])` → `exitCode === 0`,stdout+stderr 含子命令名 -2. **--help**:`runCli([..., "--help"])` → stderr 含主要 flags +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 块**末尾** +## 契约与防漂移 + +- `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))。 + ## 安全与例外 - **禁止真实破坏性操作**:`auth logout` 只用 `--dry-run`;`config set` 只用 `--dry-run` @@ -60,24 +93,35 @@ describe.skipIf()("e2e: (DashScope …)", () => { ## 新增 command 检查清单 -- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `tests/e2e/.e2e.test.ts`(新建或扩展) +- [ ] `packages/commands/src/index.ts` 导出 + `packages/cli/src/commands.ts` 暴露路径 + `harness/e2e-command-map.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) - [ ] skip 块:每个 required flag 缺参;可 dry-run 则加一条 - [ ] 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序 -- [ ] `pnpm test packages/cli/tests/e2e/` 通过 +- [ ] `vp test packages/commands/tests/e2e/` 通过 + +## 调试命令 + +```sh +pnpm --filter bailian-cli-commands exec vp test packages/commands/tests/e2e/text-chat.e2e.test.ts +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 test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCli(["foo", "bar", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "foo", "bar", "--dry-run", @@ -97,4 +141,4 @@ test("foo bar --dry-run 仅输出计划", async () => { - **E2E**:单条/少量调用、断言固定、可进 `vp test`(见上文 skip 条件) - **批量压测**:`packages/cli/tests/stress/run.mjs` + `targets/*.mjs`,并发 + 报告,**仅手动** `pnpm run test:stress -- ` -勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md)。 +勿把压测并入 E2E 或默认 CI。详见 [stress-batch-tests.md](stress-batch-tests.md). diff --git a/docs/agents/command-add-remove.md b/docs/agents/command-add-remove.md index 2ea3cce..145ccfb 100644 --- a/docs/agents/command-add-remove.md +++ b/docs/agents/command-add-remove.md @@ -92,9 +92,12 @@ packages/commands/src/index.ts ### D. 测试层 -- [ ] 按 [cli-e2e-tests.md](cli-e2e-tests.md) 新建或更新 `packages/cli/tests/e2e/.e2e.test.ts` -- [ ] 删除命令时一并删对应 e2e / README 示例 / reference 生成结果 -- [ ] 如果 shared command 在不同入口路径下复用,至少确保 `bl` 入口 e2e 覆盖;`rag` 入口改动需补对应入口测试或手工 smoke +- [ ] 按 [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 条目 +- [ ] 如果 shared command 在不同入口路径下复用,至少确保 commands e2e 覆盖 `bl` path;`rag` / `kscli` 入口改动需补对应 smoke 或说明不测 flat path live ### E. 重命名特殊处理 @@ -111,7 +114,8 @@ packages/commands/src/index.ts pnpm run sync:skill-assets node packages/cli/src/main.ts --help node packages/cli/src/main.ts -vp test packages/cli/tests/e2e/.e2e.test.ts +vp test packages/commands/tests/e2e/.e2e.test.ts +node tools/compare-e2e-command-map.ts ``` 如改了 `rag` 入口: diff --git a/packages/cli/package.json b/packages/cli/package.json index 3f19aec..29f53e4 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -59,6 +59,7 @@ "ajv": "catalog:", "boxen": "catalog:", "chalk": "catalog:", + "e2e": "workspace:*", "typescript": "^6.0.2", "undici": "catalog:", "vite-plus": "0.1.22", diff --git a/packages/cli/tests/e2e/helpers.ts b/packages/cli/tests/e2e/helpers.ts index f695ed5..8d42da7 100644 --- a/packages/cli/tests/e2e/helpers.ts +++ b/packages/cli/tests/e2e/helpers.ts @@ -1,206 +1,52 @@ -import { execFile } from "child_process"; -import { mkdirSync, readFileSync } from "fs"; -import { promisify } from "util"; -import { basename, dirname, join } from "path"; +import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { readConfigFile } from "bailian-cli-core"; - -const execFileAsync = promisify(execFile); - -/** - * Vitest `global-setup.ts` 写入 `test/output/` 下本文件名,供各 worker 进程读取同一会话 id。 - * (仅模块内变量无法跨 Vitest 多进程 worker 共享。) - */ -export const E2E_RUN_SESSION_FILENAME = ".e2e-run-session"; - -/** - * 单次 `vp test` / Vitest 运行共用的 E2E 输出会话目录名(惰性缓存于当前进程)。 - */ -let e2eOutputSessionId: string | undefined; - -/** `packages/cli` 根目录(含 `src/main.ts`) */ +import { + cliTimeoutPrefix, + cliTimeoutSeconds, + e2eLabelFromMetaUrl, + isConsoleAuthFailure, + makeE2eOutputDir, + parseStdoutJson, +} from "e2e/output"; +import { runNodeMain, type RunCliResult } from "e2e/runner"; +import { + isBailianE2EEnabled, + isBailianE2EMediaEnabled, + isBailianE2EVideoEnabled, + isChatE2EReady, + isConsoleE2EReady, + isDashScopeE2EReady, + isSearchE2EReady, +} from "e2e/gating"; +import { monorepoRoot } from "e2e/monorepo-root"; + +export { + cliTimeoutPrefix, + cliTimeoutSeconds, + e2eLabelFromMetaUrl, + isBailianE2EEnabled, + isBailianE2EMediaEnabled, + isBailianE2EVideoEnabled, + isChatE2EReady, + isConsoleAuthFailure, + isConsoleE2EReady, + isDashScopeE2EReady, + isSearchE2EReady, + makeE2eOutputDir, + monorepoRoot, + parseStdoutJson, +}; +export type { RunCliResult }; + +/** `packages/cli` 根目录 */ export const cliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const mainTs = join(cliPackageRoot, "src", "main.ts"); -/** Monorepo 根(含根 `package.json`) */ -export function monorepoRoot(): string { - return join(cliPackageRoot, "..", ".."); -} - -function readE2eRunSessionFromOutputDir(): string | undefined { - try { - const p = join(monorepoRoot(), "test", "output", E2E_RUN_SESSION_FILENAME); - const t = readFileSync(p, "utf8").trim(); - return t.length > 0 ? t : undefined; - } catch { - return undefined; - } -} - -function getE2eOutputSessionId(): string { - if (!e2eOutputSessionId) { - const fromEnv = process.env.BAILIAN_E2E_RUN_ID?.trim(); - if (fromEnv) { - e2eOutputSessionId = fromEnv.replace(/[^a-zA-Z0-9._-]+/g, "-"); - } else { - const fromFile = readE2eRunSessionFromOutputDir(); - if (fromFile) { - e2eOutputSessionId = fromFile.replace(/[^a-zA-Z0-9._-]+/g, "-"); - } else { - e2eOutputSessionId = `e2e-run-${Date.now()}-${process.pid}`; - } - } - } - return e2eOutputSessionId; -} - -/** - * 在 `test/output/<会话>/` 下创建用例子目录。 - * 会话 id 优先 `BAILIAN_E2E_RUN_ID`,否则读 Vitest globalSetup 写入的 `test/output/.e2e-run-session`, - * 再否则回退为单进程 id(非 Vitest 直接跑用例时)。 - * 若已设 `BAILIAN_E2E_OUT` 则直接使用(不再套会话目录)。 - */ -export function makeE2eOutputDir(label: string): string { - const fromEnv = process.env.BAILIAN_E2E_OUT?.trim(); - if (fromEnv) { - mkdirSync(fromEnv, { recursive: true }); - return fromEnv; - } - const safe = label.replace(/[^a-zA-Z0-9._-]+/g, "-"); - const sessionDir = join(monorepoRoot(), "test", "output", getE2eOutputSessionId()); - mkdirSync(sessionDir, { recursive: true }); - const dir = join(sessionDir, `e2e-vp-${safe}-${Date.now()}`); - mkdirSync(dir, { recursive: true }); - return dir; -} - -/** 全局 `--timeout` 秒数(视频等长任务) */ -export function cliTimeoutSeconds(): string { - return process.env.BAILIAN_E2E_TIMEOUT_SEC?.trim() || "3600"; -} - -export function cliTimeoutPrefix(): string[] { - return ["--timeout", cliTimeoutSeconds()]; -} - -/** 显式开启后才跑真实网络 E2E,避免默认 `vp test` 依赖密钥或打外网 */ -export function isBailianE2EEnabled(): boolean { - return process.env.BAILIAN_E2E === "1"; -} - -/** 可调 DashScope 的 API Key:环境变量优先,否则读 ~/.bailian/config.json */ -export function isDashScopeE2EReady(): boolean { - if (!isBailianE2EEnabled()) return false; - if (process.env.DASHSCOPE_API_KEY?.trim()) return true; - try { - const f = readConfigFile(); - return typeof f.api_key === "string" && f.api_key.length > 0; - } catch { - return false; - } -} - -/** - * Console-gateway 命令(quota / usage free / usage stats)的 E2E 就绪检查: - * 需 `BAILIAN_E2E=1` 且存在 console access_token(`~/.bailian/config.json` 的 - * `access_token`;凭证解析已集中到 authStage,不再读环境变量)。 - * - * 仅检查 token 是否存在——无法本地判断是否过期。token 过期时 gated 用例仍会执行, - * 但用 `isConsoleAuthFailure` 把“session 未登录/已过期”的优雅报错视为通过,保持 - * 与 deploy/dataset “无 key / 有效 key / 失效 key 均绿”的一致策略。 - */ -export function isConsoleE2EReady(): boolean { - if (!isBailianE2EEnabled()) return false; - try { - const config = readConfigFile(); - return typeof config.access_token === "string" && config.access_token.length > 0; - } catch { - return false; - } -} - -/** 语音与图像(可设 `BAILIAN_E2E_MEDIA=0` 在仅跑文本/记忆/知识库时跳过) */ -export function isBailianE2EMediaEnabled(): boolean { - if (process.env.BAILIAN_E2E_MEDIA === "0") return false; - return isBailianE2EEnabled(); -} - -/** 文生视频 / 图生视频 / 参考视频 / 视频编辑(耗时长,默认关闭) */ -export function isBailianE2EVideoEnabled(): boolean { - return isBailianE2EEnabled() && process.env.BAILIAN_E2E_VIDEO === "1"; -} - -/** 从 `import.meta.url` 生成 OUT 子目录标签,避免并行用例目录冲突 */ -export function e2eLabelFromMetaUrl(metaUrl: string): string { - return basename(fileURLToPath(metaUrl), ".ts").replace(/\.e2e\.test$/, ""); -} - -/** 知识库用例:须显式索引 ID + API-KEY */ -export function isKnowledgeE2EReady(): boolean { - if (!isBailianE2EEnabled()) return false; - if (!process.env.BAILIAN_E2E_INDEX_ID) return false; - return isDashScopeE2EReady(); -} - -export interface RunCliResult { - stdout: string; - stderr: string; - exitCode: number; -} - -/** - * 子进程执行 CLI(等价于在 `packages/cli` 下 `node src/main.ts ...`)。 - * request_id 等诊断信息在 stderr;`--output json` 时 JSON 在 stdout。 - */ +/** 子进程执行 bl CLI */ export async function runCli( args: string[], envOverrides: NodeJS.ProcessEnv = {}, ): Promise { - try { - const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { - cwd: cliPackageRoot, - encoding: "utf8", - maxBuffer: 32 * 1024 * 1024, - env: { - ...process.env, - NODE_NO_WARNINGS: "1", - DO_NOT_TRACK: "1", - ...envOverrides, - }, - }); - return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 }; - } catch (err: unknown) { - const e = err as { - stdout?: string; - stderr?: string; - code?: number; - }; - return { - stdout: e.stdout ?? "", - stderr: e.stderr ?? "", - exitCode: typeof e.code === "number" ? e.code : 1, - }; - } -} - -export function parseStdoutJson(stdout: string): T { - const t = stdout.trim(); - // Extract JSON object — stdout may contain [perf] console.time lines before JSON - const jsonMatch = t.match(/\{[\s\S]*\}/); - if (!jsonMatch) throw new Error(`No JSON object found in stdout: ${t.slice(0, 200)}`); - return JSON.parse(jsonMatch[0]) as T; -} - -/** - * 判断一次 CLI 运行是否因 console session 未登录/已过期而失败。 - * - * Console E2E 用例的 readiness 闸(`isConsoleE2EReady`)只能判断 token 是否存在, - * 无法判断是否过期;token 失效时 gated 用例仍会执行并拿到鉴权错误。本函数让用例 - * 参考 deploy/dataset 的做法:只要 CLI 把鉴权错误优雅上抛(非零退出 + stderr 说明 - * session 失效),即视为通过,而不是强求 exit 0 的成功输出。 - */ -export function isConsoleAuthFailure(result: RunCliResult): boolean { - if (result.exitCode === 0) return false; - return /not logged in|has expired|NotLogined|Run `bl auth login/i.test(result.stderr); + return runNodeMain(mainTs, args, { cwd: cliPackageRoot, env: envOverrides }); } diff --git a/packages/cli/tests/e2e/registry.smoke.e2e.test.ts b/packages/cli/tests/e2e/registry.smoke.e2e.test.ts new file mode 100644 index 0000000..8d48c26 --- /dev/null +++ b/packages/cli/tests/e2e/registry.smoke.e2e.test.ts @@ -0,0 +1,56 @@ +import { describe, expect, test } from "vite-plus/test"; +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); + +describe("e2e: bl registry smoke", () => { + test("根帮助展示 bl 与全局 flag", async () => { + const { stderr, exitCode } = await runCli(["--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/\bbl\b/i); + expect(stderr).toMatch(/--base-url/); + expect(stderr).toMatch(/--console-region/); + expect(stderr).toMatch(/--console-site/); + expect(stderr).toMatch(/--console-switch-agent/); + expect(stderr).not.toMatch(/^\s*--region\s/m); + }); + + test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => { + const { stderr, exitCode } = await runCli(["quota", "check", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/Global Flags:/); + expect(stderr).toMatch(/--console-region /); + expect(stderr).toMatch(/--model /); + expect(stderr).toMatch(/--period /); + expect(stderr).toMatch(/--output /); + expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/); + }); + + test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => { + const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]); + expect(exitCode, stderr).toBe(0); + }); + + test.each(groupPaths)("命令分组 %s --help 成功", async (path) => { + const { stderr, exitCode } = await runCli([...path.split(" "), "--help"]); + expect(exitCode, stderr).toBe(0); + }); +}); diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts index 769e51e..dc315e3 100644 --- a/packages/cli/vite.config.ts +++ b/packages/cli/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ test: { - globalSetup: "./tests/e2e/global-setup.ts", + globalSetup: "../e2e/src/global-setup.ts", testTimeout: 60_000, hookTimeout: 60_000, }, diff --git a/packages/commands/package.json b/packages/commands/package.json index 6caffa2..a7d2761 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -49,6 +49,7 @@ "devDependencies": { "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260328.1", + "e2e": "workspace:*", "typescript": "^6.0.2", "vite-plus": "0.1.22" }, diff --git a/packages/cli/tests/e2e/advisor-recommend.e2e.test.ts b/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts similarity index 89% rename from packages/cli/tests/e2e/advisor-recommend.e2e.test.ts rename to packages/commands/tests/e2e/advisor-recommend.e2e.test.ts index 201e036..5f01005 100644 --- a/packages/cli/tests/e2e/advisor-recommend.e2e.test.ts +++ b/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts @@ -1,15 +1,15 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; describe("e2e: advisor recommend", () => { test("advisor shows subcommand groups and exits successfully", async () => { - const { stdout, stderr, exitCode } = await runCli(["advisor"]); + 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 runCli(["advisor", "recommend", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["advisor", "recommend", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recommend|--message|dry-run/i); }); @@ -17,13 +17,13 @@ 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 runCli(["advisor", "recommend", "--quiet"]); + const { stdout, stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--dry-run", @@ -63,7 +63,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: advisor recommend (DashScope)", () }, 60_000); test("advisor recommend full flow returns results", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--message", @@ -96,7 +96,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--dry-run", @@ -116,7 +116,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--dry-run", @@ -136,7 +136,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--dry-run", @@ -166,7 +166,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "advisor", "recommend", "--dry-run", diff --git a/packages/cli/tests/e2e/auth.e2e.test.ts b/packages/commands/tests/e2e/auth.e2e.test.ts similarity index 73% rename from packages/cli/tests/e2e/auth.e2e.test.ts rename to packages/commands/tests/e2e/auth.e2e.test.ts index 89905cc..8cc340b 100644 --- a/packages/cli/tests/e2e/auth.e2e.test.ts +++ b/packages/commands/tests/e2e/auth.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; /** * Auth 相关 E2E:只验证 CLI 进程能正常解析参数并退出。 @@ -7,39 +7,39 @@ import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; describe("e2e: auth", () => { test("auth 分组展示子命令帮助且退出码为 0", async () => { - const { stdout, stderr, exitCode } = await runCli(["auth"]); + 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 runCli(["auth", "login", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["auth", "logout", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["auth", "status", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["auth", "login", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "auth", "login", "--dry-run", @@ -51,7 +51,7 @@ describe("e2e: auth", () => { }); test("auth login --dry-run 覆盖全局参数 --output json --timeout", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "auth", "login", "--dry-run", @@ -67,7 +67,7 @@ describe("e2e: auth", () => { }); test("auth login 缺少密钥且 --output json 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["auth", "login", "--output", "json"]); + const { stderr, exitCode } = await runCommandE2e(["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,20 +75,25 @@ describe("e2e: auth", () => { }); test("auth logout --dry-run 不写入配置", async () => { - const { stdout, stderr, exitCode } = await runCli(["auth", "logout", "--dry-run"]); + const { stdout, stderr, exitCode } = await runCommandE2e(["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 runCli(["auth", "logout", "--dry-run", "--quiet"]); + const { stdout, stderr, exitCode } = await runCommandE2e([ + "auth", + "logout", + "--dry-run", + "--quiet", + ]); expect(exitCode, stderr).toBe(0); expect(stdout).toContain("No changes made."); }); test("auth logout --dry-run --output json(不清除密钥)", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "auth", "logout", "--dry-run", @@ -101,7 +106,12 @@ describe("e2e: auth", () => { }); test.skipIf(!isDashScopeE2EReady())("auth status 文本输出", async () => { - const { stdout, stderr, exitCode } = await runCli(["auth", "status", "--output", "text"]); + const { stdout, stderr, exitCode } = await runCommandE2e([ + "auth", + "status", + "--output", + "text", + ]); expect(exitCode, stderr).toBe(0); expect(stdout).toMatch( /Authentication Status|API key:|Console token:|DashScope API:|Console gateway:/, @@ -109,7 +119,12 @@ describe("e2e: auth", () => { }); test.skipIf(!isDashScopeE2EReady())("auth status --output json", async () => { - const { stdout, stderr, exitCode } = await runCli(["auth", "status", "--output", "json"]); + const { stdout, stderr, exitCode } = await runCommandE2e([ + "auth", + "status", + "--output", + "json", + ]); expect(exitCode, stderr).toBe(0); const data = parseStdoutJson<{ authenticated?: boolean; @@ -122,7 +137,7 @@ describe("e2e: auth", () => { test.skipIf(!isDashScopeE2EReady())( "auth status --output json --quiet(base_url 经 env 指定;凭证域 flag 对 status 不可见)", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( ["auth", "status", "--output", "json", "--quiet"], { DASHSCOPE_BASE_URL: "https://dashscope.aliyuncs.com" }, ); @@ -134,7 +149,12 @@ describe("e2e: auth", () => { ); test("auth status 不接受凭证域覆盖 flag(--base-url 报 Unknown flag)", async () => { - const { stderr, exitCode } = await runCli(["auth", "status", "--base-url", "https://x.test"]); + const { stderr, exitCode } = await runCommandE2e([ + "auth", + "status", + "--base-url", + "https://x.test", + ]); expect(exitCode).not.toBe(0); expect(stderr).toMatch(/Unknown flag.*--base-url/); }); diff --git a/packages/cli/tests/e2e/config.e2e.test.ts b/packages/commands/tests/e2e/config.e2e.test.ts similarity index 75% rename from packages/cli/tests/e2e/config.e2e.test.ts rename to packages/commands/tests/e2e/config.e2e.test.ts index 07700db..47f3cb6 100644 --- a/packages/cli/tests/e2e/config.e2e.test.ts +++ b/packages/commands/tests/e2e/config.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; /** * Config 相关 E2E @@ -7,26 +7,31 @@ import { parseStdoutJson, runCli } from "./helpers.ts"; describe("e2e: config", () => { test("config 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["config"]); + 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 runCli(["config", "show", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["config", "show", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/show|config/i); }); test("config set --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["config", "set", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["config", "show", "--output", "json"]); + const { stdout, stderr, exitCode } = await runCommandE2e([ + "config", + "show", + "--output", + "json", + ]); expect(exitCode, stderr).toBe(0); const data = parseStdoutJson<{ config_file?: string; @@ -39,19 +44,24 @@ describe("e2e: config", () => { }); test("config show --output text", async () => { - const { stdout, stderr, exitCode } = await runCli(["config", "show", "--output", "text"]); + const { stdout, stderr, exitCode } = await runCommandE2e([ + "config", + "show", + "--output", + "text", + ]); expect(exitCode, stderr).toBe(0); expect(stdout).toMatch(/config_file|timeout|base_url/i); }); test("config set 缺少 --key / --value 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["config", "set", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["config", "set", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--key|--value|Usage:/i); }); test("config set 非法 key 时退出为用法错误", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "config", "set", "--key", @@ -64,7 +74,7 @@ describe("e2e: config", () => { }); test("config set 非法 output", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "config", "set", "--key", @@ -77,7 +87,7 @@ describe("e2e: config", () => { }); test("config set 非法 timeout", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "config", "set", "--key", @@ -90,7 +100,7 @@ describe("e2e: config", () => { }); test("config set --dry-run 不落盘(仅输出 would_set)", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "config", "set", "--dry-run", @@ -107,7 +117,7 @@ describe("e2e: config", () => { }); test("config set --dry-run 支持连字符别名 key", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "config", "set", "--dry-run", diff --git a/packages/cli/tests/e2e/console-flags.e2e.test.ts b/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts similarity index 58% rename from packages/cli/tests/e2e/console-flags.e2e.test.ts rename to packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts index f5b8a6e..f0d1c8f 100644 --- a/packages/cli/tests/e2e/console-flags.e2e.test.ts +++ b/packages/commands/tests/e2e/console-flags-dry-run.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; type ConsoleDryRunMeta = { consoleRegion?: string; @@ -7,55 +7,9 @@ type ConsoleDryRunMeta = { consoleSwitchAgent?: number; }; -/** - * E2E for global console flags (`--console-region`, `--console-site`, - * `--console-switch-agent`) and DashScope `--base-url`. - */ - -describe("e2e: console global flags", () => { - test("根帮助展示 --base-url 与 console 全局标志", async () => { - const { stderr, exitCode } = await runCli(["--help"]); - expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/--base-url/); - expect(stderr).toMatch(/--console-region/); - expect(stderr).toMatch(/--console-site/); - expect(stderr).toMatch(/--console-switch-agent/); - expect(stderr).not.toMatch(/^\s*--region\s/m); - }); - - test("quota check --help:Flags 含 console 域鉴权 flag,Global Flags 全量列出", async () => { - const { stderr, exitCode } = await runCli(["quota", "check", "--help"]); - expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/Global Flags:/); - expect(stderr).toMatch(/--console-region /); - expect(stderr).toMatch(/--model /); - expect(stderr).toMatch(/--period /); - expect(stderr).toMatch(/--output /); - expect(stderr).not.toMatch(/API region \(default: cn-beijing\)/); - }); - - test("跨域 flag 拒绝:model 命令传 --console-region 报 Unknown flag", async () => { - const { stderr, exitCode } = await runCli([ - "text", - "chat", - "--message", - "hi", - "--console-region", - "cn-hangzhou", - "--dry-run", - ]); - expect(exitCode).not.toBe(0); - expect(stderr).toMatch(/Unknown flag.*--console-region/); - }); - - test("跨域 flag 拒绝:console 命令传 --api-key 报 Unknown flag", async () => { - const { stderr, exitCode } = await runCli(["mcp", "list", "--api-key", "sk-test", "--dry-run"]); - expect(exitCode).not.toBe(0); - expect(stderr).toMatch(/Unknown flag.*--api-key/); - }); - +describe("e2e: console global flags (dry-run)", () => { test("console call --help 不暴露命令级 region/site,示例使用 --console-region", async () => { - const { stderr, exitCode } = await runCli(["console", "call", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["console", "call", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api /); expect(stderr).toMatch(/--data /); @@ -65,7 +19,7 @@ describe("e2e: console global flags", () => { }); test("auth login --help:自有 flag 含 --console-site,不含其余 console 域 flag", async () => { - const { stderr, exitCode } = await runCli(["auth", "login", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["auth", "login", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--api-key /); expect(stderr).toMatch(/--base-url /); @@ -75,7 +29,7 @@ describe("e2e: console global flags", () => { }); test("console call --dry-run 默认 consoleRegion 为 cn-beijing", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "console", "call", "--api", @@ -93,7 +47,7 @@ describe("e2e: console global flags", () => { }); test("console call --dry-run --console-region / --console-site / --console-switch-agent 透传", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "console", "call", "--api", @@ -118,7 +72,7 @@ describe("e2e: console global flags", () => { }); test("console call 拒绝未知全局 flag --region", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "console", "call", "--api", @@ -134,7 +88,7 @@ describe("e2e: console global flags", () => { }); test("mcp list --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "list", "--dry-run", @@ -149,7 +103,7 @@ describe("e2e: console global flags", () => { }); test("quota check --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "check", "--dry-run", diff --git a/packages/cli/tests/e2e/dataset.e2e.test.ts b/packages/commands/tests/e2e/dataset.e2e.test.ts similarity index 79% rename from packages/cli/tests/e2e/dataset.e2e.test.ts rename to packages/commands/tests/e2e/dataset.e2e.test.ts index ef7f08e..6dd19e2 100644 --- a/packages/cli/tests/e2e/dataset.e2e.test.ts +++ b/packages/commands/tests/e2e/dataset.e2e.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); +import { join } from "path"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; /** * Dataset (fine-tune file) E2E. @@ -20,21 +17,21 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset --help 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCli(["dataset"]); + 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 runCli(["dataset", "upload", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["dataset", "upload", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--file|jsonl/i); }); test("dataset validate 通过合法 JSONL", async () => { - const file = join(__dirname, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -49,8 +46,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset validate 拒绝 pretty-printed JSON 并以非零码退出", async () => { - const file = join(__dirname, ".dataset-invalid.jsonl"); - const { stdout, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-invalid.jsonl"); + const { stdout, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -68,8 +65,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset upload --no-validate --dry-run 跳过本地校验", async () => { - const file = join(__dirname, ".dataset-invalid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-invalid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "upload", "--file", @@ -88,8 +85,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate 自动识别 DPO 并校验 chosen/rejected", async () => { // No --schema: a record carrying chosen/rejected is auto-detected as DPO // and the valid fixture passes. - const file = join(__dirname, ".dataset-dpo-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-dpo-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -106,8 +103,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { test("dataset validate 自动识别 CPT 并校验 {text} 记录", async () => { // No --schema: a record carrying `text` (and no `messages`) is auto-detected // as CPT and the valid fixture passes. - const file = join(__dirname, ".dataset-cpt-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-cpt-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -122,8 +119,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset validate --schema cpt 拒绝缺失 text 的记录", async () => { - const file = join(__dirname, ".dataset-valid.jsonl"); // SFT {messages}, no text - const { stdout, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); // SFT {messages}, no text + const { stdout, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -142,8 +139,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset validate --schema dpo 拒绝缺失 rejected 的记录", async () => { - const file = join(__dirname, ".dataset-dpo-invalid.jsonl"); - const { stdout, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-dpo-invalid.jsonl"); + const { stdout, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -163,8 +160,8 @@ 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(__dirname, ".dataset-dpo-invalid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-dpo-invalid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -181,8 +178,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset validate --schema 以非零码退出", async () => { - const file = join(__dirname, ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "validate", "--file", @@ -197,8 +194,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { }); test("dataset upload --dry-run 转发 --schema", async () => { - const file = join(__dirname, ".dataset-dpo-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const file = join(e2eFixturesDir, ".dataset-dpo-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "upload", "--file", @@ -218,7 +215,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (offline)", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: dataset (DashScope)", () => { test("dataset list --output json 返回结构化结果", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "dataset", "list", "--page-size", diff --git a/packages/cli/tests/e2e/deploy.e2e.test.ts b/packages/commands/tests/e2e/deploy.e2e.test.ts similarity index 88% rename from packages/cli/tests/e2e/deploy.e2e.test.ts rename to packages/commands/tests/e2e/deploy.e2e.test.ts index 5fdcd63..b2b4891 100644 --- a/packages/cli/tests/e2e/deploy.e2e.test.ts +++ b/packages/commands/tests/e2e/deploy.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; /** * Deploy E2E. @@ -15,20 +15,20 @@ import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { test("deploy 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCli(["deploy"]); + 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 runCli(["deploy", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", "create", "--model", @@ -57,7 +57,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy scale --dry-run 转发 capacity", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", "scale", "--deployed-model", @@ -80,7 +80,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy update --dry-run 转发 rate limits", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", "update", "--deployed-model", @@ -104,7 +104,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (offline)", () => { }); test("deploy scale --dry-run 缺少 capacity/input-tpm/output-tpm 时报错", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", "scale", "--deployed-model", @@ -124,7 +124,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", sub, ...extra, @@ -147,7 +147,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: deploy (DashScope)", () => { * 而非进程崩溃),即视为通过。 */ test("deploy list --output json 优雅返回(空账号或鉴权失败均通过)", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "deploy", "list", "--page-size", diff --git a/packages/cli/tests/e2e/file-upload.e2e.test.ts b/packages/commands/tests/e2e/file-upload.e2e.test.ts similarity index 66% rename from packages/cli/tests/e2e/file-upload.e2e.test.ts rename to packages/commands/tests/e2e/file-upload.e2e.test.ts index 241910f..f49c6d2 100644 --- a/packages/cli/tests/e2e/file-upload.e2e.test.ts +++ b/packages/commands/tests/e2e/file-upload.e2e.test.ts @@ -1,9 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; - -const __dirname = dirname(fileURLToPath(import.meta.url)); +import { join } from "path"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; /** * File upload E2E @@ -11,14 +8,14 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); describe("e2e: file upload", () => { test("file 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["file"]); + 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 runCli(["file", "upload", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["file", "upload", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/upload|--file|--model/i); }); @@ -26,21 +23,26 @@ describe("e2e: file upload", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: file upload(DashScope)", () => { test("file upload 缺少 --file 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["file", "upload", "--model", "qwen3-vl-plus"]); + const { stderr, exitCode } = await runCommandE2e([ + "file", + "upload", + "--model", + "qwen3-vl-plus", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--file|Usage:/i); }); test("file upload 缺少 --model 时报用法错误并退出 (2)", async () => { - const testFile = join(__dirname, ".smoke-32.png"); - const { stderr, exitCode } = await runCli(["file", "upload", "--file", testFile]); + const testFile = join(e2eFixturesDir, ".smoke-32.png"); + const { stderr, exitCode } = await runCommandE2e(["file", "upload", "--file", testFile]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--model|Usage:/i); }); test("上传文件成功返回oss临时 URL", async () => { - const testFile = join(__dirname, ".smoke-32.png"); - const { stdout, stderr, exitCode } = await runCli([ + const testFile = join(e2eFixturesDir, ".smoke-32.png"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "file", "upload", "--file", diff --git a/packages/cli/tests/e2e/finetune.e2e.test.ts b/packages/commands/tests/e2e/finetune.e2e.test.ts similarity index 89% rename from packages/cli/tests/e2e/finetune.e2e.test.ts rename to packages/commands/tests/e2e/finetune.e2e.test.ts index 067cf13..c5b32c1 100644 --- a/packages/cli/tests/e2e/finetune.e2e.test.ts +++ b/packages/commands/tests/e2e/finetune.e2e.test.ts @@ -1,6 +1,6 @@ import { describe, expect, test } from "vite-plus/test"; import { join } from "path"; -import { isDashScopeE2EReady, parseStdoutJson, runCli, cliPackageRoot } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e, e2eFixturesDir } from "./helpers.ts"; /** * Fine-tune E2E. @@ -16,20 +16,20 @@ import { isDashScopeE2EReady, parseStdoutJson, runCli, cliPackageRoot } from "./ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune 列出子命令", async () => { - const { stdout, stderr, exitCode } = await runCli(["finetune"]); + 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 runCli(["finetune", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -62,7 +62,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 转发训练类型与超参", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -115,7 +115,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --training-type 拒绝不支持的训练类型值", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -132,8 +132,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 把本地路径标记为 pending 上传且不发起网络请求", async () => { - const localPath = join(cliPackageRoot, "tests", "e2e", ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -163,7 +163,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --datasets 为空时拒绝", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -182,8 +182,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { // so 3 <= 8 trips the pre-submit gate. The gate fires before any upload, // 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(cliPackageRoot, "tests", "e2e", ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -203,8 +203,8 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { test("finetune create --batch-size 过小仍按 8 下限比较(不绕过卡口)", async () => { // 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(cliPackageRoot, "tests", "e2e", ".dataset-valid.jsonl"); - const { stdout, stderr, exitCode } = await runCli([ + const localPath = join(e2eFixturesDir, ".dataset-valid.jsonl"); + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -231,7 +231,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", sub, ...extra, @@ -245,7 +245,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (offline)", () => { }); test("finetune create --dry-run 解析多 datasets 中的空白", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "create", "--model", @@ -273,7 +273,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: finetune (DashScope)", () => { * 而非进程崩溃),即视为通过。 */ test("finetune list --output json 优雅返回(空账号或鉴权失败均通过)", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "finetune", "list", "--page-size", diff --git a/packages/cli/tests/e2e/.dataset-cpt-valid.jsonl b/packages/commands/tests/e2e/fixtures/.dataset-cpt-valid.jsonl similarity index 100% rename from packages/cli/tests/e2e/.dataset-cpt-valid.jsonl rename to packages/commands/tests/e2e/fixtures/.dataset-cpt-valid.jsonl diff --git a/packages/cli/tests/e2e/.dataset-dpo-invalid.jsonl b/packages/commands/tests/e2e/fixtures/.dataset-dpo-invalid.jsonl similarity index 100% rename from packages/cli/tests/e2e/.dataset-dpo-invalid.jsonl rename to packages/commands/tests/e2e/fixtures/.dataset-dpo-invalid.jsonl diff --git a/packages/cli/tests/e2e/.dataset-dpo-valid.jsonl b/packages/commands/tests/e2e/fixtures/.dataset-dpo-valid.jsonl similarity index 100% rename from packages/cli/tests/e2e/.dataset-dpo-valid.jsonl rename to packages/commands/tests/e2e/fixtures/.dataset-dpo-valid.jsonl diff --git a/packages/cli/tests/e2e/.dataset-invalid.jsonl b/packages/commands/tests/e2e/fixtures/.dataset-invalid.jsonl similarity index 100% rename from packages/cli/tests/e2e/.dataset-invalid.jsonl rename to packages/commands/tests/e2e/fixtures/.dataset-invalid.jsonl diff --git a/packages/cli/tests/e2e/.dataset-valid.jsonl b/packages/commands/tests/e2e/fixtures/.dataset-valid.jsonl similarity index 100% rename from packages/cli/tests/e2e/.dataset-valid.jsonl rename to packages/commands/tests/e2e/fixtures/.dataset-valid.jsonl diff --git a/packages/cli/tests/e2e/.smoke-32.png b/packages/commands/tests/e2e/fixtures/.smoke-32.png similarity index 100% rename from packages/cli/tests/e2e/.smoke-32.png rename to packages/commands/tests/e2e/fixtures/.smoke-32.png 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 new file mode 100644 index 0000000..e384570 --- /dev/null +++ b/packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts @@ -0,0 +1,71 @@ +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 new file mode 100644 index 0000000..bc976b5 --- /dev/null +++ b/packages/commands/tests/e2e/harness/e2e-command-map.ts @@ -0,0 +1,233 @@ +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/main.ts b/packages/commands/tests/e2e/harness/main.ts new file mode 100644 index 0000000..01def24 --- /dev/null +++ b/packages/commands/tests/e2e/harness/main.ts @@ -0,0 +1,9 @@ +import { createCli } from "bailian-cli-runtime"; +import { e2eCommandMap } from "./e2e-command-map.ts"; + +void createCli(e2eCommandMap, { + binName: "bl", + version: "0.0.0-e2e", + clientName: "commands-e2e", + npmPackage: "bailian-cli-commands", +}).run(); diff --git a/packages/commands/tests/e2e/helpers.ts b/packages/commands/tests/e2e/helpers.ts new file mode 100644 index 0000000..4082a70 --- /dev/null +++ b/packages/commands/tests/e2e/helpers.ts @@ -0,0 +1,51 @@ +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { + cliTimeoutPrefix, + cliTimeoutSeconds, + e2eLabelFromMetaUrl, + isConsoleAuthFailure, + makeE2eOutputDir, + parseStdoutJson, +} from "e2e/output"; +import { runNodeMain, type RunCliResult } from "e2e/runner"; + +export { + cliTimeoutPrefix, + cliTimeoutSeconds, + e2eLabelFromMetaUrl, + isConsoleAuthFailure, + makeE2eOutputDir, + parseStdoutJson, +}; +export type { RunCliResult }; + +export { + isBailianE2EEnabled, + isBailianE2EMediaEnabled, + isBailianE2EVideoEnabled, + isChatE2EReady, + isConsoleE2EReady, + isDashScopeE2EReady, + isSearchE2EReady, +} from "e2e/gating"; + +const e2eDir = dirname(fileURLToPath(import.meta.url)); +const harnessMainTs = join(e2eDir, "harness", "main.ts"); + +/** `packages/commands` 根目录 */ +export const commandsPackageRoot = join(e2eDir, "..", ".."); + +/** E2E fixtures 目录 */ +export const e2eFixturesDir = join(e2eDir, "fixtures"); + +/** 通过 harness 子进程执行 E2E 命令 */ +export async function runCommandE2e( + args: string[], + envOverrides: NodeJS.ProcessEnv = {}, +): Promise { + return runNodeMain(harnessMainTs, args, { + cwd: commandsPackageRoot, + env: envOverrides, + }); +} diff --git a/packages/cli/tests/e2e/image-edit.e2e.test.ts b/packages/commands/tests/e2e/image-edit.e2e.test.ts similarity index 81% rename from packages/cli/tests/e2e/image-edit.e2e.test.ts rename to packages/commands/tests/e2e/image-edit.e2e.test.ts index 6a11685..3fd3272 100644 --- a/packages/cli/tests/e2e/image-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/image-edit.e2e.test.ts @@ -1,37 +1,35 @@ import { describe, expect, test } from "vite-plus/test"; -import { dirname, join } from "path"; -import { fileURLToPath } from "url"; +import { join } from "path"; import { + e2eFixturesDir, e2eLabelFromMetaUrl, isBailianE2EMediaEnabled, isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; -const __dirname = dirname(fileURLToPath(import.meta.url)); - /** * Image edit E2E */ describe("e2e: image edit", () => { test("image 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["image"]); + 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 runCli(["image", "edit", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "image", "edit", "--dry-run", @@ -58,21 +56,21 @@ describe("e2e: image edit", () => { describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: image edit", () => { test("image edit 缺少 --image 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["image", "edit", "--prompt", "仅提示词"]); + const { stderr, exitCode } = await runCommandE2e(["image", "edit", "--prompt", "仅提示词"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--image|Usage:/i); }); test("image edit 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const testPng = join(__dirname, ".smoke-32.png"); - const { stderr, exitCode } = await runCli(["image", "edit", "--image", testPng]); + const testPng = join(e2eFixturesDir, ".smoke-32.png"); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const gen = await runCommandE2e([ "image", "generate", "--model", @@ -92,7 +90,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())("e2e: ima expect(imagePath).toBeTruthy(); - const ed = await runCli([ + const ed = await runCommandE2e([ "image", "edit", "--model", diff --git a/packages/cli/tests/e2e/image-generate.e2e.test.ts b/packages/commands/tests/e2e/image-generate.e2e.test.ts similarity index 82% rename from packages/cli/tests/e2e/image-generate.e2e.test.ts rename to packages/commands/tests/e2e/image-generate.e2e.test.ts index 445fb5a..7bee37e 100644 --- a/packages/cli/tests/e2e/image-generate.e2e.test.ts +++ b/packages/commands/tests/e2e/image-generate.e2e.test.ts @@ -5,7 +5,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,14 +16,14 @@ import { describe("e2e: image generate", () => { test("image 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["image"]); + 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 runCli(["image", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["image", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--model/i); }); @@ -33,14 +33,19 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: image generate", () => { test("image generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["image", "generate", "--model", "qwen-image-2.0"]); + const { stderr, exitCode } = await runCommandE2e([ + "image", + "generate", + "--model", + "qwen-image-2.0", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--prompt|Usage:/i); }); test("【qwen-image-2.0】图片生成", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "image", "generate", "--model", diff --git a/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts b/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts similarity index 54% rename from packages/cli/tests/e2e/knowledge-chat.e2e.test.ts rename to packages/commands/tests/e2e/knowledge-chat.e2e.test.ts index 0640857..93796f3 100644 --- a/packages/cli/tests/e2e/knowledge-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge-chat.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { isChatE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; interface ContentPart { type: string; @@ -24,7 +24,7 @@ interface DryRunBody { describe("e2e: knowledge chat", () => { test("knowledge chat --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "chat", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "chat", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--message/i); expect(stderr).toMatch(/--agent-id/i); @@ -32,19 +32,24 @@ describe("e2e: knowledge chat", () => { }); test("缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "chat", "--agent-id", "aid_test"]); + const { stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--agent-id", + "aid_test", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--message|Usage:/i); }); test("缺少 --agent-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "chat", "--message", "Hello"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "chat", "--message", "Hello"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--agent-id|Usage:/i); }); test("缺少 --workspace-id 时非零退出并提示", async () => { - const { stderr, exitCode } = await runCli( + const { stderr, exitCode } = await runCommandE2e( // 假 key + 隔离配置目录:避免本机 config 的 workspace_id/api_key 漏入 [ "knowledge", @@ -65,7 +70,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run 输出 endpoint 和 request body", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "chat", "--dry-run", @@ -88,7 +93,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run 多轮消息解析 role:content 前缀", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "chat", "--dry-run", @@ -118,7 +123,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run + --image 输出多模态 content 数组", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "chat", "--dry-run", @@ -147,7 +152,7 @@ describe("e2e: knowledge chat", () => { }); test("--dry-run + --image 无 --message 自动创建空 user message", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "chat", "--dry-run", @@ -178,3 +183,133 @@ describe("e2e: knowledge chat", () => { }); }); }); + +interface ChatJsonResult { + answer: string; + request_id: string; +} + +describe.skipIf(!isChatE2EReady())("e2e: knowledge chat (live)", () => { + const agentId = process.env.BAILIAN_E2E_CHAT_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("chat (JSON mode) returns answer", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "什么是大模型?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (text mode) returns plain text", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "什么是RAG?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat (stream, JSON mode) collects and returns answer", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "什么是检索增强生成?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + expect(data.request_id).toBeTruthy(); + }); + + test("chat (stream, text mode) outputs streaming text", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "什么是向量检索?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + expect(stdout.trim().length).toBeGreaterThan(0); + }); + + test("chat with multi-turn messages returns context-aware answer", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "user:什么是大模型", + "--message", + "assistant:大模型是大规模语言模型,具有强大的理解和生成能力", + "--message", + "它有哪些应用场景?", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.answer).toBeTruthy(); + expect(data.answer.length).toBeGreaterThan(0); + }); + + test("chat with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runCommandE2e([ + "knowledge", + "chat", + "--message", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/cli/tests/e2e/knowledge-search.e2e.test.ts b/packages/commands/tests/e2e/knowledge-search.e2e.test.ts similarity index 52% rename from packages/cli/tests/e2e/knowledge-search.e2e.test.ts rename to packages/commands/tests/e2e/knowledge-search.e2e.test.ts index fee03f6..9ff1963 100644 --- a/packages/cli/tests/e2e/knowledge-search.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge-search.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { isSearchE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; interface DryRunBody { endpoint?: string; @@ -13,7 +13,7 @@ interface DryRunBody { describe("e2e: knowledge search", () => { test("knowledge search --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "search", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "search", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--query/i); expect(stderr).toMatch(/--agent-id/i); @@ -23,19 +23,24 @@ describe("e2e: knowledge search", () => { }); test("缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "search", "--agent-id", "aid_test"]); + const { stderr, exitCode } = await runCommandE2e([ + "knowledge", + "search", + "--agent-id", + "aid_test", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--query|Usage:/i); }); test("缺少 --agent-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "search", "--query", "test"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "search", "--query", "test"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--agent-id|Usage:/i); }); test("缺少 --workspace-id 时非零退出并提示", async () => { - const { stderr, exitCode } = await runCli( + const { stderr, exitCode } = await runCommandE2e( // 假 key + 隔离配置目录:避免本机 config 的 workspace_id/api_key 漏入 [ "knowledge", @@ -56,7 +61,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run 输出 endpoint 和 request body", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "search", "--dry-run", @@ -78,7 +83,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --image 输出 images", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "search", "--dry-run", @@ -104,7 +109,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --query-history 输出用户对话历史", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "knowledge", "search", "--dry-run", @@ -128,7 +133,7 @@ describe("e2e: knowledge search", () => { }); test("--dry-run + --query-history 无效 JSON 非零退出", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "knowledge", "search", "--dry-run", @@ -147,3 +152,109 @@ describe("e2e: knowledge search", () => { expect(stderr).toMatch(/query-history.*valid JSON/i); }); }); + +interface SearchResponse { + code: string; + status_code: number; + request_id: string; + data: { + total: number; + cost_time: number; + nodes: Array<{ + score: number; + text: string; + metadata: Record; + }>; + }; +} + +describe.skipIf(!isSearchE2EReady())("e2e: knowledge search (live)", () => { + const agentId = process.env.BAILIAN_E2E_SEARCH_AGENT_ID!; + const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; + + test("search returns results in JSON mode", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "search", + "--query", + "什么是大模型", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.request_id).toBeTruthy(); + expect(data.data.total).toBeGreaterThan(0); + expect(data.data.nodes.length).toBeGreaterThan(0); + + const firstNode = data.data.nodes[0]!; + expect(typeof firstNode.score).toBe("number"); + expect(firstNode.score).toBeGreaterThanOrEqual(0); + expect(typeof firstNode.text).toBe("string"); + expect(firstNode.text.length).toBeGreaterThan(0); + }); + + test("search returns results in text mode", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "search", + "--query", + "RAG", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--output", + "text", + ]); + + expect(exitCode, stderr).toBe(0); + expect(stdout).toMatch(/\[1\].*score/); + }); + + test("search with --query-history returns results", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e([ + "knowledge", + "search", + "--query", + "它怎么工作", + "--agent-id", + agentId, + "--workspace-id", + workspaceId, + "--query-history", + '[{"role":"user","content":"什么是大模型"},{"role":"assistant","content":"大模型是大规模语言模型"}]', + "--output", + "json", + ]); + + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson(stdout); + expect(data.code).toBe("Success"); + expect(data.data.nodes.length).toBeGreaterThan(0); + }); + + test("search with invalid agent_id fails gracefully", async () => { + const { stderr, exitCode } = await runCommandE2e([ + "knowledge", + "search", + "--query", + "test", + "--agent-id", + "aid-invalid-not-exist", + "--workspace-id", + workspaceId, + "--output", + "json", + ]); + + expect(exitCode).not.toBe(0); + expect(stderr).toBeTruthy(); + }); +}); diff --git a/packages/cli/tests/e2e/knowledge.e2e.test.ts b/packages/commands/tests/e2e/knowledge.e2e.test.ts similarity index 85% rename from packages/cli/tests/e2e/knowledge.e2e.test.ts rename to packages/commands/tests/e2e/knowledge.e2e.test.ts index f4c960c..88be979 100644 --- a/packages/cli/tests/e2e/knowledge.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; // ---- Types ---- @@ -21,14 +21,14 @@ interface DryRunBody { describe("e2e: knowledge retrieve", () => { test("knowledge 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["knowledge"]); + 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 runCli(["knowledge", "retrieve", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "retrieve", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--index-id/i); expect(stderr).toMatch(/--query/i); @@ -37,13 +37,18 @@ describe("e2e: knowledge retrieve", () => { }); test("缺少 --index-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--query", "test"]); + const { stderr, exitCode } = await runCommandE2e(["knowledge", "retrieve", "--query", "test"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--index-id|Usage:/i); }); test("缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--index-id", "idx_test"]); + const { stderr, exitCode } = await runCommandE2e([ + "knowledge", + "retrieve", + "--index-id", + "idx_test", + ]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--query|Usage:/i); }); @@ -53,7 +58,7 @@ describe("e2e: knowledge retrieve", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: knowledge retrieve errors", () => { test("无任何凭证时提示缺少密钥并非零退出", async () => { - const { stderr, exitCode } = await runCli( + const { stderr, exitCode } = await runCommandE2e( ["knowledge", "retrieve", "--index-id", "idx_test", "--query", "test", "--output", "json"], { DASHSCOPE_API_KEY: "", @@ -70,7 +75,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 runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( [ "knowledge", "retrieve", @@ -92,7 +97,7 @@ describe("e2e: knowledge retrieve dry-run", () => { }); test("--dry-run + --top-k 转发到 rerank_top_n 并输出废弃警告", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( [ "knowledge", "retrieve", @@ -115,7 +120,7 @@ describe("e2e: knowledge retrieve dry-run", () => { }); test("--dry-run + --rerank-top-n 优先于 --top-k", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( [ "knowledge", "retrieve", @@ -139,7 +144,7 @@ describe("e2e: knowledge retrieve dry-run", () => { }); test("--dry-run + rerank 参数完整输出", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( [ "knowledge", "retrieve", diff --git a/packages/cli/tests/e2e/mcp.e2e.test.ts b/packages/commands/tests/e2e/mcp.e2e.test.ts similarity index 86% rename from packages/cli/tests/e2e/mcp.e2e.test.ts rename to packages/commands/tests/e2e/mcp.e2e.test.ts index fec9919..71a9fef 100644 --- a/packages/cli/tests/e2e/mcp.e2e.test.ts +++ b/packages/commands/tests/e2e/mcp.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; /** * `bl mcp` E2E. @@ -17,7 +17,7 @@ import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; describe("e2e: mcp", () => { test("mcp 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["mcp"]); + const { stdout, stderr, exitCode } = await runCommandE2e(["mcp"]); expect(exitCode, stderr).toBe(0); const out = `${stdout}\n${stderr}`; expect(out).toMatch(/mcp/i); @@ -25,31 +25,31 @@ describe("e2e: mcp", () => { }); test("mcp list --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["mcp", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["mcp", "tools", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["mcp", "tools", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/tools|--server|--url/i); }); test("mcp call --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["mcp", "call", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["mcp", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "list", "--dry-run", @@ -87,7 +87,7 @@ describe("e2e: mcp", () => { }); test("mcp list --dry-run 自定义 --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "list", "--dry-run", @@ -102,7 +102,7 @@ describe("e2e: mcp", () => { }); test("mcp tools --server --dry-run 输出 /api/v1/mcps//mcp 形态 URL", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "tools", "--server", @@ -121,7 +121,7 @@ describe("e2e: mcp", () => { }); test("mcp tools --url 覆盖 baseUrl 约定", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "tools", "--server", @@ -139,13 +139,13 @@ describe("e2e: mcp", () => { }); test("mcp tools 缺少 --server 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["mcp", "tools", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "call", "--target", @@ -171,7 +171,7 @@ describe("e2e: mcp", () => { }); test("mcp call --json 与 --arg 合并(arg 覆盖 json),--query 等价 arg.query", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "call", "--target", @@ -207,7 +207,7 @@ describe("e2e: mcp", () => { }); test("mcp call --target 缺少 . 时报错且非零退出", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "mcp", "call", "--target", @@ -220,7 +220,7 @@ describe("e2e: mcp", () => { }); test("mcp call --arg 非 K=V 形式时报错且非零退出", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "mcp", "call", "--target", @@ -235,7 +235,7 @@ describe("e2e: mcp", () => { }); test("mcp call --json 无效 JSON 报错且非零退出", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "mcp", "call", "--target", @@ -250,7 +250,7 @@ describe("e2e: mcp", () => { }); test("mcp call 缺少 --target 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["mcp", "call", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["mcp", "call", "--quiet"]); expect(exitCode, stderr).toBe(2); expect(stderr).toMatch(/--target|Usage:/i); }); @@ -261,7 +261,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "mcp", "tools", "--server", diff --git a/packages/cli/tests/e2e/memory.e2e.test.ts b/packages/commands/tests/e2e/memory.e2e.test.ts similarity index 86% rename from packages/cli/tests/e2e/memory.e2e.test.ts rename to packages/commands/tests/e2e/memory.e2e.test.ts index 66bcf9d..e94bfe9 100644 --- a/packages/cli/tests/e2e/memory.e2e.test.ts +++ b/packages/commands/tests/e2e/memory.e2e.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { + isBailianE2EEnabled, + isDashScopeE2EReady, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; interface MemoryAddBody { memory_ids?: string[]; @@ -32,32 +37,32 @@ function memoryLibraryCliArgs(): string[] { describe("e2e: memory", () => { test("memory 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["memory"]); + 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 runCli(["memory", "add", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["memory", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["memory", "search", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["memory", "profile", "create", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["memory", "profile", "create", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/profile|create|user-id/i); }); @@ -70,7 +75,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( "e2e: memory CRUD + search", () => { test("memory add 缺少 --user-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "memory", "add", ...memoryLibraryCliArgs(), @@ -83,7 +88,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 runCli([ + const { stderr, exitCode } = await runCommandE2e([ "memory", "add", ...memoryLibraryCliArgs(), @@ -96,7 +101,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "memory", "add", "--dry-run", @@ -121,7 +126,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const contentA = "CLI vp test:记忆写入(可删)"; const contentB = "CLI vp test:记忆已更新"; - const addRes = await runCli([ + const addRes = await runCommandE2e([ "memory", "add", ...memoryLibraryCliArgs(), @@ -136,7 +141,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 runCli([ + const listRes = await runCommandE2e([ "memory", "list", ...memoryLibraryCliArgs(), @@ -157,7 +162,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const nodeId = listed.memory_nodes![0]!.memory_node_id.trim(); expect(nodeId.length).toBeGreaterThan(0); - const searchRes = await runCli([ + const searchRes = await runCommandE2e([ "memory", "search", ...memoryLibraryCliArgs(), @@ -174,7 +179,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( const searched = parseStdoutJson(searchRes.stdout); expect(searched.memory_nodes?.length ?? 0).toBeGreaterThan(0); - const updRes = await runCli([ + const updRes = await runCommandE2e([ "memory", "update", ...memoryLibraryCliArgs(), @@ -189,7 +194,7 @@ describe.skipIf(!isBailianE2EEnabled() || !isDashScopeE2EReady())( ]); expect(updRes.exitCode, updRes.stderr).toBe(0); - const delRes = await runCli([ + const delRes = await runCommandE2e([ "memory", "delete", ...memoryLibraryCliArgs(), diff --git a/packages/cli/tests/e2e/omni.e2e.test.ts b/packages/commands/tests/e2e/omni.e2e.test.ts similarity index 88% rename from packages/cli/tests/e2e/omni.e2e.test.ts rename to packages/commands/tests/e2e/omni.e2e.test.ts index b6f6853..cdf83b6 100644 --- a/packages/cli/tests/e2e/omni.e2e.test.ts +++ b/packages/commands/tests/e2e/omni.e2e.test.ts @@ -6,12 +6,12 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; describe("e2e: omni", () => { test("omni --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["omni", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["omni", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/omni|--message|--audio|text-only/i); }); @@ -21,7 +21,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: omni(DashScope 媒体)", () => { test("omni --list-voices 输出音色列表并退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["omni", "--list-voices"]); + const { stdout, stderr, exitCode } = await runCommandE2e(["omni", "--list-voices"]); expect(exitCode, stderr).toBe(0); expect(stdout).toMatch(/Omni output voices:/); expect(stdout).toMatch(/Tina/); @@ -30,13 +30,13 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("omni 缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["omni", "--model", "qwen3.5-omni-flash"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stderr, exitCode } = await runCommandE2e([ "omni", "--model", "qwen3.5-omni-flash", @@ -51,7 +51,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("omni --dry-run --audio 构造 input_audio 而非 audio_url", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "omni", "--dry-run", "--model", @@ -91,7 +91,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( const clipText = "端到端Omni音频测试"; const clipWav = join(outDir, "e2e-omni-input.wav"); - const syn = await runCli([ + const syn = await runCommandE2e([ "speech", "synthesize", "--model", @@ -109,7 +109,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( ]); expect(syn.exitCode, syn.stderr).toBe(0); - const omni = await runCli([ + const omni = await runCommandE2e([ "omni", "--model", "qwen3.5-omni-flash", diff --git a/packages/cli/tests/e2e/pipeline.e2e.test.ts b/packages/commands/tests/e2e/pipeline.e2e.test.ts similarity index 87% rename from packages/cli/tests/e2e/pipeline.e2e.test.ts rename to packages/commands/tests/e2e/pipeline.e2e.test.ts index 5fa8b85..8589a77 100644 --- a/packages/cli/tests/e2e/pipeline.e2e.test.ts +++ b/packages/commands/tests/e2e/pipeline.e2e.test.ts @@ -2,7 +2,7 @@ import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { parseStdoutJson, runCommandE2e } from "./helpers.ts"; describe("e2e: pipeline", () => { let tempDir: string; @@ -64,7 +64,7 @@ describe("e2e: pipeline", () => { }); test("pipeline 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["pipeline"]); + const { stdout, stderr, exitCode } = await runCommandE2e(["pipeline"]); expect(exitCode, stderr).toBe(0); const out = `${stdout}\n${stderr}`; expect(out).toMatch(/pipeline|run|validate/i); @@ -73,20 +73,20 @@ describe("e2e: pipeline", () => { }); test("pipeline run --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["pipeline", "run", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["pipeline", "validate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "pipeline", "validate", "--file", @@ -101,7 +101,7 @@ describe("e2e: pipeline", () => { }); test("pipeline validate 使用 config 输出格式", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( ["pipeline", "validate", "--file", chatBasicPath], { DASHSCOPE_OUTPUT: "text", @@ -112,7 +112,7 @@ describe("e2e: pipeline", () => { }); test("pipeline validate 拒绝非法依赖 workflow", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "pipeline", "validate", "--file", @@ -128,13 +128,13 @@ describe("e2e: pipeline", () => { }); test("pipeline run 缺少 --file 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["pipeline", "run", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "pipeline", "run", "--file", @@ -167,7 +167,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run 使用 config 输出格式", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( ["pipeline", "run", "--file", chatBasicPath, "--input", '{"message":"hello"}', "--dry-run"], { DASHSCOPE_OUTPUT: "text" }, ); @@ -177,7 +177,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run --verbose 打印总步数和当前步骤序号", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "pipeline", "run", "--file", @@ -193,7 +193,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run --events jsonl 在 dry-run 下输出生命周期事件", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "pipeline", "run", "--file", @@ -221,7 +221,7 @@ describe("e2e: pipeline", () => { }); test("pipeline run 拒绝未知 events format", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "pipeline", "run", "--file", diff --git a/packages/cli/tests/e2e/quota.e2e.test.ts b/packages/commands/tests/e2e/quota.e2e.test.ts similarity index 77% rename from packages/cli/tests/e2e/quota.e2e.test.ts rename to packages/commands/tests/e2e/quota.e2e.test.ts index 857d5d8..d332b76 100644 --- a/packages/cli/tests/e2e/quota.e2e.test.ts +++ b/packages/commands/tests/e2e/quota.e2e.test.ts @@ -1,16 +1,21 @@ import { describe, expect, test } from "vite-plus/test"; -import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts"; +import { + isConsoleE2EReady, + isConsoleAuthFailure, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; describe("e2e: quota", () => { test("quota list --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["quota", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["quota", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--model"); expect(stderr).toContain("--all"); }); test("quota list --help 包含所有示例", async () => { - const { stderr, exitCode } = await runCli(["quota", "list", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["quota", "list", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl quota list"); expect(stderr).toContain("bl quota list --model qwen3.6-plus"); @@ -18,21 +23,21 @@ describe("e2e: quota", () => { }); test("quota request --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["quota", "request", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["quota", "history", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["quota", "history", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--page"); expect(stderr).toContain("--model"); }); test("quota check --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["quota", "check", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["quota", "check", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--model"); expect(stderr).toContain("--period"); @@ -40,7 +45,7 @@ describe("e2e: quota", () => { }); test("quota check --period 0 报错最小值", async () => { - const { stderr, exitCode } = await runCli(["quota", "check", "--period", "0.5"]); + const { stderr, exitCode } = await runCommandE2e(["quota", "check", "--period", "0.5"]); expect(exitCode).toBe(2); expect(stderr).toContain("at least 1 minute"); }); @@ -48,7 +53,7 @@ describe("e2e: quota", () => { describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { test("quota list --dry-run 输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "list", "--dry-run", @@ -68,7 +73,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list --dry-run --all 不传 supports 过滤", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "list", "--all", @@ -84,19 +89,26 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list 文本输出包含英文表头", async () => { - const result = await runCli(["quota", "list", "--output", "text"]); + const result = await runCommandE2e(["quota", "list", "--output", "text"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota list --model 指定模型返回结果", async () => { - const result = await runCli(["quota", "list", "--model", "qwen3.6-plus", "--output", "text"]); + const result = await runCommandE2e([ + "quota", + "list", + "--model", + "qwen3.6-plus", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota list --model 不存在的模型报错", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "quota", "list", "--model", @@ -110,13 +122,13 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota list JSON 输出包含 model/rpm/tpm/maxTPM", async () => { - const result = await runCli(["quota", "list", "--output", "json"]); + const result = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "request", "--model", @@ -138,7 +150,14 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota request TPM 超范围报错", async () => { - const result = await runCli(["quota", "request", "--model", "qwen3.6-plus", "--tpm", "999"]); + const result = await runCommandE2e([ + "quota", + "request", + "--model", + "qwen3.6-plus", + "--tpm", + "999", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode).toBe(2); expect(result.stderr).toContain("out of range"); @@ -147,7 +166,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota request 不支持提额的模型报错", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "quota", "request", "--model", @@ -161,7 +180,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota history --dry-run 输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "history", "--dry-run", @@ -179,7 +198,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check --dry-run 输出 API 信息", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "check", "--dry-run", @@ -196,7 +215,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check --dry-run --console-region 透传", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "check", "--dry-run", @@ -211,19 +230,26 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check 文本输出包含英文表头", async () => { - const result = await runCli(["quota", "check", "--output", "text"]); + const result = await runCommandE2e(["quota", "check", "--output", "text"]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota check --model 指定单模型", async () => { - const result = await runCli(["quota", "check", "--model", "qwen3.6-plus", "--output", "text"]); + const result = await runCommandE2e([ + "quota", + "check", + "--model", + "qwen3.6-plus", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota check --model 逗号分隔多模型", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "quota", "check", "--model", @@ -236,13 +262,20 @@ describe.skipIf(!isConsoleE2EReady())("e2e: quota(Console)", () => { }); test("quota check JSON 输出包含用量和限额字段", async () => { - const result = await runCli(["quota", "check", "--model", "qwen3.6-plus", "--output", "json"]); + const result = await runCommandE2e([ + "quota", + "check", + "--model", + "qwen3.6-plus", + "--output", + "json", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("quota history --dry-run --page 2 --page-size 20", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "quota", "history", "--page", diff --git a/packages/cli/tests/e2e/search-web.e2e.test.ts b/packages/commands/tests/e2e/search-web.e2e.test.ts similarity index 83% rename from packages/cli/tests/e2e/search-web.e2e.test.ts rename to packages/commands/tests/e2e/search-web.e2e.test.ts index 817f7f7..9ae5c64 100644 --- a/packages/cli/tests/e2e/search-web.e2e.test.ts +++ b/packages/commands/tests/e2e/search-web.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> { const envelope = parseStdoutJson<{ content?: Array<{ type?: string; text?: string }> }>(stdout); @@ -16,20 +16,20 @@ function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: describe("e2e: search web", () => { test("search 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["search"]); + 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 runCli(["search", "web", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( ["search", "web", "--dry-run", "--list-tools", "--output", "json"], { DASHSCOPE_API_KEY: undefined, @@ -44,13 +44,13 @@ describe("e2e: search web", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { test("search web 缺少 --query 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["search", "web", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "search", "web", "--dry-run", @@ -74,7 +74,7 @@ describe.skipIf(!isDashScopeE2EReady())("e2e: search web", () => { }); test("联网搜索返回 JSON 且含搜索结果", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "search", "web", "--query", diff --git a/packages/cli/tests/e2e/speech-list-voices.e2e.test.ts b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts similarity index 72% rename from packages/cli/tests/e2e/speech-list-voices.e2e.test.ts rename to packages/commands/tests/e2e/speech-list-voices.e2e.test.ts index 65a7120..057c020 100644 --- a/packages/cli/tests/e2e/speech-list-voices.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-list-voices.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, runCommandE2e } from "./helpers.ts"; /** * Speech list-voices E2E @@ -7,20 +7,20 @@ import { isDashScopeE2EReady, runCli } from "./helpers.ts"; describe("e2e: speech list-voices", () => { test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["speech"]); + 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 runCli(["speech", "synthesize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli(["speech", "recognize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recognize|--url|audio|model/i); }); @@ -28,13 +28,13 @@ describe("e2e: speech list-voices", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: speech list-voices", () => { test("speech synthesize 缺少 --text 且非 --list-voices 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["speech", "synthesize", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["speech", "synthesize", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--text|Usage:/i); }); test("【cosyvoice-v3-flash】获取音色列表", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "speech", "synthesize", "--list-voices", diff --git a/packages/cli/tests/e2e/speech-recognize.e2e.test.ts b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts similarity index 86% rename from packages/cli/tests/e2e/speech-recognize.e2e.test.ts rename to packages/commands/tests/e2e/speech-recognize.e2e.test.ts index cc4a106..a2209ed 100644 --- a/packages/cli/tests/e2e/speech-recognize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts @@ -7,7 +7,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,13 +16,13 @@ import { describe("e2e: speech recognize", () => { test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["speech"]); + 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 runCli(["speech", "recognize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/recognize|--url|model|audio/i); }); @@ -32,7 +32,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: speech recognize(DashScope 媒体)", () => { test("speech recognize 缺少 --url 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["speech", "recognize", "--quiet"]); + const { stderr, exitCode } = await runCommandE2e(["speech", "recognize", "--quiet"]); expect(exitCode).toBe(2); expect(stderr).toMatch(/--url|Usage:/i); }); @@ -40,7 +40,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 runCli([ + const syn = await runCommandE2e([ "speech", "synthesize", "--model", @@ -60,7 +60,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( expect(audioUrl?.startsWith("http")).toBe(true); const asrJson = join(outDir, "e2e-asr.json"); - const rec = await runCli([ + const rec = await runCommandE2e([ "speech", "recognize", "--model", diff --git a/packages/cli/tests/e2e/speech-synthesize.e2e.test.ts b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts similarity index 87% rename from packages/cli/tests/e2e/speech-synthesize.e2e.test.ts rename to packages/commands/tests/e2e/speech-synthesize.e2e.test.ts index 6dfe1a0..3f73472 100644 --- a/packages/cli/tests/e2e/speech-synthesize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts @@ -6,7 +6,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -15,13 +15,13 @@ import { describe("e2e: speech synthesize", () => { test("speech 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["speech"]); + 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 runCli(["speech", "synthesize", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["speech", "synthesize", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/synthesize|--text|--voice|model/i); }); @@ -31,7 +31,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( "e2e: speech synthesize(DashScope 媒体)", () => { test("speech synthesize 缺少 --text 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "speech", "synthesize", "--model", @@ -44,7 +44,7 @@ describe.skipIf(!isBailianE2EMediaEnabled() || !isDashScopeE2EReady())( }); test("speech synthesize --dry-run 仅输出 request 且不调 TTS", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "speech", "synthesize", "--dry-run", @@ -68,7 +68,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "speech", "synthesize", "--model", diff --git a/packages/cli/tests/e2e/text-chat.e2e.test.ts b/packages/commands/tests/e2e/text-chat.e2e.test.ts similarity index 80% rename from packages/cli/tests/e2e/text-chat.e2e.test.ts rename to packages/commands/tests/e2e/text-chat.e2e.test.ts index c54b6fb..c8e10b0 100644 --- a/packages/cli/tests/e2e/text-chat.e2e.test.ts +++ b/packages/commands/tests/e2e/text-chat.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; /** * Text chat:help / 分组不依赖密钥;对话需 DashScope。 @@ -7,13 +7,13 @@ import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; describe("e2e: text chat", () => { test("text 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["text"]); + 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 runCli(["text", "chat", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["text", "chat", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/chat|--message|model|stream/i); }); @@ -21,13 +21,13 @@ describe("e2e: text chat", () => { describe.skipIf(!isDashScopeE2EReady())("e2e: text chat(DashScope)", () => { test("text chat 缺少 --message 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["text", "chat", "--model", "qwen3.7-max"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "text", "chat", "--model", diff --git a/packages/cli/tests/e2e/usage-free.e2e.test.ts b/packages/commands/tests/e2e/usage-free.e2e.test.ts similarity index 73% rename from packages/cli/tests/e2e/usage-free.e2e.test.ts rename to packages/commands/tests/e2e/usage-free.e2e.test.ts index 40521d0..7967b65 100644 --- a/packages/cli/tests/e2e/usage-free.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-free.e2e.test.ts @@ -1,22 +1,27 @@ import { describe, expect, test } from "vite-plus/test"; -import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts"; +import { + isConsoleE2EReady, + isConsoleAuthFailure, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; describe("e2e: usage free", () => { test("usage 分组展示子命令帮助且退出码为 0", async () => { - const { stdout, stderr, exitCode } = await runCli(["usage"]); + 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 runCli(["usage", "free", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["usage", "free", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--model|quota|free-tier/i); }); test("usage free --help 包含所有示例", async () => { - const { stderr, exitCode } = await runCli(["usage", "free", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["usage", "free", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl usage free"); expect(stderr).toContain("bl usage free --model qwen3-max"); @@ -26,7 +31,7 @@ describe("e2e: usage free", () => { describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { test("usage free --dry-run --model 输出请求参数不发起调用", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "free", "--dry-run", @@ -45,7 +50,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 逗号分隔多个模型", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "free", "--dry-run", @@ -62,7 +67,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 重复模型名自动去重", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "free", "--dry-run", @@ -79,7 +84,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run --model 逗号间有空格也能正确解析", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "free", "--dry-run", @@ -96,30 +101,57 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --dry-run 不指定 --model 传全量模型列表", async () => { - const { stderr, exitCode } = await runCli(["usage", "free", "--dry-run", "--output", "json"]); + const { stderr, exitCode } = await runCommandE2e([ + "usage", + "free", + "--dry-run", + "--output", + "json", + ]); expect(exitCode, stderr).toBe(0); }); test("usage free --model 单模型查询返回 JSON 结果", async () => { - const result = await runCli(["usage", "free", "--model", "qwen3-max", "--output", "json"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "qwen3-max", + "--output", + "json", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model 单模型文本输出包含表头", async () => { - const result = await runCli(["usage", "free", "--model", "qwen3-max", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "qwen3-max", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model 文本输出包含模型名", async () => { - const result = await runCli(["usage", "free", "--model", "qwen3-max", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "qwen3-max", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model 逗号分隔多模型文本输出包含所有模型", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "free", "--model", @@ -132,25 +164,46 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model 文本输出包含正确的 Type 列", async () => { - const result = await runCli(["usage", "free", "--model", "qwen3-max", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "qwen3-max", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model quotaStatus 为 UNKNOWN 时 Auto-Stop 显示 Unsupported", async () => { - const result = await runCli(["usage", "free", "--model", "wan2.7-image", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "wan2.7-image", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model quotaStatus 为 UNKNOWN 时额度显示为 -", async () => { - const result = await runCli(["usage", "free", "--model", "wan2.7-image", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "wan2.7-image", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model 不存在的模型仍返回表格行", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "free", "--model", @@ -163,13 +216,20 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage free(Console)", () => { }); test("usage free --model Auto-Stop 显示 ON、OFF 或 Unsupported", async () => { - const result = await runCli(["usage", "free", "--model", "qwen3-max", "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "free", + "--model", + "qwen3-max", + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage free --model --console-region cn-beijing 指定区域查询", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "free", "--model", diff --git a/packages/cli/tests/e2e/usage-stats.e2e.test.ts b/packages/commands/tests/e2e/usage-stats.e2e.test.ts similarity index 83% rename from packages/cli/tests/e2e/usage-stats.e2e.test.ts rename to packages/commands/tests/e2e/usage-stats.e2e.test.ts index cdda8bf..b6e2320 100644 --- a/packages/cli/tests/e2e/usage-stats.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-stats.e2e.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vite-plus/test"; -import { isConsoleE2EReady, isConsoleAuthFailure, parseStdoutJson, runCli } from "./helpers.ts"; +import { + isConsoleE2EReady, + isConsoleAuthFailure, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; import { readConfigFile } from "bailian-cli-core"; function getStaticWorkspaceId(): string | undefined { @@ -20,7 +25,7 @@ async function fetchDefaultWorkspaceId(): Promise { const staticId = getStaticWorkspaceId(); if (staticId) return staticId; - const result = await runCli(["workspace", "list", "--output", "json"]); + const result = await runCommandE2e(["workspace", "list", "--output", "json"]); if (isConsoleAuthFailure(result) || result.exitCode !== 0) return FALLBACK_WORKSPACE_ID; try { const parsed = JSON.parse(result.stdout); @@ -36,13 +41,13 @@ async function fetchDefaultWorkspaceId(): Promise { describe("e2e: usage stats", () => { test("usage stats --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--model|--days|stats/i); }); test("usage stats --help 包含所有示例", async () => { - const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("bl usage stats"); expect(stderr).toContain("bl usage stats --model qwen-turbo"); @@ -50,7 +55,7 @@ describe("e2e: usage stats", () => { }); test("usage stats --help 包含 --workspace-id 选项", async () => { - const { stderr, exitCode } = await runCli(["usage", "stats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["usage", "stats", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toContain("--workspace-id"); }); @@ -66,7 +71,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run 概览模式输出请求参数", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -95,7 +100,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --days 30 时间跨度约 30 天", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -117,7 +122,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --model 指定模型使用 list API", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -139,7 +144,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --dry-run --type Text 传递 obsModelType", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -158,25 +163,46 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats 概览模式返回 JSON 结果", async () => { - const result = await runCli(["usage", "stats", "--workspace-id", wsId, "--output", "json"]); + const result = await runCommandE2e([ + "usage", + "stats", + "--workspace-id", + wsId, + "--output", + "json", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage stats 概览文本输出包含英文标签", async () => { - const result = await runCli(["usage", "stats", "--workspace-id", wsId, "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "stats", + "--workspace-id", + wsId, + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage stats 概览文本输出包含 Token 用量", async () => { - const result = await runCli(["usage", "stats", "--workspace-id", wsId, "--output", "text"]); + const result = await runCommandE2e([ + "usage", + "stats", + "--workspace-id", + wsId, + "--output", + "text", + ]); if (isConsoleAuthFailure(result)) return; expect(result.exitCode, result.stderr).toBe(0); }); test("usage stats --model 单模型文本输出包含英文表头", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -191,7 +217,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --model 逗号分隔多模型返回多行", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -206,7 +232,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --model 不存在的模型返回空表格", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -221,7 +247,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --days 1 短时间范围正常返回", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "stats", "--workspace-id", @@ -236,7 +262,7 @@ describe.skipIf(!isConsoleE2EReady())("e2e: usage stats(Console)", () => { }); test("usage stats --type Vision 按类型过滤", async () => { - const result = await runCli([ + const result = await runCommandE2e([ "usage", "stats", "--workspace-id", diff --git a/packages/cli/tests/e2e/video-download.e2e.test.ts b/packages/commands/tests/e2e/video-download.e2e.test.ts similarity index 89% rename from packages/cli/tests/e2e/video-download.e2e.test.ts rename to packages/commands/tests/e2e/video-download.e2e.test.ts index 70e79c7..46a6bd9 100644 --- a/packages/cli/tests/e2e/video-download.e2e.test.ts +++ b/packages/commands/tests/e2e/video-download.e2e.test.ts @@ -8,7 +8,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** dry-run 占位 UUID */ @@ -21,13 +21,13 @@ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001"; describe("e2e: video download", () => { test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["video"]); + 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 runCli(["video", "download", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["video", "download", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/download|--task-id|--out/i); }); @@ -37,7 +37,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video download(DashScope 视频)", () => { test("video download 缺少 --task-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "download", "--out", @@ -48,7 +48,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video download 缺少 --out 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "download", "--task-id", @@ -61,7 +61,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "download", "--dry-run", @@ -83,7 +83,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const genMp4 = join(outDir, "e2e-gen-for-download.mp4"); - const gen = await runCli([ + const gen = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), @@ -111,7 +111,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( expect(existsSync(genMp4)).toBe(true); const downloadMp4 = join(outDir, "e2e-download.mp4"); - const dl = await runCli([ + const dl = await runCommandE2e([ "video", "download", ...cliTimeoutPrefix(), diff --git a/packages/cli/tests/e2e/video-edit.e2e.test.ts b/packages/commands/tests/e2e/video-edit.e2e.test.ts similarity index 88% rename from packages/cli/tests/e2e/video-edit.e2e.test.ts rename to packages/commands/tests/e2e/video-edit.e2e.test.ts index fa6e9a2..fd692eb 100644 --- a/packages/cli/tests/e2e/video-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/video-edit.e2e.test.ts @@ -7,7 +7,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,19 +16,19 @@ import { describe("e2e: video edit", () => { test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["video"]); + 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 runCli(["video", "edit", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "edit", "--dry-run", @@ -54,7 +54,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video edit(DashScope 视频)", () => { test("video edit 缺少 --video 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "edit", ...cliTimeoutPrefix(), @@ -71,7 +71,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); const t2vPath = join(outDir, "e2e-video-t2v.mp4"); - const t2v = await runCli([ + const t2v = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), @@ -88,7 +88,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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "edit", ...cliTimeoutPrefix(), diff --git a/packages/cli/tests/e2e/video-generate-i2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts similarity index 88% rename from packages/cli/tests/e2e/video-generate-i2v.e2e.test.ts rename to packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts index 1c54b9d..e0f3ecd 100644 --- a/packages/cli/tests/e2e/video-generate-i2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts @@ -7,7 +7,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,13 +16,13 @@ import { describe("e2e: video generate (i2v)", () => { test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["video"]); + 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 runCli(["video", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["video", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--image|model/i); }); @@ -32,7 +32,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video generate (i2v)(DashScope 视频)", () => { test("video generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), @@ -46,7 +46,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video generate --dry-run(无 --image)仅输出 request(t2v 路径不调上传)", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), @@ -69,7 +69,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 runCli([ + const gen = await runCommandE2e([ "image", "generate", "--model", @@ -87,7 +87,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const genData = parseStdoutJson<{ saved?: string[] }>(gen.stdout); const imagePath = genData.saved?.[0] ?? png; - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), diff --git a/packages/cli/tests/e2e/video-generate-t2v.e2e.test.ts b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts similarity index 87% rename from packages/cli/tests/e2e/video-generate-t2v.e2e.test.ts rename to packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts index 41d8685..86507dc 100644 --- a/packages/cli/tests/e2e/video-generate-t2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts @@ -7,7 +7,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,13 +16,13 @@ import { describe("e2e: video generate (t2v)", () => { test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["video"]); + 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 runCli(["video", "generate", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["video", "generate", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/generate|--prompt|--model|download|image/i); }); @@ -32,7 +32,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video generate (t2v)(DashScope 视频)", () => { test("video generate 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), @@ -44,7 +44,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video generate --dry-run(无 --image)仅输出 request 且不调生成接口", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "generate", "--dry-run", @@ -66,7 +66,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("【happyhorse-1.1-t2v】文本生成视频", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "generate", ...cliTimeoutPrefix(), diff --git a/packages/cli/tests/e2e/video-ref-r2v.e2e.test.ts b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts similarity index 88% rename from packages/cli/tests/e2e/video-ref-r2v.e2e.test.ts rename to packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts index 55b80a0..b3839f6 100644 --- a/packages/cli/tests/e2e/video-ref-r2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts @@ -7,7 +7,7 @@ import { isDashScopeE2EReady, makeE2eOutputDir, parseStdoutJson, - runCli, + runCommandE2e, } from "./helpers.ts"; /** @@ -16,19 +16,19 @@ import { describe("e2e: video ref (r2v)", () => { test("video 分组展示子命令帮助且成功退出", async () => { - const { stdout, stderr, exitCode } = await runCli(["video"]); + 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 runCli(["video", "ref", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["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 runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "ref", "--dry-run", @@ -54,7 +54,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( "e2e: video ref (r2v)(DashScope 视频)", () => { test("video ref 缺少 --prompt 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "ref", ...cliTimeoutPrefix(), @@ -68,7 +68,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( }); test("video ref 缺少 --image 与 --ref-video 时退出为用法错误 (2)", async () => { - const { stderr, exitCode } = await runCli([ + const { stderr, exitCode } = await runCommandE2e([ "video", "ref", ...cliTimeoutPrefix(), @@ -83,7 +83,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( test("【happyhorse-1.1-r2v】视频参考生成", async () => { const outDir = makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url)); - const gen = await runCli([ + const gen = await runCommandE2e([ "image", "generate", "--model", @@ -102,7 +102,7 @@ describe.skipIf(!isBailianE2EVideoEnabled() || !isDashScopeE2EReady())( const imagePath = genData.saved?.[0]; expect(imagePath).toBeTruthy(); - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "ref", ...cliTimeoutPrefix(), diff --git a/packages/cli/tests/e2e/video-task-get.e2e.test.ts b/packages/commands/tests/e2e/video-task-get.e2e.test.ts similarity index 77% rename from packages/cli/tests/e2e/video-task-get.e2e.test.ts rename to packages/commands/tests/e2e/video-task-get.e2e.test.ts index 0ea0fc7..ddd4162 100644 --- a/packages/cli/tests/e2e/video-task-get.e2e.test.ts +++ b/packages/commands/tests/e2e/video-task-get.e2e.test.ts @@ -1,5 +1,10 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { + isBailianE2EEnabled, + isDashScopeE2EReady, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim(); @@ -9,13 +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 runCli(["video"]); + 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 runCli(["video", "task", "get", "--help"]); + const { stderr, exitCode } = await runCommandE2e(["video", "task", "get", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/task|get|--task-id/i); }); @@ -25,7 +30,13 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( "e2e: video task get(DashScope)", () => { test("video task get 缺少 --task-id 时报用法错误并退出 (2)", async () => { - const { stderr, exitCode } = await runCli(["video", "task", "get", "--output", "json"]); + const { stderr, exitCode } = await runCommandE2e([ + "video", + "task", + "get", + "--output", + "json", + ]); expect(exitCode).toBe(2); const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } }; expect(err.error?.code).toBe(2); @@ -33,7 +44,7 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( }); test("video task get --dry-run 仅回显 task_id 且不调任务接口", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "task", "get", @@ -49,7 +60,7 @@ describe.skipIf(!isBailianE2EEnabled() || !taskId || !isDashScopeE2EReady())( }); test("根据 task_id 查询任务状态", async () => { - const { stdout, stderr, exitCode } = await runCli([ + const { stdout, stderr, exitCode } = await runCommandE2e([ "video", "task", "get", diff --git a/packages/commands/vite.config.ts b/packages/commands/vite.config.ts index 1c26ed4..7325db8 100644 --- a/packages/commands/vite.config.ts +++ b/packages/commands/vite.config.ts @@ -1,6 +1,11 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ + test: { + globalSetup: "../e2e/src/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, pack: { minify: true, dts: { diff --git a/packages/e2e/package.json b/packages/e2e/package.json new file mode 100644 index 0000000..9abcd17 --- /dev/null +++ b/packages/e2e/package.json @@ -0,0 +1,25 @@ +{ + "name": "e2e", + "version": "0.0.0", + "private": true, + "description": "Shared E2E harness utilities for bailian-cli monorepo (not published).", + "type": "module", + "exports": { + "./gating": "./src/gating.ts", + "./runner": "./src/run-subprocess.ts", + "./output": "./src/output.ts", + "./global-setup": "./src/global-setup.ts", + "./monorepo-root": "./src/monorepo-root.ts" + }, + "dependencies": { + "bailian-cli-core": "workspace:*" + }, + "devDependencies": { + "@types/node": "catalog:", + "typescript": "^6.0.2", + "vite-plus": "0.1.22" + }, + "engines": { + "node": ">=22.12.0" + } +} diff --git a/packages/e2e/src/gating.ts b/packages/e2e/src/gating.ts new file mode 100644 index 0000000..36ade27 --- /dev/null +++ b/packages/e2e/src/gating.ts @@ -0,0 +1,56 @@ +import { readConfigFile } from "bailian-cli-core"; + +/** 显式开启后才跑真实网络 E2E */ +export function isBailianE2EEnabled(): boolean { + return process.env.BAILIAN_E2E === "1"; +} + +/** 可调 DashScope 的 API Key:环境变量优先,否则读 ~/.bailian/config.json */ +export function isDashScopeE2EReady(): boolean { + if (!isBailianE2EEnabled()) return false; + if (process.env.DASHSCOPE_API_KEY?.trim()) return true; + try { + const f = readConfigFile(); + return typeof f.api_key === "string" && f.api_key.length > 0; + } catch { + return false; + } +} + +/** Console-gateway 命令 E2E 就绪检查 */ +export function isConsoleE2EReady(): boolean { + if (!isBailianE2EEnabled()) return false; + try { + const config = readConfigFile(); + return typeof config.access_token === "string" && config.access_token.length > 0; + } catch { + return false; + } +} + +/** 语音与图像(可设 `BAILIAN_E2E_MEDIA=0` 跳过) */ +export function isBailianE2EMediaEnabled(): boolean { + if (process.env.BAILIAN_E2E_MEDIA === "0") return false; + return isBailianE2EEnabled(); +} + +/** 文生视频 / 图生视频等(耗时长,默认关闭) */ +export function isBailianE2EVideoEnabled(): boolean { + return isBailianE2EEnabled() && process.env.BAILIAN_E2E_VIDEO === "1"; +} + +/** 知识检索 E2E 就绪 */ +export function isSearchE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return ( + !!process.env.BAILIAN_E2E_SEARCH_AGENT_ID?.trim() && !!process.env.BAILIAN_WORKSPACE_ID?.trim() + ); +} + +/** 知识问答 E2E 就绪 */ +export function isChatE2EReady(): boolean { + if (!isDashScopeE2EReady()) return false; + return ( + !!process.env.BAILIAN_E2E_CHAT_AGENT_ID?.trim() && !!process.env.BAILIAN_WORKSPACE_ID?.trim() + ); +} diff --git a/packages/cli/tests/e2e/global-setup.ts b/packages/e2e/src/global-setup.ts similarity index 65% rename from packages/cli/tests/e2e/global-setup.ts rename to packages/e2e/src/global-setup.ts index de36bd8..f12704c 100644 --- a/packages/cli/tests/e2e/global-setup.ts +++ b/packages/e2e/src/global-setup.ts @@ -1,16 +1,17 @@ -import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs"; +import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs"; import { join } from "path"; import { parseEnv } from "util"; -import { E2E_RUN_SESSION_FILENAME, monorepoRoot } from "./helpers.ts"; +import { monorepoRoot } from "./monorepo-root.ts"; + +/** Vitest globalSetup 写入的会话标记文件名 */ +export const E2E_RUN_SESSION_FILENAME = ".e2e-run-session"; /** - * Vitest 在所有 worker 启动前执行一次:写入共享会话 id,使多进程并行时仍共用一个 `test/output/<会话>/`。 - * 结束后删除标记文件,避免非 Vitest 流程误用上一次会话 id。 + * Vitest 在所有 worker 启动前执行一次:加载 `.env` 并写入共享 E2E 输出会话 id。 */ - export default function vitestGlobalSetup(): () => void { - // 加载根目录 `.env` 合并变量(包含shell 注入) - const rootEnv = join(monorepoRoot(), ".env"); + const root = monorepoRoot(); + const rootEnv = join(root, ".env"); if (existsSync(rootEnv)) { const parsed = parseEnv(readFileSync(rootEnv, "utf8")); Object.assign(process.env, parsed); @@ -18,34 +19,36 @@ export default function vitestGlobalSetup(): () => void { process.env.BAILIAN_E2E = "1"; process.env.BAILIAN_E2E_MEDIA = "1"; process.env.BAILIAN_E2E_VIDEO = "1"; - // 如根目录不存在 .env,则生成一个 .env const envContent = `# 是否开启 E2E 测试 BAILIAN_E2E=1 # 是否开启图片/语音 E2E 测试 BAILIAN_E2E_MEDIA=1 # 是否开启视频 E2E 测试 BAILIAN_E2E_VIDEO=1 +# DashScope Base URL +DASHSCOPE_BASE_URL= # DashScope API Key DASHSCOPE_API_KEY= # ------------------------------- BAILIAN_E2E_VIDEO_TASK_ID=b499a8cb-1fc4-4d43-9495-e23c7f78ae0d # ------------------------------- -# 知识库 ID +# Workspace ID BAILIAN_WORKSPACE_ID= -# 索引 ID -BAILIAN_E2E_INDEX_ID= # ------------------------------- - `; +# 知识库检索Agent ID +BAILIAN_E2E_SEARCH_AGENT_ID= +# 知识库问答Agent ID +BAILIAN_E2E_CHAT_AGENT_ID= +`; writeFileSync(rootEnv, envContent, "utf8"); } - // 创建生成内容目录 const now = new Date(); const pad = (n: number) => n.toString().padStart(2, "0"); const dateStr = [now.getFullYear(), pad(now.getMonth() + 1), pad(now.getDate())].join("-"); const timeStr = [pad(now.getHours()), pad(now.getMinutes()), pad(now.getSeconds())].join(":"); const runId = `e2e-run-${dateStr} ${timeStr}`; - const outDir = join(monorepoRoot(), "test", "output"); + const outDir = join(root, "test", "output"); mkdirSync(outDir, { recursive: true }); const marker = join(outDir, E2E_RUN_SESSION_FILENAME); writeFileSync(marker, `${runId}\n`, "utf8"); @@ -53,7 +56,7 @@ BAILIAN_E2E_INDEX_ID= try { unlinkSync(marker); } catch { - /* 忽略:已删或权限等 */ + /* 忽略 */ } }; } diff --git a/packages/e2e/src/monorepo-root.ts b/packages/e2e/src/monorepo-root.ts new file mode 100644 index 0000000..5676bd2 --- /dev/null +++ b/packages/e2e/src/monorepo-root.ts @@ -0,0 +1,15 @@ +import { existsSync } from "fs"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; + +/** Monorepo 根目录(含根 `package.json`) */ +export function monorepoRoot(fromModuleUrl: string = import.meta.url): string { + let dir = dirname(fileURLToPath(fromModuleUrl)); + for (let i = 0; i < 8; i++) { + if (existsSync(join(dir, "pnpm-workspace.yaml"))) return dir; + const parent = dirname(dir); + if (parent === dir) break; + dir = parent; + } + throw new Error("Could not locate monorepo root (pnpm-workspace.yaml)"); +} diff --git a/packages/e2e/src/output.ts b/packages/e2e/src/output.ts new file mode 100644 index 0000000..385299d --- /dev/null +++ b/packages/e2e/src/output.ts @@ -0,0 +1,77 @@ +import { mkdirSync, readFileSync } from "fs"; +import { basename, join } from "path"; +import { fileURLToPath } from "url"; +import { E2E_RUN_SESSION_FILENAME } from "./global-setup.ts"; +import { monorepoRoot } from "./monorepo-root.ts"; +import type { RunCliResult } from "./run-subprocess.ts"; + +let e2eOutputSessionId: string | undefined; + +function readE2eRunSessionFromOutputDir(): string | undefined { + try { + const p = join(monorepoRoot(), "test", "output", E2E_RUN_SESSION_FILENAME); + const t = readFileSync(p, "utf8").trim(); + return t.length > 0 ? t : undefined; + } catch { + return undefined; + } +} + +function getE2eOutputSessionId(): string { + if (!e2eOutputSessionId) { + const fromEnv = process.env.BAILIAN_E2E_RUN_ID?.trim(); + if (fromEnv) { + e2eOutputSessionId = fromEnv.replace(/[^a-zA-Z0-9._-]+/g, "-"); + } else { + const fromFile = readE2eRunSessionFromOutputDir(); + if (fromFile) { + e2eOutputSessionId = fromFile.replace(/[^a-zA-Z0-9._-]+/g, "-"); + } else { + e2eOutputSessionId = `e2e-run-${Date.now()}-${process.pid}`; + } + } + } + return e2eOutputSessionId; +} + +/** 在 `test/output/<会话>/` 下创建用例子目录 */ +export function makeE2eOutputDir(label: string): string { + const fromEnv = process.env.BAILIAN_E2E_OUT?.trim(); + if (fromEnv) { + mkdirSync(fromEnv, { recursive: true }); + return fromEnv; + } + const safe = label.replace(/[^a-zA-Z0-9._-]+/g, "-"); + const sessionDir = join(monorepoRoot(), "test", "output", getE2eOutputSessionId()); + mkdirSync(sessionDir, { recursive: true }); + const dir = join(sessionDir, `e2e-vp-${safe}-${Date.now()}`); + mkdirSync(dir, { recursive: true }); + return dir; +} + +/** 全局 `--timeout` 秒数(视频等长任务) */ +export function cliTimeoutSeconds(): string { + return process.env.BAILIAN_E2E_TIMEOUT_SEC?.trim() || "3600"; +} + +export function cliTimeoutPrefix(): string[] { + return ["--timeout", cliTimeoutSeconds()]; +} + +/** 从 `import.meta.url` 生成 OUT 子目录标签 */ +export function e2eLabelFromMetaUrl(metaUrl: string): string { + return basename(fileURLToPath(metaUrl), ".ts").replace(/\.e2e\.test$/, ""); +} + +export function parseStdoutJson(stdout: string): T { + const t = stdout.trim(); + const jsonMatch = t.match(/\{[\s\S]*\}/); + if (!jsonMatch) throw new Error(`No JSON object found in stdout: ${t.slice(0, 200)}`); + return JSON.parse(jsonMatch[0]) as T; +} + +/** Console session 未登录/已过期时的优雅失败判定 */ +export function isConsoleAuthFailure(result: RunCliResult): boolean { + if (result.exitCode === 0) return false; + return /not logged in|has expired|NotLogined|Run `bl auth login/i.test(result.stderr); +} diff --git a/packages/e2e/src/run-subprocess.ts b/packages/e2e/src/run-subprocess.ts new file mode 100644 index 0000000..91b6626 --- /dev/null +++ b/packages/e2e/src/run-subprocess.ts @@ -0,0 +1,43 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +export interface RunCliResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** 子进程执行指定 main.ts 入口 */ +export async function runNodeMain( + mainTs: string, + args: string[], + options: { cwd: string; env?: NodeJS.ProcessEnv } = { cwd: process.cwd() }, +): Promise { + try { + const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { + cwd: options.cwd, + encoding: "utf8", + maxBuffer: 32 * 1024 * 1024, + env: { + ...process.env, + NODE_NO_WARNINGS: "1", + DO_NOT_TRACK: "1", + ...options.env, + }, + }); + return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 }; + } catch (err: unknown) { + const e = err as { + stdout?: string; + stderr?: string; + code?: number; + }; + return { + stdout: e.stdout ?? "", + stderr: e.stderr ?? "", + exitCode: typeof e.code === "number" ? e.code : 1, + }; + } +} diff --git a/packages/e2e/vite.config.ts b/packages/e2e/vite.config.ts new file mode 100644 index 0000000..168a023 --- /dev/null +++ b/packages/e2e/vite.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vite-plus"; + +export default defineConfig({ + lint: { + options: { + typeAware: true, + typeCheck: true, + }, + }, + fmt: {}, +}); diff --git a/packages/kscli/package.json b/packages/kscli/package.json index 6c266d3..a2eaf61 100644 --- a/packages/kscli/package.json +++ b/packages/kscli/package.json @@ -59,6 +59,7 @@ "ajv": "catalog:", "boxen": "catalog:", "chalk": "catalog:", + "e2e": "workspace:*", "typescript": "^6.0.2", "undici": "catalog:", "vite-plus": "0.1.22", diff --git a/packages/kscli/tests/e2e/chat.e2e.test.ts b/packages/kscli/tests/e2e/chat.e2e.test.ts deleted file mode 100644 index ed4535f..0000000 --- a/packages/kscli/tests/e2e/chat.e2e.test.ts +++ /dev/null @@ -1,131 +0,0 @@ -import { describe, expect, test } from "vite-plus/test"; -import { isChatE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; - -// ---- Types ---- - -interface ChatJsonResult { - answer: string; - request_id: string; -} - -// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- - -describe.skipIf(!isChatE2EReady())("e2e: kscli chat (live)", () => { - const agentId = process.env.BAILIAN_E2E_CHAT_AGENT_ID!; - const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; - - test("chat (JSON mode) returns answer", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "chat", - "--message", - "什么是大模型?", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.answer).toBeTruthy(); - expect(data.answer.length).toBeGreaterThan(0); - expect(data.request_id).toBeTruthy(); - }); - - test("chat (text mode) returns plain text", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "chat", - "--message", - "什么是RAG?", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "text", - ]); - - expect(exitCode, stderr).toBe(0); - expect(stdout.trim().length).toBeGreaterThan(0); - }); - - test("chat (stream, JSON mode) collects and returns answer", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "chat", - "--message", - "什么是检索增强生成?", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.answer).toBeTruthy(); - expect(data.answer.length).toBeGreaterThan(0); - expect(data.request_id).toBeTruthy(); - }); - - test("chat (stream, text mode) outputs streaming text", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "chat", - "--message", - "什么是向量检索?", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "text", - ]); - - expect(exitCode, stderr).toBe(0); - // Streaming text mode: output should contain some text content - expect(stdout.trim().length).toBeGreaterThan(0); - }); - - test("chat with multi-turn messages returns context-aware answer", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "chat", - "--message", - "user:什么是大模型", - "--message", - "assistant:大模型是大规模语言模型,具有强大的理解和生成能力", - "--message", - "它有哪些应用场景?", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.answer).toBeTruthy(); - expect(data.answer.length).toBeGreaterThan(0); - }); - - test("chat with invalid agent_id fails gracefully", async () => { - const { stderr, exitCode } = await runKscli([ - "chat", - "--message", - "test", - "--agent-id", - "aid-invalid-not-exist", - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode).not.toBe(0); - expect(stderr).toBeTruthy(); - }); -}); diff --git a/packages/kscli/tests/e2e/global-setup.ts b/packages/kscli/tests/e2e/global-setup.ts deleted file mode 100644 index fc93de0..0000000 --- a/packages/kscli/tests/e2e/global-setup.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { loadRootEnv } from "./helpers.ts"; - -/** - * Vitest globalSetup: load monorepo root `.env` into `process.env` before tests run. - */ -export default function vitestGlobalSetup(): () => void { - loadRootEnv(); - return () => {}; -} diff --git a/packages/kscli/tests/e2e/helpers.ts b/packages/kscli/tests/e2e/helpers.ts index c575061..65e56b3 100644 --- a/packages/kscli/tests/e2e/helpers.ts +++ b/packages/kscli/tests/e2e/helpers.ts @@ -1,129 +1,42 @@ -import { execFile } from "child_process"; -import { existsSync, mkdtempSync, readFileSync } from "fs"; +import { mkdtempSync } from "fs"; import { tmpdir } from "os"; -import { promisify } from "util"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { parseEnv } from "util"; - -const execFileAsync = promisify(execFile); - -/** `packages/kscli` 根目录(含 `src/main.ts`) */ +import { parseStdoutJson } from "e2e/output"; +import { runNodeMain, type RunCliResult } from "e2e/runner"; +import { + isBailianE2EEnabled, + isChatE2EReady, + isDashScopeE2EReady, + isSearchE2EReady, +} from "e2e/gating"; +import { monorepoRoot } from "e2e/monorepo-root"; + +export { + isBailianE2EEnabled, + isChatE2EReady, + isDashScopeE2EReady, + isSearchE2EReady, + monorepoRoot, + parseStdoutJson, +}; +export type { RunCliResult }; + +/** `packages/kscli` 根目录 */ export const kscliPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); const mainTs = join(kscliPackageRoot, "src", "main.ts"); -/** Monorepo 根(含根 `package.json` 和 `.env`) */ -export function monorepoRoot(): string { - return join(kscliPackageRoot, "..", ".."); -} - -// ---- E2E gating helpers ---- - -// ---- .env loader (cached) ---- - -let _rootEnvCache: Record | null = null; - -/** 读取 monorepo 根目录 `.env` 并缓存(.env 值优先于 shell 环境变量) */ -function getRootEnv(): Record { - if (_rootEnvCache !== null) return _rootEnvCache; - const rootEnvPath = join(monorepoRoot(), ".env"); - _rootEnvCache = existsSync(rootEnvPath) ? parseEnv(readFileSync(rootEnvPath, "utf8")) : {}; - return _rootEnvCache; -} - -/** 从 .env 或 process.env 获取值(.env 优先) */ -function envVar(key: string): string | undefined { - return getRootEnv()[key] ?? process.env[key]; -} - -// ---- E2E gating helpers ---- - -/** 显式开启后才跑真实网络 E2E */ -export function isBailianE2EEnabled(): boolean { - return envVar("BAILIAN_E2E") === "1"; -} - -/** 是否有 DashScope API Key 可用 */ -export function isDashScopeE2EReady(): boolean { - if (!isBailianE2EEnabled()) return false; - return !!envVar("DASHSCOPE_API_KEY")?.trim(); -} - -/** 知识检索 E2E 就绪:E2E 开启 + API Key + search agent ID + workspace ID */ -export function isSearchE2EReady(): boolean { - if (!isDashScopeE2EReady()) return false; - return ( - !!envVar("BAILIAN_E2E_SEARCH_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim() - ); -} - -/** 知识问答 E2E 就绪:E2E 开启 + API Key + chat agent ID + workspace ID */ -export function isChatE2EReady(): boolean { - if (!isDashScopeE2EReady()) return false; - return !!envVar("BAILIAN_E2E_CHAT_AGENT_ID")?.trim() && !!envVar("BAILIAN_WORKSPACE_ID")?.trim(); -} - -// ---- CLI runner ---- - -export interface RunCliResult { - stdout: string; - stderr: string; - exitCode: number; -} - -/** - * 子进程执行 kscli(等价于 `node packages/kscli/src/main.ts ...`)。 - */ +/** 子进程执行 kscli */ export async function runKscli( args: string[], envOverrides: NodeJS.ProcessEnv = {}, ): Promise { - try { - const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { - cwd: kscliPackageRoot, - encoding: "utf8", - maxBuffer: 32 * 1024 * 1024, - env: { - ...process.env, - // .env values override shell env vars (ensures correct API key is used) - ...getRootEnv(), - // Unique clean config dir per run — prevents stale config.json from previous tests - BAILIAN_CONFIG_DIR: mkdtempSync(join(tmpdir(), "kscli-test-")), - NODE_NO_WARNINGS: "1", - DO_NOT_TRACK: "1", - ...envOverrides, - }, - }); - return { stdout: stdout ?? "", stderr: stderr ?? "", exitCode: 0 }; - } catch (err: unknown) { - const e = err as { - stdout?: string; - stderr?: string; - code?: number; - }; - return { - stdout: e.stdout ?? "", - stderr: e.stderr ?? "", - exitCode: typeof e.code === "number" ? e.code : 1, - }; - } -} - -export function parseStdoutJson(stdout: string): T { - const t = stdout.trim(); - return JSON.parse(t) as T; -} - -// ---- Global setup: load root .env ---- - -/** - * Vitest globalSetup:加载 monorepo 根目录 `.env` 合并到 `process.env`。 - */ -export function loadRootEnv(): void { - const rootEnv = join(monorepoRoot(), ".env"); - if (existsSync(rootEnv)) { - const parsed = parseEnv(readFileSync(rootEnv, "utf8")); - Object.assign(process.env, parsed); - } + return runNodeMain(mainTs, args, { + cwd: kscliPackageRoot, + env: { + BAILIAN_CONFIG_DIR: mkdtempSync(join(tmpdir(), "kscli-test-")), + ...envOverrides, + }, + }); } diff --git a/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts b/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts new file mode 100644 index 0000000..6cbe5be --- /dev/null +++ b/packages/kscli/tests/e2e/registry.smoke.e2e.test.ts @@ -0,0 +1,45 @@ +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; +import { describe, expect, test } from "vite-plus/test"; +import { runNodeMain } from "e2e/runner"; +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(); +} + +describe("e2e: kscli registry smoke", () => { + test.each(kscliPaths)("已注册命令 %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)); + }); +}); diff --git a/packages/kscli/tests/e2e/search.e2e.test.ts b/packages/kscli/tests/e2e/search.e2e.test.ts deleted file mode 100644 index 93f5b6f..0000000 --- a/packages/kscli/tests/e2e/search.e2e.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { describe, expect, test } from "vite-plus/test"; -import { isSearchE2EReady, parseStdoutJson, runKscli } from "./helpers.ts"; - -// ---- Types ---- - -interface SearchResponse { - code: string; - status_code: number; - request_id: string; - data: { - total: number; - cost_time: number; - nodes: Array<{ - score: number; - text: string; - metadata: { - content?: string; - title?: string; - doc_id?: string; - doc_name?: string; - doc_url?: string; - pipeline_id?: string; - workspace_id?: string; - page_number?: number; - image_url?: string; - _knowledge_type?: string; - _citation_index?: number; - _score?: number; - }; - }>; - }; -} - -// ---- Real API call tests (gated by BAILIAN_E2E + credentials) ---- - -describe.skipIf(!isSearchE2EReady())("e2e: kscli search (live)", () => { - const agentId = process.env.BAILIAN_E2E_SEARCH_AGENT_ID!; - const workspaceId = process.env.BAILIAN_WORKSPACE_ID!; - - test("search returns results in JSON mode", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "search", - "--query", - "什么是大模型", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.code).toBe("Success"); - expect(data.request_id).toBeTruthy(); - expect(data.data.total).toBeGreaterThan(0); - expect(data.data.nodes.length).toBeGreaterThan(0); - - const firstNode = data.data.nodes[0]!; - expect(typeof firstNode.score).toBe("number"); - expect(firstNode.score).toBeGreaterThanOrEqual(0); - expect(typeof firstNode.text).toBe("string"); - expect(firstNode.text.length).toBeGreaterThan(0); - }); - - test("search returns results in text mode", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "search", - "--query", - "RAG", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--output", - "text", - ]); - - expect(exitCode, stderr).toBe(0); - // Text mode: [1] (score: 0.xxxx) followed by text content - expect(stdout).toMatch(/\[1\].*score/); - }); - - test("search with --query-history returns results", async () => { - const { stdout, stderr, exitCode } = await runKscli([ - "search", - "--query", - "它怎么工作", - "--agent-id", - agentId, - "--workspace-id", - workspaceId, - "--query-history", - '[{"role":"user","content":"什么是大模型"},{"role":"assistant","content":"大模型是大规模语言模型"}]', - "--output", - "json", - ]); - - expect(exitCode, stderr).toBe(0); - const data = parseStdoutJson(stdout); - expect(data.code).toBe("Success"); - expect(data.data.nodes.length).toBeGreaterThan(0); - }); - - test("search with invalid agent_id fails gracefully", async () => { - const { stderr, exitCode } = await runKscli([ - "search", - "--query", - "test", - "--agent-id", - "aid-invalid-not-exist", - "--workspace-id", - workspaceId, - "--output", - "json", - ]); - - expect(exitCode).not.toBe(0); - expect(stderr).toBeTruthy(); - }); -}); diff --git a/packages/kscli/vite.config.ts b/packages/kscli/vite.config.ts index 3bfbf90..f8bef24 100644 --- a/packages/kscli/vite.config.ts +++ b/packages/kscli/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ test: { - globalSetup: "./tests/e2e/global-setup.ts", + globalSetup: "../e2e/src/global-setup.ts", testTimeout: 60_000, hookTimeout: 60_000, }, diff --git a/packages/runtime/package.json b/packages/runtime/package.json index 50f989f..393827b 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -50,6 +50,8 @@ "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260328.1", "ajv": "catalog:", + "bailian-cli-commands": "workspace:*", + "e2e": "workspace:*", "typescript": "^6.0.2", "vite-plus": "0.1.22", "yaml": "catalog:" diff --git a/packages/runtime/tests/console-flags.e2e.test.ts b/packages/runtime/tests/console-flags.e2e.test.ts new file mode 100644 index 0000000..a03a3c3 --- /dev/null +++ b/packages/runtime/tests/console-flags.e2e.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "vite-plus/test"; +import { runCommandE2e } from "../../commands/tests/e2e/helpers.ts"; + +/** + * 跨域 flag 拒绝:runtime 凭证域与命令 flag 解析边界。 + */ + +describe("e2e: console global flags (cross-domain rejection)", () => { + test("跨域 flag 拒绝:model 命令传 --console-region 报 Unknown flag", async () => { + const { stderr, exitCode } = await runCommandE2e([ + "text", + "chat", + "--message", + "hi", + "--console-region", + "cn-hangzhou", + "--dry-run", + ]); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/Unknown flag.*--console-region/); + }); + + test("跨域 flag 拒绝:console 命令传 --api-key 报 Unknown flag", async () => { + const { stderr, exitCode } = await runCommandE2e([ + "mcp", + "list", + "--api-key", + "sk-test", + "--dry-run", + ]); + expect(exitCode).not.toBe(0); + expect(stderr).toMatch(/Unknown flag.*--api-key/); + }); +}); diff --git a/packages/cli/tests/e2e/proxy.e2e.test.ts b/packages/runtime/tests/proxy.e2e.test.ts similarity index 70% rename from packages/cli/tests/e2e/proxy.e2e.test.ts rename to packages/runtime/tests/proxy.e2e.test.ts index 80b7f0b..ce902dc 100644 --- a/packages/cli/tests/e2e/proxy.e2e.test.ts +++ b/packages/runtime/tests/proxy.e2e.test.ts @@ -3,37 +3,30 @@ import { createServer, type Server } from "http"; import { mkdtempSync, rmSync, writeFileSync } from "fs"; import type { AddressInfo } from "net"; import { tmpdir } from "os"; -import { join } from "path"; +import { dirname, join } from "path"; +import { fileURLToPath } from "url"; import { promisify } from "util"; import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test"; -import { cliPackageRoot } from "./helpers.ts"; const execFileAsync = promisify(execFile); +const runtimePackageRoot = join(dirname(fileURLToPath(import.meta.url)), ".."); + /** - * 代理支持 E2E(issue #35):只验证 `setupProxyFromEnv()` 是否把代理 dispatcher - * 正确装到全局 fetch 上——设了 HTTPS_PROXY 后裸 `fetch()` 走代理,未设置时直连, - * NO_PROXY 命中时跳过,非法代理值给出明确报错。 - * - * 不经过任何 CLI 命令(不解析凭证、不打 gateway),因此 CI 上无需 api key / - * access token,与既有 e2e 设计一致。全程离线:目标域名用 `.invalid`(保留顶级域, - * 必然无法解析),代理收到 CONNECT 后规范返回 502,不产生真实外网请求。 + * 代理支持 E2E:验证 `setupProxyFromEnv()` 是否把代理 dispatcher 正确装到全局 fetch 上。 + * 不经过 CLI 命令,CI 无需 api key。 */ const FAKE_HOST = "bl-proxy-e2e.invalid"; const FAKE_URL = `https://${FAKE_HOST}/probe`; -/** - * 最小探针脚本:调用真实的 `setupProxyFromEnv()`,再对目标发一个普通 fetch。 - * 代理行为由进程环境变量决定,正是被测对象;fetch 成败不重要,我们只看代理是否收到 CONNECT。 - */ const PROBE_SCRIPT = ` -import { setupProxyFromEnv } from ${JSON.stringify(join(cliPackageRoot, "..", "runtime", "src", "proxy.ts"))}; +import { setupProxyFromEnv } from ${JSON.stringify(join(runtimePackageRoot, "src", "proxy.ts"))}; setupProxyFromEnv(); try { await fetch(${JSON.stringify(FAKE_URL)}, { signal: AbortSignal.timeout(5000) }); } catch { - // 目标不可达/隧道被拒都正常——本测试只关心代理是否收到 CONNECT + // 目标不可达/隧道被拒都正常 } `; @@ -45,7 +38,6 @@ const connectTargets: string[] = []; beforeAll(async () => { proxy = createServer(); - // 记录收到的 CONNECT 目标(host:port),并以 502 拒绝隧道 proxy.on("connect", (req, clientSocket) => { connectTargets.push(req.url ?? ""); clientSocket.end("HTTP/1.1 502 Bad Gateway\r\n\r\n"); @@ -63,7 +55,6 @@ afterAll(async () => { rmSync(scriptDir, { recursive: true, force: true }); }); -/** 清空所有代理相关环境变量,确保每个用例只受自身设置影响 */ const PROXY_ENV_CLEARED = { HTTPS_PROXY: "", https_proxy: "", @@ -73,13 +64,12 @@ const PROXY_ENV_CLEARED = { no_proxy: "", }; -/** 以给定代理环境变量运行探针脚本,返回 { exitCode, stderr } */ async function runProbe( envOverrides: NodeJS.ProcessEnv, ): Promise<{ exitCode: number; stderr: string }> { try { await execFileAsync("node", [scriptPath], { - cwd: cliPackageRoot, + cwd: runtimePackageRoot, encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1", ...PROXY_ENV_CLEARED, ...envOverrides }, }); diff --git a/packages/runtime/vite.config.ts b/packages/runtime/vite.config.ts index 1c26ed4..7325db8 100644 --- a/packages/runtime/vite.config.ts +++ b/packages/runtime/vite.config.ts @@ -1,6 +1,11 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ + test: { + globalSetup: "../e2e/src/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, pack: { minify: true, dts: { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a316881..012b867 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -70,6 +70,9 @@ importers: chalk: specifier: 'catalog:' version: 5.6.2 + e2e: + specifier: workspace:* + version: link:../e2e typescript: specifier: ^6.0.2 version: 6.0.3 @@ -107,6 +110,9 @@ importers: '@typescript/native-preview': specifier: 7.0.0-dev.20260328.1 version: 7.0.0-dev.20260328.1 + e2e: + specifier: workspace:* + version: link:../e2e typescript: specifier: ^6.0.2 version: 6.0.3 @@ -133,6 +139,22 @@ importers: specifier: 'catalog:' version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + packages/e2e: + dependencies: + bailian-cli-core: + specifier: workspace:* + version: link:../core + devDependencies: + '@types/node': + specifier: 'catalog:' + version: 24.12.2 + typescript: + specifier: ^6.0.2 + version: 6.0.3 + vite-plus: + specifier: 0.1.22 + version: 0.1.22(@types/node@24.12.2)(jiti@2.6.1)(typescript@6.0.3)(vite@8.0.10(@types/node@24.12.2)(jiti@2.6.1)(yaml@2.8.3))(yaml@2.8.3) + packages/kscli: dependencies: bailian-cli-commands: @@ -163,6 +185,9 @@ importers: chalk: specifier: 'catalog:' version: 5.6.2 + e2e: + specifier: workspace:* + version: link:../e2e typescript: specifier: ^6.0.2 version: 6.0.3 @@ -203,6 +228,12 @@ importers: ajv: specifier: 'catalog:' version: 8.20.0 + bailian-cli-commands: + specifier: workspace:* + version: link:../commands + e2e: + specifier: workspace:* + version: link:../e2e typescript: specifier: ^6.0.2 version: 6.0.3 diff --git a/tools/compare-e2e-command-map.ts b/tools/compare-e2e-command-map.ts new file mode 100644 index 0000000..0a501ae --- /dev/null +++ b/tools/compare-e2e-command-map.ts @@ -0,0 +1,35 @@ +#!/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); diff --git a/vite.config.ts b/vite.config.ts index e3173cd..36c4ad5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -2,7 +2,7 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ test: { - globalSetup: "./packages/cli/tests/e2e/global-setup.ts", + globalSetup: "./packages/e2e/src/global-setup.ts", testTimeout: 60_000, hookTimeout: 60_000, }, From 301986b669d110ac6efff070c8aefb737f49be95 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 12:26:41 +0800 Subject: [PATCH 2/8] fix(ci): stop emitting and checking generated tests helpers.d.ts Runtime pack build was pulling in e2e test imports and tsgo emitted helpers.d.ts beside commands/tests; restrict runtime tsconfig to src and ignore tests/**/*.d.ts in fmt/gitignore as a safety net. --- .gitignore | 1 + packages/runtime/tsconfig.json | 3 ++- vite.config.ts | 3 +++ 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f1c88cb..46ba8f8 100644 --- a/.gitignore +++ b/.gitignore @@ -43,6 +43,7 @@ tools/generated # Test & scene outputs /test/ packages/cli/scene/**/outputs/ +**/tests/**/*.d.ts # Environment variables (sensitive data) .env diff --git a/packages/runtime/tsconfig.json b/packages/runtime/tsconfig.json index ff4adab..e2bb481 100644 --- a/packages/runtime/tsconfig.json +++ b/packages/runtime/tsconfig.json @@ -16,5 +16,6 @@ "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true - } + }, + "include": ["src/**/*"] } diff --git a/vite.config.ts b/vite.config.ts index 36c4ad5..413af82 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -30,4 +30,7 @@ export default defineConfig({ run: { cache: true, }, + fmt: { + ignorePatterns: ["**/tests/**/*.d.ts"], + }, }); From 6e5ecf8923e64ee3a847d7f75670db3c188e8aed Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 12:32:30 +0800 Subject: [PATCH 3/8] chore: drop redundant helpers.d.ts fmt/gitignore guards Runtime tsconfig already limits pack to src, so tests/**/*.d.ts is never emitted during build; the fmt ignore and gitignore entries are unnecessary. --- .gitignore | 1 - vite.config.ts | 3 --- 2 files changed, 4 deletions(-) diff --git a/.gitignore b/.gitignore index 46ba8f8..f1c88cb 100644 --- a/.gitignore +++ b/.gitignore @@ -43,7 +43,6 @@ tools/generated # Test & scene outputs /test/ packages/cli/scene/**/outputs/ -**/tests/**/*.d.ts # Environment variables (sensitive data) .env diff --git a/vite.config.ts b/vite.config.ts index 413af82..36c4ad5 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -30,7 +30,4 @@ export default defineConfig({ run: { cache: true, }, - fmt: { - ignorePatterns: ["**/tests/**/*.d.ts"], - }, }); From 7b77bf6a6dde76ac4307b3e017d4ae27e3ae2f5c Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 13:01:10 +0800 Subject: [PATCH 4/8] fix(e2e): run subprocess harness and proxy probe via tsx Node's strip-only TypeScript mode cannot execute workspace source that uses parameter properties. Use the monorepo tsx binary for runNodeMain and the proxy e2e probe (.mts for top-level await). --- packages/e2e/src/run-subprocess.ts | 13 +++++++++++-- packages/runtime/tests/proxy.e2e.test.ts | 5 +++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/e2e/src/run-subprocess.ts b/packages/e2e/src/run-subprocess.ts index 91b6626..6d41cd2 100644 --- a/packages/e2e/src/run-subprocess.ts +++ b/packages/e2e/src/run-subprocess.ts @@ -1,5 +1,7 @@ import { execFile } from "child_process"; +import { join } from "path"; import { promisify } from "util"; +import { monorepoRoot } from "./monorepo-root.ts"; const execFileAsync = promisify(execFile); @@ -9,14 +11,21 @@ export interface RunCliResult { exitCode: number; } -/** 子进程执行指定 main.ts 入口 */ +/** monorepo 根目录下的 tsx 可执行文件 */ +export function resolveTsxBin(): string { + const root = monorepoRoot(); + const name = process.platform === "win32" ? "tsx.cmd" : "tsx"; + return join(root, "node_modules", ".bin", name); +} + +/** 子进程通过 tsx 执行指定 main.ts 入口 */ export async function runNodeMain( mainTs: string, args: string[], options: { cwd: string; env?: NodeJS.ProcessEnv } = { cwd: process.cwd() }, ): Promise { try { - const { stdout, stderr } = await execFileAsync("node", [mainTs, ...args], { + const { stdout, stderr } = await execFileAsync(resolveTsxBin(), [mainTs, ...args], { cwd: options.cwd, encoding: "utf8", maxBuffer: 32 * 1024 * 1024, diff --git a/packages/runtime/tests/proxy.e2e.test.ts b/packages/runtime/tests/proxy.e2e.test.ts index ce902dc..dbd2ae3 100644 --- a/packages/runtime/tests/proxy.e2e.test.ts +++ b/packages/runtime/tests/proxy.e2e.test.ts @@ -7,6 +7,7 @@ import { dirname, join } from "path"; import { fileURLToPath } from "url"; import { promisify } from "util"; import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test"; +import { resolveTsxBin } from "e2e/runner"; const execFileAsync = promisify(execFile); @@ -46,7 +47,7 @@ beforeAll(async () => { proxyUrl = `http://127.0.0.1:${(proxy.address() as AddressInfo).port}`; scriptDir = mkdtempSync(join(tmpdir(), "bl-proxy-e2e-")); - scriptPath = join(scriptDir, "probe.ts"); + scriptPath = join(scriptDir, "probe.mts"); writeFileSync(scriptPath, PROBE_SCRIPT); }); @@ -68,7 +69,7 @@ async function runProbe( envOverrides: NodeJS.ProcessEnv, ): Promise<{ exitCode: number; stderr: string }> { try { - await execFileAsync("node", [scriptPath], { + await execFileAsync(resolveTsxBin(), [scriptPath], { cwd: runtimePackageRoot, encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1", ...PROXY_ENV_CLEARED, ...envOverrides }, From 91a7106bf42cd38dbb5590aa1250150f842c1c12 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 14:04:21 +0800 Subject: [PATCH 5/8] fix(e2e): update CLI version to match package.json --- packages/commands/tests/e2e/harness/main.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/commands/tests/e2e/harness/main.ts b/packages/commands/tests/e2e/harness/main.ts index 01def24..0b8941e 100644 --- a/packages/commands/tests/e2e/harness/main.ts +++ b/packages/commands/tests/e2e/harness/main.ts @@ -1,9 +1,10 @@ import { createCli } from "bailian-cli-runtime"; +import pkg from "../../../../cli/package.json" with { type: "json" }; import { e2eCommandMap } from "./e2e-command-map.ts"; void createCli(e2eCommandMap, { binName: "bl", - version: "0.0.0-e2e", + version: pkg.version, clientName: "commands-e2e", npmPackage: "bailian-cli-commands", }).run(); From d646a4e7805bd0f7f351ad7cca7c4088437f91f2 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 15:55:53 +0800 Subject: [PATCH 6/8] refactor(e2e): update test routes and streamline command handling - Replaced hardcoded command paths with dynamic route mappings in E2E tests. - Enhanced documentation for command testing, specifying minimum route requirements. - Consolidated command path management into `topic-routes.ts` for better maintainability. - Updated various E2E test cases to utilize the new routing structure, ensuring consistency across tests. - Removed redundant helper functions and improved test clarity by directly referencing route exports. --- docs/agents/cli-e2e-tests.md | 55 ++--- docs/agents/command-add-remove.md | 12 +- .../cli/tests/e2e/registry.smoke.e2e.test.ts | 17 +- .../tests/e2e/advisor-recommend.e2e.test.ts | 31 +-- packages/commands/tests/e2e/auth.e2e.test.ts | 44 ++-- .../commands/tests/e2e/config.e2e.test.ts | 32 +-- .../e2e/console-flags-dry-run.e2e.test.ts | 23 +- .../commands/tests/e2e/dataset.e2e.test.ts | 36 ++- .../commands/tests/e2e/deploy.e2e.test.ts | 22 +- .../tests/e2e/file-upload.e2e.test.ts | 25 +- .../commands/tests/e2e/finetune.e2e.test.ts | 34 ++- .../harness/e2e-command-map.contract.test.ts | 71 ------ .../tests/e2e/harness/e2e-command-map.ts | 233 ------------------ .../commands/tests/e2e/harness/identity.ts | 13 + packages/commands/tests/e2e/harness/main.ts | 35 ++- packages/commands/tests/e2e/helpers.ts | 18 +- .../commands/tests/e2e/image-edit.e2e.test.ts | 30 ++- .../tests/e2e/image-generate.e2e.test.ts | 14 +- .../tests/e2e/knowledge-chat.e2e.test.ts | 37 ++- .../tests/e2e/knowledge-search.e2e.test.ts | 33 ++- .../commands/tests/e2e/knowledge.e2e.test.ts | 28 ++- packages/commands/tests/e2e/mcp.e2e.test.ts | 41 ++- .../commands/tests/e2e/memory.e2e.test.ts | 37 ++- packages/commands/tests/e2e/omni.e2e.test.ts | 22 +- .../commands/tests/e2e/pipeline.e2e.test.ts | 42 ++-- packages/commands/tests/e2e/quota.e2e.test.ts | 52 ++-- .../commands/tests/e2e/search-web.e2e.test.ts | 25 +- .../tests/e2e/speech-list-voices.e2e.test.ts | 28 ++- .../tests/e2e/speech-recognize.e2e.test.ts | 23 +- .../tests/e2e/speech-synthesize.e2e.test.ts | 19 +- .../commands/tests/e2e/text-chat.e2e.test.ts | 20 +- packages/commands/tests/e2e/topic-routes.ts | 129 ++++++++++ .../commands/tests/e2e/usage-free.e2e.test.ts | 42 ++-- .../tests/e2e/usage-stats.e2e.test.ts | 33 +-- .../tests/e2e/video-download.e2e.test.ts | 19 +- .../commands/tests/e2e/video-edit.e2e.test.ts | 17 +- .../tests/e2e/video-generate-i2v.e2e.test.ts | 17 +- .../tests/e2e/video-generate-t2v.e2e.test.ts | 15 +- .../tests/e2e/video-ref-r2v.e2e.test.ts | 19 +- .../tests/e2e/video-task-get.e2e.test.ts | 20 +- packages/e2e/package.json | 3 +- packages/e2e/src/registry-smoke.ts | 18 ++ packages/kscli/src/commands.ts | 23 ++ packages/kscli/src/main.ts | 24 +- .../tests/e2e/registry.smoke.e2e.test.ts | 69 +++--- .../runtime/tests/console-flags.e2e.test.ts | 18 +- .../tests/harness/cross-domain-main.ts | 33 +++ tools/compare-e2e-command-map.ts | 35 --- 48 files changed, 797 insertions(+), 889 deletions(-) delete mode 100644 packages/commands/tests/e2e/harness/e2e-command-map.contract.test.ts delete mode 100644 packages/commands/tests/e2e/harness/e2e-command-map.ts create mode 100644 packages/commands/tests/e2e/harness/identity.ts create mode 100644 packages/commands/tests/e2e/topic-routes.ts create mode 100644 packages/e2e/src/registry-smoke.ts create mode 100644 packages/kscli/src/commands.ts create mode 100644 packages/runtime/tests/harness/cross-domain-main.ts delete mode 100644 tools/compare-e2e-command-map.ts 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 commandPaths = Object.keys(commands).sort(); +const groupPaths = deriveGroupPaths(commandPaths); -const harnessMainTs = join( - dirname(fileURLToPath(import.meta.url)), - "../../../commands/tests/e2e/harness/main.ts", -); -const commandsPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "../../../commands"); +describe("e2e: kscli registry smoke", () => { + 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); + }); -/** 提取 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(); -} + test("--version 输出产品名与版本", async () => { + const { stdout, exitCode } = await runKscli(["--version"]); + expect(exitCode).toBe(0); + expect(stdout.trim()).toBe(`kscli ${pkg.version}`); + }); -describe("e2e: kscli registry smoke", () => { - test.each(kscliPaths)("已注册命令 %s --help 成功", async (path) => { - const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]); + 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 --help flag 与 harness knowledge search --help 一致", async () => { - const [kscliHelp, harnessHelp] = await Promise.all([ - runKscli(["search", "--help"]), - runNodeMain(harnessMainTs, ["knowledge", "search", "--help"], { - cwd: commandsPackageRoot, - }), - ]); + 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); + }); - expect(kscliHelp.exitCode, kscliHelp.stderr).toBe(0); - expect(harnessHelp.exitCode, harnessHelp.stderr).toBe(0); + test.each(commandPaths)("已注册命令 %s --help 成功", async (path) => { + const { stderr, exitCode } = await runKscli([...path.split(" "), "--help"]); + expect(exitCode, 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); From a0b466694030881f40d657cdb77202de6444956a Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 17:11:20 +0800 Subject: [PATCH 7/8] refactor(e2e): remove outdated token-plan tests and add new route exports - Deleted the `token-plan.e2e.test.ts` file as it contained outdated test cases. - Introduced `TOKEN_PLAN_ROUTES` in `topic-routes.ts` to define new command routes for token-plan operations. - This update streamlines the E2E testing structure and prepares for future enhancements. --- .../tests/e2e/token-plan.e2e.test.ts | 40 +++++++++++++------ packages/commands/tests/e2e/topic-routes.ts | 7 ++++ 2 files changed, 34 insertions(+), 13 deletions(-) rename packages/{cli => commands}/tests/e2e/token-plan.e2e.test.ts (62%) diff --git a/packages/cli/tests/e2e/token-plan.e2e.test.ts b/packages/commands/tests/e2e/token-plan.e2e.test.ts similarity index 62% rename from packages/cli/tests/e2e/token-plan.e2e.test.ts rename to packages/commands/tests/e2e/token-plan.e2e.test.ts index b37fb14..7bc6cda 100644 --- a/packages/cli/tests/e2e/token-plan.e2e.test.ts +++ b/packages/commands/tests/e2e/token-plan.e2e.test.ts @@ -1,16 +1,22 @@ import { describe, expect, test } from "vite-plus/test"; -import { makeE2eOutputDir, parseStdoutJson, runCli } from "./helpers.ts"; +import { makeE2eOutputDir, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { TOKEN_PLAN_ROUTES } from "./topic-routes.ts"; describe("e2e: token-plan", () => { test("token-plan help shows centralized OpenAPI auth flags", async () => { - const { stderr, exitCode } = await runCli(["token-plan", "list-seats", "--help"]); + const { stderr, exitCode } = await runCommandE2e(TOKEN_PLAN_ROUTES, [ + "token-plan", + "list-seats", + "--help", + ]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--access-key-id/); expect(stderr).toMatch(/--access-key-secret/); }); test("token-plan dry-run does not require OpenAPI AK/SK", async () => { - const { stdout, stderr, exitCode } = await runCli( + const { stdout, stderr, exitCode } = await runCommandE2e( + TOKEN_PLAN_ROUTES, ["token-plan", "list-seats", "--dry-run", "--output", "json"], { ALIBABA_CLOUD_ACCESS_KEY_ID: "", @@ -25,22 +31,30 @@ describe("e2e: token-plan", () => { test("token-plan non-dry-run requires OpenAPI AK/SK", async () => { const configDir = makeE2eOutputDir("token-plan-missing-openapi"); - const { stderr, exitCode } = await runCli(["token-plan", "list-seats"], { - BAILIAN_CONFIG_DIR: configDir, - ALIBABA_CLOUD_ACCESS_KEY_ID: "", - ALIBABA_CLOUD_ACCESS_KEY_SECRET: "", - }); + const { stderr, exitCode } = await runCommandE2e( + TOKEN_PLAN_ROUTES, + ["token-plan", "list-seats"], + { + BAILIAN_CONFIG_DIR: configDir, + ALIBABA_CLOUD_ACCESS_KEY_ID: "", + ALIBABA_CLOUD_ACCESS_KEY_SECRET: "", + }, + ); expect(exitCode).not.toBe(0); expect(stderr).toMatch(/OpenAPI AK\/SK|access-key-id|ALIBABA_CLOUD_ACCESS_KEY_ID/); }); test("token-plan partial OpenAPI env reports AK/SK hint without API key onboarding", async () => { const configDir = makeE2eOutputDir("token-plan-partial-openapi-env"); - const { stderr, exitCode } = await runCli(["token-plan", "list-seats"], { - BAILIAN_CONFIG_DIR: configDir, - ALIBABA_CLOUD_ACCESS_KEY_ID: "ak-e2e-placeholder", - ALIBABA_CLOUD_ACCESS_KEY_SECRET: "", - }); + const { stderr, exitCode } = await runCommandE2e( + TOKEN_PLAN_ROUTES, + ["token-plan", "list-seats"], + { + BAILIAN_CONFIG_DIR: configDir, + ALIBABA_CLOUD_ACCESS_KEY_ID: "ak-e2e-placeholder", + ALIBABA_CLOUD_ACCESS_KEY_SECRET: "", + }, + ); expect(exitCode).not.toBe(0); expect(stderr).toMatch(/Incomplete OpenAPI AK\/SK/); expect(stderr).toMatch(/ALIBABA_CLOUD_ACCESS_KEY_ID/); diff --git a/packages/commands/tests/e2e/topic-routes.ts b/packages/commands/tests/e2e/topic-routes.ts index 596012e..faf1850 100644 --- a/packages/commands/tests/e2e/topic-routes.ts +++ b/packages/commands/tests/e2e/topic-routes.ts @@ -136,3 +136,10 @@ export const CONSOLE_FLAGS_DRY_RUN_ROUTES: E2eRouteExports = { "mcp list": "mcpList", "quota check": "quotaCheck", }; + +export const TOKEN_PLAN_ROUTES: E2eRouteExports = { + "token-plan list-seats": "tokenPlanListSeats", + "token-plan create-key": "tokenPlanCreateKey", + "token-plan assign-seats": "tokenPlanAssignSeats", + "token-plan add-member": "tokenPlanAddMember", +}; From 94adb919b183bd019c58497a4a16e06e08952a3e Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Fri, 10 Jul 2026 17:46:17 +0800 Subject: [PATCH 8/8] refactor(e2e): enhance auth login tests and remove unused dependencies - Removed the `bailian-cli-commands` dependency from `pnpm-lock.yaml` and `package.json` as it was no longer needed. - Expanded the E2E tests in `auth.e2e.test.ts` to cover additional scenarios for the `auth login` command, ensuring proper error handling for conflicting flags and missing parameters. - Improved test assertions for better clarity and coverage of authentication flows. --- packages/commands/tests/e2e/auth.e2e.test.ts | 148 ++++++++++++++++++- packages/runtime/package.json | 1 - pnpm-lock.yaml | 3 - 3 files changed, 145 insertions(+), 7 deletions(-) diff --git a/packages/commands/tests/e2e/auth.e2e.test.ts b/packages/commands/tests/e2e/auth.e2e.test.ts index a4f5425..6e8a2a4 100644 --- a/packages/commands/tests/e2e/auth.e2e.test.ts +++ b/packages/commands/tests/e2e/auth.e2e.test.ts @@ -1,5 +1,12 @@ +import { readFileSync } from "fs"; +import { join } from "path"; import { describe, expect, test } from "vite-plus/test"; -import { isDashScopeE2EReady, parseStdoutJson, runCommandE2e } from "./helpers.ts"; +import { + isDashScopeE2EReady, + makeE2eOutputDir, + parseStdoutJson, + runCommandE2e, +} from "./helpers.ts"; import { AUTH_ROUTES } from "./topic-routes.ts"; /** @@ -12,6 +19,63 @@ describe("e2e: auth", () => { expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/login|api-key/i); expect(stderr).toMatch(/--console-site/); + expect(stderr).toMatch(/--open-api/); + }); + + test("auth login 一次只能选择一种登录模式", async () => { + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--console", + "--api-key", + "sk-e2e-placeholder", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/Choose exactly one login mode/); + }); + + test("auth login 模式专属参数不能脱离对应模式", async () => { + const openApiFlagOnly = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--access-key-id", + "LTAI-e2e", + ]); + expect(openApiFlagOnly.exitCode).toBe(2); + expect(openApiFlagOnly.stderr).toMatch(/Use --open-api with --access-key-id/); + + const baseUrlWithoutApiKey = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--console", + "--base-url", + "https://dashscope.aliyuncs.com", + ]); + expect(baseUrlWithoutApiKey.exitCode).toBe(2); + expect(baseUrlWithoutApiKey.stderr).toMatch(/Use --base-url only with --api-key/); + + const consoleSiteWithoutConsole = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--api-key", + "sk-e2e-placeholder", + "--console-site", + "international", + ]); + expect(consoleSiteWithoutConsole.exitCode).toBe(2); + expect(consoleSiteWithoutConsole.stderr).toMatch(/Use --console-site only with --console/); + }); + + test("auth login --open-api 要求 AK/SK 成对输入", async () => { + const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, [ + "auth", + "login", + "--open-api", + "--access-key-id", + "LTAI-e2e", + ]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/Provide --access-key-id and --access-key-secret with --open-api/); }); test("auth logout --help 正常退出", async () => { @@ -29,7 +93,7 @@ describe("e2e: auth", () => { test("auth login 缺少 --api-key 时报用法错误并退出 (2)", async () => { const { stderr, exitCode } = await runCommandE2e(AUTH_ROUTES, ["auth", "login", "--quiet"]); expect(exitCode, stderr).toBe(2); - expect(stderr).toMatch(/--api-key|Usage:/i); + expect(stderr).toMatch(/Choose exactly one login mode/); }); test("auth login --dry-run --api-key 不发起校验与落盘", async () => { @@ -70,7 +134,7 @@ describe("e2e: auth", () => { expect(exitCode).toBe(2); const err = JSON.parse(stderr.trim()) as { error?: { code?: number; message?: string } }; expect(err.error?.code).toBe(2); - expect(err.error?.message).toMatch(/--api-key|console/i); + expect(err.error?.message).toMatch(/Choose exactly one login mode/); }); test("auth logout --dry-run 不写入配置", async () => { @@ -162,4 +226,82 @@ describe("e2e: auth", () => { expect(exitCode).not.toBe(0); expect(stderr).toMatch(/Unknown flag.*--base-url/); }); + + test("auth status 展示 env OpenAPI AK/SK 且不接受 OpenAPI flag 覆盖", async () => { + const { stdout, stderr, exitCode } = await runCommandE2e( + AUTH_ROUTES, + ["auth", "status", "--output", "json"], + { + ALIBABA_CLOUD_ACCESS_KEY_ID: "LTAI-e2e-placeholder", + ALIBABA_CLOUD_ACCESS_KEY_SECRET: "secret-e2e-placeholder", + }, + ); + expect(exitCode, stderr).toBe(0); + const data = parseStdoutJson<{ + authenticated?: boolean; + openapi?: { source?: string; access_key_id?: string; access_key_secret?: string }; + }>(stdout); + expect(data.authenticated).toBe(true); + expect(data.openapi?.source).toBe("env"); + expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-placeholder"); + expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-placeholder"); + + const denied = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--access-key-id", "ak"]); + expect(denied.exitCode).not.toBe(0); + expect(denied.stderr).toMatch(/Unknown flag.*--access-key-id/); + }); + + test("auth login --open-api 持久化 OpenAPI AK/SK 并支持单独 logout", async () => { + const configDir = makeE2eOutputDir("auth-openapi-login"); + const env = { + BAILIAN_CONFIG_DIR: configDir, + ALIBABA_CLOUD_ACCESS_KEY_ID: "", + ALIBABA_CLOUD_ACCESS_KEY_SECRET: "", + }; + + const login = await runCommandE2e( + AUTH_ROUTES, + [ + "auth", + "login", + "--open-api", + "--access-key-id", + "LTAI-e2e-login-placeholder", + "--access-key-secret", + "secret-e2e-login-placeholder", + ], + env, + ); + expect(login.exitCode, login.stderr).toBe(0); + expect(login.stderr).toMatch(/OpenAPI credentials saved/); + + const config = JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")) as Record< + string, + unknown + >; + expect(config.access_key_id).toBe("LTAI-e2e-login-placeholder"); + expect(config.access_key_secret).toBe("secret-e2e-login-placeholder"); + expect(config.openapi_access_key_id).toBeUndefined(); + expect(config.openapi_access_key_secret).toBeUndefined(); + + const status = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--output", "json"], env); + expect(status.exitCode, status.stderr).toBe(0); + const data = parseStdoutJson<{ + authenticated?: boolean; + openapi?: { source?: string; access_key_id?: string; access_key_secret?: string }; + }>(status.stdout); + expect(data.authenticated).toBe(true); + expect(data.openapi?.source).toBe("config"); + expect(data.openapi?.access_key_id).not.toBe("LTAI-e2e-login-placeholder"); + expect(data.openapi?.access_key_secret).not.toBe("secret-e2e-login-placeholder"); + + const logout = await runCommandE2e(AUTH_ROUTES, ["auth", "logout", "--open-api"], env); + expect(logout.exitCode, logout.stderr).toBe(0); + expect(logout.stderr).toMatch(/Cleared access_key_id/); + + const after = await runCommandE2e(AUTH_ROUTES, ["auth", "status", "--output", "json"], env); + expect(after.exitCode, after.stderr).toBe(0); + const afterData = parseStdoutJson<{ authenticated?: boolean; openapi?: unknown }>(after.stdout); + expect(afterData.openapi).toBeUndefined(); + }); }); diff --git a/packages/runtime/package.json b/packages/runtime/package.json index c16dec9..20a5401 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -50,7 +50,6 @@ "@types/node": "catalog:", "@typescript/native-preview": "7.0.0-dev.20260328.1", "ajv": "catalog:", - "bailian-cli-commands": "workspace:*", "e2e": "workspace:*", "typescript": "^6.0.2", "vite-plus": "0.1.22", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e512f48..a434066 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -246,9 +246,6 @@ importers: ajv: specifier: 'catalog:' version: 8.20.0 - bailian-cli-commands: - specifier: workspace:* - version: link:../commands e2e: specifier: workspace:* version: link:../e2e