diff --git a/src/main/ssh/ssh-remote-cli-format.test.ts b/src/main/ssh/ssh-remote-cli-format.test.ts index d4135f8dd3a..9ce0c2eee8b 100644 --- a/src/main/ssh/ssh-remote-cli-format.test.ts +++ b/src/main/ssh/ssh-remote-cli-format.test.ts @@ -96,4 +96,38 @@ describe('formatRemoteCli', () => { stderr: '' }) }) + + it('falls back to JSON for malformed Linear issue row fields', () => { + const response: RpcResponse = { + id: 'rpc-1', + ok: true, + _meta: meta, + result: { + issues: [ + { + id: 'issue-1', + identifier: 'ENG-1', + title: 'Broken state', + url: 'https://linear.app/acme/issue/ENG-1', + labels: [], + state: { name: 42 }, + workspace: { id: 'workspace-1', name: 'Acme' } + } + ], + meta: { + limit: null, + returned: 1, + hasMore: false, + orderBy: 'updatedAt', + partial: false, + workspaceErrors: [] + } + } + } + + expect(formatRemoteCli(response)).toEqual({ + stdout: `${JSON.stringify(response.result)}\n`, + stderr: '' + }) + }) }) diff --git a/src/main/ssh/ssh-remote-linear-result-guards.ts b/src/main/ssh/ssh-remote-linear-result-guards.ts index 41ad667f311..54b8ae42c17 100644 --- a/src/main/ssh/ssh-remote-linear-result-guards.ts +++ b/src/main/ssh/ssh-remote-linear-result-guards.ts @@ -47,10 +47,48 @@ function isLinearMcpIssue(issue: unknown): boolean { typeof issue.title === 'string' && typeof issue.url === 'string' && Array.isArray(issue.labels) && + issue.labels.every(isLinearNamedEntity) && + isOptionalLinearNamedEntity(issue.state) && + isOptionalLinearNamedEntity(issue.team) && + isOptionalLinearNamedEntity(issue.project) && + isOptionalLinearNamedEntity(issue.cycle) && + isOptionalLinearUser(issue.assignee) && + (issue.priority === undefined || + issue.priority === null || + typeof issue.priority === 'number') && isLinearWorkspaceCandidate(issue.workspace) ) } +function isOptionalLinearNamedEntity(value: unknown): boolean { + return value === undefined || value === null || isLinearNamedEntity(value) +} + +function isLinearNamedEntity(value: unknown): boolean { + return ( + isRecord(value) && + (value.id === undefined || value.id === null || typeof value.id === 'string') && + (value.name === undefined || value.name === null || typeof value.name === 'string') && + (value.color === undefined || value.color === null || typeof value.color === 'string') && + (value.type === undefined || value.type === null || typeof value.type === 'string') + ) +} + +function isOptionalLinearUser(value: unknown): boolean { + return ( + value === undefined || + value === null || + (isRecord(value) && + (value.id === undefined || value.id === null || typeof value.id === 'string') && + (value.displayName === undefined || + value.displayName === null || + typeof value.displayName === 'string') && + (value.avatarUrl === undefined || + value.avatarUrl === null || + typeof value.avatarUrl === 'string')) + ) +} + function isLinearWorkspaceCandidate(workspace: unknown): boolean { return ( isRecord(workspace) && typeof workspace.id === 'string' && typeof workspace.name === 'string'