diff --git a/internal/memory/social/test/social_contract_test.go b/internal/memory/social/test/social_contract_test.go new file mode 100644 index 00000000..326334e9 --- /dev/null +++ b/internal/memory/social/test/social_contract_test.go @@ -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) + } +} diff --git a/internal/memory/text/test/text_contract_test.go b/internal/memory/text/test/text_contract_test.go new file mode 100644 index 00000000..e0c038e5 --- /dev/null +++ b/internal/memory/text/test/text_contract_test.go @@ -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") + } +} diff --git a/pkg/embedding/test/registry_contract_test.go b/pkg/embedding/test/registry_contract_test.go new file mode 100644 index 00000000..a32f8ff7 --- /dev/null +++ b/pkg/embedding/test/registry_contract_test.go @@ -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) + } + } +} diff --git a/pkg/generation/test/registry_contract_test.go b/pkg/generation/test/registry_contract_test.go new file mode 100644 index 00000000..4dbf53a3 --- /dev/null +++ b/pkg/generation/test/registry_contract_test.go @@ -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) + } +}