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

发布时间:2026/9/10 20:33:16
Metabase 变异测试自动化 Skill:用 nREPL、Claude CLI 与 Linear/GitHub 流水线绞杀存活变异 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 项目管理每个函数对应的测试补齐任务。从仓库代码结构看它由三部分组成技能入口.claude/skills/mutation-testing/SKILL.md —— Agent 读取的操作手册规定参数协议、执行步骤、失败处理与配置方法变异引擎dev/src/dev/coverage.clj476 行——实现运行时调用覆盖追踪、基于源码 AST 的变异体生成、逐变异回测与 Markdown 报告输出编排引擎dev/src/dev/mutation_testing.clj886 行——实现 Linear GraphQL 客户端、命名空间解析、函数分组、Claude 提示词构造、ghPR/评论辅助以及最终的run!主流程。三者协作的完整链路如下对应 SKILL.md 中run!的五步描述先生成基线报告 → 为命名空间建立/复用 Linear project → 按覆盖关系分组函数 → 逐组建分支 → Claude 写测试 → 验证 → 重试 → 提交推送 → 建 Issue → 建 draft PR → 打印含 PR 链接的汇总。二、环境前提使用前必须满足的四个条件SKILL 文档列出的前提都是硬性要求缺一不可运行中的 nREPL必须已连接到 Metabase 开发环境Clojure 侧所有逻辑都在 REPL 内执行变异回测依赖进程内的clojure.test与evalLINEAR_API_KEY环境变量持有有效 Linear 个人 API key。读取逻辑见 mutation_testing.clj 中的api-key未设置会直接抛出 LINEAR_API_KEY environment variable is not setghCLI已通过 GitHub 认证用于建分支后的 PR 创建、评论与建议修改claudeCLI位于PATH上由编排引擎以claude -p子进程方式调用见 invoke-claude!。从源码看gh的使用是按需出现的create-draft-pr!执行gh pr create --draft --base base --title ... --body ... --label no-backportmutation_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.cljset-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内部实际完成脏工作区快速失败先用git status --porcelain -uno检查未提交改动有则直接抛错run!与create-branch!都做了此检查保证后续checkout不会误伤工作内容生成基线变异测试报告调用coverage/generate-report把结果写入mutation-testing-report.target-ns.before.md创建或复用 Linear project若 config 中已有:project-id则直接复用并打印提示否则调用create-project-for-namespace!新建并把 project 状态置为 In Progress分组函数基于coverage/test-namespace的覆盖结果调用group-functions逐组流水线对每个分组执行process-group!——建分支 → 调用 Claude 写测试 →verify-and-retry!验证 → 必要时重试 → commit push → 创建 Linear Issue → 创建 draft PR → 切回基准分支打印汇总通过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-promptmutation_testing.clj拼装的提示词包含四块内容被测函数源码、现有测试文件全文、待杀存活变异清单每个变异含描述与具体代码 diff以及给 Claude 的约束沿用既有测试风格、不得直接调私有函数、测试插到相近位置而非文件末尾、用 Edit 工具修改test-path、写完用clj-nrepl-eval加载测试命名空间等。五、底层原理之一coverage.clj 的变异引擎dev/src/dev/coverage.clj 是本流水线的度量大脑。若想真正理解存活变异是怎么算出来的需要分别看懂它的三个机制。5.1 运行时调用覆盖动态包一层记账函数ns-coveragecoverage.clj的思路是临时替换目标命名空间里的每个函数统计哪个测试执行了哪个函数对被测命名空间执行require :reload遍历ns-interns得到所有 intern凡值是fn?或MultiFn的 var保存原函数到original-fns再用alter-var-root替换为带记账的包装函数——包装函数在执行时会记录当前正在跑的测试名coverage.clj依次运行测试命名空间里所有带:test元数据的 var期间current-test原子始终指向正在执行的测试跑完后把原函数还原到所有 var 上返回[function, test]二元组集合、覆盖映射、以及uncovered-fns从未被任何测试执行到的函数。从源码结构看这一步同时覆盖 public 与 private 函数为后续私有函数挂靠公共函数的分组逻辑提供了数据基础。5.2 变异体生成从源码文本解析 AST 并按规则改写generate-mutationswalk-and-mutatecoverage.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也被跳过数字number0 → 1其它数字一律→ 0。函数源码定位依赖 var 元数据中的:file与:linefind-function-source再通过read-function-from-file结合行号用 zipper 精确定位到单个defn/defn-/defmethod或诸如mu/defn、s/defn等带前缀的 defn 宏coverage.clj从而只对该函数体生成变异而不误伤同文件的其它定义。5.3 逐变异回测eval 变异体、跑测试、统计 kill-ratetest-mutationscoverage.clj是变异判定的核心循环对每个变异体在目标命名空间绑定*ns*后执行(eval (read-string (:mutation mutation)))把源码中改写后的表达式重新 eval 成对 var 的替换顺序运行该函数关联的测试一旦某个测试失败/报错立即判定为:killed并提前终止reduced风格的短路循环若变异体导致编译/运行时异常也直接记为:killed因为这种代码改动会让测试套件崩溃同样无法存活finally块中还原原函数与 var 元数据确保下一个变异体在干净状态下开始。单函数判定结果形如{:killed [...] :survived [...] :original-source ...}。而test-resultscoverage.clj通过 bindclojure.test/report拦截:fail/:error事件来判定单测是否击杀变异generate-reportcoverage.clj则把全部结果渲染成三段的 Markdown 报告Uncovered Functions从未执行、Partially Covered Functions有存活变异每个变异附代码片段、Fully Covered Functions。六、底层原理之二mutation_testing.clj 的编排与外部集成dev/src/dev/mutation_testing.clj 负责把引擎结果变成工程现实。6.1 命名空间解析与报告路径推导parse-namespacemutation_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-functionsmutation_testing.clj实现了一套启发式分组策略有存活变异的public 函数各自成组成为该组的primary-fnprivate 函数被指派给与之测试重叠数最大的 public 函数对每个 private 函数按set/intersection计算其测试集合与各 public 函数测试集合的交集大小取交集最大者一组内会合并所有成员的存活变异与测试集合无存活变异的函数被直接跳过未被指派到任何 public 函数的 private 函数单独自成一组。这样每个分组恰好对应一条可独立提 PR 的工作单元测试改动互相隔离。6.3 Linear 集成GraphQL 直连全套 Linear 操作走 GraphQLhttps://api.linear.app/graphqlgraphql-requestmutation_testing.clj响应中的errors会被转为异常抛出。主要能力list-teams!列出团队create-project!创建 project支持:name、:description≤255 字符的摘要与:content长文 Markdown默认填入含基线统计表与流程说明的正文见project-content创建成功后自动把新 project-id 写回 configmutation_testing.cljcreate-issue!在配置的 team project 下建 issue返回:identifier形如QUE-1234与:urlcreate-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-columnscommit-and-push!git add指定文件后以[Mutation Testing] Add tests for ns/fn提交并push -u origincreate-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 文档与其底层实现对照后可以清晰看到变异测试自动化的价值不在于多跑了几次测试而在于它把测试缺口从模糊的直觉变成了可追踪、可消化的工程任务覆盖追踪告诉我们每个函数被哪些测试碰过、哪些函数完全没被碰过变异回测把覆盖率 100% 但质量不足的假象撕开——存活变异就是一份可直接指导补测的 TODO 清单函数分组让每个 PR 聚焦一组语义相关的函数与变异代码评审者可以逐组核对哪些变异被合理击杀、哪些变异因为语义如默认值从不被实际使用被判定为不可杀并记录 rationale自动化生成 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),仅供参考