4dd67742f9
PR Ready Gate / clear-labels (pull_request) Successful in 2s
CI / test (pull_request) Successful in 15s
CI / review (/anthropic/v1, anthropic--claude-4.6-sonnet, sonnet, anthropic, SONNET_REVIEW_TOKEN) (pull_request) Successful in 43s
CI / review (/openai/v1, gpt-5, gpt, openai, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m28s
CI / review (/openai/v1, gpt-5, security, openai, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m55s
MAJOR fixes: - Remove external YAML dependency (github.com/goccy/go-yaml) Per project convention: Go standard library only, zero dependencies. Convert all persona files from YAML to JSON format. - Fix TestValidateWorkspacePath error expectation Go 1.21+ filepath.Join normalizes absolute paths differently. MINOR fixes: - Remove custom contains helper in persona_test.go (use strings.Contains) - Add Unicode-safe CapitalizeFirst function for header titles - ListBuiltinPersonas returns empty slice instead of nil on error - Fix test comment about filepath.Join behavior Documentation: - Update README to reflect JSON-only persona format - Update design doc with note about JSON decision - Fix action.yml description for persona-file input
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")
|
|
}
|
|
}
|