Skip to content

Commit d23cd87

Browse files
committed
feat: use automation profile for terminal creation
- Add getAutomationShell() function to retrieve automation profile settings - Modify Terminal class to use automation profile when creating terminals - Add comprehensive tests for automation profile functionality - Respects terminal.integrated.automationProfile settings for all platforms Fixes #7892
1 parent 8fee312 commit d23cd87

File tree

4 files changed

+364
-2
lines changed

4 files changed

+364
-2
lines changed

src/integrations/terminal/Terminal.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { BaseTerminal } from "./BaseTerminal"
66
import { TerminalProcess } from "./TerminalProcess"
77
import { ShellIntegrationManager } from "./ShellIntegrationManager"
88
import { mergePromise } from "./mergePromise"
9+
import { getAutomationShell } from "../../utils/shell"
910

1011
export class Terminal extends BaseTerminal {
1112
public terminal: vscode.Terminal
@@ -17,7 +18,19 @@ export class Terminal extends BaseTerminal {
1718

1819
const env = Terminal.getEnv()
1920
const iconPath = new vscode.ThemeIcon("rocket")
20-
this.terminal = terminal ?? vscode.window.createTerminal({ cwd, name: "Roo Code", iconPath, env })
21+
22+
// Try to get the automation shell first, fall back to default behavior if not configured
23+
const shellPath = getAutomationShell() || undefined
24+
25+
const terminalOptions: vscode.TerminalOptions = {
26+
cwd,
27+
name: "Roo Code",
28+
iconPath,
29+
env,
30+
shellPath,
31+
}
32+
33+
this.terminal = terminal ?? vscode.window.createTerminal(terminalOptions)
2134

2235
if (Terminal.getTerminalZdotdir()) {
2336
ShellIntegrationManager.terminalTmpDirs.set(id, env.ZDOTDIR)

src/integrations/terminal/__tests__/TerminalRegistry.spec.ts

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,18 @@
33
import * as vscode from "vscode"
44
import { Terminal } from "../Terminal"
55
import { TerminalRegistry } from "../TerminalRegistry"
6+
import * as shellUtils from "../../../utils/shell"
67

78
const PAGER = process.platform === "win32" ? "" : "cat"
89

910
vi.mock("execa", () => ({
1011
execa: vi.fn(),
1112
}))
1213

14+
vi.mock("../../../utils/shell", () => ({
15+
getAutomationShell: vi.fn(() => null),
16+
}))
17+
1318
describe("TerminalRegistry", () => {
1419
let mockCreateTerminal: any
1520

@@ -118,5 +123,80 @@ describe("TerminalRegistry", () => {
118123
Terminal.setTerminalZshP10k(false)
119124
}
120125
})
126+
127+
it("uses automation profile shell when configured", () => {
128+
// Mock getAutomationShell to return a specific shell path
129+
const getAutomationShellMock = shellUtils.getAutomationShell as any
130+
getAutomationShellMock.mockReturnValue("/bin/bash")
131+
132+
try {
133+
TerminalRegistry.createTerminal("/test/path", "vscode")
134+
135+
expect(mockCreateTerminal).toHaveBeenCalledWith({
136+
cwd: "/test/path",
137+
name: "Roo Code",
138+
iconPath: expect.any(Object),
139+
env: {
140+
PAGER,
141+
VTE_VERSION: "0",
142+
PROMPT_EOL_MARK: "",
143+
},
144+
shellPath: "/bin/bash",
145+
})
146+
} finally {
147+
// Reset mock
148+
getAutomationShellMock.mockReturnValue(null)
149+
}
150+
})
151+
152+
it("does not set shellPath when automation profile is not configured", () => {
153+
// Mock getAutomationShell to return null (no automation profile)
154+
const getAutomationShellMock = shellUtils.getAutomationShell as any
155+
getAutomationShellMock.mockReturnValue(null)
156+
157+
TerminalRegistry.createTerminal("/test/path", "vscode")
158+
159+
expect(mockCreateTerminal).toHaveBeenCalledWith({
160+
cwd: "/test/path",
161+
name: "Roo Code",
162+
iconPath: expect.any(Object),
163+
env: {
164+
PAGER,
165+
VTE_VERSION: "0",
166+
PROMPT_EOL_MARK: "",
167+
},
168+
shellPath: undefined,
169+
})
170+
})
171+
172+
it("uses Windows automation profile when configured", () => {
173+
// Mock for Windows platform
174+
const originalPlatform = process.platform
175+
Object.defineProperty(process, "platform", { value: "win32", configurable: true })
176+
177+
// Mock getAutomationShell to return PowerShell 7 path
178+
const getAutomationShellMock = shellUtils.getAutomationShell as any
179+
getAutomationShellMock.mockReturnValue("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
180+
181+
try {
182+
TerminalRegistry.createTerminal("/test/path", "vscode")
183+
184+
expect(mockCreateTerminal).toHaveBeenCalledWith({
185+
cwd: "/test/path",
186+
name: "Roo Code",
187+
iconPath: expect.any(Object),
188+
env: {
189+
PAGER: "",
190+
VTE_VERSION: "0",
191+
PROMPT_EOL_MARK: "",
192+
},
193+
shellPath: "C:\\Program Files\\PowerShell\\7\\pwsh.exe",
194+
})
195+
} finally {
196+
// Restore platform and mock
197+
Object.defineProperty(process, "platform", { value: originalPlatform, configurable: true })
198+
getAutomationShellMock.mockReturnValue(null)
199+
}
200+
})
121201
})
122202
})

src/utils/__tests__/shell.spec.ts

Lines changed: 210 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
22
import * as vscode from "vscode"
33
import { userInfo } from "os"
4-
import { getShell } from "../shell"
4+
import { getShell, getAutomationShell } from "../shell"
55

66
// Mock vscode module
77
vi.mock("vscode", () => ({
@@ -485,4 +485,213 @@ describe("Shell Detection Tests", () => {
485485
expect(result).toBe("/bin/bash") // Should fall back to safe default
486486
})
487487
})
488+
489+
// --------------------------------------------------------------------------
490+
// Automation Shell Detection Tests
491+
// --------------------------------------------------------------------------
492+
describe("Automation Shell Detection", () => {
493+
describe("Windows Automation Shell", () => {
494+
beforeEach(() => {
495+
Object.defineProperty(process, "platform", { value: "win32" })
496+
})
497+
498+
it("uses automation profile path when configured", () => {
499+
const mockConfig = {
500+
get: vi.fn((key: string) => {
501+
if (key === "automationProfile.windows") {
502+
return { path: "C:\\Program Files\\PowerShell\\7\\pwsh.exe" }
503+
}
504+
return undefined
505+
}),
506+
}
507+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
508+
509+
expect(getAutomationShell()).toBe("C:\\Program Files\\PowerShell\\7\\pwsh.exe")
510+
})
511+
512+
it("handles array path in automation profile", () => {
513+
const mockConfig = {
514+
get: vi.fn((key: string) => {
515+
if (key === "automationProfile.windows") {
516+
return { path: ["C:\\Program Files\\Git\\bin\\bash.exe", "bash.exe"] }
517+
}
518+
return undefined
519+
}),
520+
}
521+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
522+
523+
expect(getAutomationShell()).toBe("C:\\Program Files\\Git\\bin\\bash.exe")
524+
})
525+
526+
it("returns null when no automation profile is configured", () => {
527+
const mockConfig = {
528+
get: vi.fn((key: string) => {
529+
if (key === "automationProfile.windows") {
530+
return null
531+
}
532+
return undefined
533+
}),
534+
}
535+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
536+
537+
expect(getAutomationShell()).toBeNull()
538+
})
539+
540+
it("returns null when automation profile has no path", () => {
541+
const mockConfig = {
542+
get: vi.fn((key: string) => {
543+
if (key === "automationProfile.windows") {
544+
return { source: "PowerShell" } // No path property
545+
}
546+
return undefined
547+
}),
548+
}
549+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
550+
551+
expect(getAutomationShell()).toBeNull()
552+
})
553+
554+
it("returns null when automation profile path is not in allowlist", () => {
555+
const mockConfig = {
556+
get: vi.fn((key: string) => {
557+
if (key === "automationProfile.windows") {
558+
return { path: "C:\\malicious\\shell.exe" }
559+
}
560+
return undefined
561+
}),
562+
}
563+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
564+
565+
expect(getAutomationShell()).toBeNull()
566+
})
567+
})
568+
569+
describe("macOS Automation Shell", () => {
570+
beforeEach(() => {
571+
Object.defineProperty(process, "platform", { value: "darwin" })
572+
})
573+
574+
it("uses automation profile path when configured", () => {
575+
const mockConfig = {
576+
get: vi.fn((key: string) => {
577+
if (key === "automationProfile.osx") {
578+
return { path: "/bin/bash" }
579+
}
580+
return undefined
581+
}),
582+
}
583+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
584+
585+
expect(getAutomationShell()).toBe("/bin/bash")
586+
})
587+
588+
it("handles array path in automation profile", () => {
589+
const mockConfig = {
590+
get: vi.fn((key: string) => {
591+
if (key === "automationProfile.osx") {
592+
return { path: ["/usr/local/bin/bash", "/bin/bash"] }
593+
}
594+
return undefined
595+
}),
596+
}
597+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
598+
599+
expect(getAutomationShell()).toBe("/usr/local/bin/bash")
600+
})
601+
602+
it("returns null when no automation profile is configured", () => {
603+
const mockConfig = {
604+
get: vi.fn(() => undefined),
605+
}
606+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
607+
608+
expect(getAutomationShell()).toBeNull()
609+
})
610+
})
611+
612+
describe("Linux Automation Shell", () => {
613+
beforeEach(() => {
614+
Object.defineProperty(process, "platform", { value: "linux" })
615+
})
616+
617+
it("uses automation profile path when configured", () => {
618+
const mockConfig = {
619+
get: vi.fn((key: string) => {
620+
if (key === "automationProfile.linux") {
621+
return { path: "/bin/bash" }
622+
}
623+
return undefined
624+
}),
625+
}
626+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
627+
628+
expect(getAutomationShell()).toBe("/bin/bash")
629+
})
630+
631+
it("handles array path in automation profile", () => {
632+
const mockConfig = {
633+
get: vi.fn((key: string) => {
634+
if (key === "automationProfile.linux") {
635+
return { path: ["/usr/bin/zsh", "/bin/zsh"] }
636+
}
637+
return undefined
638+
}),
639+
}
640+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
641+
642+
expect(getAutomationShell()).toBe("/usr/bin/zsh")
643+
})
644+
645+
it("returns null when automation profile path is empty array", () => {
646+
const mockConfig = {
647+
get: vi.fn((key: string) => {
648+
if (key === "automationProfile.linux") {
649+
return { path: [] }
650+
}
651+
return undefined
652+
}),
653+
}
654+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
655+
656+
expect(getAutomationShell()).toBeNull()
657+
})
658+
659+
it("validates automation shell against allowlist", () => {
660+
const mockConfig = {
661+
get: vi.fn((key: string) => {
662+
if (key === "automationProfile.linux") {
663+
return { path: "/usr/bin/evil-shell" }
664+
}
665+
return undefined
666+
}),
667+
}
668+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
669+
670+
expect(getAutomationShell()).toBeNull()
671+
})
672+
})
673+
674+
describe("Unknown Platform Automation Shell", () => {
675+
it("returns null for unknown platforms", () => {
676+
Object.defineProperty(process, "platform", { value: "sunos" })
677+
const mockConfig = {
678+
get: vi.fn(() => undefined),
679+
}
680+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue(mockConfig as any)
681+
682+
expect(getAutomationShell()).toBeNull()
683+
})
684+
})
685+
686+
describe("Error Handling in Automation Shell", () => {
687+
it("handles configuration errors gracefully", () => {
688+
Object.defineProperty(process, "platform", { value: "win32" })
689+
vscode.workspace.getConfiguration = () => {
690+
throw new Error("Configuration error")
691+
}
692+
693+
expect(getAutomationShell()).toBeNull()
694+
})
695+
})
696+
})
488697
})

0 commit comments

Comments
 (0)