mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-01 06:47:29 +00:00
背景:老大指出「CI 测试不够灵活、覆盖不到内核方方面面,应从启动图入手」。 先定位 CI 失败:六个门禁在基线上本地全绿,复现不出红 —— 实质是**覆盖面太窄** 而非配置写错(原 race 门只跑 ./internal/agent/core/ 与 ./cmd/waiter/ 两个包, kernelboot 下 scheduler/syncbus/stress 的并发用例全在裸奔,绿是因为没测到)。 ## 一、新增 internal/kernelboot/modules/registry_test.go(913 行,5 条判据) kernelboot 自身判据很密(图编解码/分层/调度/迁移回滚/并发压测),但**全部用 假模块**在图上验算法。「真实 17 个模块按真实登记表装配成整机、逐层启动、 再严格逆序关闭」一条用例都没有。这中间藏着一整类失败:算法对、单模块对, 但装配起来不对(漏挂模块 / 某层关闭函数没登记 / 关闭顺序反了)。 5 条判据(零假模块,全部基于 kernelmod.Registered() 的 17 个真模块): - TestRealRegistryAssemblesWholeMachine:每模块进图、设施真就绪(不是"登记过") - TestLayerTopologyIsRespected:层间严格有序、层内两两无依赖、分层真带来并行 - TestShutdownIsStrictlyReverseLayerOrder:关闭严格逆序(实测哨兵序 [4 3 2 1 0]) - TestShutdownLeavesNoResidue:连续启停 3 轮,goroutine 回基线 + 临时目录删净 + syncbus 订阅者不增长 - TestEveryRegisteredModuleIsCompiledIn:源码树扫描与登记表**双向**集合比对 ### 判据 5 的真相源为何是"类型+四方法集"而不是 Register 调用点(初版栽过) 初版扫 `kernelmod.Register(...)` 调用点。实测:删掉叶子模块 multimodal 的 init() 后,**5 条判据全绿** —— 调用点消失 ⇒ 源码侧与登记表侧同时少一个 ⇒ 集合比对照样相等。而"忘记初始化注册"恰是它声称要抓的场景。 根因是把「要检测的对象」当成了「检测的入口」,入口一被破坏就失去视野。 改成扫「实现了 Name/Requires/Provides/Init 四方法的类型」:模块哪怕不 Register、不被 import,四个方法仍写在源码里。 坑:本仓大量类型都叫 Module,只按类型名做键会撞成一堆(实测撞到只剩 2 个) ⇒ 用「目录+类型名」复合键;agent 是指针接收者 *Module,要处理 StarExpr。 修后恰扫出 17 个类型,与登记表 17 个名字一一对应,零误报。 ### 失败面逐条实测(不是注释声称) M1 删叶子模块 init() → 只有判据 5 红(1–4 全绿,这正是它存在的理由) M2 CloseAll 逆序改正序 → 判据 3 红 M3 删聚合 import → 判据 5 红 其余:跳过某层会让内核 panic(llm 拿到 nil LuaVM,SIGSEGV);层顺序改逆序 红在装配失败(依赖数据为 nil)而非顺序断言 —— 这两种能发现但报错可读性差, 已写入文件注释,修它属业务改动(另派)。 ## 二、.github/workflows/ci.yml - race 门:写死两包 → `go list ./...` 全仓动态枚举(剔除 cmd/gui, 枚举 <50 包则响亮失败),实测 92 包 - 新增 -skip 排除表:每项必须写「原因 + 修复单号」,无原因该步骤自身失败; 每次 CI 打印完整清单,避免"排除项静默腐烂" - 新增「已知红的可核对证据」步:用**静态断言**检查两处已知缺陷是否仍在 (webui gzipResponseWriter 无锁、kernel Set* 无锁直写)。 不用"DATA RACE 报告数>0"当断言 —— 实测这些竞争偶发(正向 0 条、 另一次 8 条),那样必然假绿。 - cross 门:cmd 清单由写死 → 逐包 `CGO_ENABLED=0 go build` 实测枚举 (实测 4 个纯 Go / 8 个依赖 cgo;homed-key-rotate 一直是纯 Go 却从没进过 清单,白少一个覆盖);矩阵补 windows/arm64 与 linux/386 - go job:单列「整机启动图判据」一步(带 -count=1,便于失败定位) ## 三、新增 .github/scripts/check-ci-gates.py(180 行) 官方复核时发现:第一版判据只跑 gofmt/build/vet/go test,**完全不校验 ci.yml**, 而 ci.yml 正是改动占比最大的部分 —— 用「删掉 cmd/gui 剔除」这个变异咬出了该缺口。 本脚本用 YAML 解析断言 6 类门禁机制仍在,并给每个 run 步骤做 bash -n。 关键细节:`run: |` 块里的 `# ...` 是**字符串不是 YAML 注释**,`"cmd/gui" in run` 会被一句说明性注释满足 —— 真把 grep -v 删掉断言仍绿。故剥 shell 注释后再断言, 并给剥注释函数**本身**加了自测。 ## 四、两处纯注释纠偏 - modules/registry.go:文档写「防漏 import 判据见 registry_test.go」(main 上该 文件当时不存在)、第④步指向 `modules/imports.go`(真实位置是 internal/kernelmod/all/all.go)。已纠正并落地为两条判据的指引。 - kernelboot/kernelstate.go:把「判据见 modules」具体化为函数名。 ## 门禁(全部 -count=1,无缓存) go build ./... = 0 go vet ./... = 0 go test ./... = 0(59 ok / 0 FAIL) go test -race 91 包 = 0(58 ok / 0 FAIL) check-ci-gates.py = 0 证据步骤正向 = 0;反向(给 SetLuaVM 加锁)= 1(证明是活断言不是死代码) ## 已知红(均待另开线,本线不越界改业务) 1. 内核:agent.wirePluginSDK 与 pluginhost.injectBaseDeps 在第 4 层并行重复 写同一批 plugin.Registry 字段,而 Set* 全不取锁(r.mu 存在但没用)。 实测偶发约 4%(1/25)。当前以 pkg 级排除临时挡住,**定性为临时手段**; 唯一出口是给 Set* 加锁(修复单 KERNEL-REGISTRY-SETLOCK)。 2. webui:gzipResponseWriter 的 mode/wroteHeader/buf 无锁,handler goroutine 与增量写 goroutine 并发写同一 ResponseWriter(修复单 WEBUI-GZIP-RACE)。 3. internal/agent/core TestStress_Nesting... 时序偶发(-race 放大 300ms 假设, 无 DATA RACE 报告),未 skip,复现命令记在 ci.yml 注释里。 边界:未改任何内核业务逻辑(两个业务文件 diff 逐行只有注释);未 push、未开 PR。
102 lines
3.6 KiB
Go
102 lines
3.6 KiB
Go
// Package modules 是内核模块的**登记表**(不含任何具体模块的实现)。
|
||
//
|
||
// ## 设计意图(用户设计)
|
||
//
|
||
// > 每个内核模块显式各自声明依赖 … 最先初始化的 core 自然不依赖任何模块,
|
||
// > 下一步初始化的依赖 core 以此类推
|
||
//
|
||
// ## 怎么加一个新模块(这是本设计的**验收标准**)
|
||
//
|
||
// ① 新建一个文件(如 modules/mymod.go)
|
||
// ② 写一个类型,实现 kernelboot.Module(Name/Requires/Provides/Init)
|
||
// ③ 在同文件的 init() 里调 modules.Register(&MyMod{})
|
||
// ④ 在 `internal/kernelmod/all/all.go` 加一行空白 import —— **仅此而已**
|
||
//
|
||
// **不需要**编辑任何中央清单:依赖写在模块**自己**的 Requires() 里。
|
||
//
|
||
// ★ 为何要"防漏 import"的判据(见 `internal/kernelboot/modules/registry_test.go`):
|
||
// 忘了 import ⇒ 模块**根本不被编译进来** ⇒ 它不参与装配 ⇒ 依赖它的模块报
|
||
// "缺依赖"(响亮),但**不依赖它的模块会静默少了那个设施**
|
||
// (如插件注册表没建 ⇒ 插件全部加载不了,而错误信息指向插件而非根因)。
|
||
//
|
||
// 判据有两条(缺一不可):
|
||
//
|
||
// · `TestEveryRegisteredModuleIsCompiledIn` —— 用 AST 扫源码树里的
|
||
// `kernelmod.Register(X{})`,与登记表做集合比对(独立真相源,
|
||
// 手写清单会跟着漏,所以不用清单)
|
||
// · `TestRealRegistryAssemblesWholeMachine` 等 —— 用真实登记表装配整机、
|
||
// 断言每个模块都进图且设施就绪、并严格逆序关闭(见同目录测试文件)
|
||
package modules
|
||
|
||
import (
|
||
"fmt"
|
||
"sort"
|
||
"sync"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/kernelboot"
|
||
)
|
||
|
||
var (
|
||
regMu sync.Mutex
|
||
registry []kernelboot.Module
|
||
seenName = map[string]bool{}
|
||
)
|
||
|
||
// Register 由模块在自己的 init() 里调用(自注册)。
|
||
//
|
||
// 重复注册同一模块名会**记录**并在 BuildModules 时报错 —— 不用 panic:
|
||
// panic 发生在 init 阶段,栈信息对使用者毫无意义(他只想知道哪个模块重名)。
|
||
func Register(m kernelboot.Module) {
|
||
if m == nil {
|
||
return
|
||
}
|
||
regMu.Lock()
|
||
defer regMu.Unlock()
|
||
if seenName[m.Name()] {
|
||
dupErr = append(dupErr, m.Name())
|
||
return
|
||
}
|
||
seenName[m.Name()] = true
|
||
registry = append(registry, m)
|
||
}
|
||
|
||
// dupErr 累积重名模块(BuildModules 时统一报告)。
|
||
var dupErr []string
|
||
|
||
// BuildModules 返回全部已登记模块(按名字排序,保证可复现)。
|
||
//
|
||
// ## 为什么排序
|
||
//
|
||
// 分层结果的**层内顺序**若依赖 map/切片顺序,同一份声明两次构建会得到
|
||
// 不同顺序 ⇒ 不可审计(设计 §2.5:图要能进 PR diff 比对)。
|
||
// 排序让结果稳定。
|
||
func BuildModules() ([]kernelboot.Module, error) {
|
||
regMu.Lock()
|
||
defer regMu.Unlock()
|
||
if len(dupErr) > 0 {
|
||
names := append([]string(nil), dupErr...)
|
||
sort.Strings(names)
|
||
return nil, fmt.Errorf("kernelboot/modules: 模块名重复注册: %v"+
|
||
"(同一个模块被 import 了两次?或两个模块取了同名)", names)
|
||
}
|
||
out := append([]kernelboot.Module(nil), registry...)
|
||
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
||
if len(out) == 0 {
|
||
return nil, fmt.Errorf("kernelboot/modules: 没有任何模块被登记 —— " +
|
||
"通常是没有 import 模块包(见本文件顶部的'怎么加一个新模块')")
|
||
}
|
||
return out, nil
|
||
}
|
||
|
||
// Names 返回已登记模块名(诊断/判据用)。
|
||
func Names() []string {
|
||
regMu.Lock()
|
||
defer regMu.Unlock()
|
||
out := make([]string, 0, len(registry))
|
||
for _, m := range registry {
|
||
out = append(out, m.Name())
|
||
}
|
||
sort.Strings(out)
|
||
return out
|
||
}
|