Skip to content

Commit 8fbfd56

Browse files
fix(slack-search): acknowledge missing sources in the thread
1 parent ebce133 commit 8fbfd56

2 files changed

Lines changed: 89 additions & 4 deletions

File tree

apps/sim/lib/knowledge/application/slack-search/onboarding.test.ts

Lines changed: 70 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ import {
7272
slackSearchConversation,
7373
slackSearchConversationKey,
7474
} from '@/lib/slack-search/conversation'
75+
import type { SlackSearchMessage } from '@/lib/slack-search/types'
7576

7677
const principal = { kind: 'session', userId: 'user1', sessionId: 'session1' } as const
7778
const job = {
@@ -275,14 +276,18 @@ describe('Slack onboarding control delivery', () => {
275276
eventId: 'Ev1',
276277
receivedAt: new Date(),
277278
} as const
278-
const send = (reason: 'account' | 'sources' = 'account') =>
279+
const send = (
280+
reason: 'account' | 'sources' = 'account',
281+
message: SlackSearchMessage = job.message,
282+
signal = new AbortController().signal
283+
) =>
279284
sendSlackSearchOnboarding(slackPrincipal, {
280-
job,
285+
job: { ...job, message },
281286
turnId: 'turn1',
282287
leaseId: 'lease1',
283288
email: state.email,
284289
reason,
285-
signal: new AbortController().signal,
290+
signal,
286291
})
287292
it('posts a thread-scoped signup link without bot secrets or email in its URL', async () => {
288293
const result = await send()
@@ -324,9 +329,27 @@ describe('Slack onboarding control delivery', () => {
324329
},
325330
signal: expect.any(AbortSignal),
326331
})
327-
expect(m.post).not.toHaveBeenCalled()
328332
expect(m.outcome).toHaveBeenCalledWith(context.installation, 'sources_required')
329333
})
334+
it.each([job.message.threadTs, undefined])(
335+
'acknowledges missing sources in the question thread after private setup delivery: %s',
336+
async (threadTs) => {
337+
await send('sources', { ...job.message, threadTs })
338+
expect(m.post).toHaveBeenCalledExactlyOnceWith(
339+
'bot-secret',
340+
{
341+
channel: 'D1',
342+
thread_ts: threadTs ?? job.message.messageTs,
343+
text: 'I don’t have any sources I can search for you yet. Check the “Connect sources” message in our DM to get set up, then retry this question.',
344+
unfurl_links: false,
345+
unfurl_media: false,
346+
},
347+
expect.any(AbortSignal)
348+
)
349+
expect(m.api.mock.invocationCallOrder[1]).toBeLessThan(m.post.mock.invocationCallOrder[0])
350+
expect(m.post.mock.invocationCallOrder[0]).toBeLessThan(m.outcome.mock.invocationCallOrder[0])
351+
}
352+
)
330353
it.each(['rejected', 'ambiguous'] as const)(
331354
'does not replace a %s ephemeral delivery with a persistent message',
332355
async (outcome) => {
@@ -360,6 +383,49 @@ describe('Slack onboarding control delivery', () => {
360383
expect(m.lease).toHaveBeenCalledWith('turn1', 'lease1')
361384
}
362385
)
386+
it.each(['binding', 'lease', 'cancellation'] as const)(
387+
'stops before the thread notice if %s changes after the ephemeral prompt',
388+
async (change) => {
389+
const controller = new AbortController()
390+
m.api.mockResolvedValueOnce({
391+
status: 200,
392+
data: { ok: true, permalink: state.slackUrl },
393+
})
394+
m.api.mockImplementationOnce(async () => {
395+
if (change === 'binding') m.authorize.mockResolvedValueOnce(null)
396+
if (change === 'lease') m.lease.mockRejectedValueOnce(new Error('lease lost'))
397+
if (change === 'cancellation') controller.abort(new Error('cancelled'))
398+
return { status: 200, data: { ok: true } }
399+
})
400+
await expect(send('sources', job.message, controller.signal)).rejects.toThrow(
401+
change === 'binding' ? 'disabled' : change === 'lease' ? 'lease lost' : 'cancelled'
402+
)
403+
expect(m.api).toHaveBeenCalledTimes(2)
404+
expect(m.post).not.toHaveBeenCalled()
405+
expect(m.outcome).not.toHaveBeenCalled()
406+
}
407+
)
408+
it.each(['rejected', 'ambiguous'] as const)(
409+
'fails without replaying either message when the sources notice delivery is %s',
410+
async (outcome) => {
411+
if (outcome === 'rejected') {
412+
m.post.mockResolvedValueOnce({
413+
status: 200,
414+
data: { ok: false, error: 'channel_not_found' },
415+
})
416+
} else {
417+
m.post.mockRejectedValueOnce(new Error('connection lost after send'))
418+
}
419+
await expect(send('sources')).rejects.toThrow(
420+
outcome === 'rejected'
421+
? 'Could not deliver the Slack sources notice'
422+
: 'connection lost after send'
423+
)
424+
expect(m.api).toHaveBeenCalledTimes(2)
425+
expect(m.post).toHaveBeenCalledOnce()
426+
expect(m.outcome).not.toHaveBeenCalled()
427+
}
428+
)
363429
it('does not retry an ambiguous post or fall back to another transport', async () => {
364430
m.post.mockRejectedValueOnce(new Error('connection lost after send'))
365431
await expect(send()).rejects.toThrow('connection lost after send')

apps/sim/lib/knowledge/application/slack-search/onboarding.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,25 @@ export async function sendSlackSearchOnboarding(
155155
)
156156
if (response.status !== 200 || response.data.ok !== true)
157157
throw new Error('Could not deliver Slack onboarding')
158+
if (reason === 'sources') {
159+
await requireSlackSearchTurnLease(turnId, leaseId)
160+
if (!(await authorizeSlackSearchInstallation(principal, job)))
161+
throw new OrchestrationError('forbidden', 'Slack Search is disabled')
162+
signal.throwIfAborted()
163+
const reply = await postSlackMessage(
164+
context.secret.botToken,
165+
{
166+
channel: job.message.channelId,
167+
thread_ts: job.message.threadTs ?? job.message.messageTs,
168+
text: 'I don’t have any sources I can search for you yet. Check the “Connect sources” message in our DM to get set up, then retry this question.',
169+
unfurl_links: false,
170+
unfurl_media: false,
171+
},
172+
signal
173+
)
174+
if (reply.status !== 200 || reply.data.ok !== true)
175+
throw new Error('Could not deliver the Slack sources notice')
176+
}
158177
await recordSlackSearchOutcome(
159178
context.installation,
160179
reason === 'account' ? 'account_required' : 'sources_required'

0 commit comments

Comments
 (0)