@@ -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
7677const principal = { kind : 'session' , userId : 'user1' , sessionId : 'session1' } as const
7778const 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' )
0 commit comments