|
| 1 | +import { describe, it, expect, vi, beforeEach } from "vitest" |
| 2 | +import { webviewMessageHandler } from "../webviewMessageHandler" |
| 3 | +import type { ClineProvider } from "../ClineProvider" |
| 4 | +import type { ClineMessage } from "@roo-code/types" |
| 5 | + |
| 6 | +describe("webviewMessageHandler - findMessageIndices", () => { |
| 7 | + let mockClineProvider: any |
| 8 | + let mockTask: any |
| 9 | + |
| 10 | + beforeEach(() => { |
| 11 | + // Create mock messages with specific timestamps |
| 12 | + const mockMessages: ClineMessage[] = [ |
| 13 | + { ts: 1000, type: "say", say: "user_feedback", text: "Message 1" }, |
| 14 | + { ts: 1500, type: "say", say: "user_feedback", text: "Message 2" }, |
| 15 | + { ts: 2000, type: "say", say: "user_feedback", text: "Message 3" }, |
| 16 | + { ts: 2999, type: "say", say: "user_feedback", text: "Message 4" }, // Within 1 second of message 3 |
| 17 | + { ts: 3000, type: "say", say: "user_feedback", text: "Message 5" }, |
| 18 | + ] |
| 19 | + |
| 20 | + const mockApiHistory = [ |
| 21 | + { ts: 1000, role: "user", content: "API Message 1" }, |
| 22 | + { ts: 1500, role: "assistant", content: "API Message 2" }, |
| 23 | + { ts: 2000, role: "user", content: "API Message 3" }, |
| 24 | + { ts: 2999, role: "assistant", content: "API Message 4" }, |
| 25 | + { ts: 3000, role: "user", content: "API Message 5" }, |
| 26 | + ] |
| 27 | + |
| 28 | + mockTask = { |
| 29 | + taskId: "test-task-id", |
| 30 | + clineMessages: mockMessages, |
| 31 | + apiConversationHistory: mockApiHistory, |
| 32 | + overwriteClineMessages: vi.fn(), |
| 33 | + overwriteApiConversationHistory: vi.fn(), |
| 34 | + handleWebviewAskResponse: vi.fn(), |
| 35 | + } |
| 36 | + |
| 37 | + mockClineProvider = { |
| 38 | + getCurrentTask: vi.fn(() => mockTask), |
| 39 | + getTaskWithId: vi.fn().mockResolvedValue({ |
| 40 | + historyItem: { ts: Date.now(), task: "Test", tokensIn: 0, tokensOut: 0 }, |
| 41 | + }), |
| 42 | + createTaskWithHistoryItem: vi.fn(), |
| 43 | + postMessageToWebview: vi.fn(), |
| 44 | + } as unknown as ClineProvider |
| 45 | + }) |
| 46 | + |
| 47 | + describe("deleteMessage with exact timestamp matching", () => { |
| 48 | + it("should delete only the message with exact timestamp match", async () => { |
| 49 | + // First, show the delete dialog |
| 50 | + await webviewMessageHandler(mockClineProvider, { |
| 51 | + type: "deleteMessage", |
| 52 | + value: 2000, // Delete message at timestamp 2000 |
| 53 | + }) |
| 54 | + |
| 55 | + // Verify dialog was shown |
| 56 | + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ |
| 57 | + type: "showDeleteMessageDialog", |
| 58 | + messageTs: 2000, |
| 59 | + }) |
| 60 | + |
| 61 | + // Now confirm the deletion |
| 62 | + await webviewMessageHandler(mockClineProvider, { |
| 63 | + type: "deleteMessageConfirm", |
| 64 | + messageTs: 2000, |
| 65 | + }) |
| 66 | + |
| 67 | + // Should delete from index 2 onwards (messages 3, 4, 5) |
| 68 | + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([ |
| 69 | + mockTask.clineMessages[0], |
| 70 | + mockTask.clineMessages[1], |
| 71 | + ]) |
| 72 | + |
| 73 | + expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ |
| 74 | + mockTask.apiConversationHistory[0], |
| 75 | + mockTask.apiConversationHistory[1], |
| 76 | + ]) |
| 77 | + }) |
| 78 | + |
| 79 | + it("should not delete messages within 1 second buffer when using exact matching", async () => { |
| 80 | + // Delete message at timestamp 3000 |
| 81 | + await webviewMessageHandler(mockClineProvider, { |
| 82 | + type: "deleteMessage", |
| 83 | + value: 3000, |
| 84 | + }) |
| 85 | + |
| 86 | + await webviewMessageHandler(mockClineProvider, { |
| 87 | + type: "deleteMessageConfirm", |
| 88 | + messageTs: 3000, |
| 89 | + }) |
| 90 | + |
| 91 | + // Should delete only from index 4 (message 5), NOT from index 3 (message 4 at 2999) |
| 92 | + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([ |
| 93 | + mockTask.clineMessages[0], |
| 94 | + mockTask.clineMessages[1], |
| 95 | + mockTask.clineMessages[2], |
| 96 | + mockTask.clineMessages[3], // Message at 2999 should be preserved |
| 97 | + ]) |
| 98 | + |
| 99 | + expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ |
| 100 | + mockTask.apiConversationHistory[0], |
| 101 | + mockTask.apiConversationHistory[1], |
| 102 | + mockTask.apiConversationHistory[2], |
| 103 | + mockTask.apiConversationHistory[3], // API message at 2999 should be preserved |
| 104 | + ]) |
| 105 | + }) |
| 106 | + |
| 107 | + it("should handle case when message timestamp is not found", async () => { |
| 108 | + // Try to delete a message with non-existent timestamp |
| 109 | + await webviewMessageHandler(mockClineProvider, { |
| 110 | + type: "deleteMessage", |
| 111 | + value: 9999, |
| 112 | + }) |
| 113 | + |
| 114 | + await webviewMessageHandler(mockClineProvider, { |
| 115 | + type: "deleteMessageConfirm", |
| 116 | + messageTs: 9999, |
| 117 | + }) |
| 118 | + |
| 119 | + // Should not call overwrite methods since message wasn't found |
| 120 | + expect(mockTask.overwriteClineMessages).not.toHaveBeenCalled() |
| 121 | + expect(mockTask.overwriteApiConversationHistory).not.toHaveBeenCalled() |
| 122 | + expect(mockClineProvider.createTaskWithHistoryItem).not.toHaveBeenCalled() |
| 123 | + }) |
| 124 | + }) |
| 125 | + |
| 126 | + describe("editMessage with exact timestamp matching", () => { |
| 127 | + it("should edit only the message with exact timestamp match", async () => { |
| 128 | + // First, show the edit dialog |
| 129 | + await webviewMessageHandler(mockClineProvider, { |
| 130 | + type: "submitEditedMessage", |
| 131 | + value: 2000, |
| 132 | + editedMessageContent: "Edited message content", |
| 133 | + }) |
| 134 | + |
| 135 | + // Verify dialog was shown |
| 136 | + expect(mockClineProvider.postMessageToWebview).toHaveBeenCalledWith({ |
| 137 | + type: "showEditMessageDialog", |
| 138 | + messageTs: 2000, |
| 139 | + text: "Edited message content", |
| 140 | + images: undefined, |
| 141 | + }) |
| 142 | + |
| 143 | + // Now confirm the edit |
| 144 | + await webviewMessageHandler(mockClineProvider, { |
| 145 | + type: "editMessageConfirm", |
| 146 | + messageTs: 2000, |
| 147 | + text: "Edited message content", |
| 148 | + }) |
| 149 | + |
| 150 | + // Should delete from index 2 onwards before adding the edited message |
| 151 | + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([ |
| 152 | + mockTask.clineMessages[0], |
| 153 | + mockTask.clineMessages[1], |
| 154 | + ]) |
| 155 | + |
| 156 | + expect(mockTask.overwriteApiConversationHistory).toHaveBeenCalledWith([ |
| 157 | + mockTask.apiConversationHistory[0], |
| 158 | + mockTask.apiConversationHistory[1], |
| 159 | + ]) |
| 160 | + }) |
| 161 | + |
| 162 | + it("should not affect messages within 1 second buffer when editing", async () => { |
| 163 | + // Edit message at timestamp 3000 |
| 164 | + await webviewMessageHandler(mockClineProvider, { |
| 165 | + type: "submitEditedMessage", |
| 166 | + value: 3000, |
| 167 | + editedMessageContent: "Edited message at 3000", |
| 168 | + }) |
| 169 | + |
| 170 | + await webviewMessageHandler(mockClineProvider, { |
| 171 | + type: "editMessageConfirm", |
| 172 | + messageTs: 3000, |
| 173 | + text: "Edited message at 3000", |
| 174 | + }) |
| 175 | + |
| 176 | + // Should delete only from index 4, preserving message at 2999 |
| 177 | + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([ |
| 178 | + mockTask.clineMessages[0], |
| 179 | + mockTask.clineMessages[1], |
| 180 | + mockTask.clineMessages[2], |
| 181 | + mockTask.clineMessages[3], // Message at 2999 should be preserved |
| 182 | + ]) |
| 183 | + }) |
| 184 | + }) |
| 185 | + |
| 186 | + describe("edge cases", () => { |
| 187 | + it("should handle empty message arrays gracefully", async () => { |
| 188 | + mockTask.clineMessages = [] |
| 189 | + mockTask.apiConversationHistory = [] |
| 190 | + |
| 191 | + await webviewMessageHandler(mockClineProvider, { |
| 192 | + type: "deleteMessage", |
| 193 | + value: 1000, |
| 194 | + }) |
| 195 | + |
| 196 | + await webviewMessageHandler(mockClineProvider, { |
| 197 | + type: "deleteMessageConfirm", |
| 198 | + messageTs: 1000, |
| 199 | + }) |
| 200 | + |
| 201 | + // Should not throw errors and should not call overwrite methods |
| 202 | + expect(mockTask.overwriteClineMessages).not.toHaveBeenCalled() |
| 203 | + expect(mockTask.overwriteApiConversationHistory).not.toHaveBeenCalled() |
| 204 | + }) |
| 205 | + |
| 206 | + it("should handle messages with duplicate timestamps correctly", async () => { |
| 207 | + // Create messages with duplicate timestamps |
| 208 | + mockTask.clineMessages = [ |
| 209 | + { ts: 1000, type: "say", say: "user_feedback", text: "Message 1" }, |
| 210 | + { ts: 2000, type: "say", say: "user_feedback", text: "Message 2a" }, |
| 211 | + { ts: 2000, type: "say", say: "user_feedback", text: "Message 2b" }, // Duplicate timestamp |
| 212 | + { ts: 3000, type: "say", say: "user_feedback", text: "Message 3" }, |
| 213 | + ] |
| 214 | + |
| 215 | + await webviewMessageHandler(mockClineProvider, { |
| 216 | + type: "deleteMessage", |
| 217 | + value: 2000, |
| 218 | + }) |
| 219 | + |
| 220 | + await webviewMessageHandler(mockClineProvider, { |
| 221 | + type: "deleteMessageConfirm", |
| 222 | + messageTs: 2000, |
| 223 | + }) |
| 224 | + |
| 225 | + // Should delete from the first occurrence of timestamp 2000 |
| 226 | + expect(mockTask.overwriteClineMessages).toHaveBeenCalledWith([mockTask.clineMessages[0]]) |
| 227 | + }) |
| 228 | + }) |
| 229 | +}) |
0 commit comments