测试指南
wesgine 使用多层测试策略确保引擎的正确性和稳定性:单元测试、集成测试、闸门测试(audit-lock)和自研静态分析器(internal/lint/)共同构成验证体系。
验证命令
所有验证通过一个命令完成:
make verify
这个目标包含:
go build ./...— 编译go vet ./...— 标准静态分析internal/lint/— 自研 analyzerscripts/audit-lock/— 闸门测试- 设计文档验证
go test ./...— 单元测试和集成测试
步骤清单只在 Makefile 的 verify 目标里定义一份——新增检查加进那里,不要另起套件。
单元测试
组织原则
- 测试文件与被测文件同目录
- 文件命名:
xxx_test.go - 测试函数命名:
TestXxx_Yyy(被测函数名_场景)
运行
# 全部测试
go test ./...
# 指定包
go test ./internal/cell ./internal/agentcatalog
# 指定测试函数
go test ./internal/memory -run TestSave_TrustZeroValueBackfill
# 带 race 检测
go test -race ./internal/memory/...
# 显示详细输出
go test -v ./internal/cell/...
测试模式
行为测试
测试应该断言行为关系,而不是冻结当前值:
// ✅ 好的:断言行为关系
func TestSave_EmptyKeyGetsContentDedupKey(t *testing.T) {
entry := MemoryEntry{Content: "test", Kind: KindFact}
id, err := store.Save(ctx, entry)
require.NoError(t, err)
got, _ := store.Get(ctx, id)
assert.NotEmpty(t, got.Key) // 行为:空 key 会被自动填充
}
// ❌ 坏的:冻结具体值
func TestSave_HasSpecificKey(t *testing.T) {
// ...
assert.Equal(t, "content:abc123", got.Key) // 快照测试
}
负向测试
每个新 guard 都要有能恢复目标缺陷并使其在正确原因上失败的负向测试:
func TestSave_RejectsEmptyKind(t *testing.T) {
entry := MemoryEntry{Content: "test"} // Kind 为空
_, err := store.Save(ctx, entry)
assert.ErrorIs(t, err, ErrKindRequired)
}
并发测试
并发测试必须证明竞态条件存在——拆开临界区确认它变红:
func TestChannelQuota_ConcurrentAdd(t *testing.T) {
const workers = 10
var wg sync.WaitGroup
var successCount atomic.Int32
for i := 0; i < workers; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
err := adapter.AddChannel(ctx, channelConfig(id))
if err == nil {
successCount.Add(1)
}
}(i)
}
wg.Wait()
// cap=5,只有 5 个成功
assert.Equal(t, int32(5), successCount.Load())
}
测试辅助
// 创建临时 Cell 用于测试
func testCell(t *testing.T) *Cell {
t.Helper()
dir := t.TempDir()
// ...
t.Cleanup(func() { cell.Stop(ctx) })
return cell
}
集成测试
引擎级集成测试
go test ./cmd/wesgine ./internal/cell ./internal/agentcatalog
跨 Cell 隔离测试
修改 Hypervisor/Cell 交界的 PR 必须跨 2+ Cell 验证隔离性:
func TestCrossCell_MemoryIsolation(t *testing.T) {
hyp := testHypervisor(t)
cellA, _ := hyp.Cells().Create(ctx, specA)
cellB, _ := hyp.Cells().Create(ctx, specB)
// Cell A 写入记忆
cellA.Memory().Save(ctx, entry)
// Cell B 不应该看到
results, _ := cellB.Memory().List(ctx, ListOptions{})
assert.Empty(t, results)
}
Live API 测试
tests/live/ 目录包含实时 API 测试,需要运行中的 wesgine 实例:
# 启动 wesgine
bin/wesgine serve --data-dir /tmp/test --addr :9091
# 运行 live 测试
go test ./tests/live/... -tags live
闸门测试(audit-lock)
闸门是 wesgine 的架构守卫——用 shell 脚本检查源码是否满足不变量。闸门集 = scripts/audit-lock/ 文件系统。
闸门列表
| 编号 | 文件 | 守护的不变量 |
|---|---|---|
| 06 | 06-skill-tiere.sh | Tier E 技能不含行业术语 |
| 38 | 38-host-env-shared.sh | 主机环境感知进程级共享 |
| 39 | 39-proc-single-spawn.sh | 子进程创建单点 |
| 40 | 40-bounded-line-reads.sh | 行式读取有界 |
| 41 | 41-hypervisor-whitelist.sh | Hypervisor 方法白名单 |
| 42 | 42-hypervisor-api-sync.sh | Hypervisor API 与分析器同步 |
| 44 | 44-layout-tree.sh | 仓库布局树完整性 |
| 60 | 60-actor-single-judgement.sh | Actor 边界单点判据 |
| 65 | 65-degraded-secret-no-writeback.sh | 降级密钥不回写 |
| 66 | 66-quota-single-writepoint.sh | 配额判据单写点 |
运行闸门
# 运行全部闸门
scripts/audit-lock/run-all.sh
# 运行单个闸门
bash scripts/audit-lock/39-proc-single-spawn.sh
闸门原则
- 闸门能区分代码与注释——手写 grep 不能
- 闸门是双向的——多一个文件和少一个文件都判负
- 闸门的验收标准写在脚本开头注释里
新增闸门
在 scripts/audit-lock/ 下创建编号脚本:
#!/usr/bin/env bash
# Gate 99: 描述不变量
# 守护:INV-XXX-01
set -euo pipefail
# 检查逻辑
if ...; then
echo "PASS: gate-99"
else
echo "FAIL: gate-99: 描述失败原因"
exit 1
fi
自研静态分析器
internal/lint/ 包含 wesgine 自研的 Go AST 分析器:
分析器列表
| 分析器 | 检查内容 |
|---|---|
hypervisornobusiness | Hypervisor 不暴露业务方法 |
| 其他 | 各种架构不变量 |
运行
go vet ./... # 自研分析器随 go vet 一起运行
原理
分析器通过 Go 的 analysis 框架实现,在编译期检查 AST:
var Analyzer = &analysis.Analyzer{
Name: "hypervisornobusiness",
Doc: "检查 Hypervisor 不暴露业务方法",
Run: run,
}
func run(pass *analysis.Pass) (interface{}, error) {
// 遍历 AST,检查 Hypervisor 类型的方法
// ...
}
设计文档验证
验证设计文档的完整性和一致性:
# 作为 make verify 的一部分自动运行
# 检查内容:
# - 文档引用的文件是否存在
# - 不变量编号是否连续
# - 闸门编号与脚本对应
测试最佳实践
DO
- 测试行为,不测试实现——断言输入到输出的关系
- 测试要有负向用例——不只测 happy path
- 并发测试要能变红——拆开临界区验证
- 跨 Cell 隔离必测——修改边界代码时
- 使用
t.TempDir()——不污染真实环境 - 使用
t.Cleanup()——确保资源释放
DON'T
- 不写快照测试——冻结当前值的测试是 change-detector
- 不 mock 整个世界——优先用真实 SQLite 而非 mock
- 不忽略
-race——并发 bug 只有 race detector 能发现 - 不在测试中
time.Sleep——使用 channel 或 condition 同步 - 不跳过
make verify——它是最终门禁
CI 集成
最小验证
# 每个 PR 必须通过
make verify
完整验证
# 发布前
make verify
go test -race -count=3 ./...
特定包深度测试
# 记忆子系统
go test -race -count=3 ./internal/memory/...
# Cell 生命周期
go test -race -count=3 ./internal/cell/...
# 治理
go test -race -count=3 ./internal/govern/...
故障排查
| 症状 | 可能原因 | 排查方法 |
|---|---|---|
| 闸门失败 | 源码不满足不变量 | 读闸门脚本的注释了解预期 |
| race 检测报错 | 并发访问无保护 | 查看 race 报告的 goroutine 栈 |
| 测试超时 | 死锁或无限循环 | go test -timeout 30s 缩短超时 |
| 测试在 CI 失败本地通过 | 环境差异或时序依赖 | 检查 TZ、LANG、并发度 |