Add types.custom config key mirroring the status.custom pattern: - Add CustomTypeConfigKey constant and GetCustomTypes() to storage interface - Add IssueType.IsValidWithCustom() method for validation - Add ValidateWithCustom() to Issue for combined status/type validation - Update all validation call sites to use GetCustomTypes() - Rename parseCustomStatuses to parseCommaSeparated for reuse This enables Gas Town to register custom types like agent/role/rig/convoy without hardcoding them in beads core, supporting the type extraction epic. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> Executed-By: beads/crew/dave Rig: beads Role: crew
221 lines
6.3 KiB
Go
221 lines
6.3 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/steveyegge/beads/internal/types"
|
|
)
|
|
|
|
func TestValidatePriority(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid priority 0", 0, false},
|
|
{"valid priority 1", 1, false},
|
|
{"valid priority 2", 2, false},
|
|
{"valid priority 3", 3, false},
|
|
{"valid priority 4", 4, false},
|
|
{"invalid negative", -1, true},
|
|
{"invalid too high", 5, true},
|
|
{"non-int ignored", "not an int", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validatePriority(tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validatePriority() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid open", string(types.StatusOpen), false},
|
|
{"valid in_progress", string(types.StatusInProgress), false},
|
|
{"valid blocked", string(types.StatusBlocked), false},
|
|
{"valid closed", string(types.StatusClosed), false},
|
|
{"invalid status", "invalid", true},
|
|
{"non-string ignored", 123, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateStatus(tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateStatus() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateIssueType(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid bug", string(types.TypeBug), false},
|
|
{"valid feature", string(types.TypeFeature), false},
|
|
{"valid task", string(types.TypeTask), false},
|
|
{"valid epic", string(types.TypeEpic), false},
|
|
{"valid chore", string(types.TypeChore), false},
|
|
{"invalid type", "invalid", true},
|
|
{"non-string ignored", 123, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateIssueType(tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateIssueType() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateTitle(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid title", "Valid Title", false},
|
|
{"empty title", "", true},
|
|
{"max length title", string(make([]byte, 500)), false},
|
|
{"too long title", string(make([]byte, 501)), true},
|
|
{"non-string ignored", 123, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateTitle(tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateTitle() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateEstimatedMinutes(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid zero", 0, false},
|
|
{"valid positive", 60, false},
|
|
{"invalid negative", -1, true},
|
|
{"non-int ignored", "not an int", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateEstimatedMinutes(tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateEstimatedMinutes() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFieldUpdate(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
value interface{}
|
|
wantErr bool
|
|
}{
|
|
{"valid priority", "priority", 1, false},
|
|
{"invalid priority", "priority", 5, true},
|
|
{"valid status", "status", string(types.StatusOpen), false},
|
|
{"invalid status", "status", "invalid", true},
|
|
{"unknown field", "unknown_field", "any value", false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateFieldUpdate(tt.key, tt.value)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateFieldUpdate() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestValidateFieldUpdateWithCustomStatuses(t *testing.T) {
|
|
customStatuses := []string{"awaiting_review", "awaiting_testing"}
|
|
|
|
tests := []struct {
|
|
name string
|
|
key string
|
|
value interface{}
|
|
customStatuses []string
|
|
wantErr bool
|
|
}{
|
|
// Built-in statuses work with or without custom statuses
|
|
{"built-in status no custom", "status", string(types.StatusOpen), nil, false},
|
|
{"built-in status with custom", "status", string(types.StatusOpen), customStatuses, false},
|
|
{"built-in closed with custom", "status", string(types.StatusClosed), customStatuses, false},
|
|
|
|
// Custom statuses work when configured
|
|
{"custom status configured", "status", "awaiting_review", customStatuses, false},
|
|
{"custom status awaiting_testing", "status", "awaiting_testing", customStatuses, false},
|
|
|
|
// Custom statuses fail without config
|
|
{"custom status not configured", "status", "awaiting_review", nil, true},
|
|
{"custom status not in list", "status", "unknown_status", customStatuses, true},
|
|
|
|
// Non-status fields work as before
|
|
{"valid priority", "priority", 1, customStatuses, false},
|
|
{"invalid priority", "priority", 5, customStatuses, true},
|
|
{"unknown field", "unknown_field", "any value", customStatuses, false},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
err := validateFieldUpdateWithCustomStatuses(tt.key, tt.value, tt.customStatuses)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("validateFieldUpdateWithCustomStatuses() error = %v, wantErr %v", err, tt.wantErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestParseCustomStatuses(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
value string
|
|
want []string
|
|
}{
|
|
{"empty string", "", nil},
|
|
{"single status", "awaiting_review", []string{"awaiting_review"}},
|
|
{"multiple statuses", "awaiting_review,awaiting_testing", []string{"awaiting_review", "awaiting_testing"}},
|
|
{"with spaces", "awaiting_review, awaiting_testing, awaiting_docs", []string{"awaiting_review", "awaiting_testing", "awaiting_docs"}},
|
|
{"empty entries filtered", "awaiting_review,,awaiting_testing", []string{"awaiting_review", "awaiting_testing"}},
|
|
{"whitespace only entries", "awaiting_review, , awaiting_testing", []string{"awaiting_review", "awaiting_testing"}},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := parseCommaSeparated(tt.value)
|
|
if len(got) != len(tt.want) {
|
|
t.Errorf("parseCommaSeparated() = %v, want %v", got, tt.want)
|
|
return
|
|
}
|
|
for i := range got {
|
|
if got[i] != tt.want[i] {
|
|
t.Errorf("parseCommaSeparated()[%d] = %v, want %v", i, got[i], tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|