@@ -40,6 +40,7 @@ vi.mock('@/lib/webhooks/slack-stream-sessions', () => ({
4040} ) )
4141
4242import { ExecuteEventProjection } from '@/lib/mothership/request/lifecycle/execute-events'
43+ import type { SlackStreamChunk } from '@/lib/webhooks/slack-agent-api'
4344import { SlackExecutionStreamController } from '@/lib/webhooks/slack-execution-stream'
4445import type { SlackStreamResponseConfig } from '@/lib/webhooks/slack-stream-config'
4546import { type AgentStreamEvent , createAgentEventReadableStream } from '@/providers/stream-events'
@@ -598,6 +599,65 @@ describe('SlackExecutionStreamController', () => {
598599 controller . assertSucceeded ( )
599600 } )
600601
602+ it . each ( [ 'live' , 'settled' ] as const ) (
603+ 'delivers a long %s answer with each Slack append inside the request limit' ,
604+ async ( delivery ) => {
605+ const answer = `${ 'x' . repeat ( 11_999 ) } 🚀${ 'y' . repeat ( 17_000 ) } The complete ending.`
606+ mockAppendSlackAgentStream . mockImplementation (
607+ async ( _token , _channel , _ts , chunks : SlackStreamChunk [ ] ) => {
608+ const text = chunks
609+ . filter ( ( chunk ) => chunk . type === 'markdown_text' )
610+ . map ( ( chunk ) => chunk . text )
611+ . join ( '' )
612+ if ( text . length > 12_000 ) throw new Error ( 'Slack chat.appendStream: msg_too_long' )
613+ expect ( text . isWellFormed ( ) ) . toBe ( true )
614+ }
615+ )
616+ const { controller } = await deliver (
617+ delivery === 'live'
618+ ? [
619+ { type : 'text_delta' , text : answer , turn : 'pending' } ,
620+ { type : 'turn_end' , turn : 'final' } ,
621+ ]
622+ : [ ] ,
623+ answer
624+ )
625+ controller . assertSucceeded ( )
626+ expect ( sentText ( ) ) . toBe ( answer )
627+ expect ( mockAppendSlackAgentStream ) . toHaveBeenCalledTimes ( 3 )
628+ expect ( mockStartSlackAgentStream ) . toHaveBeenCalledTimes ( 1 )
629+ expect ( mockStopSlackAgentStream ) . toHaveBeenCalledTimes ( 1 )
630+ }
631+ )
632+
633+ it ( 'does not replay acknowledged long-answer chunks after a later append fails' , async ( ) => {
634+ mockAppendSlackAgentStream . mockResolvedValueOnce ( undefined )
635+ mockAppendSlackAgentStream . mockRejectedValueOnce ( new Error ( 'append acknowledgment lost' ) )
636+ const answer = `${ 'x' . repeat ( 25_000 ) } The complete ending.`
637+ const { controller } = await deliver (
638+ [
639+ { type : 'text_delta' , text : answer , turn : 'pending' } ,
640+ { type : 'turn_end' , turn : 'final' } ,
641+ ] ,
642+ answer
643+ )
644+ expect ( ( ) => controller . assertSucceeded ( ) ) . toThrow ( 'append acknowledgment lost' )
645+ expect ( mockAppendSlackAgentStream ) . toHaveBeenCalledTimes ( 2 )
646+ expect ( mockAppendSlackAgentStream . mock . calls . map ( ( call ) => call [ 3 ] ) ) . toEqual ( [
647+ [ { type : 'markdown_text' , text : answer . slice ( 0 , 12_000 ) } ] ,
648+ [ { type : 'markdown_text' , text : answer . slice ( 12_000 , 24_000 ) } ] ,
649+ ] )
650+ expect ( mockStopSlackAgentStream ) . toHaveBeenCalledWith (
651+ expect . anything ( ) ,
652+ expect . anything ( ) ,
653+ expect . anything ( ) ,
654+ 'suspended' ,
655+ undefined ,
656+ undefined ,
657+ [ expect . objectContaining ( { status : 'error' } ) ]
658+ )
659+ } )
660+
601661 it ( 'reconciles a settled suffix without duplicating acknowledged text' , async ( ) => {
602662 const { controller } = await deliver (
603663 [
0 commit comments