Files
orca/src/main/linear/mcp-issue-list.test.ts
T
mirror 34fa2cd5ca
Skill update round trip / roundtrip (false, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, ubuntu-latest, symlink, latest) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (false, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, macos-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, ubuntu-latest, symlink, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, copy, 1.5.17) (push) Canceled after 0s
Skill update round trip / roundtrip (true, windows-latest, symlink, 1.5.17) (push) Canceled after 0s
source snapshot 2026-09-02
2026-09-02 02:03:16 +08:00

304 lines
9.9 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
const rawRequest = vi.fn()
const getClients = vi.fn()
const getStatus = vi.fn()
const acquire = vi.fn()
const release = vi.fn()
const clearToken = vi.fn()
const workspace = (id: string, organizationName: string) => ({
id,
organizationId: id,
organizationName,
displayName: 'Ada',
email: null
})
const clientEntry = (
id: string,
organizationName: string,
request: ReturnType<typeof vi.fn> = rawRequest
) => ({
workspace: workspace(id, organizationName),
client: { client: { rawRequest: request } }
})
vi.mock('./linear-request-concurrency', () => ({
acquire,
release
}))
vi.mock('./linear-token-store', () => ({
clearToken
}))
vi.mock('./client', () => ({
getClients,
getStatus,
isAuthError: () => false
}))
describe('MCP-compatible Linear issue listing', () => {
beforeEach(() => {
vi.clearAllMocks()
const entry = clientEntry('workspace-1', 'Acme')
getClients.mockReturnValue([entry])
getStatus.mockReturnValue({ workspaces: [entry.workspace] })
})
it('passes rich filters, ordering, archive scope, and cursor to Linear', async () => {
rawRequest.mockResolvedValue({
data: {
issues: {
nodes: Array.from({ length: 100 }, (_unused, index) => ({
id: `issue-${index + 1}`,
identifier: `ENG-${index + 1}`,
title: 'Fix auth',
url: `https://linear.app/acme/issue/ENG-${index + 1}`,
labels: { nodes: [] },
createdAt: '2026-07-01T00:00:00.000Z',
updatedAt: '2026-07-20T00:00:00.000Z'
})),
pageInfo: { hasNextPage: true, endCursor: 'next-page' }
}
}
})
const { listMcpIssues } = await import('./mcp-issue-list')
const result = await listMcpIssues({
team: 'ENG',
label: 'Bug',
state: 'started',
assignee: 'me',
project: 'Launch',
priority: 2,
query: 'auth',
updatedAt: '-P7D',
cursor: 'current-page',
orderBy: 'createdAt',
includeArchived: true,
limit: 100,
workspaceId: 'workspace-1'
})
expect(rawRequest).toHaveBeenCalledWith(
expect.stringContaining('query OrcaLinearListIssues'),
expect.objectContaining({
first: 100,
after: 'current-page',
orderBy: 'createdAt',
includeArchived: true,
filter: expect.objectContaining({
searchableContent: { contains: 'auth' },
priority: { eq: 2 },
updatedAt: { gte: '-P7D' },
assignee: { isMe: { eq: true } },
state: {
or: expect.arrayContaining([{ type: { eqIgnoreCase: 'started' } }])
},
project: {
or: expect.arrayContaining([{ slugId: { eqIgnoreCase: 'Launch' } }])
}
})
})
)
expect(rawRequest.mock.calls[0]?.[1]).not.toMatchObject({
filter: { team: { or: expect.arrayContaining([{ id: { eq: 'ENG' } }]) } }
})
expect(result.truncated).toBe(true)
expect(result.meta).toMatchObject({
limit: 100,
returned: 100,
hasMore: true,
orderBy: 'createdAt'
})
// Stops at the requested cap instead of walking on.
expect(rawRequest).toHaveBeenCalledTimes(1)
expect(result.meta.nextCursor).toMatch(/^orca\.linear\.v1\./)
expect(result.issues[0]).toMatchObject({
identifier: 'ENG-1',
workspace: { id: 'workspace-1', name: 'Acme' }
})
})
it('uses null filters and pages at the Linear per-request maximum', async () => {
rawRequest.mockResolvedValue({
data: { issues: { nodes: [], pageInfo: { hasNextPage: false } } }
})
const { listMcpIssues } = await import('./mcp-issue-list')
const result = await listMcpIssues({ assignee: 'null', parentId: 'null', limit: 999 })
expect(rawRequest.mock.calls[0]?.[1]).toMatchObject({
first: 250,
filter: { assignee: { null: true }, parent: { null: true } }
})
expect(result.meta.workspaceId).toBe('workspace-1')
})
it('uses UUID comparators only for values Linear accepts as IDs', async () => {
rawRequest.mockResolvedValue({
data: { issues: { nodes: [], pageInfo: { hasNextPage: false } } }
})
const { listMcpIssues } = await import('./mcp-issue-list')
const teamId = 'edece093-2649-4b21-9bf6-ff9192adf4f7'
const assigneeId = '256097b4-4dc3-4722-b5c3-888faa672554'
const parentId = 'c1097b4f-fa53-49da-ab5d-a38c596fbb5f'
await listMcpIssues({ team: teamId, assignee: assigneeId, parentId })
expect(rawRequest.mock.calls[0]?.[1]).toMatchObject({
filter: {
team: { or: expect.arrayContaining([{ id: { eq: teamId } }]) },
assignee: { or: expect.arrayContaining([{ id: { eq: assigneeId } }]) },
parent: { id: { eq: parentId } }
}
})
})
it('fans out one bounded provider request per workspace concurrently', async () => {
const firstRequest = vi.fn()
const secondRequest = vi.fn()
let resolveFirst: ((value: unknown) => void) | undefined
let resolveSecond: ((value: unknown) => void) | undefined
firstRequest.mockImplementation(
() => new Promise((resolve) => (resolveFirst = resolve as (value: unknown) => void))
)
secondRequest.mockImplementation(
() => new Promise((resolve) => (resolveSecond = resolve as (value: unknown) => void))
)
const firstEntry = clientEntry('workspace-1', 'Acme', firstRequest)
const secondEntry = clientEntry('workspace-2', 'Beta', secondRequest)
getStatus.mockReturnValue({ workspaces: [firstEntry.workspace, secondEntry.workspace] })
getClients.mockImplementation((workspaceId) => {
if (workspaceId === 'workspace-1') {
return [firstEntry]
}
if (workspaceId === 'workspace-2') {
return [secondEntry]
}
return []
})
const { listMcpIssues } = await import('./mcp-issue-list')
const pending = listMcpIssues({ limit: 1, workspaceId: 'all' })
await vi.waitFor(() => {
expect(firstRequest).toHaveBeenCalledTimes(1)
expect(secondRequest).toHaveBeenCalledTimes(1)
})
resolveFirst?.({
data: {
issues: {
nodes: [issueNode('issue-1', 'ENG-1', '2026-07-01T00:00:00.000Z')],
pageInfo: { hasNextPage: false, endCursor: 'first-cursor' }
}
}
})
resolveSecond?.({
data: {
issues: {
nodes: [issueNode('issue-2', 'OPS-1', '2026-07-02T00:00:00.000Z')],
pageInfo: { hasNextPage: false, endCursor: 'second-cursor' }
}
}
})
const result = await pending
expect(result.issues.map((issue) => issue.identifier)).toEqual(['OPS-1'])
expect(result.meta).toMatchObject({ returned: 1, hasMore: true, partial: false })
expect(result.meta.nextCursor).toBeUndefined()
expect(firstRequest.mock.calls[0]?.[1]).toMatchObject({ first: 1 })
expect(secondRequest.mock.calls[0]?.[1]).toMatchObject({ first: 1 })
})
it('returns healthy workspace results with a classified partial failure', async () => {
const healthyRequest = vi.fn().mockResolvedValue({
data: {
issues: {
nodes: [issueNode('issue-1', 'ENG-1', '2026-07-01T00:00:00.000Z')],
pageInfo: { hasNextPage: false }
}
}
})
const failedRequest = vi.fn().mockRejectedValue(new Error('429 rate limit exceeded'))
const healthy = clientEntry('workspace-1', 'Acme', healthyRequest)
const failed = clientEntry('workspace-2', 'Beta', failedRequest)
getStatus.mockReturnValue({ workspaces: [healthy.workspace, failed.workspace] })
getClients.mockImplementation((workspaceId) => {
if (workspaceId === 'workspace-1') {
return [healthy]
}
if (workspaceId === 'workspace-2') {
return [failed]
}
return []
})
const { listMcpIssues } = await import('./mcp-issue-list')
const result = await listMcpIssues({ workspaceId: 'all' })
expect(result.issues.map((issue) => issue.identifier)).toEqual(['ENG-1'])
expect(result.meta).toMatchObject({ partial: true, returned: 1 })
expect(result.meta.workspaceErrors).toEqual([
{
workspace: { id: 'workspace-2', name: 'Beta' },
code: 'linear_rate_limited',
message: '429 rate limit exceeded'
}
])
})
it('rejects workspace-specific cursors for all-workspace reads with a useful code', async () => {
const { listMcpIssues } = await import('./mcp-issue-list')
await expect(listMcpIssues({ cursor: 'next', workspaceId: 'all' })).rejects.toMatchObject({
code: 'linear_invalid_workspace'
})
expect(rawRequest).not.toHaveBeenCalled()
})
it('requires a workspace for cursor replay and does not emit fanout cursors', async () => {
rawRequest.mockResolvedValue({
data: {
issues: {
nodes: [issueNode('issue-1', 'ENG-1', '2026-07-01T00:00:00.000Z')],
pageInfo: { hasNextPage: true, endCursor: 'workspace-cursor' }
}
}
})
const { listMcpIssues } = await import('./mcp-issue-list')
await expect(listMcpIssues({ cursor: 'next' })).rejects.toMatchObject({
code: 'linear_invalid_workspace'
})
const result = await listMcpIssues({ workspaceId: 'all', limit: 1 })
expect(result.meta).toMatchObject({ hasMore: true, workspaceId: 'all' })
expect(result.meta.nextCursor).toBeUndefined()
expect(rawRequest).toHaveBeenCalledTimes(1)
})
it('rejects unknown explicit workspaces instead of returning an empty list', async () => {
getClients.mockReturnValue([])
const { listMcpIssues } = await import('./mcp-issue-list')
await expect(listMcpIssues({ workspaceId: 'workspace-missing' })).rejects.toMatchObject({
code: 'linear_invalid_workspace'
})
expect(rawRequest).not.toHaveBeenCalled()
})
})
function issueNode(id: string, identifier: string, updatedAt: string) {
return {
id,
identifier,
title: `Issue ${identifier}`,
url: `https://linear.app/acme/issue/${identifier}`,
labels: { nodes: [] },
createdAt: updatedAt,
updatedAt
}
}