9e15b73a23
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 28s
CI / review (gpt-5, security, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 30s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 51s
The external dependency (goccy/go-yaml) violates the repository's stdlib-only convention (CONVENTIONS.md). While YAML provides better readability for multi-line strings, the convenience doesn't justify breaking a hard rule. Reverts: - External dependency on github.com/goccy/go-yaml - YAML parsing logic in persona.go - YAML persona files (restored as JSON) - YAML-specific tests - Design document (feature rejected) The persona files work fine with JSON. Multi-line strings use \n escapes which is less pretty but acceptable for internal files. This addresses all MAJOR findings from review bots regarding the external dependency violation.
240 lines
5.5 KiB
Go
240 lines
5.5 KiB
Go
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 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
|
|
json string
|
|
wantErr string
|
|
}{
|
|
{
|
|
name: "missing name",
|
|
json: `{"identity": "test"}`,
|
|
wantErr: "name is required",
|
|
},
|
|
{
|
|
name: "missing identity",
|
|
json: `{"name": "test"}`,
|
|
wantErr: "identity is required",
|
|
},
|
|
{
|
|
name: "display_name defaults to name",
|
|
json: `{"name": "test", "identity": "test identity"}`,
|
|
// 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.json")
|
|
if err := os.WriteFile(path, []byte(tt.json), 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.json")
|
|
if err == nil {
|
|
t.Error("expected error for nonexistent file")
|
|
}
|
|
}
|
|
|
|
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 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")
|
|
}
|
|
}
|