AI 自动化单元测试技术方案

1. 背景与问题

  1. 数据契约类回归无人兜底:维度映射表里一处重复 key、漏注册,构建不报错、页面不一定立刻出错,直到线上才暴露。

  2. 测试与需求对不上:即使补了测试,也难以回答「这个需求测没测、改动有没有跑偏、有没有夹带无关改动」------测试、代码、需求之间没有可机器校验的挂钩。

    本方案只解决「有没有测、测的和需求对不对得上、改动有没有超出需求」

2. 目标

目标

  • G1 单测地基:落地一套可运行的 vitest 单测环境

  • G2 开发流程约束:用 superpowers 开发 feat 需求时,强制先提供需求文档、按 TDD 写带标签的单测。

  • G3 MR 需求对齐门禁 :提 MR 到 dev / uat / master 时,对 feat/* 源分支自动跑单测,并由 AI 比对「需求文档 × 代码 diff × 测试报告」,合并成一条 MR 评论输出。

3. 总体架构

两段约束,共享「分支名 → TAPD ID」寻址:

  • 开发阶段(本地,G2) :superpowers 流程里强制先有需求文档、按 TDD 写带 [TAPD-ID] 标签的单测。靠项目 CLAUDE.md 规则引导,Claude Code hook 卡死跳步。

  • MR 阶段(CI,G3) :门禁在单 job 内按「准备 → 审查 → 发布」三层执行:verify.mjs 确定性收集数据,verify-spec skill 做 AI 比对,发布层发一条 MR 评论。机制与团队已有的 merge-request-review 同构。

    设计要点:

  • 一个 ID 串起全链路 :分支名 提取 TAPD ID,需求文档按 ID 存(.superpowers/sdd/{ID}.md),测试按 ID 打标。需求 / 测试 / 改动之间无歧义。

  • 确定性优先,LLM 兜底:能算出来的(分支过滤、diff、跑测试、指纹去重、缺需求的软提醒)全部由脚本完成;LLM 只做「读懂需求、比对改动」这一件只有它能做的事。

  • 一个门禁,一条评论:先跑真实 vitest(保留抓数据契约类 bug 的能力),再让 AI 读需求 + diff + 测试报告,合并成一条评论。

4. 开发流程约束(superpowers)

不改 superpowers 插件本身(插件升级会覆盖),在 项目侧叠加约束。superpowers 的 skill 明确「用户偏好覆盖默认行为」,项目 CLAUDE.md 的规则会被遵循。

约束 规则 落点
先有需求文档 feat 分支开工时,若 .superpowers/sdd/{TAPD_ID}.md 不存在,先让用户粘贴需求文本/截图并存入该路径,再写代码 CLAUDE.md 规则 + PreToolUse hook
写代码配单测 走 superpowers 的 test-driven-development,先写失败测试;测试用 describe('[TAPD-{ID}] ...') 打标,就近放在被测文件旁 CLAUDE.md 规则 + Stop hook

两个 hook 注册在项目 .claude/settings.json,脚本在 .claude/hooks/,随仓库提交,团队拉代码后重开会话即生效。仅对带 TAPD ID 的 feat/* 分支生效,hotfix / 工具分支放行:

hook 拦截条件 放行情况
PreToolUse(Edit / Write / MultiEdit) 要改 src/ 下的业务源码,且 .superpowers/sdd/{ID}.md 不存在 → exit 2,提示先提供需求 测试文件、子模块、src/ 以外的文件(包括需求文档本身)
Stop 本分支(已提交 + 工作区 + untracked)改了 src/ 业务源码,且 src/ 下没有带 [TAPD-ID] 标签的测试 → decision: block,要求补单测 已有带标签测试(含未提交的新测试文件);stop_hook_active 为 true 时放行,避免死循环
  • 选 Stop 而非每次改代码前检查,是为了不在写第一个测试文件之前就反复拦截。

  • hook 脚本自身出错(读不到输入、git 不可用等)一律放行,不影响会话。

  • 业务源码、测试文件的判定与门禁共用 lib.mjs,规则只有一份。

    hook 只管 Claude Code 会话;开发者手写代码绕过时,由 MR 门禁兜底(缺需求发软提醒,缺测试在「测试场景覆盖」判 ✗)。

5. MR 门禁执行流程

5.1 触发条件

MR 且目标分支 ∈ {dev, uat, master},源分支为 feat/*。hotfix 及其他分支一律不生成评论。

5.2 执行时机:应对频繁合并

门禁结论只由「feat 分支代码 + 需求文档」决定,与目标分支、合并次数无关。同一个 feat 分支通常会依次合 DEV、UAT、master,联调期还会反复合 DEV;每个 MR 都跑一次,就是同一份内容被重复分析(参考 mr_review 实测,一次约 $2.5、5 分钟)。参考「幂等批次号 + fail-fast 漏斗」的做法,执行时机由四个机制控制:

机制 做法
前置漏斗(零 LLM 成本) 依次检查:非 feat/* 或无 TAPD ID → 跳过;diff 只涉及文档 / 锁文件 / CI 配置 → 跳过;需求文档缺失 → 脚本直接输出软提醒模板,不调 LLM
内容指纹去重 指纹 = TAPD ID + 源分支 HEAD SHA + 需求文档 hash。已跑过的指纹不再调 LLM,评论只写「复用 !xxxx 的结果」并附链接;需求文档一改,hash 变化自动重跑
手动重跑入口 MR 页手动 job(或评论命令),开发者需要时自己触发,不依赖自动时机(随 CI 接入一起配置,见 §6.4)
取消过期任务 同一 MR 有新提交时,job 设 interruptible,取消仍在跑的旧任务(随 CI 接入一起配置,见 §6.4)

指纹存放 :不引入额外存储。评论末尾写隐藏标记 <!-- verify-spec:fp=... -->,job 用 GitLab API 查同源分支其他 MR 的评论有无该标记(GITLAB_MR_REVIEW_TOKEN 已具备 api 权限)。当前 MR 自己的评论不算;缺少 CI 上下文、接口报错或超时(默认 10 秒)时不复用,照常跑 AI 比对。

效果:EAD 不变时合 DEV、UAT、master 三次只调用一次 LLM,另两次只发复用链接;只有代码或需求文档真的变了才重跑。

webhook 侧配合:只在 MR open 和「有新提交的 update」时触发,改标题、加标签、approve 不触发(由触发配置方实现)。

6. 组件详解

6.1 vitest 地基(G1)

配置 取值 为什么
vitest 0.34.6 与 Volta 锁定的 Node 16.20.2 兼容(vitest 0.34.x 基于 vite4,支持 node16)
覆盖率 @vitest/coverage-v8 0.34.6 与 vitest 同版本,v8 原生覆盖率
浏览器环境 happy-dom ^21.0.4 Vue 2.7 import 链会拉入 window.localStorage / navigator.userAgent;用文件级 // @vitest-environment happy-dom 按需开启
默认 environment node 纯逻辑/数据契约测试不需要 DOM,默认省开销
include src/**/*.{test,spec}.{ts,js} 测试就近放在业务代码旁
alias @→src、global→submodule 对齐业务代码的模块解析

npm scripts :test(vitest run)、test:watch(vitest)、test:cov(vitest run --coverage)。

6.2 数据收集脚本 verify.mjs(准备层)

确定性收集数据,并决定要不要调 AI。恒 exit 0,不做任何阻断:

  1. 取源分支:优先 MR_SOURCE_BRANCH(CI 下 checkout 是 detached HEAD,git branch 取不到),本地回退到当前分支;目标分支取 MR_TARGET_BRANCH,本地为空时不按目标过滤。

  2. 从分支名提取 TAPD ID(\b(\d{7,})\b),读取需求文档 .superpowers/sdd/{ID}.md(兼容历史位置 .claude/specs/{ID}.md)。

  3. 走前置漏斗、计算内容指纹、查复用(§5.2),得出 action。

  4. 仅 review 时才收集重数据:三点 diff(排除锁文件/快照)、跑 vitest run -t "TAPD-{ID}" --reporter=json(stdout/stderr 全吞、只读 outputFile、运行前删除残留报告)。

    输出契约:

action 含义 附带字段
skip 不在门禁范围,不发评论 reason
soft 缺需求文档 comment(脚本生成的软提醒,不调 LLM)
reuse 同指纹已在其他 MR 验证过 comment(复用链接 + 指纹标记,不调 LLM)
review 交 skill 做 AI 比对 spec、testReport、gitDiff、changedFiles、relevanceExcluded、fingerprint

relevanceExcluded 列出不参与无关变更判断的文件:需求文档本身、带本需求标签的测试、子模块指针(从 .gitmodules 读取)。

前置漏斗里「只涉及文档 / 锁文件 / CI 配置」的判定范围:*.md、锁文件、*.snap、.gitlab-ci.yml,以及 docs/、.claude/、.superpowers/、.husky/、.vscode/ 目录。

三点 diff 是正确性关键 :用 origin/master...HEAD(merge-base)而非两点 origin/master。两点 diff 在 master 领先本分支时会把 master 独有的提交误算成本次改动(实测曾出现 291 文件 vs 实际 13 文件的失真),无关变更判断会彻底失效。

6.3 verify-spec skill(审查层)

设计原则
原则 含义
纯产出 CI 无人值守,不询问、不等输入,只产出一段 MR 评论正文
判定与执行分离 数据由 verify.mjs 确定性收集,skill 只做语义比对
只对前端下结论 需求提到但前端 diff 没有的点,只列为「前端未涉及」中性信息,不判 ⚠/✗(前端 diff 看不到后端实现)
结论可核实 每条判定都引用具体位置(文件:行号)与对应需求点,禁止「部分改动与需求无关」这类笼统表述
不阻断 给出判定但不拦合并;找不到需求文档只发软提醒
判断标准
维度 比对 ✓ ⚠ ✗
测试场景覆盖 需求要求的场景 ↔ 测试报告的 describe + it 关键场景都有测试且 Pass 部分场景缺测试,或测试报告不可用 核心功能点完全没有测试
需求对齐 需求描述 ↔ 代码 diff 实现与需求一致 基本符合但有细节差异 与需求明显矛盾(逻辑写反、做成了别的功能)
无关变更 每个 diff hunk ↔ 需求点 所有改动都能追溯到需求 存在无法追溯到任何需求点的改动 ---

无关变更按 hunk 判,不按文件判 :无关改动常藏在相关文件里(如在 data.ts 改需求维度时顺手重构了另一个函数),只看文件名永远查不出。每段改动归入三类:

  • 直接实现:能对上需求里的具体某一条 → 相关

  • 必要支撑:实现需求离不开的类型定义、i18n 文案、被调用的公共函数、必要配置 → 相关

  • 无法追溯:对不上任何需求点,也不是哪条需求的前置(顺手重构、动了别的功能、残留调试代码)→ ⚠ 疑似无关

    无关变更只给 ⚠ 不给 ✗:重构、顺手修复天然是灰区,可能是有意为之,AI 分辨不了,只提示「请确认是否有意为之」。

    结论:三项全 ✓ 为通过;有 ⚠ 无 ✗ 为待关注;有任何 ✗ 为严重偏离。「前端未涉及」不影响结论。

输出结构

找到需求文档时,评论按固定模板输出:

  1. 测试场景覆盖:判定 + 一句话理由 + 缺失场景

  2. 需求对齐:判定 + 一句话理由 + 偏离点 + 前端未涉及的需求点

  3. 无关变更:判定 + 每条疑似无关改动的 文件:行号范围、改了什么、为什么对不上需求

  4. 验证结论:通过 / 待关注 / 严重偏离,注明「仅作提示,不阻断合并」

    评论末尾附隐藏指纹标记(见 §5.2)。skill 先看 action:skip 不产出正文;soft / reuse 原样输出脚本给的 comment,不做比对;只有 review 才按上面的模板产出报告。

相关推荐
Shadow(⊙o⊙)43 分钟前
仿muduo实现高并发服务器——EventLoop.hpp初步设计(未纳入超时管理模块)
运维·服务器
nlp研究牲1 小时前
Linux 常用命令速查手册:英文全称、中文释义、巧记方法与实战示例
linux·运维·php
众壹新能源科技1 小时前
逆变器半夜停机等备件48小时少发10万度电,备件安全库存怎么算
大数据·运维·安全
广州智造1 小时前
Radioss破坏模型FAILJOHNSON (Johnson-cook) 和 FAILTAB1
运维·数据库·自动化·仿真·建模·后处理·前处理
味悲1 小时前
Nginx笔记
linux·运维·笔记·nginx
电商全栈程序猿1 小时前
2026 开源商城系统选型指南:开发者实测对比Tigshop、ShopXO、CRMEB
java·运维·开源
时间售卖铺1 小时前
MySQL 库表设计:数据库范式详解
运维·数据库·mysql·云原生
Lucky_Turtle2 小时前
【Linux】ip命令,路由表相关配置
linux·运维·tcp/ip
紫金桥软件2 小时前
紫金桥国产组态软件:安全稳定护航工业现场
数据库·自动化·跨平台·scada·组态软件