Files
orca/src/main/linear/issue-relation-write.test.ts
T
Neil 83117f2860 refactor(integrations): split issue-tracker clients under the max-lines budget (#14704)
The GitLab, GitHub, Jira and Linear integration modules, their two IPC
registrars, and the shared GitHub project types each carried a file-level
`eslint-disable max-lines` and ran 351-614 counted lines against a 300-line
budget. AGENTS.md calls for splitting rather than suppressing, and
config/max-lines-baseline.txt is a shrink-only ratchet, so this removes all
eight suppressions and prunes their entries (341 -> 333).

Pure move, no behavior change. Each client is cut along the seam it already
had: per-operation modules for the issue APIs (create / update / comment /
field options), and for Jira the request queue, site credential store,
authenticated request, and site identity. The two IPC registrars keep their own
handlers and delegate the rest to per-domain sub-registrars, so they remain
real entry points rather than re-export shims.

The IPC surface is proved intact rather than assumed: comparing (method,
channel) multisets between HEAD and the split gives 52 registrations across 52
distinct channels on both sides.

Provider-neutrality is preserved -- GitLab and GitHub keep separate, parallel
module layouts rather than being merged behind a shared abstraction.

Verified: oxlint clean, ratchet passes, typecheck clean, full unit suite green
(the one remaining failure is a pre-existing load flake in an untouched file,
green when re-run serially), no new runtime import cycles among 744 modules,
and no lint suppression added anywhere.
2026-08-15 18:17:20 -07:00

228 lines
6.6 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { LinearClientForWorkspace } from './client'
const rawRequest = vi.fn()
const getClients = vi.fn()
vi.mock('./linear-request-concurrency', () => ({
acquire: vi.fn().mockResolvedValue(undefined),
release: vi.fn()
}))
vi.mock('./linear-token-store', () => ({
clearToken: vi.fn()
}))
vi.mock('./client', () => ({
getClients: (...args: unknown[]) => getClients(...args),
isAuthError: vi.fn().mockReturnValue(false)
}))
function entry(): LinearClientForWorkspace {
return {
workspace: {
id: 'workspace-1',
organizationId: 'workspace-1',
organizationName: 'Acme',
displayName: 'Ada',
email: 'ada@example.com'
},
client: { client: { rawRequest } }
} as unknown as LinearClientForWorkspace
}
const issue = {
id: 'issue-1',
identifier: 'ENG-1',
title: 'Current',
url: 'https://linear.app/acme/issue/ENG-1'
}
const relatedIssue = {
id: 'issue-2',
identifier: 'ENG-2',
title: 'Related',
url: 'https://linear.app/acme/issue/ENG-2'
}
function connection(field: 'relations' | 'inverseRelations', nodes: unknown[]) {
return {
data: { issue: { [field]: { nodes, pageInfo: { hasNextPage: false } } } }
}
}
describe('Linear issue relation writes', () => {
beforeEach(() => {
vi.clearAllMocks()
getClients.mockReturnValue([entry()])
})
it('swaps relation endpoints when adding blocked-by', async () => {
rawRequest.mockResolvedValueOnce(connection('inverseRelations', [])).mockResolvedValueOnce({
data: {
issueRelationCreate: {
success: true,
issueRelation: {
id: 'relation-1',
type: 'blocks',
issue: relatedIssue,
relatedIssue: issue
}
}
}
})
const { writeIssueRelation } = await import('./issue-relation-write')
const result = await writeIssueRelation({
issue,
relatedIssue,
relationship: 'blockedBy',
operation: 'add',
workspaceId: 'workspace-1'
})
expect(rawRequest.mock.calls[1]?.[1]).toEqual({
input: { issueId: 'issue-2', relatedIssueId: 'issue-1', type: 'blocks' }
})
expect(rawRequest).toHaveBeenCalledTimes(2)
expect(result).toMatchObject({
operation: 'add',
relation: { direction: 'inbound', relationship: 'blockedBy' },
meta: { alreadySet: false }
})
})
it('does not create an equivalent relation twice', async () => {
rawRequest.mockResolvedValueOnce(
connection('relations', [{ id: 'relation-1', type: 'related', relatedIssue }])
)
const { writeIssueRelation } = await import('./issue-relation-write')
const result = await writeIssueRelation({
issue,
relatedIssue,
relationship: 'relatedTo',
operation: 'add',
workspaceId: 'workspace-1'
})
expect(rawRequest).toHaveBeenCalledTimes(1)
expect(result.meta.alreadySet).toBe(true)
})
it('finds and removes inverse relations', async () => {
rawRequest
.mockResolvedValueOnce(
connection('inverseRelations', [{ id: 'relation-1', type: 'blocks', issue: relatedIssue }])
)
.mockResolvedValueOnce({ data: { issueRelationDelete: { success: true } } })
const { writeIssueRelation } = await import('./issue-relation-write')
const result = await writeIssueRelation({
issue,
relatedIssue,
relationship: 'blockedBy',
operation: 'remove',
workspaceId: 'workspace-1'
})
expect(rawRequest.mock.calls[1]?.[1]).toEqual({ id: 'relation-1' })
expect(rawRequest).toHaveBeenCalledTimes(2)
expect(result).toMatchObject({
operation: 'remove',
relation: { direction: 'inbound', relationship: 'blockedBy' },
meta: { alreadySet: false }
})
})
it('checks both directions before adding a symmetric related relation', async () => {
rawRequest
.mockResolvedValueOnce(connection('relations', []))
.mockResolvedValueOnce(
connection('inverseRelations', [{ id: 'relation-1', type: 'related', issue: relatedIssue }])
)
const { writeIssueRelation } = await import('./issue-relation-write')
const result = await writeIssueRelation({
issue,
relatedIssue,
relationship: 'relatedTo',
operation: 'add',
workspaceId: 'workspace-1'
})
expect(rawRequest).toHaveBeenCalledTimes(2)
expect(result.meta.alreadySet).toBe(true)
expect(result.relation).toMatchObject({ direction: 'inbound', relationship: 'relatedTo' })
})
it('refuses a write when the idempotency scan reaches its cap', async () => {
for (let page = 0; page < 5; page += 1) {
rawRequest.mockResolvedValueOnce({
data: {
issue: {
relations: {
nodes: Array.from({ length: 50 }, (_, index) => ({
id: `relation-${page}-${index}`,
type: 'blocks',
relatedIssue: { ...relatedIssue, id: `other-${page}-${index}` }
})),
pageInfo: { hasNextPage: true, endCursor: `cursor-${page}` }
}
}
}
})
}
const { writeIssueRelation } = await import('./issue-relation-write')
await expect(
writeIssueRelation({
issue,
relatedIssue,
relationship: 'blocks',
operation: 'add',
workspaceId: 'workspace-1'
})
).rejects.toMatchObject({ code: 'linear_write_failed' })
expect(rawRequest).toHaveBeenCalledTimes(5)
})
it('marks an ambiguous create transport failure as unconfirmed', async () => {
rawRequest
.mockResolvedValueOnce(connection('relations', []))
.mockRejectedValueOnce(new Error('fetch failed: socket hang up'))
const { writeIssueRelation } = await import('./issue-relation-write')
await expect(
writeIssueRelation({
issue,
relatedIssue,
relationship: 'blocks',
operation: 'add',
workspaceId: 'workspace-1'
})
).rejects.toMatchObject({ kind: 'unconfirmed' })
expect(rawRequest).toHaveBeenCalledTimes(2)
})
it('keeps safe pre-connect mutation failures retryable as network errors', async () => {
rawRequest
.mockResolvedValueOnce(connection('relations', []))
.mockRejectedValueOnce(new Error('connect ENOTFOUND api.linear.app'))
const { writeIssueRelation } = await import('./issue-relation-write')
await expect(
writeIssueRelation({
issue,
relatedIssue,
relationship: 'blocks',
operation: 'add',
workspaceId: 'workspace-1'
})
).rejects.toMatchObject({ kind: 'network' })
expect(rawRequest).toHaveBeenCalledTimes(2)
})
})