首页
/ Cline Hooks 测试夹具模板:为 Hooks 系统构建可复用的测试场景

Cline Hooks 测试夹具模板:为 Hooks 系统构建可复用的测试场景

2026-09-06 14:22:23作者:裘晴惠Vivianne

本文围绕 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.tsvalidateHookOutput()明确拒绝 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() 会在序列化前自动补全 clineVersionhookNametimestampworkspaceRootsuserIdmodel(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

文件名必须与钩子类型严格同名(PreToolUseTaskStart 等)。这一点由发现逻辑保证: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/macOSloadFixture() 直接复制文件并保留源文件的 mode 位(fs.chmod(destFile, stats.mode)test-utils.ts#L601-L611),这也是模板要求 chmod +x 的原因;
  • Windows:同名函数走 writeHookScriptForPlatform() 分支,把 Node 脚本另存为 <HookName>.js 伴生文件,并生成一个 <HookName>.ps1 PowerShell 桥接脚本把 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 提供三层工具:

  1. createHookTestEnv():建临时目录、在其下创建 .clinerules/hookscreateHooksDirectory()test-utils.ts#L127-L131),stub 掉 getAllHooksDirs 与 workspace 路径,并重置 HookDiscoveryCachecleanup() 负责还原 stub、清缓存与删除临时目录(Windows 下对 EBUSY 锁有重试)。
  2. loadFixture(fixtureName, destDir):把 fixtures/<fixtureName> 目录下的文件复制进 <destDir>/.clinerules/hooks/,如 loadFixture("hooks/pretooluse/success", env.tempDir)
  3. 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 / buildPostToolUseInputtest-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 登记)"的固定成本,为任意新钩子类型快速补充可回归、可复现的测试场景。

登录后查看全文
热门项目推荐
相关项目推荐