Files
orca/src/main/git-bash.test.ts
T
Jinwoo Hong a05649de91 fix(terminal): prove an idle Git Bash prompt through its bin launcher (#22752)
* fix(terminal): prove an idle Git Bash prompt through its bin launcher

Git for Windows' bin\bash.exe is a launcher that runs usr\bin\bash.exe as a
child and waits, so an idle Git Bash pane's job always holds two pids and the
Windows shell proof never confirmed it. Accept exactly the launcher plus its
direct bash.exe child, checked against the identity process table.

* test(terminal): wait for the Git Bash prompt before asserting the hand-off job

* fix(terminal): prove a Git Bash prompt as one unbranched MSYS bash chain

Orca launches Git Bash as bin\bash.exe -c "chcp.com ...; exec \"$BASH\" ... -i",
and each MSYS exec leaves its pre-exec process alive as a stub, so an idle
pane's job is launcher -> stub -> interactive bash. Accept any job that is one
parent-to-child chain rooted at the launcher whose every later member is
bash.exe, instead of a fixed two-process shape.

* ci: register the Git Bash shell-proof win32 test in the package-test list

* fix(terminal): read the spawned shell as a path, and keep one shell map

A spawned shell path with a space (/Users/John Doe/bin/zsh) was split as a
command line, so the POSIX proof compared against "john" and never
confirmed. Local panes now keep only the spawned shell path and derive the
name from it; the Git Bash chain walk drops guards the member check already
covers.
2026-09-25 01:00:36 -04:00

173 lines
5.7 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import {
getGitBashCandidatePaths,
isGitForWindowsBashLauncherPath,
isWindowsGitBashShellPath,
resolveGitBashPath,
resolveWindowsGitBashShellPath
} from './git-bash'
describe('Git Bash path discovery', () => {
it('includes common Git for Windows install locations and PATH-derived fallbacks', () => {
const candidates = getGitBashCandidatePaths({
ProgramFiles: 'C:\\Program Files',
LOCALAPPDATA: 'C:\\Users\\alice\\AppData\\Local',
Path: '"C:\\Program Files\\Git\\cmd";C:\\tools;C:\\PortableGit\\bin'
})
expect(candidates).toContain('C:\\Program Files\\Git\\bin\\bash.exe')
expect(candidates).toContain('C:\\Users\\alice\\AppData\\Local\\Programs\\Git\\bin\\bash.exe')
expect(candidates).toContain('C:\\Program Files\\Git\\usr\\bin\\bash.exe')
expect(candidates).toContain('C:\\PortableGit\\bin\\bash.exe')
expect(candidates).not.toContain('C:\\tools\\bash.exe')
})
it('resolves the first existing common bash.exe path on Windows', () => {
const exists = vi.fn((path: string) => path === 'C:\\Program Files\\Git\\bin\\bash.exe')
expect(
resolveGitBashPath({
platform: 'win32',
env: { ProgramFiles: 'C:\\Program Files' },
exists
})
).toBe('C:\\Program Files\\Git\\bin\\bash.exe')
})
it('does not expose Git Bash discovery on non-Windows hosts', () => {
expect(
resolveGitBashPath({
platform: 'darwin',
env: { ProgramFiles: 'C:\\Program Files' },
exists: () => true
})
).toBeNull()
})
it('maps the persisted Git Bash sentinel to a discovered bash.exe path', () => {
expect(
resolveWindowsGitBashShellPath('git-bash', {
platform: 'win32',
env: { LOCALAPPDATA: 'C:\\Users\\alice\\AppData\\Local' },
exists: (path) => path === 'C:\\Users\\alice\\AppData\\Local\\Programs\\Git\\bin\\bash.exe'
})
).toBe('C:\\Users\\alice\\AppData\\Local\\Programs\\Git\\bin\\bash.exe')
})
// Why: resolveWindowsGitBashShellPath has no platform guard of its own, so passing options
// explicitly keeps these assertions meaningful on non-Windows CI hosts.
it('honors an explicit bash.exe path for future user-configurable launch paths', () => {
expect(
resolveWindowsGitBashShellPath('D:\\PortableGit\\bin\\bash.exe', {
platform: 'win32',
env: {},
exists: (path) => path === 'D:\\PortableGit\\bin\\bash.exe'
})
).toBe('D:\\PortableGit\\bin\\bash.exe')
})
it('rejects an explicit Git Bash path that is no longer installed', () => {
expect(
resolveWindowsGitBashShellPath('D:\\PortableGit\\bin\\bash.exe', {
platform: 'win32',
env: {},
exists: () => false
})
).toBeNull()
})
it('resolves a bare bash entry through Git Bash discovery', () => {
expect(
resolveWindowsGitBashShellPath('bash', {
platform: 'win32',
env: { ProgramFiles: 'C:\\Program Files' },
exists: (path) => path === 'C:\\Program Files\\Git\\bin\\bash.exe'
})
).toBe('C:\\Program Files\\Git\\bin\\bash.exe')
})
it('returns null for a bare bash entry when Git Bash is not installed', () => {
expect(
resolveWindowsGitBashShellPath('bash', {
platform: 'win32',
env: { ProgramFiles: 'C:\\Program Files' },
exists: () => false
})
).toBeNull()
})
it('resolves an extension-less Git Bash path to the bash.exe it names', () => {
expect(
resolveWindowsGitBashShellPath('C:\\Program Files\\Git\\bin\\bash', {
platform: 'win32',
env: {},
exists: (path) => path === 'C:\\Program Files\\Git\\bin\\bash.exe'
})
).toBe('C:\\Program Files\\Git\\bin\\bash.exe')
})
it('returns null for an extension-less Git Bash path with no bash.exe beside it', () => {
expect(
resolveWindowsGitBashShellPath('C:\\Program Files\\Git\\bin\\bash', {
platform: 'win32',
env: {},
exists: () => false
})
).toBeNull()
})
it('does not treat an extension-less non-Git bash path as Git Bash', () => {
expect(
resolveWindowsGitBashShellPath('C:\\cygwin64\\bin\\bash', {
platform: 'win32',
env: {},
exists: () => true
})
).toBeNull()
})
it('does not probe a bash-prefixed path that is not bash itself', () => {
expect(
resolveWindowsGitBashShellPath('C:\\Program Files\\Git\\bin\\bash.old', {
platform: 'win32',
env: {},
exists: () => true
})
).toBeNull()
})
it('recognizes Git Bash executable paths case-insensitively', () => {
expect(isWindowsGitBashShellPath('D:\\PortableGit\\BIN\\BASH.EXE')).toBe(true)
})
it('does not classify arbitrary bash.exe paths as Git Bash', () => {
expect(
resolveWindowsGitBashShellPath('C:\\msys64\\usr\\bin\\bash.exe', {
platform: 'win32',
env: {},
exists: () => true
})
).toBeNull()
expect(isWindowsGitBashShellPath('C:\\cygwin64\\bin\\bash.exe')).toBe(false)
})
it('ignores non-Git bash.exe candidates discovered through PATH', () => {
expect(
resolveGitBashPath({
platform: 'win32',
env: { Path: 'C:\\msys64\\usr\\bin' },
exists: (path) => path === 'C:\\msys64\\usr\\bin\\bash.exe'
})
).toBeNull()
})
it('identifies only the bin\\bash.exe launcher, not the MSYS bash it runs', () => {
expect(isGitForWindowsBashLauncherPath('C:\\Program Files\\Git\\bin\\bash.exe')).toBe(true)
expect(isGitForWindowsBashLauncherPath('D:\\PortableGit\\bin\\bash.exe')).toBe(true)
expect(isGitForWindowsBashLauncherPath('C:\\Program Files\\Git\\usr\\bin\\bash.exe')).toBe(
false
)
expect(isGitForWindowsBashLauncherPath('bash.exe')).toBe(false)
})
})