hermes-agent/apps/desktop/electron/git-review-ops.test.ts
mark e361c5e204 fix(desktop): support spaced Windows Git paths in review
simple-git's custom-binary validation rejects paths containing spaces, so
the default Windows Git install (C:\Program Files\Git\cmd\git.exe) made
every Review pane git call throw and the pane silently showed 'No diffs'.

The binary is resolved inside the Electron main process from known install
locations or PATH — never renderer/user input — so for spaced paths we opt
into simple-git's supported unsafe.allowUnsafeCustomBinary escape hatch
rather than falling back to PATH (often absent in GUI-launched apps).

Simplified from PR #64713 by @unsupportedpastels; supersedes the 8.3
short-path approaches in #55337/#60156.

Fixes #54888
2026-07-19 12:22:23 -07:00

89 lines
2.7 KiB
TypeScript

import assert from 'node:assert/strict'
import { execFileSync } from 'node:child_process'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, test } from 'vitest'
import { gitFor, repoStatus, resolveRenamePath } from './git-review-ops'
const tempDirs: string[] = []
afterEach(() => {
for (const dir of tempDirs.splice(0)) {
fs.rmSync(dir, { force: true, recursive: true })
}
})
function makeRepo() {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'hermes-desktop-git-status-'))
tempDirs.push(dir)
execFileSync('git', ['init', '-q'], { cwd: dir })
execFileSync('git', ['config', 'user.email', 'hermes-test@example.com'], { cwd: dir })
execFileSync('git', ['config', 'user.name', 'Hermes Test'], { cwd: dir })
fs.writeFileSync(path.join(dir, 'tracked.txt'), 'tracked\n')
execFileSync('git', ['add', 'tracked.txt'], { cwd: dir })
execFileSync('git', ['commit', '-qm', 'initial'], { cwd: dir })
return dir
}
test('resolveRenamePath: plain path is unchanged', () => {
assert.equal(resolveRenamePath('src/a.ts'), 'src/a.ts')
})
test('gitFor accepts an internally resolved git binary path containing spaces', () => {
assert.doesNotThrow(() => gitFor(process.cwd(), 'C:\\Program Files\\Git\\cmd\\git.exe'))
})
test('gitFor runs git through a spaced binary path', async () => {
if (process.platform !== 'win32') {
return
}
const gitBin = path.join(process.env.ProgramFiles || String.raw`C:\Program Files`, 'Git', 'cmd', 'git.exe')
if (!fs.existsSync(gitBin)) {
return
}
const repo = makeRepo()
fs.writeFileSync(path.join(repo, 'changed.txt'), 'review me\n')
const status = await gitFor(repo, gitBin).status()
assert.equal(status.not_added.includes('changed.txt'), true)
})
test('resolveRenamePath: simple rename resolves to the new path', () => {
assert.equal(resolveRenamePath('old.ts => new.ts'), 'new.ts')
})
test('resolveRenamePath: brace rename resolves to the new path', () => {
assert.equal(resolveRenamePath('src/{old => new}/file.ts'), 'src/new/file.ts')
})
test('resolveRenamePath: brace rename collapsing a segment', () => {
assert.equal(resolveRenamePath('src/{lib => }/file.ts'), 'src/file.ts')
})
test('repoStatus reports an untracked directory without recursively listing its contents', async () => {
const dir = makeRepo()
const nested = path.join(dir, 'generated', 'deep')
fs.mkdirSync(nested, { recursive: true })
fs.writeFileSync(path.join(nested, 'large-output.txt'), 'generated\n')
const status = await repoStatus(dir, 'git')
assert.ok(status)
assert.equal(status.untracked, 1)
assert.equal(status.changed, 1)
assert.deepEqual(
status.files.map(file => file.path),
['generated/']
)
})