mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 12:17:36 +00:00
延续批次 1 的形态:只调模块**本来就有**的导出接口,输入 → 可观察输出。
4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 24 条判据,每模块 1 个文件。
## 各模块的接口清单与判据
### internal/security —— NewCodec / Seal / Open / Domain / Enabled
TestCodecSealOpenRoundTrip Seal → Open 得回原文;Seal 不返回原文
TestCodecDomainSeparation 不同 domain 的密文互不通用
TestCodecWrongKeyCannotOpen 换钥后旧密文不可读
TestCodecEnabledAndDomain 元信息输出;nil Codec 不报 Enabled
TestCodecOldKeysStillDecrypt NewCodecWithOldKeys 轮换后旧密文可读
TestCodecOpenLegacyPlaintextPassthrough 旧明文原样透传
TestCodecOpenRejectsCorruptedCiphertext 带密文前缀但内容坏掉必须报错
**判据写错过一次并修正**:第一版把 `Open("not-a-cipher")` 也要求报错,
实测红。查实现(codec.go:138)确认那是**明确的兼容契约** —— 加密是后来加的,
库里存在未加密的历史值,判成错误会让整库读不出来。改成两条:
旧明文透传 / 损坏密文报错。把差异显式写下来。
### internal/events —— NewBus / Subscribe / Publish / SetGuard
TestPublishDeliversPayload 订阅后发布,handler 收到原 Payload
TestUnsubscribeStopsDelivery 退订后不再收到
TestOnlyMatchingTypeReceives 不同事件类型不串台
TestPublisherIdentity 空 Name = 内核,非空 = 插件
TestSetGuardRoundTrip SetGuard → Guard 拿回同一守卫
`Guard` stub 按真实接口签名实现(AllowPublish 返回 `(bool, string)`、
Scrub* 返回 map),不是猜的。
### internal/safeclose —— Safe / SafeFunc
TestSafeFuncIsIdempotent 连调 5 次不 panic,channel 确已关闭
TestSafeReturnsFiredOnce 同一把 Once 只有第一次返回 true
TestSafeRejectsNilInputs nil once / nil chan 安全返回 false
TestConcurrentCloseIsSafe 8 goroutine 并发关同一 channel
不用任何 `NewOnceForTest` 之类的测试钩子 —— `sync.Once` 由调用方自己造,
这正是模块本来的公开用法。
### internal/kstate —— NewSyncBus / Mount / Get / State
TestSyncBusReadyLifecycle MarkDeclared/MarkReady → IsReady/AllReady/Missing
TestSyncBusReadyInOrderIsStable 就绪顺序可复现,重复标记不重复记录
TestSyncBusKnownVsReady 声明过 ≠ 已就绪
TestSyncBusKernelComplete 内核完成标记可观察
TestSyncBusSnapshotIsCopy 外部改快照不影响总线
TestMountGet Mount → Get 拿到同一份状态
TestStateCloseAllInvokesClosers AddCloser 注册的清理被调用
不用 `MountForTest`(上一轮已删除):直接用 `Mount` + `Get` 断言输出。
## 变异验证(改回旧实现必须红)
security Seal 直接返回原文 → ★ Seal 返回了原文 —— 没有加密
events 退订函数变空操作 → ★ 退订后仍收到 1 次
kstate MarkReady 不记 order → ★ ReadyInOrder = [],期望 [a b]
safeclose Safe 不用 sync.Once → panic: close of closed channel
前两条第一版变异因文本形态不匹配**没有变红**,重做后确认判据有效 ——
空变异不算变异验证,这一点按纪律写清楚。
## 验证
go build ./... PASS
go vet ./... PASS
gofmt -l 干净
python3 .github/scripts/check-ci-gates.py PASS
go test ./... -count=1 13 个测试包 / 100 包无测试 / 0 FAIL
4 个新包 -race 全绿
源码目录 _test.go 0
111 lines
3.2 KiB
Go
111 lines
3.2 KiB
Go
package test
|
||
|
||
// internal/kstate 的黑盒判据:只用 NewSyncBus / Mount / Get / State 的导出方法,
|
||
// 输入 → 可观察输出。不读源码、不用 MountForTest。
|
||
|
||
import (
|
||
"testing"
|
||
|
||
"github.com/JianFeeeee/HologramAgent/internal/kstate"
|
||
)
|
||
|
||
// TestSyncBusReadyLifecycle:声明 → 就绪 → IsReady/Snapshot/AllReady/Missing。
|
||
func TestSyncBusReadyLifecycle(t *testing.T) {
|
||
b := kstate.NewSyncBus()
|
||
b.MarkDeclared([]string{"db", "http", "memory"})
|
||
b.MarkReady("db")
|
||
b.MarkReady("http")
|
||
if !b.IsReady("db") || !b.IsReady("http") {
|
||
t.Fatal("★ 已 MarkReady 的设施报告未就绪")
|
||
}
|
||
if b.IsReady("memory") {
|
||
t.Fatal("★ 未就绪的设施报告就绪")
|
||
}
|
||
if b.AllReady([]string{"db", "http"}) != true {
|
||
t.Fatal("★ AllReady 对全部就绪的集合应返回 true")
|
||
}
|
||
if b.AllReady([]string{"db", "memory"}) {
|
||
t.Fatal("★ AllReady 对含未就绪项的集合返回了 true")
|
||
}
|
||
if missing := b.Missing([]string{"db", "http", "memory"}); len(missing) != 1 || missing[0] != "memory" {
|
||
t.Fatalf("★ Missing = %v,期望 [memory]", missing)
|
||
}
|
||
}
|
||
|
||
// TestSyncBusReadyInOrderIsStable:就绪顺序可复现,重复标记不重复记录。
|
||
func TestSyncBusReadyInOrderIsStable(t *testing.T) {
|
||
b := kstate.NewSyncBus()
|
||
b.MarkReady("a")
|
||
b.MarkReady("a")
|
||
b.MarkReady("b")
|
||
got := b.ReadyInOrder()
|
||
if len(got) != 2 || got[0] != "a" || got[1] != "b" {
|
||
t.Fatalf("★ ReadyInOrder = %v,期望 [a b]", got)
|
||
}
|
||
}
|
||
|
||
// TestSyncBusKnownVsReady:声明过但未就绪 ≠ 未声明。
|
||
func TestSyncBusKnownVsReady(t *testing.T) {
|
||
b := kstate.NewSyncBus()
|
||
b.MarkDeclared([]string{"db"})
|
||
if !b.Known("db") {
|
||
t.Fatal("★ 声明过的设施 Known=false")
|
||
}
|
||
if b.IsReady("db") {
|
||
t.Fatal("★ 只声明未就绪却报告 IsReady")
|
||
}
|
||
if b.Known("ghost") {
|
||
t.Fatal("★ 未声明的设施 Known=true")
|
||
}
|
||
}
|
||
|
||
// TestSyncBusKernelComplete:内核完成标记是可观察输出。
|
||
func TestSyncBusKernelComplete(t *testing.T) {
|
||
b := kstate.NewSyncBus()
|
||
if b.KernelComplete() {
|
||
t.Fatal("★ 初始就报告内核已完成")
|
||
}
|
||
b.MarkKernelComplete()
|
||
if !b.KernelComplete() {
|
||
t.Fatal("★ MarkKernelComplete 后仍报告未完成")
|
||
}
|
||
}
|
||
|
||
// TestSyncBusSnapshotIsCopy:改快照不得影响总线。
|
||
func TestSyncBusSnapshotIsCopy(t *testing.T) {
|
||
b := kstate.NewSyncBus()
|
||
b.MarkReady("db")
|
||
snap := b.Snapshot()
|
||
if !snap["db"] {
|
||
t.Fatal("★ Snapshot 缺 db")
|
||
}
|
||
snap["db"] = false
|
||
snap["ghost"] = true
|
||
if !b.IsReady("db") {
|
||
t.Fatal("★ 外部改快照影响了总线内部")
|
||
}
|
||
if b.IsReady("ghost") {
|
||
t.Fatal("★ 外部往快照塞值影响了总线内部")
|
||
}
|
||
}
|
||
|
||
// TestMountGet:Mount → Get 拿到同一份状态(模块最核心的输入→输出)。
|
||
func TestMountGet(t *testing.T) {
|
||
s := &kstate.State{DataDir: "/contract/tmp"}
|
||
kstate.Mount(s)
|
||
if got := kstate.Get(); got != s {
|
||
t.Fatalf("★ Get() = %v,期望刚 Mount 的那份", got)
|
||
}
|
||
}
|
||
|
||
// TestStateCloseAllInvokesClosers:AddCloser 注册的清理必须被调用一次。
|
||
func TestStateCloseAllInvokesClosers(t *testing.T) {
|
||
s := kstate.New()
|
||
n := 0
|
||
s.AddCloser(func() { n++ })
|
||
s.CloseAll()
|
||
if n != 1 {
|
||
t.Fatalf("★ CloseAll 后 closer 调用 %d 次,期望 1", n)
|
||
}
|
||
}
|