From 934ae04fa01c69a652fd794983ee5bda5e0c88d4 Mon Sep 17 00:00:00 2001 From: Steve Yegge Date: Sat, 15 Nov 2025 14:07:38 -0800 Subject: [PATCH] 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 --- internal/utils/id_parser.go | 29 ++++++++++++++++++++++++++++- internal/utils/id_parser_test.go | 31 +++++++++++++++++++++++++++++++ 2 files changed, 59 insertions(+), 1 deletion(-) diff --git a/internal/utils/id_parser.go b/internal/utils/id_parser.go index 7c5b7cc8..a1a27bf9 100644 --- a/internal/utils/id_parser.go +++ b/internal/utils/id_parser.go @@ -81,14 +81,41 @@ func ResolvePartialID(ctx context.Context, store storage.Storage, input string) hashPart := strings.TrimPrefix(normalizedID, prefixWithHyphen) var matches []string + var exactMatch string + 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 if strings.Contains(issueHash, hashPart) { matches = append(matches, issue.ID) } } + // Prefer exact match over substring matches + if exactMatch != "" { + return exactMatch, nil + } + if len(matches) == 0 { return "", fmt.Errorf("no issue found matching %q", input) } diff --git a/internal/utils/id_parser_test.go b/internal/utils/id_parser_test.go index 5c265983..bb1b4606 100644 --- a/internal/utils/id_parser_test.go +++ b/internal/utils/id_parser_test.go @@ -90,6 +90,21 @@ func TestResolvePartialID(t *testing.T) { Priority: 1, 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 { t.Fatal(err) @@ -100,6 +115,12 @@ func TestResolvePartialID(t *testing.T) { if err := store.CreateIssue(ctx, issue3, "test"); err != nil { 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 if err := store.SetConfig(ctx, "issue_prefix", "bd-"); err != nil { @@ -149,6 +170,16 @@ func TestResolvePartialID(t *testing.T) { input: "bd-1", 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 {