## 背景 `internal/cli/checkers` 的 Checker 接口只有 `Name()/Category()/Check(ctx)`,依赖全部从**包级全局或进程环境**隐式获取,而非显式注入: - `loadConfig()` → 全局 `configPath`(#976 已原子化,但仍是全局) - `os.UserHomeDir()` / `config.HotplexHome()` → 环境 - `builtin.NewRegistry()` + `reconcile.New()` → 每次 Check 重建 `init()` 注册把真实实现(如 `defaultBuiltinSkillsStatus`)绑死在全局函数,绕过了已有的注入雏形 `NewBuiltinSkillsChecker(statusFn)`。 ## 三个具体危害 1. **可测性缺口**(本次 `ErrRootOutsideHome` bug 的直接温床,见 #974):真实构造逻辑不可注入 → 无法单测 → 现有测试全为 mock statusFn → 契约分叉只能靠线上暴露 2. **隐式契约**:依赖无显式声明,改全局状态需全文 grep 引用点(#976 涉及 30+ 处) 3. **状态重复构造**:每次 Check 重建 registry + runner(性能无碍,但构造-复用未固化) ## 参照模式(仓库已确立) - `internal/admin/admin.go`:Provider 接口 + `cmd/hotplex/main.go` 桥接(见 CLAUDE.md「Admin API 包隔离模式」) - `NewBuiltinSkillsChecker` 已有构造注入雏形,`init()` 注册未使用 ## 理想形态 注册时注入 provider(持有 userHome/hotplexHome/runner),而非绑死全局函数: ```go cli.DefaultRegistry.Register(builtinSkillsChecker{ statusFn: (&skillsStatusProvider{}).Status, }) ``` 测试可注入假 provider 或真 provider + `t.TempDir()` 直接测真实构造逻辑。 ## 范围建议 - 优先级:P2(#976 已消除 race 面,剩余风险为可测性缺口) - 波及:全部 20+ checker 注册方式 + doctor/onboard 入口 - 验收:checker 源码无包级可变全局;真实 status 构造逻辑可单测;与 admin Provider 模式对齐
背景
internal/cli/checkers的 Checker 接口只有Name()/Category()/Check(ctx),依赖全部从包级全局或进程环境隐式获取,而非显式注入:loadConfig()→ 全局configPath(checkers 包级 configPath 全局变量测试竞态与环境敏感(根治) #976 已原子化,但仍是全局)os.UserHomeDir()/config.HotplexHome()→ 环境builtin.NewRegistry()+reconcile.New()→ 每次 Check 重建init()注册把真实实现(如defaultBuiltinSkillsStatus)绑死在全局函数,绕过了已有的注入雏形NewBuiltinSkillsChecker(statusFn)。三个具体危害
ErrRootOutsideHomebug 的直接温床,见 doctor 报 skills 检查恒失败:ErrRootOutsideHome(checker 与 reconciler 契约分叉) #974):真实构造逻辑不可注入 → 无法单测 → 现有测试全为 mock statusFn → 契约分叉只能靠线上暴露参照模式(仓库已确立)
internal/admin/admin.go:Provider 接口 +cmd/hotplex/main.go桥接(见 CLAUDE.md「Admin API 包隔离模式」)NewBuiltinSkillsChecker已有构造注入雏形,init()注册未使用理想形态
注册时注入 provider(持有 userHome/hotplexHome/runner),而非绑死全局函数:
测试可注入假 provider 或真 provider +
t.TempDir()直接测真实构造逻辑。范围建议