package review import ( "os" "path/filepath" "strings" "testing" ) func TestLoadBuiltinPersona(t *testing.T) { tests := []struct { name string personaName string wantErr bool wantDisplay string }{ { name: "security persona", personaName: "security", wantErr: false, wantDisplay: "Security Specialist", }, { name: "architect persona", personaName: "architect", wantErr: false, wantDisplay: "Software Architect", }, { name: "docs persona", personaName: "docs", wantErr: false, wantDisplay: "Documentation Reviewer", }, { name: "unknown persona", personaName: "nonexistent", wantErr: true, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { p, err := LoadBuiltinPersona(tt.personaName) if tt.wantErr { if err == nil { t.Error("expected error, got nil") } return } if err != nil { t.Fatalf("unexpected error: %v", err) } if p.Name != tt.personaName { t.Errorf("Name = %q, want %q", p.Name, tt.personaName) } if p.DisplayName != tt.wantDisplay { t.Errorf("DisplayName = %q, want %q", p.DisplayName, tt.wantDisplay) } if p.Identity == "" { t.Error("Identity should not be empty") } if len(p.Focus) == 0 { t.Error("Focus should not be empty") } }) } } func TestListBuiltinPersonas(t *testing.T) { names := ListBuiltinPersonas() if len(names) == 0 { t.Fatal("expected at least one built-in persona") } // Check for expected personas expected := map[string]bool{"security": false, "architect": false, "docs": false} for _, name := range names { if _, ok := expected[name]; ok { expected[name] = true } } for name, found := range expected { if !found { t.Errorf("expected built-in persona %q not found", name) } } } func TestLoadPersonaFromYAMLFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.yaml") content := `# Test persona name: test display_name: Test Persona identity: | You are a test persona. Multi-line identity works. focus: - testing - validation ignore: - nothing severity: major: Big problems minor: Small problems nit: Tiny problems ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed: %v", err) } if p.Name != "test" { t.Errorf("Name = %q, want %q", p.Name, "test") } if p.DisplayName != "Test Persona" { t.Errorf("DisplayName = %q, want %q", p.DisplayName, "Test Persona") } if len(p.Focus) != 2 { t.Errorf("Focus len = %d, want 2", len(p.Focus)) } if !strings.Contains(p.Identity, "Multi-line") { t.Error("Identity should contain multi-line content") } } func TestLoadPersonaFromYMLFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.yml") content := `name: test display_name: Test YML identity: Test identity focus: - testing ignore: [] severity: major: Big minor: Small nit: Tiny ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed: %v", err) } if p.Name != "test" { t.Errorf("Name = %q, want %q", p.Name, "test") } if p.DisplayName != "Test YML" { t.Errorf("DisplayName = %q, want %q", p.DisplayName, "Test YML") } } func TestLoadPersonaFromJSONFile(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test.json") content := `{ "name": "test", "display_name": "Test Persona", "identity": "You are a test persona.\nMulti-line identity works.", "focus": ["testing", "validation"], "ignore": ["nothing"], "severity": { "major": "Big problems", "minor": "Small problems", "nit": "Tiny problems" } }` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed: %v", err) } if p.Name != "test" { t.Errorf("Name = %q, want %q", p.Name, "test") } if p.DisplayName != "Test Persona" { t.Errorf("DisplayName = %q, want %q", p.DisplayName, "Test Persona") } if len(p.Focus) != 2 { t.Errorf("Focus len = %d, want 2", len(p.Focus)) } if !strings.Contains(p.Identity, "Multi-line") { t.Error("Identity should contain multi-line content") } } func TestLoadPersonaValidation(t *testing.T) { tests := []struct { name string content string ext string wantErr string }{ { name: "missing name yaml", content: "identity: test\n", ext: ".yaml", wantErr: "name is required", }, { name: "missing identity yaml", content: "name: test\n", ext: ".yaml", wantErr: "identity is required", }, { name: "missing name json", content: `{"identity": "test"}`, ext: ".json", wantErr: "name is required", }, { name: "missing identity json", content: `{"name": "test"}`, ext: ".json", wantErr: "identity is required", }, { name: "display_name defaults to name", content: "name: test\nidentity: test identity\n", ext: ".yaml", // No error expected - should succeed }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test"+tt.ext) if err := os.WriteFile(path, []byte(tt.content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if tt.wantErr != "" { if err == nil { t.Errorf("expected error containing %q, got nil", tt.wantErr) return } if !strings.Contains(err.Error(), tt.wantErr) { t.Errorf("error = %q, want containing %q", err.Error(), tt.wantErr) } return } if err != nil { t.Fatalf("unexpected error: %v", err) } // Check display_name defaulting if p.DisplayName == "" { t.Error("DisplayName should default to Name") } if p.DisplayName != p.Name { t.Errorf("DisplayName should default to Name, got %q", p.DisplayName) } }) } } func TestLoadPersonaFileNotFound(t *testing.T) { _, err := LoadPersona("/nonexistent/path/persona.yaml") if err == nil { t.Error("expected error for nonexistent file") } } func TestLoadPersonaInvalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "invalid.yaml") if err := os.WriteFile(path, []byte("not valid yaml:\n - [broken"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } _, err := LoadPersona(path) if err == nil { t.Error("expected error for invalid YAML") } } func TestLoadPersonaInvalidJSON(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "invalid.json") if err := os.WriteFile(path, []byte("not valid json {"), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } _, err := LoadPersona(path) if err == nil { t.Error("expected error for invalid JSON") } } func TestLoadPersonaCaseInsensitiveExtension(t *testing.T) { tests := []struct { name string ext string }{ {"lowercase yaml", ".yaml"}, {"uppercase YAML", ".YAML"}, {"mixed case Yaml", ".Yaml"}, {"lowercase yml", ".yml"}, {"uppercase YML", ".YML"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "test"+tt.ext) content := "name: test\nidentity: test identity\n" if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed for extension %s: %v", tt.ext, err) } if p.Name != "test" { t.Errorf("Name = %q, want %q", p.Name, "test") } }) } } func TestCapitalizeFirst(t *testing.T) { tests := []struct { input string want string }{ {"hello", "Hello"}, {"Hello", "Hello"}, {"HELLO", "HELLO"}, {"a", "A"}, {"", ""}, {"日本語", "日本語"}, // Non-ASCII: Japanese doesn't have case {"über", "Über"}, // German umlaut {"élève", "Élève"}, // French accent } for _, tt := range tests { t.Run(tt.input, func(t *testing.T) { got := CapitalizeFirst(tt.input) if got != tt.want { t.Errorf("CapitalizeFirst(%q) = %q, want %q", tt.input, got, tt.want) } }) } } func TestListBuiltinPersonasReturnsEmptySlice(t *testing.T) { // ListBuiltinPersonas should return an empty slice (not nil) on error. // We can't easily test the error case, but we can verify the success case // returns a proper slice. names := ListBuiltinPersonas() if names == nil { t.Error("ListBuiltinPersonas should return empty slice, not nil") } } func TestYAMLMultilineStrings(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "multiline.yaml") // Test literal block scalar (|) which preserves newlines content := `name: multiline display_name: Multiline Test identity: | First line. Second line. Third line. focus: - item one ignore: [] severity: major: Major issue minor: Minor issue nit: Nit ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed: %v", err) } // Literal block scalar preserves newlines if !strings.Contains(p.Identity, "\n") { t.Error("Identity should contain newlines from literal block scalar") } if !strings.Contains(p.Identity, "Second line") { t.Error("Identity should contain 'Second line'") } } func TestYAMLComments(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "comments.yaml") content := `# This is a comment name: commented # inline comment display_name: Commented Persona # Another comment identity: Test identity focus: - item # comment after item ignore: [] severity: major: Major minor: Minor nit: Nit ` if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("failed to write test file: %v", err) } p, err := LoadPersona(path) if err != nil { t.Fatalf("LoadPersona failed: %v", err) } // Comments should be ignored if p.Name != "commented" { t.Errorf("Name = %q, want %q", p.Name, "commented") } if p.Focus[0] != "item" { t.Errorf("Focus[0] = %q, want %q", p.Focus[0], "item") } }