mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 20:27:30 +00:00
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:
@ -1,99 +0,0 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 这组判据守的是**无头消费方**(waiter -chat、基准测试 runner、脚本)
|
||||
// 的唯一信息出口:chat 的 response 帧。
|
||||
//
|
||||
// 背景(与本轮修的 usage 缺陷同族):agent 在每次 LLM 调用后记账,
|
||||
// emitResponse 也把 usage 放进了同步回执;但 cli 的 handleChat 此前
|
||||
// 只从回执里取 content 就往帧里写 —— usage 被丢掉。
|
||||
// 后果是**有头 UI 看得到成本、无头调用方看不到**,而跑分恰恰全靠无头。
|
||||
//
|
||||
// 症状形态与链事件那次一模一样:写了、发了、没有任何报错,只是没人收到。
|
||||
|
||||
// TestChatResponseFrameCarriesUsage 是核心判据:
|
||||
// 回执里有 usage 时,帧里必须原样带上(且类型不被压扁)。
|
||||
func TestChatResponseFrameCarriesUsage(t *testing.T) {
|
||||
usage := map[string]interface{}{
|
||||
"prompt_tokens": 1000,
|
||||
"completion_tokens": 200,
|
||||
"total_tokens": 1200,
|
||||
"cache_read_tokens": 768,
|
||||
"cache_miss_tokens": 232,
|
||||
}
|
||||
frame := chatResponseFrame(map[string]interface{}{
|
||||
"content": "好的",
|
||||
"usage": usage,
|
||||
})
|
||||
|
||||
if frame["type"] != "response" {
|
||||
t.Errorf("type=%v,期望 response", frame["type"])
|
||||
}
|
||||
if frame["content"] != "好的" {
|
||||
t.Errorf("content=%v,期望「好的」", frame["content"])
|
||||
}
|
||||
got, ok := frame["usage"]
|
||||
if !ok || got == nil {
|
||||
t.Fatalf("帧里没有 usage —— 无头调用方看不到成本;frame=%v", frame)
|
||||
}
|
||||
// 必须是 map[string]interface{}:消费方(runner)按 JSON 读取,
|
||||
// 若这里被压成别的类型,序列化后会变形或丢失。
|
||||
m, ok := got.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("usage 类型=%T,期望 map[string]interface{}", got)
|
||||
}
|
||||
if m["total_tokens"] != 1200 {
|
||||
t.Errorf("total_tokens=%v,期望 1200", m["total_tokens"])
|
||||
}
|
||||
if m["cache_read_tokens"] != 768 {
|
||||
t.Errorf("cache_read_tokens=%v,期望 768", m["cache_read_tokens"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatResponseFrameOmitsUsageWhenAbsent 守住「无数据 ≠ 0」:
|
||||
// 回执里没有 usage 时,帧里也不该出现这个键。
|
||||
//
|
||||
// 若这里补一个零值 usage,消费方就无法区分「没开缓存」与「命中率为 0」,
|
||||
// 会把「不知道」画成 0% —— 与本项目 CacheHitRate 的 ok 口径一致。
|
||||
func TestChatResponseFrameOmitsUsageWhenAbsent(t *testing.T) {
|
||||
frame := chatResponseFrame(map[string]interface{}{"content": "你好"})
|
||||
if u, ok := frame["usage"]; ok {
|
||||
t.Errorf("回执没有 usage 时不该凭空造:%v", u)
|
||||
}
|
||||
// 序列化后也必须真的没有这个键。
|
||||
b, err := json.Marshal(frame)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(b), "usage") {
|
||||
t.Errorf("JSON 里不该出现 usage:%s", b)
|
||||
}
|
||||
// nil 值同样要当成「没有」(Go 里 map 取值存在但为 nil 是常见形态)。
|
||||
frame2 := chatResponseFrame(map[string]interface{}{"content": "x", "usage": nil})
|
||||
if u, ok := frame2["usage"]; ok {
|
||||
t.Errorf("usage 为 nil 时不该带上:%v", u)
|
||||
}
|
||||
}
|
||||
|
||||
// TestChatResponseFrameCarriesReasoning 保住既有能力:
|
||||
// reasoning_content 也要透传(此前 handleChat 也没带它,
|
||||
// 无头调用方因此看不到思考过程)。
|
||||
func TestChatResponseFrameCarriesReasoning(t *testing.T) {
|
||||
frame := chatResponseFrame(map[string]interface{}{
|
||||
"content": "答案",
|
||||
"reasoning_content": "先想一下",
|
||||
})
|
||||
if frame["reasoning_content"] != "先想一下" {
|
||||
t.Errorf("reasoning_content=%v,期望透传", frame["reasoning_content"])
|
||||
}
|
||||
// 空思考不该出现(避免给每个普通回复都挂一个空字段)。
|
||||
frame2 := chatResponseFrame(map[string]interface{}{"content": "x"})
|
||||
if _, ok := frame2["reasoning_content"]; ok {
|
||||
t.Error("空 reasoning_content 不该出现在帧里")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user