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)