Skip to content

Latest commit

 

History

History
172 lines (132 loc) · 12.1 KB

cli-e2e-tests.md

File metadata and controls

172 lines (132 loc) · 12.1 KB

CLI E2E 测试规范

架构分层

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

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

触发条件

  • 新增/修改 packages/commands/src/commands 下的 command 实现
  • 新增/修改 packages/cli/src/commands.ts 的 bl 命令路径 map
  • 新建或扩展 packages/commands/tests/e2e/<topic>.e2e.test.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/<kebab-topic>.e2e.test.ts;knowledge 领域集中在 packages/commands/tests/e2e/knowledge/ 子目录(新增 knowledge 命令测试放这里)
  • help:runCommandHelp(routes, [...path, "--help"]) from ./helpers.ts(当前 Vitest worker 内用真实 command + CommandRegistry 渲染,不启动子进程)
  • 离线校验 / dry-run:确认用例不依赖进程初始化、真实请求或持久化后,可显式使用 runCommandInProcess(routes, args, envOverrides);走真实 createCli、middleware 和 command,捕获 stdout / stderr / 退出码,执行后恢复进程环境。worker 内串行捕获,禁止与其他操作全局状态的并发用例混用
  • 子进程:runCommandE2e(routes, args) 保留给 live、实际写盘、进程隔离及 shell / stdio / env 契约;每个 topic 保留代表性子进程用例。NODE_OPTIONS / HTTP(S) 代理配置存在时,runCommandInProcess 自动回退子进程
  • 路由加载:harness 按 command 库公开导出名加载 topic 所需模块,不整体求值命令 barrel;新增导出形式需扩展 loader 并保留与公开导出对象一致性的测试
  • 更新检查:未发布的 harness 包 latest 查询固定返回本地 404,避免重复访问 npm;只拦截这个精确 URL,业务 fetch 和生产 updater 测试仍走原逻辑
  • fixtures:packages/commands/tests/e2e/fixtures/
  • 路由常量:topic-routes.ts(按 topic 维护,非全量产品 map)

产品 smoke

  • bl:runCli from packages/cli/tests/e2e/helpers.ts
  • kscli:runKscli from packages/kscli/tests/e2e/helpers.ts
  • 全量 leaf/group help 使用产品 commands 创建 CommandRegistry,先通过 resolve([...path, "--help"]) 检查 help 路由,再用 captureRegistryHelp 检查完整 Usage;禁止在 test.each(commandPaths/groupPaths) 中逐条启动 tsx 子进程
  • 真实子进程只保留根 help/version、产品身份、代表性叶子 help/鉴权域和缺参退出码等 shell/stdio/env 契约
  • 子进程的 parser 错误和 help 文案遵循 Profile 语言;语义断言应兼容中英文,同时检查参数名和退出码,并使用独立的中英文配置复跑,避免依赖本机 Profile

共享

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

双层 describe(固定结构)

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

// 2) skipIf:缺参 / dry-run / 真实集成
describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
  test("缺少 --<flag> 时退出为用法错误 (2)", ...);
  test("<cmd> --dry-run ...", ...); // 若适用
  test("【model】真实流程", ..., LONG_TIMEOUT);
});

skip 条件(e2e/gating,commands helpers re-export)

场景 条件
文本/搜索/记忆/配置 isDashScopeE2EReady()(.env 中的 DASHSCOPE_API_KEY)
图像/语音 isBailianE2EMediaEnabled() && isDashScopeE2EReady()
视频 isBailianE2EVideoEnabled() && isDashScopeE2EReady()
OpenAPI AK/SK isOpenApiE2EReady()(.env 中必须同时提供完整 AK/SK)
视频 download/task 另需 BAILIAN_E2E_VIDEO_TASK_ID
知识库 chat/search live isChatE2EReady() / isSearchE2EReady()(knowledge chat/search,需 BAILIAN_WORKSPACE_ID + agent ID)

用例类型

  1. --help:runCommandHelp(ROUTES, [..., "--help"]) → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
  2. 缺参:带无害全局 flag(如 --quiet)且不传 required flag → exitCode === 2
  3. --dry-run:实现在联网/上传/写盘之前返回;断言 stdout JSON/文本
  4. 真实集成:放在 skip 块末尾

高风险命令额外要求:

  • --help 展示 runtime 注入的 --yes
  • 无 --yes 返回 exit code 7 和 JSON type: "requires_confirmation"
  • --dry-run 无需 --yes,且必须证明在任何远端请求或本地写入之前返回
  • runtime 的离线 high-risk fixture 必须覆盖带 --yes 确实进入 run(),并断言 yes 不进入 command 自有 flags

Journey 层(用户旅程全链路)

  • 定位:命令 E2E 验单命令契约;journey 验“用户带着目标跨命令走通回路”,结构性断言不在 journey 重复
  • 闭环断言:fixture 埋独特标记词,以“标记词能否被召回”判定回路闭合;硬断言 fail,软断言 recordSoft 落报告人工复核
  • 日志产物:createJourneyReporter 在 test/output/<session>/ 落盘 journey-report.md、分步 stdout/stderr、resources.json(未清理资源警示)
  • 入口:pnpm run test:journey;旅程清单与约定见 journeys/README.md
  • 新增命令时:评估是否属于某条旅程的环节,是则纳入对应 journey 并更新 README 映射表

增删命令同步

  • commands export + topic 路由(topic-routes.ts 或测试文件内 ROUTES)+ 产品 map(cli/commands.ts / kscli/commands.ts)
  • 分组 help 由产品 registry.smoke 负责,无需在 commands 重复

安全与例外

  • 禁止破坏真实用户配置:auth logout 和 config set 默认只用 --dry-run;只有验证持久化契约时,才允许通过 BAILIAN_CONFIG_DIR 指向每个用例独占的临时目录实际落盘,并必须在 finally 中清理;禁止写入或复用真实 ~/.bailian
  • 默认测试隔离:runNodeMain / runCommandE2e / runCommandInProcess / runCli / runKscli 默认走 e2e/isolation(HOME 与 XDG_CONFIG_HOME 隔离、强制 language: en-US、关闭 HOME 外的系统/项目目录探测、清空 Agent 覆盖 env)。runCommandInProcess 经 buildIsolatedCliEnv 后把完整 env 快照交给 runInProcess(与子进程 env 同语义)。DashScope / OpenAPI 凭证继续走环境变量;Console live 用例用 runConsoleCommandE2e / runConsoleCli 带上 access_token。进程内单测用 withIsolatedHome / enterIsolatedHome,禁止各文件自写 inFakeHome。完整约定见 test-isolation.md。
  • 不加 dry-run:dryRun 在 resolveFileUrl / resolveCredential / 上传之后的命令(如 image edit、speech recognize 带 --url)
  • --list-voices 等旁路:先于 --text 校验的 flag,缺参用例勿带该 flag
  • 新增 required option → 至少一条缺参用例;改 dry-run 输出 → 更新对应断言

新增 command 检查清单

  • packages/commands/src/index.ts 导出 + packages/cli/src/commands.ts 暴露路径 + topic-routes.ts 补最小路由
  • packages/commands/tests/e2e/<topic>.e2e.test.ts(新建或扩展)
  • 若改了 usageArgs / flags / exampleArgs,跑 pnpm --filter bailian-cli run generate:reference 更新各 skills/<skill>/reference/ 并提交
  • 子命令 --help 使用 runCommandHelp(分组 help 由 bl registry.smoke 覆盖)
  • skip 块:每个 required flag 缺参;可 dry-run 则加一条
  • 至少一条真实集成(或说明为何仅 smoke);不破坏已有集成用例顺序
  • vp test packages/commands/tests/e2e/<file> 通过

调试命令

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

示例片段

import { FOO_ROUTES } from "./topic-routes.ts";

test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => {
  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(FOO_ROUTES, [
    "foo",
    "bar",
    "--dry-run",
    "--prompt",
    "x",
    "--output",
    "json",
  ]);
  expect(exitCode, stderr).toBe(0);
  const data = parseStdoutJson<{ request?: unknown }>(stdout);
  expect(data.request).toBeDefined();
});

与批量压测的关系

  • E2E:单条/少量调用、断言固定、可进 vp test(见上文 skip 条件)
  • 批量压测:packages/cli/tests/stress/run.mjs + targets/*.mjs,并发 + 报告,仅手动 pnpm run test:stress -- <target>

勿把压测并入 E2E 或默认 CI。详见 stress-batch-tests.md.