1010import static org .mockito .ArgumentMatchers .any ;
1111import static org .mockito .ArgumentMatchers .anyString ;
1212import static org .mockito .ArgumentMatchers .eq ;
13+ import static org .mockito .ArgumentMatchers .isNull ;
1314import static org .mockito .Mockito .mock ;
1415import static org .mockito .Mockito .never ;
1516import static org .mockito .Mockito .verify ;
3536
3637import com .microsoft .copilot .eclipse .core .AuthStatusManager ;
3738import com .microsoft .copilot .eclipse .core .logger .CopilotForEclipseLogger ;
39+ import com .microsoft .copilot .eclipse .core .lsp .protocol .AgentRound ;
3840import com .microsoft .copilot .eclipse .core .lsp .protocol .ChatProgressValue ;
3941import com .microsoft .copilot .eclipse .core .lsp .protocol .CopilotModel ;
42+ import com .microsoft .copilot .eclipse .core .lsp .protocol .Thinking ;
4043import com .microsoft .copilot .eclipse .core .lsp .protocol .Turn ;
44+ import com .microsoft .copilot .eclipse .core .persistence .CopilotTurnData .EditAgentRoundData ;
4145import com .microsoft .copilot .eclipse .core .persistence .CopilotTurnData .ReplyData ;
46+ import com .microsoft .copilot .eclipse .core .persistence .CopilotTurnData .ThinkingBlockData ;
47+ import com .microsoft .copilot .eclipse .core .persistence .CopilotTurnData .ThinkingBlockState ;
4248import com .microsoft .copilot .eclipse .core .persistence .UserTurnData .MessageData ;
4349
4450@ ExtendWith (MockitoExtension .class )
@@ -187,7 +193,7 @@ void testCacheConversationProgress_Success() throws Exception {
187193
188194 when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
189195
190- CompletableFuture <Void > result = persistenceManager .cacheConversationProgress (conversationId , progress );
196+ CompletableFuture <Void > result = persistenceManager .cacheConversationProgress (conversationId , progress , null );
191197
192198 result .get (); // Wait for completion
193199
@@ -198,6 +204,98 @@ void testCacheConversationProgress_Success() throws Exception {
198204 assertTrue (cache .containsKey (conversationId ));
199205 }
200206
207+ @ Test
208+ void testUpdateThinkingBlockTitle_updatesCachedThinkingBlockById () throws Exception {
209+ ConversationPersistenceManager manager = createManagerWithRealDataFactory ();
210+ String conversationId = "00000000-0000-0000-0000-000000000000" ;
211+ String turnId = "00000000-0000-0000-0000-000000000002" ;
212+ String thinkingBlockId = "thinking-block-1" ;
213+ ConversationData conversationData = createTestConversationData (conversationId );
214+ when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
215+
216+ manager .cacheConversationProgress (conversationId ,
217+ createThinkingProgressValue (conversationId , turnId , "thinking content" ), thinkingBlockId ).get ();
218+ manager .updateThinkingBlockTitle (conversationId , turnId , thinkingBlockId , "Generated title" ).get ();
219+
220+ ThinkingBlockData block = getCachedCopilotTurn (manager , conversationId , turnId ).getReply ()
221+ .getEditAgentRounds ().get (0 ).getThinkingBlock ();
222+ assertNotNull (block );
223+ assertEquals (thinkingBlockId , block .getId ());
224+ assertEquals ("Generated title" , block .getTitle ());
225+ }
226+
227+ @ Test
228+ void testCancelThinkingBlock_updatesCachedThinkingBlockById () throws Exception {
229+ ConversationPersistenceManager manager = createManagerWithRealDataFactory ();
230+ String conversationId = "00000000-0000-0000-0000-000000000000" ;
231+ String turnId = "00000000-0000-0000-0000-000000000002" ;
232+ String thinkingBlockId = "thinking-block-1" ;
233+ ConversationData conversationData = createTestConversationData (conversationId );
234+ when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
235+
236+ manager .cacheConversationProgress (conversationId ,
237+ createThinkingProgressValue (conversationId , turnId , "thinking content" ), thinkingBlockId ).get ();
238+ manager .cancelThinkingBlock (conversationId , turnId , thinkingBlockId ).get ();
239+
240+ ThinkingBlockData block = getCachedCopilotTurn (manager , conversationId , turnId ).getReply ()
241+ .getEditAgentRounds ().get (0 ).getThinkingBlock ();
242+ assertNotNull (block );
243+ assertEquals (thinkingBlockId , block .getId ());
244+ assertEquals (ThinkingBlockState .CANCELLED , block .getState ());
245+ }
246+
247+ @ Test
248+ void testCacheConversationProgress_withThinkingBlockId_updatesMatchingPlaceholderRound () throws Exception {
249+ ConversationPersistenceManager manager = createManagerWithRealDataFactory ();
250+ String conversationId = "00000000-0000-0000-0000-000000000000" ;
251+ String turnId = "00000000-0000-0000-0000-000000000002" ;
252+ String firstThinkingBlockId = "thinking-block-1" ;
253+ String secondThinkingBlockId = "thinking-block-2" ;
254+ ConversationData conversationData = createTestConversationData (conversationId );
255+ when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
256+
257+ manager .cacheConversationProgress (conversationId ,
258+ createThinkingProgressValue (conversationId , turnId , "first" ), firstThinkingBlockId ).get ();
259+ manager .cacheConversationProgress (conversationId ,
260+ createThinkingProgressValue (conversationId , turnId , "second" ), secondThinkingBlockId ).get ();
261+ manager .cacheConversationProgress (conversationId ,
262+ createAgentRoundProgressValue (conversationId , turnId , 1 , "round reply" ), secondThinkingBlockId ).get ();
263+
264+ ReplyData reply = getCachedCopilotTurn (manager , conversationId , turnId ).getReply ();
265+ List <EditAgentRoundData > rounds = reply .getEditAgentRounds ();
266+ assertEquals (2 , rounds .size ());
267+ EditAgentRoundData updatedRound = rounds .stream ()
268+ .filter (round -> round .getRoundId () == 1 )
269+ .findFirst ()
270+ .orElseThrow ();
271+ assertEquals (secondThinkingBlockId , updatedRound .getThinkingBlock ().getId ());
272+ assertEquals ("round reply" , updatedRound .getReply ());
273+ assertTrue (rounds .stream ()
274+ .anyMatch (round -> firstThinkingBlockId .equals (round .getThinkingBlock ().getId ())));
275+ }
276+
277+ @ Test
278+ void testCacheConversationProgress_preservesWhitespaceOnlyThinkingFragments () throws Exception {
279+ ConversationPersistenceManager manager = createManagerWithRealDataFactory ();
280+ String conversationId = "00000000-0000-0000-0000-000000000000" ;
281+ String turnId = "00000000-0000-0000-0000-000000000002" ;
282+ String thinkingBlockId = "thinking-block-1" ;
283+ ConversationData conversationData = createTestConversationData (conversationId );
284+ when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
285+
286+ manager .cacheConversationProgress (conversationId ,
287+ createThinkingProgressValue (conversationId , turnId , "before title." ), thinkingBlockId ).get ();
288+ manager .cacheConversationProgress (conversationId ,
289+ createThinkingProgressValue (conversationId , turnId , "\n " ), thinkingBlockId ).get ();
290+ manager .cacheConversationProgress (conversationId ,
291+ createThinkingProgressValue (conversationId , turnId , "**Next title**\n \n body" ), thinkingBlockId ).get ();
292+
293+ ThinkingBlockData block = getCachedCopilotTurn (manager , conversationId , turnId ).getReply ()
294+ .getEditAgentRounds ().get (0 ).getThinkingBlock ();
295+ assertNotNull (block );
296+ assertEquals ("before title.\n **Next title**\n \n body" , block .getContent ());
297+ }
298+
201299 @ Test
202300 void testPersistConversationProgress_Success () throws Exception {
203301 String conversationId = "00000000-0000-0000-0000-000000000000" ;
@@ -206,7 +304,7 @@ void testPersistConversationProgress_Success() throws Exception {
206304
207305 when (mockPersistenceService .loadConversationFromPersistedJsonFile (conversationId )).thenReturn (conversationData );
208306
209- CompletableFuture <Void > result = persistenceManager .persistConversationProgress (conversationId , progress );
307+ CompletableFuture <Void > result = persistenceManager .persistConversationProgress (conversationId , progress , null );
210308
211309 result .get (); // Wait for completion
212310
@@ -229,7 +327,7 @@ void testUpdateConversationProgress_NewConversation() throws Exception {
229327 assertNotNull (result );
230328 assertEquals (conversationId , result .getConversationId ());
231329 verify (mockDataFactory ).updateConversationMetadata (newConversationData , progress );
232- verify (mockDataFactory ).updateReplyFromProgress (any (), eq (progress ));
330+ verify (mockDataFactory ).updateReplyFromProgress (any (), eq (progress ), isNull () );
233331 }
234332
235333 // Helper methods to create test data
@@ -294,4 +392,50 @@ private ChatProgressValue createTestChatProgressValue() {
294392 progress .setSuggestedTitle ("Test Suggested Title" );
295393 return progress ;
296394 }
395+
396+ private ConversationPersistenceManager createManagerWithRealDataFactory () throws Exception {
397+ ConversationPersistenceManager manager = new ConversationPersistenceManager (mockAuthStatusManager );
398+ setPrivateField (manager , "persistenceService" , mockPersistenceService );
399+ return manager ;
400+ }
401+
402+ private CopilotTurnData getCachedCopilotTurn (ConversationPersistenceManager manager , String conversationId ,
403+ String turnId ) throws Exception {
404+ ConversationData conversationData = manager .loadConversation (conversationId ).get ();
405+ assertNotNull (conversationData );
406+ return conversationData .getTurns ().stream ()
407+ .filter (CopilotTurnData .class ::isInstance )
408+ .map (CopilotTurnData .class ::cast )
409+ .filter (turn -> turnId .equals (turn .getTurnId ()))
410+ .findFirst ()
411+ .orElseThrow ();
412+ }
413+
414+ private ChatProgressValue createThinkingProgressValue (String conversationId , String turnId , String thinkingText ) {
415+ ChatProgressValue progress = new ChatProgressValue ();
416+ progress .setKind (WorkDoneProgressKind .report );
417+ progress .setConversationId (conversationId );
418+ progress .setTurnId (turnId );
419+ progress .setThinking (new Thinking ("server-thinking-id" , thinkingText , null ));
420+ return progress ;
421+ }
422+
423+ private ChatProgressValue createAgentRoundProgressValue (String conversationId , String turnId , int roundId ,
424+ String reply ) throws Exception {
425+ ChatProgressValue progress = new ChatProgressValue ();
426+ progress .setKind (WorkDoneProgressKind .report );
427+ progress .setConversationId (conversationId );
428+ progress .setTurnId (turnId );
429+ AgentRound round = new AgentRound ();
430+ setPrivateField (round , "roundId" , roundId );
431+ setPrivateField (round , "reply" , reply );
432+ setPrivateField (progress , "editAgentRounds" , List .of (round ));
433+ return progress ;
434+ }
435+
436+ private void setPrivateField (Object target , String fieldName , Object value ) throws Exception {
437+ var field = target .getClass ().getDeclaredField (fieldName );
438+ field .setAccessible (true );
439+ field .set (target , value );
440+ }
297441}
0 commit comments