| 层级 | 路径 | 测什么 |
|---|---|---|
| 共享基建 | 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。
- 路径:
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)
- bl:
runClifrompackages/cli/tests/e2e/helpers.ts - kscli:
runKsclifrompackages/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)等显式超时
| 测试目标 | 必须采用的方式 |
|---|---|
| 纯函数、独立模块行为 | 直接调用被测模块,不为单测启动完整 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
- 轮询、重试、debounce 的普通逻辑测试使用受控时间推进,覆盖等待前后边界、请求次数、终态和必要清理;不得在矩阵内累计真实 sleep。不能把被测轮询函数整体 mock 掉;真实超时中止、信号和退出场景另保留真实计时/子进程覆盖
- 仅为需要的用例创建 SDK 项目、build、版本和文件树等重型 fixture;输出展示或参数校验不得无条件执行无关初始化
- 可共享不可变声明,不能为提速跨用例共享可写目录、State、服务端请求记录或未恢复的全局状态;服务器、连接、定时器和监听器必须清理
- 性能优化保留必要的成功、失败、权限/确认、边界和副作用断言;不得通过放宽断言、提高超时、改成 gated/skip 或删除必要覆盖获得提速。确需合并重复用例时必须说明覆盖映射,不以用例总数替代审查
- 新增/扩展套件至少运行一次并记录通过/跳过数量、套件耗时、总墙钟、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 数可按环境选择,但前后必须一致。
-
.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);
});| 场景 | 条件 |
|---|---|
| 文本/搜索/配置 | 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;记忆服务需账号单独开通) |
- --help:
runCommandHelp(ROUTES, [..., "--help"])→ stderr 含主要 flags;产品层另保留少量真实子进程 help 验证 shell/stdio/env - 缺参:带无害全局 flag(如
--quiet)且不传 required flag →exitCode === 2 - --dry-run:静态命令在联网/上传/写盘之前返回;prepare 命令可先鉴权并读取完整远端计划,但不得产生远端或本地写入。断言 stdout JSON/文本及请求清单。
- 真实集成:放在 skip 块末尾
高风险命令额外要求:
--help展示 runtime 注入的--yes- 无
--yes返回 exit code 7 和 JSONtype: "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
-
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 输出 → 更新对应断言
-
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 由 blregistry.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.tsimport { 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();
});发布判断应包含命令契约、风险到测试的映射和安装产物验证,不能只看用例总数或源代码 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.