mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
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.
228 lines
6.6 KiB
TypeScript
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)
|
|
})
|
|
})
|