perf(workspace): cancel local worktree scans

This commit is contained in:
Neil
2026-07-22 22:47:33 -07:00
parent c445f26541
commit 7a458c2059
5 changed files with 149 additions and 11 deletions
@@ -0,0 +1,49 @@
import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { Store } from '../persistence'
import type { Repo } from '../../shared/types'
const { listRepoWorktreesMock, getLocalProjectWorktreeGitOptionsMock } = vi.hoisted(() => ({
listRepoWorktreesMock: vi.fn(),
getLocalProjectWorktreeGitOptionsMock: vi.fn()
}))
vi.mock('../repo-worktrees', () => ({
createFolderWorktree: vi.fn(),
listRepoWorktrees: listRepoWorktreesMock
}))
vi.mock('../project-runtime-git-options', () => ({
getLocalProjectWorktreeGitOptions: getLocalProjectWorktreeGitOptionsMock
}))
import { scanWorkspaceCleanup } from './workspace-cleanup-scan'
const REPO: Repo = {
id: 'repo-1',
path: '/repo',
displayName: 'Repo',
badgeColor: '#000',
addedAt: 0
}
describe('workspace cleanup local Git routing', () => {
beforeEach(() => {
listRepoWorktreesMock.mockReset().mockResolvedValue([])
getLocalProjectWorktreeGitOptionsMock.mockReset()
})
it('uses the selected WSL distro while retaining the cleanup timeout signal', async () => {
const store = {
getRepos: () => [REPO]
} as Store
getLocalProjectWorktreeGitOptionsMock.mockReturnValue({ wslDistro: 'Ubuntu' })
await scanWorkspaceCleanup(store)
expect(getLocalProjectWorktreeGitOptionsMock).toHaveBeenCalledWith(store, REPO)
expect(listRepoWorktreesMock).toHaveBeenCalledWith(REPO, {
wslDistro: 'Ubuntu',
signal: expect.any(AbortSignal)
})
})
})
+5 -2
View File
@@ -32,6 +32,7 @@ import {
toSafeWorkspaceCleanupRepoScanError,
withWorkspaceCleanupTimeout
} from './workspace-cleanup-scan-primitives'
import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-options'
const WORKTREE_SCAN_CONCURRENCY = 3
@@ -111,7 +112,7 @@ async function scanRepoWorkspaces(
let gitWorktrees: GitWorktreeInfo[] = []
try {
const discovered = await listCleanupGitWorktrees(repo, repoIsFolder)
const discovered = await listCleanupGitWorktrees(store, repo, repoIsFolder)
provider = discovered.provider
gitWorktrees = discovered.gitWorktrees
} catch (error) {
@@ -216,6 +217,7 @@ function shouldResolveBroadWorkspaceCleanupActivity(
}
async function listCleanupGitWorktrees(
store: Store,
repo: Repo,
repoIsFolder: boolean
): Promise<{ provider: IGitProvider | null; gitWorktrees: GitWorktreeInfo[] }> {
@@ -237,10 +239,11 @@ async function listCleanupGitWorktrees(
)
}
}
const localGitOptions = getLocalProjectWorktreeGitOptions(store, repo)
return {
provider: null,
gitWorktrees: await withWorkspaceCleanupTimeout(
(signal) => listRepoWorktrees(repo, { signal }),
(signal) => listRepoWorktrees(repo, { ...localGitOptions, signal }),
WORKSPACE_CLEANUP_GIT_READ_TIMEOUT_MS,
'Timed out listing worktrees.'
)
+7
View File
@@ -17,6 +17,7 @@ const {
listRepoWorktreesMock,
getStatusMock,
gitExecFileAsyncMock,
getLocalProjectWorktreeGitOptionsMock,
getSshGitProviderMock,
getSshPtyProviderMock,
listRegisteredPtysMock
@@ -26,6 +27,7 @@ const {
listRepoWorktreesMock: vi.fn(),
getStatusMock: vi.fn(),
gitExecFileAsyncMock: vi.fn(),
getLocalProjectWorktreeGitOptionsMock: vi.fn(),
getSshGitProviderMock: vi.fn(),
getSshPtyProviderMock: vi.fn(),
listRegisteredPtysMock: vi.fn()
@@ -60,6 +62,10 @@ vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock
}))
vi.mock('../project-runtime-git-options', () => ({
getLocalProjectWorktreeGitOptions: getLocalProjectWorktreeGitOptionsMock
}))
vi.mock('../memory/pty-registry', () => ({
listRegisteredPtys: listRegisteredPtysMock
}))
@@ -154,6 +160,7 @@ describe('workspace cleanup scan', () => {
listRepoWorktreesMock.mockReset()
getStatusMock.mockReset()
gitExecFileAsyncMock.mockReset()
getLocalProjectWorktreeGitOptionsMock.mockReset().mockReturnValue({})
getSshGitProviderMock.mockReset()
getSshPtyProviderMock.mockReset()
listRegisteredPtysMock.mockReset()
+81 -7
View File
@@ -5,13 +5,17 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { Repo } from '../shared/types'
import type { Store } from './persistence'
const { listRepoWorktreesMock, getSshFilesystemProviderMock, getSshGitProviderMock } = vi.hoisted(
() => ({
listRepoWorktreesMock: vi.fn(),
getSshFilesystemProviderMock: vi.fn(),
getSshGitProviderMock: vi.fn()
})
)
const {
listRepoWorktreesMock,
getLocalProjectWorktreeGitOptionsMock,
getSshFilesystemProviderMock,
getSshGitProviderMock
} = vi.hoisted(() => ({
listRepoWorktreesMock: vi.fn(),
getLocalProjectWorktreeGitOptionsMock: vi.fn(),
getSshFilesystemProviderMock: vi.fn(),
getSshGitProviderMock: vi.fn()
}))
vi.mock('./repo-worktrees', () => ({
createFolderWorktree: (repo: Repo) => ({
@@ -32,6 +36,10 @@ vi.mock('./providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock
}))
vi.mock('./project-runtime-git-options', () => ({
getLocalProjectWorktreeGitOptions: getLocalProjectWorktreeGitOptionsMock
}))
import { analyzeWorkspaceSpace, WorkspaceSpaceScanCancelledError } from './workspace-space-analysis'
function createStore(repos: Repo[]): Store {
@@ -58,6 +66,7 @@ describe('analyzeWorkspaceSpace', () => {
vi.setSystemTime(new Date('2026-05-14T12:00:00Z'))
tempDir = await mkdtemp(join(tmpdir(), 'orca-space-'))
listRepoWorktreesMock.mockReset()
getLocalProjectWorktreeGitOptionsMock.mockReset().mockReturnValue({})
getSshFilesystemProviderMock.mockReset()
getSshGitProviderMock.mockReset()
})
@@ -231,6 +240,71 @@ describe('analyzeWorkspaceSpace', () => {
expect(listRepoWorktreesMock).not.toHaveBeenCalled()
})
it('aborts every in-flight local worktree list when the scan is cancelled', async () => {
const repos: Repo[] = [
{
id: 'repo-1',
path: join(tempDir!, 'repo-1'),
displayName: 'one',
badgeColor: '#000',
addedAt: 0
},
{
id: 'repo-2',
path: join(tempDir!, 'repo-2'),
displayName: 'two',
badgeColor: '#000',
addedAt: 0
}
]
const capturedSignals: AbortSignal[] = []
let markBothStarted!: () => void
const bothStarted = new Promise<void>((resolve) => {
markBothStarted = resolve
})
listRepoWorktreesMock.mockImplementation((_repo: Repo, options?: { signal?: AbortSignal }) => {
const signal = options?.signal
if (!signal) {
throw new Error('expected cancellation signal')
}
capturedSignals.push(signal)
if (capturedSignals.length === repos.length) {
markBothStarted()
}
return new Promise<never>((_resolve, reject) => {
signal.addEventListener('abort', () => reject(signal.reason), { once: true })
})
})
const controller = new AbortController()
const scan = analyzeWorkspaceSpace(createStore(repos), { signal: controller.signal })
await bothStarted
controller.abort()
await expect(scan).rejects.toBeInstanceOf(WorkspaceSpaceScanCancelledError)
expect(capturedSignals).toHaveLength(2)
expect(capturedSignals.every((signal) => signal.aborted)).toBe(true)
})
it('routes local worktree listing through the selected WSL distro', async () => {
const repo: Repo = {
id: 'repo-1',
path: tempDir!,
displayName: 'orca',
badgeColor: '#000',
addedAt: 0
}
getLocalProjectWorktreeGitOptionsMock.mockReturnValue({ wslDistro: 'Ubuntu' })
listRepoWorktreesMock.mockResolvedValue([])
await analyzeWorkspaceSpace(createStore([repo]))
expect(listRepoWorktreesMock).toHaveBeenCalledWith(repo, {
wslDistro: 'Ubuntu',
signal: undefined
})
})
it('isolates missing worktrees as row-level scan failures', async () => {
const root = tempDir!
const repoPath = join(root, 'repo')
+7 -2
View File
@@ -24,6 +24,7 @@ import { getSshFilesystemProvider } from './providers/ssh-filesystem-dispatch'
import { getSshGitProvider } from './providers/ssh-git-dispatch'
import { createFolderWorktree, listRepoWorktrees } from './repo-worktrees'
import { mergeWorktree } from './ipc/worktree-logic'
import { getLocalProjectWorktreeGitOptions } from './project-runtime-git-options'
const WORKTREE_SCAN_CONCURRENCY = 3
const LOCAL_FS_CONCURRENCY = 48
@@ -763,6 +764,7 @@ async function scanRemoteWorktree(
}
async function listWorktreesForSpaceScan(
store: Store,
repo: Repo,
signal?: AbortSignal
): Promise<WorktreeListResult> {
@@ -784,7 +786,10 @@ async function listWorktreesForSpaceScan(
throwIfAborted(signal)
return { ok: true, worktrees }
}
const worktrees = await listRepoWorktrees(repo)
const worktrees = await listRepoWorktrees(repo, {
...getLocalProjectWorktreeGitOptions(store, repo),
signal
})
throwIfAborted(signal)
return { ok: true, worktrees }
} catch (error) {
@@ -826,7 +831,7 @@ async function scanRepo(
},
options.onProgress
)
const listed = await listWorktreesForSpaceScan(repo, options.signal)
const listed = await listWorktreesForSpaceScan(store, repo, options.signal)
if (!listed.ok) {
reportProgress(
progress,