mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
When git lists the same folder twice (a leftover worktree registration that points at the main checkout), Orca's runtime listing turned each line into its own worktree with the same id, so `orca worktree current`, `active` and `branch:` failed with selector_ambiguous, and paired clients saw a duplicate row. The runtime scan now keeps git's first row per folder, the rule the desktop sidebar already uses. Separately, for a bare or separate-git-dir repo added through a linked worktree, the scan no longer relabels the main row with that worktree's folder (it relabels only when the folder's git dir is the common git dir), so the worktree keeps its own row and branch in the CLI and the sidebar. No extra git command runs. Part of #23631: the "Profile state writer command timed out" toast in that issue has a separate cause.
139 lines
4.7 KiB
TypeScript
139 lines
4.7 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { gitExecFileAsyncMock } = vi.hoisted(() => ({
|
|
gitExecFileAsyncMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./runner', () => ({
|
|
gitExecFileAsync: gitExecFileAsyncMock,
|
|
gitExecFileSync: vi.fn(),
|
|
translateWslOutputPaths: (output: string) => output
|
|
}))
|
|
|
|
import { clearGitCapabilityStateForTests } from './git-capability-state'
|
|
import { listWorktrees } from './worktree'
|
|
|
|
const WORKTREE_LIST_OUTPUT = `worktree /repo
|
|
HEAD abc123
|
|
branch refs/heads/main
|
|
`
|
|
|
|
describe('worktree Git capabilities', () => {
|
|
beforeEach(() => {
|
|
clearGitCapabilityStateForTests()
|
|
gitExecFileAsyncMock.mockReset()
|
|
})
|
|
|
|
it('does not repeat a known-unsupported -z probe on later scans', async () => {
|
|
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
|
|
if (args.includes('-z')) {
|
|
return Promise.reject(
|
|
Object.assign(new Error('git usage error'), {
|
|
code: 129,
|
|
stderr: 'usage: git worktree list [<options>]\n'
|
|
})
|
|
)
|
|
}
|
|
return Promise.resolve({ stdout: WORKTREE_LIST_OUTPUT })
|
|
})
|
|
|
|
await listWorktrees('/repo')
|
|
await listWorktrees('/repo')
|
|
|
|
expect(gitExecFileAsyncMock.mock.calls.map(([args]) => args)).toEqual([
|
|
['worktree', 'list', '--porcelain', '-z'],
|
|
['worktree', 'list', '--porcelain'],
|
|
['worktree', 'list', '--porcelain']
|
|
])
|
|
})
|
|
|
|
it('keeps native and WSL Git capability results separate', async () => {
|
|
gitExecFileAsyncMock.mockImplementation(
|
|
(args: string[], options: { cwd: string; wslDistro?: string }) => {
|
|
if (args.includes('-z') && !options.wslDistro) {
|
|
return Promise.reject(
|
|
Object.assign(new Error('git usage error'), {
|
|
code: 129,
|
|
stderr: 'usage: git worktree list [<options>]\n'
|
|
})
|
|
)
|
|
}
|
|
return Promise.resolve({
|
|
stdout: `worktree ${options.cwd}\nHEAD abc123\nbranch refs/heads/main\n`
|
|
})
|
|
}
|
|
)
|
|
|
|
await listWorktrees('/native-repo')
|
|
await listWorktrees('/wsl-repo', { wslDistro: 'Ubuntu' })
|
|
|
|
expect(
|
|
gitExecFileAsyncMock.mock.calls.map(([args, options]) => ({
|
|
args,
|
|
wslDistro: options.wslDistro
|
|
}))
|
|
).toEqual([
|
|
{ args: ['worktree', 'list', '--porcelain', '-z'], wslDistro: undefined },
|
|
{ args: ['worktree', 'list', '--porcelain'], wslDistro: undefined },
|
|
{ args: ['worktree', 'list', '--porcelain', '-z'], wslDistro: 'Ubuntu' }
|
|
])
|
|
})
|
|
|
|
it('does not repeat a known-unsupported rev-parse --path-format probe', async () => {
|
|
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
|
|
if (args[0] === 'worktree') {
|
|
return Promise.resolve({
|
|
stdout: 'worktree /git-store/project.git\nHEAD abc123\nbranch refs/heads/main\n'
|
|
})
|
|
}
|
|
if (args.includes('--path-format=absolute')) {
|
|
return Promise.reject(
|
|
Object.assign(new Error('unknown option: --path-format=absolute'), {
|
|
stderr: 'error: unknown option `path-format=absolute`\n'
|
|
})
|
|
)
|
|
}
|
|
return Promise.resolve({ stdout: '/repo\n/git-store/project.git\n/git-store/project.git\n' })
|
|
})
|
|
|
|
await listWorktrees('/repo')
|
|
await listWorktrees('/repo')
|
|
|
|
const revParseCalls = gitExecFileAsyncMock.mock.calls.filter(
|
|
([args]) => (args as string[])[0] === 'rev-parse'
|
|
)
|
|
expect(revParseCalls.map(([args]) => args)).toEqual([
|
|
['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'],
|
|
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir'],
|
|
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir']
|
|
])
|
|
})
|
|
|
|
it('remembers old Git that echoes --path-format while exiting successfully', async () => {
|
|
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
|
|
if (args[0] === 'worktree') {
|
|
return Promise.resolve({
|
|
stdout: 'worktree /git-store/project.git\nHEAD abc123\nbranch refs/heads/main\n'
|
|
})
|
|
}
|
|
if (args.includes('--path-format=absolute')) {
|
|
return Promise.resolve({
|
|
stdout: '--path-format=absolute\n/repo\n/git-store/project.git\n/git-store/project.git\n'
|
|
})
|
|
}
|
|
return Promise.resolve({ stdout: '/repo\n/git-store/project.git\n/git-store/project.git\n' })
|
|
})
|
|
|
|
await listWorktrees('/repo')
|
|
await listWorktrees('/repo')
|
|
|
|
const revParseCalls = gitExecFileAsyncMock.mock.calls.filter(
|
|
([args]) => (args as string[])[0] === 'rev-parse'
|
|
)
|
|
expect(revParseCalls.map(([args]) => args)).toEqual([
|
|
['rev-parse', '--path-format=absolute', '--show-toplevel', '--git-common-dir', '--git-dir'],
|
|
['rev-parse', '--show-toplevel', '--git-common-dir', '--git-dir']
|
|
])
|
|
})
|
|
})
|