diff --git a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ChatViewTest.java b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ChatViewTest.java index 2bdc94e9..d667fa49 100644 --- a/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ChatViewTest.java +++ b/com.microsoft.copilot.eclipse.ui.test/src/com/microsoft/copilot/eclipse/ui/chat/ChatViewTest.java @@ -3,10 +3,13 @@ package com.microsoft.copilot.eclipse.ui.chat; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.List; import org.eclipse.swt.SWT; import org.eclipse.swt.widgets.Composite; @@ -17,11 +20,12 @@ import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; import org.mockito.junit.jupiter.MockitoExtension; import com.microsoft.copilot.eclipse.core.AuthStatusManager; import com.microsoft.copilot.eclipse.core.lsp.protocol.ChatMode; +import com.microsoft.copilot.eclipse.core.persistence.AbstractTurnData; +import com.microsoft.copilot.eclipse.core.persistence.UserTurnData; import com.microsoft.copilot.eclipse.ui.chat.services.ChatServiceManager; import com.microsoft.copilot.eclipse.ui.chat.services.McpConfigService; import com.microsoft.copilot.eclipse.ui.chat.services.ReferencedFileService; @@ -45,16 +49,17 @@ class ChatViewTest { private ReferencedFileService mockReferencedFileService; @Mock private McpConfigService mockMcpConfigService; + @Mock + private ChatContentViewer mockChatContentViewer; + @Mock + private BaseTurnWidget mockTurnWidget; @BeforeEach void setUp() { - MockitoAnnotations.openMocks(this); - SwtUtils.invokeOnDisplayThread(() -> { setupSwtComponents(); setupChatView(); }); - setupMocks(); } private void setupSwtComponents() { @@ -69,10 +74,25 @@ private void setupChatView() { setFieldValue(chatView, "chatServiceManager", mockChatServiceManager); } - private void setupMocks() { - when(mockChatServiceManager.getAuthStatusManager()).thenReturn(mockAuthStatusManager); - when(mockChatServiceManager.getReferencedFileService()).thenReturn(mockReferencedFileService); - when(mockChatServiceManager.getUserPreferenceService()).thenReturn(mockUserPreferenceService); + @Test + void restoreTurnsSkipsUserTurnWithoutMessageAndRestoresFollowingTurn() throws Exception { + setFieldValue(chatView, "chatContentViewer", mockChatContentViewer); + when(mockChatContentViewer.getLatestOrCreateNewTurnWidget("valid-turn", false, true)).thenReturn(mockTurnWidget); + + UserTurnData invalidTurn = new UserTurnData(); + invalidTurn.setTurnId("missing-message-turn"); + UserTurnData validTurn = new UserTurnData(); + validTurn.setTurnId("valid-turn"); + validTurn.setMessage(new UserTurnData.MessageData("hello")); + + Method restoreTurns = ChatView.class.getDeclaredMethod("restoreTurns", List.class); + restoreTurns.setAccessible(true); + restoreTurns.invoke(chatView, List.of(invalidTurn, validTurn)); + + verify(mockChatContentViewer, never()).getLatestOrCreateNewTurnWidget("missing-message-turn", false, true); + verify(mockChatContentViewer).getLatestOrCreateNewTurnWidget("valid-turn", false, true); + verify(mockTurnWidget).appendMessage("hello"); + verify(mockTurnWidget).flushMessageBuffer(); } private void setFieldValue(Object target, String fieldName, Object value) { diff --git a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java index 74457e8c..fdea9af1 100644 --- a/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java +++ b/com.microsoft.copilot.eclipse.ui/src/com/microsoft/copilot/eclipse/ui/chat/ChatView.java @@ -311,9 +311,7 @@ public void done(IJobChangeEvent event) { // Restore all turns from the conversation if (historyConversation.getTurns() != null && !historyConversation.getTurns().isEmpty()) { - for (AbstractTurnData turn : historyConversation.getTurns()) { - restoreTurn(turn); - } + restoreTurns(historyConversation.getTurns()); // Restore the mode from the last user turn restoreModeFromLastUserTurn(historyConversation.getTurns()); @@ -1914,9 +1912,14 @@ private void restoreTurn(AbstractTurnData turn) { // Create user turn widget and populate with user message if (turn instanceof UserTurnData userTurn) { - if (userTurn.getMessage() == null || StringUtils.isNotBlank(userTurn.getMessage().getText())) { + UserTurnData.MessageData message = userTurn.getMessage(); + if (message == null) { + CopilotCore.LOGGER.info("Skipping user turn with missing message: " + userTurn.getTurnId()); + return; + } + if (StringUtils.isNotBlank(message.getText())) { BaseTurnWidget userTurnWidget = chatContentViewer.getLatestOrCreateNewTurnWidget(turn.getTurnId(), false, true); - userTurnWidget.appendMessage(userTurn.getMessage().getText()); + userTurnWidget.appendMessage(message.getText()); userTurnWidget.flushMessageBuffer(); return; } @@ -1938,6 +1941,12 @@ private void restoreTurn(AbstractTurnData turn) { } } + private void restoreTurns(List turns) { + for (AbstractTurnData turn : turns) { + restoreTurn(turn); + } + } + /** * Restores the content of a CopilotTurnData (reply text, agent rounds, tool calls, errors, agent messages) into the * given turn widget. Used for both main copilot turns and subagent turns.