diff --git a/.github/workflows/code-qa.yml b/.github/workflows/code-qa.yml index f8ac0c8642b..1592b15669b 100644 --- a/.github/workflows/code-qa.yml +++ b/.github/workflows/code-qa.yml @@ -58,66 +58,3 @@ jobs: uses: ./.github/actions/setup-node-pnpm - name: Run unit tests run: pnpm test - - check-openrouter-api-key: - runs-on: ubuntu-latest - outputs: - exists: ${{ steps.openrouter-api-key-check.outputs.defined }} - steps: - - name: Check if OpenRouter API key exists - id: openrouter-api-key-check - shell: bash - run: | - if [ "${{ secrets.OPENROUTER_API_KEY }}" != '' ]; then - echo "defined=true" >> $GITHUB_OUTPUT; - else - echo "defined=false" >> $GITHUB_OUTPUT; - fi - - integration-test: - runs-on: ubuntu-latest - needs: [check-openrouter-api-key] - if: needs.check-openrouter-api-key.outputs.exists == 'true' - steps: - - name: Checkout code - uses: actions/checkout@v4 - - name: Setup Node.js and pnpm - uses: ./.github/actions/setup-node-pnpm - - name: Create .env.local file - working-directory: apps/vscode-e2e - run: echo "OPENROUTER_API_KEY=${{ secrets.OPENROUTER_API_KEY }}" > .env.local - - name: Set VS Code test version - run: echo "VSCODE_VERSION=1.101.2" >> $GITHUB_ENV - - name: Cache VS Code test runtime - uses: actions/cache@v4 - with: - path: apps/vscode-e2e/.vscode-test - key: ${{ runner.os }}-vscode-test-${{ env.VSCODE_VERSION }} - - name: Pre-download VS Code test runtime with retry - working-directory: apps/vscode-e2e - run: | - for attempt in 1 2 3; do - echo "Download attempt $attempt of 3..." - node -e " - const { downloadAndUnzipVSCode } = require('@vscode/test-electron'); - downloadAndUnzipVSCode({ version: process.env.VSCODE_VERSION || '1.101.2' }) - .then(() => { - console.log('✅ VS Code test runtime downloaded successfully'); - process.exit(0); - }) - .catch(err => { - console.error('❌ Failed to download VS Code (attempt $attempt):', err); - process.exit(1); - }); - " && break || { - if [ $attempt -eq 3 ]; then - echo "All download attempts failed" - exit 1 - fi - echo "Retrying in 5 seconds..." - sleep 5 - } - done - - name: Run integration tests - working-directory: apps/vscode-e2e - run: xvfb-run -a pnpm test:ci diff --git a/packages/cloud/src/__tests__/CloudService.integration.test.ts b/packages/cloud/src/__tests__/CloudService.integration.test.ts deleted file mode 100644 index 2896b43554b..00000000000 --- a/packages/cloud/src/__tests__/CloudService.integration.test.ts +++ /dev/null @@ -1,147 +0,0 @@ -// npx vitest run src/__tests__/CloudService.integration.test.ts - -import type { ExtensionContext } from "vscode" - -import { CloudService } from "../CloudService.js" -import { StaticSettingsService } from "../StaticSettingsService.js" -import { CloudSettingsService } from "../CloudSettingsService.js" - -vi.mock("vscode", () => ({ - ExtensionContext: vi.fn(), - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - env: { - openExternal: vi.fn(), - }, - Uri: { - parse: vi.fn(), - }, -})) - -describe("CloudService Integration - Settings Service Selection", () => { - let mockContext: ExtensionContext - - beforeEach(() => { - CloudService.resetInstance() - - mockContext = { - subscriptions: [], - workspaceState: { - get: vi.fn(), - update: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - secrets: { - get: vi.fn(), - store: vi.fn(), - delete: vi.fn(), - onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }), - }, - globalState: { - get: vi.fn(), - update: vi.fn(), - setKeysForSync: vi.fn(), - keys: vi.fn().mockReturnValue([]), - }, - extensionUri: { scheme: "file", path: "/mock/path" }, - extensionPath: "/mock/path", - extensionMode: 1, - asAbsolutePath: vi.fn((relativePath: string) => `/mock/path/${relativePath}`), - storageUri: { scheme: "file", path: "/mock/storage" }, - extension: { - packageJSON: { - version: "1.0.0", - }, - }, - } as unknown as ExtensionContext - }) - - afterEach(() => { - CloudService.resetInstance() - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - }) - - it("should use CloudSettingsService when no environment variable is set", async () => { - // Ensure no environment variables are set - delete process.env.ROO_CODE_CLOUD_ORG_SETTINGS - delete process.env.ROO_CODE_CLOUD_TOKEN - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(CloudSettingsService) - }) - - it("should use StaticSettingsService when ROO_CODE_CLOUD_ORG_SETTINGS is set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set the environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Access the private settingsService to check its type - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Verify the settings are correctly loaded - expect(cloudService.getAllowList()).toEqual(validSettings.allowList) - }) - - it("should throw error when ROO_CODE_CLOUD_ORG_SETTINGS contains invalid data", async () => { - // Set invalid environment variable - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = "invalid-base64-data" - - await expect(CloudService.createInstance(mockContext)).rejects.toThrow("Failed to initialize CloudService") - }) - - it("should prioritize static token auth when both environment variables are set", async () => { - const validSettings = { - version: 1, - cloudSettings: { - recordTaskMessages: true, - enableTaskSharing: true, - taskShareExpirationDays: 30, - }, - defaultSettings: { - enableCheckpoints: true, - }, - allowList: { - allowAll: true, - providers: {}, - }, - } - - // Set both environment variables - process.env.ROO_CODE_CLOUD_TOKEN = "test-token" - process.env.ROO_CODE_CLOUD_ORG_SETTINGS = Buffer.from(JSON.stringify(validSettings)).toString("base64") - - const cloudService = await CloudService.createInstance(mockContext) - - // Should use StaticSettingsService for settings - const settingsService = (cloudService as unknown as { settingsService: unknown }).settingsService - expect(settingsService).toBeInstanceOf(StaticSettingsService) - - // Should use StaticTokenAuthService for auth (from the existing logic) - expect(cloudService.isAuthenticated()).toBe(true) - expect(cloudService.hasActiveSession()).toBe(true) - }) -}) diff --git a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts b/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts deleted file mode 100644 index bd13439ea78..00000000000 --- a/src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts +++ /dev/null @@ -1,406 +0,0 @@ -// Integration tests for command execution timeout functionality -// npx vitest run src/core/tools/__tests__/executeCommandTimeout.integration.spec.ts - -import * as vscode from "vscode" -import * as fs from "fs/promises" -import { executeCommandInTerminal, executeCommandTool, ExecuteCommandOptions } from "../ExecuteCommandTool" -import { Task } from "../../task/Task" -import { TerminalRegistry } from "../../../integrations/terminal/TerminalRegistry" - -// Mock dependencies -vitest.mock("vscode", () => ({ - workspace: { - getConfiguration: vitest.fn(), - }, -})) - -vitest.mock("fs/promises") -vitest.mock("../../../integrations/terminal/TerminalRegistry") -vitest.mock("../../task/Task") -vitest.mock("../../prompts/responses", () => ({ - formatResponse: { - toolError: vitest.fn((msg) => `Tool Error: ${msg}`), - rooIgnoreError: vitest.fn((msg) => `RooIgnore Error: ${msg}`), - }, -})) -vitest.mock("../../../utils/text-normalization", () => ({ - unescapeHtmlEntities: vitest.fn((text) => text), -})) -vitest.mock("../../../shared/package", () => ({ - Package: { - name: "roo-cline", - }, -})) - -describe("Command Execution Timeout Integration", () => { - let mockTask: any - let mockTerminal: any - let mockProcess: any - - beforeEach(() => { - vitest.clearAllMocks() - - // Mock fs.access to resolve successfully for working directory - ;(fs.access as any).mockResolvedValue(undefined) - - // Mock task - mockTask = { - cwd: "/test/directory", - terminalProcess: undefined, - providerRef: { - deref: vitest.fn().mockResolvedValue({ - postMessageToWebview: vitest.fn(), - }), - }, - say: vitest.fn().mockResolvedValue(undefined), - } - - // Mock terminal process - mockProcess = { - abort: vitest.fn(), - then: vitest.fn(), - catch: vitest.fn(), - } - - // Mock terminal - mockTerminal = { - runCommand: vitest.fn().mockReturnValue(mockProcess), - getCurrentWorkingDirectory: vitest.fn().mockReturnValue("/test/directory"), - } - - // Mock TerminalRegistry - ;(TerminalRegistry.getOrCreateTerminal as any).mockResolvedValue(mockTerminal) - - // Mock VSCode configuration - const mockGetConfiguration = vitest.fn().mockReturnValue({ - get: vitest.fn().mockReturnValue(0), // Default 0 (no timeout) - }) - ;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration()) - }) - - it("should pass timeout configuration to executeCommand", async () => { - const customTimeoutMs = 15000 // 15 seconds in milliseconds - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "echo test", - commandExecutionTimeout: customTimeoutMs, - } - - // Mock a quick-completing process - const quickProcess = Promise.resolve() - mockTerminal.runCommand.mockReturnValue(quickProcess) - - await executeCommandInTerminal(mockTask as Task, options) - - // Verify that the terminal was called with the command - expect(mockTerminal.runCommand).toHaveBeenCalledWith("echo test", expect.any(Object)) - }) - - it("should handle timeout scenario", async () => { - const shortTimeoutMs = 100 // Very short timeout in milliseconds - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "sleep 10", - commandExecutionTimeout: shortTimeoutMs, - } - - // Create a process that never resolves but has an abort method - const longRunningProcess = new Promise(() => { - // Never resolves to simulate a hanging command - }) - - // Add abort method to the promise - ;(longRunningProcess as any).abort = vitest.fn() - - mockTerminal.runCommand.mockReturnValue(longRunningProcess) - - // Execute with timeout - const result = await executeCommandInTerminal(mockTask as Task, options) - - // Should return timeout error - expect(result[0]).toBe(false) // Not rejected by user - expect(result[1]).toContain("terminated after exceeding") - expect(result[1]).toContain("0.1s") // Should show seconds in error message - }, 10000) // Increase test timeout to 10 seconds - - it("should abort process on timeout", async () => { - const shortTimeoutMs = 50 // Short timeout in milliseconds - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "sleep 10", - commandExecutionTimeout: shortTimeoutMs, - } - - // Create a process that can be aborted - const abortSpy = vitest.fn() - - // Mock the process to never resolve but be abortable - const neverResolvingPromise = new Promise(() => {}) - ;(neverResolvingPromise as any).abort = abortSpy - - mockTerminal.runCommand.mockReturnValue(neverResolvingPromise) - - await executeCommandInTerminal(mockTask as Task, options) - - // Verify abort was called - expect(abortSpy).toHaveBeenCalled() - }, 5000) // Increase test timeout to 5 seconds - - it("should clean up timeout on successful completion", async () => { - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "echo test", - commandExecutionTimeout: 5000, - } - - // Mock a process that completes quickly - const quickProcess = Promise.resolve() - mockTerminal.runCommand.mockReturnValue(quickProcess) - - const result = await executeCommandInTerminal(mockTask as Task, options) - - // Should complete successfully without timeout - expect(result[0]).toBe(false) // Not rejected - expect(result[1]).not.toContain("terminated after exceeding") - }) - - it("should use default timeout when not specified (0 = no timeout)", async () => { - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "echo test", - // commandExecutionTimeout not specified, should use default (0) - } - - const quickProcess = Promise.resolve() - mockTerminal.runCommand.mockReturnValue(quickProcess) - - await executeCommandInTerminal(mockTask as Task, options) - - // Should complete without issues using default (no timeout) - expect(mockTerminal.runCommand).toHaveBeenCalled() - }) - - it("should not timeout when commandExecutionTimeout is 0", async () => { - const options: ExecuteCommandOptions = { - executionId: "test-execution", - command: "sleep 10", - commandExecutionTimeout: 0, // No timeout - } - - // Create a process that resolves after a delay to simulate a long-running command - const longRunningProcess = new Promise((resolve) => { - setTimeout(resolve, 200) // 200ms delay - }) - - mockTerminal.runCommand.mockReturnValue(longRunningProcess) - - const result = await executeCommandInTerminal(mockTask as Task, options) - - // Should complete successfully without timeout - expect(result[0]).toBe(false) // Not rejected - expect(result[1]).not.toContain("terminated after exceeding") - }) - - describe("Command Timeout Allowlist", () => { - let mockBlock: any - let mockAskApproval: any - let mockHandleError: any - let mockPushToolResult: any - - beforeEach(() => { - // Reset mocks for allowlist tests - vitest.clearAllMocks() - ;(fs.access as any).mockResolvedValue(undefined) - ;(TerminalRegistry.getOrCreateTerminal as any).mockResolvedValue(mockTerminal) - - // Mock the executeCommandTool parameters - mockBlock = { - type: "tool_use", - name: "execute_command", - params: { - command: "", - cwd: undefined, - }, - nativeArgs: { - command: "", - cwd: undefined, - }, - partial: false, - } - - mockAskApproval = vitest.fn().mockResolvedValue(true) // Always approve - mockHandleError = vitest.fn() - mockPushToolResult = vitest.fn() - - // Mock task with additional properties needed by executeCommandTool - mockTask = { - cwd: "/test/directory", - terminalProcess: undefined, - providerRef: { - deref: vitest.fn().mockResolvedValue({ - postMessageToWebview: vitest.fn(), - getState: vitest.fn().mockResolvedValue({ - terminalOutputLineLimit: 500, - terminalShellIntegrationDisabled: false, - }), - }), - }, - say: vitest.fn().mockResolvedValue(undefined), - consecutiveMistakeCount: 0, - recordToolError: vitest.fn(), - sayAndCreateMissingParamError: vitest.fn(), - rooIgnoreController: { - validateCommand: vitest.fn().mockReturnValue(null), - }, - lastMessageTs: Date.now(), - ask: vitest.fn(), - didRejectTool: false, - } - }) - - it("should skip timeout for commands in allowlist", async () => { - // Mock VSCode configuration with timeout and allowlist - const mockGetConfiguration = vitest.fn().mockReturnValue({ - get: vitest.fn().mockImplementation((key: string) => { - if (key === "commandExecutionTimeout") return 1 // 1 second timeout - if (key === "commandTimeoutAllowlist") return ["npm", "git"] - return undefined - }), - }) - ;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration()) - - mockBlock.params.command = "npm install" - mockBlock.nativeArgs.command = "npm install" - - // Create a process that would timeout if not allowlisted - const longRunningProcess = new Promise((resolve) => { - setTimeout(resolve, 2000) // 2 seconds, longer than 1 second timeout - }) - mockTerminal.runCommand.mockReturnValue(longRunningProcess) - - await executeCommandTool.handle(mockTask as Task, mockBlock, { - askApproval: mockAskApproval, - handleError: mockHandleError, - pushToolResult: mockPushToolResult, - }) - - // Should complete successfully without timeout because "npm" is in allowlist - expect(mockPushToolResult).toHaveBeenCalled() - const result = mockPushToolResult.mock.calls[0][0] - expect(result).not.toContain("terminated after exceeding") - }, 3000) - - it("should apply timeout for commands not in allowlist", async () => { - // Mock VSCode configuration with timeout and allowlist - const mockGetConfiguration = vitest.fn().mockReturnValue({ - get: vitest.fn().mockImplementation((key: string) => { - if (key === "commandExecutionTimeout") return 1 // 1 second timeout - if (key === "commandTimeoutAllowlist") return ["npm", "git"] - return undefined - }), - }) - ;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration()) - - mockBlock.params.command = "sleep 10" // Not in allowlist - mockBlock.nativeArgs.command = "sleep 10" - - // Create a process that never resolves - const neverResolvingProcess = new Promise(() => {}) - ;(neverResolvingProcess as any).abort = vitest.fn() - mockTerminal.runCommand.mockReturnValue(neverResolvingProcess) - - await executeCommandTool.handle(mockTask as Task, mockBlock, { - askApproval: mockAskApproval, - handleError: mockHandleError, - pushToolResult: mockPushToolResult, - }) - - // Should timeout because "sleep" is not in allowlist - expect(mockPushToolResult).toHaveBeenCalled() - const result = mockPushToolResult.mock.calls[0][0] - expect(result).toContain("terminated after exceeding") - }, 3000) - - it("should handle empty allowlist", async () => { - // Mock VSCode configuration with timeout and empty allowlist - const mockGetConfiguration = vitest.fn().mockReturnValue({ - get: vitest.fn().mockImplementation((key: string) => { - if (key === "commandExecutionTimeout") return 1 // 1 second timeout - if (key === "commandTimeoutAllowlist") return [] - return undefined - }), - }) - ;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration()) - - mockBlock.params.command = "npm install" - mockBlock.nativeArgs.command = "npm install" - - // Create a process that never resolves - const neverResolvingProcess = new Promise(() => {}) - ;(neverResolvingProcess as any).abort = vitest.fn() - mockTerminal.runCommand.mockReturnValue(neverResolvingProcess) - - await executeCommandTool.handle(mockTask as Task, mockBlock, { - askApproval: mockAskApproval, - handleError: mockHandleError, - pushToolResult: mockPushToolResult, - }) - - // Should timeout because allowlist is empty - expect(mockPushToolResult).toHaveBeenCalled() - const result = mockPushToolResult.mock.calls[0][0] - expect(result).toContain("terminated after exceeding") - }, 3000) - - it("should match command prefixes correctly", async () => { - // Mock VSCode configuration with timeout and allowlist - const mockGetConfiguration = vitest.fn().mockReturnValue({ - get: vitest.fn().mockImplementation((key: string) => { - if (key === "commandExecutionTimeout") return 1 // 1 second timeout - if (key === "commandTimeoutAllowlist") return ["git log", "npm run"] - return undefined - }), - }) - ;(vscode.workspace.getConfiguration as any).mockReturnValue(mockGetConfiguration()) - - const longRunningProcess = new Promise((resolve) => { - setTimeout(resolve, 2000) // 2 seconds - }) - const neverResolvingProcess = new Promise(() => {}) - ;(neverResolvingProcess as any).abort = vitest.fn() - - // Test exact prefix match - should not timeout - mockBlock.params.command = "git log --oneline" - mockBlock.nativeArgs.command = "git log --oneline" - mockTerminal.runCommand.mockReturnValueOnce(longRunningProcess) - - await executeCommandTool.handle(mockTask as Task, mockBlock, { - askApproval: mockAskApproval, - handleError: mockHandleError, - pushToolResult: mockPushToolResult, - }) - - expect(mockPushToolResult).toHaveBeenCalled() - const result1 = mockPushToolResult.mock.calls[0][0] - expect(result1).not.toContain("terminated after exceeding") - - // Reset mocks for second test - mockPushToolResult.mockClear() - - // Test partial prefix match (should not match) - should timeout - mockBlock.params.command = "git status" // "git" alone is not in allowlist, only "git log" - mockBlock.nativeArgs.command = "git status" - mockTerminal.runCommand.mockReturnValueOnce(neverResolvingProcess) - - await executeCommandTool.handle(mockTask as Task, mockBlock, { - askApproval: mockAskApproval, - handleError: mockHandleError, - pushToolResult: mockPushToolResult, - }) - - expect(mockPushToolResult).toHaveBeenCalled() - const result2 = mockPushToolResult.mock.calls[0][0] - expect(result2).toContain("terminated after exceeding") - }, 5000) - }) -}) diff --git a/src/core/webview/__tests__/webviewMessageHandler.imageMentions.integration.spec.ts b/src/core/webview/__tests__/webviewMessageHandler.imageMentions.integration.spec.ts deleted file mode 100644 index 277e56626ad..00000000000 --- a/src/core/webview/__tests__/webviewMessageHandler.imageMentions.integration.spec.ts +++ /dev/null @@ -1,130 +0,0 @@ -import * as fs from "fs/promises" -import * as path from "path" -import * as os from "os" - -// Must mock dependencies before importing the handler module. -vi.mock("../../../api/providers/fetchers/modelCache") - -import { webviewMessageHandler } from "../webviewMessageHandler" -import type { ClineProvider } from "../ClineProvider" - -vi.mock("vscode", () => ({ - window: { - showInformationMessage: vi.fn(), - showErrorMessage: vi.fn(), - }, - workspace: { - workspaceFolders: [{ uri: { fsPath: "/mock/workspace" } }], - }, -})) - -// Mock imageHelpers - use actual implementations for functions that need real file access -vi.mock("../../tools/helpers/imageHelpers", async (importOriginal) => { - const actual = await importOriginal() - return { - ...actual, - validateImageForProcessing: vi.fn().mockResolvedValue({ isValid: true, sizeInMB: 0.001 }), - ImageMemoryTracker: vi.fn().mockImplementation(() => ({ - getTotalMemoryUsed: vi.fn().mockReturnValue(0), - addMemoryUsage: vi.fn(), - })), - } -}) - -describe("webviewMessageHandler - image mentions (integration)", () => { - it("resolves image mentions for newTask and passes images to createTask", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "roo-image-mentions-")) - try { - const imgBytes = Buffer.from("png-bytes") - await fs.writeFile(path.join(tmpRoot, "cat.png"), imgBytes) - - const mockProvider = { - cwd: tmpRoot, - getCurrentTask: vi.fn().mockReturnValue(undefined), - createTask: vi.fn().mockResolvedValue(undefined), - postMessageToWebview: vi.fn().mockResolvedValue(undefined), - getState: vi.fn().mockResolvedValue({ - maxImageFileSize: 5, - maxTotalImageSize: 20, - }), - } as unknown as ClineProvider - - await webviewMessageHandler(mockProvider, { - type: "newTask", - text: "Please look at @/cat.png", - images: [], - } as any) - - expect(mockProvider.createTask).toHaveBeenCalledWith("Please look at @/cat.png", [ - `data:image/png;base64,${imgBytes.toString("base64")}`, - ]) - } finally { - await fs.rm(tmpRoot, { recursive: true, force: true }) - } - }) - - it("resolves image mentions for askResponse and passes images to handleWebviewAskResponse", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "roo-image-mentions-")) - try { - const imgBytes = Buffer.from("jpg-bytes") - await fs.writeFile(path.join(tmpRoot, "cat.jpg"), imgBytes) - - const handleWebviewAskResponse = vi.fn() - const mockProvider = { - cwd: tmpRoot, - getCurrentTask: vi.fn().mockReturnValue({ - cwd: tmpRoot, - handleWebviewAskResponse, - }), - getState: vi.fn().mockResolvedValue({ - maxImageFileSize: 5, - maxTotalImageSize: 20, - }), - } as unknown as ClineProvider - - await webviewMessageHandler(mockProvider, { - type: "askResponse", - askResponse: "messageResponse", - text: "Please look at @/cat.jpg", - images: [], - } as any) - - expect(handleWebviewAskResponse).toHaveBeenCalledWith("messageResponse", "Please look at @/cat.jpg", [ - `data:image/jpeg;base64,${imgBytes.toString("base64")}`, - ]) - } finally { - await fs.rm(tmpRoot, { recursive: true, force: true }) - } - }) - - it("resolves gif image mentions (matching read_file behavior)", async () => { - const tmpRoot = await fs.mkdtemp(path.join(os.tmpdir(), "roo-image-mentions-")) - try { - const imgBytes = Buffer.from("gif-bytes") - await fs.writeFile(path.join(tmpRoot, "animation.gif"), imgBytes) - - const mockProvider = { - cwd: tmpRoot, - getCurrentTask: vi.fn().mockReturnValue(undefined), - createTask: vi.fn().mockResolvedValue(undefined), - postMessageToWebview: vi.fn().mockResolvedValue(undefined), - getState: vi.fn().mockResolvedValue({ - maxImageFileSize: 5, - maxTotalImageSize: 20, - }), - } as unknown as ClineProvider - - await webviewMessageHandler(mockProvider, { - type: "newTask", - text: "See @/animation.gif", - images: [], - } as any) - - expect(mockProvider.createTask).toHaveBeenCalledWith("See @/animation.gif", [ - `data:image/gif;base64,${imgBytes.toString("base64")}`, - ]) - } finally { - await fs.rm(tmpRoot, { recursive: true, force: true }) - } - }) -})