Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions apps/vscode-e2e/fixtures/read-file.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
}
]
}
6 changes: 6 additions & 0 deletions apps/vscode-e2e/src/fixtures/read-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,12 @@ export function addReadFileResultFixtures(mock: InstanceType<typeof LLMock>) {
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) {
Expand Down
39 changes: 38 additions & 1 deletion apps/vscode-e2e/src/runTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,11 +26,25 @@ 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")) {
return true
}

if (!testFile && !testGrep) {
Comment thread
roomote[bot] marked this conversation as resolved.
// The default mocked CI path runs the whole suite, which includes the multi-root repro.
return true
}

return testGrep?.toLowerCase().includes("multi-root") ?? false
Comment thread
roomote[bot] marked this conversation as resolved.
}

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")
Expand Down Expand Up @@ -58,11 +72,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")
Expand Down Expand Up @@ -134,14 +165,20 @@ async function main() {
await runTests({
extensionDevelopmentPath,
extensionTestsPath,
launchArgs: [testWorkspace],
launchArgs: [multiRootWorkspaceFile ?? testWorkspace],
extensionTestsEnv,
version: process.env.VSCODE_VERSION || "1.101.2",
})
} catch (error) {
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 })
}
Expand Down
96 changes: 96 additions & 0 deletions apps/vscode-e2e/src/suite/multi-root-read-file-content.test.ts
Original file line number Diff line number Diff line change
@@ -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 })
}
})
})
Loading