@@ -1647,6 +1647,7 @@ describe('handleSessionHistoryChanged', () => {
16471647 const context = createFlowChatContext ( ) ;
16481648 handleSessionHistoryChanged ( context , {
16491649 sessionId : 'session-1' ,
1650+ settledTurnId : 'turn-1' ,
16501651 } ) ;
16511652 await Promise . resolve ( ) ;
16521653
@@ -1672,12 +1673,84 @@ describe('handleSessionHistoryChanged', () => {
16721673 const context = createFlowChatContext ( ) ;
16731674 handleSessionHistoryChanged ( context , {
16741675 sessionId : 'session-1' ,
1676+ settledTurnId : 'turn-1' ,
16751677 } ) ;
16761678 await Promise . resolve ( ) ;
16771679
16781680 expect ( reconcile ) . not . toHaveBeenCalled ( ) ;
16791681 expect ( context . pendingHistoryFenceSessions . has ( 'session-1' ) ) . toBe ( true ) ;
16801682 } ) ;
1683+
1684+ it . each ( [
1685+ { sessionId : 'session-1' , settledTurnId : null } ,
1686+ { sessionId : 'session-1' } ,
1687+ { session_id : 'session-1' , settled_turn_id : null } ,
1688+ ] ) ( 'removes rolled-back turns with an authoritative reload, including legacy payloads: %j' , async event => {
1689+ createSessionWithTurn ( {
1690+ ...createFinishingTurn ( ) ,
1691+ status : 'completed' ,
1692+ endTime : 1000 ,
1693+ } ) ;
1694+ const store = FlowChatStore . getInstance ( ) ;
1695+ store . setState ( prev => ( {
1696+ ...prev ,
1697+ activeSessionId : 'session-1' ,
1698+ sessions : new Map ( [ [ 'session-1' , {
1699+ ...prev . sessions . get ( 'session-1' ) ! ,
1700+ workspacePath : '/workspace' ,
1701+ workspaceId : 'workspace-1' ,
1702+ config : { agentType : 'Standard' , workspaceId : 'workspace-1' } ,
1703+ } ] ] ) ,
1704+ } ) ) ;
1705+ vi . spyOn ( agentAPI , 'restoreSessionView' ) . mockResolvedValue ( {
1706+ session : {
1707+ sessionId : 'session-1' , sessionName : 'Session 1' , agentType : 'Standard' ,
1708+ state : 'Idle' , turnCount : 0 , createdAt : 1 , updatedAt : 2 ,
1709+ } ,
1710+ turns : [ ] ,
1711+ contextRestoreState : 'ready' ,
1712+ isPartial : false ,
1713+ loadedTurnCount : 0 ,
1714+ totalTurnCount : 0 ,
1715+ } as any ) ;
1716+ const reconcile = vi . spyOn ( store , 'reconcileSettledDialogTurn' ) ;
1717+ const context = createFlowChatContext ( ) ;
1718+ context . pendingHistoryFenceSessions . add ( 'session-1' ) ;
1719+
1720+ handleSessionHistoryChanged ( context , event ) ;
1721+
1722+ await vi . waitFor ( ( ) => expect ( store . getState ( ) . sessions . get ( 'session-1' ) ?. dialogTurns ) . toEqual ( [ ] ) ) ;
1723+ expect ( reconcile ) . not . toHaveBeenCalled ( ) ;
1724+ expect ( context . pendingHistoryFenceSessions . has ( 'session-1' ) ) . toBe ( false ) ;
1725+ } ) ;
1726+
1727+ it ( 'reloads an out-of-lifecycle edit even when the obsolete local tail is still finishing' , ( ) => {
1728+ createSessionWithTurn ( createFinishingTurn ( ) ) ;
1729+ const store = FlowChatStore . getInstance ( ) ;
1730+ const reload = vi . spyOn ( store , 'loadSessionHistory' ) . mockResolvedValue ( undefined ) ;
1731+ const context = createFlowChatContext ( ) ;
1732+
1733+ handleSessionHistoryChanged ( context , { sessionId : 'session-1' , settledTurnId : null } ) ;
1734+
1735+ expect ( reload ) . toHaveBeenCalledExactlyOnceWith ( 'session-1' , { includeInternal : false } ) ;
1736+ expect ( context . pendingHistoryFenceSessions . has ( 'session-1' ) ) . toBe ( false ) ;
1737+ } ) ;
1738+
1739+ it ( 'repairs the identified settled turn without dropping a newer live turn' , ( ) => {
1740+ createSessionWithTurn ( { ...createFinishingTurn ( ) , status : 'completed' , endTime : 1000 } ) ;
1741+ const store = FlowChatStore . getInstance ( ) ;
1742+ store . addDialogTurn ( 'session-1' , { ...createFinishingTurn ( ) , id : 'turn-2' , status : 'processing' } ) ;
1743+ const reconcile = vi . spyOn ( store , 'reconcileSettledDialogTurn' ) . mockResolvedValue ( false ) ;
1744+ const reload = vi . spyOn ( store , 'loadSessionHistory' ) . mockResolvedValue ( undefined ) ;
1745+ const context = createFlowChatContext ( ) ;
1746+
1747+ handleSessionHistoryChanged ( context , { session_id : 'session-1' , settled_turn_id : 'turn-1' } ) ;
1748+
1749+ expect ( reconcile ) . toHaveBeenCalledExactlyOnceWith ( 'session-1' , 'turn-1' ) ;
1750+ expect ( reload ) . not . toHaveBeenCalled ( ) ;
1751+ expect ( context . pendingHistoryFenceSessions . has ( 'session-1' ) ) . toBe ( false ) ;
1752+ expect ( store . getState ( ) . sessions . get ( 'session-1' ) ?. dialogTurns . at ( - 1 ) ?. status ) . toBe ( 'processing' ) ;
1753+ } ) ;
16811754} ) ;
16821755
16831756describe ( 'handleDialogTurnComplete' , ( ) => {
@@ -1761,7 +1834,7 @@ describe('handleDialogTurnComplete', () => {
17611834
17621835 // Durable fence lands while the turn is still finalizing: it must be
17631836 // parked instead of racing the persistence commit with an extra read.
1764- handleSessionHistoryChanged ( context , { sessionId : 'session-1' } ) ;
1837+ handleSessionHistoryChanged ( context , { sessionId : 'session-1' , settledTurnId : 'turn-1' } ) ;
17651838 expect ( FlowChatStore . getInstance ( ) . reconcileSettledDialogTurn )
17661839 . not . toHaveBeenCalled ( ) ;
17671840 expect ( context . pendingHistoryFenceSessions . has ( 'session-1' ) ) . toBe ( true ) ;
0 commit comments