You've already forked agentic-coding-workflow
bugfix
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user