fix(worktrees): retain missing-path recovery when git marker vanishes

This commit is contained in:
Neil
2026-09-14 05:31:19 -07:00
parent c2022351fb
commit 8b3ae1e3f4
3 changed files with 45 additions and 23 deletions
+31 -22
View File
@@ -402,30 +402,39 @@ describe('registerWorktreeHandlers', () => {
}
})
it('retries stale Git registration cleanup after prior local filesystem recovery', async () => {
setPlatform('win32')
const missingWorktreePath = 'C:\\workspace\\already-removed'
const worktreeId = `repo-1::${missingWorktreePath}`
const registeredWorktrees = mockKnownFeatureWorktree(missingWorktreePath)
listWorktreesMock.mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([])
store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta())
it.each([false, true])(
'retries missing registration cleanup (prunable marker: %s)',
async (prunableMarker) => {
setPlatform('win32')
const missingWorktreePath = prunableMarker
? 'C:\\workspace\\already-removed\\.git'
: 'C:\\workspace\\already-removed'
const worktreeId = `repo-1::${missingWorktreePath}`
const registeredWorktrees = mockKnownFeatureWorktree(missingWorktreePath).map((row) =>
prunableMarker && row.path === missingWorktreePath
? { ...row, branch: 'refs/heads/feature', prunable: true }
: row
)
listWorktreesMock.mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([])
store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta())
const result = await handlers['worktrees:remove'](null, {
worktreeId,
force: true
})
const result = await handlers['worktrees:remove'](null, {
worktreeId,
force: true
})
expect(result).toEqual({
preservedBranch: { branchName: 'feature', head: 'feature' }
})
expect(runHookMock).not.toHaveBeenCalled()
expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
})
expect(result).toEqual({
preservedBranch: { branchName: 'feature', head: 'feature' }
})
expect(runHookMock).not.toHaveBeenCalled()
expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local')
}
)
it('cleans a prunable Git-file row before archive or checkout teardown', async () => {
const root = await mkdtemp(join(tmpdir(), 'orca-prunable-ipc-'))
@@ -52,6 +52,10 @@ describe('prunable Git-file registration proof', () => {
await expect(isPrunableGitFileWorktree(worktree)).resolves.toBe(false)
}
)
it('leaves a vanished marker to existing missing-path recovery', async () => {
statPath.mockRejectedValue(Object.assign(new Error('marker vanished'), { code: 'ENOENT' }))
await expect(isPrunableGitFileWorktree(worktree)).resolves.toBe(false)
})
it('does not turn host failure into cleanup permission', async () => {
statPath.mockRejectedValue(new Error('host unavailable'))
await expect(isPrunableGitFileWorktree(worktree)).rejects.toThrow('host unavailable')
+10 -1
View File
@@ -1,3 +1,4 @@
import { isENOENT } from './ipc/filesystem-path-containment'
import type { GitWorktreeInfo } from '../shared/worktree/types'
import type { LocalWorktreeFilesystemOptions } from './local-worktree-filesystem'
import { getLocalWorktreePathAccess, toLocalWorktreeRuntimePath } from './local-worktree-filesystem'
@@ -20,7 +21,15 @@ export async function isPrunableGitFileWorktree(
return false
}
const access = getLocalWorktreePathAccess(options)
const entry = await access.statPath(toLocalWorktreeRuntimePath(worktree.path, options))
const entry = await access
.statPath(toLocalWorktreeRuntimePath(worktree.path, options))
.catch((error: unknown) => {
// A vanished marker leaves missing-path recovery to its existing stricter gate.
if (isENOENT(error)) {
return null
}
throw error
})
if (!entry || typeof entry !== 'object') {
return false
}