Skip to content

Latest commit

 

History

History
236 lines (179 loc) · 20.7 KB

cli-e2e-tests.md

File metadata and controls

236 lines (179 loc) · 20.7 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 路径除外)。

触发条件

  • 新增/修改任意包的 **/tests/**,包括单测、tests/output、runtime 和测试 helper;均须执行下方「执行方式与性能必查清单」,命令 E2E 另遵循本文其余章节
  • 新增/修改 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 渲染,不启动子进程)
  • 离线 CLI 契约(校验 / dry-run / 本地模拟 HTTP):不依赖进程初始化或跨进程持久化的用例必须优先使用 runCommandInProcess(routes, args, envOverrides, { cwd? });走真实 createCli、middleware 和 command,捕获 stdout / stderr / 退出码,执行后恢复进程环境。本地模拟 HTTP 仍走真实 SDK / client 和回环服务,不能仅因存在 HTTP 就逐用例启动子进程。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) 等显式超时

执行方式与性能必查清单

A. 按测试目标选择执行层

测试目标 必须采用的方式
纯函数、独立模块行为 直接调用被测模块,不为单测启动完整 CLI
全量 help / 命令路径发现 registry + runCommandHelp;产品身份和代表性入口另保留子进程
CLI 参数、输出格式矩阵、普通 dry-run、本地模拟 HTTP 契约 使用已有隔离的进程内 runner;commands 用 runCommandInProcess,runtime 用 e2e/in-process + 真实入口逻辑
live、实际持久化及跨进程恢复、启动时 env / preload / proxy、TTY / 管道 / 信号 / 进程退出 保留真实子进程,并在用例或 helper 附近说明所验证的进程边界
  • 每个 topic 保留与其风险对应的代表性子进程覆盖;不得把格式/参数矩阵逐项乘上 CLI 启动成本。迁移 runner 时,对代表性成功、错误或确认分支核对 stdout、stderr 和退出码;确定性 fixture 可直接逐字节比较
  • CLI 进程内契约测试必须复用 e2e/isolation 和 e2e/in-process,不得为此自写 stdout/stderr、env、cwd 捕获器。涉及项目或相对文件路径时显式传 { cwd: fixtureDirectory };修改 helper 时验证直接执行和自动子进程回退均保留 cwd,调用后恢复环境
  • 直接调用模块的单测无需套用 CLI runner,可以注入 reporter/输出流并监测意外的全局写入;需要 HOME/env 隔离时按 test-isolation.md 分层选择。core 单测使用自身的 tests/helpers/isolation.ts,不得反向依赖 e2e
  • 本地模拟 HTTP 继续断言实际请求的方法、路径、查询、鉴权头、body 和响应;静态命令的确认前分支与静态 dry-run 证明零请求/零写入;带 prepare 的命令在确认前或 dry-run 中可先鉴权并读取远端计划,须断言仅执行计划所需的只读请求,且零远端/本地写入。进程内执行不能替换掉这些协议断言
  • 离线 fixture 显式固定会影响结果的环境变量(如输出开关、凭证、代理、preload),不继承本机状态;启动环境专项测试仍显式注入并走子进程,不能通过清空变量绕过被测行为。完整隔离要求见 test-isolation.md

B. 控制等待与 fixture 成本

  • 轮询、重试、debounce 的普通逻辑测试使用受控时间推进,覆盖等待前后边界、请求次数、终态和必要清理;不得在矩阵内累计真实 sleep。不能把被测轮询函数整体 mock 掉;真实超时中止、信号和退出场景另保留真实计时/子进程覆盖
  • 仅为需要的用例创建 SDK 项目、build、版本和文件树等重型 fixture;输出展示或参数校验不得无条件执行无关初始化
  • 可共享不可变声明,不能为提速跨用例共享可写目录、State、服务端请求记录或未恢复的全局状态;服务器、连接、定时器和监听器必须清理
  • 性能优化保留必要的成功、失败、权限/确认、边界和副作用断言;不得通过放宽断言、提高超时、改成 gated/skip 或删除必要覆盖获得提速。确需合并重复用例时必须说明覆盖映射,不以用例总数替代审查

C. 提交前提供耗时证据

  • 新增/扩展套件至少运行一次并记录通过/跳过数量、套件耗时、总墙钟、Node 版本与 worker 数;修改既有套件须有同条件基线。结果写入 PR/交付说明,不新增依赖本机速度的断言
  • 性能优化或发现明显耗时增长时,前后各连续运行三轮,报告原始值和中位数;同机器、Node、worker、门控及 fixture 条件,不与其他重型任务并跑。并行套件耗时之和不能冒充总墙钟
  • 耗时明显增长时,定位新增的子进程、真实等待和重型初始化;说明必要成本,消除无关成本后再交付。修改共享 helper 时回归现有调用方及其回退路径
  • 保留/新增哪些覆盖、哪些仍用子进程、哪些因环境未执行,必须在交付中说明;缺凭证跳过和初始化失败不能当作通过

定向计时示例(报告路径使用仓库外目录;该命令本身不会自动比较基线或阻止性能回退):

pnpm exec vp test packages/commands/tests/e2e/managed-agent-webhook.e2e.test.ts \
  --maxWorkers=2 --reporter=json --outputFile=/tmp/managed-webhook-before-1.json

需要三轮对比时为每轮分别命名 before-1/2/3 和 after-1/2/3,从报告读取套件耗时与总墙钟;worker 数可按环境选择,但前后必须一致。

D. CI 分片与汇总门槛

  • .github/workflows/ci.yml 使用 4 个独立 runner 的原生 --shard=N/4,保留完整测试范围、默认文件隔离、既有门控及每片相同的 Node/Bun 版本;不能用新增 skip、文件过滤或同一 checkout 多进程共享全局状态替代分片
  • 质量检查与测试矩阵并行;矩阵 fail-fast: false,失败时仍上传已有报告。每片输出独立命名的 blob + JSON 报告,保留 3 天
  • jobs.check 和名称 lint + typecheck + test 保持现有 required check 身份;汇总 job 用 always() 执行,并显式要求质量检查和整个测试矩阵均成功,不能让失败、取消或整个 job 跳过变成绿灯
  • tools/ci/verify-shard-reports.mjs 在合并前核对 4 片报告完整,动态发现的全部测试文件恰好出现一次,报告及用例没有失败;缺报告、漏跑、重复或元数据不一致必须使门槛失败。不要硬编码文件数或用例数
  • 用 --mergeReports 汇总 blob 报告;即使校验失败仍尽可能合并已有报告以便诊断,合并成功不能覆盖前面的失败结果。调整该链路时验证成功路径与失败/取消/跳过、缺报告、漏跑和重复路径

离线与真实集成分组

// 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) 不 skip:无密钥的离线校验、dry-run、本地模拟 HTTP 契约
describe("e2e: <topic> offline contracts", () => {
  test("缺少 --<flag> 时退出为用法错误 (2)", ...);
  test("<cmd> --dry-run ...", ...); // 若适用
});

// 3) skipIf:需要真实凭证/服务的集成
describe.skipIf(<ready>)("e2e: <topic>(DashScope …)", () => {
  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)
记忆 live isMemoryE2EReady()(另需 BAILIAN_E2E_MEMORY_LIBRARY_ID;记忆服务需账号单独开通)

用例类型

  1. --help:runCommandHelp(ROUTES, [..., "--help"]) → stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env
  2. 缺参:带无害全局 flag(如 --quiet)且不传 required flag → exitCode === 2
  3. --dry-run:静态命令在联网/上传/写盘之前返回;prepare 命令可先鉴权并读取完整远端计划,但不得产生远端或本地写入。断言 stdout JSON/文本及请求清单。
  4. 真实集成:放在 skip 块末尾

高风险命令额外要求:

  • --help 展示 runtime 注入的 --yes
  • 无 --yes 返回 exit code 7 和 JSON type: "requires_confirmation"
  • --dry-run 无需 --yes;静态命令证明零网络/零写入,prepare 命令证明缺凭证返回 AUTH、只调用读接口且零写入
  • prepare 命令同时验证:实际无风险可执行,有风险时错误包含 plan/notices,预览/确认前不运行自动升级,服务端错误原样透传
  • --introspect 根/组/叶均跳过必填、validate、prepare、auth 和 run;输出实际产品路径及确定性 JSON
  • runtime 的离线 high-risk fixture 必须覆盖带 --yes 确实进入 run(),并断言 yes 不进入 command 自有 flags

Journey 层(用户旅程全链路)

  • Memory 主干:packages/commands/tests/e2e/memory/journeys(J1–J5 CLI)与 packages/bailian-memo-dsh/tests/journeys(J6–J8 受控宿主);严格入口 node tools/test-memory-journeys.mjs。默认库只要求 Key/Workspace,缺环境或跳过必跑项不能当作通过。真实 Agent 会话验收与受控宿主分开报告。

  • 定位:命令 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 覆盖)
  • 离线块:每个 required flag 缺参;可 dry-run 则加一条;仅需要真实服务的用例放进 skip 块
  • 至少一条真实集成(或说明为何仅 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 { parseStdoutJson, runCommandInProcess } from "./helpers.ts";
import { FOO_ROUTES } from "./topic-routes.ts";

test("foo bar 缺少 --prompt 时退出为用法错误 (2)", async () => {
  const { stderr, exitCode } = await runCommandInProcess(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 runCommandInProcess(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();
});

Memory 发布前审计

发布判断应包含命令契约、风险到测试的映射和安装产物验证,不能只看用例总数或源代码 harness 全绿。参考 Memory CLI 发布审计。tools/smoke-memory-package.mjs 接受本地 tarball 目录,使用隔离配置安装并执行产物 bin;--live 只对随机测试身份执行自清理 CRUD。服务 fixture 未配置的分组必须在报告中列为未运行,不能当作通过。真实 dsh/Skill 宿主是否作为门槛按当次用户明确的范围决定。

与批量压测的关系

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

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