diff --git a/src/main/git/wsl-worktree-git-path.test.ts b/src/main/git/wsl-worktree-git-path.test.ts new file mode 100644 index 00000000000..3bf671043f0 --- /dev/null +++ b/src/main/git/wsl-worktree-git-path.test.ts @@ -0,0 +1,96 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest' + +const { gitExecFileAsyncMock, readWorktreeGitPathMock } = vi.hoisted(() => ({ + gitExecFileAsyncMock: vi.fn(), + readWorktreeGitPathMock: vi.fn() +})) + +vi.mock('./command-runner/git-exec-file', () => ({ gitExecFileAsync: gitExecFileAsyncMock })) +vi.mock('./worktree-git-path', () => ({ readWorktreeGitPath: readWorktreeGitPathMock })) +vi.mock('../wsl/wsl-automount-root', async (importOriginal) => ({ + ...(await importOriginal()), + knownWslAutomountRoots: () => ['/mnt/'] +})) + +import { resolveWslWorktreeGitPath } from './wsl-worktree-git-path' + +const WORKTREE = '\\\\wsl.localhost\\Ubuntu\\home\\jin\\feature' +const FROM_LINK = + '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo\\.git\\worktrees\\feature\\orca\\setup-runner.sh' + +describe('resolveWslWorktreeGitPath', () => { + beforeEach(() => { + gitExecFileAsyncMock.mockReset() + readWorktreeGitPathMock.mockReset() + }) + + it('uses the .git link without running Git', async () => { + readWorktreeGitPathMock.mockReturnValue(FROM_LINK) + + await expect(resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh')).resolves.toBe( + FROM_LINK + ) + expect(readWorktreeGitPathMock).toHaveBeenCalledWith(WORKTREE, 'orca/setup-runner.sh', [ + '/mnt/' + ]) + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it('retries the link once after a transient miss', async () => { + readWorktreeGitPathMock.mockReturnValueOnce(null).mockReturnValueOnce(FROM_LINK) + + await expect(resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh')).resolves.toBe( + FROM_LINK + ) + expect(readWorktreeGitPathMock).toHaveBeenCalledTimes(2) + expect(gitExecFileAsyncMock).not.toHaveBeenCalled() + }) + + it("asks the distro's Git when the link stays unreadable", async () => { + readWorktreeGitPathMock.mockReturnValue(null) + gitExecFileAsyncMock.mockResolvedValue({ + stdout: '/home/jin/repo/.git/worktrees/feature/orca/setup-runner.sh\n', + stderr: '' + }) + + await expect(resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh')).resolves.toBe( + FROM_LINK + ) + expect(readWorktreeGitPathMock).toHaveBeenCalledTimes(2) + expect(gitExecFileAsyncMock).toHaveBeenCalledWith( + ['rev-parse', '--git-path', 'orca/setup-runner.sh'], + { cwd: WORKTREE } + ) + }) + + it("maps Git's mounted-drive answer to the Windows drive", async () => { + readWorktreeGitPathMock.mockReturnValue(null) + gitExecFileAsyncMock.mockResolvedValue({ + stdout: '/mnt/c/src/repo/.git/worktrees/feature/orca/setup-runner.sh\n', + stderr: '' + }) + + await expect(resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh')).resolves.toBe( + 'C:\\src\\repo\\.git\\worktrees\\feature\\orca\\setup-runner.sh' + ) + }) + + it("resolves Git's worktree-relative answer against the worktree", async () => { + readWorktreeGitPathMock.mockReturnValue(null) + gitExecFileAsyncMock.mockResolvedValue({ stdout: '.git/orca/setup-runner.sh\n', stderr: '' }) + + await expect(resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh')).resolves.toBe( + '\\\\wsl.localhost\\Ubuntu\\home\\jin\\feature\\.git\\orca\\setup-runner.sh' + ) + }) + + it("names the worktree and keeps Git's error when both fail", async () => { + readWorktreeGitPathMock.mockReturnValue(null) + const gitError = new Error('fatal: not a git repository') + gitExecFileAsyncMock.mockRejectedValue(gitError) + + const failure = resolveWslWorktreeGitPath(WORKTREE, 'orca/setup-runner.sh') + await expect(failure).rejects.toThrow(/couldn't find this WSL worktree's Git folder/) + await expect(failure).rejects.toMatchObject({ cause: gitError }) + }) +}) diff --git a/src/main/git/wsl-worktree-git-path.ts b/src/main/git/wsl-worktree-git-path.ts new file mode 100644 index 00000000000..d5f4c57ea9d --- /dev/null +++ b/src/main/git/wsl-worktree-git-path.ts @@ -0,0 +1,46 @@ +import { win32 } from 'node:path' +import { parseWslUncPath, toWindowsWslUncPath } from '../../shared/wsl-paths' +import { knownWslAutomountRoots, wslDrivePathUnderRoots } from '../wsl/wsl-automount-root' +import { gitExecFileAsync } from './command-runner/git-exec-file' +import { readWorktreeGitPath } from './worktree-git-path' + +/** + * A per-worktree Git path for a worktree inside a distro, as a path Windows can write. Reads the + * `.git` link first; the distro's own Git answers when that read fails. + */ +export async function resolveWslWorktreeGitPath( + worktreePath: string, + relativePath: string +): Promise { + const automountRoots = knownWslAutomountRoots() + const read = (): string | null => readWorktreeGitPath(worktreePath, relativePath, automountRoots) + // A Windows stat over \\wsl.localhost can report a transient ENOENT. + const fromLink = read() ?? read() + if (fromLink) { + return fromLink + } + const distro = parseWslUncPath(worktreePath)?.distro + try { + // Async and fenced; the caller is a user act, so starting the distro is fine here. + const { stdout } = await gitExecFileAsync(['rev-parse', '--git-path', relativePath], { + cwd: worktreePath + }) + const gitPath = stdout.trim() + if (gitPath && distro) { + // A git dir on a mounted drive maps to the drive itself, as the link read does. + return gitPath.startsWith('/') + ? (wslDrivePathUnderRoots(gitPath, automountRoots) ?? toWindowsWslUncPath(gitPath, distro)) + : win32.join(worktreePath, gitPath) + } + } catch (error) { + throw wslWorktreeGitPathError(worktreePath, error) + } + throw wslWorktreeGitPathError(worktreePath) +} + +function wslWorktreeGitPathError(worktreePath: string, cause?: unknown): Error { + return new Error( + `Orca couldn't find this WSL worktree's Git folder to write its setup script. Check that ${worktreePath} is a Git worktree.`, + cause === undefined ? undefined : { cause } + ) +} diff --git a/src/main/hooks-runner.test.ts b/src/main/hooks-runner.test.ts index 4bcac803e85..a69ae5ee5e5 100644 --- a/src/main/hooks-runner.test.ts +++ b/src/main/hooks-runner.test.ts @@ -60,7 +60,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo\\feature\\', 'pnpm install\npnpm build' @@ -105,7 +105,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( { ...makeRepo(), path: 'C:\\Users\\jinwo\\git\\orca' }, 'C:\\repo\\feature', '#!/usr/bin/env bash\npnpm install', @@ -150,7 +150,7 @@ describe('createSetupRunnerScript', () => { const { createSetupRunnerScript } = await import('./worktree-runner-script') const { TERMINAL_GIT_CREDENTIAL_GUARD_POLICY_ENV } = await import('../shared/terminal-git-credential-guard') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo\\feature', 'pnpm install', @@ -172,7 +172,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( { ...makeRepo(), path: 'C:\\Users\\jinwo\\git\\orca' }, 'C:\\repo\\feature', 'pnpm install' @@ -255,7 +255,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript(makeRepo(), '/test/repo-feature', 'pnpm install') + const result = await createSetupRunnerScript(makeRepo(), '/test/repo-feature', 'pnpm install') expect(result.envVars).toEqual( expect.objectContaining({ @@ -284,7 +284,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( { ...makeRepo(), path: 'C:\\Users\\jinwo\\git\\orca' @@ -338,7 +338,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), '\\\\wsl.localhost\\Ubuntu\\home\\jin\\repo\\feature', 'pnpm install' diff --git a/src/main/hooks-setup-runner-script.test.ts b/src/main/hooks-setup-runner-script.test.ts index 4762e597f6f..737c3e4b37d 100644 --- a/src/main/hooks-setup-runner-script.test.ts +++ b/src/main/hooks-setup-runner-script.test.ts @@ -101,7 +101,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo-worktree', '#!/usr/bin/env bash\r\npnpm install\r\nnpm run build', @@ -140,7 +140,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo-worktree', 'copy .env.example .env\r\nxcopy /E assets dist', @@ -177,7 +177,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') const { buildSetupRunnerCommand } = await import('../shared/setup-runner-command') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo-worktree', 'copy .env.example .env', @@ -207,7 +207,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - createSetupRunnerScript( + await createSetupRunnerScript( makeRepo(), 'C:\\repo-worktree', '#!/usr/bin/env -S bash -euo pipefail\nmake build | tee build.log', @@ -236,7 +236,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( makeRepo(), 'C:\\repo-worktree', 'pnpm install\nnpm run build', @@ -277,7 +277,7 @@ describe('createSetupRunnerScript', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript(makeRepo(), '/test/worktree', 'pnpm install') + const result = await createSetupRunnerScript(makeRepo(), '/test/worktree', 'pnpm install') expect(gitExecFileSyncMock).toHaveBeenCalledWith( ['rev-parse', '--git-path', 'orca/setup-runner.sh'], @@ -301,34 +301,39 @@ describe('createSetupRunnerScript', () => { const { createSetupRunnerScript } = await import('./worktree-runner-script') expect( - createSetupRunnerScript(makeRepo(), '/test/worktree', 'echo setup').waitForAgentStartup - ).toBeUndefined() - expect( - createSetupRunnerScript(makeRepo('start-immediately'), '/test/worktree', 'echo setup') + (await createSetupRunnerScript(makeRepo(), '/test/worktree', 'echo setup')) .waitForAgentStartup ).toBeUndefined() expect( - createSetupRunnerScript(makeRepo('wait-for-setup'), '/test/worktree', 'echo setup') + (await createSetupRunnerScript(makeRepo('start-immediately'), '/test/worktree', 'echo setup')) + .waitForAgentStartup + ).toBeUndefined() + expect( + (await createSetupRunnerScript(makeRepo('wait-for-setup'), '/test/worktree', 'echo setup')) .waitForAgentStartup ).toBe(true) expect( - createSetupRunnerScript( - makeRepo('start-immediately'), - '/test/worktree', - 'echo setup', - undefined, - undefined, - 'wait-for-setup' + ( + await createSetupRunnerScript( + makeRepo('start-immediately'), + '/test/worktree', + 'echo setup', + undefined, + undefined, + 'wait-for-setup' + ) ).waitForAgentStartup ).toBe(true) expect( - createSetupRunnerScript( - makeRepo('wait-for-setup'), - '/test/worktree', - 'echo setup', - undefined, - undefined, - 'start-immediately' + ( + await createSetupRunnerScript( + makeRepo('wait-for-setup'), + '/test/worktree', + 'echo setup', + undefined, + undefined, + 'start-immediately' + ) ).waitForAgentStartup ).toBe(true) }) @@ -338,7 +343,7 @@ describe('createSetupRunnerScript', () => { gitExecFileSyncMock.mockReturnValue('/test/repo/.git/orca/setup-runner.sh\n') const { createSetupRunnerScript } = await import('./worktree-runner-script') - const setup = createSetupRunnerScript(makeRepo(), '/test/worktree', 'git fetch') + const setup = await createSetupRunnerScript(makeRepo(), '/test/worktree', 'git fetch') expect(setup.envVars).toMatchObject({ ORCA_ROOT_PATH: '/test/repo', diff --git a/src/main/hooks.test.ts b/src/main/hooks.test.ts index 2bfc06c0c35..e5ed8822609 100644 --- a/src/main/hooks.test.ts +++ b/src/main/hooks.test.ts @@ -326,7 +326,7 @@ describe('runHook', () => { try { const { createSetupRunnerScript } = await import('./worktree-runner-script') - const result = createSetupRunnerScript( + const result = await createSetupRunnerScript( { ...makeRepo(), path: 'C:\\Users\\jinwo\\git\\orca' diff --git a/src/main/ipc/worktree-remote.ts b/src/main/ipc/worktree-remote.ts index 9afb65c3a99..985b95d1af1 100644 --- a/src/main/ipc/worktree-remote.ts +++ b/src/main/ipc/worktree-remote.ts @@ -1701,7 +1701,7 @@ async function performLocalWorktreeCreate( if (setupScript && shouldLaunchSetup) { try { // Main generates setup scripts without executing them; generation failure leaves the created workspace usable. - setup = createSetupRunnerScript( + setup = await createSetupRunnerScript( repo, worktreePath, setupScript, diff --git a/src/main/ipc/worktrees-automation-create-terminals.test.ts b/src/main/ipc/worktrees-automation-create-terminals.test.ts index 09414f8e52b..ccdb1dbfc96 100644 --- a/src/main/ipc/worktrees-automation-create-terminals.test.ts +++ b/src/main/ipc/worktrees-automation-create-terminals.test.ts @@ -148,7 +148,7 @@ describe('automation creates: main provisions setup and default tabs', () => { ])( 'runs a $platform runner with the $platform command', async ({ runnerScriptPath, platform }) => { - createSetupRunnerScriptMock.mockReturnValueOnce({ runnerScriptPath, envVars: {} }) + createSetupRunnerScriptMock.mockResolvedValueOnce({ runnerScriptPath, envVars: {} }) await handlers['worktrees:create'](null, { repoId: 'repo-1', diff --git a/src/main/ipc/worktrees-local-create-flow.test.ts b/src/main/ipc/worktrees-local-create-flow.test.ts index aad9bcef988..9d9a1d0e30e 100644 --- a/src/main/ipc/worktrees-local-create-flow.test.ts +++ b/src/main/ipc/worktrees-local-create-flow.test.ts @@ -758,7 +758,7 @@ describe('registerWorktreeHandlers', () => { getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) shouldRunSetupForCreateMock.mockReturnValue(true) - createSetupRunnerScriptMock.mockReturnValueOnce({ + createSetupRunnerScriptMock.mockResolvedValueOnce({ runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { diff --git a/src/main/ipc/worktrees-test-harness.ts b/src/main/ipc/worktrees-test-harness.ts index 21e28eda802..4c9200f4b6c 100644 --- a/src/main/ipc/worktrees-test-harness.ts +++ b/src/main/ipc/worktrees-test-harness.ts @@ -253,7 +253,7 @@ export function setupWorktreeHandlers(): WorktreeRuntimeStub { GHOSTX_ROOT_PATH: repoArg.path }) ) - createSetupRunnerScriptMock.mockReturnValue({ + createSetupRunnerScriptMock.mockResolvedValue({ runnerScriptPath: '/workspace/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/workspace/repo', diff --git a/src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts index eeb39ab4209..9c07560299c 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts @@ -63,7 +63,7 @@ async function createFromRendererPayload(options: { } ]) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } }) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: RUNNER, envVars: SETUP_ENV }) diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts index aa3978d99a9..936c5cde5b2 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-02.spec.ts @@ -70,7 +70,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\tmp\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { @@ -167,7 +167,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -243,7 +243,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\tmp\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { @@ -557,7 +557,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts index ee6962a3660..4e9955faefc 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-03.spec.ts @@ -65,7 +65,7 @@ describe('OrcaRuntimeService', () => { ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-bg-split-setup') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -146,7 +146,7 @@ describe('OrcaRuntimeService', () => { ensurePathWithinWorkspaceMock.mockReturnValue('/tmp/workspaces/runtime-active-split-setup') vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts index 7b2143a289a..c8466ce846c 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup-part-04.spec.ts @@ -309,7 +309,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -441,7 +441,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\tmp\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup.spec.ts index 5e82c64b7f4..af67dda1d0b 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-and-startup.spec.ts @@ -44,7 +44,7 @@ describe('OrcaRuntimeService', () => { setup: 'pnpm worktree:setup' } }) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -132,7 +132,7 @@ describe('OrcaRuntimeService', () => { setup: 'pnpm worktree:setup' } }) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -199,7 +199,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(resolveSetupRunnerShell).mockReturnValue({ family: 'posix' }) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix' }, envVars: { @@ -282,7 +282,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/tmp/repo', @@ -396,7 +396,7 @@ describe('OrcaRuntimeService', () => { } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { @@ -474,7 +474,7 @@ describe('OrcaRuntimeService', () => { }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) vi.mocked(resolveSetupRunnerShell).mockReturnValue({ family: 'posix' }) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: 'C:\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix' }, envVars: { diff --git a/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts b/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts index 7d3b9dfb379..bf6926cbec8 100644 --- a/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts +++ b/src/main/runtime/orca-runtime-tests/worktree-setup-single-creator.spec.ts @@ -79,7 +79,7 @@ async function createActivated(options: { ensurePathWithinWorkspaceMock.mockReturnValue(WORKTREE_PATH) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: RUNNER, envVars: { ORCA_ROOT_PATH: '/tmp/repo', ORCA_WORKTREE_PATH: WORKTREE_PATH } }) diff --git a/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts b/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts index 71096fe72c1..2e0644f7cef 100644 --- a/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts +++ b/src/main/runtime/orca-runtime-worktree-provisioning-availability.test.ts @@ -71,7 +71,7 @@ describe('host worktree creation while its renderer is unavailable', () => { ]) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'echo setup' } }) vi.mocked(shouldRunSetupForCreate).mockReturnValue(true) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/tmp/setup-runner.sh', envVars: {} }) diff --git a/src/main/runtime/runtime-local-worktree-setup.ts b/src/main/runtime/runtime-local-worktree-setup.ts index 72788e4734c..2e958956f4a 100644 --- a/src/main/runtime/runtime-local-worktree-setup.ts +++ b/src/main/runtime/runtime-local-worktree-setup.ts @@ -57,7 +57,7 @@ export async function prepareRuntimeLocalWorktreeSetup(args: { if (shouldRunSetup && hooks?.scripts.setup) { if (args.shouldUseSetupRunner) { try { - setup = createSetupRunnerScript( + setup = await createSetupRunnerScript( repo, worktreePath, hooks.scripts.setup, diff --git a/src/main/runtime/worktree-agent-first-surface.test.ts b/src/main/runtime/worktree-agent-first-surface.test.ts index 4dd5f7d6bc9..62943a7f6b1 100644 --- a/src/main/runtime/worktree-agent-first-surface.test.ts +++ b/src/main/runtime/worktree-agent-first-surface.test.ts @@ -84,7 +84,7 @@ function chatCreateRuntime(args: { ensurePathWithinWorkspaceMock.mockReturnValue(path) vi.mocked(getEffectiveHooks).mockReturnValue({ scripts: { setup: 'pnpm install' } }) vi.mocked(getDefaultTabsLaunch).mockReturnValue(args.withDefaults ? DEFAULTS : undefined) - vi.mocked(createSetupRunnerScript).mockReturnValue({ + vi.mocked(createSetupRunnerScript).mockResolvedValue({ runnerScriptPath: '/repo/setup.sh', envVars: {}, ...(args.wait ? { waitForAgentStartup: true } : {}) diff --git a/src/main/worktree-runner-script.ts b/src/main/worktree-runner-script.ts index bfa8ff27242..72e5d236818 100644 --- a/src/main/worktree-runner-script.ts +++ b/src/main/worktree-runner-script.ts @@ -8,6 +8,7 @@ import { resolveWindowsGitBashShellPath } from './git-bash' import { gitExecFileSync } from './git/runner' import { isLocalGitRuntimeRouterInstalled } from './git/command-runner/local-git-runtime-route' import { readWorktreeGitPath } from './git/worktree-git-path' +import { resolveWslWorktreeGitPath } from './git/wsl-worktree-git-path' import { knownWslAutomountRoots } from './wsl/wsl-automount-root' import { isWslPath, toWindowsWslPath, toLinuxPath } from './wsl' import { getHookRuntimeTarget, getHookWslContext } from './hook-runtime-target' @@ -30,14 +31,14 @@ function getGitPath(cwd: string, relativePath: string): string { }).trim() } -export function createSetupRunnerScript( +export async function createSetupRunnerScript( repo: Repo, worktreePath: string, script: string, projectRuntime?: ProjectExecutionRuntimeResolution | HookRuntimeTarget, setupShell?: SetupRunnerShell, projectStartupPolicy?: SetupAgentStartupPolicy -): WorktreeSetupLaunch { +): Promise { return createWorktreeRunnerScript({ repo, worktreePath, @@ -52,7 +53,7 @@ export function createSetupRunnerScript( }) } -function createWorktreeRunnerScript(args: { +async function createWorktreeRunnerScript(args: { repo: Repo worktreePath: string script: string @@ -60,7 +61,7 @@ function createWorktreeRunnerScript(args: { runtimeTarget?: HookRuntimeTarget waitForAgentStartup?: boolean setupShell?: SetupRunnerShell -}): WorktreeSetupLaunch { +}): Promise { const { repo, worktreePath, @@ -88,18 +89,13 @@ function createWorktreeRunnerScript(args: { // Linked worktrees have .git files; resolve the per-worktree gitdir through Git. const runnerExtension = runnerShell.family === 'cmd' ? 'cmd' : 'sh' const gitRelPath = `orca/${runnerBaseName}.${runnerExtension}` - // Read the link with the D1c Git router (if WSL Git made the worktree, host Git can't), and always - // for a worktree inside a distro: Git there would be a synchronous wsl.exe on the main thread. - const inDistro = isWslPath(worktreePath) - const fromLink = - isLocalGitRuntimeRouterInstalled() || inDistro + // Read the link with the D1c Git router (if WSL Git made the worktree, host Git can't). A worktree + // inside a distro never runs synchronous Git: that would be a synchronous wsl.exe on the main thread. + const fromLink = isWslPath(worktreePath) + ? await resolveWslWorktreeGitPath(worktreePath, gitRelPath) + : isLocalGitRuntimeRouterInstalled() ? readWorktreeGitPath(worktreePath, gitRelPath, knownWslAutomountRoots()) : null - if (!fromLink && inDistro) { - throw new Error( - `Orca couldn't find this WSL worktree's Git folder to write its setup script. Check that ${worktreePath} is a Git worktree.` - ) - } let runnerScriptPath = fromLink ?? getGitPath(worktreePath, gitRelPath) // Convert WSL Git's Linux path to a path Windows filesystem calls can reach.