@@ -17,6 +17,16 @@ import { createSlackAdapter, SlackAdapter } from "./index";
1717
1818const FILE_ID_PATTERN = / ^ f i l e - / ;
1919
20+ function createStreamExpiredError ( ) : Error & {
21+ code : string ;
22+ data : { error : string ; ok : false } ;
23+ } {
24+ return Object . assign ( new Error ( "message_not_in_streaming_state" ) , {
25+ code : "slack_webapi_platform_error" ,
26+ data : { error : "message_not_in_streaming_state" , ok : false as const } ,
27+ } ) ;
28+ }
29+
2030// Mock @slack /socket-mode
2131const mockSocketStart = vi . fn ( ) . mockResolvedValue ( { } ) ;
2232const mockSocketDisconnect = vi . fn ( ) . mockResolvedValue ( undefined ) ;
@@ -7977,6 +7987,7 @@ describe("stream with empty threadTs", () => {
79777987 await adapter . stream ( "slack:D123:1234567890.000000" , stream ( ) ) ;
79787988
79797989 expect ( chatStream ) . toHaveBeenCalledWith ( {
7990+ buffer_size : 1 ,
79807991 channel : "D123" ,
79817992 thread_ts : "1234567890.000000" ,
79827993 } ) ;
@@ -8061,6 +8072,196 @@ describe("stream with empty threadTs", () => {
80618072 expect . objectContaining ( { token : "xoxb-test-token" } )
80628073 ) ;
80638074 } ) ;
8075+
8076+ it ( "rolls to a fresh stream and retries only the pending markdown delta" , async ( ) => {
8077+ const adapter = createSlackAdapter ( {
8078+ botToken : "xoxb-test-token" ,
8079+ signingSecret : "test-signing-secret" ,
8080+ logger : mockLogger ,
8081+ } ) ;
8082+ const firstAppend = vi
8083+ . fn ( )
8084+ . mockResolvedValueOnce ( { ok : true } )
8085+ . mockRejectedValueOnce ( createStreamExpiredError ( ) ) ;
8086+ const firstStop = vi . fn ( ) ;
8087+ const secondAppend = vi . fn ( ) . mockResolvedValue ( { ok : true } ) ;
8088+ const secondStop = vi . fn ( ) . mockResolvedValue ( {
8089+ ok : true ,
8090+ ts : "1234567890.222222" ,
8091+ } ) ;
8092+ const chatStream = vi
8093+ . fn ( )
8094+ . mockReturnValueOnce ( {
8095+ append : firstAppend ,
8096+ stop : firstStop ,
8097+ ts : "1234567890.111111" ,
8098+ } )
8099+ . mockReturnValueOnce ( {
8100+ append : secondAppend ,
8101+ stop : secondStop ,
8102+ ts : "1234567890.222222" ,
8103+ } ) ;
8104+ mockClientMethod ( adapter , "chatStream" , chatStream ) ;
8105+
8106+ async function * textStream ( ) {
8107+ yield "first\n" ;
8108+ yield "second\n" ;
8109+ }
8110+
8111+ const result = await adapter . stream (
8112+ "slack:C123:1234567890.000000" ,
8113+ textStream ( ) ,
8114+ {
8115+ recipientUserId : "U123" ,
8116+ recipientTeamId : "T123" ,
8117+ }
8118+ ) ;
8119+
8120+ expect ( chatStream ) . toHaveBeenCalledTimes ( 2 ) ;
8121+ expect ( firstAppend ) . toHaveBeenNthCalledWith (
8122+ 1 ,
8123+ expect . objectContaining ( { markdown_text : "first\n" } )
8124+ ) ;
8125+ expect ( firstAppend ) . toHaveBeenNthCalledWith (
8126+ 2 ,
8127+ expect . objectContaining ( { markdown_text : "second\n" } )
8128+ ) ;
8129+ expect ( secondAppend ) . toHaveBeenCalledTimes ( 1 ) ;
8130+ expect ( secondAppend ) . toHaveBeenCalledWith (
8131+ expect . objectContaining ( { markdown_text : "second\n" } )
8132+ ) ;
8133+ expect ( firstStop ) . not . toHaveBeenCalled ( ) ;
8134+ expect ( secondStop ) . toHaveBeenCalledTimes ( 1 ) ;
8135+ expect ( result ?. id ) . toBe ( "1234567890.222222" ) ;
8136+ } ) ;
8137+
8138+ it ( "re-seeds the latest structured chunks when rolling streams" , async ( ) => {
8139+ const adapter = createSlackAdapter ( {
8140+ botToken : "xoxb-test-token" ,
8141+ signingSecret : "test-signing-secret" ,
8142+ logger : mockLogger ,
8143+ } ) ;
8144+ const taskChunk = {
8145+ details : "working" ,
8146+ id : "task-1" ,
8147+ status : "in_progress" as const ,
8148+ title : "Task one" ,
8149+ type : "task_update" as const ,
8150+ } ;
8151+ const firstAppend = vi
8152+ . fn ( )
8153+ . mockResolvedValueOnce ( { ok : true } )
8154+ . mockResolvedValueOnce ( { ok : true } )
8155+ . mockRejectedValueOnce ( createStreamExpiredError ( ) ) ;
8156+ const secondAppend = vi . fn ( ) . mockResolvedValue ( { ok : true } ) ;
8157+ const secondStop = vi . fn ( ) . mockResolvedValue ( {
8158+ ok : true ,
8159+ ts : "1234567890.222222" ,
8160+ } ) ;
8161+ mockClientMethod (
8162+ adapter ,
8163+ "chatStream" ,
8164+ vi
8165+ . fn ( )
8166+ . mockReturnValueOnce ( {
8167+ append : firstAppend ,
8168+ stop : vi . fn ( ) ,
8169+ ts : "1234567890.111111" ,
8170+ } )
8171+ . mockReturnValueOnce ( {
8172+ append : secondAppend ,
8173+ stop : secondStop ,
8174+ ts : "1234567890.222222" ,
8175+ } )
8176+ ) ;
8177+
8178+ async function * chunkStream ( ) {
8179+ yield taskChunk ;
8180+ yield "hello\n" ;
8181+ yield "world\n" ;
8182+ }
8183+
8184+ await adapter . stream ( "slack:C123:1234567890.000000" , chunkStream ( ) , {
8185+ recipientUserId : "U123" ,
8186+ recipientTeamId : "T123" ,
8187+ } ) ;
8188+
8189+ expect ( secondAppend ) . toHaveBeenNthCalledWith (
8190+ 1 ,
8191+ expect . objectContaining ( { chunks : [ taskChunk ] } )
8192+ ) ;
8193+ expect ( secondAppend ) . toHaveBeenNthCalledWith (
8194+ 2 ,
8195+ expect . objectContaining ( { markdown_text : "world\n" } )
8196+ ) ;
8197+ } ) ;
8198+
8199+ it ( "does not reject when Slack expires the stream before stop" , async ( ) => {
8200+ const adapter = createSlackAdapter ( {
8201+ botToken : "xoxb-test-token" ,
8202+ signingSecret : "test-signing-secret" ,
8203+ logger : mockLogger ,
8204+ } ) ;
8205+ const append = vi . fn ( ) . mockResolvedValue ( { ok : true } ) ;
8206+ const stop = vi . fn ( ) . mockRejectedValue ( createStreamExpiredError ( ) ) ;
8207+ const update = vi . fn ( ) ;
8208+ mockClientMethod (
8209+ adapter ,
8210+ "chatStream" ,
8211+ vi . fn ( ) . mockReturnValue ( {
8212+ append,
8213+ stop,
8214+ ts : "1234567890.111111" ,
8215+ } )
8216+ ) ;
8217+ mockClientMethod ( adapter , "chat.update" , update ) ;
8218+
8219+ async function * textStream ( ) {
8220+ yield "hello\n" ;
8221+ }
8222+
8223+ const result = await adapter . stream (
8224+ "slack:C123:1234567890.000000" ,
8225+ textStream ( ) ,
8226+ {
8227+ recipientUserId : "U123" ,
8228+ recipientTeamId : "T123" ,
8229+ }
8230+ ) ;
8231+
8232+ expect ( stop ) . toHaveBeenCalledTimes ( 1 ) ;
8233+ expect ( update ) . not . toHaveBeenCalled ( ) ;
8234+ expect ( result ?. id ) . toBe ( "1234567890.111111" ) ;
8235+ } ) ;
8236+
8237+ it ( "still rejects non-expiry stream append errors" , async ( ) => {
8238+ const adapter = createSlackAdapter ( {
8239+ botToken : "xoxb-test-token" ,
8240+ signingSecret : "test-signing-secret" ,
8241+ logger : mockLogger ,
8242+ } ) ;
8243+ const append = vi . fn ( ) . mockRejectedValue ( new Error ( "boom" ) ) ;
8244+ mockClientMethod (
8245+ adapter ,
8246+ "chatStream" ,
8247+ vi . fn ( ) . mockReturnValue ( {
8248+ append,
8249+ stop : vi . fn ( ) ,
8250+ ts : "1234567890.111111" ,
8251+ } )
8252+ ) ;
8253+
8254+ async function * textStream ( ) {
8255+ yield "hello\n" ;
8256+ }
8257+
8258+ await expect (
8259+ adapter . stream ( "slack:C123:1234567890.000000" , textStream ( ) , {
8260+ recipientUserId : "U123" ,
8261+ recipientTeamId : "T123" ,
8262+ } )
8263+ ) . rejects . toThrow ( "boom" ) ;
8264+ } ) ;
80648265} ) ;
80658266
80668267describe ( "scheduleMessage with empty threadTs" , ( ) => {
0 commit comments