mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-25 11:20:07 +08:00
* feat: allow custom Git Bash path for Claude Code Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * format code * format code * update i18n * fix: correct Git Bash invalid path translation key Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * test: cover null inputs for validateGitBashPath Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * refactor: rely on findGitBash for env override check Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * fix: validate env override for Git Bash path Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * chore: align Git Bash path getter with platform guard Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * test: cover env override behavior in findGitBash Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> * refactor: unify Git Bash path detection logic - Add customPath parameter to findGitBash() for config-based paths - Simplify checkGitBash IPC handler by delegating to findGitBash - Change validateGitBashPath success log level from info to debug - Only show success Alert when custom path is configured - Add tests for customPath parameter priority handling 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> --------- Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com> Co-authored-by: Claude <noreply@anthropic.com>
699 lines
22 KiB
TypeScript
699 lines
22 KiB
TypeScript
import { execFileSync } from 'child_process'
|
|
import fs from 'fs'
|
|
import path from 'path'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
import { findExecutable, findGitBash, validateGitBashPath } from '../process'
|
|
|
|
// Mock dependencies
|
|
vi.mock('child_process')
|
|
vi.mock('fs')
|
|
vi.mock('path')
|
|
|
|
// These tests only run on Windows since the functions have platform guards
|
|
describe.skipIf(process.platform !== 'win32')('process utilities', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
|
|
// Mock path.join to concatenate paths with backslashes (Windows-style)
|
|
vi.mocked(path.join).mockImplementation((...args) => args.join('\\'))
|
|
|
|
// Mock path.resolve to handle path resolution with .. support
|
|
vi.mocked(path.resolve).mockImplementation((...args) => {
|
|
let result = args.join('\\')
|
|
|
|
// Handle .. navigation
|
|
while (result.includes('\\..')) {
|
|
result = result.replace(/\\[^\\]+\\\.\./g, '')
|
|
}
|
|
|
|
// Ensure absolute path
|
|
if (!result.match(/^[A-Z]:/)) {
|
|
result = `C:\\cwd\\${result}`
|
|
}
|
|
|
|
return result
|
|
})
|
|
|
|
// Mock path.dirname
|
|
vi.mocked(path.dirname).mockImplementation((p) => {
|
|
const parts = p.split('\\')
|
|
parts.pop()
|
|
return parts.join('\\')
|
|
})
|
|
|
|
// Mock path.sep
|
|
Object.defineProperty(path, 'sep', { value: '\\', writable: true })
|
|
|
|
// Mock process.cwd()
|
|
vi.spyOn(process, 'cwd').mockReturnValue('C:\\cwd')
|
|
})
|
|
|
|
describe('findExecutable', () => {
|
|
describe('git common paths', () => {
|
|
it('should find git at Program Files path', () => {
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
process.env.ProgramFiles = 'C:\\Program Files'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
expect(fs.existsSync).toHaveBeenCalledWith(gitPath)
|
|
})
|
|
|
|
it('should find git at Program Files (x86) path', () => {
|
|
const gitPath = 'C:\\Program Files (x86)\\Git\\cmd\\git.exe'
|
|
process.env['ProgramFiles(x86)'] = 'C:\\Program Files (x86)'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
expect(fs.existsSync).toHaveBeenCalledWith(gitPath)
|
|
})
|
|
|
|
it('should use fallback paths when environment variables are not set', () => {
|
|
delete process.env.ProgramFiles
|
|
delete process.env['ProgramFiles(x86)']
|
|
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === gitPath)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
})
|
|
})
|
|
|
|
describe('where.exe PATH lookup', () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
|
|
// Common paths don't exist
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
})
|
|
|
|
it('should find executable via where.exe', () => {
|
|
const gitPath = 'C:\\Git\\bin\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
expect(execFileSync).toHaveBeenCalledWith('where.exe', ['git.exe'], {
|
|
encoding: 'utf8',
|
|
stdio: ['pipe', 'pipe', 'pipe']
|
|
})
|
|
})
|
|
|
|
it('should add .exe extension when calling where.exe', () => {
|
|
vi.mocked(execFileSync).mockImplementation(() => {
|
|
throw new Error('Not found')
|
|
})
|
|
|
|
findExecutable('node')
|
|
|
|
expect(execFileSync).toHaveBeenCalledWith('where.exe', ['node.exe'], expect.any(Object))
|
|
})
|
|
|
|
it('should handle Windows line endings (CRLF)', () => {
|
|
const gitPath1 = 'C:\\Git\\bin\\git.exe'
|
|
const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\r\n${gitPath2}\r\n`)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
// Should return the first valid path
|
|
expect(result).toBe(gitPath1)
|
|
})
|
|
|
|
it('should handle Unix line endings (LF)', () => {
|
|
const gitPath1 = 'C:\\Git\\bin\\git.exe'
|
|
const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\n${gitPath2}\n`)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath1)
|
|
})
|
|
|
|
it('should handle mixed line endings', () => {
|
|
const gitPath1 = 'C:\\Git\\bin\\git.exe'
|
|
const gitPath2 = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`${gitPath1}\r\n${gitPath2}\n`)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath1)
|
|
})
|
|
|
|
it('should trim whitespace from paths', () => {
|
|
const gitPath = 'C:\\Git\\bin\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(` ${gitPath} \n`)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
})
|
|
|
|
it('should filter empty lines', () => {
|
|
const gitPath = 'C:\\Git\\bin\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`\n\n${gitPath}\n\n`)
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(gitPath)
|
|
})
|
|
})
|
|
|
|
describe('security checks', () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
})
|
|
|
|
it('should skip executables in current directory', () => {
|
|
const maliciousPath = 'C:\\cwd\\git.exe'
|
|
const safePath = 'C:\\Git\\bin\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`${maliciousPath}\n${safePath}`)
|
|
|
|
vi.mocked(path.resolve).mockImplementation((p) => {
|
|
if (p.includes('cwd\\git.exe')) return 'c:\\cwd\\git.exe'
|
|
return 'c:\\git\\bin\\git.exe'
|
|
})
|
|
|
|
vi.mocked(path.dirname).mockImplementation((p) => {
|
|
if (p.includes('cwd\\git.exe')) return 'c:\\cwd'
|
|
return 'c:\\git\\bin'
|
|
})
|
|
|
|
const result = findExecutable('git')
|
|
|
|
// Should skip malicious path and return safe path
|
|
expect(result).toBe(safePath)
|
|
})
|
|
|
|
it('should skip executables in current directory subdirectories', () => {
|
|
const maliciousPath = 'C:\\cwd\\subdir\\git.exe'
|
|
const safePath = 'C:\\Git\\bin\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(`${maliciousPath}\n${safePath}`)
|
|
|
|
vi.mocked(path.resolve).mockImplementation((p) => {
|
|
if (p.includes('cwd\\subdir')) return 'c:\\cwd\\subdir\\git.exe'
|
|
return 'c:\\git\\bin\\git.exe'
|
|
})
|
|
|
|
vi.mocked(path.dirname).mockImplementation((p) => {
|
|
if (p.includes('cwd\\subdir')) return 'c:\\cwd\\subdir'
|
|
return 'c:\\git\\bin'
|
|
})
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBe(safePath)
|
|
})
|
|
|
|
it('should return null when only malicious executables are found', () => {
|
|
const maliciousPath = 'C:\\cwd\\git.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(maliciousPath)
|
|
|
|
vi.mocked(path.resolve).mockReturnValue('c:\\cwd\\git.exe')
|
|
vi.mocked(path.dirname).mockReturnValue('c:\\cwd')
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('error handling', () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
})
|
|
|
|
it('should return null when where.exe fails', () => {
|
|
vi.mocked(execFileSync).mockImplementation(() => {
|
|
throw new Error('Command failed')
|
|
})
|
|
|
|
const result = findExecutable('nonexistent')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('should return null when where.exe returns empty output', () => {
|
|
vi.mocked(execFileSync).mockReturnValue('')
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('should return null when where.exe returns only whitespace', () => {
|
|
vi.mocked(execFileSync).mockReturnValue(' \n\n ')
|
|
|
|
const result = findExecutable('git')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('non-git executables', () => {
|
|
beforeEach(() => {
|
|
Object.defineProperty(process, 'platform', { value: 'win32', writable: true })
|
|
})
|
|
|
|
it('should skip common paths check for non-git executables', () => {
|
|
const nodePath = 'C:\\Program Files\\nodejs\\node.exe'
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(nodePath)
|
|
|
|
const result = findExecutable('node')
|
|
|
|
expect(result).toBe(nodePath)
|
|
// Should not check common Git paths
|
|
expect(fs.existsSync).not.toHaveBeenCalledWith(expect.stringContaining('Git\\cmd\\node.exe'))
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('validateGitBashPath', () => {
|
|
it('returns null when path is null', () => {
|
|
const result = validateGitBashPath(null)
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns null when path is undefined', () => {
|
|
const result = validateGitBashPath(undefined)
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns normalized path when valid bash.exe exists', () => {
|
|
const customPath = 'C:\\PortableGit\\bin\\bash.exe'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === 'C:\\PortableGit\\bin\\bash.exe')
|
|
|
|
const result = validateGitBashPath(customPath)
|
|
|
|
expect(result).toBe('C:\\PortableGit\\bin\\bash.exe')
|
|
})
|
|
|
|
it('returns null when file does not exist', () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
|
|
const result = validateGitBashPath('C:\\missing\\bash.exe')
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('returns null when path is not bash.exe', () => {
|
|
const customPath = 'C:\\PortableGit\\bin\\git.exe'
|
|
vi.mocked(fs.existsSync).mockReturnValue(true)
|
|
|
|
const result = validateGitBashPath(customPath)
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('findGitBash', () => {
|
|
describe('customPath parameter', () => {
|
|
beforeEach(() => {
|
|
delete process.env.CLAUDE_CODE_GIT_BASH_PATH
|
|
})
|
|
|
|
it('uses customPath when valid', () => {
|
|
const customPath = 'C:\\CustomGit\\bin\\bash.exe'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === customPath)
|
|
|
|
const result = findGitBash(customPath)
|
|
|
|
expect(result).toBe(customPath)
|
|
expect(execFileSync).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('falls back when customPath is invalid', () => {
|
|
const customPath = 'C:\\Invalid\\bash.exe'
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
if (p === customPath) return false
|
|
if (p === gitPath) return true
|
|
if (p === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash(customPath)
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('prioritizes customPath over env override', () => {
|
|
const customPath = 'C:\\CustomGit\\bin\\bash.exe'
|
|
const envPath = 'C:\\EnvGit\\bin\\bash.exe'
|
|
process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === customPath || p === envPath)
|
|
|
|
const result = findGitBash(customPath)
|
|
|
|
expect(result).toBe(customPath)
|
|
})
|
|
})
|
|
|
|
describe('env override', () => {
|
|
beforeEach(() => {
|
|
delete process.env.CLAUDE_CODE_GIT_BASH_PATH
|
|
})
|
|
|
|
it('uses CLAUDE_CODE_GIT_BASH_PATH when valid', () => {
|
|
const envPath = 'C:\\OverrideGit\\bin\\bash.exe'
|
|
process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === envPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(envPath)
|
|
expect(execFileSync).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('falls back when CLAUDE_CODE_GIT_BASH_PATH is invalid', () => {
|
|
const envPath = 'C:\\Invalid\\bash.exe'
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
|
|
process.env.CLAUDE_CODE_GIT_BASH_PATH = envPath
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
if (p === envPath) return false
|
|
if (p === gitPath) return true
|
|
if (p === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
})
|
|
|
|
describe('git.exe path derivation', () => {
|
|
it('should derive bash.exe from standard Git installation (Git/cmd/git.exe)', () => {
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
|
|
// findExecutable will find git at common path
|
|
process.env.ProgramFiles = 'C:\\Program Files'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
return p === gitPath || p === bashPath
|
|
})
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should derive bash.exe from portable Git installation (Git/bin/git.exe)', () => {
|
|
const gitPath = 'C:\\PortableGit\\bin\\git.exe'
|
|
const bashPath = 'C:\\PortableGit\\bin\\bash.exe'
|
|
|
|
// Mock: common git paths don't exist, but where.exe finds portable git
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common git paths don't exist
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// Portable bash.exe exists at Git/bin/bash.exe (second path in possibleBashPaths)
|
|
if (pathStr === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
// where.exe returns portable git path
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should derive bash.exe from MSYS2 Git installation (Git/usr/bin/bash.exe)', () => {
|
|
const gitPath = 'C:\\msys64\\usr\\bin\\git.exe'
|
|
const bashPath = 'C:\\msys64\\usr\\bin\\bash.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common git paths don't exist
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// MSYS2 bash.exe exists at usr/bin/bash.exe (third path in possibleBashPaths)
|
|
if (pathStr === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should try multiple bash.exe locations in order', () => {
|
|
const gitPath = 'C:\\Git\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Git\\bin\\bash.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common git paths don't exist
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// Standard path exists (first in possibleBashPaths)
|
|
if (pathStr === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should handle when git.exe is found but bash.exe is not at any derived location', () => {
|
|
const gitPath = 'C:\\Git\\cmd\\git.exe'
|
|
|
|
// git.exe exists via where.exe, but bash.exe doesn't exist at any derived location
|
|
vi.mocked(fs.existsSync).mockImplementation(() => {
|
|
// Only return false for all bash.exe checks
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
// Should fall back to common paths check
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('common paths fallback', () => {
|
|
beforeEach(() => {
|
|
// git.exe not found
|
|
vi.mocked(execFileSync).mockImplementation(() => {
|
|
throw new Error('Not found')
|
|
})
|
|
})
|
|
|
|
it('should check Program Files path', () => {
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
process.env.ProgramFiles = 'C:\\Program Files'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should check Program Files (x86) path', () => {
|
|
const bashPath = 'C:\\Program Files (x86)\\Git\\bin\\bash.exe'
|
|
process.env['ProgramFiles(x86)'] = 'C:\\Program Files (x86)'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should check LOCALAPPDATA path', () => {
|
|
const bashPath = 'C:\\Users\\User\\AppData\\Local\\Programs\\Git\\bin\\bash.exe'
|
|
process.env.LOCALAPPDATA = 'C:\\Users\\User\\AppData\\Local'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should skip LOCALAPPDATA check when environment variable is not set', () => {
|
|
delete process.env.LOCALAPPDATA
|
|
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBeNull()
|
|
// Should not check invalid path with empty LOCALAPPDATA
|
|
expect(fs.existsSync).not.toHaveBeenCalledWith(expect.stringContaining('undefined'))
|
|
})
|
|
|
|
it('should use fallback values when environment variables are not set', () => {
|
|
delete process.env.ProgramFiles
|
|
delete process.env['ProgramFiles(x86)']
|
|
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === bashPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
})
|
|
|
|
describe('priority order', () => {
|
|
it('should prioritize git.exe derivation over common paths', () => {
|
|
const gitPath = 'C:\\CustomPath\\Git\\cmd\\git.exe'
|
|
const derivedBashPath = 'C:\\CustomPath\\Git\\bin\\bash.exe'
|
|
const commonBashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
|
|
// Both exist
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common git paths don't exist (so findExecutable uses where.exe)
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// Both bash paths exist, but derived should be checked first
|
|
if (pathStr === derivedBashPath) return true
|
|
if (pathStr === commonBashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
// Should return derived path, not common path
|
|
expect(result).toBe(derivedBashPath)
|
|
})
|
|
})
|
|
|
|
describe('error scenarios', () => {
|
|
it('should return null when Git is not installed anywhere', () => {
|
|
vi.mocked(fs.existsSync).mockReturnValue(false)
|
|
vi.mocked(execFileSync).mockImplementation(() => {
|
|
throw new Error('Not found')
|
|
})
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
it('should return null when git.exe exists but bash.exe does not', () => {
|
|
const gitPath = 'C:\\Git\\cmd\\git.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
// git.exe exists, but no bash.exe anywhere
|
|
return p === gitPath
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe('real-world scenarios', () => {
|
|
it('should handle official Git for Windows installer', () => {
|
|
const gitPath = 'C:\\Program Files\\Git\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Program Files\\Git\\bin\\bash.exe'
|
|
|
|
process.env.ProgramFiles = 'C:\\Program Files'
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
return p === gitPath || p === bashPath
|
|
})
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should handle portable Git installation in custom directory', () => {
|
|
const gitPath = 'D:\\DevTools\\PortableGit\\bin\\git.exe'
|
|
const bashPath = 'D:\\DevTools\\PortableGit\\bin\\bash.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common paths don't exist
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// Portable Git paths exist (portable uses second path: Git/bin/bash.exe)
|
|
if (pathStr === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
|
|
it('should handle Git installed via Scoop', () => {
|
|
// Scoop typically installs to %USERPROFILE%\scoop\apps\git\current
|
|
const gitPath = 'C:\\Users\\User\\scoop\\apps\\git\\current\\cmd\\git.exe'
|
|
const bashPath = 'C:\\Users\\User\\scoop\\apps\\git\\current\\bin\\bash.exe'
|
|
|
|
vi.mocked(fs.existsSync).mockImplementation((p) => {
|
|
const pathStr = p?.toString() || ''
|
|
// Common paths don't exist
|
|
if (pathStr.includes('Program Files\\Git\\cmd\\git.exe')) return false
|
|
if (pathStr.includes('Program Files (x86)\\Git\\cmd\\git.exe')) return false
|
|
// Scoop bash path exists (standard structure: cmd -> bin)
|
|
if (pathStr === bashPath) return true
|
|
return false
|
|
})
|
|
|
|
vi.mocked(execFileSync).mockReturnValue(gitPath)
|
|
|
|
const result = findGitBash()
|
|
|
|
expect(result).toBe(bashPath)
|
|
})
|
|
})
|
|
})
|
|
})
|