Skip to content

Commit e419333

Browse files
authored
Merge pull request #9325 from shanevcantwell/fix/wsl2-terminal-command-uri
fix: handle non-file URI schemes in runTerminalCommand for WSL2
2 parents b1e17af + 8e5a7c2 commit e419333

File tree

3 files changed

+270
-1
lines changed

3 files changed

+270
-1
lines changed
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { describe, expect, it } from "vitest";
2+
import { fileURLToPath } from "node:url";
3+
4+
/**
5+
* Test suite for workspace directory resolution logic.
6+
*
7+
* This tests the URI parsing behavior used in runTerminalCommand.ts
8+
* to ensure correct handling of various workspace URI formats.
9+
*/
10+
11+
// Replicate the resolution logic for testing
12+
function resolveWorkingDirectory(workspaceDirs: string[]): string {
13+
// Handle vscode-remote://wsl+distro/path URIs (WSL2 remote workspaces)
14+
const wslWorkspaceDir = workspaceDirs.find((dir) =>
15+
dir.startsWith("vscode-remote://wsl"),
16+
);
17+
if (wslWorkspaceDir) {
18+
try {
19+
const url = new URL(wslWorkspaceDir);
20+
return decodeURIComponent(url.pathname);
21+
} catch {
22+
// Fall through to other handlers
23+
}
24+
}
25+
26+
// Handle file:// URIs (local workspaces)
27+
const fileWorkspaceDir = workspaceDirs.find((dir) =>
28+
dir.startsWith("file:/"),
29+
);
30+
if (fileWorkspaceDir) {
31+
try {
32+
return fileURLToPath(fileWorkspaceDir);
33+
} catch {
34+
// Fall through to default handling
35+
}
36+
}
37+
38+
// Default to user's home directory with fallbacks
39+
try {
40+
return process.env.HOME || process.env.USERPROFILE || process.cwd();
41+
} catch {
42+
return "/tmp";
43+
}
44+
}
45+
46+
describe("resolveWorkingDirectory", () => {
47+
describe("WSL remote URIs (vscode-remote://wsl+...)", () => {
48+
it("should parse basic WSL URI", () => {
49+
const result = resolveWorkingDirectory([
50+
"vscode-remote://wsl+Ubuntu/home/user/project",
51+
]);
52+
expect(result).toBe("/home/user/project");
53+
});
54+
55+
it("should decode URL-encoded spaces in path", () => {
56+
const result = resolveWorkingDirectory([
57+
"vscode-remote://wsl+Ubuntu/home/user/my%20project",
58+
]);
59+
expect(result).toBe("/home/user/my project");
60+
});
61+
62+
it("should decode URL-encoded special characters", () => {
63+
const result = resolveWorkingDirectory([
64+
"vscode-remote://wsl+Ubuntu/home/user/path%23with%23hashes",
65+
]);
66+
expect(result).toBe("/home/user/path#with#hashes");
67+
});
68+
69+
it("should decode URL-encoded unicode characters", () => {
70+
const result = resolveWorkingDirectory([
71+
"vscode-remote://wsl+Ubuntu/home/user/%E4%B8%AD%E6%96%87%E8%B7%AF%E5%BE%84",
72+
]);
73+
expect(result).toBe("/home/user/中文路径");
74+
});
75+
76+
it("should handle different WSL distro names", () => {
77+
const ubuntu = resolveWorkingDirectory([
78+
"vscode-remote://wsl+Ubuntu-22.04/home/user/project",
79+
]);
80+
expect(ubuntu).toBe("/home/user/project");
81+
82+
const debian = resolveWorkingDirectory([
83+
"vscode-remote://wsl+Debian/home/user/project",
84+
]);
85+
expect(debian).toBe("/home/user/project");
86+
});
87+
88+
it("should handle root path", () => {
89+
const result = resolveWorkingDirectory(["vscode-remote://wsl+Ubuntu/"]);
90+
expect(result).toBe("/");
91+
});
92+
93+
it("should prioritize WSL URIs over file:// URIs", () => {
94+
const result = resolveWorkingDirectory([
95+
"file:///c:/Users/user/project",
96+
"vscode-remote://wsl+Ubuntu/home/user/project",
97+
]);
98+
expect(result).toBe("/home/user/project");
99+
});
100+
});
101+
102+
describe("file:// URIs (local workspaces)", () => {
103+
it("should parse basic file:// URI on Unix", () => {
104+
const result = resolveWorkingDirectory(["file:///home/user/project"]);
105+
expect(result).toBe("/home/user/project");
106+
});
107+
108+
it("should decode URL-encoded spaces in file:// URI", () => {
109+
const result = resolveWorkingDirectory([
110+
"file:///home/user/my%20project",
111+
]);
112+
expect(result).toBe("/home/user/my project");
113+
});
114+
115+
it("should handle Windows-style file:// URI", () => {
116+
// fileURLToPath handles Windows paths correctly
117+
const result = resolveWorkingDirectory(["file:///C:/Users/user/project"]);
118+
// On Unix, this will be /C:/Users/user/project
119+
// On Windows, this will be C:\Users\user\project
120+
expect(result).toMatch(/project$/);
121+
});
122+
});
123+
124+
describe("fallback behavior", () => {
125+
it("should fall back to HOME when no valid URIs", () => {
126+
const originalHome = process.env.HOME;
127+
try {
128+
process.env.HOME = "/test/home";
129+
const result = resolveWorkingDirectory([]);
130+
expect(result).toBe("/test/home");
131+
} finally {
132+
process.env.HOME = originalHome;
133+
}
134+
});
135+
136+
it("should handle empty workspace dirs array", () => {
137+
const result = resolveWorkingDirectory([]);
138+
// Should return HOME or USERPROFILE or cwd
139+
expect(typeof result).toBe("string");
140+
expect(result.length).toBeGreaterThan(0);
141+
});
142+
143+
it("should handle invalid URIs gracefully", () => {
144+
const result = resolveWorkingDirectory([
145+
"not-a-valid-uri",
146+
"also://not/handled",
147+
]);
148+
// Should fall through to HOME fallback
149+
expect(typeof result).toBe("string");
150+
});
151+
152+
it("should handle malformed vscode-remote URI", () => {
153+
const result = resolveWorkingDirectory([
154+
"vscode-remote://wsl+Ubuntu", // Missing path
155+
]);
156+
// new URL() should still parse this, pathname would be empty or "/"
157+
expect(typeof result).toBe("string");
158+
});
159+
});
160+
161+
describe("URL encoding edge cases", () => {
162+
it("should handle plus signs (not spaces)", () => {
163+
// In URL encoding, + is literal plus, %2B is encoded plus, %20 is space
164+
const result = resolveWorkingDirectory([
165+
"vscode-remote://wsl+Ubuntu/home/user/c%2B%2B-project",
166+
]);
167+
expect(result).toBe("/home/user/c++-project");
168+
});
169+
170+
it("should handle percent sign itself", () => {
171+
const result = resolveWorkingDirectory([
172+
"vscode-remote://wsl+Ubuntu/home/user/100%25-complete",
173+
]);
174+
expect(result).toBe("/home/user/100%-complete");
175+
});
176+
177+
it("should handle mixed encoded and unencoded characters", () => {
178+
const result = resolveWorkingDirectory([
179+
"vscode-remote://wsl+Ubuntu/home/user/normal-path/with%20space/more",
180+
]);
181+
expect(result).toBe("/home/user/normal-path/with space/more");
182+
});
183+
});
184+
185+
describe("comparison with fileURLToPath behavior", () => {
186+
it("should match fileURLToPath decoding for equivalent paths", () => {
187+
const fileResult = fileURLToPath("file:///home/user/my%20project");
188+
const wslResult = resolveWorkingDirectory([
189+
"vscode-remote://wsl+Ubuntu/home/user/my%20project",
190+
]);
191+
192+
// Both should decode %20 to space
193+
expect(fileResult).toBe("/home/user/my project");
194+
expect(wslResult).toBe("/home/user/my project");
195+
});
196+
});
197+
});

core/tools/implementations/runTerminalCommand.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,32 @@ import { getBooleanArg, getStringArg } from "../parseArgs";
4848
* Falls back to home directory or temp directory if no workspace is available.
4949
*/
5050
function resolveWorkingDirectory(workspaceDirs: string[]): string {
51+
// Handle file:// URIs (local workspaces)
5152
const fileWorkspaceDir = workspaceDirs.find((dir) =>
5253
dir.startsWith("file:/"),
5354
);
5455
if (fileWorkspaceDir) {
55-
return fileURLToPath(fileWorkspaceDir);
56+
try {
57+
return fileURLToPath(fileWorkspaceDir);
58+
} catch {
59+
// fileURLToPath can fail on malformed URIs or in some remote environments
60+
// Fall through to default handling
61+
}
62+
}
63+
64+
// Handle other URI schemes (vscode-remote://wsl, vscode-remote://ssh-remote, etc.)
65+
const remoteWorkspaceDir = workspaceDirs.find(
66+
(dir) => dir.includes("://") && !dir.startsWith("file:/"),
67+
);
68+
if (remoteWorkspaceDir) {
69+
try {
70+
const url = new URL(remoteWorkspaceDir);
71+
return decodeURIComponent(url.pathname);
72+
} catch {
73+
// Fall through to other handlers
74+
}
5675
}
76+
5777
// Default to user's home directory with fallbacks
5878
try {
5979
return process.env.HOME || process.env.USERPROFILE || process.cwd();

core/tools/implementations/runTerminalCommand.vitest.ts

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -553,6 +553,58 @@ describe("runTerminalCommandImpl", () => {
553553
// This demonstrates why the fix is needed - fileURLToPath throws on non-file URIs
554554
expect(() => fileURLToPath(nonFileUri)).toThrow();
555555
});
556+
557+
it("should handle vscode-remote URIs by extracting pathname", async () => {
558+
// Various remote URI formats that VS Code uses
559+
const remoteUris = [
560+
"vscode-remote://wsl+Ubuntu/home/user/project",
561+
"vscode-remote://ssh-remote+myserver/home/user/project",
562+
"vscode-remote://dev-container+abc123/workspace",
563+
];
564+
565+
for (const uri of remoteUris) {
566+
mockGetWorkspaceDirs.mockResolvedValue([uri]);
567+
568+
// Should not throw - the generic URI handler extracts the pathname
569+
await expect(
570+
runTerminalCommandImpl(
571+
{ command: "echo test", waitForCompletion: false },
572+
createMockExtras(),
573+
),
574+
).resolves.toBeDefined();
575+
}
576+
});
577+
578+
it("should decode URI-encoded characters in remote workspace paths", async () => {
579+
// Path with spaces and special characters
580+
const encodedUri =
581+
"vscode-remote://wsl+Ubuntu/home/user/my%20project%20%28test%29";
582+
mockGetWorkspaceDirs.mockResolvedValue([encodedUri]);
583+
584+
// Should handle without throwing - decodeURIComponent is applied
585+
await expect(
586+
runTerminalCommandImpl(
587+
{ command: "echo test", waitForCompletion: false },
588+
createMockExtras(),
589+
),
590+
).resolves.toBeDefined();
591+
});
592+
593+
it("should prefer file:// URIs over remote URIs when both present", async () => {
594+
const workspaceDirs = [
595+
"vscode-remote://wsl+Ubuntu/home/user/remote-project",
596+
"file:///home/user/local-project",
597+
];
598+
mockGetWorkspaceDirs.mockResolvedValue(workspaceDirs);
599+
600+
// Should succeed, preferring the file:// URI
601+
await expect(
602+
runTerminalCommandImpl(
603+
{ command: "echo test", waitForCompletion: false },
604+
createMockExtras(),
605+
),
606+
).resolves.toBeDefined();
607+
});
556608
});
557609

558610
describe("remote environment handling", () => {

0 commit comments

Comments
 (0)