@@ -89,6 +89,116 @@ describe('useTimeline global cache bounds', () => {
8989 expect ( screen . getByTestId ( 'window' ) . textContent ) . toBe ( '3' ) ;
9090 } ) ;
9191
92+ it ( 'keeps active session cache resident so a late stale instance cannot wipe history on the next event' , async ( ) => {
93+ const sessionName = `deck_transport_live_${ Date . now ( ) } ` ;
94+ const cacheKey = `srv:${ sessionName } ` ;
95+ let handlerA : ( ( msg : ServerMessage ) => void ) | null = null ;
96+ let handlerB : ( ( msg : ServerMessage ) => void ) | null = null ;
97+
98+ const wsA : WsClient = {
99+ connected : true ,
100+ onMessage : ( next : ( msg : ServerMessage ) => void ) => {
101+ handlerA = next ;
102+ return ( ) => { handlerA = null ; } ;
103+ } ,
104+ sendTimelineHistoryRequest : ( ) => 'history-a' ,
105+ } as unknown as WsClient ;
106+
107+ const wsB : WsClient = {
108+ connected : true ,
109+ onMessage : ( next : ( msg : ServerMessage ) => void ) => {
110+ handlerB = next ;
111+ return ( ) => { handlerB = null ; } ;
112+ } ,
113+ sendTimelineHistoryRequest : ( ) => 'history-b' ,
114+ } as unknown as WsClient ;
115+
116+ function Probe ( { name, ws } : { name : string ; ws : WsClient } ) {
117+ const { events } = useTimeline ( sessionName , ws , 'srv' ) ;
118+ return h (
119+ 'div' ,
120+ { 'data-testid' : name } ,
121+ events . map ( ( event ) => String ( event . payload . text ?? '' ) ) . join ( '|' ) ,
122+ ) ;
123+ }
124+
125+ const view = render ( h ( Probe , { name : 'primary' , ws : wsA } ) ) ;
126+
127+ await act ( async ( ) => {
128+ handlerA ?.( {
129+ type : 'timeline.history' ,
130+ sessionName,
131+ requestId : 'history-a' ,
132+ epoch : 1 ,
133+ events : [
134+ {
135+ eventId : `${ sessionName } -1` ,
136+ sessionId : sessionName ,
137+ ts : 1 ,
138+ epoch : 1 ,
139+ seq : 1 ,
140+ source : 'daemon' ,
141+ confidence : 'high' ,
142+ type : 'user.message' ,
143+ payload : { text : 'first' } ,
144+ } ,
145+ {
146+ eventId : `${ sessionName } -2` ,
147+ sessionId : sessionName ,
148+ ts : 2 ,
149+ epoch : 1 ,
150+ seq : 2 ,
151+ source : 'daemon' ,
152+ confidence : 'high' ,
153+ type : 'assistant.text' ,
154+ payload : { text : 'second' } ,
155+ } ,
156+ ] ,
157+ } as ServerMessage ) ;
158+ } ) ;
159+
160+ await waitFor ( ( ) => {
161+ expect ( screen . getByTestId ( 'primary' ) . textContent ) . toBe ( 'first|second' ) ;
162+ } ) ;
163+
164+ for ( let i = 0 ; i < 13 ; i ++ ) {
165+ __setTimelineCacheForTests ( `srv:other-${ i } ` , makeEvents ( `other-${ i } ` , 100 ) ) ;
166+ }
167+
168+ expect ( __getTimelineCacheKeysForTests ( ) ) . toContain ( cacheKey ) ;
169+
170+ vi . spyOn ( TimelineDB . prototype , 'open' ) . mockImplementation ( ( ) => new Promise ( ( ) => { } ) ) ;
171+
172+ view . rerender (
173+ h ( 'div' , null ,
174+ h ( Probe , { name : 'primary' , ws : wsA } ) ,
175+ h ( Probe , { name : 'secondary' , ws : wsB } ) ,
176+ ) ,
177+ ) ;
178+
179+ await act ( async ( ) => {
180+ handlerB ?.( {
181+ type : 'timeline.event' ,
182+ event : {
183+ eventId : `${ sessionName } -3` ,
184+ sessionId : sessionName ,
185+ ts : 3 ,
186+ epoch : 1 ,
187+ seq : 3 ,
188+ source : 'daemon' ,
189+ confidence : 'high' ,
190+ type : 'assistant.text' ,
191+ payload : { text : 'third' } ,
192+ } ,
193+ } as ServerMessage ) ;
194+ } ) ;
195+
196+ await waitFor ( ( ) => {
197+ expect ( screen . getByTestId ( 'primary' ) . textContent ) . toBe ( 'first|second|third' ) ;
198+ expect ( screen . getByTestId ( 'secondary' ) . textContent ) . toBe ( 'first|second|third' ) ;
199+ } ) ;
200+ } ) ;
201+
92202 it ( 'uses the shared cache as the merge base when a late instance is locally stale' , ( ) => {
93203 const sessionName = `deck_transport_${ Date . now ( ) } ` ;
94204 const cacheKey = `srv:${ sessionName } ` ;
0 commit comments