test: 按模块导出接口重写黑盒判据(批次 8:embedding / generation / text / social)

4 个文件全部在 `<module>/test/`,源码目录仍零 `_test.go`。
共 18 条判据,只调导出接口,输入 → 可观察输出。

## pkg/embedding —— 注册表 + 校验

    TestRegisterThenOpen                 注册 → Names/Open → Info/Embed 输出
    TestOpenUnknownProviderReportsError  未注册名报错且可诊断
    TestValidateInfo                     维度/指纹真值表
    TestValidateVector                   维度不符 / NaN / Inf 必拒

## pkg/generation —— 注册表

    TestRegisterThenOpen                 注册 → Names/Open → Info/Generate 输出
    TestOpenUnknownProviderReportsError  未注册名报错

## internal/memory/text —— 真实临时目录

    TestAppendThenRecentEvents           Append → RecentEvents 读回内容
    TestReplayVisitsAppendedEvents       Replay 收到全部事件
    TestFileCountAndStats                写后文件数/统计可用

## internal/memory/social —— 真实内存图库

    TestNilDBReportsError                无图库时写操作报错(不静默)
    TestSetTraitThenGetTrait             特质往返
    TestSetTraitOverwrites               同键覆盖
    TestAddRelationThenGetRelations      关系往返
    TestListPersons                      人物列表含已写入的人

## 变异验证(改回旧实现必须红)

    embedding 非有限值检查留 NaN 通过 → ★ 非法向量被接受:[NaN 2 3]
    generation 未注册名静默返回 nil   → ★ 未注册 provider 的报错不可诊断
    text       Append 不写盘          → ★ RecentEvents = 0 条
    social     SetTrait 不写          → ★ 无图库却 SetTrait 成功

前两条第一版变异分别造成「编译失败(math 未使用)」和「panic」,
不是干净的断言红 —— 改成只改行为、仍能编译的变异后确认有效。

## 验证

    go build ./...                            PASS
    go vet ./...                              PASS
    gofmt -l                                  干净
    python3 .github/scripts/check-ci-gates.py PASS
    go test ./... -count=1                    32 个测试包 / 100 包无测试 / 0 FAIL
    4 个新包 -race                            全绿
    源码目录 _test.go                         0
This commit is contained in:
JianFeeeee
2026-11-09 13:55:15 +08:00
parent 379300d89c
commit ac525670d1
4 changed files with 307 additions and 0 deletions

View File

@ -0,0 +1,90 @@
package test
// internal/memory/social 的黑盒判据:New(GraphDB) → SetTrait/AddRelation → 输出。
// 用真实内存图库,不碰 s.db 内部。
import (
"path/filepath"
"testing"
"github.com/JianFeeeee/HologramAgent/internal/memory"
"github.com/JianFeeeee/HologramAgent/internal/memory/social"
)
func newStore(t *testing.T) *social.SocialStore {
t.Helper()
db, err := memory.NewGraphDB(filepath.Join(t.TempDir(), "graph.db"))
if err != nil {
t.Fatalf("NewGraphDB: %v", err)
}
t.Cleanup(func() { _ = db.Close() })
return social.New(db)
}
// TestNilDBReportsError:未装配图库时每个写操作都报错(不静默)。
func TestNilDBReportsError(t *testing.T) {
s := social.New(nil)
if err := s.SetTrait("张三", "职业", "工程师"); err == nil {
t.Fatal("★ 无图库却 SetTrait 成功")
}
if err := s.AddRelation("张三", "朋友", "李四"); err == nil {
t.Fatal("★ 无图库却 AddRelation 成功")
}
}
// TestSetTraitThenGetTrait:特质写入后能读回。
func TestSetTraitThenGetTrait(t *testing.T) {
s := newStore(t)
if err := s.SetTrait("张三", "职业", "工程师"); err != nil {
t.Fatalf("SetTrait: %v", err)
}
got, ok := s.GetTrait("张三", "职业")
if !ok || got != "工程师" {
t.Fatalf("★ GetTrait = %q / %v,期望 工程师 / true", got, ok)
}
}
// TestSetTraitOverwrites:同人同特质再设值覆盖,不出现两份。
func TestSetTraitOverwrites(t *testing.T) {
s := newStore(t)
_ = s.SetTrait("张三", "职业", "工程师")
_ = s.SetTrait("张三", "职业", "教师")
got, _ := s.GetTrait("张三", "职业")
if got != "教师" {
t.Fatalf("★ 覆盖后 GetTrait = %q,期望 教师", got)
}
}
// TestAddRelationThenGetRelations:关系写入后能读回。
func TestAddRelationThenGetRelations(t *testing.T) {
s := newStore(t)
if err := s.AddRelation("张三", "朋友", "李四"); err != nil {
t.Fatalf("AddRelation: %v", err)
}
rels, err := s.GetRelations("张三")
if err != nil {
t.Fatalf("GetRelations: %v", err)
}
if len(rels) == 0 {
t.Fatal("★ GetRelations 返回空")
}
}
// TestListPersons:写入人物后 ListPersons 含该人名。
func TestListPersons(t *testing.T) {
s := newStore(t)
_ = s.SetTrait("王五", "职业", "医生")
persons, err := s.ListPersons()
if err != nil {
t.Fatalf("ListPersons: %v", err)
}
found := false
for _, p := range persons {
if p == "王五" {
found = true
}
}
if !found {
t.Fatalf("★ ListPersons = %v,缺 王五", persons)
}
}

View File

@ -0,0 +1,67 @@
package test
// internal/memory/text 的黑盒判据:New/Start → Append → RecentEvents/Replay/Stats。
// 用真实临时目录,不碰 m.current/m.encoder。
import (
"testing"
"github.com/JianFeeeee/HologramAgent/internal/memory/text"
)
func newMemory(t *testing.T) *text.Memory {
t.Helper()
m := text.New(t.TempDir())
if err := m.Start(); err != nil {
t.Fatalf("Start: %v", err)
}
t.Cleanup(m.Stop)
return m
}
// TestAppendThenRecentEvents:Append 后 RecentEvents 能读回内容。
func TestAppendThenRecentEvents(t *testing.T) {
m := newMemory(t)
if err := m.Append(text.Event{Source: "qq", Input: "你好", Response: "在"}); err != nil {
t.Fatalf("Append: %v", err)
}
evts, err := m.RecentEvents(5)
if err != nil {
t.Fatalf("RecentEvents: %v", err)
}
if len(evts) != 1 {
t.Fatalf("★ RecentEvents = %d 条,期望 1", len(evts))
}
if evts[0].Input != "你好" || evts[0].Response != "在" {
t.Fatalf("★ 内容不符:%+v", evts[0])
}
}
// TestReplayVisitsAppendedEvents:Replay 回调收到已写入的事件。
func TestReplayVisitsAppendedEvents(t *testing.T) {
m := newMemory(t)
_ = m.Append(text.Event{Source: "webui", Input: "A"})
_ = m.Append(text.Event{Source: "webui", Input: "B"})
inputs := map[string]bool{}
if err := m.Replay(func(e text.Event) error {
inputs[e.Input] = true
return nil
}); err != nil {
t.Fatalf("Replay: %v", err)
}
if !inputs["A"] || !inputs["B"] {
t.Fatalf("★ Replay 没收到全部事件:%v", inputs)
}
}
// TestFileCountAndStats:写后文件数/统计输出可用。
func TestFileCountAndStats(t *testing.T) {
m := newMemory(t)
_ = m.Append(text.Event{Source: "s", Input: "x"})
if got := m.FileCount(); got < 1 {
t.Fatalf("★ FileCount = %d,期望 ≥1", got)
}
if st := m.Stats(); st == nil {
t.Fatal("★ Stats() 返回 nil")
}
}

View File

@ -0,0 +1,94 @@
package test
// pkg/embedding 的黑盒判据:Register → Names/Open → 输出;Validate* 的输入→输出。
// 不碰 registry map。
import (
"context"
"math"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/pkg/embedding"
)
type stubProvider struct{ dim int }
func (p *stubProvider) Embed(ctx context.Context, in embedding.Input) ([]float64, error) {
return make([]float64, p.dim), nil
}
func (p *stubProvider) Info() embedding.Info {
return embedding.Info{Dimension: p.dim, Fingerprint: "stub-fp"}
}
func (p *stubProvider) Close() {}
// TestRegisterThenOpen:注册后 Names/Open 能取回该 provider,且 Info 正确。
func TestRegisterThenOpen(t *testing.T) {
const name = "contract-embed-provider"
embedding.Register(name, func(cfg embedding.Config) (embedding.Provider, error) {
return &stubProvider{dim: 8}, nil
})
found := false
for _, n := range embedding.Names() {
if n == name {
found = true
}
}
if !found {
t.Fatal("★ Names() 里没有刚注册的 provider")
}
p, err := embedding.Open(name, embedding.Config{})
if err != nil {
t.Fatalf("Open: %v", err)
}
defer p.Close()
if got := p.Info().Dimension; got != 8 {
t.Fatalf("★ Info.Dimension = %d,期望 8", got)
}
vec, err := p.Embed(context.Background(), embedding.Input{Text: "x"})
if err != nil || len(vec) != 8 {
t.Fatalf("★ Embed = %v / %v", vec, err)
}
}
// TestOpenUnknownProviderReportsError:未注册名字报错且列出可用项。
func TestOpenUnknownProviderReportsError(t *testing.T) {
_, err := embedding.Open("definitely-not-registered-xyz", embedding.Config{})
if err == nil {
t.Fatal("★ 未注册 provider 却 Open 成功")
}
if !strings.Contains(err.Error(), "unknown provider") {
t.Fatalf("★ 报错信息不可诊断:%v", err)
}
}
// TestValidateInfo:维度/指纹的输入→输出。
func TestValidateInfo(t *testing.T) {
cases := []struct {
info embedding.Info
wantErr bool
}{
{embedding.Info{Dimension: 3, Fingerprint: "fp"}, false},
{embedding.Info{Dimension: 0, Fingerprint: "fp"}, true},
{embedding.Info{Dimension: -1, Fingerprint: "fp"}, true},
{embedding.Info{Dimension: 3, Fingerprint: " "}, true},
}
for _, c := range cases {
err := embedding.ValidateInfo(c.info)
if (err != nil) != c.wantErr {
t.Errorf("★ ValidateInfo(%+v) err=%v,wantErr=%v", c.info, err, c.wantErr)
}
}
}
// TestValidateVector:维度不符 / NaN / Inf 必须报错。
func TestValidateVector(t *testing.T) {
if err := embedding.ValidateVector([]float64{1, 2, 3}, 3); err != nil {
t.Fatalf("★ 合法向量被拒:%v", err)
}
for _, bad := range [][]float64{{1, 2}, {math.NaN(), 2, 3}, {math.Inf(1), 2, 3}} {
if err := embedding.ValidateVector(bad, 3); err == nil {
t.Fatalf("★ 非法向量被接受:%v", bad)
}
}
}

View File

@ -0,0 +1,56 @@
package test
// pkg/generation 的黑盒判据:Register → Names/Open → 输出。
import (
"context"
"strings"
"testing"
"github.com/JianFeeeee/HologramAgent/pkg/generation"
)
type stubProvider struct{}
func (p *stubProvider) Generate(ctx context.Context, r generation.Request) (generation.Response, error) {
return generation.Response{Text: "generated"}, nil
}
func (p *stubProvider) Info() generation.Info {
return generation.Info{Model: "stub-model"}
}
func (p *stubProvider) Close() {}
func TestRegisterThenOpen(t *testing.T) {
const name = "contract-gen-provider"
generation.Register(name, func(cfg generation.Config) (generation.Provider, error) {
return &stubProvider{}, nil
})
found := false
for _, n := range generation.Names() {
if n == name {
found = true
}
}
if !found {
t.Fatal("★ Names() 里没有刚注册的 provider")
}
p, err := generation.Open(name, generation.Config{})
if err != nil {
t.Fatalf("Open: %v", err)
}
defer p.Close()
if got := p.Info().Model; got != "stub-model" {
t.Fatalf("★ Info.Model = %q", got)
}
out, err := p.Generate(context.Background(), generation.Request{})
if err != nil || out.Text != "generated" {
t.Fatalf("★ Generate = %+v / %v", out, err)
}
}
func TestOpenUnknownProviderReportsError(t *testing.T) {
_, err := generation.Open("definitely-not-registered-xyz", generation.Config{})
if err == nil || !strings.Contains(err.Error(), "unknown provider") {
t.Fatalf("★ 未注册 provider 的报错不可诊断:%v", err)
}
}