Fix Linear issue list response compatibility

Fixes #4588
This commit is contained in:
Neil
2026-06-03 14:23:26 -07:00
committed by GitHub
parent 280234a73e
commit 9b3eace37b
4 changed files with 128 additions and 10 deletions
@@ -102,6 +102,47 @@ describe('Branch source results', () => {
])
})
it('uses Linear rows from a paginated collection shape', () => {
const rows = buildSmartWorkspaceSourceRows({
mode: 'smart',
value: '',
branches: [],
githubItems: [],
gitlabItems: [],
linearIssues: {
items: [{ id: 'linear-1', identifier: 'ENG-1', title: 'Fix composer crash' } as never],
hasMore: true
},
gitlabAvailable: false,
linearAvailable: true,
resultLimit: 12
})
expect(rows).toEqual([
{
kind: 'linear',
value: 'linear-linear-1',
issue: { id: 'linear-1', identifier: 'ENG-1', title: 'Fix composer crash' }
}
])
})
it('ignores malformed Linear collection rows instead of throwing during render', () => {
expect(() =>
buildSmartWorkspaceSourceRows({
mode: 'smart',
value: '',
branches: [],
githubItems: [],
gitlabItems: [],
linearIssues: { items: { id: 'not-an-array' } } as never,
gitlabAvailable: false,
linearAvailable: true,
resultLimit: 12
})
).not.toThrow()
})
it('describes empty Branch results after the empty-query search runs', () => {
expect(getSmartWorkspaceEmptyHint('branches')).toBe('No matching branches.')
})
@@ -2,6 +2,7 @@ import type {
BaseRefSearchResult,
GitHubWorkItem,
GitLabWorkItem,
LinearCollectionResult,
LinearIssue
} from '../../../../shared/types'
@@ -15,6 +16,8 @@ export type SmartWorkspaceSourceRow =
| { kind: 'branch'; value: string; refName: string; localBranchName: string }
| { kind: 'linear'; value: string; issue: LinearIssue }
type LinearIssueSourceInput = LinearIssue[] | LinearCollectionResult<LinearIssue> | null | undefined
const EMPTY_HINT_BY_MODE: Record<SmartNameMode, string> = {
smart: 'Start typing to create a name or find a source.',
github: 'Start typing to search GitHub PRs and issues.',
@@ -92,7 +95,7 @@ export function buildSmartWorkspaceSourceRows({
gitlabAvailable: boolean
gitlabItems: GitLabWorkItem[]
linearAvailable: boolean
linearIssues: LinearIssue[]
linearIssues: LinearIssueSourceInput
mode: SmartNameMode
resultLimit: number
value: string
@@ -141,8 +144,15 @@ export function buildSmartWorkspaceSourceRows({
)
}
if (linearAvailable && (mode === 'smart' || mode === 'linear')) {
// Why: mixed-version runtime responses may briefly carry the paginated
// collection shape into this render path; rendering must stay recoverable.
const resolvedLinearIssues = Array.isArray(linearIssues)
? linearIssues
: Array.isArray(linearIssues?.items)
? linearIssues.items
: []
nextRows.push(
...linearIssues.map((issue) => ({
...resolvedLinearIssues.map((issue) => ({
kind: 'linear' as const,
value: `linear-${issue.id}`,
issue
@@ -132,6 +132,14 @@ describe('runtime linear client', () => {
expect(runtimeEnvironmentCall).not.toHaveBeenCalled()
})
it('wraps legacy local Linear issue list arrays as collection results', async () => {
linearListIssuesLocal.mockResolvedValue([{ id: 'legacy-issue' }])
await expect(
linearListIssues({ activeRuntimeEnvironmentId: null }, 'assigned', 20)
).resolves.toEqual({ items: [{ id: 'legacy-issue' }] })
})
it('does not throw when an older local preload lacks project listing', async () => {
delete (window.api.linear as { listProjects?: unknown }).listProjects
@@ -285,6 +293,39 @@ describe('runtime linear client', () => {
expect(linearListIssuesLocal).not.toHaveBeenCalled()
})
it('wraps legacy remote Linear issue list arrays as collection results', async () => {
runtimeEnvironmentCall.mockResolvedValueOnce({
id: 'rpc-list',
ok: true,
result: [{ id: 'legacy-issue' }],
_meta: { runtimeId: 'runtime-1' }
})
await expect(
linearListIssues({ activeRuntimeEnvironmentId: 'env-1' }, 'assigned', 20)
).resolves.toEqual({ items: [{ id: 'legacy-issue' }] })
expect(runtimeEnvironmentCall).toHaveBeenCalledWith({
selector: 'env-1',
method: 'linear.listIssues',
params: { filter: 'assigned', limit: 20, workspaceId: undefined },
timeoutMs: 30_000
})
})
it('falls back to an empty Linear issue collection for malformed list responses', async () => {
runtimeEnvironmentCall.mockResolvedValueOnce({
id: 'rpc-list',
ok: true,
result: { items: { id: 'not-an-array' }, hasMore: true },
_meta: { runtimeId: 'runtime-1' }
})
await expect(
linearListIssues({ activeRuntimeEnvironmentId: 'env-1' }, 'assigned', 20)
).resolves.toEqual({ items: [] })
})
it('routes Linear mutations and metadata through the selected runtime environment', async () => {
runtimeEnvironmentCall
.mockResolvedValueOnce({
@@ -39,6 +39,26 @@ function linearReadForce(options?: LinearReadOptions): { force: true } | {} {
return options?.force ? { force: true } : {}
}
function normalizeLinearIssueCollectionResult(
result: unknown
): LinearCollectionResult<LinearIssue> {
if (Array.isArray(result)) {
return { items: result as LinearIssue[] }
}
if (!result || typeof result !== 'object') {
return { items: [] }
}
const collection = result as Partial<LinearCollectionResult<LinearIssue>>
if (!Array.isArray(collection.items)) {
return { items: [] }
}
return {
items: collection.items,
...(Array.isArray(collection.errors) ? { errors: collection.errors } : {}),
...(typeof collection.hasMore === 'boolean' ? { hasMore: collection.hasMore } : {})
}
}
export async function linearStatus(
settings: RuntimeLinearSettings
): Promise<LinearConnectionStatus> {
@@ -144,14 +164,20 @@ export async function linearListIssues(
workspaceId?: LinearWorkspaceSelection | null
): Promise<LinearCollectionResult<LinearIssue>> {
const target = getActiveRuntimeTarget(settings)
return target.kind === 'environment'
? callRuntimeRpc<LinearCollectionResult<LinearIssue>>(
target,
'linear.listIssues',
{ filter, limit, workspaceId: workspaceId ?? undefined },
{ timeoutMs: 30_000 }
)
: window.api.linear.listIssues({ filter, limit, workspaceId: workspaceId ?? undefined })
const result =
target.kind === 'environment'
? await callRuntimeRpc<unknown>(
target,
'linear.listIssues',
{ filter, limit, workspaceId: workspaceId ?? undefined },
{ timeoutMs: 30_000 }
)
: await window.api.linear.listIssues({
filter,
limit,
workspaceId: workspaceId ?? undefined
})
return normalizeLinearIssueCollectionResult(result)
}
export async function linearCreateIssue(