@@ -28,6 +28,9 @@ vi.mock("path", async () => {
2828 }
2929} )
3030
31+ // Captured before any test spies patch it, so a suite can restore real profile resolution.
32+ const getProfileShellOriginal = Terminal . getProfileShell . bind ( Terminal )
33+
3134describe ( "Shell Detection Tests" , ( ) => {
3235 let originalPlatform : string
3336 let originalEnv : NodeJS . ProcessEnv
@@ -82,6 +85,7 @@ describe("Shell Detection Tests", () => {
8285 // Clear Zoo profile override and execa shell path between tests.
8386 Terminal . setTerminalProfile ( undefined )
8487 BaseTerminal . setExecaShellPath ( undefined )
88+ BaseTerminal . setShellIntegrationDisabled ( false )
8589 } )
8690
8791 afterEach ( ( ) => {
@@ -90,6 +94,7 @@ describe("Shell Detection Tests", () => {
9094 vscode . workspace . getConfiguration = originalGetConfig
9195 Terminal . setTerminalProfile ( undefined )
9296 BaseTerminal . setExecaShellPath ( undefined )
97+ BaseTerminal . setShellIntegrationDisabled ( false )
9398 vi . clearAllMocks ( )
9499 } )
95100
@@ -595,4 +600,165 @@ describe("Shell Detection Tests", () => {
595600 expect ( getShell ( ) ) . toBe ( "/bin/bash" )
596601 } )
597602 } )
603+
604+ describe ( "Inline Terminal (execa default shell) — issue #1568" , ( ) => {
605+ beforeEach ( ( ) => {
606+ // Earlier suites leak a getProfileShell spy (their afterEach does not
607+ // restore spies); force the unpatched implementation in this block.
608+ vi . spyOn ( Terminal , "getProfileShell" ) . mockImplementation ( getProfileShellOriginal )
609+ } )
610+
611+ it ( "win32: reports COMSPEC cmd.exe, not the VS Code PowerShell profile" , ( ) => {
612+ Object . defineProperty ( process , "platform" , { value : "win32" } )
613+ vi . mocked ( existsSync ) . mockImplementation (
614+ ( p ) => String ( p ) === "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" ,
615+ )
616+ mockVsCodeConfig ( "windows" , "PowerShell" , {
617+ PowerShell : { path : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" } ,
618+ } )
619+ process . env . COMSPEC = "C:\\Windows\\System32\\cmd.exe"
620+ BaseTerminal . setShellIntegrationDisabled ( true )
621+ expect ( getShell ( ) ) . toBe ( "C:\\Windows\\System32\\cmd.exe" )
622+ } )
623+
624+ it ( "win32: without COMSPEC reports the safe cmd.exe default" , ( ) => {
625+ Object . defineProperty ( process , "platform" , { value : "win32" } )
626+ vi . mocked ( existsSync ) . mockReturnValue ( false )
627+ mockVsCodeConfig ( "windows" , null , { } )
628+ // If the COMSPEC fallback ever resolved falsy, resolution would leak
629+ // into the userInfo step; the /bin/zsh stub makes that observable.
630+ vi . mocked ( userInfo ) . mockReturnValue ( {
631+ uid : 1000 ,
632+ gid : 1000 ,
633+ username : "tester" ,
634+ homedir : "C:\\Users\\tester" ,
635+ shell : "/bin/zsh" ,
636+ } )
637+ delete process . env . COMSPEC
638+ BaseTerminal . setShellIntegrationDisabled ( true )
639+ expect ( getShell ( ) ) . toBe ( "C:\\Windows\\System32\\cmd.exe" )
640+ } )
641+
642+ it ( "win32: explicit execa shell path wins over the inline default" , ( ) => {
643+ Object . defineProperty ( process , "platform" , { value : "win32" } )
644+ mockVsCodeConfig ( "windows" , null , { } )
645+ BaseTerminal . setExecaShellPath ( "C:\\Program Files\\Git\\bin\\bash.exe" )
646+ BaseTerminal . setShellIntegrationDisabled ( true )
647+ expect ( getShell ( ) ) . toBe ( "C:\\Program Files\\Git\\bin\\bash.exe" )
648+ } )
649+
650+ it ( "win32: non-allowlisted COMSPEC is gated to the safe cmd.exe default" , ( ) => {
651+ Object . defineProperty ( process , "platform" , { value : "win32" } )
652+ vi . mocked ( existsSync ) . mockReturnValue ( false )
653+ mockVsCodeConfig ( "windows" , null , { } )
654+ process . env . COMSPEC = "C:\\Custom\\cmd.exe"
655+ BaseTerminal . setShellIntegrationDisabled ( true )
656+ expect ( getShell ( ) ) . toBe ( "C:\\Windows\\System32\\cmd.exe" )
657+ } )
658+
659+ it ( "win32: lowercase COMSPEC passes the case-insensitive allowlist verbatim" , ( ) => {
660+ Object . defineProperty ( process , "platform" , { value : "win32" } )
661+ vi . mocked ( existsSync ) . mockReturnValue ( false )
662+ mockVsCodeConfig ( "windows" , null , { } )
663+ process . env . COMSPEC = "c:\\windows\\system32\\cmd.exe"
664+ BaseTerminal . setShellIntegrationDisabled ( true )
665+ expect ( getShell ( ) ) . toBe ( "c:\\windows\\system32\\cmd.exe" )
666+ } )
667+
668+ it ( "win32: Zoo profile override is ignored when inline is on" , ( ) => {
669+ Object . defineProperty ( process , "platform" , { value : "win32" } )
670+ vi . mocked ( existsSync ) . mockImplementation ( ( p ) => String ( p ) === "C:\\Program Files\\Git\\bin\\bash.exe" )
671+ mockVsCodeConfig ( "windows" , null , {
672+ "Git Bash" : { path : "C:\\Program Files\\Git\\bin\\bash.exe" } ,
673+ } )
674+ Terminal . setTerminalProfile ( "Git Bash" )
675+ process . env . COMSPEC = "C:\\Windows\\System32\\cmd.exe"
676+ BaseTerminal . setShellIntegrationDisabled ( true )
677+ expect ( getShell ( ) ) . toBe ( "C:\\Windows\\System32\\cmd.exe" )
678+ } )
679+
680+ it ( "darwin: reports /bin/sh even when userInfo().shell is /bin/zsh" , ( ) => {
681+ Object . defineProperty ( process , "platform" , { value : "darwin" } )
682+ mockVsCodeConfig ( "osx" , null , { } )
683+ vi . mocked ( userInfo ) . mockReturnValue ( {
684+ uid : 1000 ,
685+ gid : 1000 ,
686+ username : "tester" ,
687+ homedir : "/home/tester" ,
688+ shell : "/bin/zsh" ,
689+ } )
690+ BaseTerminal . setShellIntegrationDisabled ( true )
691+ expect ( getShell ( ) ) . toBe ( "/bin/sh" )
692+ } )
693+
694+ it ( "linux: reports /bin/sh even when SHELL env is /usr/bin/fish" , ( ) => {
695+ Object . defineProperty ( process , "platform" , { value : "linux" } )
696+ mockVsCodeConfig ( "linux" , null , { } )
697+ process . env . SHELL = "/usr/bin/fish"
698+ BaseTerminal . setShellIntegrationDisabled ( true )
699+ expect ( getShell ( ) ) . toBe ( "/bin/sh" )
700+ } )
701+
702+ it ( "win32: inline off still reports the VS Code PowerShell profile" , ( ) => {
703+ Object . defineProperty ( process , "platform" , { value : "win32" } )
704+ vi . mocked ( existsSync ) . mockImplementation (
705+ ( p ) => String ( p ) === "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" ,
706+ )
707+ mockVsCodeConfig ( "windows" , "PowerShell" , {
708+ PowerShell : { path : "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" } ,
709+ } )
710+ BaseTerminal . setShellIntegrationDisabled ( false )
711+ expect ( getShell ( ) ) . toBe ( "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" )
712+ } )
713+ } )
714+
715+ // --------------------------------------------------------------------------
716+ // Unseeded static default
717+ // --------------------------------------------------------------------------
718+ describe ( "Unseeded inline-terminal default" , ( ) => {
719+ // A reset module registry reproduces the headless host state: no
720+ // resolveWebviewView or updateSettings ever seeds the static, so the
721+ // declaration defaults are exactly what a fresh process starts with.
722+ it ( "fresh module exposes inline-terminal default matching the execution default" , async ( ) => {
723+ vi . resetModules ( )
724+ const { BaseTerminal : FreshBaseTerminal } = await import ( "../../integrations/terminal/BaseTerminal" )
725+ expect ( FreshBaseTerminal . getShellIntegrationDisabled ( ) ) . toBe ( true )
726+ } )
727+
728+ it ( "unseeded static reports the execa shell, not the VS Code profile" , async ( ) => {
729+ vi . resetModules ( )
730+ Object . defineProperty ( process , "platform" , { value : "win32" } )
731+ process . env . COMSPEC = "C:\\Windows\\System32\\cmd.exe"
732+ const psPath = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"
733+ // A freshly imported shell.ts resolves its own imports from the reset
734+ // registry, so the stubs below must target that generation. The profile
735+ // must actually resolve (existsSync true): a non-inline regression would
736+ // then report the profile path instead of COMSPEC and fail this test,
737+ // whereas a failed profile resolution would mask it via the COMSPEC env.
738+ const freshFs = await import ( "fs" )
739+ vi . mocked ( freshFs . existsSync ) . mockImplementation ( ( p ) => String ( p ) === psPath )
740+ // Object.assign avoids a type assertion: the fresh mock's getConfiguration
741+ // carries the generic inspect<T> signature, which a plain literal stub
742+ // cannot satisfy without widening.
743+ const freshVscode = await import ( "vscode" )
744+ Object . assign ( freshVscode . workspace , {
745+ getConfiguration : ( section ?: string ) => ( {
746+ get : ( ) => undefined ,
747+ has : ( ) => false ,
748+ inspect : ( key : string ) => {
749+ if ( section === "terminal.integrated" && key === "defaultProfile.windows" ) {
750+ return { key, globalValue : "PowerShell" }
751+ }
752+ if ( section === "terminal.integrated.profiles" && key === "windows" ) {
753+ return { key, globalValue : { PowerShell : { path : psPath } } }
754+ }
755+ return undefined
756+ } ,
757+ update : async ( ) => { } ,
758+ } ) ,
759+ } )
760+ const { getShell : freshGetShell } = await import ( "../shell" )
761+ expect ( freshGetShell ( ) ) . toBe ( "C:\\Windows\\System32\\cmd.exe" )
762+ } )
763+ } )
598764} )
0 commit comments