diff --git a/src/utils/__tests__/migrateSettings.spec.ts b/src/utils/__tests__/migrateSettings.spec.ts index 924d249986..215b7b4593 100644 --- a/src/utils/__tests__/migrateSettings.spec.ts +++ b/src/utils/__tests__/migrateSettings.spec.ts @@ -4,6 +4,7 @@ import type { ContextProxy } from "../../core/config/ContextProxy" import type { HistoryItem } from "@roo-code/types" describe("migrateTaskHistoryWithContextProxy", () => { + let mockContext: any let mockContextProxy: any let mockWorkspaceFolder: any @@ -19,7 +20,7 @@ describe("migrateTaskHistoryWithContextProxy", () => { } // Mock VSCode context - const mockContext = { + mockContext = { globalState: { get: vi.fn(), update: vi.fn(), @@ -37,7 +38,6 @@ describe("migrateTaskHistoryWithContextProxy", () => { updateWorkspaceState: vi.fn(), getWorkspaceState: vi.fn(), getWorkspaceSettings: vi.fn(), - context: mockContext, } as any }) @@ -83,16 +83,19 @@ describe("migrateTaskHistoryWithContextProxy", () => { ] // Mock context.globalState.get to return the raw state with taskHistory - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string, defaultValue?: any) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string, defaultValue?: any) => { if (key === "globalSettings") { return { taskHistory: mockTaskHistory } } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } return defaultValue }) vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({}) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert // Should update workspace state with only tasks from current workspace @@ -102,45 +105,51 @@ describe("migrateTaskHistoryWithContextProxy", () => { ]) // Should update global state to keep only tasks from other workspaces - expect(mockContextProxy.context.globalState.update).toHaveBeenCalledWith("globalSettings", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("globalSettings", { taskHistory: [mockTaskHistory[2]], }) }) it("should handle empty task history in global state", async () => { // Arrange - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { if (key === "globalSettings") { return { taskHistory: [] } } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } return undefined }) vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({}) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() - expect(mockContextProxy.context.globalState.update).not.toHaveBeenCalled() + expect(mockContext.globalState.update).not.toHaveBeenCalled() }) it("should handle undefined task history in global state", async () => { // Arrange - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { if (key === "globalSettings") { return {} } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } return undefined }) vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({}) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() - expect(mockContextProxy.context.globalState.update).not.toHaveBeenCalled() + expect(mockContext.globalState.update).not.toHaveBeenCalled() }) it("should merge with existing workspace task history", async () => { @@ -171,16 +180,19 @@ describe("migrateTaskHistoryWithContextProxy", () => { workspace: "/test/workspace", } - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { if (key === "globalSettings") { return { taskHistory: [globalTask] } } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } return undefined }) vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({ taskHistory: [existingWorkspaceTask] }) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert // Should merge tasks, keeping both existing and migrated @@ -190,7 +202,7 @@ describe("migrateTaskHistoryWithContextProxy", () => { ]) // Should clear the migrated task from global state - expect(mockContextProxy.context.globalState.update).toHaveBeenCalledWith("globalSettings", {}) + expect(mockContext.globalState.update).toHaveBeenCalledWith("globalSettings", {}) }) it("should handle tasks without workspacePath", async () => { @@ -208,29 +220,89 @@ describe("migrateTaskHistoryWithContextProxy", () => { // No workspace property } as HistoryItem - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { if (key === "globalSettings") { return { taskHistory: [taskWithoutPath] } } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } return undefined }) vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({}) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert // Should not migrate tasks without workspace path expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() // Should keep the task in global state - expect(mockContextProxy.context.globalState.update).toHaveBeenCalledWith("globalSettings", { + expect(mockContext.globalState.update).toHaveBeenCalledWith("globalSettings", { taskHistory: [taskWithoutPath], }) }) it("should handle no workspace folder", async () => { // Arrange - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation((key: string) => { + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { + if (key === "globalSettings") { + return { + taskHistory: [ + { + id: "task1", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 50, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.01, + workspace: "/test/workspace", + }, + ], + } + } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } + return undefined + }) + + // Act + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, undefined) + + // Assert + // Should not perform any migration without a workspace + expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() + expect(mockContext.globalState.update).not.toHaveBeenCalled() + }) + + it("should handle errors gracefully", async () => { + // Arrange + const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) + vi.mocked(mockContext.globalState.get).mockImplementation(() => { + throw new Error("Failed to get global state") + }) + + // Act + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) + + // Assert + expect(consoleSpy).toHaveBeenCalledWith("Failed to migrate task history to workspace:", expect.any(Error)) + expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() + expect(mockContext.globalState.update).not.toHaveBeenCalled() + + consoleSpy.mockRestore() + }) + + it("should skip migration if already migrated", async () => { + // Arrange + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { + if (key === "taskHistoryMigratedToWorkspace") { + return true // Already migrated + } if (key === "globalSettings") { return { taskHistory: [ @@ -253,29 +325,47 @@ describe("migrateTaskHistoryWithContextProxy", () => { }) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, undefined) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert - // Should not perform any migration without a workspace + // Should not perform any migration operations expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() - expect(mockContextProxy.context.globalState.update).not.toHaveBeenCalled() + expect(mockContext.globalState.update).not.toHaveBeenCalled() }) - it("should handle errors gracefully", async () => { + it("should set migration flag after successful migration", async () => { // Arrange - const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}) - vi.mocked(mockContextProxy.context.globalState.get).mockImplementation(() => { - throw new Error("Failed to get global state") + const mockTaskHistory: HistoryItem[] = [ + { + id: "task1", + number: 1, + ts: Date.now(), + task: "Test task", + tokensIn: 100, + tokensOut: 50, + cacheWrites: 0, + cacheReads: 0, + totalCost: 0.01, + workspace: "/test/workspace", + }, + ] + + vi.mocked(mockContext.globalState.get).mockImplementation((key: string) => { + if (key === "globalSettings") { + return { taskHistory: mockTaskHistory } + } + if (key === "taskHistoryMigratedToWorkspace") { + return false + } + return undefined }) + vi.mocked(mockContextProxy.getWorkspaceSettings).mockReturnValue({}) // Act - await migrateTaskHistoryWithContextProxy(mockContextProxy, mockWorkspaceFolder) + await migrateTaskHistoryWithContextProxy(mockContext, mockContextProxy, mockWorkspaceFolder) // Assert - expect(consoleSpy).toHaveBeenCalledWith("Failed to migrate task history to workspace:", expect.any(Error)) - expect(mockContextProxy.updateWorkspaceState).not.toHaveBeenCalled() - expect(mockContextProxy.context.globalState.update).not.toHaveBeenCalled() - - consoleSpy.mockRestore() + // Should set the migration flag + expect(mockContext.globalState.update).toHaveBeenCalledWith("taskHistoryMigratedToWorkspace", true) }) }) diff --git a/src/utils/migrateSettings.ts b/src/utils/migrateSettings.ts index 7f0cab8dfd..389fdbc986 100644 --- a/src/utils/migrateSettings.ts +++ b/src/utils/migrateSettings.ts @@ -134,16 +134,15 @@ export async function migrateTaskHistoryWithContextProxy( contextProxy: ContextProxy, workspaceFolder: vscode.WorkspaceFolder | undefined, ): Promise { - const alreadyMigrated = context.globalState.get(TASK_HISTORY_MIGRATION_KEY) - if (alreadyMigrated) { - return - } - - if (!workspaceFolder) { - return - } - try { + const alreadyMigrated = context.globalState.get(TASK_HISTORY_MIGRATION_KEY) + if (alreadyMigrated) { + return + } + + if (!workspaceFolder) { + return + } // Get the raw global state directly from context const rawGlobalState = context.globalState.get("globalSettings", {}) const taskHistory = rawGlobalState.taskHistory as HistoryItem[] | undefined