覆盖率任务收官报告(2026-09)
任务:把 Herald 全仓测试覆盖率提升到 100%(行覆盖 + 分支覆盖双满),以测试工具实测为准,禁止凑数的假覆盖;确实不可达的分支如实记录原因;CI 门禁同步抬到实测水位。全程只提交推送 main,无任何发版动作。
方法与过程详见覆盖率攻坚总结(2026-09),口径定义与残余零块清单见测试覆盖率口径。本文是任务链的最终状态记录。
最终结果
| 项目 | 值 |
|---|---|
| Go 语句覆盖率(go-test 口径原始值) | 99.64%(5012/5030 语句,唯一块聚合口径) |
Go 语句覆盖率(门禁口径:合并三个 main() 子进程实测后) | 99.78%(5019/5030。期 5 收官时为 99.7%,其后规则/群组等功能代码与台账条目增长摊低;2026-09-27 三处登记(main.go:556、client.go:197、handler_groups.go:71/:88)经 seam 升格实测,回到并越过原水位) |
| Go CI 门禁 | tools/zero_check.py --gate 100:排除台账登记块后等效语句覆盖率 100%,且无未定性零块;生效且通过 |
| Go 残余零块 | 台账登记 8 条(详见台账):3 块 os.Exit 失败分支 + 2 块 provider 重复名守卫 + 2 块 gorilla SetWriteDeadline 恒 nil + 1 块 wechatmp 写锁双检命中(调度偶发走到,非零轮次自然不参与豁免)。三处登记(main.go:556、client.go:197、handler_groups.go:71/:88)已先后按 writeControl 先例提为包级 seam、由注入失败用例实测覆盖并移出台账。2026-09-27 以 CI 同流程(go1.26.2)复测,该轮实测零块 7 个、全部命中台账 |
| Go 残余零块状态 | 全部在 KNOWN_UNCOVERABLE.md 逐条登记原因,只剩两类:os.Exit 跳过 GOCOVERDIR 转储(工具链原理性不可测)、下游契约恒常使其不可达(gorilla SetWriteDeadline 恒 nil、调度窗口不可确定性构造)。曾经"补个 seam 就能实测"的一类——Manager.Get 契约守卫(handler_groups.go:71/:88)、两处 SetReadDeadline(main.go:556、client.go:197)——已于 2026-09-27 全部升格实测、清空。该类的教训:缺 seam 是待办,不是死因 |
Dashboard 行覆盖率(vitest + @vitest/coverage-v8) | 100%(457/457;15 个测试文件 143 个用例,2026-09-27 复测全过) |
| Dashboard CI 门禁 | thresholds: { lines: 100, branches: 100 }——期 4 时仅 lines: 100,43a85ab 起行 + 分支双阈值,生效且通过(ci.yml dashboard job) |
| Dashboard 分支覆盖率 | 100%(分支 260/260;语句 482/482、函数 149/149 同满,四项均无任何台账豁免。期 4 收官时记为 86.6%,经 95% → 97.69% 爬坡后由 43a85ab 补齐最后一轮,逐分支定性见口径文档) |
| CI | 43a85ab 全部 job 全绿(Lint / Test / Dashboard / Docker Build / Build + Docs workflow) |
| 发版动作 | 无(未打 tag、未发 release、未改版本号) |
为什么 Go 侧门禁口径是"排除台账后 100%"而不是字面上的 100%:三个 main() 的成功路径入口块已通过 go build -cover 子进程口径实测,并经 tools/covermerge.py 合并进门禁 profile(合并还带防假绿硬失败:空 dump、入口无实测非零块、工具链漂移一律拒绝);残余块全部在 KNOWN_UNCOVERABLE.md 逐条登记原因(8 条,见上表)——除 os.Exit 跳过 GOCOVERDIR 转储这一类工具链限制外,还有下游契约恒常使其不可达的分支;曾属"需加 seam 才能构造"的三处(main.go:556、client.go:197、handler_groups.go:71/:88)已按先例补 seam 升格实测、移出台账。门禁强制"排除登记块后等效覆盖率 100%"。
提交链
| 提交 | 内容 |
|---|---|
1f7eab9 | 期 1:私有函数直调实测——compileRule 六类字段错误(表驱动)、pkcs7Unpad 非法输入、显式 GroupInterval 编译路径 |
b570ec0 | 期 2+3:防御分支清零(直调/seam 实测、重构消灭、恒 nil 改写)+ 三个 main() 双口径实测 + 门禁 98.7%→99.5% + 口径文档更新 |
c68b4a7 | 修复 CI 暴露的 awaitingQueue.wait select 双就绪竞态(取消优先预检) |
c9e9483 | 零块核对器入库 tools/zero_check.py |
89dd2e5 | 修复 vitepress 死链(docs 内不能链接站点根之外的文件) |
ffa237a | 零块 KNOWN_UNCOVERABLE 登记机制与两处 flake 修复(Go 侧收尾) |
acbe735 | 期 4:dashboard 前端测试从 0 建到行覆盖 100%,CI 门禁同步(含两个生产缺陷修复) |
fb11d07 | 期 5:main() 子进程覆盖合并进门禁(tools/covermerge.py + zero_check --gate 100),残余零块 6 → 3(仅剩 os.Exit 原理性不可测) |
43b8154 | 修复 CI 合并步骤:HERALD_MAIN_COVERDIR 须用绝对路径——go test 以各包目录为测试二进制 cwd,相对路径让 dump 散落到各包目录下,glob 落空硬失败 |
b81bb99 | escalation 重排窗口 100ms→250ms:高负载下 Sleep(40ms) 超调越过 100ms 窗口使「未提前触发」断言误报,容差加到 210ms |
7ea0f75 | 修两处 CI 红:dashboard 卸载泄漏(setup.ts 以 liveRoots 登记挂载根、afterAll 确定性 unmount,替代计时窗冲刷)与台账行号错位。后者根因更深:zero_check.py 曾按「文件:起始行」聚合取 max,单行 if 的条件块与分支体块同起始行,恒非零的条件块把 8 处真零块静默掩盖出门禁——改为与 covermerge 一致的完整区间 (文件, 起, 止) 聚合后缺口现形,台账据此补齐并重锚到 go1.26.2(CI 钉死工具链;本地 go1.27.1 块边界整体 +1,照它抄行号必失配) |
43a85ab | dashboard 分支覆盖 97.69%→100%:上一期定性为「工具口径残余 + 契约防御」的 6 个分支逐个重查,全部实为可测业务路径——toGroupPayload 提为导出纯函数直测、空试用例改 waitFor 真等(not.toBeNull() 对 undefined 也通过的假断言纠正)、schema 真正省略键、无 token 时点保存/测试按钮补 token ? {} : 假支、抓 setInterval 回调直调补上 % Lines 看不见的轮询函数缺口。阈值随之抬到 thresholds: { lines: 100, branches: 100 } |
三期冲刺内容
期 1 —— 私有函数直调实测
包级私有函数不走生产入口也能测:同包测试直接调用,一次覆盖一类错误分支。compileRule 对 for / group_interval / inhibit.ttl / silence.window / silence.match / ack_timeout 六类字段错误的表驱动实测,pkcs7Unpad 的 nil / 空 / 非对齐 / 零填充非法输入实测。
期 2 —— 防御分支清零
原清单几十处防御性零块,三种结局,profile 中不再存在任何防御性零块:
- 直调 / seam 实测:
writeControl改包级变量注入传输失败(真实 dial);evalProgram打桩超时(真实表达式被安全护栏限制,无法自然超时);runProgram取消与超时路径直调。 - 重构消灭死分支:
Manager.Deliver的IsEnabled+GetProvider双锁竞态合并为单锁lookupEnabled;Engine.Put把compileRule前置到Validate之前,编译错误分支从"不可达守卫"变为可实测;runProgram的非 bool 返回值分支改单值断言(不变量破坏必须 panic)。 - 恒 nil 显式接受:
x, _ := f(...)加注释说明契约——gorillaSetWriteDeadline只记录时间戳、可序列化结构的json.Marshal、回退式限流器工厂、base64 密码摘要、SHA-256 摘要上的aes.NewCipher、类型断言式parseConfig。若契约变化,注释要求恢复显式错误处理。
期 3 —— main() 双口径实测
三个入口(cmd/heraldd、examples/quickstart、worker-sdk/go/example)统一模式:
main改写为成功路径return,失败才os.Exit(code);- 各包
main_cover_test.go以go build -cover -coverpkg=./...编译子进程(不加-coverpkg时 main 包本身不被插桩),在GOCOVERDIR下真实运行——heraldd 起 HTTP 服务后 SIGTERM 优雅退出、quickstart 同步派发后自然退出、worker-sdk 示例对ws://localhost:8081完成注册握手后 SIGTERM(端口被占则 skip); - 断言
go tool covdata textfmt转储中main()区间内除os.Exit行(mainExitAllow豁免表)之外每个块计数非零。
期 4 —— dashboard 前端从 0 到行覆盖 100%
前端 dashboard/ 此前没有任何测试。本期引入 vitest + Testing Library + jsdom,建立 13 个测试文件 84 个用例,覆盖 API 层、zustand store、WebSocket hook、全部 7 个页面与应用入口;vitest.config.ts 的 thresholds: { lines: 100 } 进 CI(新增 dashboard job),门禁哲学与 Go 侧一致。关键技术点:
- seam 导出可测面(沿用 Go 侧
evalProgram先例):src/api导出真实 axios 实例,测试注入自定义 adapter 走真实拦截器链(vi.mock('axios')对 CJS externalized 模块的源文件导入不生效,seam 是唯一可靠路径);src/main导出createStaticRenderer工厂,绕开 rc-motion 在 jsdom 里收不到transitionend的动画死路,直接验证 antd 静态方法渲染注入的 render/unmount 两条路径。 - React 19 + antd v5 兼容注入:antd 静态 message 依赖已移除的
ReactDOM.render,unstableSetRender注入createRoot渲染器(生产入口与测试 setup 双处)。 - jsdom 环境补齐:
matchMedia/ResizeObserver/scrollTo/requestAnimationFramepolyfill;antd message 的 portal 容器是全局单例,跨用例只清内容不删容器(删了会往 detached 节点渲染)。 - 残余分支的定性(后被整体推翻):行覆盖 100% 是门禁;当时把分支覆盖 86.6% 的缺口整体定性为
|| '默认文案'兜底支、allowClear清除到空路径与 jsdom 环境性分支(如https:→wss:),登记于口径文档,并宣称「无一是未测的业务路径」。这一结论是错的:2026-09 末逐个重查,残余分支全部是可测的业务路径,其中一条更是把「用例空转」误判成「v8 归属偏差」(not.toBeNull()对undefined也通过,等于零断言)。43a85ab已把缺口全部补齐、无一进台账,逐分支定性与教训见口径文档「前端分支覆盖补齐的最后一轮」。
期 5 —— Go 侧收满:main() 子进程覆盖合并进门禁
期 3 结束时,三个入口的 main() 入口块在 go-test 口径下仍是零块,以"登记豁免"存在。本期把子进程口径的实测计数真正合并进门禁 profile,让入口块不再靠豁免:
- 三个
main_cover_test.go在设了HERALD_MAIN_COVERDIR时持久化 textfmt 转储(不设时行为不变); - 新增
tools/covermerge.py:主 profile 与子进程 dump 按块键同键取 max 合并(同工具链下块集逐字节一致,已实证);四条防假绿硬失败——空 dump 拒并、dump 含主 profile 未知块(工具链/代码漂移)拒并、--expect入口必须 basename 为main.go(按文件名识别入口,不按行号硬编码)、入口在 dump 中必须有非零块(子进程真的执行到了); tools/zero_check.py新增--gate N:等效覆盖率 =(总语句 − 全部零块语句)/(总语句 − 台账登记块语句),登记块从分子分母同时剔除,与台账"排除登记项后 100%"声明同一口径;只做就地注释不进台账的零块会拉低等效值、打红门禁;- CI Test job 串起
HERALD_MAIN_COVERDIR→ covermerge →--gate 100,替换原 99.5% 裸阈值,codecov 上传合并后 profile; - KNOWN_UNCOVERABLE.md 删去 3 个
main()入口块登记(合并后实测非零,不再是"不可覆盖"),只保留 3 个os.Exit失败分支。
效果:合并口径 99.5% → 99.7%,残余零块 6 → 3;门禁从裸数字阈值升级为"无未定性零块 + 排除台账后等效 100%"的结构化断言——绕过合并流程(不做子进程合并)会直接被 --gate 100 打红。
顺带消灭的真问题
冲刺过程中暴露并修复了五处与覆盖率无关的缺陷(Go 三处、前端两处):
runProgram取消竞态:select在「评估完成」与「上下文取消」同时就绪时伪随机择一,canceled context 的评估可能误报成功。修复:进入等待前预检父ctx.Err(),取消路径确定性失败。awaitingQueue.wait同款竞态:任务已交付且 ctx 已取消时select双就绪随机返回,取消的DispatchSync可能误报成功。CI 上被TestRunCanceledContext抓住。修复:同款取消优先预检。evalTimeout过紧:50ms 在高负载下会被调度延迟击穿(CI 上正常表达式评估报超时)。放宽至 500ms,对微秒级的正常评估无感知,护栏(防失控程序)语义不变。(极端负载下 500ms 仍会击穿,后由3dd66f0提至 2s。)- React 19 下 antd 静态 message 静默不弹:antd v5 静态方法内部依赖
ReactDOM.render(React 19 已移除),生产环境所有message.success/error无声丢失。修复:unstableSetRender注入基于createRoot的渲染器(src/main.tsx)。 - SendPage 渲染期 fetch 死循环:
if (providers.length === 0) fetchProviders()写在渲染体内,zustand 每次set换 state 引用,形成「set → 重渲染 → 再 fetch」无限循环(测试挂起实证)。修复:副作用移入useEffect空依赖数组。
验证清单
每批提交前的本地验收(全部通过后才 push):
gofmt -l .无输出,go vet ./...、golangci-lint run干净go test -race -count=1 ./...至少两遍全绿(flake 修复类提交对目标包额外 -race 重复 3-5 遍)- 零块核对器
python3 tools/zero_check.py coverage.merged.out --gate 100GREEN(HERALD_MAIN_COVERDIR下跑全仓-coverpkg=./...,covermerge 合并三个main()转储后验证;本地跑链必须GOTOOLCHAIN=go1.26.6与go.mod钉定的最低工具链一致——go1.27 的块边界整体 +1,照它核对或登记必失配) - 涉及文档站时
pnpm run build通过(无死链) - 前端批次:
pnpm test:coverage(含 lines + branches 双 100 阈值)与pnpm build全绿后才 push - CI 全部 job(Lint / Test / Dashboard / Docker Build / Build)全绿确认
维护
新代码以"触发每个分支"为测试目标;新增 main() 或常驻进程入口时同步补 GOCOVERDIR 子进程守卫测试,并把它加进 ci.yml 中 covermerge 的 --expect 清单;前端新增组件/页面同步补组件测试并维持行 + 分支双 100%(达不到的分支在测试文件或口径文档登记原因);门禁只随实测水位上调,不预留缓冲。
后续
本文是 2026-09 那一轮攻坚的收官记录,数字停留在当时的水位。此后 dashboard 又补了通知规则与通知群组两个页面及其组件测试,并修掉两类"覆盖率满却没测到"的问题:一类是"行覆盖 100% 却 exit 1"的异步泄漏(未 act 收口的 React 调度在环境销毁后才冲刷),一类是把"用例空转"误判成"v8 归属偏差"(not.toBeNull() 对 undefined 也通过)。此后前端为 15 个文件 143 个用例,语句 / 分支 / 函数 / 行四项均 100%,门禁阈值已抬到 branches: 100。2026-09-27 按 CI 同流程复测(Go 侧链两轮 GREEN、等效 100.00%,前端 143 用例全过四项 100%)后,本文的表格与提交链已按复测同步到当前水位,期 1–5 的叙述保留为该轮的历史记录;最新水位始终以测试覆盖率口径为准。
维持复核
| 日期 | go vet | go test(-count=1 -race 全包) | 覆盖率(go-test 原始口径) | 覆盖率(门禁合并口径) | zero_check --gate 100 | 结论 |
|---|---|---|---|---|---|---|
| 2026-09-27(维持复核) | 0 | 47 包全绿,0 FAIL | 99.64%(5012/5030,零块 10 = 台账 7 + 3 个 main() 入口块经子进程实测 rescue) | 99.78%(5019/5030,块 3262、零块 7) | GREEN,等效 100.00%(台账豁免 7/7,无未定性零块) | 与收官水位逐位一致,零漂移;零块集合与 KNOWN_UNCOVERABLE.md 台账一一对应(wechatmp 双检块本轮实测非零、按台账语义不参与豁免),无新增可达缺口 |
| 2026-10-01(维持复核 + 最大缺口补测) | 0 | 47 包全绿,0 FAIL | 99.72%(5022/5036,零块 8 = 台账 5 + 3 个 main() 入口块经子进程实测 rescue) | 99.86%(5029/5036,块 3267、零块 5) | GREEN,等效 100.00%(台账豁免 5/5,无未定性零块) | 普查先确认 7 个零块全在台账内,但最大非 os.Exit 缺口簇 cmd/heraldd main.go:130/:381 的登记论证只覆盖了 map 键唯一性——按 writeControl 先例提为 seam registerProvider,由 TestDuplicateNameGuardAbortsRegistration 注入失败实测两处 return 1 分支(均在绑端口/起 worker 前退出)。该包 98.29%→99.66%(缺 5→1 语句,残余为 os.Exit 失败分支),残余零块 7→5、台账 8→6 条,逐处定性见口径文档。门禁途中另暴露一处与本任务无关的真 flake:core/worker TestPoolRunProcessesTasks 报 registry Count()=1,根因是 workerLoop 的 Pop 错误分支在 ctx 取消时 return 前不注销,已改 defer 统一退出清理并由 TestPoolRunDeregistersWorkersOnShutdown 确定性复现(修复前红、修复后绿),本行数字取自修复后的串行跑 |
复核方法:GOTOOLCHAIN=go1.26.6 go test -count=1 -race -coverpkg=./... -coverprofile=coverage.out ./... → tools/covermerge.py(三个 main() 入口 --expect,3 块 rescue)→ python3 tools/zero_check.py coverage.merged.out --gate 100;语句数按完整块区间去重聚合(与核对器同口径)。台账残余 5 条全部维持原理性不可测定性(os.Exit 跳过 GOCOVERDIR ×3、gorilla SetWriteDeadline 恒 nil ×2,另有 wechatmp 双检命中块调度偶发走到非零),测试侧无可达 seam,不重复登记;原登记的「map 键唯一性矛盾 ×2」两条已于 2026-10-01 被反证——生产不可达不等于测试不可达,提 seam 后实测移出台账。