编写和添加测试
测试临时目录¶
使用 test/helpers/temp-dir.ts 中的共享辅助函数来创建测试拥有的临时目录,这样所有权明确,清理工作也留在测试生命周期内:
import { afterEach } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
it("uses a temp workspace", () => {
const workspace = tempDirs.make("openclaw-example-");
// use workspace
});
useAutoCleanupTempDirTracker(afterEach) 有意不提供手动清理方法——Vitest 会在每个测试后负责清理。旧的低层辅助函数(makeTempDir、cleanupTempDirs、createTempDirTracker)仍然存在于尚未迁移的测试中;请避免新使用它们,也避免新增裸 fs.mkdtemp* 调用,除非测试明确验证原始临时目录行为。当确实需要裸临时目录时,请添加一条可审计的 allow 注释并说明原因:
// openclaw-temp-dir: allow verifies raw fs cleanup behavior
const workspace = fs.mkdtempSync(prefix);
node scripts/report-test-temp-creations.mjs 会在新增的 diff 行中报告新的裸临时目录创建和新的手动共享辅助函数使用,同时不阻断现有的清理方式。它遵循与 scripts/changed-lanes.mjs 相同的测试路径分类,并跳过共享辅助函数实现本身。check:changed 会对变更的测试路径运行此报告,作为仅警告的 CI 信号(GitHub 警告注解,而非失败)。
Agent 可靠性评估(技能)¶
我们已经有一些类似“Agent 可靠性评估”的 CI 安全测试:
- 通过真实 Gateway 和 mock OpenAI provider 进行 Agent 准入、run-ID 响应和中止请求(
src/gateway/gateway.test.ts)。 - 验证会话装配和配置效果的端到端向导流程(
src/gateway/gateway.test.ts)。
对于技能(参见 技能),仍缺少以下内容:
- 决策: 当技能列在 prompt 中时,Agent 是否会选择正确的技能(或避免不相关的技能)?
- 合规性: Agent 是否在使用前阅读
SKILL.md,并遵循必需的步骤/参数? - 工作流契约: 多轮场景,断言工具顺序、会话历史延续和沙箱边界。
未来的评估应首先保持确定性:
- 一个使用 mock providers 的场景运行器,用于断言工具调用及其顺序、技能文件读取和会话装配。
- 一套小型的专注于技能的场景(使用 vs 避免、门控、prompt 注入)。
- 仅在 CI 安全套件就绪后,才提供可选的在线评估(opt-in,受环境变量门控)。
成本预算¶
CI 按所属领域选择测试,因此每个 PR 的成本会重复支付。预算在一台 worker 上通过 pnpm test <file> --maxWorkers=1 测量:
- 每个文件的目标测试时间低于 5 秒。若超过 30 秒,PR 正文需解释哪个契约需要这么长时间,以及为什么没有更廉价的层级能证明它。
- 某个文件需要的测试时间超过其规划器(planner)的每作业预算时,它不能与其他文件共享该预算,并可设置其作业的墙钟时间。请按所有者边界拆分它,或考虑仅发布层级(
scripts/lib/ci-node-test-plan.mts中的RELEASE_ONLY_*集合)。权衡无关 PR 破坏其契约的可能性,以及在发布时才发现该故障的成本。未变更的套件本身可以提供发布前证明;并不强制要求独立的重复发布测试。仅凭速度慢并不足以证明删除覆盖率合理。 - 维护者工具族使用
RELEASE_ONLY_TOOLING_SHARDS,并在混合快速配置中使用匹配的维护者叶子:仅产品的 PR 和 main 会忽略它,工具所有者 PR 会选择其受影响文件(对于无法解析的所有者则有全族回退),而手动 CI 和完整发布验证会保留它。将测试保留在其规范配置中,并保留其进程和计时器策略,以便新文件继承相同的所有者路由。专门的产品 E2E 和在线测试仍在该层级之外。参见 Node 测试通道。 - 在所有者处使用注入式时钟,而不是真实计时器、睡眠或轮询;通过
src/test-utils/port-claims.ts声明端口;让每个文件拥有自己的状态目录;复用套件级别的 Gateway 和进程 fixture,而不是每个测试都启动;导入插件或模块的窄测试 API,而不是其完整 barrel 导出。不要添加串行 Vitest 配置或 worker 固定(pin):请修复会需要这种配置的共享状态。 - 在收集阶段加载编译子进程声明(
scripts/lib/vitest-worker-declarations.mts)。在 Vitest 调用中,第一次这样的加载会准备整个已编译 worker 生成(热时数十秒,冷时数分钟),因此,在测试或 hook 中,如果其依赖图触及声明,await import()会在该测试或 hook 的截止时间内消耗这份准备时间。请静态导入被测对象;每个测试都重新导入被测对象的套件,应添加对src/test-utils/prepare-compiled-subprocesses.ts的副作用导入。 - 在每个新增或实质变更的测试文件的 PR 中说明实测成本,并在运行存在后说明 CI 秒数。
withTestTimeout 和 raceWithTimeoutResult 是历史遗留的墙钟竞态;check:test-timeout-race-ratchet 将其每文件计数保存在 config/test-timeout-race-baseline.txt 中,且该计数只允许缩减。请使用 awaitGateBeforeSettlement(gate, operation, message) 或来自 test/helpers/promise.ts 的 withinTest(work, signal) 等待所属完成信号,或通过所有者的注入时钟接缝使用 vi.useFakeTimers()。在移除相关调用点后,运行 pnpm check:test-timeout-race-ratchet --prune 以缩减基线。
原始 SQLite 状态访问¶
closeOpenClawStateDatabaseForTest() 会同步关闭原生句柄,但由 worker 支持的状态写入(插件状态、延迟插件迁移以及其他 worker 存储)会保留一个 worker 连接,而该连接的回收只从该调用开始。其最终关闭会在稍后的任意时刻,在排他锁下执行 checkpoint 并删除 WAL。在使用原始 DatabaseSync 打开数据库,或复制、哈希、对其文件做快照之前,请 await closeOpenClawStateDatabaseAsync()(或使用来自 src/test-utils/database-cleanup.ts 的 closeStateDatabaseForTest(),它还会清除失败闩锁)。否则原始连接可能因 SQLITE_BUSY 失败,或者快照可能在测试期间发生变化。在原始连接上执行 PRAGMA locking_mode=EXCLUSIVE; BEGIN EXCLUSIVE 可以证明不存在其他连接。
技能监视器¶
skills.status 和技能快照准备会启动真实的 @openclaw/fs-safe
监视器。在共享 worker 通道中,非隔离 runner 会关闭某个文件遗留打开的
任何监视器,并以 skills watchers failed 使该文件失败;否则,
它们重新武装的定时器会落到后续文件的假时钟上,并中止其
vi.runAllTimersAsync()。请在 afterEach 中通过
closeSkillsWatchers(true) 关闭它们,或者当测试不涉及监视功能时,
设置 skills.load.watch: false。
不稳定测试排查¶
没有相关变更的失败就是缺陷。绝不要通过重新运行、重新推送或刷新 来获得绿色结果。
- 首先按失败 shard 的文件顺序复现(计划中的文件列表在 作业日志中),然后单独运行。仅顺序相关的失败是前面文件 造成的共享状态泄漏。
- 分类:fixture(临时状态、端口、cwd、env、模块单例)、顺序 (在所属完成信号之前进行断言)或产品(真实竞态)。
- 在所有者处修复。产品竞态需要添加一个在原始缺陷上失败的回归测试;fixture 泄漏应在 fixture 所有者处修复,而不是在失败的测试中修复。
- 证明标准:该文件 20 次干净的独立运行,原始 shard 3 次干净运行,以及所有者的同级测试。在 PR 中记录根本原因。
- 如果所有者是另一个通道或 PR,请引用该修复;这是在红色作业上继续推进的唯一理由。
添加回归测试(指南)¶
对于清单增长和容量回归测试,请通过真实 planner 传递有界的合成清单 和固定时序数据。将 fixture 放在容量边界处,并保留覆盖、所有权 和执行预算断言。将真实 checkout 的清单覆盖保留在其集成测试中, 而不是为每个合成变体重建不断增长的仓库计划。
当你修复在 live 中发现的 provider/model 问题时:
- 如果可能,添加 CI 安全的回归测试(mock/stub provider,或捕获精确的请求形状转换)
- 如果问题本质上仅限 live(速率限制、认证策略),请保持 live 测试范围狭窄,并通过环境变量选择加入
- 优先定位能捕获 bug 的最小层:
- provider 请求转换/重放 bug -> 直接 models 测试
- gateway 会话/历史/工具流水线 bug -> gateway live 冒烟测试或 CI 安全的 gateway mock 测试
- SecretRef 遍历防护栏:
src/secrets/exec-secret-ref-id-parity.test.ts从注册表元数据(listSecretTargetRegistryEntries())为每个 SecretRef 类派生一个采样目标,然后断言遍历段 exec id 被拒绝。- 如果你在
src/secrets/target-registry-data.ts中添加了新的includeInPlanSecretRef 目标家族,请更新该测试中的classifyTargetClass。该测试会故意对未分类的目标 id 失败,因此新类无法被静默跳过。
本页原文 Markdown:在 AtomGit 查看·内容源自开源项目 cl/openclaw