refactor: deduplicate Linear team readers (#13417)

This commit is contained in:
Neil 2026-08-09 18:54:22 -07:00 committed by GitHub
parent 8a38e2f3f8
commit 934faaec76
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 51 additions and 97 deletions

View File

@ -5,10 +5,12 @@ import { credentialDecryptionMessage } from '../../shared/integration-credential
const getClients = vi.fn()
const clearToken = vi.fn()
const isAuthError = vi.fn()
const acquire = vi.fn().mockResolvedValue(undefined)
const release = vi.fn()
vi.mock('./client', () => ({
acquire: vi.fn().mockResolvedValue(undefined),
release: vi.fn(),
acquire,
release,
getClients: (...args: unknown[]) => getClients(...args),
isAuthError: (...args: unknown[]) => isAuthError(...args),
clearToken: (...args: unknown[]) => clearToken(...args)
@ -283,6 +285,30 @@ describe('Linear teams', () => {
expect(members).toHaveBeenCalledWith({ first: 100 })
})
it.each([
['getTeamStates', 'getTeamStatesOrThrow'],
['getTeamLabels', 'getTeamLabelsOrThrow'],
['getTeamMembers', 'getTeamMembersOrThrow']
] as const)('preserves the %s error policy', async (fallbackReader, throwingReader) => {
const error = new Error('request failed')
const entry = makeTeamLookupEntry('workspace-1', 'Workspace', null)
vi.mocked(entry.client.team).mockRejectedValue(error)
getClients.mockReturnValue([entry])
const readers = await import('./teams')
const warn = vi.spyOn(console, 'warn').mockImplementation(() => undefined)
await expect(readers[fallbackReader]('team-1', 'workspace-1')).resolves.toEqual([])
await expect(readers[throwingReader]('team-1', 'workspace-1')).rejects.toBe(error)
expect(warn).toHaveBeenCalledWith(`[linear] ${fallbackReader} failed:`, error)
expect(release).toHaveBeenCalledTimes(2)
isAuthError.mockReturnValue(true)
await expect(readers[fallbackReader]('team-1', 'workspace-1')).rejects.toBe(error)
expect(clearToken).toHaveBeenCalledWith('workspace-1')
expect(release).toHaveBeenCalledTimes(3)
warn.mockRestore()
})
it('surfaces Linear credential decrypt errors on active team reads', async () => {
const error = new Error(credentialDecryptionMessage('Linear'))
getClients.mockImplementation(() => {

View File

@ -1,3 +1,4 @@
import type { Team } from '@linear/sdk'
import type {
LinearTeam,
LinearWorkflowState,
@ -108,10 +109,12 @@ export async function listTeamsForAgent(
}
}
export async function getTeamStates(
async function readTeamResource<T>(
teamId: string,
workspaceId?: string | null
): Promise<LinearWorkflowState[]> {
workspaceId: string | null | undefined,
fetchAll: (team: Team) => Promise<T[]>,
fallbackWarning?: string
): Promise<T[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
@ -119,137 +122,62 @@ export async function getTeamStates(
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamStates(team)
return await fetchAll(await entry.client.team(teamId))
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
throw error
}
console.warn('[linear] getTeamStates failed:', error)
if (!fallbackWarning) {
throw error
}
console.warn(`[linear] ${fallbackWarning} failed:`, error)
return []
} finally {
release()
}
}
export async function getTeamStates(
teamId: string,
workspaceId?: string | null
): Promise<LinearWorkflowState[]> {
return readTeamResource(teamId, workspaceId, fetchAllTeamStates, 'getTeamStates')
}
export async function getTeamStatesOrThrow(
teamId: string,
workspaceId?: string | null
): Promise<LinearWorkflowState[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamStates(team)
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
}
throw error
} finally {
release()
}
return readTeamResource(teamId, workspaceId, fetchAllTeamStates)
}
export async function getTeamLabels(
teamId: string,
workspaceId?: string | null
): Promise<LinearLabel[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamLabels(team)
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
throw error
}
console.warn('[linear] getTeamLabels failed:', error)
return []
} finally {
release()
}
return readTeamResource(teamId, workspaceId, fetchAllTeamLabels, 'getTeamLabels')
}
export async function getTeamLabelsOrThrow(
teamId: string,
workspaceId?: string | null
): Promise<LinearLabel[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamLabels(team)
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
}
throw error
} finally {
release()
}
return readTeamResource(teamId, workspaceId, fetchAllTeamLabels)
}
export async function getTeamMembers(
teamId: string,
workspaceId?: string | null
): Promise<LinearMember[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamMembers(team)
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
throw error
}
console.warn('[linear] getTeamMembers failed:', error)
return []
} finally {
release()
}
return readTeamResource(teamId, workspaceId, fetchAllTeamMembers, 'getTeamMembers')
}
export async function getTeamMembersOrThrow(
teamId: string,
workspaceId?: string | null
): Promise<LinearMember[]> {
const entry = getClients(workspaceId)[0]
if (!entry) {
return []
}
await acquire()
try {
const team = await entry.client.team(teamId)
return await fetchAllTeamMembers(team)
} catch (error) {
if (isAuthError(error)) {
clearToken(entry.workspace.id)
}
throw error
} finally {
release()
}
return readTeamResource(teamId, workspaceId, fetchAllTeamMembers)
}
export async function getViewerForWorkspaceOrThrow(