| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- import type { Mock } from "vitest"
- import { describe, it, expect, vi, beforeEach } from "vitest"
- // Mock dependencies first
- vi.mock("vscode", () => ({
- window: {
- showWarningMessage: vi.fn(),
- showErrorMessage: vi.fn(),
- },
- workspace: {
- workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }],
- getConfiguration: vi.fn().mockReturnValue({
- get: vi.fn(),
- update: vi.fn(),
- }),
- },
- Uri: {
- file: vi.fn((path) => ({ fsPath: path })),
- },
- env: {
- uriScheme: "vscode",
- },
- }))
- vi.mock("../../task-persistence", () => ({
- saveTaskMessages: vi.fn(),
- }))
- vi.mock("../../../api/providers/fetchers/modelCache", () => ({
- getModels: vi.fn(),
- flushModels: vi.fn(),
- }))
- vi.mock("../checkpointRestoreHandler", () => ({
- handleCheckpointRestoreOperation: vi.fn(),
- }))
- // Import after mocks
- import { webviewMessageHandler } from "../webviewMessageHandler"
- import type { ClineProvider } from "../ClineProvider"
- import type { ClineMessage } from "@roo-code/types"
- import type { ApiMessage } from "../../task-persistence/apiMessages"
- import { MessageManager } from "../../message-manager"
- describe("webviewMessageHandler - Edit Message with Timestamp Fallback", () => {
- let mockClineProvider: ClineProvider
- let mockCurrentTask: any
- beforeEach(() => {
- vi.clearAllMocks()
- // Create a mock task with messages
- mockCurrentTask = {
- taskId: "test-task-id",
- clineMessages: [] as ClineMessage[],
- apiConversationHistory: [] as ApiMessage[],
- overwriteClineMessages: vi.fn(),
- overwriteApiConversationHistory: vi.fn(),
- handleWebviewAskResponse: vi.fn(),
- }
- mockCurrentTask.messageManager = new MessageManager(mockCurrentTask)
- // Create mock provider
- mockClineProvider = {
- getCurrentTask: vi.fn().mockReturnValue(mockCurrentTask),
- postMessageToWebview: vi.fn(),
- contextProxy: {
- getValue: vi.fn(),
- setValue: vi.fn(),
- globalStorageUri: { fsPath: "/mock/storage" },
- },
- log: vi.fn(),
- } as unknown as ClineProvider
- })
- it("should not modify API history when apiConversationHistoryIndex is -1", async () => {
- // Setup: User message followed by attempt_completion
- const userMessageTs = 1000
- const assistantMessageTs = 2000
- const completionMessageTs = 3000
- // UI messages (clineMessages)
- mockCurrentTask.clineMessages = [
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Hello",
- } as ClineMessage,
- {
- ts: completionMessageTs,
- type: "say",
- say: "completion_result",
- text: "Task Completed!",
- } as ClineMessage,
- ]
- // API conversation history - note the user message is missing (common scenario after condense)
- mockCurrentTask.apiConversationHistory = [
- {
- ts: assistantMessageTs,
- role: "assistant",
- content: [
- {
- type: "text",
- text: "I'll help you with that.",
- },
- ],
- },
- {
- ts: completionMessageTs,
- role: "assistant",
- content: [
- {
- type: "tool_use",
- name: "attempt_completion",
- id: "tool-1",
- input: {
- result: "Task Completed!",
- },
- },
- ],
- },
- ] as ApiMessage[]
- // Trigger edit confirmation
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Hello World", // edited content
- restoreCheckpoint: false,
- })
- // Verify that UI messages were truncated at the correct index
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith(
- [], // All messages before index 0 (empty array)
- )
- // API history should be truncated from first message at/after edited timestamp (fallback)
- expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
- })
- it("should preserve messages before the edited message when message not in API history", async () => {
- const earlierMessageTs = 500
- const userMessageTs = 1000
- const assistantMessageTs = 2000
- // UI messages
- mockCurrentTask.clineMessages = [
- {
- ts: earlierMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Earlier message",
- } as ClineMessage,
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Hello",
- } as ClineMessage,
- {
- ts: assistantMessageTs,
- type: "say",
- say: "text",
- text: "Response",
- } as ClineMessage,
- ]
- // API history - missing the exact user message at ts=1000
- mockCurrentTask.apiConversationHistory = [
- {
- ts: earlierMessageTs,
- role: "user",
- content: [{ type: "text", text: "Earlier message" }],
- },
- {
- ts: assistantMessageTs,
- role: "assistant",
- content: [{ type: "text", text: "Response" }],
- },
- ] as ApiMessage[]
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Hello World",
- restoreCheckpoint: false,
- })
- // Verify UI messages were truncated to preserve earlier message
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([
- {
- ts: earlierMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Earlier message",
- },
- ])
- // API history should be truncated from the first API message at/after the edited timestamp (fallback)
- expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([
- {
- ts: earlierMessageTs,
- role: "user",
- content: [{ type: "text", text: "Earlier message" }],
- },
- ])
- })
- it("should not use fallback when exact apiConversationHistoryIndex is found", async () => {
- const userMessageTs = 1000
- const assistantMessageTs = 2000
- // Both UI and API have the message at the same timestamp
- mockCurrentTask.clineMessages = [
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Hello",
- } as ClineMessage,
- {
- ts: assistantMessageTs,
- type: "say",
- say: "text",
- text: "Response",
- } as ClineMessage,
- ]
- mockCurrentTask.apiConversationHistory = [
- {
- ts: userMessageTs,
- role: "user",
- content: [{ type: "text", text: "Hello" }],
- },
- {
- ts: assistantMessageTs,
- role: "assistant",
- content: [{ type: "text", text: "Response" }],
- },
- ] as ApiMessage[]
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Hello World",
- restoreCheckpoint: false,
- })
- // Both should be truncated at index 0
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
- expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
- })
- it("should handle case where no API messages match timestamp criteria", async () => {
- const userMessageTs = 3000
- mockCurrentTask.clineMessages = [
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Hello",
- } as ClineMessage,
- ]
- // All API messages have timestamps before the edited message
- mockCurrentTask.apiConversationHistory = [
- {
- ts: 1000,
- role: "assistant",
- content: [{ type: "text", text: "Old message 1" }],
- },
- {
- ts: 2000,
- role: "assistant",
- content: [{ type: "text", text: "Old message 2" }],
- },
- ] as ApiMessage[]
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Hello World",
- restoreCheckpoint: false,
- })
- // UI messages truncated
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
- // API history should not be modified when no API messages meet the timestamp criteria
- expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
- })
- it("should handle empty API conversation history gracefully", async () => {
- const userMessageTs = 1000
- mockCurrentTask.clineMessages = [
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Hello",
- } as ClineMessage,
- ]
- mockCurrentTask.apiConversationHistory = []
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Hello World",
- restoreCheckpoint: false,
- })
- // UI messages should be truncated
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
- // API history should not be modified when message not found
- expect(mockCurrentTask.overwriteApiConversationHistory).not.toHaveBeenCalled()
- })
- it("should correctly handle attempt_completion in API history", async () => {
- const userMessageTs = 1000
- const completionTs = 2000
- const feedbackTs = 3000
- mockCurrentTask.clineMessages = [
- {
- ts: userMessageTs,
- type: "say",
- say: "user_feedback",
- text: "Do something",
- } as ClineMessage,
- {
- ts: completionTs,
- type: "say",
- say: "completion_result",
- text: "Task Completed!",
- } as ClineMessage,
- {
- ts: feedbackTs,
- type: "say",
- say: "user_feedback",
- text: "Thanks",
- } as ClineMessage,
- ]
- // API history with attempt_completion tool use (user message missing)
- mockCurrentTask.apiConversationHistory = [
- {
- ts: completionTs,
- role: "assistant",
- content: [
- {
- type: "tool_use",
- name: "attempt_completion",
- id: "tool-1",
- input: {
- result: "Task Completed!",
- },
- },
- ],
- },
- {
- ts: feedbackTs,
- role: "user",
- content: [
- {
- type: "text",
- text: "Thanks",
- },
- ],
- },
- ] as ApiMessage[]
- // Edit the first user message
- await webviewMessageHandler(mockClineProvider, {
- type: "editMessageConfirm",
- messageTs: userMessageTs,
- text: "Do something else",
- restoreCheckpoint: false,
- })
- // UI messages truncated at edited message
- expect(mockCurrentTask.overwriteClineMessages).toHaveBeenCalledWith([])
- // API history should be truncated from first message at/after edited timestamp (fallback)
- expect(mockCurrentTask.overwriteApiConversationHistory).toHaveBeenCalledWith([])
- })
- })
|