diff --git a/src/tests/grayStateRecovery.test.ts b/src/tests/grayStateRecovery.test.ts index d376b8eb15..457ecd7bcb 100644 --- a/src/tests/grayStateRecovery.test.ts +++ b/src/tests/grayStateRecovery.test.ts @@ -1,9 +1,9 @@ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' -import { Task } from '../core/task/Task' -import { ClineProvider } from '../core/webview/ClineProvider' +import { describe, it, expect, vi, beforeEach, afterEach } from "vitest" +import { Task } from "../core/task/Task" +import { ClineProvider } from "../core/webview/ClineProvider" // Mock dependencies -vi.mock('vscode', () => ({ +vi.mock("vscode", () => ({ window: { showErrorMessage: vi.fn(), showInformationMessage: vi.fn(), @@ -22,26 +22,26 @@ vi.mock('vscode', () => ({ EventEmitter: vi.fn(), })) -vi.mock('@anthropic-ai/sdk', () => ({ +vi.mock("@anthropic-ai/sdk", () => ({ Anthropic: vi.fn(), })) -vi.mock('delay', () => ({ +vi.mock("delay", () => ({ default: vi.fn(), })) -vi.mock('axios', () => ({ +vi.mock("axios", () => ({ default: { get: vi.fn(), post: vi.fn(), }, })) -vi.mock('p-wait-for', () => ({ +vi.mock("p-wait-for", () => ({ default: vi.fn(), })) -describe('Gray State Recovery', () => { +describe("Gray State Recovery", () => { let mockTask: Partial let mockProvider: Partial let mockWebview: any @@ -57,23 +57,22 @@ describe('Gray State Recovery', () => { // Mock task with gray state scenario mockTask = { - taskId: 'test-task-123', + taskId: "test-task-123", isStreaming: false, isPaused: false, - enableButtons: false, abort: false, resumePausedTask: vi.fn(), recursivelyMakeClineRequests: vi.fn(), + say: vi.fn(), } // Mock provider mockProvider = { - taskStack: [mockTask as Task], - currentTask: mockTask as Task, - webview: mockWebview, + getCurrentCline: vi.fn().mockReturnValue(mockTask as Task), postMessageToWebview: vi.fn(), finishSubTask: vi.fn(), - recoverFromGrayState: vi.fn(), + clearTask: vi.fn(), + postStateToWebview: vi.fn(), } }) @@ -81,160 +80,115 @@ describe('Gray State Recovery', () => { vi.restoreAllMocks() }) - describe('Task.resumePausedTask error recovery', () => { - it('should handle provider disconnection gracefully', async () => { - const mockError = new Error('Provider disconnected') + describe("Task.resumePausedTask error recovery", () => { + it("should handle provider disconnection gracefully", async () => { + const mockError = new Error("Provider disconnected") const resumeSpy = vi.fn().mockRejectedValue(mockError) mockTask.resumePausedTask = resumeSpy // Mock the actual implementation const task = mockTask as Task - task.resumePausedTask = async function() { + task.resumePausedTask = async function (lastMessage: string) { try { throw mockError } catch (error) { - console.warn('[Task] Failed to resume paused task, attempting recovery:', error) - + console.warn("[Task] Failed to resume paused task, attempting recovery:", error) + // Recovery mechanism: reset task state this.isStreaming = false this.isPaused = false - this.enableButtons = true - - // Add recovery message - const recoveryMessage = { - ts: Date.now(), - type: 'say' as const, - say: 'error' as const, - text: 'Task was interrupted but has been recovered. You can continue or start a new task.', - partial: false, + + // Add recovery message using the say method + try { + await this.say( + "error", + "Task was interrupted but has been recovered. You can continue or start a new task.", + ) + } catch (sayError) { + console.warn("[Task] Failed to add recovery message:", sayError) } - - // In real implementation, this would add to messages - console.log('[Task] Added recovery message:', recoveryMessage) - + return } } - await task.resumePausedTask() + await task.resumePausedTask("test message") expect(task.isStreaming).toBe(false) expect(task.isPaused).toBe(false) - expect(task.enableButtons).toBe(true) }) - it('should handle API failures during task restoration', async () => { - const mockApiError = new Error('API request failed') + it("should handle API failures during task restoration", async () => { + const mockApiError = new Error("API request failed") const recursiveSpy = vi.fn().mockRejectedValue(mockApiError) mockTask.recursivelyMakeClineRequests = recursiveSpy // Mock the actual implementation const task = mockTask as Task - task.recursivelyMakeClineRequests = async function() { + task.recursivelyMakeClineRequests = async function ( + userContent: any[], + includeFileDetails?: boolean, + ): Promise { try { throw mockApiError } catch (error) { - console.warn('[Task] API request failed during task restoration, attempting recovery:', error) - - // Recovery mechanism: enable user interaction + console.warn("[Task] API request failed during task restoration, attempting recovery:", error) + + // Recovery mechanism: reset streaming state this.isStreaming = false - this.enableButtons = true - - // Add recovery message - const recoveryMessage = { - ts: Date.now(), - type: 'say' as const, - say: 'error' as const, - text: 'Connection was lost but the task has been recovered. Please try your request again.', - partial: false, + + // Add recovery message using the say method + try { + await this.say( + "error", + "Connection was lost but the task has been recovered. Please try your request again.", + ) + } catch (sayError) { + console.warn("[Task] Failed to add recovery message:", sayError) } - - console.log('[Task] Added API recovery message:', recoveryMessage) - return + + return false } } - await task.recursivelyMakeClineRequests() + const result = await task.recursivelyMakeClineRequests([{ type: "text", text: "test" }]) expect(task.isStreaming).toBe(false) - expect(task.enableButtons).toBe(true) + expect(result).toBe(false) }) }) - describe('ClineProvider.finishSubTask error recovery', () => { - it('should handle subtask completion failures gracefully', async () => { - const mockError = new Error('Subtask completion failed') - const finishSpy = vi.fn().mockRejectedValue(mockError) - - // Mock the actual implementation + describe("ClineProvider.finishSubTask error recovery", () => { + it("should handle subtask completion failures gracefully", async () => { + const mockError = new Error("Subtask completion failed") + + // Mock the actual implementation to simulate the recovery behavior const provider = mockProvider as ClineProvider - provider.finishSubTask = async function() { + provider.finishSubTask = async function (lastMessage: string) { try { + // Simulate the normal flow that would fail + await this.getCurrentCline()?.resumePausedTask(lastMessage) throw mockError } catch (error) { - console.warn('[ClineProvider] Failed to finish subtask, attempting recovery:', error) - - // Recovery mechanism: attempt to recover from gray state - await this.recoverFromGrayState?.() - + console.warn("[ClineProvider] Failed to finish subtask, attempting recovery:", error) + + // Simulate recovery by calling clearTask (which is public) + await this.clearTask() + return } } - provider.recoverFromGrayState = async function() { - console.log('[ClineProvider] Attempting gray state recovery...') - - const currentTask = this.currentTask - if (currentTask) { - // Strategy 1: Force task resume - try { - currentTask.isStreaming = false - currentTask.enableButtons = true - currentTask.isPaused = false - - console.log('[ClineProvider] Gray state recovery: Reset task state') - - // Strategy 2: Refresh UI state - this.postMessageToWebview?.({ - type: 'state', - state: { - task: currentTask, - enableButtons: true, - isStreaming: false, - } - }) - - console.log('[ClineProvider] Gray state recovery: Refreshed UI state') - - } catch (recoveryError) { - console.error('[ClineProvider] Gray state recovery failed:', recoveryError) - - // Strategy 3: Clear task as last resort - this.taskStack = [] - this.currentTask = undefined - - this.postMessageToWebview?.({ - type: 'state', - state: { - task: undefined, - enableButtons: true, - isStreaming: false, - } - }) - - console.log('[ClineProvider] Gray state recovery: Cleared task as last resort') - } - } - } + // Test that finishSubTask handles errors gracefully + await provider.finishSubTask("test message") - await provider.finishSubTask() - - expect(provider.recoverFromGrayState).toBeDefined() + // Verify that clearTask was called (recovery mechanism) + expect(provider.clearTask).toHaveBeenCalled() }) }) - describe('Gray state detection', () => { - it('should detect gray state conditions correctly', () => { + describe("Gray state detection", () => { + it("should detect gray state conditions correctly", () => { // Simulate gray state: task exists, not streaming, buttons disabled const hasTask = !!mockTask const hasMessages = true @@ -247,7 +201,7 @@ describe('Gray State Recovery', () => { expect(isInGrayState).toBe(true) }) - it('should not detect gray state when buttons are enabled', () => { + it("should not detect gray state when buttons are enabled", () => { const hasTask = !!mockTask const hasMessages = true const isStreaming = false @@ -259,7 +213,7 @@ describe('Gray State Recovery', () => { expect(isInGrayState).toBe(false) }) - it('should not detect gray state when streaming', () => { + it("should not detect gray state when streaming", () => { const hasTask = !!mockTask const hasMessages = true const isStreaming = true // Currently streaming @@ -271,12 +225,12 @@ describe('Gray State Recovery', () => { expect(isInGrayState).toBe(false) }) - it('should not detect gray state when there is an active ask', () => { + it("should not detect gray state when there is an active ask", () => { const hasTask = !!mockTask const hasMessages = true const isStreaming = false const enableButtons = false - const clineAsk = { type: 'tool', tool: 'test' } // Active ask + const clineAsk = { type: "tool", tool: "test" } // Active ask const isInGrayState = hasTask && hasMessages && !isStreaming && !enableButtons && !clineAsk @@ -284,51 +238,38 @@ describe('Gray State Recovery', () => { }) }) - describe('Recovery strategies', () => { - it('should attempt multiple recovery strategies in order', async () => { + describe("Recovery strategies", () => { + it("should attempt multiple recovery strategies in order", async () => { const provider = mockProvider as ClineProvider const strategies: string[] = [] - provider.recoverFromGrayState = async function() { - const currentTask = this.currentTask + // Mock finishSubTask to simulate recovery strategies + provider.finishSubTask = async function (lastMessage: string) { + const currentTask = this.getCurrentCline() if (currentTask) { // Strategy 1: Force task resume try { - strategies.push('force_resume') + strategies.push("force_resume") currentTask.isStreaming = false - currentTask.enableButtons = true currentTask.isPaused = false - + // Strategy 2: Add recovery message - strategies.push('add_recovery_message') - + strategies.push("add_recovery_message") + // Strategy 3: Refresh UI state - strategies.push('refresh_ui') - this.postMessageToWebview?.({ - type: 'state', - state: { - task: currentTask, - enableButtons: true, - isStreaming: false, - } - }) - + strategies.push("refresh_ui") + await this.postStateToWebview() } catch (recoveryError) { // Strategy 4: Clear task as last resort - strategies.push('clear_task') - this.taskStack = [] - this.currentTask = undefined + strategies.push("clear_task") + await this.clearTask() } } } - await provider.recoverFromGrayState?.() + await provider.finishSubTask("test message") - expect(strategies).toEqual([ - 'force_resume', - 'add_recovery_message', - 'refresh_ui' - ]) + expect(strategies).toEqual(["force_resume", "add_recovery_message", "refresh_ui"]) }) }) -}) \ No newline at end of file +})