From f056c40a574262fa498fb43802774a76035a4002 Mon Sep 17 00:00:00 2001 From: luvs01 Date: Fri, 28 Aug 2026 11:01:47 +0900 Subject: [PATCH] fix(slug): prefer self-namespaced exact matches --- src/providers/slug-codec.ts | 12 +++++++----- tests/slug-codec.test.ts | 19 ++++++++++++++++++- 2 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/providers/slug-codec.ts b/src/providers/slug-codec.ts index 47573b878c..2854917cf3 100644 --- a/src/providers/slug-codec.ts +++ b/src/providers/slug-codec.ts @@ -139,14 +139,16 @@ export function resolveSlugSelection( // `a/b` is a bare NATIVE id that happens to contain a slash. Treating every slash-bearing // selection as provider-qualified made `a/b` resolve against provider "a", so the same // collision reported ambiguous through the dash spelling and unambiguous through the slash - // spelling — the exact asymmetry this resolver exists to remove. - const qualified = selection.startsWith(`${provider}/`) - ? selection - : routedSlug(provider, selection); + // spelling — the exact asymmetry this resolver exists to remove. Prefer a known native exact + // match before interpreting the same text as a provider-qualified selection. + const ids = [...knownIds]; + const qualified = ids.includes(selection) || !selection.startsWith(`${provider}/`) + ? routedSlug(provider, selection) + : selection; const selectionKey = slugEquivalenceKey(qualified); const matched: string[] = []; let exact: string | undefined; - for (const id of knownIds) { + for (const id of ids) { if (slugEquivalenceKey(routedSlug(provider, id)) !== selectionKey) continue; matched.push(id); if (id === selection || `${provider}/${id}` === selection) exact = id; diff --git a/tests/slug-codec.test.ts b/tests/slug-codec.test.ts index 93c780a74e..f64cc47a5c 100644 --- a/tests/slug-codec.test.ts +++ b/tests/slug-codec.test.ts @@ -355,6 +355,24 @@ describe("#2491 one selection resolver reports what it actually matched", () => expect(match.matched.sort()).toEqual(["a-b", "a/b"]); }); + test("a self-namespaced native id is preferred over a routed suffix", () => { + const match = resolveSlugSelection( + "orcarouter", + "orcarouter/auto", + ["orcarouter/auto", "auto"], + ); + expect(match.matched).toEqual(["orcarouter/auto"]); + expect(match.exact).toBe("orcarouter/auto"); + expect(match.ambiguous).toBe(false); + }); + + test("a self-namespaced native id resolves when it is the only known id", () => { + const match = resolveSlugSelection("orcarouter", "orcarouter/auto", ["orcarouter/auto"]); + expect(match.matched).toEqual(["orcarouter/auto"]); + expect(match.exact).toBe("orcarouter/auto"); + expect(match.ambiguous).toBe(false); + }); + test("an id absent from an incomplete roster still reports no match rather than guessing", () => { // Live discovery can omit a published id; the resolver must not invent one. const match = resolveSlugSelection("p", "missing", ["a-b"]); @@ -370,4 +388,3 @@ describe("#2491 one selection resolver reports what it actually matched", () => expect(match.exact).toBeUndefined(); }); }); -