mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-06 01:07:35 +00:00
test: 判据改为面向接口输入输出 + 文档立规 + CI 补缺口
本提交是三件事的第一批,后续按计划分步推进。
## 一、判据改造(两条实证)
### 1. turn_hint 判据原是自证循环
原测试在测试里**手动重放**了真实注入逻辑:
f.Msgs = a.buildMessages(...)
hint := stageHint(f.StageCtx, "turn_hint")
f.Msgs = append(f.Msgs, Message{Role:"system", Content:"[Turn hints] "+hint})
// 然后断言"最后一条是 system、内容含 [Turn hints]"
它断言的是**它自己刚 append 的那一行** —— 把 task.go:656-661 抄进测试
再验证抄得对不对。真实代码那段删掉,它照样绿。
改为:把 turn_hint 放进 StageCtx,跑**真实的 stepPrepare**
(newTaskFrame 起始就是 StepPrepare),检查它产出的 Msgs。
变异验证:删掉 task.go 里 `if hint := stageHint(...)` 那段 ⇒ 实测红。
### 2. webui 文件下发判据原是白盒
原测试直接调未导出的 `stageWebFile(src, isImage)`、断言 `webFilesDir`
包级变量 —— 那都是**实现方式**。
改为从**输出通道 handler** 走(`RegisterOutputChannel("webui", …)`,
agent 发图/发文件时走的就是它):
输入:{payload: "<路径或URL>", type: "image"|"file"}
输出:{status:"ok", url:"/files/<name>.<ext>", size:N}
4 条判据(新版 handler_files_contract_test.go):
· 图片 → 可下载 URL(前缀/扩展名/size)
· 远端 URL 原样透传(不下载)
· 危险扩展名被中和(.html 保留会在 webui 域执行脚本)
· 空 payload 被拒
变异验证:丢扩展名 ⇒ 红;远端也拷贝 ⇒ 红。
## 二、文档立规:engineering-lessons 新增第五节
原来只有「判据会骗人」,没有「判据本身形状错了」。新增 6 小节:
5.1 判据必须把被测模块当黑箱(含"如果重写内部实现、接口不变,
这条判据还会绿吗"这个可操作判据)
5.2 禁止读源码做断言(附正确替代表)
5.2.1 判据不得以文档为依据
5.3 测试文件与业务代码分离(现状:521 文件 / 11.2 万行,
agent/core 一个包 196 文件里 144 个是测试)
5.4 CI 必须覆盖每个独立 module
5.5 CI 与本地必须跑同一套门
§5.2.1 的直接触发:`interrupt_invariant_test.go` 里有一条判据写着
「事实清单(来自探测,见 docs/zh/interrupt-subsystem-split-plan.md §1.5)」
—— 被引用方自己标着「状态:计划(未实施)」,而那条判据的实际断言
只是"入队后队列里 1 条",与声称的"反向依赖现状快照"毫无关系。
该判据已删除(入队行为已由 scheduler_preempt_test 的黑箱断言覆盖)。
## 三、CI 补两个缺口
### gofmt 门
提交纪律要求 `gofmt -l` 干净,但三个 workflow 里 `gofmt` 出现 **0 次**
—— 纪律只靠人自觉。补上后未格式化的代码进不了主干。
只查本仓自管目录:SDK 是独立仓,格式由它自己的 CI 管。
### SDK 判据步骤
`third_party/hologramagentsdk/` 有自己独立的 go.mod:
go list ./... | grep third_party → 0 个
主仓 `go test ./...` **永远不包含** SDK 任何测试。实测 SDK 有 30 个判据
文件,主仓 CI 一次都没跑过。
补的步骤只跑主仓 git 跟踪的文件(CI 上只有 22 个 vendored 文件,
工作机上是完整独立仓)—— 实测 `git archive` 出 22 个文件后
`go test ./sdk/ ./meta/` 全绿。
## 四、SDK 仓测试迁移(独立仓,已单独提交 3a38ef5)
`sdk/{plugin,proxy,stress}_test.go` → `sdk/test/`,包名 `sdk` → `test`。
关键:原测试直接写 `&PluginSDK{regStage: X, name: "test"}` —— **私有字段**。
而 `New(name, sett, regTool, regStage, regAPI, regOutput)` 本来就把所有
registrar 作为参数,等价且是生产路径。共改 19 处,0 处剩余。
主仓同步更新了跟踪路径(这三个文件主仓也 vendor 了一份)。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
go test ./... -count=1 62 包全过,0 FAIL
干净检出(worktree) build + 改造后的判据全绿
## 后续(按计划分步)
步骤 2 webui 29 个测试分类迁移(已实测出精确分类)
步骤 3 agent/core 739 处白盒引用先分类再动
步骤 4 SDK tools/hmapdev(package main,无法迁出,只加说明)
步骤 5 CI 补 coverage + staticcheck
步骤 6 文档双语(需定术语口径)
This commit is contained in:
@ -160,6 +160,187 @@
|
||||
- **未做登记**(`future-plan.md`):还剩什么没做
|
||||
- **操作手册**(`*-runbook.md`):怎么做
|
||||
|
||||
---
|
||||
|
||||
## 五、判据形态类(2026-11-04 立)
|
||||
|
||||
> 这一节回答一个具体问题:**判据应该写成什么形状?**
|
||||
>
|
||||
> 前面四节讲"判据会骗人"的各种形态。这一节讲**判据本身的形状错了**会怎样 ——
|
||||
> 那比"判据写得不够严"更贵,因为形状错的判据会把**实现细节固化成契约**,
|
||||
> 于是重构内部就必然破坏判据,作者只能二选一:不改代码,或者删判据。
|
||||
|
||||
### 5.1 判据必须把被测模块当黑箱
|
||||
|
||||
**模块化的目的就是"改内部实现不破坏外部接口"。判据如果不按这个层次写,
|
||||
就等于取消了模块化的收益。**
|
||||
|
||||
白盒判据把内部状态变成契约。实测代价:
|
||||
|
||||
| 白盒判据 | 它固化了什么 | 后果 |
|
||||
| --- | --- | --- |
|
||||
| `if sf.Step != StepLLM` | 状态机停在哪一步 | 把 `StepLLM` 改名/拆步骤就假红,而行为完全正确 |
|
||||
| `if f.ToolIdx == 3` | 循环推进方式 | 改成"整批提交"就假红,而结果一样 |
|
||||
| `strings.Contains(src, "kstate.Mount(ks)")` | 源码字面量 | 改写成 `kstate.Mount(state)` 就假红(那是对的) |
|
||||
| `os.ReadFile("../../events/bus.go")` 正则取常量 | 常量定义在哪个文件 | 把常量挪到 `types.go` 就假红 |
|
||||
|
||||
**判据要问**:**这条判据约束的是"对外承诺",还是"当前怎么实现的"?**
|
||||
|
||||
如果是后者,它就不该存在 —— 或者必须降级为"实现注释"(代码里的注释说明
|
||||
为什么这样做),而不是判据。
|
||||
|
||||
#### 允许白盒的三种例外(其余一律黑箱)
|
||||
|
||||
1. **纯算法**:输入输出就是全部语义(裁剪算法、版本比较、分页计算)。
|
||||
这类即使白盒也不固化"流程",因为它本来就只有"输入→输出"。
|
||||
2. **并发/生命周期不变量**:无法从外部观测的时序约束,如"Dispose 期间
|
||||
emit 不能读到已释放的指针"。这类**必须**用 `-race` + 并发压力,
|
||||
而不是断言内部字段。
|
||||
3. **熔断/重试状态机**:内部状态本身就是被需求规定的("连续 3 次崩溃
|
||||
进入熔断")。这类要断言**状态转移的可观测效果**(还健康吗、
|
||||
进重载队列了吗),不是断言状态字段的值。
|
||||
|
||||
#### 一个可操作的判据
|
||||
|
||||
写完一条判据,问自己:
|
||||
|
||||
> **"如果我把这个模块的内部实现完全重写、公开接口不变,
|
||||
> 这条判据还会绿吗?"**
|
||||
|
||||
- 会绿 → 形状对了
|
||||
- 会红 → 它钉的是实现,不是契约
|
||||
|
||||
### 5.2 禁止"读源码做断言"
|
||||
|
||||
`os.ReadFile("*.go")` + `strings.Contains` / `regexp` 是**反模式**,
|
||||
即使动机正确(如"防止有人删掉装配的那一行")。
|
||||
|
||||
**为什么它看起来有吸引力**:当行为判据需要很重的 fixture 时,读源码很便宜。
|
||||
|
||||
**为什么它必须禁用**:
|
||||
|
||||
- 它测的是**文本**,不是**语义**。加空格、换变量名、等价重构都会让判断失效
|
||||
(假红或假绿)
|
||||
- 它把"文件布局"变成契约。移动代码到新文件就假红
|
||||
- 它无法区分"代码存在"与"代码被执行"
|
||||
|
||||
**正确的替代**(按优先级):
|
||||
|
||||
| 你想保证的 | 错误做法 | 正确做法 |
|
||||
| --- | --- | --- |
|
||||
| 生产装配接了某根线 | 读源码找 `kstate.Mount(ks)` | 从零启动真实装配,断言**可观测结果**(如 `kstate.Get() != nil`) |
|
||||
| 协议两端字段一致 | 正则提取两边的字符串字面量 | 起 handler,发真实事件,断言**收到的 JSON 字段** |
|
||||
| 某段保护代码没被删 | 读源码找字符串 | 构造触发条件,断言**保护真的生效**(如请求被拒绝) |
|
||||
| 某人没删掉某条判据 | 读测试文件找函数名 | CI 层守卫:`git diff --name-status` 检测删除 |
|
||||
|
||||
**唯一的例外**:CI 配置自检(如"排除表不许静默退化")。
|
||||
那类检查的**对象就是配置文件本身**,读它是正当的 —— 因为它没有别的
|
||||
可观测行为。但即便如此,也要在注释里写明"这是配置自检,不是行为判据"。
|
||||
|
||||
### 5.2.1 判据不得以文档为依据
|
||||
|
||||
**"测试引用文档"本身就是错的** —— 文档可以被改、被删、被改写,
|
||||
而判据必须锚在**代码的当前行为**上。
|
||||
|
||||
实测反例(`internal/agent/core/interrupt_invariant_test.go:336`):
|
||||
|
||||
```go
|
||||
// 事实清单(来自探测,见 docs/zh/interrupt-subsystem-split-plan.md §1.5)。
|
||||
// 这里只断言"这些字段在 scheduler 上存在",作为改写时的锚点;
|
||||
```
|
||||
|
||||
问题有三层:
|
||||
|
||||
1. **它引用的是一份"未实施计划"** —— 被引用方自己写着
|
||||
「状态:计划(未实施)」。判据锚在一个**不存在的事实**上。
|
||||
2. **它没测任何契约**。实际断言只有"入队后队列里 1 条",
|
||||
而那是中断功能最基本的正确性 —— 跟它声称的"反向依赖现状快照"毫无关系。
|
||||
3. **它把"未做的工作"变成了通过的判据**。改任何东西都不会让它红,
|
||||
除非入队函数彻底坏掉;而那时别的判据早就红了。
|
||||
|
||||
⇒ 出现这种形态时,正确处置是**删掉它**(或改写成真判据),
|
||||
然后把这个待办登记进 `future-plan.md`。
|
||||
**不存在"用一条永远绿的判据占位待办"这种合法形态。**
|
||||
|
||||
**判据要问**:这条判据的依据是代码行为,还是一份可能会变的文档?
|
||||
如果是后者,它保护的不是系统,是文字。
|
||||
|
||||
### 5.3 测试文件与业务代码分离
|
||||
|
||||
**现状(2026-11-04 实测)**:
|
||||
|
||||
```text
|
||||
全仓测试文件 521 个
|
||||
测试代码行数 112,068 行
|
||||
internal/agent/core/ 196 个文件里 144 个是测试(73%)
|
||||
internal/plugins/webui/ 29 个测试文件,全部 package webui(白盒)
|
||||
全仓 package X_test 仅 3 个
|
||||
```
|
||||
|
||||
问题不是"测试多",而是:
|
||||
|
||||
1. `ls internal/agent/core/` 看不出这个模块由什么组成
|
||||
2. 100% 白盒 ⇒ 100% 的判据都能碰内部
|
||||
3. 想改成黑箱必须先移动文件,而移动文件又会被源码扫描类判据挡住
|
||||
|
||||
**约定**(新代码必须遵守,存量逐步迁):
|
||||
|
||||
- **黑箱判据用 `package <pkg>_test`**,放 `*_contract_test.go`
|
||||
- **白盒判据**(仅 §5.1 的三个例外)放 `*_internal_test.go`,
|
||||
并在文件头注明"为什么必须白盒"
|
||||
- 新增判据**默认黑箱**;要写白盒必须在文件头回答:
|
||||
这条判据能用黑箱表达吗?为什么不能?
|
||||
|
||||
**这不是风格问题**:一个包里如果 90% 判据是白盒,这个包的模块边界
|
||||
事实上不存在 —— 谁都不敢改内部实现。
|
||||
|
||||
### 5.4 CI 必须覆盖每个独立 module
|
||||
|
||||
**实测缺口(2026-11-04)**:主仓有 `third_party/hologramagentsdk/`,
|
||||
它有自己独立的 `go.mod`(模块 `github.com/JianFeeeee/hologramagentsdk`)
|
||||
**和独立的 `.git`**(remote: `github.com/JianFeeeee/hologramagentsdk`)。
|
||||
|
||||
后果:
|
||||
|
||||
```text
|
||||
go list ./... | grep third_party → 0 个
|
||||
```
|
||||
|
||||
主仓的 `go test ./...` **永远不包含** SDK 的任何测试。
|
||||
|
||||
- SDK 仓自己的 CI 会跑(已核实它的 `.github/workflows/ci.yml` 有
|
||||
`go test ./...` 且单独处理了嵌套 module)
|
||||
- 但**主仓 CI 只验证"能编译"**,不验证"SDK 的判据还绿"
|
||||
|
||||
⇒ 主仓改动可能依赖 SDK 的内部行为,而 SDK 判据红了主仓不知道。
|
||||
|
||||
**规则**:
|
||||
|
||||
1. 子目录里出现 `go.mod` ⇒ 它是独立 module,`go test ./...` 覆盖不到
|
||||
2. 每个独立 module 必须在 CI 里有**自己的**测试步骤
|
||||
3. 有独立 `.git` 的目录**不由主仓管理**,主仓只 vendor 必需文件;
|
||||
`go.mod` 的 `replace` 指向它时,CI 必须确保这些文件存在且与
|
||||
上游 HEAD 一致
|
||||
|
||||
### 5.5 CI 与本地必须跑同一套门
|
||||
|
||||
**实测缺口**:提交纪律要求 `gofmt -l` 干净,但三个 workflow 里
|
||||
`gofmt` 出现 **0 次**。纪律只靠人遵守,CI 不兜底。
|
||||
|
||||
同理缺 `staticcheck` / `govulncheck` / coverage。
|
||||
|
||||
**规则**:写进提交纪律的每一条,都必须在 CI 里有对应步骤。
|
||||
否则它不是纪律,是**建议**。
|
||||
|
||||
| 纪律条目 | CI 有没有兜底 |
|
||||
| --- | --- |
|
||||
| `go build ./...` | ✅ |
|
||||
| `go vet ./...` | ✅ |
|
||||
| `gofmt -l` 干净 | ❌ **必须补** |
|
||||
| `go test -race` 全绿 | ✅ |
|
||||
| 干净 worktree 能编译 | ❌ 靠人 |
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 附:这些教训的共同点
|
||||
@ -174,6 +355,9 @@
|
||||
- 单数路径对 ≠ 复数路径也对
|
||||
- 名字相同 ≠ 是同一个东西
|
||||
- 计划写完 ≠ 事情做完
|
||||
- **判据通过 ≠ 契约被保护**(它可能钉的是实现,见 §5.1)
|
||||
- **`go test ./...` 绿 ≠ 全部测试跑过**(独立 module 不在里面,见 §5.4)
|
||||
- **纪律写着 ≠ 有人执行**(CI 没兜底的纪律只是建议,见 §5.5)
|
||||
|
||||
**所以写判据、写文档、写代码时,都要问一句:我现在拿到的这个信号,
|
||||
在什么情况下会是假的?**
|
||||
|
||||
Reference in New Issue
Block a user