From 8aa6cec90c4337886774cd89292c4b8eb9f8e96d Mon Sep 17 00:00:00 2001 From: Roomote Date: Thu, 21 May 2026 03:00:31 +0000 Subject: [PATCH 1/2] test: isolate multi-root e2e coverage --- apps/vscode-e2e/fixtures/read-file.json | 15 +++ apps/vscode-e2e/src/fixtures/read-file.ts | 6 ++ apps/vscode-e2e/src/runTest.ts | 34 ++++++- .../multi-root-read-file-content.test.ts | 96 +++++++++++++++++++ 4 files changed, 150 insertions(+), 1 deletion(-) create mode 100644 apps/vscode-e2e/src/suite/multi-root-read-file-content.test.ts diff --git a/apps/vscode-e2e/fixtures/read-file.json b/apps/vscode-e2e/fixtures/read-file.json index 9839348abd..813b18703d 100644 --- a/apps/vscode-e2e/fixtures/read-file.json +++ b/apps/vscode-e2e/fixtures/read-file.json @@ -109,6 +109,21 @@ } ] } + }, + { + "match": { + "sequenceIndex": 0, + "userMessage": "READ_FILE_MULTI_ROOT_REPRO" + }, + "response": { + "toolCalls": [ + { + "name": "read_file", + "arguments": "{\"path\":\"secondary-root-read-file.txt\",\"mode\":\"slice\",\"offset\":1,\"limit\":50,\"indentation\":{\"anchor_line\":1,\"max_levels\":0,\"include_siblings\":false,\"include_header\":true,\"max_lines\":50}}", + "id": "call_read_file_multi_root_secondary_001" + } + ] + } } ] } diff --git a/apps/vscode-e2e/src/fixtures/read-file.ts b/apps/vscode-e2e/src/fixtures/read-file.ts index dd9bb1c10b..7c55d6cbcb 100644 --- a/apps/vscode-e2e/src/fixtures/read-file.ts +++ b/apps/vscode-e2e/src/fixtures/read-file.ts @@ -71,6 +71,12 @@ export function addReadFileResultFixtures(mock: InstanceType) { result: "The file [`large-read-file.txt`](large-read-file.txt) contains 100 lines, each following the pattern: `Line N: This is a test line with some content`, where `N` is the line number (from 1 to 100). The structure is consistent throughout the file, with only the line number changing on each line.", id: "call_read_file_large_002", }, + { + toolCallId: "call_read_file_multi_root_secondary_001", + expected: ["File: secondary-root-read-file.txt", "SECONDARY_ROOT_MARKER_204"], + result: "The read_file tool successfully read `secondary-root-read-file.txt` from the secondary workspace root. Its contents include `SECONDARY_ROOT_MARKER_204`.", + id: "call_read_file_multi_root_secondary_002", + }, ] for (const fixture of fixtures) { diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index b2550559b1..8aa4b431cc 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -26,11 +26,20 @@ function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) { return testGrep?.toLowerCase().includes("deepseek") ?? false } +function isMultiRootTargetedRun(testFile?: string, testGrep?: string) { + if (testFile?.toLowerCase().includes("multi-root-read-file-content.test")) { + return true + } + + return testGrep?.toLowerCase().includes("multi-root") ?? false +} + async function main() { const isRecord = process.env.AIMOCK_RECORD === "true" const testGrep = getCliFlagValue("--grep") || process.env.TEST_GREP const testFile = getCliFlagValue("--file") || process.env.TEST_FILE const isDeepSeekTest = isDeepSeekTargetedRun(testFile, testGrep) + const isMultiRootTest = isMultiRootTargetedRun(testFile, testGrep) if (isRecord && isDeepSeekTest && !process.env.DEEPSEEK_API_KEY) { throw new Error("AIMOCK_RECORD=true requires DEEPSEEK_API_KEY to record DeepSeek fixtures") @@ -58,11 +67,28 @@ async function main() { const extensionTestsPath = path.resolve(__dirname, "./suite/index") let testWorkspace: string | undefined + let secondaryWorkspace: string | undefined + let multiRootWorkspaceFile: string | undefined try { // Create a temporary workspace folder for tests before installing fixtures that // need workspace-specific paths. testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-workspace-")) + if (isMultiRootTest) { + secondaryWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-secondary-workspace-")) + multiRootWorkspaceFile = path.join(os.tmpdir(), `roo-test-workspace-${Date.now()}.code-workspace`) + await fs.writeFile( + multiRootWorkspaceFile, + JSON.stringify( + { + folders: [{ path: testWorkspace }, { path: secondaryWorkspace }], + }, + null, + 2, + ), + "utf8", + ) + } if (useMock) { const fixturesDir = path.resolve(__dirname, "../fixtures") @@ -134,7 +160,7 @@ async function main() { await runTests({ extensionDevelopmentPath, extensionTestsPath, - launchArgs: [testWorkspace], + launchArgs: [multiRootWorkspaceFile ?? testWorkspace], extensionTestsEnv, version: process.env.VSCODE_VERSION || "1.101.2", }) @@ -142,6 +168,12 @@ async function main() { console.error("Failed to run tests", error) process.exitCode = 1 } finally { + if (multiRootWorkspaceFile) { + await fs.rm(multiRootWorkspaceFile, { force: true }) + } + if (secondaryWorkspace) { + await fs.rm(secondaryWorkspace, { recursive: true, force: true }) + } if (testWorkspace) { await fs.rm(testWorkspace, { recursive: true, force: true }) } diff --git a/apps/vscode-e2e/src/suite/multi-root-read-file-content.test.ts b/apps/vscode-e2e/src/suite/multi-root-read-file-content.test.ts new file mode 100644 index 0000000000..3da2752d4f --- /dev/null +++ b/apps/vscode-e2e/src/suite/multi-root-read-file-content.test.ts @@ -0,0 +1,96 @@ +import * as assert from "assert" +import * as fs from "fs/promises" +import * as path from "path" +import * as vscode from "vscode" + +import { RooCodeEventName, type ClineMessage } from "@roo-code/types" + +import { setDefaultSuiteTimeout } from "./test-utils" +import { waitFor } from "./utils" + +suite("Multi-root readFileContent repro", function () { + setDefaultSuiteTimeout(this) + + test("should read a file that exists only in the secondary workspace root", async () => { + await waitFor(() => (vscode.workspace.workspaceFolders?.length ?? 0) >= 2, { + timeout: 60_000, + interval: 250, + }) + + const primaryWorkspace = vscode.workspace.workspaceFolders?.[0] + assert.ok(primaryWorkspace, "Expected a primary workspace folder") + const secondaryWorkspace = vscode.workspace.workspaceFolders?.[1] + assert.ok(secondaryWorkspace, "Expected a secondary workspace folder") + + const primaryRoot = primaryWorkspace.uri.fsPath + const secondaryRoot = secondaryWorkspace.uri.fsPath + const secondaryFileName = "secondary-root-read-file.txt" + const expectedContent = "SECONDARY_ROOT_MARKER_204\n" + const secondaryFilePath = path.join(secondaryRoot, secondaryFileName) + + await fs.writeFile(secondaryFilePath, expectedContent, "utf8") + + const api = globalThis.api + const messages: ClineMessage[] = [] + const messageHandler = ({ message }: { message: ClineMessage }) => { + if (message.partial !== true) { + messages.push(message) + } + } + api.on(RooCodeEventName.Message, messageHandler) + + let taskCompleted = false + let taskId = "" + const taskCompletedHandler = (id: string) => { + if (id === taskId) { + taskCompleted = true + } + } + api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + try { + taskId = await api.startNewTask({ + configuration: { + mode: "code", + autoApprovalEnabled: true, + alwaysAllowReadOnly: true, + alwaysAllowReadOnlyOutsideWorkspace: true, + }, + text: + `READ_FILE_MULTI_ROOT_REPRO: Use only the read_file tool to read "${secondaryFileName}". ` + + `The file exists in the current VS Code workspace, but only inside the secondary workspace root. ` + + `After the read attempt, explain exactly what happened.`, + }) + + await waitFor(() => taskCompleted, { timeout: 60_000, interval: 250 }) + + assert.ok( + vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === secondaryRoot), + `Expected secondary root ${secondaryRoot} to remain part of the workspace during the repro`, + ) + + const completionMessage = messages.find( + (message) => + message.type === "say" && + (message.say === "completion_result" || message.say === "text") && + message.text?.includes("SECONDARY_ROOT_MARKER_204"), + ) + + assert.ok( + completionMessage, + `Expected the task to read the secondary-root file. Primary root was ${primaryRoot}, secondary root was ${secondaryRoot}, secondary file was ${secondaryFilePath}, and messages were ${JSON.stringify(messages, null, 2)}.`, + ) + } finally { + api.off(RooCodeEventName.Message, messageHandler) + api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler) + + try { + await api.cancelCurrentTask() + } catch { + // Ignore cleanup races if the task already ended. + } + + await fs.rm(secondaryFilePath, { force: true }) + } + }) +}) From 430d19c09634cb51a28a1810fc22b786ab210784 Mon Sep 17 00:00:00 2001 From: Roomote Date: Thu, 21 May 2026 03:19:29 +0000 Subject: [PATCH 2/2] fix: run multi-root e2e in full mock suite --- apps/vscode-e2e/src/runTest.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/apps/vscode-e2e/src/runTest.ts b/apps/vscode-e2e/src/runTest.ts index 8aa4b431cc..ba5b24fb49 100644 --- a/apps/vscode-e2e/src/runTest.ts +++ b/apps/vscode-e2e/src/runTest.ts @@ -27,7 +27,12 @@ function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) { } function isMultiRootTargetedRun(testFile?: string, testGrep?: string) { - if (testFile?.toLowerCase().includes("multi-root-read-file-content.test")) { + if (testFile?.toLowerCase().includes("multi-root-read-file-content")) { + return true + } + + if (!testFile && !testGrep) { + // The default mocked CI path runs the whole suite, which includes the multi-root repro. return true }