@@ -13,6 +13,7 @@ let mockIsChatLoaded = true;
1313let mockConnectionStatus : string | undefined ;
1414let mockCurrentSupportInteraction : Record < string , unknown > | undefined ;
1515let mockConversation : { id : string ; messages : Message [ ] } | null ;
16+ let mockOdieChat : Record < string , unknown > | undefined ;
1617const mockGetZendeskConversation = jest . fn ( ) ;
1718const mockStartNewInteraction = jest . fn ( ) ;
1819
@@ -43,7 +44,7 @@ jest.mock( '../use-logged-out-session', () => ( {
4344jest . mock ( '../../data' , ( ) => ( {
4445 useGetZendeskConversation : ( ) => mockGetZendeskConversation ,
4546 useManageSupportInteraction : ( ) => ( { startNewInteraction : mockStartNewInteraction } ) ,
46- useOdieChat : ( ) => ( { data : undefined , isFetching : false } ) ,
47+ useOdieChat : ( ) => ( { data : mockOdieChat , isFetching : false } ) ,
4748} ) ) ;
4849
4950jest . mock ( '../../data/use-current-support-interaction' , ( ) => ( {
@@ -100,9 +101,132 @@ beforeEach( () => {
100101 mockConnectionStatus = undefined ;
101102 mockCurrentSupportInteraction = undefined ;
102103 mockConversation = null ;
104+ mockOdieChat = undefined ;
103105 mockGetZendeskConversation . mockImplementation ( ( ) => Promise . resolve ( mockConversation ) ) ;
104106} ) ;
105107
108+ describe ( 'useGetCombinedChat — merging the Odie and Zendesk halves' , ( ) => {
109+ const userMessage = ( id : number , content : string ) : Message =>
110+ ( {
111+ content,
112+ role : 'user' ,
113+ type : 'message' ,
114+ message_id : id ,
115+ metadata : { } ,
116+ } ) as unknown as Message ;
117+
118+ const countMessages = ( messages : Message [ ] , content : string ) =>
119+ messages . filter ( ( message ) => message . content === content ) . length ;
120+
121+ it ( 'keeps a pre-escalation user message single after the conversation is re-fetched' , async ( ) => {
122+ mockCurrentSupportInteraction = {
123+ uuid : 'int-1' ,
124+ conversationId : 'conv-1' ,
125+ odieId : 42 ,
126+ status : 'open' ,
127+ } ;
128+ mockOdieChat = {
129+ odieId : 42 ,
130+ wpcomUserId : 99 ,
131+ conversationId : null ,
132+ provider : 'odie' ,
133+ status : 'loaded' ,
134+ messages : [ userMessage ( 10 , 'How do I backup my site?' ) ] ,
135+ } ;
136+ mockConversation = { id : 'conv-1' , messages : [ agentMessage ( 1 , 'Happiness Engineer here' ) ] } ;
137+
138+ const { result, rerender } = renderCombinedChat ( ) ;
139+
140+ await waitFor ( ( ) => {
141+ expect ( mockGetZendeskConversation ) . toHaveBeenCalledWith ( 'conv-1' ) ;
142+ expect (
143+ countMessages ( result . current . mainChatState . messages , 'How do I backup my site?' )
144+ ) . toBe ( 1 ) ;
145+ } ) ;
146+
147+ // Any reconnect re-runs the merge against the previous chat state. The second agent
148+ // message marks when that re-merge has landed.
149+ mockConversation = {
150+ id : 'conv-1' ,
151+ messages : [ agentMessage ( 1 , 'Happiness Engineer here' ) , agentMessage ( 2 , 'still here' ) ] ,
152+ } ;
153+ act ( ( ) => {
154+ mockConnectionStatus = 'connected' ;
155+ } ) ;
156+ rerender ( ) ;
157+
158+ await waitFor ( ( ) => {
159+ expect ( result . current . mainChatState . messages . some ( ( m ) => m . message_id === 2 ) ) . toBe (
160+ true
161+ ) ;
162+ } ) ;
163+
164+ expect (
165+ countMessages ( result . current . mainChatState . messages , 'How do I backup my site?' )
166+ ) . toBe ( 1 ) ;
167+ } ) ;
168+
169+ it ( 'keeps a queued Zendesk message that the re-fetched conversation does not have yet' , async ( ) => {
170+ mockCurrentSupportInteraction = {
171+ uuid : 'int-1' ,
172+ conversationId : 'conv-1' ,
173+ odieId : 42 ,
174+ status : 'open' ,
175+ } ;
176+ mockOdieChat = {
177+ odieId : 42 ,
178+ wpcomUserId : 99 ,
179+ conversationId : null ,
180+ provider : 'odie' ,
181+ status : 'loaded' ,
182+ messages : [ ] ,
183+ } ;
184+ mockConversation = { id : 'conv-1' , messages : [ agentMessage ( 1 , 'Happiness Engineer here' ) ] } ;
185+
186+ const { result, rerender } = renderCombinedChat ( ) ;
187+
188+ await waitFor ( ( ) => {
189+ expect ( result . current . mainChatState . messages . some ( ( m ) => m . message_id === 1 ) ) . toBe (
190+ true
191+ ) ;
192+ } ) ;
193+
194+ // The user sends a message that has not reached the server yet: it lives only in the
195+ // chat state, carrying the temporary id the composer attaches for Zendesk sends.
196+ act ( ( ) => {
197+ result . current . setMainChatState ( ( chat ) => ( {
198+ ...chat ,
199+ messages : [
200+ ...chat . messages ,
201+ {
202+ content : 'my site is down' ,
203+ role : 'user' ,
204+ type : 'message' ,
205+ metadata : { temporary_id : 'temp-1' } ,
206+ } as unknown as Message ,
207+ ] ,
208+ } ) ) ;
209+ } ) ;
210+
211+ mockConversation = {
212+ id : 'conv-1' ,
213+ messages : [ agentMessage ( 1 , 'Happiness Engineer here' ) , agentMessage ( 2 , 'still here' ) ] ,
214+ } ;
215+ act ( ( ) => {
216+ mockConnectionStatus = 'connected' ;
217+ } ) ;
218+ rerender ( ) ;
219+
220+ await waitFor ( ( ) => {
221+ expect ( result . current . mainChatState . messages . some ( ( m ) => m . message_id === 2 ) ) . toBe (
222+ true
223+ ) ;
224+ } ) ;
225+
226+ expect ( countMessages ( result . current . mainChatState . messages , 'my site is down' ) ) . toBe ( 1 ) ;
227+ } ) ;
228+ } ) ;
229+
106230describe ( 'useGetCombinedChat — message recovery on Smooch re-init' , ( ) => {
107231 it ( 're-fetches the conversation and recovers gap messages when isChatLoaded flips false → true' , async ( ) => {
108232 mockCurrentSupportInteraction = {
0 commit comments