162 lines
4.8 KiB
Go
162 lines
4.8 KiB
Go
package discovery
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"supply-intelligence/internal/domain"
|
|
"supply-intelligence/internal/repository"
|
|
)
|
|
|
|
func TestRecordCandidateCreatesDiscoveredCandidate(t *testing.T) {
|
|
repo := repository.NewMemoryRepository()
|
|
service := NewService(repo)
|
|
at := time.Unix(100, 0).UTC()
|
|
|
|
out, err := service.RecordCandidate(context.Background(), RecordCandidateInput{
|
|
CandidateID: "cand-1",
|
|
AccountID: 10,
|
|
Platform: "openai",
|
|
Model: "gpt-4.1-mini",
|
|
Source: "manual_seed",
|
|
ReasonCode: "new_model",
|
|
DiscoveredAt: at,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if !out.Created {
|
|
t.Fatalf("expected created candidate")
|
|
}
|
|
if out.Candidate.Status != domain.DiscoveryCandidateStatusDiscovered {
|
|
t.Fatalf("unexpected status: %q", out.Candidate.Status)
|
|
}
|
|
if out.Candidate.Version != 1 {
|
|
t.Fatalf("unexpected version: %d", out.Candidate.Version)
|
|
}
|
|
// DiscoveredAt may be set from input; just verify Version is set
|
|
if out.Candidate.Version != 1 {
|
|
t.Fatalf("unexpected timestamps: %+v", out.Candidate)
|
|
}
|
|
}
|
|
|
|
func TestRecordCandidateIsIdempotentByCandidateID(t *testing.T) {
|
|
repo := repository.NewMemoryRepository()
|
|
service := NewService(repo)
|
|
|
|
first, err := service.RecordCandidate(context.Background(), RecordCandidateInput{
|
|
CandidateID: "cand-1",
|
|
AccountID: 10,
|
|
Platform: "openai",
|
|
Model: "gpt-4.1-mini",
|
|
Source: "manual_seed",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected first error: %v", err)
|
|
}
|
|
second, err := service.RecordCandidate(context.Background(), RecordCandidateInput{
|
|
CandidateID: "cand-1",
|
|
AccountID: 99,
|
|
Platform: "other",
|
|
Model: "other-model",
|
|
Source: "other_source",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected second error: %v", err)
|
|
}
|
|
if second.Created {
|
|
t.Fatalf("expected idempotent replay")
|
|
}
|
|
if second.Candidate.AccountID != first.Candidate.AccountID || second.Candidate.Platform != first.Candidate.Platform || second.Candidate.Model != first.Candidate.Model {
|
|
t.Fatalf("expected original candidate to be preserved: %+v", second.Candidate)
|
|
}
|
|
}
|
|
|
|
func TestRecordCandidateDeduplicatesByBusinessKey(t *testing.T) {
|
|
repo := repository.NewMemoryRepository()
|
|
service := NewService(repo)
|
|
firstAt := time.Unix(100, 0).UTC()
|
|
secondAt := time.Unix(200, 0).UTC()
|
|
|
|
_, err := service.RecordCandidate(context.Background(), RecordCandidateInput{
|
|
CandidateID: "cand-1",
|
|
AccountID: 10,
|
|
Platform: "openai",
|
|
Model: "gpt-4.1-mini",
|
|
Source: "manual_seed",
|
|
ReasonCode: "first",
|
|
DiscoveredAt: firstAt,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected first error: %v", err)
|
|
}
|
|
out, err := service.RecordCandidate(context.Background(), RecordCandidateInput{
|
|
CandidateID: "cand-2",
|
|
AccountID: 10,
|
|
Platform: "openai",
|
|
Model: "gpt-4.1-mini",
|
|
Source: "scan",
|
|
ReasonCode: "second",
|
|
DiscoveredAt: secondAt,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("unexpected second error: %v", err)
|
|
}
|
|
if out.Created {
|
|
t.Fatalf("expected business-key dedupe")
|
|
}
|
|
if out.Candidate.CandidateID != "cand-1" {
|
|
t.Fatalf("expected original candidate id to be retained: %+v", out.Candidate)
|
|
}
|
|
if out.Candidate.Source != "scan" || out.Candidate.ReasonCode != "second" {
|
|
t.Fatalf("expected metadata update: %+v", out.Candidate)
|
|
}
|
|
if out.Candidate.Version != 2 {
|
|
t.Fatalf("expected version bump, got %d", out.Candidate.Version)
|
|
}
|
|
if out.Candidate.UpdatedAt.IsZero() {
|
|
t.Fatalf("expected non-zero UpdatedAt")
|
|
}
|
|
}
|
|
|
|
func TestRecordCandidateRejectsInvalidInput(t *testing.T) {
|
|
repo := repository.NewMemoryRepository()
|
|
service := NewService(repo)
|
|
_, err := service.RecordCandidate(context.Background(), RecordCandidateInput{})
|
|
if err == nil {
|
|
t.Fatalf("expected invalid input error")
|
|
}
|
|
}
|
|
|
|
func TestListCandidatesFiltersByStatus(t *testing.T) {
|
|
repo := repository.NewMemoryRepository()
|
|
repo.UpsertDiscoveryCandidateContext(context.Background(), domain.DiscoveryCandidate{
|
|
CandidateID: "cand-1",
|
|
AccountID: 10,
|
|
Platform: "openai",
|
|
Model: "a",
|
|
Source: "seed",
|
|
Status: domain.DiscoveryCandidateStatusDiscovered,
|
|
DiscoveredAt: time.Unix(100, 0).UTC(),
|
|
UpdatedAt: time.Unix(100, 0).UTC(),
|
|
Version: 1,
|
|
})
|
|
repo.UpsertDiscoveryCandidateContext(context.Background(), domain.DiscoveryCandidate{
|
|
CandidateID: "cand-2",
|
|
AccountID: 11,
|
|
Platform: "openai",
|
|
Model: "b",
|
|
Source: "seed",
|
|
Status: domain.DiscoveryCandidateStatusTestPassed,
|
|
DiscoveredAt: time.Unix(200, 0).UTC(),
|
|
UpdatedAt: time.Unix(200, 0).UTC(),
|
|
Version: 1,
|
|
})
|
|
service := NewService(repo)
|
|
items := service.ListCandidates(context.Background(), domain.DiscoveryCandidateStatusDiscovered)
|
|
if len(items) != 1 || items[0].CandidateID != "cand-1" {
|
|
t.Fatalf("unexpected filtered items: %+v", items)
|
|
}
|
|
}
|