fix(workspaces): gate the Jira palette match on the issue's tenant (#14671)

* fix(workspaces): gate the Jira palette match on the issue's tenant

Pasting a Jira issue URL matched any worktree whose linked item carried
the same `jiraIdentifier`, with no comparison of the site it came from.
Jira issue keys are per-project, not per-tenant, so every tenant with a
PROJ project has a PROJ-123 — pasting one tenant's URL could jump to a
worktree tracking a different tenant's issue entirely.

The stored linked URL is the only tenant evidence available here, so
where it exists it now decides, comparing origin and site path the way
the fallback already did. This also covers path-scoped Jira Server
installs sharing one host. The bare identifier still matches when no URL
was stored, since it is then the only evidence there is.

This mirrors the check `isWorkspaceLinkedItemSourceContextMatch` already
makes on the same fields.

* test(workspaces): cover the reachable Jira identifier fallback

The fallback fixture used a blank url, a shape `normalizeWorkspaceLinkedItem`
rejects outright, so it pinned a state that cannot reach the palette. The
reachable way to have no tenant evidence is a url that is present but is not
a Jira browse link, which is now what the test uses.

Also covers a pasted url carrying Jira's `atlOrigin` tracking query, since
the matcher compares pathname only.
This commit is contained in:
Neil
2026-08-14 21:36:00 -07:00
committed by GitHub
parent 3908978ba4
commit 2252de0f7f
2 changed files with 121 additions and 9 deletions
@@ -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', () => {
@@ -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: {