diff --git a/packages/gittensory-mcp/lib/local-branch.js b/packages/gittensory-mcp/lib/local-branch.js index ca32023d56..95bfb25e1e 100644 --- a/packages/gittensory-mcp/lib/local-branch.js +++ b/packages/gittensory-mcp/lib/local-branch.js @@ -557,9 +557,13 @@ function assertSourceUploadDisabled() { } } -function extractLinkedIssues(text) { +// Word-boundary the closing keywords (as the server-side extractors in src/db/repositories.ts and +// src/signals/engine.ts already do) so a keyword embedded in a longer word does not spuriously link an +// issue: without \b, `hotfix 5` / `prefixes 12` matched the `fix`/`fixes` substring and captured the +// trailing number. The bare `#` branch stays boundary-free so `#123` still matches anywhere. +export function extractLinkedIssues(text) { const issues = []; - for (const match of String(text).matchAll(/(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?|#)\s*#?(\d+)/gi)) issues.push(Number(match[1])); + for (const match of String(text).matchAll(/(?:\b(?:close[sd]?|fix(?:e[sd])?|resolve[sd]?)|#)\s*#?(\d+)/gi)) issues.push(Number(match[1])); return issues.filter((issue) => Number.isInteger(issue) && issue > 0); } diff --git a/test/unit/local-branch.test.ts b/test/unit/local-branch.test.ts index 0238b24de0..16ad62dff5 100644 --- a/test/unit/local-branch.test.ts +++ b/test/unit/local-branch.test.ts @@ -1737,6 +1737,21 @@ describe("local MCP git metadata collection", () => { ); }); + it("extracts linked issues only from standalone closing keywords, not keyword substrings", async () => { + // @ts-expect-error package helper is plain JS because the local wrapper ships as a Node bin package. + const { extractLinkedIssues } = await import("../../packages/gittensory-mcp/lib/local-branch.js"); + // Standalone closing keywords (hash optional, as this client-side extractor allows) and bare #refs link. + expect(extractLinkedIssues("fixes #5")).toEqual([5]); + expect(extractLinkedIssues("Closes 12 and resolves #34")).toEqual([12, 34]); + expect(extractLinkedIssues("see #7")).toEqual([7]); + expect(extractLinkedIssues("closes#3")).toEqual([3]); + // Regression: a closing keyword embedded in a longer word must NOT capture a trailing number. + expect(extractLinkedIssues("hotfix 5")).toEqual([]); + expect(extractLinkedIssues("prefixes 12")).toEqual([]); + expect(extractLinkedIssues("unclosed 9")).toEqual([]); + expect(extractLinkedIssues("no references here")).toEqual([]); + }); + it("parses remotes, changed-file stats, linked issues, and refuses source upload mode", async () => { // @ts-expect-error package helper is plain JS because the local wrapper ships as a Node bin package. const { collectLocalBranchMetadata, parseGitRemote } = await import("../../packages/gittensory-mcp/lib/local-branch.js");