11import { describe , expect , test } from "bun:test"
2- import { coalesceServerEvents , resumeStreamAfterPageShow } from "./server-sdk"
2+ import { coalesceServerEvents , enqueueServerEvent , resumeStreamAfterPageShow } from "./server-sdk"
33import type { Event } from "@opencode-ai/sdk/v2/client"
44
55describe ( "resumeStreamAfterPageShow" , ( ) => {
@@ -15,19 +15,23 @@ describe("resumeStreamAfterPageShow", () => {
1515} )
1616
1717describe ( "coalesceServerEvents" , ( ) => {
18- const delta = ( value : string , field = "text" ) => ( {
18+ const delta = ( value : string , field = "text" , partID = "part" ) => ( {
1919 directory : "/repo" ,
2020 payload : {
2121 type : "message.part.delta" ,
22- properties : { messageID : "msg" , partID : "part" , field, delta : value } ,
22+ properties : { messageID : "msg" , partID, field, delta : value } ,
2323 } as Event ,
2424 } )
2525
2626 test ( "merges adjacent deltas for the same field" , ( ) => {
27- const result = coalesceServerEvents ( [ delta ( "hello " ) , delta ( "world" ) ] )
27+ const first = delta ( "hello " )
28+ const second = delta ( "world" )
29+ first . payload . id = "first"
30+ second . payload . id = "second"
31+ const result = coalesceServerEvents ( [ first , second ] )
2832
2933 expect ( result ) . toHaveLength ( 1 )
30- expect ( result [ 0 ] ?. payload ) . toMatchObject ( { properties : { delta : "hello world" } } )
34+ expect ( result [ 0 ] ?. payload ) . toMatchObject ( { id : "second" , properties : { delta : "hello world" } } )
3135 } )
3236
3337 test ( "preserves event boundaries and distinct fields" , ( ) => {
@@ -45,9 +49,113 @@ describe("coalesceServerEvents", () => {
4549 ] )
4650 } )
4751
48- test ( "drops stale deltas" , ( ) => {
49- const result = coalesceServerEvents ( [ delta ( "stale" ) ] , new Set ( [ "/repo:msg:part" ] ) )
52+ test ( "preserves event ID order across interleaved deltas" , ( ) => {
53+ const first = delta ( "a" )
54+ const other = delta ( "b" , "text" , "other" )
55+ const last = delta ( "c" )
56+ first . payload . id = "1"
57+ other . payload . id = "2"
58+ last . payload . id = "3"
5059
51- expect ( result ) . toEqual ( [ ] )
60+ const result = coalesceServerEvents ( [ first , other , last ] )
61+
62+ expect ( result . map ( ( event ) => event . payload . id ) ) . toEqual ( [ "1" , "2" , "3" ] )
63+ } )
64+
65+ } )
66+
67+ describe ( "enqueueServerEvent" , ( ) => {
68+ const partUpdated = ( text : string ) =>
69+ ( {
70+ type : "message.part.updated" ,
71+ properties : {
72+ sessionID : "session" ,
73+ part : { id : "part" , sessionID : "session" , messageID : "message" , type : "text" , text } ,
74+ } ,
75+ } ) as Event
76+
77+ test ( "preserves part updates across message remove and re-add barriers" , ( ) => {
78+ const events : Array < { directory : string ; payload : Event } > = [ ]
79+ const enqueue = ( payload : Event ) => enqueueServerEvent ( events , { directory : "/repo" , payload } )
80+
81+ enqueue ( partUpdated ( "old" ) )
82+ enqueue ( { type : "message.removed" , properties : { sessionID : "session" , messageID : "message" } } as Event )
83+ enqueue ( {
84+ type : "message.updated" ,
85+ properties : {
86+ sessionID : "session" ,
87+ info : {
88+ id : "message" ,
89+ sessionID : "session" ,
90+ role : "user" ,
91+ time : { created : 1 } ,
92+ agent : "build" ,
93+ model : { providerID : "provider" , modelID : "model" } ,
94+ } ,
95+ } ,
96+ } as Event )
97+ enqueue ( partUpdated ( "new" ) )
98+
99+ expect ( events . map ( ( event ) => event . payload . type ) ) . toEqual ( [
100+ "message.part.updated" ,
101+ "message.removed" ,
102+ "message.updated" ,
103+ "message.part.updated" ,
104+ ] )
105+ } )
106+
107+ test ( "preserves deltas after a replacement snapshot" , ( ) => {
108+ const events : Array < { directory : string ; payload : Event } > = [ ]
109+ const enqueue = ( payload : Event ) => enqueueServerEvent ( events , { directory : "/repo" , payload } )
110+
111+ enqueue ( partUpdated ( "a" ) )
112+ enqueue ( partUpdated ( "ab" ) )
113+ enqueue ( {
114+ type : "message.part.delta" ,
115+ properties : { sessionID : "session" , messageID : "message" , partID : "part" , field : "text" , delta : "c" } ,
116+ } as Event )
117+
118+ const result = coalesceServerEvents ( events )
119+ expect ( result . map ( ( event ) => event . payload . type ) ) . toEqual ( [ "message.part.updated" , "message.part.delta" ] )
120+ expect ( result [ 0 ] ?. payload ) . toMatchObject ( { properties : { part : { text : "ab" } } } )
121+ expect ( result [ 1 ] ?. payload ) . toMatchObject ( { properties : { delta : "c" } } )
122+ } )
123+
124+ test ( "preserves updates after session deletion" , ( ) => {
125+ const events : Array < { directory : string ; payload : Event } > = [ ]
126+ const enqueue = ( payload : Event ) => enqueueServerEvent ( events , { directory : "/repo" , payload } )
127+
128+ enqueue ( partUpdated ( "old" ) )
129+ enqueue ( {
130+ type : "session.deleted" ,
131+ properties : { sessionID : "session" , info : { id : "session" } } ,
132+ } as Event )
133+ enqueue ( partUpdated ( "new" ) )
134+
135+ expect ( events . map ( ( event ) => event . payload . type ) ) . toEqual ( [
136+ "message.part.updated" ,
137+ "session.deleted" ,
138+ "message.part.updated" ,
139+ ] )
140+ } )
141+
142+ test ( "does not coalesce edge-triggered session statuses" , ( ) => {
143+ const events : Array < { directory : string ; payload : Event } > = [ ]
144+ const enqueue = ( status : "retry" | "busy" ) =>
145+ enqueueServerEvent ( events , {
146+ directory : "/repo" ,
147+ payload : {
148+ type : "session.status" ,
149+ properties : {
150+ sessionID : "session" ,
151+ status : status === "retry" ? { type : "retry" , attempt : 1 , message : "retry" , next : 1 } : { type : "busy" } ,
152+ } ,
153+ } as Event ,
154+ } )
155+
156+ enqueue ( "retry" )
157+ enqueue ( "busy" )
158+
159+ expect ( events ) . toHaveLength ( 2 )
52160 } )
53161} )
0 commit comments