Files
orca/src/main/worktree-root-preparation.test.ts
T
Neil e6257b6e32 perf(worktree): resolve the WSL workspace root off the main thread when preparing (#17792)
`computeWorkspaceRoot` resolves a WSL repo's mirror root through `getWslHome`,
which is a synchronous `execFileSync('wsl.exe', ...)` with a 5s timeout. Two
worktree preparation paths ran it on the Electron main thread:
`prepareLocalWorktreeRootForRepo` (repo registration, clone completion, repo
update, project host setup, folder->git upgrade) and `prepareWorktreeCreateForRepo`
(the speculative checkout started while the create composer is open). On a stopped
or cold distro that froze every window for up to 5s. Being fire-and-forget did not
help: only 3 of the 16 `prepareLocalWorktreeRootForRepo` call sites are `void`-ed,
the other 13 are awaited inside IPC handlers, and the sync probe blocks the main
thread either way. `prepareLocalWorktreeRootsForRepos` runs the same probe for
every repo from the settings-save handler.

Adopt the existing `computeWorkspaceRootAsync` (now exported) at those two call
sites, and give the two resolvers a shared mirror-distro decision and shared
root-from-home layout so they cannot drift apart.

Also thread the mirror distro into the prepare-side path settings.
`createLocalWorktree` passes `getWorktreeMirrorDistro(store, repo)` and
`prepareWorktreeCreateForRepo` did not, so a `C:\` repo on a WSL project runtime
prepared under `C:\workspaces` while the create click looked under the mirrored
WSL root: the keys never matched and every prepared checkout was discarded, after
paying for a full checkout that sat until the 5min TTL. Pre-existing on main;
included because it is the same line and the same resolver.

Scope of the win, stated precisely: only those two preparation paths stop
blocking. On a reachable distro `getWslHome` caches on success, so before this
change the first repo paid one blocking probe and the rest were cache hits -- the
change makes that one probe non-blocking, it does not remove N probes. Failed
probes are never cached, so on a stopped distro N repos did pay N sequential 5s
blocking probes and now share one in-flight async probe.

Costs: five sync `computeWorkspaceRoot` callers remain (allowed-roots resolution,
the create click in worktree-remote, CLI create, watch targets, worktree trash),
and `getWslHome` reads only `wslHomeCache` -- it cannot join an in-flight async
probe. The guaranteed synchronous cache warm-up therefore becomes a window in
which one of those callers can still block and can spawn a second concurrent
`wsl.exe`. Concretely: opening the create composer and clicking Create within a
few hundred ms on a cold distro now pays the freeze on the click instead of on the
background prep. Separately, the mirror-distro fix makes prepare spawn an async
`wsl.exe` home probe for `C:\` repos on a WSL runtime, where it previously spawned
none.

No race added: `prepareWorktreeCreateForRepo` computes the preparation key and
inserts the registry entry in one synchronous run after the await, so two
concurrent creates still dedupe to a single prepared checkout.

`worktree-create-preparation-wsl-root.test.ts` runs the real resolver through
prepare and then claims the entry with the production consume-side call shape
(including the mirror distro), so a divergence between the two resolvers fails a
test instead of silently discarding every prepared checkout.
2026-08-31 21:11:09 -07:00

127 lines
4.0 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type * as WslModule from './wsl'
import type { Repo } from '../shared/repo-types'
const { mkdirMock, authorizeExternalPathMock, getWslHomeMock, getWslHomeAsyncMock } = vi.hoisted(
() => ({
mkdirMock: vi.fn(),
authorizeExternalPathMock: vi.fn(),
getWslHomeMock: vi.fn(),
getWslHomeAsyncMock: vi.fn()
})
)
vi.mock('fs/promises', () => ({
mkdir: mkdirMock
}))
vi.mock('./ipc/filesystem-auth', () => ({
authorizeExternalPath: authorizeExternalPathMock
}))
vi.mock('./wsl', async (importOriginal) => ({
...(await importOriginal<typeof WslModule>()),
getWslHome: getWslHomeMock,
getWslHomeAsync: getWslHomeAsyncMock
}))
import { prepareLocalWorktreeRootForRepo } from './worktree-root-preparation'
const repo: Repo = {
id: 'repo-1',
path: '/projects/app',
displayName: 'app',
badgeColor: '#000',
addedAt: 1,
kind: 'git'
}
const store = {
getSettings: vi.fn()
}
describe('prepareLocalWorktreeRootForRepo', () => {
beforeEach(() => {
mkdirMock.mockReset().mockResolvedValue(undefined)
authorizeExternalPathMock.mockReset()
getWslHomeMock.mockReset().mockImplementation(() => {
throw new Error('synchronous wsl.exe home probe must not run on the main thread')
})
getWslHomeAsyncMock.mockReset().mockResolvedValue(null)
store.getSettings.mockReset().mockReturnValue({
workspaceDir: '/Users/alice/orca/workspaces',
nestWorkspaces: false
})
})
it('creates the effective worktree root for local git repos', async () => {
await prepareLocalWorktreeRootForRepo(store as never, repo)
expect(mkdirMock).toHaveBeenCalledWith('/Users/alice/orca/workspaces', { recursive: true })
})
it('resolves a WSL repo root through the async probe instead of blocking on wsl.exe', async () => {
const originalPlatform = process.platform
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
let resolveHome!: (home: string) => void
getWslHomeAsyncMock.mockReturnValue(
new Promise<string>((resolve) => {
resolveHome = resolve
})
)
store.getSettings.mockReturnValue({ workspaceDir: 'C:\\workspaces', nestWorkspaces: false })
try {
const preparation = prepareLocalWorktreeRootForRepo(store as never, {
...repo,
path: '\\\\wsl.localhost\\Ubuntu\\home\\jin\\src\\repo'
})
await Promise.resolve()
expect(mkdirMock).not.toHaveBeenCalled()
resolveHome('\\\\wsl.localhost\\Ubuntu\\home\\jin')
await preparation
expect(getWslHomeMock).not.toHaveBeenCalled()
expect(mkdirMock).toHaveBeenCalledWith(
'\\\\wsl.localhost\\Ubuntu\\home\\jin\\orca\\workspaces',
{ recursive: true }
)
} finally {
Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform })
}
})
it('uses repo-specific worktree base paths', async () => {
await prepareLocalWorktreeRootForRepo(store as never, {
...repo,
worktreeBasePath: '../worktrees'
})
expect(mkdirMock).toHaveBeenCalledWith('/projects/worktrees', { recursive: true })
})
it('skips non-local and folder repos', async () => {
await prepareLocalWorktreeRootForRepo(store as never, { ...repo, connectionId: 'ssh-1' })
await prepareLocalWorktreeRootForRepo(store as never, {
...repo,
executionHostId: 'ssh:ssh-1'
})
await prepareLocalWorktreeRootForRepo(store as never, {
...repo,
executionHostId: 'runtime:env-1'
})
await prepareLocalWorktreeRootForRepo(store as never, { ...repo, kind: 'folder' })
expect(mkdirMock).not.toHaveBeenCalled()
expect(authorizeExternalPathMock).not.toHaveBeenCalled()
})
it('does not fail repo setup when root preparation fails', async () => {
mkdirMock.mockRejectedValueOnce(new Error('permission denied'))
await expect(prepareLocalWorktreeRootForRepo(store as never, repo)).resolves.toBeUndefined()
expect(authorizeExternalPathMock).not.toHaveBeenCalled()
})
})