From b7645779432d06c89e6ef27b9d42ce5fc9ed26fa Mon Sep 17 00:00:00 2001 From: Isaac Sanchez-Hawkins <266845420+isanchez404@users.noreply.github.com> Date: Sat, 23 May 2026 13:51:45 -0400 Subject: [PATCH] fix: preserve canonical snippet names (#1380) * fix: preserve canonical snippet names * test: cover same-directory snippet alias precedence --------- Co-authored-by: Isaac Sanchez --- packages/web/server/lib/opencode/snippets.js | 21 ++++++++++---- .../web/server/lib/opencode/snippets.test.js | 29 +++++++++++++++++++ 2 files changed, 45 insertions(+), 5 deletions(-) diff --git a/packages/web/server/lib/opencode/snippets.js b/packages/web/server/lib/opencode/snippets.js index d77d7a89..bbef3c7a 100644 --- a/packages/web/server/lib/opencode/snippets.js +++ b/packages/web/server/lib/opencode/snippets.js @@ -85,27 +85,38 @@ function loadSnippetFile(dir, filename, source) { }; } -function registerSnippet(registry, snippet) { +function removeSnippetAliases(registry, snippet, canonicalNames) { + for (const alias of snippet.aliases) { + const aliasKey = alias.toLowerCase(); + if (canonicalNames.has(aliasKey)) continue; + if (registry.get(aliasKey) === snippet) registry.delete(aliasKey); + } +} + +function registerSnippet(registry, snippet, canonicalNames) { const key = snippet.name.toLowerCase(); const existing = registry.get(key); - if (existing) { - for (const alias of existing.aliases) registry.delete(alias.toLowerCase()); + if (existing?.name.toLowerCase() === key) { + removeSnippetAliases(registry, existing, canonicalNames); } registry.set(key, snippet); + canonicalNames.add(key); for (const alias of snippet.aliases) { - if (SNIPPET_NAME_PATTERN.test(alias)) registry.set(alias.toLowerCase(), snippet); + const aliasKey = alias.toLowerCase(); + if (SNIPPET_NAME_PATTERN.test(alias) && !canonicalNames.has(aliasKey)) registry.set(aliasKey, snippet); } } function loadSnippetRegistry(workingDirectory) { const registry = new Map(); + const canonicalNames = new Set(); for (const { dir, source } of getLoadDirs(workingDirectory)) { if (!fs.existsSync(dir)) continue; for (const filename of fs.readdirSync(dir)) { if (!filename.endsWith(SNIPPET_EXTENSION)) continue; try { const snippet = loadSnippetFile(dir, filename, source); - if (snippet) registerSnippet(registry, snippet); + if (snippet) registerSnippet(registry, snippet, canonicalNames); } catch (error) { console.warn(`[Snippets] Failed to load ${path.join(dir, filename)}:`, error); } diff --git a/packages/web/server/lib/opencode/snippets.test.js b/packages/web/server/lib/opencode/snippets.test.js index 69202658..de3afe37 100644 --- a/packages/web/server/lib/opencode/snippets.test.js +++ b/packages/web/server/lib/opencode/snippets.test.js @@ -44,6 +44,35 @@ describe('snippets', () => { expect(getSnippet('same', projectDir)?.content).toBe('New'); }); + test('canonical snippet names take precedence over colliding aliases', () => { + writeSnippet('.opencode/snippets/review.md', 'Review by name'); + writeSnippet('.opencode/snippet/helper.md', '---\naliases: [review, help]\n---\nReview by alias'); + + expect(getSnippet('review', projectDir)).toEqual(expect.objectContaining({ name: 'review', content: 'Review by name' })); + expect(getSnippet('help', projectDir)).toEqual(expect.objectContaining({ name: 'helper', content: 'Review by alias' })); + expect(expandSnippets('Use #review and #help', projectDir)).toBe('Use Review by name and Review by alias'); + }); + + test('canonical snippet names override earlier same-directory aliases', () => { + writeSnippet('.opencode/snippet/alias-first.md', '---\naliases: [review, assist]\n---\nReview by alias'); + writeSnippet('.opencode/snippet/review.md', 'Review by name'); + + const snippetDir = path.join(projectDir, '.opencode', 'snippet'); + const originalReaddirSync = fs.readdirSync; + fs.readdirSync = function readdirSync(dir, ...args) { + if (dir === snippetDir) return ['alias-first.md', 'review.md']; + return originalReaddirSync.call(fs, dir, ...args); + }; + + try { + expect(getSnippet('review', projectDir)).toEqual(expect.objectContaining({ name: 'review', content: 'Review by name' })); + expect(getSnippet('assist', projectDir)).toEqual(expect.objectContaining({ name: 'alias-first', content: 'Review by alias' })); + expect(expandSnippets('Use #review and #assist', projectDir)).toBe('Use Review by name and Review by alias'); + } finally { + fs.readdirSync = originalReaddirSync; + } + }); + test('creates updates and deletes snippets', () => { expect(createSnippet('custom-one', { content: 'Body', aliases: ['co'] }, projectDir, 'project')).toEqual( expect.objectContaining({ name: 'custom-one', content: 'Body', aliases: ['co'] }),