refactor(test): 肃清全部白盒测试 —— 源码目录零 _test.go,判据只走模块导出接口

## 用户裁定(本次唯一方向)

1. 「测试不应该面向过程而是面向模块的输入和输出」
2. 「每个模块下应该使用 test 目录搜集所有测试文件,而不是和源码混在一起」
3. 「现在 test 都超过源码数量了,这很明显不符合模块接口黑盒化的要求」
4. 「ast判包依赖?这不是工具链该做的吗?为什么在测试里」
5. 「抽行为判据?只需要面向模块输入接受模块输出就行」
6. 「不管ci,直接把所有白盒测试全清理了再回去一鼓作气改ci」

## 实测起点

    537 个测试文件 / 377 个源码文件 / 2853 条 Test
    523 / 537(97%)直接调用私有函数
    514 个是 in-package 白盒(package xxx,与源码同目录)
     88 个读 .go/.md 源码做断言
     94 个断言私有字段(f.Step / r.buf / a.sched …)
     38 个直调私有函数(stepLLM / runStage / classifyProviderError …)
    internal/agent/core: 140 test / 52 src

## 做了什么

### ① 删除 514 个 in-package 白盒测试

不做「抽行为判据」,也不做逐文件搬运。理由:Go 按目录分包,
`pkg/test/` 里写 `package pkg` 也看不到 unexported 符号 —— 所以
「挪进 test/」和「改成黑盒」是同一件事的两面。把 514 个白盒文件
搬走只会得到一堆编译不过的文件,或者为了迁就它们把内部全导出。

用户明确否定「抽」这个动作后,剩下 9 个 test/ 里的判据也一并
重新审视:凡是靠读源码/前端资源文本做断言的(dashboardJSSource /
FrontendAsset / GraphDataForVisual / SSEEventRing …)全部删除,
只留走模块**本来就有**的公开 API 的输入→输出判据:

    internal/agent/core/child/test/     Registry 的 Add/Get/Stop/Remove 输出
    internal/agent/core/test/           StageHost 的 Register/Run 输出
    internal/plugins/files/test/        工具 handler 的 {path} → {content/error}
    internal/plugins/webui/test/        HTTP 请求 → 响应(NewHandler/RegisterRoutes)
    internal/tfidf/test/                文本 → 向量/检索结果

现存 9 个测试文件全部在 `<module>/test/`,源码目录零 `_test.go`。

### ② 回退「为测试而导出」的接口

这些是前几轮为了把白盒判据挪出源码目录而导出的,属于白盒残留,
现在没有调用方了,全部收回私有:

    webui.FrontendAsset / DashboardPage        删除
    webui.GraphDataForVisual → graphDataForVisual
    webui.SSEEventRing/Record → sseEventRing/sseEventRecord(含字段)
    webui.NewSSEEventRing                       删除
    kstate.MountForTest                         删除
    security.ClassifyPrincipalForTest           删除

生产代码对它们的内部调用同步改回私有名,不改任何行为。

### ③ CI 一鼓作气改到位(用户指定顺序)

**删**:`整机启动图判据` 单列步骤(它跑的 bootgraph_test.go 已删),
以及 `check-ci-gates.py` 里盯着那个步骤的断言 —— 继续盯着一个
不存在的步骤就是永远红的空转。

**加两道构建期静态门**(原先这些约束错放在 go test 里):

- `测试归拢(源码目录不得有 _test.go)`
  `find internal cmd pkg providers deploy -name '*_test.go' -not -path '*/test/*'`
  非空即红。物理上挡住白盒回潮。
- `包依赖门(kstate 为叶子 / 底层包不反向依赖装配器)`
  用 `go list -deps` 判 8 组禁止依赖 + `go build core` 确认环不存在。
  这是**工具链**的职责,不该由 `go test` 兼职。

`check-ci-gates.py` 同步改为盯这两道门;ci.yml 里 13 处既有
>80 字符行一并折行(门禁标了 STOP,不留尾巴)。

### ④ 顺带(上一批未提交的)

`scripts/make-package.sh` 产物名 `HomeAgent-linux-*` →
`HologramAgent-linux-*`;清理 gitignored 构建产物
`build/ dist/ .go/ .npm-cache/` 共 20G(工作区 21G → 1.7G)。
运行契约(~/.homeagent/cli.sock、线上域名、兼容别名)未动。

## 验证

    go build ./...                     PASS
    go vet ./...                       PASS
    gofmt -l                           干净
    python3 .github/scripts/check-ci-gates.py   PASS
    go test ./... -count=1             5 个测试包全过 / 100 个包无测试 / 0 FAIL
    源码目录 _test.go 数              0
    测试文件数                         9(全部在 <module>/test/)

## 后续

按模块导出接口清单重写判据(不是从旧白盒里抽):
internal/agent/api、internal/memory、internal/security、internal/plugin、
internal/knowledge、cmd/* 等目前 0 判据,需要逐个列接口 → 写输入→输出判据
→ 变异验证。这是新一轮工作,本提交不含。
This commit is contained in:
JianFeeeee
2026-11-09 11:13:56 +08:00
parent eac8e808f1
commit 53cdc7f2a6
539 changed files with 158 additions and 116960 deletions

View File

@ -1,85 +0,0 @@
package memory
// 蒸馏器接线回归判据(从 cmd/homed/bootstrap_test.go 迁移而来)。
//
// ## 为什么这条判据必须存在(历史 bug)
//
// `main()` 拆分时,initMemoryStack 函数体内残留一句 `defer distiller.Stop()`,
// 函数一 return 就 cancel 掉刚启动的循环 ⇒ **规则蒸馏 10min 心跳从不运行**。
//
// ★ 该缺陷**不会让任何单测变红** —— pipeline 的 `TestDistillOnce*` 直接调
// `distillOnce`,**绕过了 Start/Stop 接线**。只有按「启动阶段函数」的真实
// 调用方式断言(调用 → 检查仍在跑 → 调 cleanup → 检查已停),才照得出来。
//
// ## 迁移说明
//
// 本判据原先针对 `cmd/homed` 的 `initMemoryStack`。模块化后该逻辑搬进
// `internal/kernelmod/memory.Module.Init`,所以判据跟着搬 —— 且**必须保留**:
// 迁移过程中"忘了 AddCloser"或"改成 defer"是同一类缺陷的新形态。
//
// 判据改为直接跑真实模块(经 kernelboot.Run),确保测的是**真实入口**。
import (
"github.com/JianFeeeee/HologramAgent/internal/kstate"
"os"
"path/filepath"
"testing"
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
)
// ★★★ 判据:模块 Init 返回后蒸馏器**仍在运行**(无 defer Stop 残留)。
func TestMemoryModuleKeepsDistillerRunning(t *testing.T) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "memory"), 0o755); err != nil {
t.Fatal(err)
}
// ★ 需要真 cfgReg:memory 模块读 core.agent.embedding_model_path,
// 而它对 nil 会现在**明确报错**(而不是 panic 在 strings.Split)。
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
defer reg.Close()
ks := &kstate.State{DataDir: dir, CfgReg: reg}
if err := (Module{}).Init(ks); err != nil {
t.Fatalf("memory 模块 Init: %v", err)
}
if ks.Memory == nil || ks.Memory.Distiller == nil {
t.Fatal("memory 模块未产出蒸馏器")
}
if ks.Memory.DB == nil {
t.Skip("图库未初始化,无法验证蒸馏接线")
}
// ★ 关键断言:Init 已返回,蒸馏循环必须还在跑
if ks.Memory.Distiller.Stopped() {
t.Fatal("★ memory 模块 Init 返回后蒸馏循环已被停掉" +
"(defer Stop 残留?)—— 规则蒸馏 10min 心跳将从不运行")
}
// 关闭函数是唯一的停机点(经 AddCloser 登记,由 KernelState.CloseAll 逆序执行)
if err := ks.CloseAll(); err != nil {
t.Fatalf("CloseAll: %v", err)
}
if !ks.Memory.Distiller.Stopped() {
t.Fatal("CloseAll 之后蒸馏器应已停止(关闭函数未登记?)")
}
}
// ★★★ 判据:**关闭函数经 AddCloser 登记**(不是 defer)。
//
// 这是上一条的机制保证:若有人把 AddCloser 改回 defer,
// 上一条判据报红;本判据报"没登记",两条互相印证、定位更快。
func TestMemoryModuleRegistersCloser(t *testing.T) {
dir := t.TempDir()
os.MkdirAll(filepath.Join(dir, "memory"), 0o755)
reg := internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
defer reg.Close()
ks := &kstate.State{DataDir: dir, CfgReg: reg}
if err := (Module{}).Init(ks); err != nil {
t.Fatal(err)
}
// 内存模块应恰好登记 1 个关闭函数(停蒸馏器 + 关图库,合在一个闭包里)
if len(ks.Closers) != 1 {
t.Fatalf("★ 应登记 1 个关闭函数,实际 %d —— "+
"若为 0 说明改成了 defer(Init 返回即停机)", len(ks.Closers))
}
}

View File

@ -1,71 +0,0 @@
package memory
// "nil 加读取前检查" 的判据(用户指定的做法)。
//
// ## 为什么需要判据守它
//
// `KernelState` 的字段是**类型化的**(用户设计),好处是拼错/取错类型是
// 编译错误。但**跨模块的"未就绪"仍表现为 nil** —— 而 nil 解引用会
// **panic**,栈信息指向无关处(如 `strings.Split`),排查成本高。
//
// 用户明确要求:"结构体成员初始置 nil 加**读取前检查**"。
// 本判据确保每个模块对**不由依赖图保证**的字段(Group 0 产物)做检查。
//
// ★ 为什么要判据而不是靠自觉:`ks.CfgReg` 是 Group 0 的产物,
// **无法通过 Requires 声明**(Group 0 不是调度模块)⇒ 依赖图保证不了它。
// 这正是最容易忘、忘了就 panic 的地方。
import (
"path/filepath"
"strings"
"testing"
internalConfig "github.com/JianFeeeee/HologramAgent/internal/config"
"github.com/JianFeeeee/HologramAgent/internal/kstate"
)
// ★★★ 判据:**CfgReg 为 nil 时给可操作错误,而不是 panic**。
func TestMemoryModuleRejectsNilCfgReg(t *testing.T) {
dir := t.TempDir()
ks := &kstate.State{DataDir: dir} // CfgReg 故意为 nil
defer func() {
if r := recover(); r != nil {
t.Fatalf("★ CfgReg 为 nil 时 panic 了(应返回可操作错误): %v\n"+
" 用户要求的是「nil 加读取前检查」,panic 的栈信息指向无关处", r)
}
}()
err := (Module{}).Init(ks)
if err == nil {
t.Fatal("★ CfgReg 为 nil 却成功返回 —— 后面会在读配置时 panic")
}
// 错误必须点出**是哪个字段**以及**该做什么**
msg := err.Error()
if !strings.Contains(msg, "CfgReg") {
t.Errorf("错误应点名字段 CfgReg(便于定位): %v", err)
}
if !strings.Contains(msg, "Group 0") {
t.Errorf("错误应指出根因(Group 0 必须先跑): %v", err)
}
}
// ★★★ 判据:**DataDir 为空时也拒绝**(同类检查)。
func TestMemoryModuleRejectsEmptyDataDir(t *testing.T) {
dir := t.TempDir()
reg := newTestReg(t, dir)
ks := &kstate.State{CfgReg: reg} // DataDir 空
err := (Module{}).Init(ks)
if err == nil {
t.Fatal("★ DataDir 为空却成功返回")
}
if !strings.Contains(err.Error(), "DataDir") {
t.Errorf("错误应点名字段 DataDir: %v", err)
}
}
func newTestReg(t *testing.T, dir string) *internalConfig.ConfigRegistry {
t.Helper()
return internalConfig.NewConfigRegistry(filepath.Join(dir, "config.db"))
}