fix(validation): support hyphenated prefixes in ValidateIDFormat (#1013)
* test(validation): add failing tests for hyphenated prefix parsing Reproduces bug where `bd create --parent` fails for projects with hyphenated prefixes like "bead-me-up" or "web-app". Root cause: ValidateIDFormat splits on first hyphen, so: "bead-me-up-3e9.1" → prefix "bead" (wrong, should be "bead-me-up") The bug flow in create.go: 1. User runs: bd create "Child" --parent bead-me-up-3e9 2. GetNextChildID generates: bead-me-up-3e9.1 3. ValidateIDFormat extracts: "bead" (splits at first hyphen) 4. ValidatePrefix compares: "bead" vs "bead-me-up" → MISMATCH Tests added: - TestValidateIDFormat: 6 cases for hyphenated prefix IDs - TestValidateIDFormat_ParentChildFlow: simulates exact --parent flow, showing simple prefixes pass while hyphenated prefixes fail Workaround: use --force flag to bypass prefix validation. * fix(validation): support hyphenated prefixes in ValidateIDFormat Use utils.ExtractIssuePrefix instead of naive first-hyphen splitting. This fixes bd create --parent failing for projects with hyphenated prefixes like "bead-me-up" or "web-app". Before: "bead-me-up-3e9" → prefix "bead" (wrong) After: "bead-me-up-3e9" → prefix "bead-me-up" (correct) ExtractIssuePrefix uses smart heuristics: split on last hyphen, check if suffix is hash-like (3-8 chars, alphanumeric, digits for 4+). * Update internal/validation/bead_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
5941544a5e
commit
d04bffb9b6
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/steveyegge/beads/internal/types"
|
||||
"github.com/steveyegge/beads/internal/utils"
|
||||
)
|
||||
|
||||
// ParsePriority extracts and validates a priority value from content.
|
||||
@@ -51,6 +52,7 @@ func ValidatePriority(priorityStr string) (int, error) {
|
||||
|
||||
// ValidateIDFormat validates that an ID has the correct format.
|
||||
// Supports: prefix-number (bd-42), prefix-hash (bd-a3f8e9), or hierarchical (bd-a3f8e9.1)
|
||||
// Also supports hyphenated prefixes like "bead-me-up-3e9" or "web-app-abc123".
|
||||
// Returns the prefix part or an error if invalid.
|
||||
func ValidateIDFormat(id string) (string, error) {
|
||||
if id == "" {
|
||||
@@ -62,9 +64,11 @@ func ValidateIDFormat(id string) (string, error) {
|
||||
return "", fmt.Errorf("invalid ID format '%s' (expected format: prefix-hash or prefix-hash.number, e.g., 'bd-a3f8e9' or 'bd-a3f8e9.1')", id)
|
||||
}
|
||||
|
||||
// Extract prefix (before the first hyphen)
|
||||
hyphenIdx := strings.Index(id, "-")
|
||||
prefix := id[:hyphenIdx]
|
||||
// Use ExtractIssuePrefix which correctly handles hyphenated prefixes
|
||||
// by looking at the last hyphen and checking if suffix is hash-like.
|
||||
// This fixes the bug where "bead-me-up-3e9" was parsed as prefix "bead"
|
||||
// instead of "bead-me-up".
|
||||
prefix := utils.ExtractIssuePrefix(id)
|
||||
|
||||
return prefix, nil
|
||||
}
|
||||
|
||||
@@ -93,6 +93,16 @@ func TestValidateIDFormat(t *testing.T) {
|
||||
{"bd-a3f8e9.1", "bd", false},
|
||||
{"foo-bar", "foo", false},
|
||||
{"nohyphen", "", true},
|
||||
|
||||
// Hyphenated prefix support
|
||||
// These test cases verify that ValidateIDFormat correctly extracts
|
||||
// prefixes containing hyphens (e.g., "bead-me-up" not just "bead")
|
||||
{"bead-me-up-3e9", "bead-me-up", false}, // 3-char hash suffix
|
||||
{"bead-me-up-3e9.1", "bead-me-up", false}, // hierarchical child
|
||||
{"bead-me-up-3e9.1.2", "bead-me-up", false}, // deeply nested child
|
||||
{"web-app-a3f8e9", "web-app", false}, // 6-char hash suffix
|
||||
{"my-cool-project-1a2b", "my-cool-project", false}, // 4-char hash suffix
|
||||
{"document-intelligence-0sa", "document-intelligence", false}, // 3-char hash
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
@@ -109,6 +119,90 @@ func TestValidateIDFormat(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestValidateIDFormat_ParentChildFlow tests the exact scenario that fails with --parent flag:
|
||||
// When creating a child of a parent with a hyphenated prefix, the generated child ID
|
||||
// (e.g., "bead-me-up-3e9.1") should have its prefix correctly extracted as "bead-me-up",
|
||||
// not "bead". This test simulates the create.go flow at lines 352-391.
|
||||
func TestValidateIDFormat_ParentChildFlow(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
parentID string
|
||||
childSuffix string
|
||||
dbPrefix string
|
||||
wantPrefix string
|
||||
shouldMatch bool
|
||||
}{
|
||||
{
|
||||
name: "simple prefix - child creation works",
|
||||
parentID: "bd-a3f8e9",
|
||||
childSuffix: ".1",
|
||||
dbPrefix: "bd",
|
||||
wantPrefix: "bd",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "hyphenated prefix - child creation FAILS with current impl",
|
||||
parentID: "bead-me-up-3e9",
|
||||
childSuffix: ".1",
|
||||
dbPrefix: "bead-me-up",
|
||||
wantPrefix: "bead-me-up", // Current impl returns "bead" - THIS IS THE BUG
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "hyphenated prefix - deeply nested child",
|
||||
parentID: "bead-me-up-3e9.1",
|
||||
childSuffix: ".2",
|
||||
dbPrefix: "bead-me-up",
|
||||
wantPrefix: "bead-me-up",
|
||||
shouldMatch: true,
|
||||
},
|
||||
{
|
||||
name: "multi-hyphen prefix - web-app style",
|
||||
parentID: "web-app-abc123",
|
||||
childSuffix: ".1",
|
||||
dbPrefix: "web-app",
|
||||
wantPrefix: "web-app",
|
||||
shouldMatch: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Simulate the child ID generation that happens in create.go:352
|
||||
childID := tt.parentID + tt.childSuffix
|
||||
|
||||
// Simulate the validation flow from create.go:361
|
||||
extractedPrefix, err := ValidateIDFormat(childID)
|
||||
if err != nil {
|
||||
t.Fatalf("ValidateIDFormat(%q) unexpected error: %v", childID, err)
|
||||
}
|
||||
|
||||
// Check that extracted prefix matches expected
|
||||
if extractedPrefix != tt.wantPrefix {
|
||||
t.Errorf("ValidateIDFormat(%q) extracted prefix = %q, want %q",
|
||||
childID, extractedPrefix, tt.wantPrefix)
|
||||
}
|
||||
|
||||
// Simulate the prefix validation from create.go:389
|
||||
// This is where the "prefix mismatch" error occurs
|
||||
err = ValidatePrefix(extractedPrefix, tt.dbPrefix, false)
|
||||
prefixMatches := (err == nil)
|
||||
|
||||
if prefixMatches != tt.shouldMatch {
|
||||
if tt.shouldMatch {
|
||||
t.Errorf("--parent %s flow: prefix validation failed unexpectedly: %v\n"+
|
||||
" Child ID: %s\n"+
|
||||
" Extracted prefix: %q\n"+
|
||||
" Database prefix: %q",
|
||||
tt.parentID, err, childID, extractedPrefix, tt.dbPrefix)
|
||||
} else {
|
||||
t.Errorf("--parent %s flow: expected prefix mismatch but validation passed", tt.parentID)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseIssueType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
Reference in New Issue
Block a user