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

@ -129,7 +129,7 @@ elif "${{" in _tbl:
fails.append("排除表步骤含 GitHub 表达式,本脚本无法本地执行它(需改用别的验证方式)")
else:
import re as _re
_m = _re.search(r"read -r -d '' TABLE <<'EOF'[^\n]*\n(.*?)\nEOF\n", _tbl, _re.S)
_m = _re.search(r"read -r -d '' TABLE <<'EOF'[^\n]*\n(.*?)\nEOF\n", _tbl, _re.DOTALL)
if _m is None:
fails.append("排除表步骤里找不到 TABLE heredoc —— 无法注入合成坏行")
else:
@ -227,17 +227,18 @@ else:
if _run_body(_inject(_probe + "|合成探针原因|")).returncode == 0:
fails.append("合成行「reason 有效 / ticket 空」竟通过 ⇒ ticket 守卫已失效")
# ── 4. go job:整机启动图判据必须单列一步跑 kernelmod/all ──
# ── 4. go job:测试归拢门 + 包依赖门必须都在 ──
#
# ★ 目标包随 ID:13 从 internal/kernelboot/modules(死包,已删)迁到
# internal/kernelmod/all(真实聚合入口)。本断言跟着改,否则它盯的
# 是一个**已不存在的包** —— 那会变成"永远红"或(更糟)"永远绿"的空转。
GO_BOOT_PKG = "./internal/kernelmod/all/"
# 2026-11-05:整机启动图判据(bootgraph_test.go)等白盒测试已全部删除,
# 原先「必须单列一步跑 kernelmod/all」的断言随之作废 —— 盯着一个不再
# 存在的步骤就是永远红的空转。改为盯两道新的构建期静态门。
gojob = step_runs("go")
boot = [r for _n, r in gojob if GO_BOOT_PKG in r and "go test" in r]
check(bool(boot), "go job 缺少「整机启动图判据」步骤(go test " + GO_BOOT_PKG + ")")
for r in boot:
check("-count=1" in r, "整机启动图判据步骤必须带 -count=1(不许吃 go test 缓存)")
allgo = "\n".join(r for _n, r in gojob)
check("源码目录不得有 _test.go" in allgo or
"find internal cmd pkg providers deploy -name '*_test.go'" in allgo,
"go job 缺少「测试归拢」硬门(源码目录不得有 _test.go)")
check("go list -deps" in allgo,
"go job 缺少「包依赖门」(go list -deps 属工具链,不写在 go test 里)")
# ── 5. cross job:cmd 清单必须实测枚举,不许写死 ──
cross = step_runs("cross")
@ -309,4 +310,4 @@ if fails:
print(" x " + f)
sys.exit(1)
print("CI 门禁自检通过:动态枚举 / 排除表校验 / 启动图判据步骤 / 交叉枚举 / 全部 run 步骤语法")
print("CI 门禁自检通过:动态枚举 / 排除表校验 / 测试归拢门 / 包依赖门 / 交叉枚举 / 全部 run 步骤语法")