fix(orphan): handle prefixes with dots in orphan detection (GH#508)
The orphan detection was incorrectly flagging issues with dots in their
prefix (e.g., "my.project-abc123") as orphans because it was looking for
any dot in the ID, treating everything before the first dot as the
parent ID.
The fix:
- Add IsHierarchicalID() helper that correctly detects hierarchical IDs
by checking if the ID ends with .{digits} (e.g., "bd-abc.1")
- Update SQL query in orphan detection migration to use GLOB patterns
that only match IDs ending with numeric suffixes
- Update all Go code that checks for hierarchical IDs to use the new
helper function
Test cases added:
- Unit tests for IsHierarchicalID covering normal, dotted prefix, and
edge cases
- Integration test verifying dotted prefixes do not trigger false
positives
Fixes: #508
This commit is contained in:
@@ -620,4 +620,83 @@ func TestMigrateOrphanDetection(t *testing.T) {
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
// GH#508: Verify that prefixes with dots don't trigger false positives
|
||||
t.Run("prefix with dots is not flagged as orphan", func(t *testing.T) {
|
||||
store, cleanup := setupTestDB(t)
|
||||
defer cleanup()
|
||||
db := store.db
|
||||
|
||||
// Override the prefix for this test
|
||||
_, err := db.Exec(`UPDATE config SET value = 'my.project' WHERE key = 'issue_prefix'`)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to update prefix: %v", err)
|
||||
}
|
||||
|
||||
// Insert issues with dotted prefix directly (bypassing prefix validation)
|
||||
testCases := []struct {
|
||||
id string
|
||||
expectOrphan bool
|
||||
}{
|
||||
// These should NOT be flagged as orphans (dots in prefix)
|
||||
{"my.project-abc123", false},
|
||||
{"my.project-xyz789", false},
|
||||
{"com.example.app-issue1", false},
|
||||
|
||||
// This SHOULD be flagged as orphan (hierarchical, parent doesn't exist)
|
||||
{"my.project-missing.1", true},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO issues (id, title, status, priority, issue_type, created_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
`, tc.id, "Test Issue", "open", 1, "task")
|
||||
if err != nil {
|
||||
t.Fatalf("failed to insert %s: %v", tc.id, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Query for orphans using the same logic as the migration
|
||||
rows, err := db.Query(`
|
||||
SELECT id
|
||||
FROM issues
|
||||
WHERE
|
||||
(id GLOB '*.[0-9]' OR id GLOB '*.[0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9]' OR id GLOB '*.[0-9][0-9][0-9][0-9]')
|
||||
AND rtrim(rtrim(id, '0123456789'), '.') NOT IN (SELECT id FROM issues)
|
||||
ORDER BY id
|
||||
`)
|
||||
if err != nil {
|
||||
t.Fatalf("query failed: %v", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var orphans []string
|
||||
for rows.Next() {
|
||||
var id string
|
||||
if err := rows.Scan(&id); err != nil {
|
||||
t.Fatalf("scan failed: %v", err)
|
||||
}
|
||||
orphans = append(orphans, id)
|
||||
}
|
||||
|
||||
// Verify only the expected orphan is detected
|
||||
if len(orphans) != 1 {
|
||||
t.Errorf("expected 1 orphan, got %d: %v", len(orphans), orphans)
|
||||
}
|
||||
if len(orphans) == 1 && orphans[0] != "my.project-missing.1" {
|
||||
t.Errorf("expected orphan 'my.project-missing.1', got %q", orphans[0])
|
||||
}
|
||||
|
||||
// Verify non-hierarchical dotted IDs are NOT flagged
|
||||
for _, tc := range testCases {
|
||||
if !tc.expectOrphan {
|
||||
for _, orphan := range orphans {
|
||||
if orphan == tc.id {
|
||||
t.Errorf("false positive: %s was incorrectly flagged as orphan", tc.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user