Cline Hooks 测试夹具模板:为 Hooks 系统构建可复用的测试场景
本文围绕 Cline VS Code 扩展中 Hooks 子系统的测试夹具(fixture)模板展开,讲解如何基于 template 目录 提供的标准模板,为 PreToolUse、TaskStart 等 Hook 类型编写、定制并注册新的测试场景。读完本文,你将掌握 Hooks 测试夹具的目录约定、钩子脚本的输入/输出协议、跨平台(Unix/Windows)执行差异,以及如何通过 loadFixture() / withFixtureRunner() 把新夹具接入 hook-factory.ts 的测试体系。
模板定位:Hooks 测试基础设施的起点
Cline 的 Hooks 系统允许用户在工具调用前后、任务生命周期各阶段挂接自定义脚本。为了验证这一子系统的行为(放行、拦截、上下文注入、报错),仓库在 apps/vscode/src/core/hooks/__tests__/fixtures/ 下维护了一整套预写的"钩子脚本样本",即测试夹具。fixtures 总目录的 README 将其组织为:
fixtures/
├── hooks/
│ ├── pretooluse/ # PreToolUse 钩子夹具(success / blocking / context-injection / error)
│ ├── posttooluse/ # PostToolUse 钩子夹具
│ ├── taskcancel/ taskcomplete/ taskresume/ taskstart/ userpromptsubmit/ ...
└── template/ # 新夹具的模板目录
模板目录 template 包含两个文件:
- HookName —— 一个可直接执行的 Node.js 钩子脚本骨架,内置从 stdin 读取 JSON 输入、按工具名分支注入上下文、以及 catch 兜底输出错误 JSON 的完整范式;
- README.md —— 即本文所依据的模板说明文档,给出创建新夹具的四步流程与最佳实践。
新增夹具时的标准工作流是:选定要覆盖的场景类型 → 从模板复制并改名 → 实现场景逻辑 → 更新 fixtures/README.md 的文档登记。
四类基础场景:先确定夹具要验证什么
模板文档(Step 1)要求先明确新夹具测试的场景类型:
| 场景类型 | 语义 | 对应的输出约定 |
|---|---|---|
success |
立即放行 | { cancel: false, contextModification: "...", errorMessage: "" } |
blocking |
阻止工具/任务执行 | { cancel: true, errorMessage: "..." } |
context-injection |
向会话注入带类型前缀的上下文 | contextModification 使用大写前缀,如 WORKSPACE_RULES: |
error |
以非零退出码终止 | 向 stderr 输出错误并 exit 1 |
这些语义并非文档自说自话,而是与运行时严格对齐的。以仓库中现存的 pretooluse/success 夹具 为例,其全部实现就是:
#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
console.log(JSON.stringify({
cancel: false,
contextModification: "PreToolUse hook executed successfully",
errorMessage: ""
}));
而 pretooluse/blocking 仅把 cancel 翻转为 true、将拦截原因写入 errorMessage("Tool execution blocked by hook")。taskstart/blocking 遵循同样的两行差异模式,说明"场景差异最小化"正是夹具设计的核心原则:一个夹具只回答一个问题。
输出字段的运行时约束
模板脚本骨架中曾使用 shouldContinue 字段,但需要注意:当前运行时 hook-factory.ts 的 validateHookOutput() 会明确拒绝 shouldContinue 字段并提示迁移到 cancel: true。因此编写新夹具时必须以现行协议为准:
cancel(可选,布尔):true时请求取消任务/拦截执行;contextModification(可选,字符串):注入到会话上下文的文本,超过 50KB(MAX_CONTEXT_MODIFICATION_SIZE,见 hook-factory.ts#L29)会被截断并追加省略标记;errorMessage(可选,字符串):面向用户/日志的错误描述。
运行时对退出行为还有额外规则,直接影响 error 类夹具的编写:
- 非零退出且能解析出合法 JSON:以 JSON 为准(仅记录警告,见 hook-factory.ts#L471-L481);
- 退出码 0 但无 JSON:视为"成功但不产生取消",返回
cancel: false; - 非零退出且无合法 JSON:抛出
HookExecutionError.execution,这正是error夹具要触发的路径; - 输出中混有调试日志也没关系——运行时会从 stdout 末尾反向扫描括号,提取最后一个完整 JSON 对象(hook-factory.ts#L392-L462)。
模板脚本解剖:stdin 输入协议
HookName 模板 的核心是"从 stdin 读 JSON、向 stdout 写 JSON"的协议,与 HookProcess 子进程执行方式一一对应。模板中各段的含义:
#!/usr/bin/env node
try {
// 1. 从 stdin(fd 0)读取并解析 Hook 输入
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
// 2. 按钩子类型解构业务字段
// PreToolUse: { toolName, parameters }
const { toolName, parameters } = input.preToolUse || {};
// PostToolUse 还可取 result, success, executionTimeMs
// 3. 所有钩子类型共有的元数据
const { hookName: hookType, timestamp, taskId, workspaceRoots, userId } = input;
// 4. 输出变量三件套
let shouldContinue = true; // 模板遗留写法,新夹具应直接输出 cancel
let contextModification = "";
let errorMessage = "";
// 5. === CUSTOMIZE THIS LOGIC ===(自定义分支)
if (toolName === "write_to_file") {
contextModification = "FILE_OPERATIONS: File modification operation";
} else if (toolName === "run_command") {
contextModification = "SYSTEM_OPERATIONS: Command execution operation";
}
console.log(JSON.stringify({ shouldContinue, contextModification, errorMessage }));
} catch (error) {
// 6. 兜底:把脚本自身异常包装为带 HOOK_ERROR 前缀的错误输出
console.log(JSON.stringify({
cancel: true,
contextModification: "",
errorMessage: `HOOK_ERROR: ${error instanceof Error ? error.message : String(error)}`
}));
}
其中第 3 步的元数据并非空谈——hook-factory.ts 的 completeParams() 会在序列化前自动补全 clineVersion、hookName、timestamp、workspaceRoots、userId 与 model(provider/slug),夹具脚本因此始终能拿到稳定的公共上下文。第 6 步的 catch 兜底体现了模板的最佳实践之一:钩子应自行优雅处理错误,而不是裸抛异常后无输出。
创建新夹具:完整操作步骤
Step 1:创建目录结构
按模板文档(Step 2),假设要新增一个 PreToolUse 校验场景:
mkdir -p apps/vscode/src/core/hooks/__tests__/fixtures/hooks/pretooluse/validation/
cp apps/vscode/src/core/hooks/__tests__/fixtures/template/HookName \
apps/vscode/src/core/hooks/__tests__/fixtures/hooks/pretooluse/validation/PreToolUse
chmod +x apps/vscode/src/core/hooks/__tests__/fixtures/hooks/pretooluse/validation/PreToolUse
文件名必须与钩子类型严格同名(PreToolUse、TaskStart 等)。这一点由发现逻辑保证:findUnixHook() 在 Unix 上定位 hooksDir/<hookName> 并同时 fs.stat + fs.access(X_OK),文件不存在或不可执行都会静默视为"无钩子";findWindowsHook() 在 Windows 上只认 <HookName>.ps1,无扩展名脚本被刻意忽略。
Step 2:实现场景逻辑
模板文档(Step 3)给出的参数校验示例(注意已将输出改为现行 cancel 协议):
#!/usr/bin/env node
const input = JSON.parse(require('fs').readFileSync(0, 'utf-8'));
const { toolName, parameters } = input.preToolUse;
let cancel = false;
let contextModification = "";
let errorMessage = "";
// 自定义逻辑:缺少 path 参数则拦截
if (!parameters || !parameters.path) {
cancel = true;
errorMessage = "ERROR: Tool requires a 'path' parameter";
} else {
contextModification = "VALIDATION: Basic input validation passed";
}
console.log(JSON.stringify({ cancel, contextModification, errorMessage }));
Step 3:登记到文档
模板文档(Step 4)要求把新夹具补进 fixtures/README.md,登记四要素:夹具路径、返回值、用途、特殊行为备注。该 README 目前按钩子类型分节(PreToolUse / PostToolUse / UserPromptSubmit / TaskStart …),并给出每个夹具的精确返回对象,例如 hooks/pretooluse/context-injection 返回 { cancel: false, contextModification: "WORKSPACE_RULES: Tool [toolName] requires review" } 且动态引用输入中的工具名。
跨平台执行:夹具为什么"看起来只是 Node 脚本"
模板文档(Best Practices → Platform Compatibility)强调夹具要写可移植的 Node.js 代码、避免平台特定逻辑,因为"这些夹具通过内嵌 shell 执行(类似 git hooks)"。从源码看,这一约束来自 test-utils.ts 的加载逻辑:
- Unix/macOS:
loadFixture()直接复制文件并保留源文件的 mode 位(fs.chmod(destFile, stats.mode),test-utils.ts#L601-L611),这也是模板要求chmod +x的原因; - Windows:同名函数走 writeHookScriptForPlatform() 分支,把 Node 脚本另存为
<HookName>.js伴生文件,并生成一个<HookName>.ps1PowerShell 桥接脚本把 stdin 直通给node,退出码透传。buildPowerShellNodeBridge() 的注释还记录了历史坑:早期[Console]::In.ReadToEnd()+ 管道的写法在 Windows CI 上与子进程退出竞态、偶发吞掉 stdout,故改为让子进程直接继承父进程的管道。
另外,模板 README 引用的旧路径 src/core/hooks/... 是仓库重组前的写法;当前实际路径为 apps/vscode/src/core/hooks/__tests__/fixtures/...,创建夹具时以后者为准。
把夹具接进测试:loadFixture 与 withFixtureRunner
新夹具最终要服务于 tests 目录 下的各钩子类型测试。test-utils.ts 提供三层工具:
createHookTestEnv():建临时目录、在其下创建.clinerules/hooks(createHooksDirectory(),test-utils.ts#L127-L131),stub 掉getAllHooksDirs与 workspace 路径,并重置 HookDiscoveryCache;cleanup()负责还原 stub、清缓存与删除临时目录(Windows 下对 EBUSY 锁有重试)。loadFixture(fixtureName, destDir):把fixtures/<fixtureName>目录下的文件复制进<destDir>/.clinerules/hooks/,如loadFixture("hooks/pretooluse/success", env.tempDir)。withFixtureRunner(hookName, fixtureName, callback):在独立环境中完成"清目录 → 建 hooks 目录 → 重置发现缓存 → 加载夹具 →new HookFactory().create(hookName)"全流程并保证清理,避免多场景测试共享状态。
fixtures/README.md 中的用法示例:
import { createHookTestEnv, loadFixture } from '../test-utils'
it("should work with real hook", async () => {
const env = await createHookTestEnv()
try {
await loadFixture("hooks/pretooluse/success", env.tempDir)
const factory = new HookFactory()
const runner = await factory.create("PreToolUse")
const result = await runner.run(buildPreToolUseInput({ toolName: "test_tool" }))
result.cancel.should.be.false
} finally {
await env.cleanup()
}
})
输入构造器(buildPreToolUseInput / buildPostToolUseInput,test-utils.ts#L321-L366)生成的对象与运行时 NamedHookInput 类型一致——taskId + 类型专属数据块。真实测试中的用法可参考 taskcancel.test.ts,其中连续用 loadFixture("hooks/taskcancel/false-no-error" | "true-with-error" | ...) 覆盖 TaskCancel 的四种返回组合,验证"cancel 标志 × 是否带错误消息"的全真值表。
命名与编写最佳实践
模板文档(Best Practices)的三条规范,结合仓库现状可以落到更可执行的程度:
- 单一场景:一个夹具只验证一个行为,逻辑保持简单;
- 上下文前缀大写:
contextModification使用WORKSPACE_RULES:、FILE_OPERATIONS:这类大写类型前缀,便于在会话上下文中辨识来源(现存夹具如 taskcancel/true-no-error 均遵循此约定); - 平台可移植:只用标准 Node API(如
fs.readFileSync(0)读 stdin),不依赖 shell 特性。
维护层面,fixtures/README.md 的 Maintenance 节要求:新增夹具必须同步登记文档,废弃夹具连同引用一起移除——这与模板文档 Step 4 的"更新文档"要求首尾呼应,保证夹具清单与实际目录长期一致。
小结
模板 README 看似简短,实则是 Cline Hooks 测试体系的"契约说明书":它定义了场景四分法(success / blocking / context-injection / error)、stdin/stdout 的 JSON 协议、跨平台可移植性约束和文档登记流程。配合 hook-factory.ts 中的输出校验(cancel 协议、50KB 截断、非零退出与 JSON 优先规则)和 test-utils.ts 中的环境/夹具加载工具链,开发者可以按"复制模板 → 改三处(目录、逻辑、README 登记)"的固定成本,为任意新钩子类型快速补充可回归、可复现的测试场景。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00