mirror of
https://github.com/stablyai/orca.git
synced 2026-10-31 17:17:38 +00:00
fix(worktrees): skip missing roots in headless scans
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { runProcess } from '@orca/process-host'
|
||||
import { lstat, readFile } from 'node:fs/promises'
|
||||
import { isDefinitiveAbsence } from '../shared/definitive-filesystem-absence'
|
||||
import { buildWslExecArgs, quotePosixShell } from '../shared/wsl-login-shell-command'
|
||||
import { removeHostTree } from './host-tree-removal'
|
||||
import { toLinuxPath } from './wsl'
|
||||
@@ -119,6 +120,21 @@ export function getLocalWorktreePathAccess(
|
||||
}
|
||||
}
|
||||
|
||||
export async function isLocalWorktreePathMissing(
|
||||
path: string,
|
||||
options: LocalWorktreeFilesystemOptions = {}
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
await getLocalWorktreePathAccess(options).statPath(path)
|
||||
return false
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return true
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function removeLocalWorktreePath(
|
||||
targetPath: string,
|
||||
options: LocalWorktreeFilesystemOptions = {}
|
||||
|
||||
@@ -1,17 +1,14 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const { listWorktreeGraphMock, listWorktreesMock, listWorktreesStrictMock, statMock } = vi.hoisted(
|
||||
() => ({
|
||||
const { listWorktreeGraphMock, listWorktreesMock, listWorktreesStrictMock, missingPathMock } =
|
||||
vi.hoisted(() => ({
|
||||
listWorktreeGraphMock: vi.fn(),
|
||||
listWorktreesMock: vi.fn(),
|
||||
listWorktreesStrictMock: vi.fn(),
|
||||
statMock: vi.fn()
|
||||
})
|
||||
)
|
||||
missingPathMock: vi.fn()
|
||||
}))
|
||||
|
||||
vi.mock('./local-worktree-filesystem', () => ({
|
||||
getLocalWorktreePathAccess: () => ({ statPath: statMock })
|
||||
}))
|
||||
vi.mock('./local-worktree-filesystem', () => ({ isLocalWorktreePathMissing: missingPathMock }))
|
||||
|
||||
vi.mock('./git/worktree', () => ({
|
||||
listWorktreeGraph: listWorktreeGraphMock,
|
||||
@@ -30,32 +27,12 @@ import {
|
||||
} from './repo-worktrees'
|
||||
import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability'
|
||||
|
||||
const testGitRepo = {
|
||||
id: 'repo-1',
|
||||
path: '/workspace/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#000',
|
||||
addedAt: 0,
|
||||
kind: 'git' as const
|
||||
}
|
||||
|
||||
describe('repo-worktrees', () => {
|
||||
beforeEach(() => {
|
||||
listWorktreeGraphMock.mockReset()
|
||||
listWorktreesMock.mockReset()
|
||||
listWorktreesStrictMock.mockReset()
|
||||
statMock.mockReset().mockResolvedValue({})
|
||||
})
|
||||
|
||||
it('skips Git on each missing-folder scan and resumes when the folder returns', async () => {
|
||||
statMock.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' }))
|
||||
statMock.mockRejectedValueOnce(Object.assign(new Error('missing'), { code: 'ENOENT' }))
|
||||
expect(await listRepoWorktreesForDetectedScan(testGitRepo)).toEqual([])
|
||||
expect(await listRepoWorktreesForDetectedScan(testGitRepo)).toEqual([])
|
||||
expect(listWorktreesStrictMock).not.toHaveBeenCalled()
|
||||
listWorktreesStrictMock.mockResolvedValueOnce([])
|
||||
await listRepoWorktreesForDetectedScan(testGitRepo)
|
||||
expect(listWorktreesStrictMock).toHaveBeenCalledOnce()
|
||||
missingPathMock.mockReset().mockResolvedValue(false)
|
||||
})
|
||||
|
||||
it('creates a stable synthetic worktree for folder repos', () => {
|
||||
@@ -128,7 +105,17 @@ describe('repo-worktrees', () => {
|
||||
])
|
||||
const signal = new AbortController().signal
|
||||
|
||||
const result = await listRepoWorktrees(testGitRepo, { signal })
|
||||
const result = await listRepoWorktrees(
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/workspace/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#000',
|
||||
addedAt: 0,
|
||||
kind: 'git'
|
||||
},
|
||||
{ signal }
|
||||
)
|
||||
|
||||
expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { signal })
|
||||
expect(result).toHaveLength(1)
|
||||
@@ -140,7 +127,14 @@ describe('repo-worktrees', () => {
|
||||
{ path: '/workspace/repo', head: 'abc', branch: '', isBare: false, isMainWorktree: true }
|
||||
])
|
||||
|
||||
const result = await listRepoWorktreeGraph(testGitRepo)
|
||||
const result = await listRepoWorktreeGraph({
|
||||
id: 'repo-1',
|
||||
path: '/workspace/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#000',
|
||||
addedAt: 0,
|
||||
kind: 'git'
|
||||
})
|
||||
|
||||
expect(listWorktreeGraphMock).toHaveBeenCalledWith('/workspace/repo')
|
||||
expect(listWorktreesMock).not.toHaveBeenCalled()
|
||||
@@ -176,10 +170,17 @@ describe('repo-worktrees', () => {
|
||||
])
|
||||
const signal = new AbortController().signal
|
||||
|
||||
const result = await listLocalRepoWorktreesStrict(testGitRepo, {
|
||||
signal,
|
||||
wslDistro: 'Ubuntu'
|
||||
})
|
||||
const result = await listLocalRepoWorktreesStrict(
|
||||
{
|
||||
id: 'repo-1',
|
||||
path: '/workspace/repo',
|
||||
displayName: 'repo',
|
||||
badgeColor: '#000',
|
||||
addedAt: 0,
|
||||
kind: 'git'
|
||||
},
|
||||
{ signal, wslDistro: 'Ubuntu' }
|
||||
)
|
||||
|
||||
expect(listWorktreesStrictMock).toHaveBeenCalledWith('/workspace/repo', {
|
||||
signal,
|
||||
|
||||
@@ -12,8 +12,7 @@ import { getRepoExecutionHostId, LOCAL_EXECUTION_HOST_ID } from '../shared/execu
|
||||
import { resolveGitRouteForHost } from './providers/execution-host-provider-dispatch'
|
||||
import { areWorktreePathsEqual } from './ipc/worktree-logic'
|
||||
import { WorktreeCatalogUnavailableError } from '../shared/worktree/worktree-catalog-availability'
|
||||
import { isDefinitiveAbsence } from '../shared/definitive-filesystem-absence'
|
||||
import { getLocalWorktreePathAccess } from './local-worktree-filesystem'
|
||||
import { isLocalWorktreePathMissing } from './local-worktree-filesystem'
|
||||
|
||||
type LocalRepoWorktreeListOptions = {
|
||||
wslDistro?: string
|
||||
@@ -64,14 +63,8 @@ export async function listRepoWorktreesForDetectedScan(
|
||||
options?: LocalRepoWorktreeListOptions
|
||||
): Promise<GitWorktreeInfo[]> {
|
||||
return listRoutedRepoWorktrees(repo, options, async (repoPath, localOptions) => {
|
||||
try {
|
||||
// The local route runs on the execution host, including the server for SSH repositories.
|
||||
await getLocalWorktreePathAccess(localOptions).statPath(repoPath)
|
||||
} catch (error) {
|
||||
if (isDefinitiveAbsence(error)) {
|
||||
return []
|
||||
}
|
||||
throw error
|
||||
if (await isLocalWorktreePathMissing(repoPath, localOptions)) {
|
||||
return []
|
||||
}
|
||||
return listWorktreesSharedStrictAllowingTrueEmpty(repoPath, localOptions)
|
||||
})
|
||||
|
||||
@@ -2,11 +2,26 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { listWorktreesStrict } from '../git/worktree'
|
||||
import { scanLocalRepoWorktreesForResolution } from './repo-worktree-resolution-scan'
|
||||
|
||||
const { missingPathMock } = vi.hoisted(() => ({ missingPathMock: vi.fn() }))
|
||||
|
||||
vi.mock('../git/worktree', () => ({ listWorktreesStrict: vi.fn() }))
|
||||
vi.mock('../local-worktree-filesystem', () => ({
|
||||
isLocalWorktreePathMissing: missingPathMock
|
||||
}))
|
||||
|
||||
describe('scanLocalRepoWorktreesForResolution', () => {
|
||||
beforeEach(() => {
|
||||
vi.mocked(listWorktreesStrict).mockReset()
|
||||
missingPathMock.mockReset().mockResolvedValue(false)
|
||||
})
|
||||
|
||||
it('skips Git for a missing repo folder', async () => {
|
||||
missingPathMock.mockResolvedValue(true)
|
||||
expect(await scanLocalRepoWorktreesForResolution('/repo', {})).toEqual({
|
||||
ok: false,
|
||||
worktrees: []
|
||||
})
|
||||
expect(listWorktreesStrict).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('degrades a local Git execution failure instead of reporting an empty success', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { GitWorktreeInfo } from '../../shared/worktree/types'
|
||||
import { listWorktreesStrict } from '../git/worktree'
|
||||
import { isLocalWorktreePathMissing } from '../local-worktree-filesystem'
|
||||
import type { LocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
|
||||
|
||||
export type RuntimeWorktreeScanResult =
|
||||
@@ -10,6 +11,13 @@ export async function scanLocalRepoWorktreesForResolution(
|
||||
repoPath: string,
|
||||
options: LocalProjectWorktreeGitOptions
|
||||
): Promise<RuntimeWorktreeScanResult> {
|
||||
try {
|
||||
if (await isLocalWorktreePathMissing(repoPath, options)) {
|
||||
return { ok: false, worktrees: [] }
|
||||
}
|
||||
} catch {
|
||||
// A failed host check is not proof of absence; preserve the Git scan's verdict.
|
||||
}
|
||||
try {
|
||||
const worktrees = options.wslDistro
|
||||
? await listWorktreesStrict(repoPath, options)
|
||||
|
||||
Reference in New Issue
Block a user