Skip to content

Commit a8e7d22

Browse files
committed
fix(search): preserve partial Confluence lookup failures
1 parent 6295a80 commit a8e7d22

3 files changed

Lines changed: 90 additions & 7 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/home/components/search-sources/atlassian-source-setup-modal.tsx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ export function AtlassianSourceSetupModal({
3636
onClose,
3737
onConnected,
3838
}: AtlassianSourceSetupModalProps) {
39-
const [selectedAccount, setSelectedAccount] = useState<string>()
4039
const account = usePersonalSourceAccount({
4140
organizationId,
4241
connectorType,
@@ -50,6 +49,7 @@ export function AtlassianSourceSetupModal({
5049
connectorConfig: connector.meta,
5150
accessMode: 'members',
5251
})
52+
const [selectedAccount, setSelectedAccount] = useState<string>()
5353
const accounts = account.accounts.data?.accounts ?? []
5454
const requestedAccount =
5555
selectedAccount ??

‎apps/sim/lib/selectors/server/providers/confluence.test.ts‎

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,87 @@ describe('Confluence server selector adapters', () => {
354354
).rejects.toMatchObject({ name: 'SelectorConnectionUnavailableError', status: 403 })
355355
})
356356

357+
it.each([
358+
{ failedStatus: 'current', status: 401 },
359+
{ failedStatus: 'current', status: 403 },
360+
{ failedStatus: 'current', status: 429 },
361+
{ failedStatus: 'archived', status: 401 },
362+
{ failedStatus: 'archived', status: 403 },
363+
{ failedStatus: 'archived', status: 429 },
364+
])(
365+
'preserves $status from the $failedStatus lookup when the other status has no matching space',
366+
async ({ failedStatus, status }) => {
367+
mockFetch.mockImplementation((input: URL) =>
368+
new URL(input).searchParams.get('status') === failedStatus
369+
? new Response(null, { status })
370+
: Response.json({ results: [{ id: '99999', key: 'OTHER', name: 'Other space' }] })
371+
)
372+
373+
await expect(
374+
confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
375+
).rejects.toMatchObject({
376+
name:
377+
status === 429 ? 'SelectorOptionsUnavailableError' : 'SelectorConnectionUnavailableError',
378+
status,
379+
})
380+
expect(mockFetch).toHaveBeenCalledTimes(2)
381+
}
382+
)
383+
384+
it.each(['current', 'archived'] as const)(
385+
'returns an exact %s match even when the other status lookup fails',
386+
async (matchingStatus) => {
387+
mockFetch.mockImplementation((input: URL) =>
388+
new URL(input).searchParams.get('status') === matchingStatus
389+
? Response.json({ results: [{ id: '12345', key: 'ENG', name: 'Engineering' }] })
390+
: new Response(null, { status: 429 })
391+
)
392+
393+
await expect(
394+
confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
395+
).resolves.toEqual({
396+
kind: 'detail',
397+
item: {
398+
id: 'ENG',
399+
label:
400+
matchingStatus === 'archived' ? 'Engineering (ENG) — archived' : 'Engineering (ENG)',
401+
},
402+
})
403+
}
404+
)
405+
406+
it('reports a space key missing only when both status lookups succeed without a match', async () => {
407+
mockFetch
408+
.mockResolvedValueOnce(
409+
Response.json({ results: [{ id: '99999', key: 'OTHER', name: 'Other space' }] })
410+
)
411+
.mockResolvedValueOnce(Response.json({ results: [] }))
412+
413+
await expect(
414+
confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
415+
).resolves.toEqual({ kind: 'detail', item: null })
416+
expect(mockFetch).toHaveBeenCalledTimes(2)
417+
})
418+
419+
it('sanitizes an arbitrary partial lookup failure rather than reporting the key missing', async () => {
420+
const fetchProviderJson = vi
421+
.spyOn(providerHttp, 'fetchProviderJson')
422+
.mockRejectedValueOnce(new Error('raw provider failure'))
423+
.mockResolvedValueOnce({ results: [] })
424+
425+
try {
426+
await expect(
427+
confluenceSelectorAttachments['confluence.spaces'].execute(spaceDetailArgs())
428+
).rejects.toMatchObject({
429+
name: 'SelectorOptionsUnavailableError',
430+
message: 'Options unavailable',
431+
status: 502,
432+
})
433+
} finally {
434+
fetchProviderJson.mockRestore()
435+
}
436+
})
437+
357438
it('preserves the first safe provider failure when both space detail requests fail', async () => {
358439
mockFetch
359440
.mockResolvedValueOnce(new Response(null, { status: 401 }))

‎apps/sim/lib/selectors/server/providers/confluence.ts‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -162,12 +162,6 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key'
162162
requestSpaces({ ...auth, params: paramsFor('archived'), signal: args.signal }),
163163
])
164164
args.signal?.throwIfAborted()
165-
if (current.status === 'rejected' && archived.status === 'rejected') {
166-
for (const result of [current, archived]) {
167-
if (isPublicSelectorError(result.reason)) throw result.reason
168-
}
169-
throw new SelectorOptionsUnavailableError()
170-
}
171165
const spaces = [
172166
...(current.status === 'fulfilled'
173167
? (current.value.results ?? []).map((space) => ({ space, status: 'current' as const }))
@@ -177,6 +171,14 @@ async function executeSpaces(args: ExecuteServerSelectorArgs, identifier: 'key'
177171
: []),
178172
]
179173
const match = spaces.find(({ space }) => space.key === key)
174+
if (!match && (current.status === 'rejected' || archived.status === 'rejected')) {
175+
for (const result of [current, archived]) {
176+
if (result.status === 'rejected' && isPublicSelectorError(result.reason)) {
177+
throw result.reason
178+
}
179+
}
180+
throw new SelectorOptionsUnavailableError()
181+
}
180182
return detailSelectorResult(
181183
match
182184
? {

0 commit comments

Comments
 (0)