Skip to content

Commit 8aa6cec

Browse files
committed
test: isolate multi-root e2e coverage
1 parent d4df00b commit 8aa6cec

4 files changed

Lines changed: 150 additions & 1 deletion

File tree

apps/vscode-e2e/fixtures/read-file.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,21 @@
109109
}
110110
]
111111
}
112+
},
113+
{
114+
"match": {
115+
"sequenceIndex": 0,
116+
"userMessage": "READ_FILE_MULTI_ROOT_REPRO"
117+
},
118+
"response": {
119+
"toolCalls": [
120+
{
121+
"name": "read_file",
122+
"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}}",
123+
"id": "call_read_file_multi_root_secondary_001"
124+
}
125+
]
126+
}
112127
}
113128
]
114129
}

apps/vscode-e2e/src/fixtures/read-file.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ export function addReadFileResultFixtures(mock: InstanceType<typeof LLMock>) {
7171
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.",
7272
id: "call_read_file_large_002",
7373
},
74+
{
75+
toolCallId: "call_read_file_multi_root_secondary_001",
76+
expected: ["File: secondary-root-read-file.txt", "SECONDARY_ROOT_MARKER_204"],
77+
result: "The read_file tool successfully read `secondary-root-read-file.txt` from the secondary workspace root. Its contents include `SECONDARY_ROOT_MARKER_204`.",
78+
id: "call_read_file_multi_root_secondary_002",
79+
},
7480
]
7581

7682
for (const fixture of fixtures) {

apps/vscode-e2e/src/runTest.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,20 @@ function isDeepSeekTargetedRun(testFile?: string, testGrep?: string) {
2626
return testGrep?.toLowerCase().includes("deepseek") ?? false
2727
}
2828

29+
function isMultiRootTargetedRun(testFile?: string, testGrep?: string) {
30+
if (testFile?.toLowerCase().includes("multi-root-read-file-content.test")) {
31+
return true
32+
}
33+
34+
return testGrep?.toLowerCase().includes("multi-root") ?? false
35+
}
36+
2937
async function main() {
3038
const isRecord = process.env.AIMOCK_RECORD === "true"
3139
const testGrep = getCliFlagValue("--grep") || process.env.TEST_GREP
3240
const testFile = getCliFlagValue("--file") || process.env.TEST_FILE
3341
const isDeepSeekTest = isDeepSeekTargetedRun(testFile, testGrep)
42+
const isMultiRootTest = isMultiRootTargetedRun(testFile, testGrep)
3443

3544
if (isRecord && isDeepSeekTest && !process.env.DEEPSEEK_API_KEY) {
3645
throw new Error("AIMOCK_RECORD=true requires DEEPSEEK_API_KEY to record DeepSeek fixtures")
@@ -58,11 +67,28 @@ async function main() {
5867
const extensionTestsPath = path.resolve(__dirname, "./suite/index")
5968

6069
let testWorkspace: string | undefined
70+
let secondaryWorkspace: string | undefined
71+
let multiRootWorkspaceFile: string | undefined
6172

6273
try {
6374
// Create a temporary workspace folder for tests before installing fixtures that
6475
// need workspace-specific paths.
6576
testWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-workspace-"))
77+
if (isMultiRootTest) {
78+
secondaryWorkspace = await fs.mkdtemp(path.join(os.tmpdir(), "roo-test-secondary-workspace-"))
79+
multiRootWorkspaceFile = path.join(os.tmpdir(), `roo-test-workspace-${Date.now()}.code-workspace`)
80+
await fs.writeFile(
81+
multiRootWorkspaceFile,
82+
JSON.stringify(
83+
{
84+
folders: [{ path: testWorkspace }, { path: secondaryWorkspace }],
85+
},
86+
null,
87+
2,
88+
),
89+
"utf8",
90+
)
91+
}
6692

6793
if (useMock) {
6894
const fixturesDir = path.resolve(__dirname, "../fixtures")
@@ -134,14 +160,20 @@ async function main() {
134160
await runTests({
135161
extensionDevelopmentPath,
136162
extensionTestsPath,
137-
launchArgs: [testWorkspace],
163+
launchArgs: [multiRootWorkspaceFile ?? testWorkspace],
138164
extensionTestsEnv,
139165
version: process.env.VSCODE_VERSION || "1.101.2",
140166
})
141167
} catch (error) {
142168
console.error("Failed to run tests", error)
143169
process.exitCode = 1
144170
} finally {
171+
if (multiRootWorkspaceFile) {
172+
await fs.rm(multiRootWorkspaceFile, { force: true })
173+
}
174+
if (secondaryWorkspace) {
175+
await fs.rm(secondaryWorkspace, { recursive: true, force: true })
176+
}
145177
if (testWorkspace) {
146178
await fs.rm(testWorkspace, { recursive: true, force: true })
147179
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import * as assert from "assert"
2+
import * as fs from "fs/promises"
3+
import * as path from "path"
4+
import * as vscode from "vscode"
5+
6+
import { RooCodeEventName, type ClineMessage } from "@roo-code/types"
7+
8+
import { setDefaultSuiteTimeout } from "./test-utils"
9+
import { waitFor } from "./utils"
10+
11+
suite("Multi-root readFileContent repro", function () {
12+
setDefaultSuiteTimeout(this)
13+
14+
test("should read a file that exists only in the secondary workspace root", async () => {
15+
await waitFor(() => (vscode.workspace.workspaceFolders?.length ?? 0) >= 2, {
16+
timeout: 60_000,
17+
interval: 250,
18+
})
19+
20+
const primaryWorkspace = vscode.workspace.workspaceFolders?.[0]
21+
assert.ok(primaryWorkspace, "Expected a primary workspace folder")
22+
const secondaryWorkspace = vscode.workspace.workspaceFolders?.[1]
23+
assert.ok(secondaryWorkspace, "Expected a secondary workspace folder")
24+
25+
const primaryRoot = primaryWorkspace.uri.fsPath
26+
const secondaryRoot = secondaryWorkspace.uri.fsPath
27+
const secondaryFileName = "secondary-root-read-file.txt"
28+
const expectedContent = "SECONDARY_ROOT_MARKER_204\n"
29+
const secondaryFilePath = path.join(secondaryRoot, secondaryFileName)
30+
31+
await fs.writeFile(secondaryFilePath, expectedContent, "utf8")
32+
33+
const api = globalThis.api
34+
const messages: ClineMessage[] = []
35+
const messageHandler = ({ message }: { message: ClineMessage }) => {
36+
if (message.partial !== true) {
37+
messages.push(message)
38+
}
39+
}
40+
api.on(RooCodeEventName.Message, messageHandler)
41+
42+
let taskCompleted = false
43+
let taskId = ""
44+
const taskCompletedHandler = (id: string) => {
45+
if (id === taskId) {
46+
taskCompleted = true
47+
}
48+
}
49+
api.on(RooCodeEventName.TaskCompleted, taskCompletedHandler)
50+
51+
try {
52+
taskId = await api.startNewTask({
53+
configuration: {
54+
mode: "code",
55+
autoApprovalEnabled: true,
56+
alwaysAllowReadOnly: true,
57+
alwaysAllowReadOnlyOutsideWorkspace: true,
58+
},
59+
text:
60+
`READ_FILE_MULTI_ROOT_REPRO: Use only the read_file tool to read "${secondaryFileName}". ` +
61+
`The file exists in the current VS Code workspace, but only inside the secondary workspace root. ` +
62+
`After the read attempt, explain exactly what happened.`,
63+
})
64+
65+
await waitFor(() => taskCompleted, { timeout: 60_000, interval: 250 })
66+
67+
assert.ok(
68+
vscode.workspace.workspaceFolders?.some((folder) => folder.uri.fsPath === secondaryRoot),
69+
`Expected secondary root ${secondaryRoot} to remain part of the workspace during the repro`,
70+
)
71+
72+
const completionMessage = messages.find(
73+
(message) =>
74+
message.type === "say" &&
75+
(message.say === "completion_result" || message.say === "text") &&
76+
message.text?.includes("SECONDARY_ROOT_MARKER_204"),
77+
)
78+
79+
assert.ok(
80+
completionMessage,
81+
`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)}.`,
82+
)
83+
} finally {
84+
api.off(RooCodeEventName.Message, messageHandler)
85+
api.off(RooCodeEventName.TaskCompleted, taskCompletedHandler)
86+
87+
try {
88+
await api.cancelCurrentTask()
89+
} catch {
90+
// Ignore cleanup races if the task already ended.
91+
}
92+
93+
await fs.rm(secondaryFilePath, { force: true })
94+
}
95+
})
96+
})

0 commit comments

Comments
 (0)