关于 test-harness
用于测试 Claude Code 的 hooks、skills、设置和 CLAUDE.md,覆盖完整运行环境。它在单元、确定性和真实模型评估层级中选择,并优先采用足以回答问题的低成本方案。
主要功能
- 01选择单元、确定性或评估层级
- 02测试 hook 逻辑与环境接线
- 03检查上下文传递和技能触发
- 04通过 vigiles 命令行运行测试
- 05指导评估锁定与 CI 设置
适用场景
- 01验证 hook 是否拦截或放行事件
- 02检查注入的上下文是否到达模型
- 03衡量技能触发的精确率与召回率
- 04比较环境变更前后的 Agent 行为
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ zernie/ vigiles --skill test-harness安装后在 Claude Code 中输入/test-harness确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/cost-and-expectations.mdobserving-a-run.mdwriting-tests.md详细介绍
测试 Claude Code harness——引导 Agent 的钩子、技能、设置和 CLAUDE.md——作为其交付的完整系统。vigiles 提供三个层级,按成本从低到高排列;这个技能会选出合适的层级、编写测试并运行。
指导原则:从能回答问题的最低成本层级开始,只有在确实无法回答时才升级。 三个层级中有两个无需模型和 API 密钥,因此每次提交都能免费运行;只有问题确实需要真实模型时,才使用付费的真实模型层级。
步骤 0 — 选择层级(判断环节)
根据测试内容,选择能回答问题的最低成本层级:
| 测试内容 | 层级 | 成本 | API |
|---|---|---|---|
| “这个钩子会阻止/允许事件 X 吗?”——纯钩子逻辑,所有事件类型(包括 Edit/Write、PreCompact、SessionEnd、SubagentStop) | Unit | 免费,毫秒级,无需 claude | runHook |
| “这个钩子是否确实接入了组装好的插件,并在真实会话中触发?” | Deterministic | 免费,无需 API 密钥(真实 claude + 脚本化 mock) | runHarnessTest + scriptModel |
“注入的上下文(SessionStart 钩子、/command)是否确实传达给了模型?” | Deterministic | 免费,无需 API 密钥 | runHarnessTest → trace.modelRequests / assertRequestContains |
| “这个技能的描述是否会在该触发时触发(召回),并在不该触发时保持安静(精确率)?” | Eval | 付费(真实模型) | measureTriggerRate(加上 irrelevantPrompts)→ assertTriggerRate({ min, maxFalsePositive }) |
| “能否在更便宜的模型上测量触发情况,并将其作为基准?” | Eval | 付费(运行两次) | compareContainment(weak, strong) → formatContainment |
| “这个特定技能的输出质量如何?”——绝对质量,不设启用/禁用基准(测试单个技能时的默认方式) | Eval | 付费(真实模型) | measure({ checks: [judged(rubric)] }) → assertRates({ min }) |
| “这个 harness 改动相较于关闭时,是否改变了 Agent 的行为?”——A/B 提升、回归、信号与噪声 | Eval | 付费(真实模型) | runEval(arms)+ assertSignificant |
大多数 harness 问题——阻止/允许、是否接入、上下文是否到位——都不需要模型。只有“模型是否会触发/表现是否不同”才需要评估层级。
⚠️ 如果每条提示词的触发率都是 0%,在证明并非如此之前,都应视为接线错误。
这看起来像是在给描述下结论,但有三种独立的配置错误会导致这种情况:fired 中用了不带命名空间的 ID,而此处必须使用命名空间形式的 <plugin>:<skill>;松散的 .claude/skills 应使用 skillsDir,却误用了 pluginDir;以及缺少 fixture,因为运行会在空目录中启动,而针对一个不存在的文件的提示词,模型拒绝处理是合理的。报告问题之前,先排除这三种情况。(部分触发率是真实数据,不要反复质疑。)
在确认便宜模型确实构成下限之前,不要针对它调优。
compareContainment(weak, strong) 会给出答案:它会报告弱模型触发、但强模型未触发的提示词;每个这样的提示词都意味着弱模型并非下限,而是一个_不同的路由器_。只有强模型触发的提示词符合预期,不算失败。一次实测(21 个技能、84 条提示词,haiku 对比 sonnet):弱模型独有 3 条,且有一个技能在 haiku 上得分更高——因此尚未证明包含关系,这就是为什么仍将其视为下限。
如果单元测试层和确定性层都能回答这个问题,优先选择单元测试:它速度更快,也能触及确定性模拟无法驱动的事件。
步骤 0.4 — 观察一次运行及其成本
两个问题各有对应的参考文档——需要哪个就打开哪个,不要猜:
- “运行实际做了什么?” — 调用了哪些工具、是否遵守声明的
allowed-tools、写入了什么,以及如何记录调用而不实际执行 →references/observing-a-run.md - “这是免费、低价,还是需要容器?” — 三种类别,以及付费运行后该如何告知用户 →
references/cost-and-expectations.md
在先弄清第二个问题之前,绝不要说“我们来测试一下”。
步骤 1 — 确保已安装 vigiles
检查 vigiles 是否是依赖项(package.json),如果不是,就将其安装为开发依赖:
npm i -D vigiles # or: pnpm add -D vigiles / yarn add -D vigiles
确定性层还需要 PATH 中存在 claude CLI(无需 API 密钥):npm i -g @anthropic-ai/claude-code。评估层需要模型身份验证。如果缺少 claude CLI,仍然可以编写并运行单元测试层的测试。
步骤 2 — 找到要测试的测试框架入口
按以下顺序查找项目实际提供的内容:
.claude/settings.json/.claude/settings.local.json— 内联hooks。.claude-plugin/plugin.json— 插件清单(hooks、skills、agents、mcpServers)。hooks/hooks.json— 插件 hooks 约定(例如 obra/superpowers)。skills/<name>/SKILL.md、agents/<name>.md、commands/<name>.md。
选定一项具体内容进行验证——一个特定的 PreToolUse hook、一个特定的 SessionStart 注入,或一个特定的技能。
步骤 3 — 为所选层级编写测试
各层级的测试骨架,以及一个会悄无声息地吞掉失败的错误(手写运行器会吞掉 stderr)→
references/writing-tests.md
编写文件前请先阅读该文档——不同层级使用不同的骨架,而且运行器警告已让人花费了不少调试时间。
步骤 4 — 运行测试
在运行器(node:test / vitest / jest)中,测试是普通的异步函数。也可以使用无需设置的 CLI,它会发现并运行这些文件:
npx vigiles test # *.harness.{mjs,ts} — unit + deterministic, no API key
npx vigiles eval --trials=6 # *.eval.{mjs,ts} — real model (local / nightly, not CI)
单元测试层的 runHook 测试不需要 claude,而且始终可以运行——即使没有安装 claude,也要编写并运行它们。确实无法运行的层级会明确报告 ⊘ SKIPPED(单独统计,绝不会伪装成 ✓);独立脚本则通过 vigiles 中的 skip(reason) 输出此状态。默认情况下,跳过测试也算通过;但在要求具备相应能力的 CI 任务中,应运行 vigiles test --no-skip,让跳过的层级判为失败——全部通过但有跳过,意味着有些内容未经测试。CI 中保留单元测试和确定性测试(免费);评估则在本地运行,或在有身份验证的情况下按计划运行。
CI — 不要手动编写步骤
这些级别应纳入 CI,并且已有发布的 Action。运行 vigiles init:它会写入 .github/workflows/vigiles.yml,为可使用 Action 的作业接入 Action(zernie/vigiles@v1),并为此级别添加一个普通的 npx vigiles test 作业——该作业需要仓库本地的 node_modules,而 Action 不会安装它,所以有意保留为手动编写。
如果仓库已有工作流,Action 的输入参数已记录在
docs/github-action.md 中。请在那里查看,不要猜测:
输入列表定义在 action.yml 中;如果在此处复制一份,就会形成第二个事实来源,
且可能在无人察觉的情况下过时——这正是本文档的姊妹文档和某个使用者的 CI 注释
都发生过的情况,记录日期为 2026-08-18。
步骤 5 — 锁定评测,确保 CI 如实反映结果(自动执行)
真实模型评测使用用户的订阅运行——仅在本地运行,绝不在 CI 中运行。因此,编写 评测时,要确保结果对用户而言始终是最新的。 请主动完成以下两件事,无需等待 他人要求:
-
为每个评测设置
name。 锁定文件以此作为键。 -
运行评测后,提交锁定文件:
vigiles eval --update # records the result → .vigiles/eval-locks/<name>.lock.json然后提交该文件。CI 会运行
vigiles eval --check(不使用模型)来验证它 仍与输入相符——这样,之后若有修改却忘了重新评测,就会明确失败, 而不是带着过时的数据发布。
之后如果更改技能的描述或提示词,请重新运行 vigiles eval --update 并提交更新后的锁定文件——因为这些更改改变了评测的测量对象。(vigiles
也会提醒你:存在锁定文件时,编辑 SKILL.md 会触发一条非阻塞提醒。)
这样做开销很低:--check 只会对输入(技能文本、提示词、模型)进行哈希计算。测试中
仅更改阈值时,会复用已保存的数据(不使用模型);只有更改输入时,才需要
重新运行 --update。完整机制见:
docs/harness-testing.md。
用户没有说明要测试什么时
不要要求他们指定——选一个实际案例并演示。 检查测试框架的范围(步骤 2),选择 成本最低且有意义的测试,编写并运行,然后展示结果。推荐的默认选择如下,按顺序排列:
PreToolUsehook → 单元测试,验证它会拦截预期拦截的内容(并允许安全的同类操作)。- 注入上下文的
SessionStarthook → 确定性测试,验证文本确实到达模型(assertRequestContains)。 - 技能 → 确定性测试,验证它通过
pluginDir解析,然后将付费的measureTriggerRate评测作为后续选项。
然后说明使用了哪个级别及其原因;如果成本更低的测试无法完整回答他们的问题, 再询问是否要提升测试级别。
参考
完整指南——涵盖所有级别、实际测试技能、“触发 ≠ 生效”、默认安全的沙盒、
覆盖率矩阵,以及与 promptfoo 的比较——见 docs/harness-testing.md。