mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 00:03:08 +00:00
`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.
127 lines
4.0 KiB
TypeScript
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()
|
|
})
|
|
})
|