@@ -3014,6 +3014,121 @@ describe("OpenPI Web store", () => {
30143014 store . getState ( ) . actions . stop ( ) ;
30153015 } ) ;
30163016
3017+ it . each ( [
3018+ { snapshotCursor : 6 , expectedReads : 2 } ,
3019+ { snapshotCursor : 5 , expectedReads : 3 } ,
3020+ ] ) (
3021+ "keeps only uncovered event refreshes after an in-flight snapshot at cursor $snapshotCursor" ,
3022+ async ( { snapshotCursor, expectedReads } ) => {
3023+ vi . useFakeTimers ( ) ;
3024+ const client = new FakeClient ( ) ;
3025+ const reading = deferred < WebSnapshot > ( ) ;
3026+ const canonical = { ...snapshot ( "Canonical" ) , cursor : snapshotCursor } ;
3027+ client . snapshots . push (
3028+ Promise . resolve ( snapshot ( ) ) ,
3029+ reading . promise ,
3030+ Promise . resolve ( { ...canonical , cursor : 6 } ) ,
3031+ ) ;
3032+ const stream = eventStreamHarness ( ) ;
3033+ const store = createWebStore ( client , {
3034+ consumeEvents : stream . consumeEvents ,
3035+ } ) ;
3036+ await store . getState ( ) . actions . refreshSnapshot ( ) ;
3037+ store . getState ( ) . actions . start ( ) ;
3038+ try {
3039+ stream . emit ( runtimeEvent ( 5 , "agent_start" , { sessionId : "session-1" } ) ) ;
3040+ await vi . advanceTimersByTimeAsync ( 160 ) ;
3041+ expect ( client . snapshotPaths ) . toHaveLength ( 2 ) ;
3042+ stream . emit (
3043+ runtimeEvent ( 6 , "message_end" , {
3044+ messageKey : "answer" ,
3045+ message : { role : "assistant" , content : "Finished answer" } ,
3046+ } ) ,
3047+ ) ;
3048+ // Live projection remains immediate while canonical history is read.
3049+ expect ( store . getState ( ) . liveMessages [ 0 ] ?. message . content ) . toBe (
3050+ "Finished answer" ,
3051+ ) ;
3052+ reading . resolve ( canonical ) ;
3053+ await vi . advanceTimersByTimeAsync ( 320 ) ;
3054+ expect ( client . snapshotPaths ) . toHaveLength ( expectedReads ) ;
3055+ expect ( store . getState ( ) . snapshot ?. cursor ) . toBe ( 6 ) ;
3056+ expect ( store . getState ( ) . snapshot ?. sessions [ 0 ] ?. name ) . toBe ( "Canonical" ) ;
3057+ } finally {
3058+ store . getState ( ) . actions . stop ( ) ;
3059+ }
3060+ } ,
3061+ ) ;
3062+
3063+ it ( "does not let a superseded snapshot suppress refreshes for newer events" , async ( ) => {
3064+ vi . useFakeTimers ( ) ;
3065+ const client = new FakeClient ( ) ;
3066+ const older = deferred < WebSnapshot > ( ) ;
3067+ client . snapshots . push (
3068+ Promise . resolve ( snapshot ( ) ) ,
3069+ older . promise ,
3070+ Promise . resolve ( { ...snapshot ( ) , cursor : 5 } ) ,
3071+ Promise . resolve ( { ...snapshot ( "Newer event confirmed" ) , cursor : 6 } ) ,
3072+ ) ;
3073+ const stream = eventStreamHarness ( ) ;
3074+ const store = createWebStore ( client , {
3075+ consumeEvents : stream . consumeEvents ,
3076+ } ) ;
3077+ await store . getState ( ) . actions . refreshSnapshot ( ) ;
3078+ store . getState ( ) . actions . start ( ) ;
3079+ try {
3080+ stream . emit ( runtimeEvent ( 5 , "agent_start" , { sessionId : "session-1" } ) ) ;
3081+ await vi . advanceTimersByTimeAsync ( 160 ) ;
3082+ stream . emit (
3083+ runtimeEvent ( 6 , "session_renamed" , { sessionId : "session-1" } ) ,
3084+ ) ;
3085+ await store . getState ( ) . actions . refreshSnapshot ( ) ;
3086+ // This cursor would cover the event, but the newer request superseded it.
3087+ older . resolve ( { ...snapshot ( "Superseded" ) , cursor : 6 } ) ;
3088+ await vi . advanceTimersByTimeAsync ( 320 ) ;
3089+ expect ( client . snapshotPaths ) . toHaveLength ( 4 ) ;
3090+ expect ( store . getState ( ) . snapshot ?. sessions [ 0 ] ?. name ) . toBe (
3091+ "Newer event confirmed" ,
3092+ ) ;
3093+ } finally {
3094+ store . getState ( ) . actions . stop ( ) ;
3095+ }
3096+ } ) ;
3097+
3098+ it ( "retains heartbeat reconciliation even when a snapshot covers pending events" , async ( ) => {
3099+ vi . useFakeTimers ( ) ;
3100+ const client = new FakeClient ( ) ;
3101+ client . snapshots . push (
3102+ Promise . resolve ( snapshot ( ) ) ,
3103+ Promise . resolve ( { ...snapshot ( ) , cursor : 5 } ) ,
3104+ Promise . resolve ( { ...snapshot ( "Externally updated" ) , cursor : 5 } ) ,
3105+ ) ;
3106+ let heartbeat : ( ( ) => void ) | undefined ;
3107+ const stream = eventStreamHarness ( ) ;
3108+ const store = createWebStore ( client , {
3109+ consumeEvents : ( options ) => {
3110+ heartbeat = options . onHeartbeat ;
3111+ return stream . consumeEvents ( options ) ;
3112+ } ,
3113+ } ) ;
3114+ await store . getState ( ) . actions . refreshSnapshot ( ) ;
3115+ store . getState ( ) . actions . start ( ) ;
3116+ try {
3117+ stream . emit (
3118+ runtimeEvent ( 5 , "session_renamed" , { sessionId : "session-1" } ) ,
3119+ ) ;
3120+ await store . getState ( ) . actions . refreshSnapshot ( ) ;
3121+ heartbeat ?.( ) ;
3122+ await vi . advanceTimersByTimeAsync ( 160 ) ;
3123+ expect ( client . snapshotPaths ) . toHaveLength ( 3 ) ;
3124+ expect ( store . getState ( ) . snapshot ?. sessions [ 0 ] ?. name ) . toBe (
3125+ "Externally updated" ,
3126+ ) ;
3127+ } finally {
3128+ store . getState ( ) . actions . stop ( ) ;
3129+ }
3130+ } ) ;
3131+
30173132 it ( "refreshes Session metadata events emitted by another browser tab" , async ( ) => {
30183133 vi . useFakeTimers ( ) ;
30193134 const client = new FakeClient ( ) ;
0 commit comments