package service import ( "context" "testing" "git.threesix.ai/jordan/persona-community-5/pkg/logging" "git.threesix.ai/jordan/persona-community-5/pkg/queue" "git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/adapter/memory" "git.threesix.ai/jordan/persona-community-5/services/persona-api/internal/domain" ) // mockProducer implements queue.Producer for testing. type mockProducer struct { jobs []struct { jobType string payload map[string]any } } var _ queue.Producer = (*mockProducer)(nil) func (m *mockProducer) Enqueue(_ context.Context, jobType string, payload map[string]any) (string, error) { m.jobs = append(m.jobs, struct { jobType string payload map[string]any }{jobType: jobType, payload: payload}) return "test-job-id", nil } func (m *mockProducer) EnqueueWithOptions(_ context.Context, job queue.Job) (string, error) { m.jobs = append(m.jobs, struct { jobType string payload map[string]any }{jobType: job.Type, payload: job.Payload}) return "test-job-id", nil } func TestPersonaService_Create(t *testing.T) { repo := memory.NewPersonaRepository() q := &mockProducer{} svc := NewPersonaService(repo, q, nil, logging.Nop()) t.Run("creates persona with custom name", func(t *testing.T) { persona, err := svc.Create(context.Background(), "mysterious woman with dark hair", "woman", "Luna Shadow") if err != nil { t.Fatalf("unexpected error: %v", err) } if persona.Name != "Luna Shadow" { t.Errorf("expected name 'Luna Shadow', got '%s'", persona.Name) } if persona.ID.IsZero() { t.Error("expected non-empty ID") } if persona.Gender != "woman" { t.Errorf("expected gender 'woman', got '%s'", persona.Gender) } if persona.Status != domain.PersonaStatusPending { t.Errorf("expected status 'pending', got '%s'", persona.Status) } if persona.Handle == "" { t.Error("expected non-empty handle") } }) t.Run("creates persona with generated name", func(t *testing.T) { persona, err := svc.Create(context.Background(), "energetic man who loves music", "man", "") if err != nil { t.Fatalf("unexpected error: %v", err) } if persona.Name == "" { t.Error("expected auto-generated name") } if persona.ID.IsZero() { t.Error("expected non-empty ID") } }) t.Run("enqueues generate_spec job", func(t *testing.T) { found := false for _, j := range q.jobs { if j.jobType == "generate_spec" { found = true if j.payload["stage"] != "spec" { t.Errorf("expected stage 'spec', got %v", j.payload["stage"]) } if _, ok := j.payload["persona_id"]; !ok { t.Error("expected persona_id in job payload") } } } if !found { t.Error("expected generate_spec job to be enqueued") } }) t.Run("initializes empty slices", func(t *testing.T) { persona, err := svc.Create(context.Background(), "test persona for slices", "non_binary", "SliceTest") if err != nil { t.Fatalf("unexpected error: %v", err) } if persona.Tags == nil { t.Error("expected Tags to be initialized (not nil)") } if persona.ImageURLs == nil { t.Error("expected ImageURLs to be initialized (not nil)") } if persona.VideoURLs == nil { t.Error("expected VideoURLs to be initialized (not nil)") } }) } func TestPersonaService_GetByID(t *testing.T) { repo := memory.NewPersonaRepository() q := &mockProducer{} svc := NewPersonaService(repo, q, nil, logging.Nop()) created, _ := svc.Create(context.Background(), "test persona", "woman", "GetTest") t.Run("returns existing persona", func(t *testing.T) { persona, err := svc.GetByID(context.Background(), created.ID) if err != nil { t.Fatalf("unexpected error: %v", err) } if persona.Name != "GetTest" { t.Errorf("expected name 'GetTest', got '%s'", persona.Name) } }) t.Run("returns not found for missing persona", func(t *testing.T) { _, err := svc.GetByID(context.Background(), "nonexistent-id") if err != domain.ErrPersonaNotFound { t.Errorf("expected ErrPersonaNotFound, got %v", err) } }) } func TestPersonaService_List(t *testing.T) { repo := memory.NewPersonaRepository() q := &mockProducer{} svc := NewPersonaService(repo, q, nil, logging.Nop()) t.Run("returns empty list initially", func(t *testing.T) { personas, err := svc.List(context.Background(), 20, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 0 { t.Errorf("expected 0 personas, got %d", len(personas)) } }) // Create some personas _, _ = svc.Create(context.Background(), "persona one", "woman", "ListOne") _, _ = svc.Create(context.Background(), "persona two", "man", "ListTwo") _, _ = svc.Create(context.Background(), "persona three", "non_binary", "ListThree") t.Run("returns all personas", func(t *testing.T) { personas, err := svc.List(context.Background(), 20, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 3 { t.Errorf("expected 3 personas, got %d", len(personas)) } }) t.Run("respects limit", func(t *testing.T) { personas, err := svc.List(context.Background(), 2, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 2 { t.Errorf("expected 2 personas, got %d", len(personas)) } }) t.Run("clamps negative limit to default", func(t *testing.T) { personas, err := svc.List(context.Background(), -1, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 3 { t.Errorf("expected 3 personas (default limit), got %d", len(personas)) } }) t.Run("clamps limit above 100", func(t *testing.T) { personas, err := svc.List(context.Background(), 200, 0) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 3 { t.Errorf("expected 3 personas (all within max limit), got %d", len(personas)) } }) t.Run("respects offset", func(t *testing.T) { personas, err := svc.List(context.Background(), 20, 2) if err != nil { t.Fatalf("unexpected error: %v", err) } if len(personas) != 1 { t.Errorf("expected 1 persona, got %d", len(personas)) } }) } func TestGenerateHandle(t *testing.T) { tests := []struct { name string input string }{ {name: "simple name", input: "Luna Shadow"}, {name: "special chars", input: "DJ Beats!@#"}, {name: "unicode", input: "Café Noir"}, {name: "long name", input: "This Is A Very Long Name That Exceeds The Maximum Handle Length Allowed"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { handle := generateHandle(tt.input) if handle == "" { t.Error("expected non-empty handle") } if len(handle) > 50 { t.Errorf("handle too long: %d chars", len(handle)) } }) } } func TestGeneratePersonaName(t *testing.T) { tests := []struct { name string desc string check func(string) bool }{ { name: "short description", desc: "mysterious woman", check: func(s string) bool { return s != "" }, }, { name: "long description uses first 3 words", desc: "mysterious woman with dark hair who loves poetry", check: func(s string) bool { return s != "" && len(s) <= 50 }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { name := generatePersonaName(tt.desc) if !tt.check(name) { t.Errorf("unexpected name for desc %q: %q", tt.desc, name) } }) } }