mirror of
https://gitcode.com/JianFeeeee/HomeAgent.git
synced 2026-11-12 04:07:34 +00:00
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:
90
internal/memory/social/test/social_contract_test.go
Normal file
90
internal/memory/social/test/social_contract_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
67
internal/memory/text/test/text_contract_test.go
Normal file
67
internal/memory/text/test/text_contract_test.go
Normal 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")
|
||||
}
|
||||
}
|
||||
94
pkg/embedding/test/registry_contract_test.go
Normal file
94
pkg/embedding/test/registry_contract_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
56
pkg/generation/test/registry_contract_test.go
Normal file
56
pkg/generation/test/registry_contract_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user