Files
2026-06-22 08:55:57 +08:00

134 lines
4.5 KiB
Go

package projectmemory
import (
"context"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/require"
"github.com/yan1h/agent-coding-workflow/internal/infra/llm"
)
// fakeRepo is an in-memory Repository for service tests.
type fakeRepo struct {
entries map[uuid.UUID]*Entry
embedding map[uuid.UUID][]float32
knnResult []Entry // returned by SearchKNN
keywordHits []Entry // returned by SearchKeyword
knnCalls int
kwCalls int
}
func newFakeRepo() *fakeRepo {
return &fakeRepo{entries: map[uuid.UUID]*Entry{}, embedding: map[uuid.UUID][]float32{}}
}
func (r *fakeRepo) Insert(_ context.Context, in WriteInput, model *string) (*Entry, error) {
e := &Entry{
ID: uuid.New(), ProjectID: in.ProjectID, WorkspaceID: in.WorkspaceID,
Kind: in.Kind, Title: in.Title, Body: in.Body, Tags: in.Tags, Source: in.Source,
EmbeddingModel: model, CreatedBy: in.CreatedBy, CreatedAt: time.Now(), UpdatedAt: time.Now(),
}
if e.Tags == nil {
e.Tags = []string{}
}
r.entries[e.ID] = e
return e, nil
}
func (r *fakeRepo) SetEmbedding(_ context.Context, id uuid.UUID, vec []float32, _ string) error {
r.embedding[id] = vec
return nil
}
func (r *fakeRepo) Get(_ context.Context, id uuid.UUID) (*Entry, error) { return r.entries[id], nil }
func (r *fakeRepo) ListByProject(_ context.Context, _ uuid.UUID, _ string) ([]Entry, error) {
out := make([]Entry, 0, len(r.entries))
for _, e := range r.entries {
out = append(out, *e)
}
return out, nil
}
func (r *fakeRepo) SearchKeyword(_ context.Context, _ uuid.UUID, _, _ string, _ int) ([]Entry, error) {
r.kwCalls++
return r.keywordHits, nil
}
func (r *fakeRepo) SearchKNN(_ context.Context, _ uuid.UUID, _ []float32, _ string, _ int) ([]Entry, error) {
r.knnCalls++
return r.knnResult, nil
}
func (r *fakeRepo) Delete(_ context.Context, id uuid.UUID) error { delete(r.entries, id); return nil }
type fakeSel struct{ emb llm.Embedder }
func (s fakeSel) Embedder(context.Context) (llm.Embedder, uuid.UUID, string, int, error) {
if s.emb == nil {
return nil, uuid.Nil, "", 0, llm.ErrEmbeddingsUnsupported
}
return s.emb, uuid.New(), "fake-model", llm.PlatformEmbeddingDims, nil
}
func TestService_Write_WithEmbedder(t *testing.T) {
repo := newFakeRepo()
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
e, err := svc.Write(context.Background(), WriteInput{
ProjectID: uuid.New(), Kind: KindDecision, Title: "t", Body: "b",
})
require.NoError(t, err)
require.NotNil(t, e.EmbeddingModel)
require.NotEmpty(t, repo.embedding[e.ID], "embedding must be stored")
}
func TestService_Write_WithoutEmbedder(t *testing.T) {
repo := newFakeRepo()
svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled
e, err := svc.Write(context.Background(), WriteInput{
ProjectID: uuid.New(), Kind: KindGotcha, Title: "t", Body: "b",
})
require.NoError(t, err)
require.Nil(t, e.EmbeddingModel)
require.Empty(t, repo.embedding[e.ID])
}
func TestService_Write_InvalidKind(t *testing.T) {
svc := NewService(newFakeRepo(), fakeSel{}, nil)
_, err := svc.Write(context.Background(), WriteInput{ProjectID: uuid.New(), Kind: "bogus", Title: "t", Body: "b"})
require.Error(t, err)
}
func TestService_Search_VectorPath(t *testing.T) {
repo := newFakeRepo()
repo.knnResult = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "hit"}}
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, 1, repo.knnCalls)
require.Equal(t, 0, repo.kwCalls)
}
func TestService_Search_KeywordFallback_NoEmbedder(t *testing.T) {
repo := newFakeRepo()
repo.keywordHits = []Entry{{ID: uuid.New(), Kind: KindDecision, Title: "kw"}}
svc := NewService(repo, fakeSel{emb: nil}, nil) // disabled → keyword path
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, 0, repo.knnCalls)
require.Equal(t, 1, repo.kwCalls)
}
func TestService_Search_VectorEmptyFallsBackToKeyword(t *testing.T) {
repo := newFakeRepo()
repo.knnResult = nil // no embedded entries yet
repo.keywordHits = []Entry{{ID: uuid.New(), Title: "kw"}}
svc := NewService(repo, fakeSel{emb: llm.NewFakeEmbedder()}, nil)
res, err := svc.Search(context.Background(), Query{ProjectID: uuid.New(), Text: "q"})
require.NoError(t, err)
require.Len(t, res, 1)
require.Equal(t, 1, repo.knnCalls)
require.Equal(t, 1, repo.kwCalls)
}
var _ Repository = (*fakeRepo)(nil)