diff --git a/src/renderer/src/lib/worktree-palette-task-url-match.test.ts b/src/renderer/src/lib/worktree-palette-task-url-match.test.ts index 0ccdf75d931..78921af029c 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.test.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.test.ts @@ -622,6 +622,115 @@ describe('matchWorktreePaletteTaskUrl', () => { }) ).toBeNull() }) + + it('matches a Jira issue URL on its own tenant', () => { + const intent = parseCmdJTaskSourceUrl('https://acme.atlassian.net/browse/PROJ-123') + + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ + linkedWorkItem: { + provider: 'jira', + type: 'issue', + number: 0, + title: 'Tenant scoped', + jiraIdentifier: 'PROJ-123', + url: 'https://acme.atlassian.net/browse/PROJ-123' + } + }), + intent: intent! + }) + ).toMatchObject({ supportingText: { text: 'PROJ-123' } }) + }) + + // Why: Jira issue keys are per-project, so the same PROJ-123 exists on every + // tenant that has a PROJ project. + it('rejects a Jira issue URL from a different tenant with the same issue key', () => { + const intent = parseCmdJTaskSourceUrl('https://acme.atlassian.net/browse/PROJ-123') + + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ + linkedWorkItem: { + provider: 'jira', + type: 'issue', + number: 0, + title: 'Other tenant', + jiraIdentifier: 'PROJ-123', + url: 'https://other.atlassian.net/browse/PROJ-123' + } + }), + intent: intent! + }) + ).toBeNull() + }) + + // Same host, different site path: Jira Server installs are commonly path-scoped. + it('rejects a Jira issue URL from a different site path on the same host', () => { + const intent = parseCmdJTaskSourceUrl('https://jira.acme.test/one/browse/PROJ-123') + + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ + linkedWorkItem: { + provider: 'jira', + type: 'issue', + number: 0, + title: 'Other site', + jiraIdentifier: 'PROJ-123', + url: 'https://jira.acme.test/two/browse/PROJ-123' + } + }), + intent: intent! + }) + ).toBeNull() + }) + + // The reachable fallback: `normalizeWorkspaceLinkedItem` drops any item with a + // blank url, so the only way to have no tenant evidence is a url that is present + // but is not a Jira browse link. The identifier is then all there is. + it('falls back to the Jira identifier when the stored url is not a Jira link', () => { + const intent = parseCmdJTaskSourceUrl('https://acme.atlassian.net/browse/PROJ-123') + + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ + linkedWorkItem: { + provider: 'jira', + type: 'issue', + number: 0, + title: 'Linked elsewhere', + jiraIdentifier: 'PROJ-123', + url: 'https://github.com/stablyai/orca/issues/14198' + } + }), + intent: intent! + }) + ).toMatchObject({ supportingText: { text: 'PROJ-123' } }) + }) + + // Jira appends a tracking query on copy, and the matcher is pathname-only. + it('matches a pasted Jira url carrying a tracking query string', () => { + const intent = parseCmdJTaskSourceUrl( + 'https://acme.atlassian.net/browse/PROJ-123?atlOrigin=eyJpIjoiZm9vIn0' + ) + + expect( + matchWorktreePaletteTaskUrl({ + worktree: makeWorktree({ + linkedWorkItem: { + provider: 'jira', + type: 'issue', + number: 0, + title: 'Tenant scoped', + jiraIdentifier: 'PROJ-123', + url: 'https://acme.atlassian.net/browse/PROJ-123' + } + }), + intent: intent! + }) + ).toMatchObject({ supportingText: { text: 'PROJ-123' } }) + }) }) describe('getCmdJTaskUrlCreatePreview', () => { diff --git a/src/renderer/src/lib/worktree-palette-task-url-match.ts b/src/renderer/src/lib/worktree-palette-task-url-match.ts index 854ba244c71..88c933938a6 100644 --- a/src/renderer/src/lib/worktree-palette-task-url-match.ts +++ b/src/renderer/src/lib/worktree-palette-task-url-match.ts @@ -275,18 +275,21 @@ function worktreeMatchesLinearUrl(worktree: Worktree, intent: LinearIssueUrlInte } function worktreeMatchesJiraUrl(worktree: Worktree, parsed: ParsedJiraIssueUrl): boolean { - if (worktree.linkedWorkItem?.jiraIdentifier?.toUpperCase() === parsed.issueKey) { - return true - } const linkedUrl = worktree.linkedWorkItem?.url ? parseJiraIssueUrl(worktree.linkedWorkItem.url) : null - return ( - linkedUrl !== null && - linkedUrl.issueKey === parsed.issueKey && - linkedUrl.origin === parsed.origin && - linkedUrl.sitePath === parsed.sitePath - ) + // Why url first: issue keys are per-project, not per-tenant, so two Jira sites + // routinely both have a PROJ-123. The stored URL is the only tenant evidence + // here, so where it exists it decides — matching on the bare identifier would + // jump to another tenant's worktree. + if (linkedUrl) { + return ( + linkedUrl.issueKey === parsed.issueKey && + linkedUrl.origin === parsed.origin && + linkedUrl.sitePath === parsed.sitePath + ) + } + return worktree.linkedWorkItem?.jiraIdentifier?.toUpperCase() === parsed.issueKey } export function matchWorktreePaletteTaskUrl(args: {