mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
perf(workspace): cancel local worktree scans
This commit is contained in:
@@ -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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -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.'
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user