☰
Metabase 变异测试自动化 Skill:用 nREPL、Claude CLI 与 Linear/GitHub 流水线绞杀存活变异
2026/10/6 13:07:52 网站建设 项目流程

Metabase 变异测试自动化 Skill:用 nREPL、Claude CLI 与 Linear/GitHub 流水线绞杀存活变异

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

导读

本文讲解当前 Metabase 仓库中一个面向 Clojure 开发工作流的自动化技能——.claude/skills/mutation-testing/SKILL.md。它把"变异测试(Mutation Testing)"从一种手工研究手段升级为一条端到端自动化流水线:针对你指定的任意 Clojure 命名空间,先在 REPL 中生成基线变异测试报告,再把带有存活变异(surviving mutation)的函数按覆盖关系分组,逐组让 Claude 自动补写测试、在 nREPL 里回测验证、迭代重试,最后自动完成建分支、提交、推送、创建 Linear Issue 与 GitHub draft PR 的全过程。读完本文,你将掌握该 Skill 的完整调用协议、底层两个 Clojure 引擎(变异/覆盖引擎 dev/src/dev/coverage.clj 与 流水线编排 dev/src/dev/mutation_testing.clj)的实现原理、失败后的手工接管姿势,以及如何用gh/claude/Linear 三大外部设施打通"测试即证据"的提交流程。


一、这是什么:一种基于 Agent 的变异测试自动化工作流

变异测试的核心思想非常朴素:把被测源码故意"改坏"(例如把=改成not=、把0换成1、把:asc换成:asc__),然后跑一遍现有测试。凡是改坏后没有任何测试报错的"变异体",就是存活变异(surviving mutation)——它标记着一块"现有测试测不到、将来引入 bug 也不会被发现"的代码路径。

传统上变异测试只作为人工研究手段使用。而本 Skill 的不同之处在于:它定义了一套技能(skill)协议,由 Agent 在已连接的 Metabase 开发 REPL 中执行dev.mutation-testing/run!,将"识别测试缺口 → 编写针对性测试 → 验证变异被杀 → 提交成 PR"这一整套动作全部自动化,并用 Linear 项目管理每个函数对应的测试补齐任务。

从仓库代码结构看,它由三部分组成:

  1. 技能入口:.claude/skills/mutation-testing/SKILL.md —— Agent 读取的"操作手册",规定参数协议、执行步骤、失败处理与配置方法;
  2. 变异引擎:dev/src/dev/coverage.clj(476 行)——实现运行时调用覆盖追踪、基于源码 AST 的变异体生成、逐变异回测与 Markdown 报告输出;
  3. 编排引擎:dev/src/dev/mutation_testing.clj(886 行)——实现 Linear GraphQL 客户端、命名空间解析、函数分组、Claude 提示词构造、ghPR/评论辅助,以及最终的run!主流程。

三者协作的完整链路如下(对应 SKILL.md 中run!的五步描述):先生成基线报告 → 为命名空间建立/复用 Linear project → 按覆盖关系分组函数 → 逐组"建分支 → Claude 写测试 → 验证 → 重试 → 提交推送 → 建 Issue → 建 draft PR" → 打印含 PR 链接的汇总。


二、环境前提:使用前必须满足的四个条件

SKILL 文档列出的前提都是硬性要求,缺一不可:

  • 运行中的 nREPL:必须已连接到 Metabase 开发环境(Clojure 侧所有逻辑都在 REPL 内执行,变异回测依赖进程内的clojure.test与eval);
  • LINEAR_API_KEY环境变量:持有有效 Linear 个人 API key。读取逻辑见 mutation_testing.clj 中的api-key,未设置会直接抛出 "LINEAR_API_KEY environment variable is not set";
  • ghCLI:已通过 GitHub 认证(用于建分支后的 PR 创建、评论与建议修改);
  • claudeCLI:位于PATH上(由编排引擎以claude -p子进程方式调用,见 invoke-claude!)。

从源码看,gh的使用是"按需出现"的:create-draft-pr!执行gh pr create --draft --base <base> --title ... --body ... --label no-backport(mutation_testing.clj),并自动附带no-backport标签,避免这类纯测试 PR 被后续流程误带回旧分支;而代码改动建议则通过 GitHub REST API 以 review comment 形式投递(见add-suggested-change!,mutation_testing.clj)。


三、Invocation:调用参数协议

Skill 的调用方式是通过/mutation-testing指令带上一个 Clojure 命名空间作为位置参数,后面可接两个可选 flag:

/mutation-testing metabase.lib.order-by /mutation-testing metabase.lib.order-by --base-branch release-x.52.x /mutation-testing metabase.lib.order-by --project-id abc-123 /mutation-testing metabase.lib.order-by --base-branch release-x.52.x --project-id abc-123

参数语义:

  • namespace(必需):第一个位置参数,即被测命名空间,例如metabase.lib.order-by;
  • --base-branch(可选):紧跟的下一个参数是基准分支名。默认取set-config!中配置的:base-branch,若未配置则回落到"master"(见 base-branch);
  • --project-id(可选):紧跟的下一个参数是已存在的 Linear project ID。提供时,任务并入该既有 project,跳过新建 project 的步骤(对应run!中对 config 的注入逻辑,mutation_testing.clj)。

需要说明的是,metabase.lib.order-by这类命名空间在当前仓库是真实存在的:源码位于 src/metabase/lib/order_by.cljc,配套测试为 test/metabase/lib/order_by_test.cljc,后者正是本 Skill 的引擎要补齐的测试目标。


四、Step-by-Step 执行流程(SKILL 的五步法)

SKILL 文档要求 Agent 严格按以下顺序推进。

4.1 解析参数

从$ARGUMENTS中提取 namespace、--base-branch与--project-id,只把确实出现的 flag 透传给后续 opts map。

4.2 加载与配置

进入 REPL 后先加载两个引擎命名空间(:reload确保拿到最新代码):

(require '[dev.mutation-testing :as mut-test] :reload) (require '[dev.coverage :as cov] :reload)

首次调用(或 REPL 重启后)必须初始化 Linear team,两步完成:

(mut-test/list-teams!) (mut-test/set-config! {:team-id "<id>"})
  • list-teams!通过 Linear GraphQL 查询teams { nodes { id name key } },返回当前账号可访问的团队列表(mutation_testing.clj);
  • set-config!是一个合并型配置写入(set-config!),它把传入的 mapmerge进一个defonce的 atom 中,支持反复调用追加配置。

配置的必需/可选键位如下(对应 SKILL 文档的 Configuration 一节):

配置键是否必需默认值说明
:team-id每个 REPL 会话需设置一次无Linear 团队 ID,缺省时team-id会抛错(mutation_testing.clj)
:project-id创建 project 后自动写入由run!调create-project-for-namespace!自动设置也可在set-config!中预置以复用既有 project
:base-branch否"master"每个 feature 分支的拉取基线,可用 opts 覆盖

4.3 运行主流程

在 REPL 中执行:

(mut-test/run! '<target-ns>) ;; 或携带可选参数: (mut-test/run! '<target-ns> {:base-branch "release-x.52.x"}) (mut-test/run! '<target-ns> {:project-id "abc-123"}) (mut-test/run! '<target-ns> {:base-branch "release-x.52.x" :project-id "abc-123"})

如果命令行带了对应 flag,就把:base-branch和/或:project-id放进 opts map 一并传入。

run!(主实现见 mutation_testing.clj)内部实际完成:

  1. 脏工作区快速失败:先用git status --porcelain -uno检查未提交改动,有则直接抛错(run!与create-branch!都做了此检查),保证后续checkout不会误伤工作内容;
  2. 生成基线变异测试报告:调用coverage/generate-report把结果写入mutation-testing-report.<target-ns>.before.md;
  3. 创建或复用 Linear project:若 config 中已有:project-id则直接复用并打印提示,否则调用create-project-for-namespace!新建(并把 project 状态置为 In Progress);
  4. 分组函数:基于coverage/test-namespace的覆盖结果调用group-functions;
  5. 逐组流水线:对每个分组执行process-group!——建分支 → 调用 Claude 写测试 →verify-and-retry!验证 → 必要时重试 → commit & push → 创建 Linear Issue → 创建 draft PR → 切回基准分支;
  6. 打印汇总:通过print-summary!输出 project 名、组数、成功/失败 PR 数、新增测试数、被杀变异数与 PR 链接列表。

值得注意run!的容错设计:单个分组中途抛异常时会被try/catch接住,错误堆栈写入mutation-testing-error-<fn>.log文件,然后尽力return-to-base!恢复现场,再继续处理下一个分组——这正是 SKILL 文档"失败不中断整轮"的依据(run!)。

4.4 失败处理:手工接管单个分组

如果某组在批量流程中失败,可先取得解析后的命名空间信息与覆盖数据,再只处理目标组:

;; 获取解析后的命名空间信息 (def parsed (mut-test/parse-namespace '<target-ns>)) ;; 运行覆盖,拿到数据 (def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)])) ;; 分组,找到你要的那一组 (def groups (mut-test/group-functions coverage-results)) ;; 只处理指定分组 (mut-test/process-group! parsed (nth groups <index>))

process-group!(mutation_testing.clj)是"一键式"单元入口,内部按「建分支 → 统计测试基数 → 构造提示词调 Claude → 验证与重试 → 提交推送 → 建 Issue → 建 draft PR → 返回基准分支」八步执行。

4.5 Dry Run:预检 Claude 将看到的提示词

不真正调用 Claude,直接打印要发送的提示词内容,适合先人工核对提示词的完整性与正确性:

(def parsed (mut-test/parse-namespace '<target-ns>)) (def coverage-results (cov/test-namespace (:target-ns parsed) [(:test-ns parsed)])) (def groups (mut-test/group-functions coverage-results)) (println (mut-test/build-test-prompt (merge (select-keys parsed [:target-ns :test-ns :source-path :test-path]) (select-keys (first groups) [:fn-names :mutations]))))

build-test-prompt(mutation_testing.clj)拼装的提示词包含四块内容:被测函数源码、现有测试文件全文、待杀存活变异清单(每个变异含描述与具体代码 diff),以及给 Claude 的约束(沿用既有测试风格、不得直接调私有函数、测试插到相近位置而非文件末尾、用 Edit 工具修改test-path、写完用clj-nrepl-eval加载测试命名空间等)。


五、底层原理之一:coverage.clj 的变异引擎

dev/src/dev/coverage.clj 是本流水线的"度量大脑"。若想真正理解"存活变异"是怎么算出来的,需要分别看懂它的三个机制。

5.1 运行时调用覆盖:动态包一层"记账"函数

ns-coverage(coverage.clj)的思路是临时替换目标命名空间里的每个函数,统计"哪个测试执行了哪个函数":

  1. 对被测命名空间执行require :reload;
  2. 遍历ns-interns得到所有 intern,凡值是fn?或MultiFn的 var,保存原函数到original-fns,再用alter-var-root替换为带记账的包装函数——包装函数在执行时会记录当前正在跑的测试名(coverage.clj);
  3. 依次运行测试命名空间里所有带:test元数据的 var,期间current-test原子始终指向正在执行的测试;
  4. 跑完后把原函数还原到所有 var 上;
  5. 返回[function, test]二元组集合、覆盖映射、以及uncovered-fns(从未被任何测试执行到的函数)。

从源码结构看,这一步同时覆盖 public 与 private 函数,为后续"私有函数挂靠公共函数"的分组逻辑提供了数据基础。

5.2 变异体生成:从源码文本解析 AST 并按规则改写

generate-mutations+walk-and-mutate(coverage.clj)基于 rewrite-clj 的 zipper 在函数源码的 AST上逐节点生成变异。每个 AST 节点按类型触发不同策略:

  • 符号(symbol):先查mutation-rules表做配对替换,同时总是追加"替换为nil"这一变异。规则表(mutation-rules)覆盖了经典的算子对偶:and ↔ or、<= ↔ <、>= ↔ >、= ↔ not=、+ ↔ -、* ↔ /、inc ↔ dec、when ↔ when-not、if ↔ if-not、do → comment、let → comment、empty? ↔ seq、nil? ↔ some?、boolean ↔ not、for → doseq、true ↔ false等;
  • 关键字(keyword):将:id一类关键字改写为:id__(schema-keyword?会排除 schema 专有关键字,:else也被跳过);
  • 数字(number):0 → 1,其它数字一律→ 0。

函数源码定位依赖 var 元数据中的:file与:line(find-function-source),再通过read-function-from-file结合行号用 zipper 精确定位到单个defn/defn-/defmethod或诸如mu/defn、s/defn等带前缀的 defn 宏(coverage.clj),从而只对该函数体生成变异而不误伤同文件的其它定义。

5.3 逐变异回测:eval 变异体、跑测试、统计 kill-rate

test-mutations(coverage.clj)是变异判定的核心循环:

  1. 对每个变异体,在目标命名空间绑定*ns*后执行(eval (read-string (:mutation mutation))),把源码中改写后的表达式重新 eval 成对 var 的替换;
  2. 顺序运行该函数关联的测试,一旦某个测试失败/报错立即判定为:killed并提前终止(reduced风格的短路循环);
  3. 若变异体导致编译/运行时异常,也直接记为:killed(因为这种代码改动会让测试套件崩溃,同样"无法存活");
  4. finally块中还原原函数与 var 元数据,确保下一个变异体在干净状态下开始。

单函数判定结果形如{:killed [...] :survived [...] :original-source ...}。而test-results(coverage.clj)通过 bindclojure.test/report拦截:fail/:error事件来判定单测是否"击杀"变异,generate-report(coverage.clj)则把全部结果渲染成三段的 Markdown 报告:Uncovered Functions(从未执行)、Partially Covered Functions(有存活变异,每个变异附代码片段)、Fully Covered Functions。


六、底层原理之二:mutation_testing.clj 的编排与外部集成

dev/src/dev/mutation_testing.clj 负责把引擎结果变成"工程现实"。

6.1 命名空间解析与报告路径推导

parse-namespace(mutation_testing.clj)把一个命名空间符号展开为全部派生信息:测试命名空间名(追加-test)、短名、.cljc/.clj源路径(自动探测文件是否真实存在,探测前缀依次为src/、enterprise/backend/src/与test/、enterprise/backend/test/,这正好对应仓库中 OSS 与 EE 代码的目录布局),并约定报告文件名为mutation-testing-report.<target-ns>.before.md。

6.2 函数分组:私有函数挂靠覆盖重叠最大的公共函数

group-functions(mutation_testing.clj)实现了一套启发式分组策略:

  • 有存活变异的public 函数各自成组,成为该组的primary-fn;
  • private 函数被指派给与之"测试重叠数最大"的 public 函数(对每个 private 函数,按set/intersection计算其测试集合与各 public 函数测试集合的交集大小,取交集最大者);一组内会合并所有成员的存活变异与测试集合;
  • 无存活变异的函数被直接跳过;
  • 未被指派到任何 public 函数的 private 函数,单独自成一组。

这样每个分组恰好对应一条可独立提 PR 的工作单元,测试改动互相隔离。

6.3 Linear 集成:GraphQL 直连

全套 Linear 操作走 GraphQLhttps://api.linear.app/graphql(graphql-request,mutation_testing.clj),响应中的errors会被转为异常抛出。主要能力:

  • list-teams!:列出团队;
  • create-project!:创建 project,支持:name、:description(≤255 字符的摘要)与:content(长文 Markdown,默认填入含基线统计表与流程说明的正文,见project-content),创建成功后自动把新 project-id 写回 config(mutation_testing.clj);
  • create-issue!:在配置的 team + project 下建 issue,返回:identifier(形如QUE-1234)与:url;
  • create-project-for-namespace!:读取基线报告的统计(report-stats解析## Uncovered / Partially / Fully Covered Functions各段标题数与#### Mutation:行数),生成 project 名Mutation Testing: <ns>、统计摘要描述与 Markdown 正文,并把 project 状态置为 In Progress(状态 ID 在源码中为"29777ad8-950c-4c88-8e18-89a87dfc880f")。

6.4 GitHub 集成:draft PR、评论与建议修改

  • create-branch!(mutation_testing.clj):校验无未提交改动后,checkout基准分支 →pull→checkout -b新分支。分支名规则见branch-name,例如为metabase.lib.order-by/orderable-columns生成mutation-testing-lib-order-by-orderable-columns;
  • commit-and-push!:git add指定文件后以[Mutation Testing] Add tests for <ns>/<fn>提交并push -u origin;
  • create-draft-pr!:PR 标题形如[Mutation Testing] Kill mutations in metabase.lib.order-by/orderable-columns;正文由pr-description生成,包含「Closes 、覆盖函数清单、Stat(存活变异数/新增测试数/本次击杀数/剩余数)、被击杀变异列表、未击杀变异及其 rationale、可选 suggested changes」等结构化章节;
  • add-suggested-change!:对 PR 中某文件行区间以 GitHub 建议语法投递可一键应用的代码建议;
  • add-pr-comment!:向 PR 追加普通评论。

6.5 验证重试与 Claude 提示词

verify-and-retry!(mutation_testing.clj)先重新枚举测试命名空间里带:test元数据的所有 var,再对组内每个函数执行coverage/test-mutations;若仍有存活变异且重试次数未耗尽,就构造"仍存活变异 + 指令(用 Edit 补充测试、不重复已有测试)"的追加提示词再调一次 Claude,默认:max-retries为 2。测试前后计数差值即该 PR 的"新增测试数"。Claude 子进程调用使用受限工具白名单Edit,Read,Bash(clj-nrepl-eval*),保证它只能编辑测试文件并做 REPL 编译校验,无法越权执行任意命令。


七、产出物形态:PR / Issue / 报告三者如何对应

一次成功的命名空间级run!会形成一条可追溯的"证据链":

  • 一个 Linear project:以命名空间为单位的收口容器,正文内嵌基线报告统计表与处理流程;
  • 每个分组一个 Linear issue:标题形如Mutation testing: metabase.lib.order-by/orderable-columns,描述解释了存活变异为何危险("代码可以在测试不失败的情况下被改动,意味着那些路径上的 bug 不会被发现");
  • 每个分组一个 GitHub draft PR:标题、函数清单、变异击杀统计、未击杀项 rationale 一应俱全,且 PR 描述内Closes <issue identifier>自动联动 Linear 与 PR;
  • 一个基线 Markdown 报告文件:mutation-testing-report.<target-ns>.before.md,作为后续对比的基线。

值得一提的是引擎还提供了不需要建分支/PR 的轻量入口run-one!(mutation_testing.clj):给定全限定函数符号与测试命名空间,只做"生成报告 → 建提示词 → 调 Claude → 验证重试",适合在 REPL 里快速验证单个函数的变异击杀情况,不必污染 git 历史。


八、在仓库中进一步探索的路线

若想深入这套变异测试流水线,推荐按以下相对路径跟进:

  • 技能协议:.claude/skills/mutation-testing/SKILL.md —— 本文的原始依据;
  • 变异引擎:dev/src/dev/coverage.clj(变异规则表)、coverage.clj(变异生成)、coverage.clj(回测判定);
  • 编排引擎:dev/src/dev/mutation_testing.clj(解析与分组)、mutation_testing.clj(提示词与重试)、mutation_testing.clj(分组处理与主入口);
  • 可被实际变异的目标示例:库层查询构建代码 src/metabase/lib/order_by.cljc 及其测试 test/metabase/lib/order_by_test.cljc;
  • 整个 dev 工具家族:dev/src/dev/ 目录下还聚集了debug.clj、debug_qp.clj、malli.clj、memory.clj等面向 Metabase Clojure 开发的辅助命名空间,可作为理解本项目开发工具链的入口。

需要留意的是,该 Skill 依赖的外部服务(Linear、GitHub、claudeCLI)与 nREPL 环境都属于 Metabase 开发团队的本地/CI 设施;在其它仓库复用时,只需保持coverage.clj的 API 契约不变,即可把这套"测一处、杀变异、成 PR、记 Issue"的闭环原样迁移到自己的 Clojure 项目中。


九、总结:这套自动化真正解决的问题

把 SKILL 文档与其底层实现对照后可以清晰看到,变异测试自动化的价值不在于"多跑了几次测试",而在于它把测试缺口从模糊的直觉变成了可追踪、可消化的工程任务:

  1. 覆盖追踪告诉我们每个函数被哪些测试碰过、哪些函数完全没被碰过;
  2. 变异回测把"覆盖率 100% 但质量不足"的假象撕开——存活变异就是一份可直接指导补测的 TODO 清单;
  3. 函数分组让每个 PR 聚焦一组语义相关的函数与变异,代码评审者可以逐组核对"哪些变异被合理击杀、哪些变异因为语义(如默认值从不被实际使用)被判定为不可杀并记录 rationale";
  4. 自动化生成 Linear project/issue 与 draft PR,让测试补齐工作像普通开发任务一样可排期、可追踪、可合并。

对任何一个重视 Clojure 代码质量与回归风险的团队而言,这套流水线把"变异测试"从一次性研究活动转变成了可持续运行的质量闭环——这恰恰是当前 Metabase 仓库 dev/src/dev/coverage.clj 与 dev/src/dev/mutation_testing.clj 这两份 1300 余行源码所承载的工程意图。

【免费下载链接】metabaseThe easy-to-use open source Business Intelligence and Embedded Analytics tool that lets everyone work with data :bar_chart:项目地址: https://gitcode.com/GitHub_Trending/me/metabase

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询