Files
HomeAgent/internal/security/test/codec_contract_test.go
JianFeeeee 8dff678640 test: 按模块导出接口重写黑盒判据(批次 2:security/events/safeclose/kstate)
延续批次 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
2026-11-09 12:21:13 +08:00

141 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package test
// internal/security 的黑盒判据:只用导出构造 + 公开方法,输入 → 输出。
// 不读源码、不碰 Codec/Store 内部字段、不用任何 ForTest 钩子。
import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/security"
)
func fixedKey(byteVal byte) []byte {
k := make([]byte, 32)
for i := range k {
k[i] = byteVal
}
return k
}
func newCodec(t *testing.T, key []byte, domain string) *security.Codec {
t.Helper()
c, err := security.NewCodec(key, domain)
if err != nil {
t.Fatalf("NewCodec: %v", err)
}
return c
}
// TestCodecSealOpenRoundTrip:Seal → Open 得回原文。
func TestCodecSealOpenRoundTrip(t *testing.T) {
c := newCodec(t, fixedKey(7), "test-domain")
const plain = "sk-super-secret-value-123"
sealed, err := c.Seal(plain)
if err != nil {
t.Fatalf("Seal: %v", err)
}
if sealed == plain {
t.Fatal("★ Seal 返回了原文 —— 没有加密")
}
got, err := c.Open(sealed)
if err != nil {
t.Fatalf("Open: %v", err)
}
if got != plain {
t.Fatalf("★ Open = %q,期望 %q", got, plain)
}
}
// TestCodecDomainSeparation:不同 domain 的密文互不通用。
func TestCodecDomainSeparation(t *testing.T) {
a := newCodec(t, fixedKey(7), "domain-a")
b := newCodec(t, fixedKey(7), "domain-b")
sealed, err := a.Seal("payload")
if err != nil {
t.Fatal(err)
}
if _, err := b.Open(sealed); err == nil {
t.Fatal("★ 另一个 domain 能解开密文 —— 域隔离失效")
}
}
// TestCodecWrongKeyCannotOpen:换钥后旧密文不可读。
func TestCodecWrongKeyCannotOpen(t *testing.T) {
a := newCodec(t, fixedKey(1), "d")
b := newCodec(t, fixedKey(2), "d")
sealed, err := a.Seal("payload")
if err != nil {
t.Fatal(err)
}
if _, err := b.Open(sealed); err == nil {
t.Fatal("★ 错误密钥能解开密文")
}
}
// TestCodecEnabledAndDomain:元信息输出。
func TestCodecEnabledAndDomain(t *testing.T) {
c := newCodec(t, fixedKey(3), "my-domain")
if !c.Enabled() {
t.Fatal("★ 有效密钥下 Enabled()=false")
}
if got := c.Domain(); got != "my-domain" {
t.Fatalf("★ Domain() = %q,期望 my-domain", got)
}
var nilCodec *security.Codec
if nilCodec.Enabled() {
t.Fatal("★ nil Codec 报告 Enabled=true")
}
}
// TestCodecOldKeysStillDecrypt:轮换后历史密文仍可读。
func TestCodecOldKeysStillDecrypt(t *testing.T) {
old := fixedKey(4)
cur := fixedKey(5)
oldCodec := newCodec(t, old, "d")
sealed, err := oldCodec.Seal("legacy-value")
if err != nil {
t.Fatal(err)
}
rotated, err := security.NewCodecWithOldKeys(cur, "d", [][]byte{old})
if err != nil {
t.Fatalf("NewCodecWithOldKeys: %v", err)
}
got, err := rotated.Open(sealed)
if err != nil {
t.Fatalf("★ 轮换后旧密文读不出:%v", err)
}
if got != "legacy-value" {
t.Fatalf("★ 旧密文解出 %q", got)
}
}
// TestCodecOpenLegacyPlaintextPassthrough:无密文前缀的旧数据原样透传。
//
// 这是明确的兼容契约:加密是后来加的,库里存在未加密的历史值。
// 把它们判成错误会让整库读不出来。
func TestCodecOpenLegacyPlaintextPassthrough(t *testing.T) {
c := newCodec(t, fixedKey(9), "d")
for _, in := range []string{"", "legacy-plain-value", "abc"} {
got, err := c.Open(in)
if err != nil {
t.Fatalf("★ Open(%q) 报错:%v —— 旧明文应原样透传", in, err)
}
if got != in {
t.Fatalf("★ Open(%q) = %q,期望原样返回", in, got)
}
}
}
// TestCodecOpenRejectsCorruptedCiphertext:带密文前缀但内容坏掉必须报错。
func TestCodecOpenRejectsCorruptedCiphertext(t *testing.T) {
c := newCodec(t, fixedKey(9), "d")
for _, in := range []string{
security.EncPrefixV1 + "!!!not-base64!!!",
security.EncPrefixV1,
} {
if _, err := c.Open(in); err == nil {
t.Fatalf("★ Open(%q) 没报错 —— 损坏密文被静默吞下", in)
}
}
}