mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
fix(new-workspace): guard all task URL transitions
This commit is contained in:
@@ -646,6 +646,13 @@ export default function SmartWorkspaceNameField({
|
||||
: null,
|
||||
[mode, value]
|
||||
)
|
||||
const gitlabUrlIntent = useMemo(
|
||||
() =>
|
||||
isSmartWorkspaceSourceQueryWithinLimit(value) && (mode === 'smart' || mode === 'gitlab')
|
||||
? parseGitLabIssueOrMRLink(value)
|
||||
: null,
|
||||
[mode, value]
|
||||
)
|
||||
const linearUrlIntent = useMemo(
|
||||
() => parseBoundedSmartWorkspaceLinearIssueUrlIntent(value),
|
||||
[value]
|
||||
@@ -1284,6 +1291,7 @@ export default function SmartWorkspaceNameField({
|
||||
value,
|
||||
debouncedQuery
|
||||
}),
|
||||
gitlabUrlIntent,
|
||||
jiraIntent: jiraSource.intent,
|
||||
jiraIssue: jiraSource.issue,
|
||||
jiraIssues: getVisibleHeldProviderResults({
|
||||
@@ -1313,6 +1321,7 @@ export default function SmartWorkspaceNameField({
|
||||
githubUrlIntent,
|
||||
gitlabSourceAvailable,
|
||||
gitlabItems,
|
||||
gitlabUrlIntent,
|
||||
jiraSource.accountChoices,
|
||||
jiraSource.intent,
|
||||
jiraSource.issue,
|
||||
|
||||
@@ -230,100 +230,6 @@ describe('Branch source results', () => {
|
||||
).toEqual([{ id: 'default-1' }])
|
||||
})
|
||||
|
||||
it('hides unrelated held results while a full GitHub URL resolves', () => {
|
||||
const targetUrl = 'https://github.com/acme/widgets/issues/42'
|
||||
const staleGitHubItem = {
|
||||
id: 'issue-17',
|
||||
type: 'issue' as const,
|
||||
number: 17,
|
||||
title: 'Wrong cached issue',
|
||||
state: 'open' as const,
|
||||
url: 'https://github.com/acme/widgets/issues/17',
|
||||
labels: [],
|
||||
updatedAt: '2026-08-01T00:00:00.000Z',
|
||||
author: 'octocat',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
githubUrlIntent: {
|
||||
slug: { owner: 'acme', repo: 'widgets', host: 'github.com' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
branches: [{ refName: 'origin/issues/17', localBranchName: 'issues/17' }],
|
||||
githubItems: [
|
||||
staleGitHubItem,
|
||||
{
|
||||
...staleGitHubItem,
|
||||
id: 'same-number-other-repo',
|
||||
number: 42,
|
||||
url: 'https://github.com/acme/other-repo/issues/42'
|
||||
},
|
||||
{
|
||||
...staleGitHubItem,
|
||||
id: 'same-number-pull-request',
|
||||
type: 'pr',
|
||||
number: 42,
|
||||
url: 'https://github.com/acme/widgets/pull/42'
|
||||
}
|
||||
],
|
||||
gitlabItems: [],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: false,
|
||||
linearAvailable: false,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([{ kind: 'use-name', value: 'use-name', name: targetUrl }])
|
||||
})
|
||||
|
||||
it('shows only the exact GitHub URL result after lookup settles', () => {
|
||||
const targetUrl = 'https://github.com/acme/widgets/issues/42'
|
||||
const exactItem = {
|
||||
id: 'issue-42',
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Exact pasted issue',
|
||||
state: 'open' as const,
|
||||
url: targetUrl,
|
||||
labels: [],
|
||||
updatedAt: '2026-08-02T00:00:00.000Z',
|
||||
author: 'octocat',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
const staleItem = {
|
||||
...exactItem,
|
||||
id: 'issue-17',
|
||||
number: 17,
|
||||
url: `${targetUrl.slice(0, -2)}17`
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
githubUrlIntent: {
|
||||
slug: { owner: 'ACME', repo: 'Widgets', host: 'GitHub.com' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
branches: [],
|
||||
githubItems: [staleItem, exactItem],
|
||||
gitlabItems: [],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: false,
|
||||
linearAvailable: false,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([
|
||||
{ kind: 'use-name', value: 'use-name', name: targetUrl },
|
||||
{ kind: 'github', value: 'github-repo-1-issue-42', item: exactItem }
|
||||
])
|
||||
})
|
||||
|
||||
it('uses stable cmdk values for typed-text actions', () => {
|
||||
expect(
|
||||
buildSmartWorkspaceSourceRows({
|
||||
|
||||
@@ -5,12 +5,11 @@ import type { LinearIssue } from '../linear/issue-types'
|
||||
import type { LinearCollectionResult } from '../linear/workspace-types'
|
||||
import type { BaseRefSearchResult } from '../repo-types'
|
||||
import { JIRA_ISSUE_KEY_PATTERN, parseJiraIssueUrl } from '../jira-issue-url'
|
||||
import { parseGitHubIssueOrPRLink, type GitHubIssueOrPRLink } from '../github/links'
|
||||
import { githubRepoIdentityKey } from '../github/repository-identity-key'
|
||||
import type { GitHubIssueOrPRLink } from '../github/links'
|
||||
import {
|
||||
isSmartWorkspaceLinearIssueIntentMatch,
|
||||
parseBoundedSmartWorkspaceLinearIssueUrlIntent
|
||||
} from './smart-workspace-linear-intent'
|
||||
buildSmartWorkspaceUrlSourceRows,
|
||||
type SmartWorkspaceGitLabUrlIntent
|
||||
} from './smart-workspace-url-source-results'
|
||||
import { isSmartWorkspaceSourceQueryWithinLimit } from './smart-workspace-source-query'
|
||||
|
||||
export {
|
||||
@@ -65,16 +64,6 @@ function toJiraSourceRow(issue: JiraIssue): SmartWorkspaceSourceRow {
|
||||
return { kind: 'jira', value: `jira-${issue.siteId ?? ''}-${issue.key}`, issue }
|
||||
}
|
||||
|
||||
function isGitHubLinkIntentMatch(intent: GitHubIssueOrPRLink, item: GitHubWorkItem): boolean {
|
||||
const itemLink = parseGitHubIssueOrPRLink(item.url)
|
||||
return (
|
||||
itemLink !== null &&
|
||||
itemLink.type === intent.type &&
|
||||
itemLink.number === intent.number &&
|
||||
githubRepoIdentityKey(itemLink.slug) === githubRepoIdentityKey(intent.slug)
|
||||
)
|
||||
}
|
||||
|
||||
export function getBranchSearchRequest({
|
||||
branchesEnabled,
|
||||
disabled,
|
||||
@@ -201,6 +190,7 @@ export function buildSmartWorkspaceSourceRows({
|
||||
githubUrlIntent,
|
||||
gitlabAvailable,
|
||||
gitlabItems,
|
||||
gitlabUrlIntent,
|
||||
jiraIntent = false,
|
||||
jiraIssue,
|
||||
jiraIssues = [],
|
||||
@@ -216,6 +206,7 @@ export function buildSmartWorkspaceSourceRows({
|
||||
githubUrlIntent?: GitHubIssueOrPRLink | null
|
||||
gitlabAvailable: boolean
|
||||
gitlabItems: GitLabWorkItem[]
|
||||
gitlabUrlIntent?: SmartWorkspaceGitLabUrlIntent | null
|
||||
jiraIntent?: boolean
|
||||
jiraIssue?: JiraIssue | null
|
||||
jiraIssues?: JiraIssue[]
|
||||
@@ -233,48 +224,30 @@ export function buildSmartWorkspaceSourceRows({
|
||||
if (!isSmartWorkspaceSourceQueryWithinLimit(value)) {
|
||||
return []
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
if (githubUrlIntent && (mode === 'smart' || mode === 'github')) {
|
||||
const githubRows = githubItems
|
||||
.filter((item) => isGitHubLinkIntentMatch(githubUrlIntent, item))
|
||||
.map((item) => ({
|
||||
kind: 'github' as const,
|
||||
value: `github-${item.repoId}-${item.type}-${item.number}`,
|
||||
item
|
||||
}))
|
||||
.slice(0, resultLimit)
|
||||
// Why: the full URL is unambiguous, so unrelated held provider rows must never become selectable.
|
||||
return trimmed && mode === 'smart'
|
||||
? [{ kind: 'use-name' as const, value: 'use-name', name: trimmed }, ...githubRows]
|
||||
: githubRows
|
||||
}
|
||||
const nextRows: SmartWorkspaceSourceRow[] = []
|
||||
const resolvedLinearIssues = Array.isArray(linearIssues)
|
||||
? linearIssues
|
||||
: Array.isArray(linearIssues?.items)
|
||||
? linearIssues.items
|
||||
: []
|
||||
const linearUrlIntent = parseBoundedSmartWorkspaceLinearIssueUrlIntent(trimmed)
|
||||
if (
|
||||
linearUrlIntentOwnsResults &&
|
||||
linearAvailable &&
|
||||
linearUrlIntent &&
|
||||
(mode === 'smart' || mode === 'linear')
|
||||
) {
|
||||
const linearRows = resolvedLinearIssues
|
||||
.filter((issue) => isSmartWorkspaceLinearIssueIntentMatch(linearUrlIntent, issue))
|
||||
.map((issue) => ({
|
||||
kind: 'linear' as const,
|
||||
value: `linear-${issue.id}`,
|
||||
issue
|
||||
}))
|
||||
.slice(0, resultLimit)
|
||||
// Why: keep "use as workspace name" available; sourceIntent focuses the issue.
|
||||
if (trimmed && mode === 'smart') {
|
||||
return [{ kind: 'use-name' as const, value: 'use-name', name: trimmed }, ...linearRows]
|
||||
}
|
||||
return linearRows
|
||||
// Why: a full task URL is unambiguous, so unrelated held rows must never remain selectable.
|
||||
const urlSourceRows = buildSmartWorkspaceUrlSourceRows({
|
||||
githubItems,
|
||||
githubUrlIntent,
|
||||
gitlabAvailable,
|
||||
gitlabItems,
|
||||
gitlabUrlIntent,
|
||||
linearAvailable,
|
||||
linearIssues: resolvedLinearIssues,
|
||||
linearUrlIntentOwnsResults,
|
||||
mode,
|
||||
resultLimit,
|
||||
value
|
||||
})
|
||||
if (urlSourceRows !== null) {
|
||||
return urlSourceRows
|
||||
}
|
||||
const trimmed = value.trim()
|
||||
const nextRows: SmartWorkspaceSourceRow[] = []
|
||||
if (trimmed && mode === 'smart') {
|
||||
// Why: stable cmdk value — embedding the query remounted the row every keystroke.
|
||||
nextRows.push({ kind: 'use-name', value: 'use-name', name: trimmed })
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { buildSmartWorkspaceUrlSourceRows } from './smart-workspace-url-source-results'
|
||||
|
||||
describe('smart workspace URL-owned source rows', () => {
|
||||
it('hides unrelated held results while a full GitHub URL resolves', () => {
|
||||
const targetUrl = 'https://github.com/acme/widgets/issues/42'
|
||||
const staleItem = {
|
||||
id: 'issue-17',
|
||||
type: 'issue' as const,
|
||||
number: 17,
|
||||
title: 'Wrong cached issue',
|
||||
state: 'open' as const,
|
||||
url: 'https://github.com/acme/widgets/issues/17',
|
||||
labels: [],
|
||||
updatedAt: '2026-08-01T00:00:00.000Z',
|
||||
author: 'octocat',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceUrlSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
githubUrlIntent: {
|
||||
slug: { owner: 'acme', repo: 'widgets', host: 'github.com' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
githubItems: [
|
||||
staleItem,
|
||||
{
|
||||
...staleItem,
|
||||
id: 'same-number-other-repo',
|
||||
number: 42,
|
||||
url: 'https://github.com/acme/other-repo/issues/42'
|
||||
},
|
||||
{
|
||||
...staleItem,
|
||||
id: 'same-number-pull-request',
|
||||
type: 'pr',
|
||||
number: 42,
|
||||
url: 'https://github.com/acme/widgets/pull/42'
|
||||
}
|
||||
],
|
||||
gitlabItems: [],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: false,
|
||||
linearAvailable: false,
|
||||
linearUrlIntentOwnsResults: true,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([{ kind: 'use-name', value: 'use-name', name: targetUrl }])
|
||||
})
|
||||
|
||||
it('shows only the exact GitHub URL result after lookup settles', () => {
|
||||
const targetUrl = 'https://github.com/acme/widgets/issues/42'
|
||||
const exactItem = {
|
||||
id: 'issue-42',
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Exact pasted issue',
|
||||
state: 'open' as const,
|
||||
url: targetUrl,
|
||||
labels: [],
|
||||
updatedAt: '2026-08-02T00:00:00.000Z',
|
||||
author: 'octocat',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceUrlSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
githubUrlIntent: {
|
||||
slug: { owner: 'ACME', repo: 'Widgets', host: 'GitHub.com' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
githubItems: [
|
||||
{ ...exactItem, id: 'issue-17', number: 17, url: `${targetUrl.slice(0, -2)}17` },
|
||||
exactItem
|
||||
],
|
||||
gitlabItems: [],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: false,
|
||||
linearAvailable: false,
|
||||
linearUrlIntentOwnsResults: true,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([
|
||||
{ kind: 'use-name', value: 'use-name', name: targetUrl },
|
||||
{ kind: 'github', value: 'github-repo-1-issue-42', item: exactItem }
|
||||
])
|
||||
})
|
||||
|
||||
it('hides unrelated held results while a full GitLab URL resolves', () => {
|
||||
const targetUrl = 'https://gitlab.example.test/acme/widgets/-/issues/42'
|
||||
const staleItem = {
|
||||
id: 'mr-17',
|
||||
type: 'mr' as const,
|
||||
number: 17,
|
||||
title: 'Wrong cached merge request',
|
||||
state: 'opened' as const,
|
||||
url: 'https://gitlab.example.test/acme/widgets/-/merge_requests/17',
|
||||
labels: [],
|
||||
updatedAt: '2026-08-01T00:00:00.000Z',
|
||||
author: 'gitlab-user',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceUrlSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
gitlabUrlIntent: {
|
||||
slug: { host: 'gitlab.example.test', path: 'acme/widgets' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
githubItems: [],
|
||||
gitlabItems: [
|
||||
staleItem,
|
||||
{
|
||||
...staleItem,
|
||||
id: 'same-number-other-project',
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
url: 'https://gitlab.example.test/acme/other/-/issues/42'
|
||||
},
|
||||
{
|
||||
...staleItem,
|
||||
id: 'same-project-other-host',
|
||||
type: 'issue',
|
||||
number: 42,
|
||||
url: 'https://gitlab.other.test/acme/widgets/-/issues/42'
|
||||
}
|
||||
],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: true,
|
||||
linearAvailable: false,
|
||||
linearUrlIntentOwnsResults: true,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([{ kind: 'use-name', value: 'use-name', name: targetUrl }])
|
||||
})
|
||||
|
||||
it('shows only the exact GitLab URL result after lookup settles', () => {
|
||||
const targetUrl = 'https://gitlab.example.test/acme/widgets/-/issues/42'
|
||||
const exactItem = {
|
||||
id: 'issue-42',
|
||||
type: 'issue' as const,
|
||||
number: 42,
|
||||
title: 'Exact pasted issue',
|
||||
state: 'opened' as const,
|
||||
url: targetUrl,
|
||||
labels: [],
|
||||
updatedAt: '2026-08-02T00:00:00.000Z',
|
||||
author: 'gitlab-user',
|
||||
repoId: 'repo-1'
|
||||
}
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceUrlSourceRows({
|
||||
mode: 'smart',
|
||||
value: targetUrl,
|
||||
gitlabUrlIntent: {
|
||||
slug: { host: 'GITLAB.EXAMPLE.TEST', path: 'Acme/Widgets' },
|
||||
type: 'issue',
|
||||
number: 42
|
||||
},
|
||||
githubItems: [],
|
||||
gitlabItems: [
|
||||
{
|
||||
...exactItem,
|
||||
id: 'issue-17',
|
||||
number: 17,
|
||||
url: 'https://gitlab.example.test/acme/widgets/-/issues/17'
|
||||
},
|
||||
exactItem
|
||||
],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: true,
|
||||
linearAvailable: false,
|
||||
linearUrlIntentOwnsResults: true,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([
|
||||
{ kind: 'use-name', value: 'use-name', name: targetUrl },
|
||||
{ kind: 'gitlab', value: 'gitlab-repo-1-issue-42', item: exactItem }
|
||||
])
|
||||
})
|
||||
|
||||
it('hides held provider rows when a Linear URL is recognized but disconnected', () => {
|
||||
const issueUrl = 'https://linear.app/stably/issue/STA-4084/restore-shell-integration'
|
||||
|
||||
expect(
|
||||
buildSmartWorkspaceUrlSourceRows({
|
||||
mode: 'smart',
|
||||
value: issueUrl,
|
||||
githubItems: [{ repoId: 'repo-1', type: 'issue', number: 17 } as never],
|
||||
gitlabItems: [{ repoId: 'repo-1', type: 'mr', number: 17 } as never],
|
||||
linearIssues: [],
|
||||
gitlabAvailable: true,
|
||||
linearAvailable: false,
|
||||
linearUrlIntentOwnsResults: true,
|
||||
resultLimit: 12
|
||||
})
|
||||
).toEqual([{ kind: 'use-name', value: 'use-name', name: issueUrl }])
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,126 @@
|
||||
import { parseGitHubIssueOrPRLink, type GitHubIssueOrPRLink } from '../github/links'
|
||||
import { githubRepoIdentityKey } from '../github/repository-identity-key'
|
||||
import type { GitHubWorkItem } from '../github/work-item-types'
|
||||
import type { GitLabWorkItem } from '../gitlab-types'
|
||||
import type { LinearIssue } from '../linear/issue-types'
|
||||
import { parseGitLabIssueOrMRLink } from './gitlab-links'
|
||||
import {
|
||||
isSmartWorkspaceLinearIssueIntentMatch,
|
||||
parseBoundedSmartWorkspaceLinearIssueUrlIntent
|
||||
} from './smart-workspace-linear-intent'
|
||||
|
||||
export type SmartWorkspaceGitLabUrlIntent = NonNullable<ReturnType<typeof parseGitLabIssueOrMRLink>>
|
||||
|
||||
type SmartWorkspaceUrlSourceMode =
|
||||
| 'smart'
|
||||
| 'github'
|
||||
| 'gitlab'
|
||||
| 'branches'
|
||||
| 'linear'
|
||||
| 'jira'
|
||||
| 'text'
|
||||
|
||||
export type SmartWorkspaceUrlSourceRow =
|
||||
| { kind: 'use-name'; value: 'use-name'; name: string }
|
||||
| { kind: 'github'; value: string; item: GitHubWorkItem }
|
||||
| { kind: 'gitlab'; value: string; item: GitLabWorkItem }
|
||||
| { kind: 'linear'; value: string; issue: LinearIssue }
|
||||
|
||||
function withSmartNameFallback(
|
||||
mode: SmartWorkspaceUrlSourceMode,
|
||||
name: string,
|
||||
rows: SmartWorkspaceUrlSourceRow[]
|
||||
): SmartWorkspaceUrlSourceRow[] {
|
||||
return name && mode === 'smart' ? [{ kind: 'use-name', value: 'use-name', name }, ...rows] : rows
|
||||
}
|
||||
|
||||
function isGitHubLinkIntentMatch(intent: GitHubIssueOrPRLink, item: GitHubWorkItem): boolean {
|
||||
const itemLink = parseGitHubIssueOrPRLink(item.url)
|
||||
return (
|
||||
itemLink !== null &&
|
||||
itemLink.type === intent.type &&
|
||||
itemLink.number === intent.number &&
|
||||
githubRepoIdentityKey(itemLink.slug) === githubRepoIdentityKey(intent.slug)
|
||||
)
|
||||
}
|
||||
|
||||
function isGitLabLinkIntentMatch(
|
||||
intent: SmartWorkspaceGitLabUrlIntent,
|
||||
item: GitLabWorkItem
|
||||
): boolean {
|
||||
const itemLink = parseGitLabIssueOrMRLink(item.url)
|
||||
return (
|
||||
itemLink !== null &&
|
||||
itemLink.type === intent.type &&
|
||||
itemLink.number === intent.number &&
|
||||
itemLink.slug.host.toLowerCase() === intent.slug.host.toLowerCase() &&
|
||||
itemLink.slug.path.toLowerCase() === intent.slug.path.toLowerCase()
|
||||
)
|
||||
}
|
||||
|
||||
export function buildSmartWorkspaceUrlSourceRows({
|
||||
githubItems,
|
||||
githubUrlIntent,
|
||||
gitlabAvailable,
|
||||
gitlabItems,
|
||||
gitlabUrlIntent,
|
||||
linearAvailable,
|
||||
linearIssues,
|
||||
linearUrlIntentOwnsResults,
|
||||
mode,
|
||||
resultLimit,
|
||||
value
|
||||
}: {
|
||||
githubItems: GitHubWorkItem[]
|
||||
githubUrlIntent?: GitHubIssueOrPRLink | null
|
||||
gitlabAvailable: boolean
|
||||
gitlabItems: GitLabWorkItem[]
|
||||
gitlabUrlIntent?: SmartWorkspaceGitLabUrlIntent | null
|
||||
linearAvailable: boolean
|
||||
linearIssues: LinearIssue[]
|
||||
linearUrlIntentOwnsResults: boolean
|
||||
mode: SmartWorkspaceUrlSourceMode
|
||||
resultLimit: number
|
||||
value: string
|
||||
}): SmartWorkspaceUrlSourceRow[] | null {
|
||||
const trimmed = value.trim()
|
||||
if (githubUrlIntent && (mode === 'smart' || mode === 'github')) {
|
||||
const rows = githubItems
|
||||
.filter((item) => isGitHubLinkIntentMatch(githubUrlIntent, item))
|
||||
.map((item) => ({
|
||||
kind: 'github' as const,
|
||||
value: `github-${item.repoId}-${item.type}-${item.number}`,
|
||||
item
|
||||
}))
|
||||
.slice(0, resultLimit)
|
||||
return withSmartNameFallback(mode, trimmed, rows)
|
||||
}
|
||||
if (gitlabUrlIntent && (mode === 'smart' || mode === 'gitlab')) {
|
||||
const rows = gitlabAvailable
|
||||
? gitlabItems
|
||||
.filter((item) => isGitLabLinkIntentMatch(gitlabUrlIntent, item))
|
||||
.map((item) => ({
|
||||
kind: 'gitlab' as const,
|
||||
value: `gitlab-${item.repoId}-${item.type}-${item.number}`,
|
||||
item
|
||||
}))
|
||||
.slice(0, resultLimit)
|
||||
: []
|
||||
return withSmartNameFallback(mode, trimmed, rows)
|
||||
}
|
||||
const linearUrlIntent = parseBoundedSmartWorkspaceLinearIssueUrlIntent(trimmed)
|
||||
if (linearUrlIntentOwnsResults && linearUrlIntent && (mode === 'smart' || mode === 'linear')) {
|
||||
const rows = linearAvailable
|
||||
? linearIssues
|
||||
.filter((issue) => isSmartWorkspaceLinearIssueIntentMatch(linearUrlIntent, issue))
|
||||
.map((issue) => ({
|
||||
kind: 'linear' as const,
|
||||
value: `linear-${issue.id}`,
|
||||
issue
|
||||
}))
|
||||
.slice(0, resultLimit)
|
||||
: []
|
||||
return withSmartNameFallback(mode, trimmed, rows)
|
||||
}
|
||||
return null
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import type { ElectronApplication, Locator, Page } from '@stablyai/playwright-test'
|
||||
import type { GitHubWorkItem } from '../../src/shared/github/work-item-types'
|
||||
import type { GitLabWorkItem } from '../../src/shared/gitlab-types'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { waitForActiveWorktree, waitForSessionReady } from './helpers/store'
|
||||
|
||||
const TARGET_URL = 'https://github.com/stablyai/orca/issues/4242'
|
||||
const WRONG_TITLE = 'Wrong cached issue'
|
||||
const TARGET_TITLE = 'Exact pasted issue'
|
||||
const GITLAB_TARGET_URL = 'https://gitlab.example.test/stablyai/orca/-/merge_requests/4242'
|
||||
const GITLAB_WRONG_TITLE = 'Wrong cached merge request'
|
||||
const GITLAB_TARGET_TITLE = 'Exact pasted merge request'
|
||||
const TRANSITION_FRAME_LIMIT = 600
|
||||
|
||||
const WRONG_ITEM: GitHubWorkItem = {
|
||||
id: 'issue-17',
|
||||
@@ -29,6 +34,28 @@ const TARGET_ITEM: GitHubWorkItem = {
|
||||
updatedAt: '2026-08-02T00:00:00.000Z'
|
||||
}
|
||||
|
||||
const GITLAB_WRONG_ITEM: GitLabWorkItem = {
|
||||
id: 'mr-17',
|
||||
type: 'mr',
|
||||
number: 17,
|
||||
title: GITLAB_WRONG_TITLE,
|
||||
state: 'opened',
|
||||
url: 'https://gitlab.example.test/stablyai/orca/-/merge_requests/17',
|
||||
labels: [],
|
||||
updatedAt: '2026-08-01T00:00:00.000Z',
|
||||
author: 'e2e',
|
||||
repoId: 'e2e-repo'
|
||||
}
|
||||
|
||||
const GITLAB_TARGET_ITEM: GitLabWorkItem = {
|
||||
...GITLAB_WRONG_ITEM,
|
||||
id: 'mr-4242',
|
||||
number: 4242,
|
||||
title: GITLAB_TARGET_TITLE,
|
||||
url: GITLAB_TARGET_URL,
|
||||
updatedAt: '2026-08-02T00:00:00.000Z'
|
||||
}
|
||||
|
||||
type TransitionFrame = {
|
||||
value: string
|
||||
wrongVisible: boolean
|
||||
@@ -41,6 +68,82 @@ function pasteChord(): string {
|
||||
return process.platform === 'darwin' ? 'Meta+V' : 'Control+V'
|
||||
}
|
||||
|
||||
async function startTransitionCapture(
|
||||
page: Page,
|
||||
frameKey: string,
|
||||
wrongTitle: string,
|
||||
targetTitle: string
|
||||
): Promise<void> {
|
||||
await page.evaluate(
|
||||
({ frameKey, frameLimit, wrongTitle, targetTitle }) => {
|
||||
const frames: TransitionFrame[] = []
|
||||
const capture = (): void => {
|
||||
const input = document.querySelector<HTMLInputElement>('[data-workspace-name-input="true"]')
|
||||
const options = [...document.querySelectorAll<HTMLElement>('[role="option"]')]
|
||||
const wrong = options.find((option) => option.textContent?.includes(wrongTitle))
|
||||
const target = options.find((option) => option.textContent?.includes(targetTitle))
|
||||
frames.push({
|
||||
value: input?.value ?? '',
|
||||
wrongVisible: Boolean(wrong && wrong.getClientRects().length > 0),
|
||||
wrongSelected: wrong?.dataset.selected === 'true',
|
||||
targetVisible: Boolean(target && target.getClientRects().length > 0),
|
||||
targetSelected: target?.dataset.selected === 'true'
|
||||
})
|
||||
if (frames.length < frameLimit) {
|
||||
requestAnimationFrame(capture)
|
||||
}
|
||||
}
|
||||
Reflect.set(window, frameKey, frames)
|
||||
capture()
|
||||
},
|
||||
{ frameKey, frameLimit: TRANSITION_FRAME_LIMIT, wrongTitle, targetTitle }
|
||||
)
|
||||
}
|
||||
|
||||
async function readTransitionFrames(page: Page, frameKey: string): Promise<TransitionFrame[]> {
|
||||
return page.evaluate((key) => Reflect.get(window, key) as TransitionFrame[], frameKey)
|
||||
}
|
||||
|
||||
async function expectLookupHeldWithoutStaleRow(
|
||||
page: Page,
|
||||
frameKey: string,
|
||||
targetUrl: string,
|
||||
wrongOption: Locator,
|
||||
targetOption: Locator
|
||||
): Promise<void> {
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const frames = await readTransitionFrames(page, frameKey)
|
||||
return frames.filter((frame) => frame.value === targetUrl).length
|
||||
})
|
||||
.toBeGreaterThan(10)
|
||||
await expect(wrongOption).toHaveCount(0)
|
||||
await expect(targetOption).toHaveCount(0)
|
||||
}
|
||||
|
||||
async function expectExactTargetAfterLookup(
|
||||
page: Page,
|
||||
frameKey: string,
|
||||
targetUrl: string,
|
||||
targetOption: Locator
|
||||
): Promise<void> {
|
||||
await expect(targetOption).toBeVisible()
|
||||
await expect(targetOption).toHaveAttribute('data-selected', 'true')
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const frames = await readTransitionFrames(page, frameKey)
|
||||
return frames.some((frame) => frame.targetVisible && frame.targetSelected)
|
||||
})
|
||||
.toBe(true)
|
||||
|
||||
const frames = await readTransitionFrames(page, frameKey)
|
||||
const pastedFrames = frames.filter((frame) => frame.value === targetUrl)
|
||||
expect(frames.some((frame) => frame.wrongVisible)).toBe(true)
|
||||
expect(pastedFrames.length).toBeGreaterThan(10)
|
||||
expect(pastedFrames.every((frame) => !frame.wrongVisible && !frame.wrongSelected)).toBe(true)
|
||||
expect(pastedFrames.some((frame) => frame.targetVisible && frame.targetSelected)).toBe(true)
|
||||
}
|
||||
|
||||
async function installHeldGitHubLookup(
|
||||
electronApp: ElectronApplication,
|
||||
page: Page
|
||||
@@ -86,6 +189,65 @@ async function releaseGitHubLookup(electronApp: ElectronApplication): Promise<vo
|
||||
})
|
||||
}
|
||||
|
||||
async function installHeldGitLabLookup(
|
||||
electronApp: ElectronApplication,
|
||||
page: Page
|
||||
): Promise<void> {
|
||||
await electronApp.evaluate(
|
||||
({ ipcMain }, { wrongItem, targetItem }) => {
|
||||
const fixture = globalThis as unknown as {
|
||||
__gitlabUrlLookupStarted?: boolean
|
||||
__releaseGitLabUrlLookup?: () => void
|
||||
}
|
||||
fixture.__gitlabUrlLookupStarted = false
|
||||
ipcMain.removeHandler('gitlab:listMRs')
|
||||
ipcMain.handle('gitlab:listMRs', () => ({
|
||||
items: [wrongItem],
|
||||
page: 1,
|
||||
perPage: 12,
|
||||
totalCount: 1,
|
||||
totalPages: 1
|
||||
}))
|
||||
ipcMain.removeHandler('gitlab:workItemByPath')
|
||||
ipcMain.handle('gitlab:workItemByPath', () => {
|
||||
fixture.__gitlabUrlLookupStarted = true
|
||||
return new Promise((resolve) => {
|
||||
fixture.__releaseGitLabUrlLookup = () => resolve(targetItem)
|
||||
})
|
||||
})
|
||||
},
|
||||
{ wrongItem: GITLAB_WRONG_ITEM, targetItem: GITLAB_TARGET_ITEM }
|
||||
)
|
||||
await page.evaluate(() => {
|
||||
const store = window.__store
|
||||
if (!store) {
|
||||
throw new Error('window.__store is not available')
|
||||
}
|
||||
const state = store.getState()
|
||||
if (!state.preflightStatusContextKey) {
|
||||
throw new Error('preflight context is not ready')
|
||||
}
|
||||
store.setState({
|
||||
preflightStatus: {
|
||||
git: state.preflightStatus?.git ?? { installed: true },
|
||||
gh: state.preflightStatus?.gh ?? { installed: true, authenticated: true },
|
||||
glab: { installed: true, authenticated: true }
|
||||
},
|
||||
preflightStatusChecked: true
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
async function releaseGitLabLookup(electronApp: ElectronApplication): Promise<void> {
|
||||
await electronApp.evaluate(() => {
|
||||
const fixture = globalThis as unknown as { __releaseGitLabUrlLookup?: () => void }
|
||||
if (!fixture.__releaseGitLabUrlLookup) {
|
||||
throw new Error('GitLab lookup is not held')
|
||||
}
|
||||
fixture.__releaseGitLabUrlLookup()
|
||||
})
|
||||
}
|
||||
|
||||
test('a pasted GitHub URL never selects a stale cached issue', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
@@ -107,28 +269,8 @@ test('a pasted GitHub URL never selects a stale cached issue', async ({
|
||||
})
|
||||
await expect(wrongOption).toBeVisible()
|
||||
|
||||
await orcaPage.evaluate(
|
||||
({ wrongTitle, targetTitle }) => {
|
||||
const frames: TransitionFrame[] = []
|
||||
const capture = (): void => {
|
||||
const input = document.querySelector<HTMLInputElement>('[data-workspace-name-input="true"]')
|
||||
const options = [...document.querySelectorAll<HTMLElement>('[role="option"]')]
|
||||
const wrong = options.find((option) => option.textContent?.includes(wrongTitle))
|
||||
const target = options.find((option) => option.textContent?.includes(targetTitle))
|
||||
frames.push({
|
||||
value: input?.value ?? '',
|
||||
wrongVisible: Boolean(wrong && wrong.getClientRects().length > 0),
|
||||
wrongSelected: wrong?.dataset.selected === 'true',
|
||||
targetVisible: Boolean(target && target.getClientRects().length > 0),
|
||||
targetSelected: target?.dataset.selected === 'true'
|
||||
})
|
||||
requestAnimationFrame(capture)
|
||||
}
|
||||
Reflect.set(window, '__githubUrlTransitionFrames', frames)
|
||||
capture()
|
||||
},
|
||||
{ wrongTitle: WRONG_TITLE, targetTitle: TARGET_TITLE }
|
||||
)
|
||||
const frameKey = '__githubUrlTransitionFrames'
|
||||
await startTransitionCapture(orcaPage, frameKey, WRONG_TITLE, TARGET_TITLE)
|
||||
|
||||
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), TARGET_URL)
|
||||
await input.focus()
|
||||
@@ -141,40 +283,68 @@ test('a pasted GitHub URL never selects a stale cached issue', async ({
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
await expect
|
||||
.poll(() =>
|
||||
orcaPage.evaluate((url) => {
|
||||
const frames = Reflect.get(window, '__githubUrlTransitionFrames') as TransitionFrame[]
|
||||
return frames.filter((frame) => frame.value === url).length
|
||||
}, TARGET_URL)
|
||||
)
|
||||
.toBeGreaterThan(10)
|
||||
await expect(wrongOption).toHaveCount(0)
|
||||
await expect(targetOption).toHaveCount(0)
|
||||
await expectLookupHeldWithoutStaleRow(orcaPage, frameKey, TARGET_URL, wrongOption, targetOption)
|
||||
|
||||
await releaseGitHubLookup(electronApp)
|
||||
await expect(targetOption).toBeVisible()
|
||||
await expect(targetOption).toHaveAttribute('data-selected', 'true')
|
||||
await expect
|
||||
.poll(() =>
|
||||
orcaPage.evaluate(() => {
|
||||
const frames = Reflect.get(window, '__githubUrlTransitionFrames') as TransitionFrame[]
|
||||
return frames.some((frame) => frame.targetVisible && frame.targetSelected)
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
|
||||
const frames = await orcaPage.evaluate(() => {
|
||||
return Reflect.get(window, '__githubUrlTransitionFrames') as TransitionFrame[]
|
||||
})
|
||||
const pastedFrames = frames.filter((frame) => frame.value === TARGET_URL)
|
||||
expect(frames.some((frame) => frame.wrongVisible)).toBe(true)
|
||||
expect(pastedFrames.length).toBeGreaterThan(10)
|
||||
expect(pastedFrames.every((frame) => !frame.wrongVisible && !frame.wrongSelected)).toBe(true)
|
||||
expect(pastedFrames.some((frame) => frame.targetVisible && frame.targetSelected)).toBe(true)
|
||||
await expectExactTargetAfterLookup(orcaPage, frameKey, TARGET_URL, targetOption)
|
||||
|
||||
await testInfo.attach('github-url-smart-input-fixed.png', {
|
||||
body: await orcaPage.screenshot(),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
})
|
||||
|
||||
test('a pasted GitLab URL never selects a stale cached merge request', async ({
|
||||
electronApp,
|
||||
orcaPage
|
||||
}, testInfo) => {
|
||||
await waitForSessionReady(orcaPage)
|
||||
await waitForActiveWorktree(orcaPage)
|
||||
await installHeldGitLabLookup(electronApp, orcaPage)
|
||||
|
||||
await orcaPage.getByRole('button', { name: 'New workspace', exact: true }).click()
|
||||
const dialog = orcaPage.getByRole('dialog', { name: /Create (Workspace|Worktree)/i })
|
||||
const input = dialog.locator('[data-workspace-name-input="true"]')
|
||||
await expect(input).toBeVisible()
|
||||
await input.click()
|
||||
|
||||
const wrongOption = orcaPage.getByRole('option', {
|
||||
name: `!17 ${GITLAB_WRONG_TITLE}`,
|
||||
exact: true
|
||||
})
|
||||
const targetOption = orcaPage.getByRole('option', {
|
||||
name: `!4242 ${GITLAB_TARGET_TITLE}`,
|
||||
exact: true
|
||||
})
|
||||
await expect(wrongOption).toBeVisible()
|
||||
|
||||
const frameKey = '__gitlabUrlTransitionFrames'
|
||||
await startTransitionCapture(orcaPage, frameKey, GITLAB_WRONG_TITLE, GITLAB_TARGET_TITLE)
|
||||
|
||||
await orcaPage.evaluate((text) => window.api.ui.writeClipboardText(text), GITLAB_TARGET_URL)
|
||||
await input.focus()
|
||||
await orcaPage.keyboard.press(pasteChord())
|
||||
await expect
|
||||
.poll(() =>
|
||||
electronApp.evaluate(() => {
|
||||
const fixture = globalThis as unknown as { __gitlabUrlLookupStarted?: boolean }
|
||||
return fixture.__gitlabUrlLookupStarted === true
|
||||
})
|
||||
)
|
||||
.toBe(true)
|
||||
await expectLookupHeldWithoutStaleRow(
|
||||
orcaPage,
|
||||
frameKey,
|
||||
GITLAB_TARGET_URL,
|
||||
wrongOption,
|
||||
targetOption
|
||||
)
|
||||
|
||||
await releaseGitLabLookup(electronApp)
|
||||
await expectExactTargetAfterLookup(orcaPage, frameKey, GITLAB_TARGET_URL, targetOption)
|
||||
|
||||
await testInfo.attach('gitlab-url-smart-input-fixed.png', {
|
||||
body: await orcaPage.screenshot(),
|
||||
contentType: 'image/png'
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user