feat(bd-1pj6): Add custom status states via config
Users can now define custom status states for multi-step pipelines using: bd config set status.custom "awaiting_review,awaiting_testing,awaiting_docs" Changes: - Add Status.IsValidWithCustom() method for custom status validation - Add Issue.ValidateWithCustomStatuses() method - Add GetCustomStatuses() method to storage interface - Update CreateIssue/UpdateIssue to support custom statuses - Add comprehensive tests for custom status functionality - Update config command help text with custom status documentation 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -69,8 +69,14 @@ func (i *Issue) ComputeContentHash() string {
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
// Validate checks if the issue has valid field values
|
||||
// Validate checks if the issue has valid field values (built-in statuses only)
|
||||
func (i *Issue) Validate() error {
|
||||
return i.ValidateWithCustomStatuses(nil)
|
||||
}
|
||||
|
||||
// ValidateWithCustomStatuses checks if the issue has valid field values,
|
||||
// allowing custom statuses in addition to built-in ones.
|
||||
func (i *Issue) ValidateWithCustomStatuses(customStatuses []string) error {
|
||||
if len(i.Title) == 0 {
|
||||
return fmt.Errorf("title is required")
|
||||
}
|
||||
@@ -80,7 +86,7 @@ func (i *Issue) Validate() error {
|
||||
if i.Priority < 0 || i.Priority > 4 {
|
||||
return fmt.Errorf("priority must be between 0 and 4 (got %d)", i.Priority)
|
||||
}
|
||||
if !i.Status.IsValid() {
|
||||
if !i.Status.IsValidWithCustom(customStatuses) {
|
||||
return fmt.Errorf("invalid status: %s", i.Status)
|
||||
}
|
||||
if !i.IssueType.IsValid() {
|
||||
@@ -110,7 +116,7 @@ const (
|
||||
StatusClosed Status = "closed"
|
||||
)
|
||||
|
||||
// IsValid checks if the status value is valid
|
||||
// IsValid checks if the status value is valid (built-in statuses only)
|
||||
func (s Status) IsValid() bool {
|
||||
switch s {
|
||||
case StatusOpen, StatusInProgress, StatusBlocked, StatusClosed:
|
||||
@@ -119,6 +125,22 @@ func (s Status) IsValid() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// IsValidWithCustom checks if the status is valid, including custom statuses.
|
||||
// Custom statuses are user-defined via bd config set status.custom "status1,status2,..."
|
||||
func (s Status) IsValidWithCustom(customStatuses []string) bool {
|
||||
// First check built-in statuses
|
||||
if s.IsValid() {
|
||||
return true
|
||||
}
|
||||
// Then check custom statuses
|
||||
for _, custom := range customStatuses {
|
||||
if string(s) == custom {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// IssueType categorizes the kind of work
|
||||
type IssueType string
|
||||
|
||||
|
||||
@@ -215,6 +215,108 @@ func TestStatusIsValid(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestStatusIsValidWithCustom(t *testing.T) {
|
||||
customStatuses := []string{"awaiting_review", "awaiting_testing", "awaiting_docs"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
status Status
|
||||
customStatuses []string
|
||||
valid bool
|
||||
}{
|
||||
// Built-in statuses should always be valid
|
||||
{"built-in open", StatusOpen, nil, true},
|
||||
{"built-in open with custom", StatusOpen, customStatuses, true},
|
||||
{"built-in closed", StatusClosed, customStatuses, true},
|
||||
|
||||
// Custom statuses with config
|
||||
{"custom awaiting_review", Status("awaiting_review"), customStatuses, true},
|
||||
{"custom awaiting_testing", Status("awaiting_testing"), customStatuses, true},
|
||||
{"custom awaiting_docs", Status("awaiting_docs"), customStatuses, true},
|
||||
|
||||
// Custom statuses without config (should fail)
|
||||
{"custom without config", Status("awaiting_review"), nil, false},
|
||||
{"custom without config empty", Status("awaiting_review"), []string{}, false},
|
||||
|
||||
// Invalid statuses
|
||||
{"invalid status", Status("not_a_status"), customStatuses, false},
|
||||
{"empty status", Status(""), customStatuses, false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := tt.status.IsValidWithCustom(tt.customStatuses); got != tt.valid {
|
||||
t.Errorf("Status(%q).IsValidWithCustom(%v) = %v, want %v", tt.status, tt.customStatuses, got, tt.valid)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateWithCustomStatuses(t *testing.T) {
|
||||
customStatuses := []string{"awaiting_review", "awaiting_testing"}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
issue Issue
|
||||
customStatuses []string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid issue with built-in status",
|
||||
issue: Issue{
|
||||
Title: "Test Issue",
|
||||
Status: StatusOpen,
|
||||
Priority: 1,
|
||||
IssueType: TypeTask,
|
||||
},
|
||||
customStatuses: nil,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "valid issue with custom status",
|
||||
issue: Issue{
|
||||
Title: "Test Issue",
|
||||
Status: Status("awaiting_review"),
|
||||
Priority: 1,
|
||||
IssueType: TypeTask,
|
||||
},
|
||||
customStatuses: customStatuses,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "invalid custom status without config",
|
||||
issue: Issue{
|
||||
Title: "Test Issue",
|
||||
Status: Status("awaiting_review"),
|
||||
Priority: 1,
|
||||
IssueType: TypeTask,
|
||||
},
|
||||
customStatuses: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid custom status not in config",
|
||||
issue: Issue{
|
||||
Title: "Test Issue",
|
||||
Status: Status("unknown_status"),
|
||||
Priority: 1,
|
||||
IssueType: TypeTask,
|
||||
},
|
||||
customStatuses: customStatuses,
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.issue.ValidateWithCustomStatuses(tt.customStatuses)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateWithCustomStatuses() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssueTypeIsValid(t *testing.T) {
|
||||
tests := []struct {
|
||||
issueType IssueType
|
||||
|
||||
Reference in New Issue
Block a user