Files
deepseek-harness/docs/testing.zh.md
Turtle f290a8b851 refactor(cli)!: one shared base config with per-surface overlays
`dsh` shipped two config trees that were 43 rows the same: apps/cli/cordis.yml
composed web as 74 flat rows, while the TUI booted examples/tui-agent/cordis.yml
whose single `@deepseek-ai/dsh-tui-demo` row mounted twelve plugins behind a
twenty-key pass-through Config. Neither file was what its location claimed —
apps/cli hardcoded the "example" as the product default and the "demo" bundle
was the application — and every capability change had to be made twice.

- apps/cli/base.cordis.yml holds the 43 shared rows; tui.cordis.yml and
  web.cordis.yml are patch lists stating only what differs per surface
- overlays apply as SIBLING patch lists at one include level, because include
  patches never cross an include boundary. Precedence: base < surface <
  (--config | personal ~/.dsh/config.yaml) < launcher flag/profile patches
- `--config` now applies an overlay INSTEAD OF the personal one, so a demo or
  test tree never inherits the user's route; new `--config-replace` boots a file
  as the entire tree (the old `--config` behaviour). Both survive /resume
- vendor/include: index each `insert`ed row as it is added so a later patch can
  configure or disable it. Upstream built the id index once before the patch
  loop, leaving every surface-only row — the whole TUI front door — silently
  unpatchable from user config. Logged as local modification 8
- session identity moves to dsh-agent-loop's CONFIGURED_AGENT_IDENTITIES_KEY;
  dsh-tui's MAIN_SESSION_ID_KEY is deleted (only the bundle read it)
- delete examples/tui-agent, examples/cordis-agent, packages/examples/tui-demo;
  TUI tests → apps/cli/tests, cordis e2e → packages/cordis/tool-cordis/tests,
  examples/code-mode survives as an overlay leaf
- `dsh web` gains --config, threaded into AppCLIEntry as an extra overlay

Three latent defects surfaced and are fixed here: the TUI captured the optional
sessionQuery service once at construction and could permanently disable /resume
when it won the mount race; the session-store root silently reverted to a
project-local ./.sessions; --config-replace was dropped by the resume handoff.

Verified by booting each tree through the real Loader (TUI 55 entries, web 75,
zero unsettled) rather than reading YAML. All eight terminal snapshots replay
byte-identically; 14/14 PTY smoke, 112/112 snapshots, 25/25 doc-sync, hygiene
and lint clean.
2026-07-29 21:15:42 +08:00

9.1 KiB
Raw Blame History

测试策略

English | 中文

本文说明本仓库的分层测试方式,以及保持绿色测试套件有意义的规则。命令见根目录 AGENTS.md;相关 Agent Noteagent 决策记录)承载设计动机。

层级

  • 单元测试pnpm run testvitest 运行包package和示例各自的 tests/** 目录下的测试,以及匹配 scripts/**/*.spec.ts 的仓库脚本测试;测试文件与其所覆盖的代码区域放在一起。每个注册表都有一个 HMR热模块替换安全测试dispose资源释放贡献的 fiber断言清理完成。优先覆盖边界情况、错误路径、事件顺序、并发竞态以及永久性契约回归packages/core/agent-loop/tests/contract-regressions.spec.ts)。
  • 覆盖率门禁pnpm run test:coverage):门禁级运行,对 packages/*/*/src 按文件 100% 覆盖。未覆盖的行往往是门禁正确标记出的死代码(应删除),而非需要补写的测试。行覆盖率是必要条件,但永远不是充分条件:它证明行被执行过,不证明功能按交付预期工作。
  • 真实 API e2epnpm run test:e2e):带密钥测试调用真实提供方 API包括 DeepSeek 模型以及各提供方特有的冒烟测试;这些测试各自由自己的密钥控制(EXA_API_KEYPERPLEXITY_API_KEY 等),缺少密钥时套件会自动跳过,使 keyless CI 保持绿色(真实 API e2e Agent Note)。
  • 快照pnpm run test:snapshot无密钥预期输出覆盖对外行为传输契约与呈现持久化日志则固定组装后的后端行为。ACP 启动真实的自动化服务器示例、回放录制会话,并对归一化 JSON-RPC 与重新持久化的日志执行 diffACP 快照 Agent Noteheadless 通过真实单次运行进程固定 stream-json。TUI 旅程通过真实循环与工具回放主会话与子会话 JSONL再将 ANSI 投影为语义化终端状态输出;包级快照保留瞬态状态,真实 PTY 覆盖进程边界(TUI 快照 Agent Note)。当模型 transcript文本记录发生变化时使用 pnpm run test:snapshot:record,回放输入仍然有效时使用 pnpm run test:snapshot:refresh;请审查每一处 JSONL 与预期输出差异。一个 ACP 场景(text-turn)固定完整的系统提示词与工具 schema 内容;其他 fixture测试前置数据将其 token 化,因此修改只会扰动一行(pinned-header Agent Note)。
  • Web 浏览器快照(豁免门禁的 pnpm run test:web):真实 chromium 在进程内 web 组装之上回放已录制 fixture与会话区 aria 预期输出比对(apps/web/tests/snapshots/DSH_SNAPSHOT=record/refresh 的语义与暂缓的 CI 浏览器决策见 web e2e 车道 Agent Note先跑 build:插件 CSS 按插件分别发布。

签入仓库的会话格式 JSONL 使用规范打包行布局,无密钥快照门禁会通过 session header 发现每一份此类 fixture。仍携带旧版 fixture 改动的在途分支应合并当前 master,并通过 pnpm run migrate:packed-session-fixtures 运行临时迁移器;待所有受影响分支收敛后,移除提案会移除该命令及这些链接。

带密钥策略:推理在这里很便宜

我们是 DeepSeek不要吝惜真实 API 测试。无密钥测试只能证明底层通路;只有带密钥运行才能证明 agent智能体能对接真实模型正常工作。覆盖文件写入提示词、包含多个轮次的对话、工具使用和流中取消。价值最高的是冒烟测试:启动真实示例、发送一条提示词,并检查外部世界;它们能捕获「单元测试全绿、产品却坏了」这一类 mock 无法发现的问题(事故复盘 0001)。自动跳过让无密钥 CI 和无密钥贡献者不受阻塞;它不是成本信号。每个示例都提供无密钥和带密钥冒烟测试(examples/AGENTS.md)。

优先使用真实实现而非 mock

只 mock 开销高或不确定的边界LLM大语言模型适配器、网络、时钟下游一切保持真实。手写替身只能证明桥接层在搬运字节不能证明交付的工具行为符合断言。桥接工具调用测试将脚本化 mock 模型与真实工具和执行器配合使用:makeBridgeHarness({ withBash: true }) 接入 dsh-bash-localdsh-tool-bash,然后运行 echo

恢复测试按步骤区分分片前与分片后的失败,并证明失败分片不会派生出消息或工具副作用。覆盖耗尽、取消、策略组合、持久化、状态、协议计数、会关闭传输的空闲超时,以及交付的 Loader 组合。

验证外部世界,而非自我报告

e2e 断言应重新运行命令或从外部重新读取文件;对 agent 自身输出做关键词探测会让作弊的 agent 通过。断言未修改的文件逐字节一致。e2e 测试自行管理资源:在测试中创建 harnessafterEach 中 dispose即使失败/重试/超时也要释放);共享 fixture 放在普通的 tests/harness.ts 中,绝不放在另一个 *.e2e.ts 中(导入一个 spec 会重新注册其 describe,导致真实 API 调用重复执行)。

测试真实入口路径

  • 产品可见的插件必须有一个非单元的真实组合测试。手动构建的 ctx.plugin(...) 套件不够:通过 Loader 和 app/process 启动仅用于测试的 cordis.yml,只 mock 外部/不确定边界,断言模型可见的请求/日志、持久状态或用户可见输出。不要把 opt-in 选项混入交付默认值。
  • 一个守卫只有在回归真的能让它失败时才有效。对于没有 inject 的插件bundle/组合插件Loader 冒烟测试在导出形状损坏时仍然绿着——需要添加显式的 expect('default' in mod).toBe(false)unwrapExports 往返断言,并证明它有效:引入回归、观察变红、回退。
  • 「真实入口路径」指已发布的产物:包的 bin 所运行的是构建后的 lib/bin.js,并由普通 node 执行,从而暴露 tsx 会掩盖的失败(等待稳定时的竞态、模块解析、被吞掉的加载失败)。同样的规则适用于非 index 运行时入口worker-thread 的同级文件 lib/worker.cjs),也适用于多个 bundle 共享的单例模块(packages/ui/jsonrpc/tests/built-scope-carrier.e2e.ts)。保持构建产物冒烟测试绿色(packages/ui/*/tests/built-bin.e2e.tspackages/code-runtime/code-runtime-worker/tests/built-lib.e2e.ts),并断言真正缺失的配置以非零状态退出。

测试解析:仅限源码

  • 每个 vitest 配置都将 vite-tsconfig-paths 指向 tsconfig.base.json;工作区包的裸导入解析到 src布局),绝不会经由包的 exports 解析到构建后的 lib/,因为其中的陈旧产物会加载第二份模块单例。构建产物只在显式指定时使用:以 lib 模式运行的子进程,以及下文的构建产物冒烟测试。

测试子进程启动模式

  • CI 与已有构建产物的测试通道通过共享双模式启动器,从构建后的 lib/ 运行每个示例或 Cordis 配置子进程。不要为这些子进程手写 --import tsx
  • 不加载 Cordis 的协议与操作系统 fixture 直接通过 Node 运行使用可擦除语法的 .ts 文件,不经过 tsx 或根路径映射。
  • 只有测试对象本身是源码路径解析时,才可以选择 src;在测试中写明这一契约。

何时需要快照测试

每项非平凡的模型可见、协议可见或人类可见变更,都必须在同一 PR 中通过可运行示例所属的快照套件添加或更新无密钥场景。包测试、e2e 断言、mock 与仅测试组合、PR 理由都不能取代组装后的 transcript必要时应扩展 harness。ACP 自动化场景使用 examples/<name>/tests/snapshots/,即基于 dsh-acp-snapshot 套件工厂的场景表(examples/acp-agent 为主套件);examples/headless-agent 拥有 stream-json 快照与回放 fixture。已完成的交互式终端旅程使用 apps/cli/tests/snapshots/ 下由 JSONL 驱动的场景瞬态呈现使用包内语义矩阵输入、Loader 选择或终端清理发生变化时还要添加 PTY 用例。新的能力 seam、生命周期形态或 transcript 呈现接口在计划阶段就要列出每个覆盖层级,并在实现前验证 harness 能够表达它们。