mcpField
T
test-harness

test-harness

github.com/zernie测试与质量152.1k 次安装2 天前更新
代码仓库

关于 test-harness

用于测试 Claude Code 的 hooks、skills、设置和 CLAUDE.md,覆盖完整运行环境。它在单元、确定性和真实模型评估层级中选择,并优先采用足以回答问题的低成本方案。

主要功能

  1. 01选择单元、确定性或评估层级
  2. 02测试 hook 逻辑与环境接线
  3. 03检查上下文传递和技能触发
  4. 04通过 vigiles 命令行运行测试
  5. 05指导评估锁定与 CI 设置

适用场景

  1. 01验证 hook 是否拦截或放行事件
  2. 02检查注入的上下文是否到达模型
  3. 03衡量技能触发的精确率与召回率
  4. 04比较环境变更前后的 Agent 行为

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/zernie/vigiles --skill test-harness

安装后在 Claude Code 中输入/test-harness确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

references/
cost-and-expectations.md
observing-a-run.md
writing-tests.md

详细介绍

测试 Claude Code harness——引导 Agent 的钩子、技能、设置和 CLAUDE.md——作为其交付的完整系统。vigiles 提供三个层级,按成本从低到高排列;这个技能会选出合适的层级、编写测试并运行。

指导原则:从能回答问题的最低成本层级开始,只有在确实无法回答时才升级。 三个层级中有两个无需模型和 API 密钥,因此每次提交都能免费运行;只有问题确实需要真实模型时,才使用付费的真实模型层级。

步骤 0 — 选择层级(判断环节)

根据测试内容,选择能回答问题的最低成本层级:

测试内容层级成本API
“这个钩子会阻止/允许事件 X 吗?”——纯钩子逻辑,所有事件类型(包括 Edit/Write、PreCompact、SessionEnd、SubagentStop)Unit免费,毫秒级,无需 clauderunHook
“这个钩子是否确实接入了组装好的插件,并在真实会话中触发?”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 — 找到要测试的测试框架入口

按以下顺序查找项目实际提供的内容:

  1. .claude/settings.json / .claude/settings.local.json — 内联 hooks。
  2. .claude-plugin/plugin.json — 插件清单(hooks、skills、agents、mcpServers)。
  3. hooks/hooks.json — 插件 hooks 约定(例如 obra/superpowers)。
  4. 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 中运行。因此,编写 评测时,要确保结果对用户而言始终是最新的。 请主动完成以下两件事,无需等待 他人要求:

  1. 为每个评测设置 name。 锁定文件以此作为键。

  2. 运行评测后,提交锁定文件:

    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),选择 成本最低且有意义的测试,编写并运行,然后展示结果。推荐的默认选择如下,按顺序排列:

  1. PreToolUse hook → 单元测试,验证它会拦截预期拦截的内容(并允许安全的同类操作)。
  2. 注入上下文的 SessionStart hook → 确定性测试,验证文本确实到达模型(assertRequestContains)。
  3. 技能 → 确定性测试,验证它通过 pluginDir 解析,然后将付费的 measureTriggerRate 评测作为后续选项。

然后说明使用了哪个级别及其原因;如果成本更低的测试无法完整回答他们的问题, 再询问是否要提升测试级别。

参考

完整指南——涵盖所有级别、实际测试技能、“触发 ≠ 生效”、默认安全的沙盒、 覆盖率矩阵,以及与 promptfoo 的比较——见 docs/harness-testing.md。

评论

支持 Markdown