Fix gh-316: Prefer exact ID matches over prefix matches

The ID disambiguation logic treated 'offlinebrew-3d0' as ambiguous when
child IDs like 'offlinebrew-3d0.1' existed. Now the system:

1. Checks for exact full ID matches first (issue.ID == input)
2. Checks for exact hash matches (handling cross-prefix scenarios)
3. Only falls back to substring matching if no exact match is found

Added test cases verifying:
- 'offlinebrew-3d0' matches exactly, not ambiguously with children
- '3d0' without prefix still resolves to exact match

Amp-Thread-ID: https://ampcode.com/threads/T-5358ea57-e9ea-49e9-aedf-7044ebf8b52a
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Steve Yegge
2025-11-15 14:07:38 -08:00
parent b9919fe031
commit 934ae04fa0
2 changed files with 59 additions and 1 deletions

View File

@@ -81,14 +81,41 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string)
hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen) hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen)
var matches []string var matches []string
var exactMatch string
for _, issue := range issues { for _, issue := range issues {
issueHash := strings.TrimPrefix(issue.ID, prefixWithHyphen) // Check for exact full ID match first (case: user typed full ID with different prefix)
if issue.ID == input {
exactMatch = issue.ID
break
}
// Extract hash from each issue, regardless of its prefix
// This handles cross-prefix matching (e.g., "3d0" matching "offlinebrew-3d0")
var issueHash string
if idx := strings.Index(issue.ID, "-"); idx >= 0 {
issueHash = issue.ID[idx+1:]
} else {
issueHash = issue.ID
}
// Check for exact hash match (excluding hierarchical children)
if issueHash == hashPart {
exactMatch = issue.ID
// Don't break - keep searching in case there's a full ID match
}
// Check if the issue hash contains the input hash as substring // Check if the issue hash contains the input hash as substring
if strings.Contains(issueHash, hashPart) { if strings.Contains(issueHash, hashPart) {
matches = append(matches, issue.ID) matches = append(matches, issue.ID)
} }
} }
// Prefer exact match over substring matches
if exactMatch != "" {
return exactMatch, nil
}
if len(matches) == 0 { if len(matches) == 0 {
return "", fmt.Errorf("no issue found matching %q", input) return "", fmt.Errorf("no issue found matching %q", input)
} }

View File

@@ -90,6 +90,21 @@ func TestResolvePartialID(t *testing.T) {
Priority: 1, Priority: 1,
IssueType: types.TypeTask, IssueType: types.TypeTask,
} }
// Test hierarchical IDs - parent and child
parentIssue := &types.Issue{
ID: "offlinebrew-3d0",
Title: "Parent Epic",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeEpic,
}
childIssue := &types.Issue{
ID: "offlinebrew-3d0.1",
Title: "Child Task",
Status: types.StatusOpen,
Priority: 1,
IssueType: types.TypeTask,
}
if err := store.CreateIssue(ctx, issue1, "test"); err != nil { if err := store.CreateIssue(ctx, issue1, "test"); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -100,6 +115,12 @@ func TestResolvePartialID(t *testing.T) {
if err := store.CreateIssue(ctx, issue3, "test"); err != nil { if err := store.CreateIssue(ctx, issue3, "test"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
if err := store.CreateIssue(ctx, parentIssue, "test"); err != nil {
t.Fatal(err)
}
if err := store.CreateIssue(ctx, childIssue, "test"); err != nil {
t.Fatal(err)
}
// Set config for prefix // Set config for prefix
if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil {
@@ -149,6 +170,16 @@ func TestResolvePartialID(t *testing.T) {
input: "bd-1", input: "bd-1",
expected: "bd-1", // Will match exactly, not ambiguously expected: "bd-1", // Will match exactly, not ambiguously
}, },
{
name: "exact match parent ID with hierarchical child - gh-316",
input: "offlinebrew-3d0",
expected: "offlinebrew-3d0", // Should match exactly, not be ambiguous with offlinebrew-3d0.1
},
{
name: "exact match parent without prefix - gh-316",
input: "3d0",
expected: "offlinebrew-3d0", // Should still prefer exact hash match
},
} }
for _, tt := range tests { for _, tt := range tests {