From 10998f31726a7080dd4e050b7c3dab90989f2030 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 16:13:28 +0800 Subject: [PATCH 1/7] test(e2e): move contract e2e to commands, add bl/rag smoke and shared harness --- AGENTS.md | 36 ++-- docs/agents/cli-e2e-tests.md | 54 ++++-- package.json | 1 + packages/cli/tests/args.test.ts | 95 --------- packages/cli/tests/e2e/helpers.ts | 183 ------------------ packages/cli/tests/e2e/setup.ts | 14 ++ packages/cli/tests/e2e/smoke.e2e.test.ts | 48 +++++ packages/cli/tests/index.test.ts | 109 ----------- packages/cli/tests/proxy.test.ts | 42 ---- packages/cli/vite.config.ts | 2 +- packages/commands/package.json | 5 + .../{cli => commands}/tests/e2e/.smoke-32.png | Bin .../tests/e2e/advisor-recommend.e2e.test.ts | 2 +- .../tests/e2e/auth.e2e.test.ts | 2 +- .../tests/e2e/config.e2e.test.ts | 2 +- .../tests/e2e/console-flags.e2e.test.ts | 2 +- .../commands/tests/e2e/core/cli-runner.ts | 61 ++++++ .../tests/e2e/core}/global-setup.ts | 6 +- packages/commands/tests/e2e/core/index.ts | 19 ++ .../commands/tests/e2e/core/output-dir.ts | 83 ++++++++ packages/commands/tests/e2e/core/skip.ts | 48 +++++ .../tests/e2e/file-upload.e2e.test.ts | 2 +- .../tests/e2e/image-edit.e2e.test.ts | 2 +- .../tests/e2e/image-generate.e2e.test.ts | 2 +- .../tests/e2e/knowledge.e2e.test.ts | 2 +- .../tests/e2e/mcp.e2e.test.ts | 2 +- .../tests/e2e/memory.e2e.test.ts | 2 +- .../tests/e2e/omni.e2e.test.ts | 2 +- .../tests/e2e/pipeline.e2e.test.ts | 2 +- .../tests/e2e/proxy.e2e.test.ts | 8 +- .../tests/e2e/quota.e2e.test.ts | 2 +- .../tests/e2e/search-web.e2e.test.ts | 2 +- packages/commands/tests/e2e/setup.ts | 31 +++ .../tests/e2e/speech-list-voices.e2e.test.ts | 2 +- .../tests/e2e/speech-recognize.e2e.test.ts | 2 +- .../tests/e2e/speech-synthesize.e2e.test.ts | 2 +- .../tests/e2e/text-chat.e2e.test.ts | 2 +- .../tests/e2e/usage-free.e2e.test.ts | 2 +- .../tests/e2e/usage-stats.e2e.test.ts | 2 +- .../tests/e2e/video-download.e2e.test.ts | 2 +- .../tests/e2e/video-edit.e2e.test.ts | 2 +- .../tests/e2e/video-generate-i2v.e2e.test.ts | 2 +- .../tests/e2e/video-generate-t2v.e2e.test.ts | 2 +- .../tests/e2e/video-ref-r2v.e2e.test.ts | 2 +- .../tests/e2e/video-task-get.e2e.test.ts | 2 +- packages/commands/tests/fixtures/test-cli.ts | 10 + packages/commands/vite.config.ts | 5 + packages/rag/tests/e2e/setup.ts | 14 ++ packages/rag/tests/e2e/smoke.e2e.test.ts | 34 ++++ packages/rag/vite.config.ts | 5 + vite.config.ts | 2 +- 51 files changed, 474 insertions(+), 493 deletions(-) delete mode 100644 packages/cli/tests/args.test.ts delete mode 100644 packages/cli/tests/e2e/helpers.ts create mode 100644 packages/cli/tests/e2e/setup.ts create mode 100644 packages/cli/tests/e2e/smoke.e2e.test.ts delete mode 100644 packages/cli/tests/index.test.ts delete mode 100644 packages/cli/tests/proxy.test.ts rename packages/{cli => commands}/tests/e2e/.smoke-32.png (100%) rename packages/{cli => commands}/tests/e2e/advisor-recommend.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/auth.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/config.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/console-flags.e2e.test.ts (98%) create mode 100644 packages/commands/tests/e2e/core/cli-runner.ts rename packages/{cli/tests/e2e => commands/tests/e2e/core}/global-setup.ts (90%) create mode 100644 packages/commands/tests/e2e/core/index.ts create mode 100644 packages/commands/tests/e2e/core/output-dir.ts create mode 100644 packages/commands/tests/e2e/core/skip.ts rename packages/{cli => commands}/tests/e2e/file-upload.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/image-edit.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/image-generate.e2e.test.ts (98%) rename packages/{cli => commands}/tests/e2e/knowledge.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/mcp.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/memory.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/omni.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/pipeline.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/proxy.e2e.test.ts (95%) rename packages/{cli => commands}/tests/e2e/quota.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/search-web.e2e.test.ts (99%) create mode 100644 packages/commands/tests/e2e/setup.ts rename packages/{cli => commands}/tests/e2e/speech-list-voices.e2e.test.ts (96%) rename packages/{cli => commands}/tests/e2e/speech-recognize.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/speech-synthesize.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/text-chat.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/usage-free.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/usage-stats.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-download.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-edit.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-generate-i2v.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-generate-t2v.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-ref-r2v.e2e.test.ts (99%) rename packages/{cli => commands}/tests/e2e/video-task-get.e2e.test.ts (98%) create mode 100644 packages/commands/tests/fixtures/test-cli.ts create mode 100644 packages/rag/tests/e2e/setup.ts create mode 100644 packages/rag/tests/e2e/smoke.e2e.test.ts diff --git a/AGENTS.md b/AGENTS.md index 98c6dd8..a24d548 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,28 +4,30 @@ ## 项目地图 -monorepo 双包结构: +monorepo 多包结构: -- `packages/cli` — `bailian-cli` 包,CLI 命令、UI、入口 -- `packages/core` — `bailian-cli-core` 包,鉴权 / HTTP / 类型,纯逻辑层 +- `packages/core` — `bailian-cli-core`,鉴权 / HTTP / 类型,纯逻辑层 +- `packages/runtime` — `bailian-cli-runtime`,`createCli` / registry / args / output +- `packages/commands` — `bailian-cli-commands`,命令实现 + **契约 e2e** + e2e 公共基建(`tests/e2e/core`) +- `packages/cli` — `bailian-cli`,`bl` 产品入口 + smoke e2e +- `packages/rag` — `bailian-cli-rag`,`rag` 产品入口 + smoke e2e + +### E2E 测试分布 + +- 契约 e2e(help / dry-run / 真实集成): `packages/commands/tests/e2e/` +- e2e 基建(`createCliRunner` 等): `packages/commands/tests/e2e/core/`(export `bailian-cli-commands/e2e`) +- 产品 smoke: `packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts` +- 一次跑全量 e2e: 根目录 `pnpm test:e2e` ### `packages/cli` 目录要点 ``` packages/cli/ -├── src/ -│ ├── main.ts # 入口、鉴权分支、调用 registry -│ ├── registry.ts # 命令树解析、动态 help(读 catalog) -│ ├── commands/ -│ │ ├── catalog.ts # 命令总表(登记处,构建脚本也读它) -│ │ ├── index.ts # re-export commands -│ │ └── /...ts # 各命令 defineCommand 实现 -│ ├── output/ # CLI 输出、prompt、progress -│ └── urls.ts # 控制台/文档 URL(仅 cli) -└── tests/e2e/ +├── src/main.ts # bl 入口(createCli + commands) +└── tests/e2e/smoke.e2e.test.ts ``` -Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts` 从 `catalog.ts` 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);与 `tools/sync-skill-metadata.ts` 一起在 **pre-commit**(`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行。 +命令实现见 `packages/commands/src/commands/`;登记在 **`groups.ts`**。`bl --help` 与 `tools/generate-reference.ts` 生成的命令手册同源,见 [command-add-remove.md](docs/agents/command-add-remove.md)。 非代码资产: @@ -37,9 +39,11 @@ Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/ 约定: - core 是纯库,不依赖 cli(详见下方通用约定) -- 文件路径与命令路径一一对应:`commands/text/chat.ts` ↔ `bl text chat` +- 文件路径与命令路径一一对应:`packages/commands/src/commands/text/chat.ts` ↔ `bl text chat` - 单级命令:`commands/.ts`(如 `update.ts`);两级:`commands//.ts` -- 命令登记在 **`catalog.ts`**;`bl --help` 与 `tools/generate-reference.ts` 生成的命令手册同源,见 [command-add-remove.md](docs/agents/command-add-remove.md) +- 命令登记在 **`packages/commands/src/commands/groups.ts`** + +Skill / 命令手册随 `skills/bailian-cli/` 经 `npx skills add modelstudioai/cli` 安装。`tools/generate-reference.ts` 从 catalog 生成命令手册到 `skills/bailian-cli/reference/`(纳入 git);与 `tools/sync-skill-metadata.ts` 一起在 **pre-commit**(`.vite-hooks/pre-commit`)及根脚本 `pnpm run sync:skill-assets` 中执行。 ## 业务场景索引 diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index 69246aa..dcd6c33 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -1,19 +1,48 @@ # CLI E2E 测试规范 +## 架构(composable-cli) + +E2E 按包分层,与命令实现 / 产品入口解耦: + +| 层级 | 路径 | 测什么 | +| ---------------- | -------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- | +| **公共基建** | `packages/commands/tests/e2e/core/` | `createCliRunner`、skip 判定、输出目录、global-setup | +| **命令契约 e2e** | `packages/commands/tests/e2e/*.e2e.test.ts` | help、缺参、dry-run、**真实集成**;跑 canonical 入口 `tests/fixtures/test-cli.ts` | +| **产品 smoke** | `packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts` | 版本、root help、命令面裁剪;跑各产品 `src/main.ts` | + +> `commands/tests/e2e/core` 是 e2e 测试基建,与 `packages/core`(`bailian-cli-core`)无关。 + ## 触发条件 -- 新增/修改 `packages/cli/src` 下的 command(`commands/catalog.ts` 登记、`defineCommand` 实现、options/usage) -- 新建或扩展 `packages/cli/tests/e2e/*.e2e.test.ts` 用例 +- 新增/修改 `packages/commands/src/commands/` 下的 command(`groups.ts` 登记、`defineCommand` 实现、options/usage) +- 新建或扩展 `packages/commands/tests/e2e/.e2e.test.ts` 用例 +- 新增/变更产品命令面(bl / rag)时同步维护对应 smoke 用例 - 为命令补 help / 缺参 / dry-run / 真实集成测试 -以上情况必须同步维护 `packages/cli/tests/e2e/.e2e.test.ts`。跑测与环境变量见 `.cursor/skills/bailian-cli-e2e/SKILL.md`。 - ## 文件与工具 -- 路径:`packages/cli/tests/e2e/.e2e.test.ts` -- 框架:`vite-plus/test`;子进程跑 CLI:`runCli` from `./helpers.ts` -- 解析 JSON stdout:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))` -- 长任务:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时 +- **契约 e2e 路径**:`packages/commands/tests/e2e/.e2e.test.ts` +- **框架**:`vite-plus/test`;契约测试 `runCli` from `./setup.ts`;cli/rag smoke from `./setup.ts`(内部 `import from "bailian-cli-commands/e2e"`) +- **解析 JSON stdout**:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))` +- **长任务**:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时 + +## 跑测命令 + +```sh +# 一次跑全部 e2e(契约 + bl/rag smoke) +pnpm test:e2e + +# 仅命令契约(含真实集成,需 BAILIAN_E2E=1 等) +pnpm --filter bailian-cli-commands test + +# 单文件(在 bailian-cli-commands 包目录下运行,路径相对于 packages/commands) +pnpm --filter bailian-cli-commands test tests/e2e/text-chat.e2e.test.ts + +# 全 monorepo(unit + e2e) +vp run -r test +``` + +环境变量与 skip 条件见 `.cursor/skills/bailian-cli-e2e/SKILL.md`(若存在)或下文 skip 表。 ## 双层 describe(固定结构) @@ -32,7 +61,7 @@ describe.skipIf()("e2e: (DashScope …)", () => { }); ``` -## skip 条件(helpers.ts) +## skip 条件(`tests/e2e/core/skip.ts`) | 场景 | 条件 | | ------------------- | ----------------------------------------------------- | @@ -59,12 +88,13 @@ describe.skipIf()("e2e: (DashScope …)", () => { ## 新增 command 检查清单 -- [ ] `commands/catalog.ts` 登记 + `tests/e2e/.e2e.test.ts`(新建或扩展) +- [ ] `packages/commands/src/commands/groups.ts` 登记 + `packages/commands/tests/e2e/.e2e.test.ts`(新建或扩展) - [ ] 若改了 `usage` / `options` / `examples`,跑 `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/` 通过 +- [ ] 若 bl/rag 命令面变更,更新对应 smoke 用例 +- [ ] `pnpm --filter bailian-cli-commands test tests/e2e/` 通过 ## 示例片段 @@ -97,4 +127,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/package.json b/package.json index 5ad800c..f82f066 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ "bl": "pnpm -F bailian-cli dev", "rag": "pnpm -F bailian-cli-rag dev", "test": "vp test", + "test:e2e": "pnpm --filter bailian-cli-commands --filter bailian-cli --filter bailian-cli-rag test", "release:check": "node tools/release/check.mjs", "wiki:crawl": "node tools/wiki-crawler/index.mjs", "test:stress": "node packages/cli/tests/stress/run.mjs" diff --git a/packages/cli/tests/args.test.ts b/packages/cli/tests/args.test.ts deleted file mode 100644 index b32d208..0000000 --- a/packages/cli/tests/args.test.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { expect, test } from "vite-plus/test"; -import { ExitCode, GLOBAL_OPTIONS } from "bailian-cli-core"; -import { parseFlags } from "../src/args.ts"; -import { BOOL_FLAG_WATERMARK } from "../src/utils/flag-descriptions.ts"; - -const IMAGE_GENERATE_OPTIONS = [ - { flag: "--prompt ", description: "Image description", required: true }, - { flag: "--model ", description: "Model ID" }, - { flag: "--watermark ", description: BOOL_FLAG_WATERMARK }, - { flag: "--no-wait", description: "Return task ID immediately without waiting" }, -]; - -test("parseFlags rejects unknown long flags", () => { - expect(() => - parseFlags(["--prompt", "cat", "--xxxx", "a"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]), - ).toThrowError( - expect.objectContaining({ - name: "BailianError", - exitCode: ExitCode.USAGE, - message: expect.stringContaining('Unknown flag "--xxxx"'), - }), - ); -}); - -test("parseFlags rejects unknown flags with = syntax", () => { - expect(() => - parseFlags( - ["--prompt=cat", "--unknown-flag=yes"], - [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS], - ), - ).toThrow(/Unknown flag "--unknown-flag"/); -}); - -test("parseFlags accepts defined command and global flags", () => { - const flags = parseFlags( - ["--quiet", "--prompt", "cat", "--watermark", "false"], - [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS], - ); - expect(flags.quiet).toBe(true); - expect(flags.prompt).toBe("cat"); - expect(flags.watermark).toBe("false"); -}); - -test("parseFlags rejects value flag when next token is another flag", () => { - const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; - for (const argv of [ - ["--watermark", "--prompt", "cat"], - ["--watermark", "-h"], - ["--prompt", "cat", "--watermark", "--model", "qwen-image-2.0"], - ]) { - expect(() => parseFlags(argv, opts)).toThrowError( - expect.objectContaining({ - name: "BailianError", - exitCode: ExitCode.USAGE, - message: expect.stringContaining("Flag --watermark requires a value"), - }), - ); - } -}); - -test("parseFlags rejects trailing value flag without value", () => { - expect(() => - parseFlags(["--prompt", "cat", "--watermark"], [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]), - ).toThrowError( - expect.objectContaining({ - message: expect.stringContaining("Flag --watermark requires a value"), - }), - ); -}); - -test("parseFlags allows boolean flags without values adjacent to other flags", () => { - const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; - const flags = parseFlags( - ["--quiet", "--dry-run", "--no-wait", "--prompt", "cat", "--watermark", "false"], - opts, - ); - expect(flags.quiet).toBe(true); - expect(flags.dryRun).toBe(true); - expect(flags.noWait).toBe(true); - expect(flags.prompt).toBe("cat"); - expect(flags.watermark).toBe("false"); -}); - -test("parseFlags does not treat the next flag as a boolean flag value", () => { - const opts = [...GLOBAL_OPTIONS, ...IMAGE_GENERATE_OPTIONS]; - expect(() => parseFlags(["--dry-run", "--prompt"], opts)).toThrowError( - expect.objectContaining({ - message: expect.stringContaining("Flag --prompt requires a value"), - }), - ); - // --dry-run is boolean: no value check; parsing continues to --prompt. - const flags = parseFlags(["--dry-run", "--prompt", "cat"], opts); - expect(flags.dryRun).toBe(true); - expect(flags.prompt).toBe("cat"); -}); diff --git a/packages/cli/tests/e2e/helpers.ts b/packages/cli/tests/e2e/helpers.ts deleted file mode 100644 index e35b8a3..0000000 --- a/packages/cli/tests/e2e/helpers.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { execFile } from "child_process"; -import { mkdirSync, readFileSync } from "fs"; -import { promisify } from "util"; -import { basename, 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`) */ -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; - } -} - -/** 语音与图像(可设 `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 或 AK/SK */ -export function isKnowledgeE2EReady(): boolean { - if (!isBailianE2EEnabled()) return false; - if (!process.env.BAILIAN_E2E_INDEX_ID) return false; - const hasApiKey = isDashScopeE2EReady(); - const hasAkSk = - !!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET; - return hasApiKey || hasAkSk; -} - -export function isKnowledgeAkSkReady(): boolean { - return ( - isBailianE2EEnabled() && - !!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && - !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET && - !!process.env.BAILIAN_E2E_INDEX_ID - ); -} - -export interface RunCliResult { - stdout: string; - stderr: string; - exitCode: number; -} - -/** - * 子进程执行 CLI(等价于在 `packages/cli` 下 `node src/main.ts ...`)。 - * request_id 等诊断信息在 stderr;`--output json` 时 JSON 在 stdout。 - */ -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(); - return JSON.parse(t) as T; -} diff --git a/packages/cli/tests/e2e/setup.ts b/packages/cli/tests/e2e/setup.ts new file mode 100644 index 0000000..082abdc --- /dev/null +++ b/packages/cli/tests/e2e/setup.ts @@ -0,0 +1,14 @@ +import { join } from "path"; +import { fileURLToPath } from "url"; +import { createCliRunner } from "bailian-cli-commands/e2e"; + +const cliRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); +const mainTs = join(cliRoot, "src", "main.ts"); + +const runner = createCliRunner({ + entry: mainTs, + cwd: cliRoot, + binName: "bl", +}); + +export const runCli = runner.runCli; diff --git a/packages/cli/tests/e2e/smoke.e2e.test.ts b/packages/cli/tests/e2e/smoke.e2e.test.ts new file mode 100644 index 0000000..62357bb --- /dev/null +++ b/packages/cli/tests/e2e/smoke.e2e.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, test } from "vite-plus/test"; +import { runCli } from "./setup.ts"; + +describe("e2e: bl smoke", () => { + test("bl --version 输出 bl 与版本号", async () => { + const { stdout, stderr, exitCode } = await runCli(["--version"]); + expect(exitCode, stderr).toBe(0); + expect(stdout.trim()).toMatch(/^bl \d+\.\d+\.\d+/); + }); + + test("bl 根 help 含各能力组代表命令", async () => { + const { stderr, exitCode } = await runCli(["--help"]); + expect(exitCode, stderr).toBe(0); + const out = stderr; + // baseCommands + expect(out).toMatch(/auth login/i); + expect(out).toMatch(/config show/i); + // knowledgeCommands + expect(out).toMatch(/knowledge retrieve/i); + // textCommands + expect(out).toMatch(/text chat/i); + expect(out).toMatch(/\bomni\b/i); + // mediaCommands + expect(out).toMatch(/image generate/i); + expect(out).toMatch(/video generate/i); + expect(out).toMatch(/speech synthesize/i); + // memoryCommands + expect(out).toMatch(/memory add/i); + // miscCommands + expect(out).toMatch(/mcp list/i); + expect(out).toMatch(/app list/i); + expect(out).toMatch(/pipeline run/i); + expect(out).toMatch(/search web/i); + }); + + test("bl knowledge retrieve --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--index-id/i); + expect(stderr).toMatch(/--query/i); + }); + + test("bl text chat --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["text", "chat", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--message|chat/i); + }); +}); diff --git a/packages/cli/tests/index.test.ts b/packages/cli/tests/index.test.ts deleted file mode 100644 index f6853c0..0000000 --- a/packages/cli/tests/index.test.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { expect, test } from "vite-plus/test"; -import { createStepDispatcher } from "../src/pipeline/dispatcher.ts"; -import { executePipeline } from "../src/pipeline/executor.ts"; -import { collectPipelineIssues } from "../src/pipeline/validation.ts"; -import { getByJsonPointer } from "../src/pipeline/schema.ts"; -import { normalizeConcurrency } from "../src/pipeline/scheduler.ts"; -import { WORKFLOW_VERSION, type PipelineDefinition } from "../src/pipeline/types.ts"; - -test("cli package skeleton", () => { - expect(true).toBe(true); -}); - -test("pipeline execution can use an isolated step dispatcher", async () => { - const dispatcher = createStepDispatcher(); - dispatcher.registerStep("test/echo", (input, ctx) => ({ - data: { input, hasSignal: !!ctx.signal }, - })); - - const controller = new AbortController(); - const pipeline: PipelineDefinition = { - version: WORKFLOW_VERSION, - steps: [{ id: "echo", type: "test/echo", input: { message: "hello" } }], - }; - - const report = await executePipeline( - pipeline, - {}, - { - stepDispatcher: dispatcher, - signal: controller.signal, - }, - ); - - expect(report.status).toBe("succeeded"); - expect(report.steps[0]?.output?.data).toEqual({ - input: { message: "hello" }, - hasSignal: true, - }); -}); - -test("dry-run never executes $js expressions (preview must not run code)", async () => { - const dispatcher = createStepDispatcher(); - dispatcher.registerStep("test/echo", (input) => ({ data: input })); - const flag = "__bailian_dryrun_should_not_run__"; - delete (globalThis as Record)[flag]; - - const pipeline: PipelineDefinition = { - version: WORKFLOW_VERSION, - steps: [ - { - id: "s1", - type: "test/echo", - input: { probe: { $js: `(globalThis[${JSON.stringify(flag)}] = true), 1` } }, - }, - ], - }; - - const report = await executePipeline(pipeline, {}, { stepDispatcher: dispatcher, dryRun: true }); - expect(report.status).toBe("planned"); - expect((globalThis as Record)[flag]).toBeUndefined(); -}); - -test("script/js rejects non-literal code sourced from another step ($from)", () => { - const dispatcher = createStepDispatcher(); - dispatcher.registerStep("test/echo", (input) => ({ data: input })); - dispatcher.registerStep("script/js", () => ({ data: {} })); - - const pipeline: PipelineDefinition = { - version: WORKFLOW_VERSION, - steps: [ - { id: "gen", type: "test/echo", input: { message: "x" } }, - { - id: "run", - type: "script/js", - input: { code: { $from: "gen", path: "/data/message" } as never }, - }, - ], - }; - - const issues = collectPipelineIssues(pipeline, dispatcher); - expect(issues.some((issue) => issue.includes('literal string "code"'))).toBe(true); -}); - -test("script/js accepts a literal string code", () => { - const dispatcher = createStepDispatcher(); - dispatcher.registerStep("script/js", () => ({ data: {} })); - - const pipeline: PipelineDefinition = { - version: WORKFLOW_VERSION, - steps: [{ id: "run", type: "script/js", input: { code: "return 1" } }], - }; - - expect(collectPipelineIssues(pipeline, dispatcher)).toEqual([]); -}); - -test("getByJsonPointer refuses prototype keys and inherited properties", () => { - const obj = { a: { b: 1 } }; - expect(getByJsonPointer(obj, "/a/b")).toBe(1); - expect(getByJsonPointer(obj, "/__proto__")).toBeUndefined(); - expect(getByJsonPointer(obj, "/constructor")).toBeUndefined(); - expect(getByJsonPointer(obj, "/a/constructor/constructor")).toBeUndefined(); - expect(getByJsonPointer(obj, "/toString")).toBeUndefined(); -}); - -test("normalizeConcurrency clamps to a safe maximum", () => { - expect(normalizeConcurrency(undefined)).toBe(1); - expect(normalizeConcurrency(4)).toBe(4); - expect(normalizeConcurrency(100000)).toBe(64); -}); diff --git a/packages/cli/tests/proxy.test.ts b/packages/cli/tests/proxy.test.ts deleted file mode 100644 index 1986459..0000000 --- a/packages/cli/tests/proxy.test.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { expect, test } from "vite-plus/test"; -import { readProxyEnv } from "../src/proxy.ts"; - -test("readProxyEnv: 未设置任何代理变量时全部为 undefined", () => { - expect(readProxyEnv({})).toEqual({ - httpProxy: undefined, - httpsProxy: undefined, - noProxy: undefined, - }); -}); - -test("readProxyEnv: 空白值视为未设置", () => { - expect(readProxyEnv({ HTTPS_PROXY: "", HTTP_PROXY: " ", NO_PROXY: "" })).toEqual({ - httpProxy: undefined, - httpsProxy: undefined, - noProxy: undefined, - }); -}); - -test("readProxyEnv: 大小写变量均可识别,小写优先", () => { - expect(readProxyEnv({ HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe("http://upper:1"); - expect(readProxyEnv({ https_proxy: "http://lower:1" }).httpsProxy).toBe("http://lower:1"); - expect( - readProxyEnv({ https_proxy: "http://lower:1", HTTPS_PROXY: "http://upper:1" }).httpsProxy, - ).toBe("http://lower:1"); -}); - -test("readProxyEnv: 空字符串小写变量不屏蔽已设置的大写变量", () => { - expect(readProxyEnv({ https_proxy: "", HTTPS_PROXY: "http://upper:1" }).httpsProxy).toBe( - "http://upper:1", - ); - expect(readProxyEnv({ http_proxy: "", HTTP_PROXY: "http://upper:2" }).httpProxy).toBe( - "http://upper:2", - ); -}); - -test("readProxyEnv: NO_PROXY 独立读取", () => { - const r = readProxyEnv({ NO_PROXY: "*.aliyuncs.com" }); - expect(r.noProxy).toBe("*.aliyuncs.com"); - expect(r.httpProxy).toBeUndefined(); - expect(r.httpsProxy).toBeUndefined(); -}); diff --git a/packages/cli/vite.config.ts b/packages/cli/vite.config.ts index 769e51e..eb9ab43 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: "../commands/tests/e2e/core/global-setup.ts", testTimeout: 60_000, hookTimeout: 60_000, }, diff --git a/packages/commands/package.json b/packages/commands/package.json index ff11647..8553cd6 100644 --- a/packages/commands/package.json +++ b/packages/commands/package.json @@ -20,10 +20,15 @@ "types": "./dist/index.d.mts", "exports": { ".": "./dist/index.mjs", + "./e2e": "./tests/e2e/core/index.ts", "./package.json": "./package.json" }, "publishConfig": { "access": "public", + "exports": { + ".": "./dist/index.mjs", + "./package.json": "./package.json" + }, "registry": "https://registry.npmjs.org/" }, "scripts": { diff --git a/packages/cli/tests/e2e/.smoke-32.png b/packages/commands/tests/e2e/.smoke-32.png similarity index 100% rename from packages/cli/tests/e2e/.smoke-32.png rename to packages/commands/tests/e2e/.smoke-32.png diff --git a/packages/cli/tests/e2e/advisor-recommend.e2e.test.ts b/packages/commands/tests/e2e/advisor-recommend.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/advisor-recommend.e2e.test.ts rename to packages/commands/tests/e2e/advisor-recommend.e2e.test.ts index 7f6724e..fd04ec2 100644 --- a/packages/cli/tests/e2e/advisor-recommend.e2e.test.ts +++ b/packages/commands/tests/e2e/advisor-recommend.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, runCli } from "./setup.ts"; describe("e2e: advisor recommend", () => { test("advisor shows subcommand groups and exits successfully", async () => { diff --git a/packages/cli/tests/e2e/auth.e2e.test.ts b/packages/commands/tests/e2e/auth.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/auth.e2e.test.ts rename to packages/commands/tests/e2e/auth.e2e.test.ts index 7c46ab3..7ce1d07 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, runCli } from "./setup.ts"; /** * Auth 相关 E2E:只验证 CLI 进程能正常解析参数并退出。 diff --git a/packages/cli/tests/e2e/config.e2e.test.ts b/packages/commands/tests/e2e/config.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/config.e2e.test.ts rename to packages/commands/tests/e2e/config.e2e.test.ts index 5527e32..5516015 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, runCli } from "./setup.ts"; /** * Config 相关 E2E diff --git a/packages/cli/tests/e2e/console-flags.e2e.test.ts b/packages/commands/tests/e2e/console-flags.e2e.test.ts similarity index 98% rename from packages/cli/tests/e2e/console-flags.e2e.test.ts rename to packages/commands/tests/e2e/console-flags.e2e.test.ts index 04d6f2a..ae22113 100644 --- a/packages/cli/tests/e2e/console-flags.e2e.test.ts +++ b/packages/commands/tests/e2e/console-flags.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { parseStdoutJson, runCli } from "./setup.ts"; type ConsoleDryRunMeta = { consoleRegion?: string; diff --git a/packages/commands/tests/e2e/core/cli-runner.ts b/packages/commands/tests/e2e/core/cli-runner.ts new file mode 100644 index 0000000..2c35bea --- /dev/null +++ b/packages/commands/tests/e2e/core/cli-runner.ts @@ -0,0 +1,61 @@ +import { execFile } from "child_process"; +import { promisify } from "util"; + +const execFileAsync = promisify(execFile); + +/** 子进程 CLI 目标:各产品注入自己的入口与 cwd。 */ +export interface CliTarget { + entry: string; + cwd: string; + binName: string; +} + +export interface RunCliResult { + stdout: string; + stderr: string; + exitCode: number; +} + +/** + * 工厂:绑定 CLI 入口后返回 `runCli`。 + * request_id 等诊断信息在 stderr;`--output json` 时 JSON 在 stdout。 + */ +export function createCliRunner(target: CliTarget): { + runCli: (args: string[], envOverrides?: NodeJS.ProcessEnv) => Promise; + binName: string; +} { + const { entry, cwd, binName } = target; + + async function runCli( + args: string[], + envOverrides: NodeJS.ProcessEnv = {}, + ): Promise { + try { + const { stdout, stderr } = await execFileAsync("node", [entry, ...args], { + cwd, + 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, + }; + } + } + + return { runCli, binName }; +} diff --git a/packages/cli/tests/e2e/global-setup.ts b/packages/commands/tests/e2e/core/global-setup.ts similarity index 90% rename from packages/cli/tests/e2e/global-setup.ts rename to packages/commands/tests/e2e/core/global-setup.ts index 0992a3c..5595d52 100644 --- a/packages/cli/tests/e2e/global-setup.ts +++ b/packages/commands/tests/e2e/core/global-setup.ts @@ -1,15 +1,13 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, unlinkSync } from "fs"; import { join } from "path"; import { parseEnv } from "util"; -import { E2E_RUN_SESSION_FILENAME, monorepoRoot } from "./helpers.ts"; +import { E2E_RUN_SESSION_FILENAME, monorepoRoot } from "./output-dir.ts"; /** * Vitest 在所有 worker 启动前执行一次:写入共享会话 id,使多进程并行时仍共用一个 `test/output/<会话>/`。 * 结束后删除标记文件,避免非 Vitest 流程误用上一次会话 id。 */ - export default function vitestGlobalSetup(): () => void { - // 加载根目录 `.env` 合并变量(包含shell 注入) const rootEnv = join(monorepoRoot(), ".env"); if (existsSync(rootEnv)) { const parsed = parseEnv(readFileSync(rootEnv, "utf8")); @@ -18,7 +16,6 @@ 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 测试 @@ -44,7 +41,6 @@ BAILIAN_E2E_INDEX_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("-"); diff --git a/packages/commands/tests/e2e/core/index.ts b/packages/commands/tests/e2e/core/index.ts new file mode 100644 index 0000000..281dfac --- /dev/null +++ b/packages/commands/tests/e2e/core/index.ts @@ -0,0 +1,19 @@ +export { createCliRunner, type CliTarget, type RunCliResult } from "./cli-runner.ts"; +export { + cliTimeoutPrefix, + cliTimeoutSeconds, + commandsPackageRoot, + e2eLabelFromMetaUrl, + E2E_RUN_SESSION_FILENAME, + makeE2eOutputDir, + monorepoRoot, + parseStdoutJson, +} from "./output-dir.ts"; +export { + isBailianE2EEnabled, + isBailianE2EMediaEnabled, + isBailianE2EVideoEnabled, + isDashScopeE2EReady, + isKnowledgeAkSkReady, + isKnowledgeE2EReady, +} from "./skip.ts"; diff --git a/packages/commands/tests/e2e/core/output-dir.ts b/packages/commands/tests/e2e/core/output-dir.ts new file mode 100644 index 0000000..1cf2a02 --- /dev/null +++ b/packages/commands/tests/e2e/core/output-dir.ts @@ -0,0 +1,83 @@ +import { mkdirSync, readFileSync } from "fs"; +import { basename, dirname, join } from "path"; +import { fileURLToPath } from "url"; + +/** + * Vitest `global-setup.ts` 写入 `test/output/` 下本文件名,供各 worker 进程读取同一会话 id。 + */ +export const E2E_RUN_SESSION_FILENAME = ".e2e-run-session"; + +/** 单次 `vp test` / Vitest 运行共用的 E2E 输出会话目录名(惰性缓存于当前进程)。 */ +let e2eOutputSessionId: string | undefined; + +/** `packages/commands` 根目录 */ +export const commandsPackageRoot = join(dirname(fileURLToPath(import.meta.url)), "..", "..", ".."); + +/** Monorepo 根(含根 `package.json`) */ +export function monorepoRoot(): string { + return join(commandsPackageRoot, "..", ".."); +} + +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/<会话>/` 下创建用例子目录。 + * 若已设 `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()]; +} + +/** 从 `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(); + return JSON.parse(t) as T; +} diff --git a/packages/commands/tests/e2e/core/skip.ts b/packages/commands/tests/e2e/core/skip.ts new file mode 100644 index 0000000..19a4b9b --- /dev/null +++ b/packages/commands/tests/e2e/core/skip.ts @@ -0,0 +1,48 @@ +import { readConfigFile } from "bailian-cli-core"; + +/** 显式开启后才跑真实网络 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; + } +} + +/** 语音与图像(可设 `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"; +} + +/** 知识库用例:须显式索引 ID + API-KEY 或 AK/SK */ +export function isKnowledgeE2EReady(): boolean { + if (!isBailianE2EEnabled()) return false; + if (!process.env.BAILIAN_E2E_INDEX_ID) return false; + const hasApiKey = isDashScopeE2EReady(); + const hasAkSk = + !!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET; + return hasApiKey || hasAkSk; +} + +export function isKnowledgeAkSkReady(): boolean { + return ( + isBailianE2EEnabled() && + !!process.env.ALIBABA_CLOUD_ACCESS_KEY_ID && + !!process.env.ALIBABA_CLOUD_ACCESS_KEY_SECRET && + !!process.env.BAILIAN_E2E_INDEX_ID + ); +} diff --git a/packages/cli/tests/e2e/file-upload.e2e.test.ts b/packages/commands/tests/e2e/file-upload.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/file-upload.e2e.test.ts rename to packages/commands/tests/e2e/file-upload.e2e.test.ts index da6c611..e90d8f3 100644 --- a/packages/cli/tests/e2e/file-upload.e2e.test.ts +++ b/packages/commands/tests/e2e/file-upload.e2e.test.ts @@ -1,7 +1,7 @@ import { describe, expect, test } from "vite-plus/test"; import { dirname, join } from "path"; import { fileURLToPath } from "url"; -import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isDashScopeE2EReady, parseStdoutJson, runCli } from "./setup.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/cli/tests/e2e/image-edit.e2e.test.ts b/packages/commands/tests/e2e/image-edit.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/image-edit.e2e.test.ts rename to packages/commands/tests/e2e/image-edit.e2e.test.ts index 8c7acf1..7244f73 100644 --- a/packages/cli/tests/e2e/image-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/image-edit.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; const __dirname = dirname(fileURLToPath(import.meta.url)); diff --git a/packages/cli/tests/e2e/image-generate.e2e.test.ts b/packages/commands/tests/e2e/image-generate.e2e.test.ts similarity index 98% rename from packages/cli/tests/e2e/image-generate.e2e.test.ts rename to packages/commands/tests/e2e/image-generate.e2e.test.ts index fae8b15..07ab22a 100644 --- a/packages/cli/tests/e2e/image-generate.e2e.test.ts +++ b/packages/commands/tests/e2e/image-generate.e2e.test.ts @@ -6,7 +6,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Image generate:先做 help / 分组等常规检测(不依赖密钥、不调生成接口)。 diff --git a/packages/cli/tests/e2e/knowledge.e2e.test.ts b/packages/commands/tests/e2e/knowledge.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/knowledge.e2e.test.ts rename to packages/commands/tests/e2e/knowledge.e2e.test.ts index 3290017..806ecfd 100644 --- a/packages/cli/tests/e2e/knowledge.e2e.test.ts +++ b/packages/commands/tests/e2e/knowledge.e2e.test.ts @@ -1,6 +1,6 @@ import { tmpdir } from "os"; import { describe, expect, test } from "vite-plus/test"; -import { parseStdoutJson, runCli } from "./helpers.ts"; +import { parseStdoutJson, runCli } from "./setup.ts"; // ---- Types ---- diff --git a/packages/cli/tests/e2e/mcp.e2e.test.ts b/packages/commands/tests/e2e/mcp.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/mcp.e2e.test.ts rename to packages/commands/tests/e2e/mcp.e2e.test.ts index 4018acf..a1b2690 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, runCli } from "./setup.ts"; /** * `bl mcp` E2E. diff --git a/packages/cli/tests/e2e/memory.e2e.test.ts b/packages/commands/tests/e2e/memory.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/memory.e2e.test.ts rename to packages/commands/tests/e2e/memory.e2e.test.ts index ae01870..664ed1b 100644 --- a/packages/cli/tests/e2e/memory.e2e.test.ts +++ b/packages/commands/tests/e2e/memory.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./setup.ts"; interface MemoryAddBody { memory_ids?: string[]; diff --git a/packages/cli/tests/e2e/omni.e2e.test.ts b/packages/commands/tests/e2e/omni.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/omni.e2e.test.ts rename to packages/commands/tests/e2e/omni.e2e.test.ts index f0f2a36..6c7ec9c 100644 --- a/packages/cli/tests/e2e/omni.e2e.test.ts +++ b/packages/commands/tests/e2e/omni.e2e.test.ts @@ -7,7 +7,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; describe("e2e: omni", () => { test("omni --help 正常退出", async () => { diff --git a/packages/cli/tests/e2e/pipeline.e2e.test.ts b/packages/commands/tests/e2e/pipeline.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/pipeline.e2e.test.ts rename to packages/commands/tests/e2e/pipeline.e2e.test.ts index d2141d5..03f39d2 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, runCli } from "./setup.ts"; describe("e2e: pipeline", () => { let tempDir: string; diff --git a/packages/cli/tests/e2e/proxy.e2e.test.ts b/packages/commands/tests/e2e/proxy.e2e.test.ts similarity index 95% rename from packages/cli/tests/e2e/proxy.e2e.test.ts rename to packages/commands/tests/e2e/proxy.e2e.test.ts index c446c77..7aace28 100644 --- a/packages/cli/tests/e2e/proxy.e2e.test.ts +++ b/packages/commands/tests/e2e/proxy.e2e.test.ts @@ -6,10 +6,12 @@ import { tmpdir } from "os"; import { join } from "path"; import { promisify } from "util"; import { afterAll, beforeAll, describe, expect, test } from "vite-plus/test"; -import { cliPackageRoot } from "./helpers.ts"; +import { commandsPackageRoot } from "./setup.ts"; const execFileAsync = promisify(execFile); +const runtimeProxyTs = join(commandsPackageRoot, "..", "runtime", "src", "proxy.ts"); + /** * 代理支持 E2E(issue #35):只验证 `setupProxyFromEnv()` 是否把代理 dispatcher * 正确装到全局 fetch 上——设了 HTTPS_PROXY 后裸 `fetch()` 走代理,未设置时直连, @@ -28,7 +30,7 @@ const FAKE_URL = `https://${FAKE_HOST}/probe`; * 代理行为由进程环境变量决定,正是被测对象;fetch 成败不重要,我们只看代理是否收到 CONNECT。 */ const PROBE_SCRIPT = ` -import { setupProxyFromEnv } from ${JSON.stringify(join(cliPackageRoot, "src", "proxy.ts"))}; +import { setupProxyFromEnv } from ${JSON.stringify(runtimeProxyTs)}; setupProxyFromEnv(); try { await fetch(${JSON.stringify(FAKE_URL)}, { signal: AbortSignal.timeout(5000) }); @@ -79,7 +81,7 @@ async function runProbe( ): Promise<{ exitCode: number; stderr: string }> { try { await execFileAsync("node", [scriptPath], { - cwd: cliPackageRoot, + cwd: commandsPackageRoot, encoding: "utf8", env: { ...process.env, NODE_NO_WARNINGS: "1", ...PROXY_ENV_CLEARED, ...envOverrides }, }); diff --git a/packages/cli/tests/e2e/quota.e2e.test.ts b/packages/commands/tests/e2e/quota.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/quota.e2e.test.ts rename to packages/commands/tests/e2e/quota.e2e.test.ts index e2d3f6b..9c991e7 100644 --- a/packages/cli/tests/e2e/quota.e2e.test.ts +++ b/packages/commands/tests/e2e/quota.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts"; +import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./setup.ts"; import { readConfigFile } from "bailian-cli-core"; function isConsoleE2EReady(): boolean { diff --git a/packages/cli/tests/e2e/search-web.e2e.test.ts b/packages/commands/tests/e2e/search-web.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/search-web.e2e.test.ts rename to packages/commands/tests/e2e/search-web.e2e.test.ts index 9b0c353..be0533f 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, runCli } from "./setup.ts"; function pagesFromSearchWebStdout(stdout: string): Array<{ title?: string; url?: string }> { const envelope = parseStdoutJson<{ content?: Array<{ type?: string; text?: string }> }>(stdout); diff --git a/packages/commands/tests/e2e/setup.ts b/packages/commands/tests/e2e/setup.ts new file mode 100644 index 0000000..be44c7c --- /dev/null +++ b/packages/commands/tests/e2e/setup.ts @@ -0,0 +1,31 @@ +import { join } from "path"; +import { fileURLToPath } from "url"; +import { createCliRunner } from "./core/cli-runner.ts"; + +export { + cliTimeoutPrefix, + cliTimeoutSeconds, + commandsPackageRoot, + e2eLabelFromMetaUrl, + makeE2eOutputDir, + monorepoRoot, + parseStdoutJson, + isBailianE2EEnabled, + isBailianE2EMediaEnabled, + isBailianE2EVideoEnabled, + isDashScopeE2EReady, + isKnowledgeAkSkReady, + isKnowledgeE2EReady, + createCliRunner, +} from "./core/index.ts"; + +const commandsRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); +const testCliEntry = join(commandsRoot, "tests", "fixtures", "test-cli.ts"); + +const runner = createCliRunner({ + entry: testCliEntry, + cwd: commandsRoot, + binName: "bl", +}); + +export const runCli = runner.runCli; 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 96% 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 70fd0c6..0e939c7 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, runCli } from "./setup.ts"; /** * Speech list-voices E2E diff --git a/packages/cli/tests/e2e/speech-recognize.e2e.test.ts b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/speech-recognize.e2e.test.ts rename to packages/commands/tests/e2e/speech-recognize.e2e.test.ts index 35396ad..38ee64a 100644 --- a/packages/cli/tests/e2e/speech-recognize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-recognize.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Speech recognize:help / 分组不依赖密钥;识别流程需媒体 E2E + DashScope。 diff --git a/packages/cli/tests/e2e/speech-synthesize.e2e.test.ts b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/speech-synthesize.e2e.test.ts rename to packages/commands/tests/e2e/speech-synthesize.e2e.test.ts index 9627e9e..c24b6f0 100644 --- a/packages/cli/tests/e2e/speech-synthesize.e2e.test.ts +++ b/packages/commands/tests/e2e/speech-synthesize.e2e.test.ts @@ -7,7 +7,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Speech synthesize:help / 分组不依赖密钥;合成本地需媒体 E2E + DashScope。 diff --git a/packages/cli/tests/e2e/text-chat.e2e.test.ts b/packages/commands/tests/e2e/text-chat.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/text-chat.e2e.test.ts rename to packages/commands/tests/e2e/text-chat.e2e.test.ts index 768a1c5..cc25fba 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, runCli } from "./setup.ts"; /** * Text chat:help / 分组不依赖密钥;对话需 DashScope。 diff --git a/packages/cli/tests/e2e/usage-free.e2e.test.ts b/packages/commands/tests/e2e/usage-free.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/usage-free.e2e.test.ts rename to packages/commands/tests/e2e/usage-free.e2e.test.ts index 7998758..7f14843 100644 --- a/packages/cli/tests/e2e/usage-free.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-free.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts"; +import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./setup.ts"; import { readConfigFile } from "bailian-cli-core"; function isConsoleE2EReady(): boolean { diff --git a/packages/cli/tests/e2e/usage-stats.e2e.test.ts b/packages/commands/tests/e2e/usage-stats.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/usage-stats.e2e.test.ts rename to packages/commands/tests/e2e/usage-stats.e2e.test.ts index 2f1af38..c3a838c 100644 --- a/packages/cli/tests/e2e/usage-stats.e2e.test.ts +++ b/packages/commands/tests/e2e/usage-stats.e2e.test.ts @@ -1,5 +1,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./helpers.ts"; +import { isBailianE2EEnabled, parseStdoutJson, runCli } from "./setup.ts"; import { readConfigFile } from "bailian-cli-core"; function isConsoleE2EReady(): boolean { diff --git a/packages/cli/tests/e2e/video-download.e2e.test.ts b/packages/commands/tests/e2e/video-download.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/video-download.e2e.test.ts rename to packages/commands/tests/e2e/video-download.e2e.test.ts index 8e4dcf8..de4d31f 100644 --- a/packages/cli/tests/e2e/video-download.e2e.test.ts +++ b/packages/commands/tests/e2e/video-download.e2e.test.ts @@ -9,7 +9,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** dry-run 占位 UUID */ const PLACEHOLDER_TASK_ID = "00000000-0000-4000-8000-000000000001"; diff --git a/packages/cli/tests/e2e/video-edit.e2e.test.ts b/packages/commands/tests/e2e/video-edit.e2e.test.ts similarity index 99% rename from packages/cli/tests/e2e/video-edit.e2e.test.ts rename to packages/commands/tests/e2e/video-edit.e2e.test.ts index 44fb32a..736e71b 100644 --- a/packages/cli/tests/e2e/video-edit.e2e.test.ts +++ b/packages/commands/tests/e2e/video-edit.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Video edit E2E 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 99% 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 df61a63..84419ea 100644 --- a/packages/cli/tests/e2e/video-generate-i2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-i2v.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Video generate (i2v):help / 分组不依赖密钥;长任务需视频 E2E + DashScope。 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 99% 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 56af3e1..b99fd4c 100644 --- a/packages/cli/tests/e2e/video-generate-t2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-generate-t2v.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Video generate (t2v):help / 分组不依赖密钥;长任务需视频 E2E + DashScope。 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 99% 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 51d4d48..b9b181d 100644 --- a/packages/cli/tests/e2e/video-ref-r2v.e2e.test.ts +++ b/packages/commands/tests/e2e/video-ref-r2v.e2e.test.ts @@ -8,7 +8,7 @@ import { makeE2eOutputDir, parseStdoutJson, runCli, -} from "./helpers.ts"; +} from "./setup.ts"; /** * Video ref (r2v):help / 分组不依赖密钥;参考生成需视频 E2E + DashScope。 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 98% 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 a6029f7..b1dc19e 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,5 @@ import { describe, expect, test } from "vite-plus/test"; -import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./helpers.ts"; +import { isBailianE2EEnabled, isDashScopeE2EReady, parseStdoutJson, runCli } from "./setup.ts"; const taskId = process.env.BAILIAN_E2E_VIDEO_TASK_ID?.trim(); diff --git a/packages/commands/tests/fixtures/test-cli.ts b/packages/commands/tests/fixtures/test-cli.ts new file mode 100644 index 0000000..1cbe63c --- /dev/null +++ b/packages/commands/tests/fixtures/test-cli.ts @@ -0,0 +1,10 @@ +import { createCli } from "bailian-cli-runtime"; +import { commands } from "../../src/index.ts"; + +// 契约 e2e 专用 canonical 入口:全量 commands,与 bl 命令面一致。 +void createCli(commands, { + binName: "bl", + version: "0.0.0-test", + clientName: "bailian-cli", + npmPackage: "bailian-cli", +}).run(); diff --git a/packages/commands/vite.config.ts b/packages/commands/vite.config.ts index bffbbc1..cb35a99 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: "./tests/e2e/core/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, pack: { dts: { tsgo: true, diff --git a/packages/rag/tests/e2e/setup.ts b/packages/rag/tests/e2e/setup.ts new file mode 100644 index 0000000..4b421d0 --- /dev/null +++ b/packages/rag/tests/e2e/setup.ts @@ -0,0 +1,14 @@ +import { join } from "path"; +import { fileURLToPath } from "url"; +import { createCliRunner } from "bailian-cli-commands/e2e"; + +const ragRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); +const mainTs = join(ragRoot, "src", "main.ts"); + +const runner = createCliRunner({ + entry: mainTs, + cwd: ragRoot, + binName: "rag", +}); + +export const runCli = runner.runCli; diff --git a/packages/rag/tests/e2e/smoke.e2e.test.ts b/packages/rag/tests/e2e/smoke.e2e.test.ts new file mode 100644 index 0000000..667b883 --- /dev/null +++ b/packages/rag/tests/e2e/smoke.e2e.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, test } from "vite-plus/test"; +import { runCli } from "./setup.ts"; + +describe("e2e: rag smoke", () => { + test("rag --version 输出 rag 与版本号", async () => { + const { stdout, stderr, exitCode } = await runCli(["--version"]); + expect(exitCode, stderr).toBe(0); + expect(stdout.trim()).toMatch(/^rag \d+\.\d+\.\d+/); + }); + + test("rag 根 help 含 knowledge 与 auth,不含 text/image/memory", async () => { + const { stderr, exitCode } = await runCli(["--help"]); + expect(exitCode, stderr).toBe(0); + const out = stderr; + expect(out).toMatch(/knowledge retrieve/i); + expect(out).toMatch(/auth login/i); + expect(out).not.toMatch(/text chat/i); + expect(out).not.toMatch(/image generate/i); + expect(out).not.toMatch(/memory add/i); + }); + + test("rag knowledge retrieve --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--help"]); + expect(exitCode, stderr).toBe(0); + expect(stderr).toMatch(/--index-id/i); + expect(stderr).toMatch(/--query/i); + }); + + test("rag text chat 为未知命令", async () => { + const { stderr, exitCode } = await runCli(["text", "chat"]); + expect(exitCode).toBe(2); + expect(stderr).toMatch(/unknown command/i); + }); +}); diff --git a/packages/rag/vite.config.ts b/packages/rag/vite.config.ts index 0499bdf..b3a1a7d 100644 --- a/packages/rag/vite.config.ts +++ b/packages/rag/vite.config.ts @@ -1,6 +1,11 @@ import { defineConfig } from "vite-plus"; export default defineConfig({ + test: { + globalSetup: "../commands/tests/e2e/core/global-setup.ts", + testTimeout: 60_000, + hookTimeout: 60_000, + }, pack: { entry: { rag: "src/main.ts", diff --git a/vite.config.ts b/vite.config.ts index 82982f6..406d7b7 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/commands/tests/e2e/core/global-setup.ts", testTimeout: 60_000, hookTimeout: 60_000, }, From 58dbb67a63be675e1a6ee5735c506187e84feeb6 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 16:16:51 +0800 Subject: [PATCH 2/7] test: remove test:e2e script --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index f82f066..5ad800c 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,6 @@ "bl": "pnpm -F bailian-cli dev", "rag": "pnpm -F bailian-cli-rag dev", "test": "vp test", - "test:e2e": "pnpm --filter bailian-cli-commands --filter bailian-cli --filter bailian-cli-rag test", "release:check": "node tools/release/check.mjs", "wiki:crawl": "node tools/wiki-crawler/index.mjs", "test:stress": "node packages/cli/tests/stress/run.mjs" From a1fbc1f281daed98997564ca5b387642f0926d9c Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 16:46:55 +0800 Subject: [PATCH 3/7] ci: add bailian-cli-runtime and bailian-cli-commands to build process --- package.json | 2 +- tools/generate-reference.ts | 3 ++- tools/release/check.mjs | 6 ++++++ 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 5ad800c..276c0c2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "ready": "vp check && vp run -r test && vp run -r build", "prepare": "vp config", "check": "vp check", - "sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version", + "sync:skill-assets": "pnpm --filter bailian-cli-core run build && pnpm --filter bailian-cli-runtime run build && pnpm --filter bailian-cli-commands run build && pnpm --filter bailian-cli run generate:reference && pnpm --filter bailian-cli run sync:skill-version", "dev": "pnpm -F bailian-cli-core dev", "bl": "pnpm -F bailian-cli dev", "rag": "pnpm -F bailian-cli-rag dev", diff --git a/tools/generate-reference.ts b/tools/generate-reference.ts index 675a4b2..edd1f8f 100644 --- a/tools/generate-reference.ts +++ b/tools/generate-reference.ts @@ -6,7 +6,8 @@ * Committed to git; consumed by the `bailian-cli` Agent Skill (`npx skills add modelstudioai/cli`). * * Run: pnpm --filter bailian-cli run generate:reference - * (Also run via `pnpm run sync:skill-assets` or the repo pre-commit hook; requires built `bailian-cli-core`.) + * (Also run via `pnpm run sync:skill-assets` or the repo pre-commit hook; requires built + * `bailian-cli-core`, `bailian-cli-runtime`, and `bailian-cli-commands`.) */ import { mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; diff --git a/tools/release/check.mjs b/tools/release/check.mjs index aad327f..b4888fb 100644 --- a/tools/release/check.mjs +++ b/tools/release/check.mjs @@ -37,6 +37,12 @@ export async function runCheck(options = {}) { step("build bailian-cli-core"); run("pnpm", ["--filter", "bailian-cli-core", "run", "build"]); + step("build bailian-cli-runtime"); + run("pnpm", ["--filter", "bailian-cli-runtime", "run", "build"]); + + step("build bailian-cli-commands"); + run("pnpm", ["--filter", "bailian-cli-commands", "run", "build"]); + step( channel ? "generate skill reference (channel: skip SKILL.md version sync)" From ac8cee23a13c5517132de11da2f2fb8d2c9a27fc Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 16:57:50 +0800 Subject: [PATCH 4/7] fix: fixed e2e error about import --- AGENTS.md | 2 +- docs/agents/cli-e2e-tests.md | 2 +- packages/cli/src/main.ts | 2 +- packages/cli/tests/e2e/setup.ts | 4 ++-- packages/commands/tests/e2e/core/cli-runner.ts | 4 +++- packages/commands/tests/e2e/core/index.ts | 2 +- packages/commands/tests/e2e/setup.ts | 5 +++-- packages/commands/tests/fixtures/test-cli.ts | 2 +- packages/core/src/console/gateway.ts | 3 ++- packages/rag/src/main.ts | 2 +- packages/rag/tests/e2e/setup.ts | 4 ++-- 11 files changed, 18 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a24d548..c11655b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,7 @@ monorepo 多包结构: ### E2E 测试分布 - 契约 e2e(help / dry-run / 真实集成): `packages/commands/tests/e2e/` -- e2e 基建(`createCliRunner` 等): `packages/commands/tests/e2e/core/`(export `bailian-cli-commands/e2e`) +- e2e 基建(`createCliRunner` 等): `packages/commands/tests/e2e/core/`(cli/rag 以相对路径引用) - 产品 smoke: `packages/cli/tests/e2e/smoke.e2e.test.ts`、`packages/rag/tests/e2e/smoke.e2e.test.ts` - 一次跑全量 e2e: 根目录 `pnpm test:e2e` diff --git a/docs/agents/cli-e2e-tests.md b/docs/agents/cli-e2e-tests.md index dcd6c33..d242dfd 100644 --- a/docs/agents/cli-e2e-tests.md +++ b/docs/agents/cli-e2e-tests.md @@ -22,7 +22,7 @@ E2E 按包分层,与命令实现 / 产品入口解耦: ## 文件与工具 - **契约 e2e 路径**:`packages/commands/tests/e2e/.e2e.test.ts` -- **框架**:`vite-plus/test`;契约测试 `runCli` from `./setup.ts`;cli/rag smoke from `./setup.ts`(内部 `import from "bailian-cli-commands/e2e"`) +- **框架**:`vite-plus/test`;契约测试 `runCli` from `./setup.ts`;cli/rag smoke 通过相对路径引用 `packages/commands/tests/e2e/core/` - **解析 JSON stdout**:`parseStdoutJson`;输出目录:`makeE2eOutputDir(e2eLabelFromMetaUrl(import.meta.url))` - **长任务**:`cliTimeoutPrefix()`;视频用例加 `test(..., 3_600_000)` 等显式超时 diff --git a/packages/cli/src/main.ts b/packages/cli/src/main.ts index 1e32e7f..0968516 100644 --- a/packages/cli/src/main.ts +++ b/packages/cli/src/main.ts @@ -2,7 +2,7 @@ import { createCli } from "bailian-cli-runtime"; import { commands } from "./commands.ts"; import pkg from "../package.json" with { type: "json" }; -createCli(commands, { +void createCli(commands, { binName: "bl", version: pkg.version, clientName: "bailian-cli", diff --git a/packages/cli/tests/e2e/setup.ts b/packages/cli/tests/e2e/setup.ts index 082abdc..a926a20 100644 --- a/packages/cli/tests/e2e/setup.ts +++ b/packages/cli/tests/e2e/setup.ts @@ -1,6 +1,6 @@ import { join } from "path"; import { fileURLToPath } from "url"; -import { createCliRunner } from "bailian-cli-commands/e2e"; +import { createCliRunner, type RunCliFn } from "../../../commands/tests/e2e/core/index.ts"; const cliRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); const mainTs = join(cliRoot, "src", "main.ts"); @@ -11,4 +11,4 @@ const runner = createCliRunner({ binName: "bl", }); -export const runCli = runner.runCli; +export const runCli: RunCliFn = runner.runCli; diff --git a/packages/commands/tests/e2e/core/cli-runner.ts b/packages/commands/tests/e2e/core/cli-runner.ts index 2c35bea..620c789 100644 --- a/packages/commands/tests/e2e/core/cli-runner.ts +++ b/packages/commands/tests/e2e/core/cli-runner.ts @@ -16,12 +16,14 @@ export interface RunCliResult { exitCode: number; } +export type RunCliFn = (args: string[], envOverrides?: NodeJS.ProcessEnv) => Promise; + /** * 工厂:绑定 CLI 入口后返回 `runCli`。 * request_id 等诊断信息在 stderr;`--output json` 时 JSON 在 stdout。 */ export function createCliRunner(target: CliTarget): { - runCli: (args: string[], envOverrides?: NodeJS.ProcessEnv) => Promise; + runCli: RunCliFn; binName: string; } { const { entry, cwd, binName } = target; diff --git a/packages/commands/tests/e2e/core/index.ts b/packages/commands/tests/e2e/core/index.ts index 281dfac..94cb90d 100644 --- a/packages/commands/tests/e2e/core/index.ts +++ b/packages/commands/tests/e2e/core/index.ts @@ -1,4 +1,4 @@ -export { createCliRunner, type CliTarget, type RunCliResult } from "./cli-runner.ts"; +export { createCliRunner, type CliTarget, type RunCliFn, type RunCliResult } from "./cli-runner.ts"; export { cliTimeoutPrefix, cliTimeoutSeconds, diff --git a/packages/commands/tests/e2e/setup.ts b/packages/commands/tests/e2e/setup.ts index be44c7c..933b03b 100644 --- a/packages/commands/tests/e2e/setup.ts +++ b/packages/commands/tests/e2e/setup.ts @@ -1,6 +1,6 @@ import { join } from "path"; import { fileURLToPath } from "url"; -import { createCliRunner } from "./core/cli-runner.ts"; +import { createCliRunner, type RunCliFn } from "./core/cli-runner.ts"; export { cliTimeoutPrefix, @@ -17,6 +17,7 @@ export { isKnowledgeAkSkReady, isKnowledgeE2EReady, createCliRunner, + type RunCliFn, } from "./core/index.ts"; const commandsRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); @@ -28,4 +29,4 @@ const runner = createCliRunner({ binName: "bl", }); -export const runCli = runner.runCli; +export const runCli: RunCliFn = runner.runCli; diff --git a/packages/commands/tests/fixtures/test-cli.ts b/packages/commands/tests/fixtures/test-cli.ts index 1cbe63c..700de0a 100644 --- a/packages/commands/tests/fixtures/test-cli.ts +++ b/packages/commands/tests/fixtures/test-cli.ts @@ -1,5 +1,5 @@ import { createCli } from "bailian-cli-runtime"; -import { commands } from "../../src/index.ts"; +import { commands } from "../../../cli/src/commands.ts"; // 契约 e2e 专用 canonical 入口:全量 commands,与 bl 命令面一致。 void createCli(commands, { diff --git a/packages/core/src/console/gateway.ts b/packages/core/src/console/gateway.ts index 2ea083e..41bec92 100644 --- a/packages/core/src/console/gateway.ts +++ b/packages/core/src/console/gateway.ts @@ -142,7 +142,8 @@ export async function callConsoleGateway( const innerData = json.data as Record | undefined; if (innerData?.success === false && innerData.errorCode) { - const errorCode = String(innerData.errorCode); + const raw = innerData.errorCode; + const errorCode = typeof raw === "string" ? raw : JSON.stringify(raw); const notLogined = errorCode.includes("NotLogined"); const errorMsg = typeof innerData.errorMsg === "string" ? innerData.errorMsg : undefined; throw new BailianError( diff --git a/packages/rag/src/main.ts b/packages/rag/src/main.ts index acfdc9c..795b08c 100644 --- a/packages/rag/src/main.ts +++ b/packages/rag/src/main.ts @@ -42,7 +42,7 @@ const commands: Record = { retrieve: knowledgeRetrieve, }; -createCli(commands, { +void createCli(commands, { binName: "rag", version: pkg.version, clientName: "rag-cli", diff --git a/packages/rag/tests/e2e/setup.ts b/packages/rag/tests/e2e/setup.ts index 4b421d0..06373c2 100644 --- a/packages/rag/tests/e2e/setup.ts +++ b/packages/rag/tests/e2e/setup.ts @@ -1,6 +1,6 @@ import { join } from "path"; import { fileURLToPath } from "url"; -import { createCliRunner } from "bailian-cli-commands/e2e"; +import { createCliRunner, type RunCliFn } from "../../../commands/tests/e2e/core/index.ts"; const ragRoot = join(fileURLToPath(new URL(".", import.meta.url)), "..", ".."); const mainTs = join(ragRoot, "src", "main.ts"); @@ -11,4 +11,4 @@ const runner = createCliRunner({ binName: "rag", }); -export const runCli = runner.runCli; +export const runCli: RunCliFn = runner.runCli; From 7fa96265c7e4e58d1d05fedbc95d53742d760e87 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 16:58:47 +0800 Subject: [PATCH 5/7] feat: update commads.d.ts --- packages/cli/src/commands.d.ts | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 packages/cli/src/commands.d.ts diff --git a/packages/cli/src/commands.d.ts b/packages/cli/src/commands.d.ts new file mode 100644 index 0000000..20c77ae --- /dev/null +++ b/packages/cli/src/commands.d.ts @@ -0,0 +1,2 @@ +import type { Command } from "bailian-cli-core"; +export declare const commands: Record; From 8b7e7a9f527b5a5ded6dd2805b2df58fd9cd448b Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 17:18:21 +0800 Subject: [PATCH 6/7] ci: exclude tests generate .d.ts --- packages/commands/.gitignore | 1 + packages/commands/tsconfig.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/commands/.gitignore b/packages/commands/.gitignore index 7535211..28598e5 100644 --- a/packages/commands/.gitignore +++ b/packages/commands/.gitignore @@ -1,4 +1,5 @@ node_modules dist +tests/**/*.d.ts *.log .DS_Store diff --git a/packages/commands/tsconfig.json b/packages/commands/tsconfig.json index 5910788..5621104 100644 --- a/packages/commands/tsconfig.json +++ b/packages/commands/tsconfig.json @@ -17,5 +17,6 @@ "isolatedModules": true, "verbatimModuleSyntax": true, "skipLibCheck": true - } + }, + "exclude": ["tests/**/*"] } From 1239e32756c4d4f7489a10387a0aaa2a5bf3b2d4 Mon Sep 17 00:00:00 2001 From: clh02467605 Date: Wed, 24 Jun 2026 17:47:32 +0800 Subject: [PATCH 7/7] test: update test case --- .../commands/tests/e2e/config.e2e.test.ts | 50 +------------------ packages/rag/tests/e2e/smoke.e2e.test.ts | 8 +-- 2 files changed, 5 insertions(+), 53 deletions(-) diff --git a/packages/commands/tests/e2e/config.e2e.test.ts b/packages/commands/tests/e2e/config.e2e.test.ts index 5516015..3122d8e 100644 --- a/packages/commands/tests/e2e/config.e2e.test.ts +++ b/packages/commands/tests/e2e/config.e2e.test.ts @@ -10,7 +10,7 @@ describe("e2e: config", () => { const { stdout, stderr, exitCode } = await runCli(["config"]); expect(exitCode, stderr).toBe(0); const out = `${stdout}\n${stderr}`; - expect(out).toMatch(/config|show|set|export-schema/i); + expect(out).toMatch(/config|show|set/i); }); test("config show --help 正常退出", async () => { @@ -25,12 +25,6 @@ describe("e2e: config", () => { expect(stderr).toMatch(/set|--key|--value/i); }); - test("config export-schema --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["config", "export-schema", "--help"]); - expect(exitCode, stderr).toBe(0); - expect(stderr).toMatch(/export-schema|--command/i); - }); - test("config show --output json", async () => { const { stdout, stderr, exitCode } = await runCli([ "config", @@ -146,46 +140,4 @@ describe("e2e: config", () => { const data = parseStdoutJson<{ would_set?: { default_text_model?: string } }>(stdout); expect(data.would_set?.default_text_model).toBe("qwen3.7-max"); }); - - test("config export-schema --command 导出单条工具 JSON", async () => { - const { stdout, stderr, exitCode } = await runCli([ - "config", - "export-schema", - "--command", - "text chat", - "--non-interactive", - ]); - expect(exitCode, stderr).toBe(0); - const schema = parseStdoutJson<{ name?: string; input_schema?: { type?: string } }>(stdout); - expect(schema.name).toMatch(/bailian_text_chat/); - expect(schema.input_schema?.type).toBe("object"); - }); - - test("config export-schema 不存在的子命令时报错", async () => { - const { stderr, exitCode } = await runCli([ - "config", - "export-schema", - "--command", - "this-command-does-not-exist-xyz", - "--non-interactive", - "--output", - "json", - ]); - expect(exitCode).toBe(2); - const err = JSON.parse(stderr.trim()) as { error?: { message?: string } }; - expect(err.error?.message).toMatch(/not found/i); - }); - - test("config export-schema 导出全部为 JSON 数组", async () => { - const { stdout, stderr, exitCode } = await runCli([ - "config", - "export-schema", - "--non-interactive", - ]); - expect(exitCode, stderr).toBe(0); - const arr = parseStdoutJson>(stdout); - expect(Array.isArray(arr)).toBe(true); - expect(arr.length).toBeGreaterThan(0); - expect(arr[0]?.name).toMatch(/^bailian_/); - }); }); diff --git a/packages/rag/tests/e2e/smoke.e2e.test.ts b/packages/rag/tests/e2e/smoke.e2e.test.ts index 667b883..9a2adb0 100644 --- a/packages/rag/tests/e2e/smoke.e2e.test.ts +++ b/packages/rag/tests/e2e/smoke.e2e.test.ts @@ -8,19 +8,19 @@ describe("e2e: rag smoke", () => { expect(stdout.trim()).toMatch(/^rag \d+\.\d+\.\d+/); }); - test("rag 根 help 含 knowledge 与 auth,不含 text/image/memory", async () => { + test("rag 根 help 含 retrieve 与 auth,不含 text/image/memory", async () => { const { stderr, exitCode } = await runCli(["--help"]); expect(exitCode, stderr).toBe(0); const out = stderr; - expect(out).toMatch(/knowledge retrieve/i); + expect(out).toMatch(/\bretrieve\b/i); expect(out).toMatch(/auth login/i); expect(out).not.toMatch(/text chat/i); expect(out).not.toMatch(/image generate/i); expect(out).not.toMatch(/memory add/i); }); - test("rag knowledge retrieve --help 正常退出", async () => { - const { stderr, exitCode } = await runCli(["knowledge", "retrieve", "--help"]); + test("rag retrieve --help 正常退出", async () => { + const { stderr, exitCode } = await runCli(["retrieve", "--help"]); expect(exitCode, stderr).toBe(0); expect(stderr).toMatch(/--index-id/i); expect(stderr).toMatch(/--query/i);