/* eslint-disable max-lines */ import { beforeEach, describe, expect, it, vi } from 'vitest' const { handleMock, removeHandlerMock, listWorktreesMock, assertWorktreeCleanForRemovalMock, addWorktreeMock, addSparseWorktreeMock, removeWorktreeMock, getGitUsernameMock, getDefaultBaseRefMock, getDefaultRemoteMock, getBranchConflictKindMock, getPRForBranchMock, getWorkItemMock, getPullRequestPushTargetMock, getEffectiveHooksMock, createIssueCommandRunnerScriptMock, createSetupRunnerScriptMock, getEffectiveHooksFromConfigMock, parseOrcaYamlMock, shouldRunSetupForCreateMock, buildPosixRunnerScriptMock, buildWindowsRunnerScriptMock, getSetupRunnerEnvVarsMock, runHookMock, hasHooksFileMock, loadHooksMock, computeWorktreePathMock, ensurePathWithinWorkspaceMock, gitExecFileAsyncMock, getSshGitProviderMock, getSshFilesystemProviderMock, getActiveMultiplexerMock } = vi.hoisted(() => ({ handleMock: vi.fn(), removeHandlerMock: vi.fn(), listWorktreesMock: vi.fn(), assertWorktreeCleanForRemovalMock: vi.fn(), addWorktreeMock: vi.fn(), addSparseWorktreeMock: vi.fn(), removeWorktreeMock: vi.fn(), getGitUsernameMock: vi.fn(), getDefaultBaseRefMock: vi.fn(), getDefaultRemoteMock: vi.fn(), getBranchConflictKindMock: vi.fn(), getPRForBranchMock: vi.fn(), getWorkItemMock: vi.fn(), getPullRequestPushTargetMock: vi.fn(), getEffectiveHooksMock: vi.fn(), createIssueCommandRunnerScriptMock: vi.fn(), createSetupRunnerScriptMock: vi.fn(), getEffectiveHooksFromConfigMock: vi.fn(), parseOrcaYamlMock: vi.fn(), shouldRunSetupForCreateMock: vi.fn(), buildPosixRunnerScriptMock: vi.fn(), buildWindowsRunnerScriptMock: vi.fn(), getSetupRunnerEnvVarsMock: vi.fn(), runHookMock: vi.fn(), hasHooksFileMock: vi.fn(), loadHooksMock: vi.fn(), computeWorktreePathMock: vi.fn(), ensurePathWithinWorkspaceMock: vi.fn(), gitExecFileAsyncMock: vi.fn(), getSshGitProviderMock: vi.fn(), getSshFilesystemProviderMock: vi.fn(), getActiveMultiplexerMock: vi.fn() })) vi.mock('electron', () => ({ ipcMain: { handle: handleMock, removeHandler: removeHandlerMock } })) vi.mock('../git/worktree', () => ({ listWorktrees: listWorktreesMock, assertWorktreeCleanForRemoval: assertWorktreeCleanForRemovalMock, addWorktree: addWorktreeMock, addSparseWorktree: addSparseWorktreeMock, removeWorktree: removeWorktreeMock })) vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock, gitExecFileSync: vi.fn() })) vi.mock('../git/repo', () => ({ getGitUsername: getGitUsernameMock, getDefaultBaseRef: getDefaultBaseRefMock, getDefaultRemote: getDefaultRemoteMock, getBranchConflictKind: getBranchConflictKindMock })) vi.mock('../github/client', () => ({ getPRForBranch: getPRForBranchMock, getWorkItem: getWorkItemMock, getPullRequestPushTarget: getPullRequestPushTargetMock })) vi.mock('../providers/ssh-git-dispatch', () => ({ getSshGitProvider: getSshGitProviderMock, SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'Remote connection dropped. Click Reconnect on the SSH target before retrying.', requireSshGitProvider: (connectionId: string) => { const provider = getSshGitProviderMock(connectionId) if (!provider) { throw new Error( 'Remote connection dropped. Click Reconnect on the SSH target before retrying.' ) } return provider } })) vi.mock('../providers/ssh-filesystem-dispatch', () => ({ getSshFilesystemProvider: getSshFilesystemProviderMock })) vi.mock('./ssh', () => ({ getActiveMultiplexer: getActiveMultiplexerMock })) vi.mock('../hooks', () => ({ buildPosixRunnerScript: buildPosixRunnerScriptMock, buildWindowsRunnerScript: buildWindowsRunnerScriptMock, createIssueCommandRunnerScript: createIssueCommandRunnerScriptMock, createSetupRunnerScript: createSetupRunnerScriptMock, getEffectiveHooks: getEffectiveHooksMock, getEffectiveHooksFromConfig: getEffectiveHooksFromConfigMock, getSetupRunnerEnvVars: getSetupRunnerEnvVarsMock, loadHooks: loadHooksMock, parseOrcaYaml: parseOrcaYamlMock, runHook: runHookMock, hasHooksFile: hasHooksFileMock, shouldRunSetupForCreate: shouldRunSetupForCreateMock })) vi.mock('./worktree-logic', async (importOriginal) => { const actual = (await importOriginal()) as Record return { ...actual, computeWorktreePath: computeWorktreePathMock, ensurePathWithinWorkspace: ensurePathWithinWorkspaceMock } }) const { deleteWorktreeHistoryDirMock } = vi.hoisted(() => ({ deleteWorktreeHistoryDirMock: vi.fn() })) vi.mock('../terminal-history', () => ({ deleteWorktreeHistoryDir: deleteWorktreeHistoryDirMock })) const { killAllProcessesForWorktreeMock, getLocalPtyProviderMock } = vi.hoisted(() => ({ killAllProcessesForWorktreeMock: vi.fn(), getLocalPtyProviderMock: vi.fn() })) vi.mock('../runtime/worktree-teardown', () => ({ killAllProcessesForWorktree: killAllProcessesForWorktreeMock })) vi.mock('./pty', () => ({ getLocalPtyProvider: getLocalPtyProviderMock })) import { registerWorktreeHandlers } from './worktrees' type HandlerMap = Record unknown> describe('registerWorktreeHandlers', () => { const handlers: HandlerMap = {} const mainWindow = { isDestroyed: () => false, webContents: { send: vi.fn() } } const store = { getRepos: vi.fn(), getRepo: vi.fn(), getSparsePresets: vi.fn(), getSettings: vi.fn(), getWorktreeMeta: vi.fn(), getAllWorktreeMeta: vi.fn(), setWorktreeMeta: vi.fn(), removeWorktreeMeta: vi.fn(), getAllWorktreeLineage: vi.fn(), removeWorktreeLineage: vi.fn() } let runtimeStub: { resolveRemoteTrackingBase: ReturnType hasRemoteTrackingRef: ReturnType getOrStartRemoteTrackingBaseRefresh: ReturnType getOrStartRemoteFetch: ReturnType fetchRemoteWithCache: ReturnType emitWorktreeBaseStatus: ReturnType recordOptimisticReconcileToken: ReturnType reconcileWorktreeBaseStatus: ReturnType clearOptimisticReconcileToken: ReturnType resolveManagedMrBase: ReturnType } beforeEach(() => { for (const m of [ handleMock, removeHandlerMock, listWorktreesMock, assertWorktreeCleanForRemovalMock, addWorktreeMock, addSparseWorktreeMock, removeWorktreeMock, getGitUsernameMock, getDefaultBaseRefMock, getDefaultRemoteMock, getBranchConflictKindMock, getPRForBranchMock, getWorkItemMock, getPullRequestPushTargetMock, getEffectiveHooksMock, getEffectiveHooksFromConfigMock, parseOrcaYamlMock, createIssueCommandRunnerScriptMock, createSetupRunnerScriptMock, buildPosixRunnerScriptMock, buildWindowsRunnerScriptMock, getSetupRunnerEnvVarsMock, shouldRunSetupForCreateMock, runHookMock, hasHooksFileMock, loadHooksMock, computeWorktreePathMock, ensurePathWithinWorkspaceMock, gitExecFileAsyncMock, getSshGitProviderMock, getSshFilesystemProviderMock, getActiveMultiplexerMock, mainWindow.webContents.send, store.getRepos, store.getRepo, store.getSparsePresets, store.getSettings, store.getWorktreeMeta, store.getAllWorktreeMeta, store.setWorktreeMeta, store.removeWorktreeMeta, store.getAllWorktreeLineage, store.removeWorktreeLineage, killAllProcessesForWorktreeMock, getLocalPtyProviderMock, deleteWorktreeHistoryDirMock ]) { m.mockReset() } killAllProcessesForWorktreeMock.mockResolvedValue({ runtimeStopped: 0, providerStopped: 0, registryStopped: 0 }) assertWorktreeCleanForRemovalMock.mockResolvedValue(undefined) getLocalPtyProviderMock.mockReturnValue({} as never) for (const key of Object.keys(handlers)) { delete handlers[key] } handleMock.mockImplementation((channel, handler) => { handlers[channel] = handler }) const repo = { id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0 } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue({ ...repo, worktreeBaseRef: null }) store.getSparsePresets.mockReturnValue([]) store.getSettings.mockReturnValue({ branchPrefix: 'none', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: false, workspaceDir: '/workspace' }) store.getWorktreeMeta.mockReturnValue(undefined) store.getAllWorktreeMeta.mockReturnValue({}) store.setWorktreeMeta.mockReturnValue({}) store.getAllWorktreeLineage.mockReturnValue({}) getGitUsernameMock.mockReturnValue('') getDefaultBaseRefMock.mockReturnValue('origin/main') getDefaultRemoteMock.mockResolvedValue('origin') getBranchConflictKindMock.mockResolvedValue(null) getPRForBranchMock.mockResolvedValue(null) getWorkItemMock.mockResolvedValue(null) getPullRequestPushTargetMock.mockResolvedValue(null) // Why: createLocalWorktree can still hit legacy git fetch fallback in // narrow unit harnesses. Return a resolved promise so catch/then chains // don't trip on undefined. gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' }) getEffectiveHooksMock.mockReturnValue(null) getEffectiveHooksFromConfigMock.mockReturnValue(null) parseOrcaYamlMock.mockReturnValue(null) shouldRunSetupForCreateMock.mockReturnValue(false) buildPosixRunnerScriptMock.mockImplementation( (script: string) => `#!/usr/bin/env bash\nset -e\n${script.replace(/\r\n/g, '\n')}\n` ) buildWindowsRunnerScriptMock.mockImplementation((script: string) => script) getSetupRunnerEnvVarsMock.mockImplementation( (repoArg: { path: string }, worktreePath: string) => ({ ORCA_ROOT_PATH: repoArg.path, ORCA_WORKTREE_PATH: worktreePath, ORCA_WORKSPACE_NAME: worktreePath.split('/').at(-1) ?? '', CONDUCTOR_ROOT_PATH: repoArg.path, GHOSTX_ROOT_PATH: repoArg.path }) ) createSetupRunnerScriptMock.mockReturnValue({ runnerScriptPath: '/workspace/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/workspace/repo', ORCA_WORKTREE_PATH: '/workspace/improve-dashboard' } }) createIssueCommandRunnerScriptMock.mockReturnValue({ runnerScriptPath: '/workspace/repo/.git/orca/issue-command-runner.sh', envVars: { ORCA_ROOT_PATH: '/workspace/repo', ORCA_WORKTREE_PATH: '/workspace/improve-dashboard' } }) computeWorktreePathMock.mockImplementation( ( sanitizedName: string, repoPath: string, settings: { nestWorkspaces: boolean; workspaceDir: string } ) => { if (settings.nestWorkspaces) { const repoName = repoPath .split(/[\\/]/) .at(-1) ?.replace(/\.git$/, '') ?? 'repo' return `${settings.workspaceDir}/${repoName}/${sanitizedName}` } return `${settings.workspaceDir}/${sanitizedName}` } ) ensurePathWithinWorkspaceMock.mockImplementation((targetPath: string) => targetPath) listWorktreesMock.mockResolvedValue([]) // Why: createLocalWorktree routes `git fetch` through // `runtime.fetchRemoteWithCache` (§3.3 Lifecycle). A minimal stub // keeps these tests focused on create-flow semantics; the full // cache behavior is covered by fetch-remote-cache.test.ts. runtimeStub = { resolveRemoteTrackingBase: vi.fn().mockResolvedValue(null), hasRemoteTrackingRef: vi.fn().mockResolvedValue(false), getOrStartRemoteTrackingBaseRefresh: vi.fn().mockResolvedValue({ ok: true }), getOrStartRemoteFetch: vi.fn().mockResolvedValue({ ok: true }), fetchRemoteWithCache: vi.fn().mockResolvedValue(undefined), emitWorktreeBaseStatus: vi.fn(), recordOptimisticReconcileToken: vi.fn().mockReturnValue('token-1'), reconcileWorktreeBaseStatus: vi.fn(), clearOptimisticReconcileToken: vi.fn(), resolveManagedMrBase: vi.fn().mockResolvedValue({ baseBranch: 'origin/mr-branch' }) } registerWorktreeHandlers(mainWindow as never, store as never, runtimeStub as never) }) it('clears the GitLab MR base handler before re-registering IPC handlers', () => { expect(removeHandlerMock).toHaveBeenCalledWith('worktrees:resolveMrBase') expect(handlers['worktrees:resolveMrBase']).toBeDefined() }) function mockKnownFeatureWorktree(path = '/workspace/feature-wt'): void { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path, head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]) } function makeWorktreeMeta(overrides: Record = {}): Record { return { displayName: '', comment: '', linkedIssue: null, linkedPR: null, linkedLinearIssue: null, isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 0, ...overrides } } it('auto-suffixes the branch name when the first choice collides with a remote branch', async () => { // Why: new-workspace flow should silently try improve-dashboard-2, -3, ... // rather than failing and forcing the user back to the name picker. getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'improve-dashboard' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard-2', head: 'abc123', branch: 'improve-dashboard-2', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard-2', 'improve-dashboard-2', 'origin/main', false ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/improve-dashboard-2', branch: 'improve-dashboard-2' }) }) }) it('uses branchNameOverride for the git branch while keeping the sanitized worktree path', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/feature-something', head: 'abc123', branch: 'feature/something', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'feature/something', branchNameOverride: 'feature/something' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['check-ref-format', '--branch', 'feature/something'], { cwd: '/workspace/repo' } ) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-something', 'feature/something', 'origin/main', false ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/feature-something', branch: 'feature/something' }) }) }) it('creates an additional workspace for folder-mode repos without git worktree add', async () => { const repo = { id: 'repo-folder', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' as const } store.getRepo.mockReturnValue(repo) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => ({ displayName: '', comment: '', linkedIssue: null, linkedPR: null, linkedLinearIssue: null, isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 0, ...meta })) const result = (await handlers['worktrees:create'](null, { repoId: 'repo-folder', name: 'folder-session', createdWithAgent: 'codex' })) as { worktree: { id: string } } expect(addWorktreeMock).not.toHaveBeenCalled() expect(result.worktree).toEqual( expect.objectContaining({ id: expect.stringMatching(/^repo-folder::\/workspace\/folder::workspace:[0-9a-f-]{36}$/), repoId: 'repo-folder', path: '/workspace/folder', displayName: 'folder-session', instanceId: expect.stringMatching(/^[0-9a-f-]{36}$/), createdWithAgent: 'codex' }) ) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-folder' }) }) it('checks out a selected existing local branch exactly', async () => { listWorktreesMock .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/fix-bug-0', head: 'abc123', branch: 'refs/heads/fix/bug-0', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix/bug-0', baseBranch: 'fix/bug-0', branchNameOverride: 'fix/bug-0' }) expect(getBranchConflictKindMock).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-bug-0', 'fix/bug-0', 'fix/bug-0', false, false, { checkoutExistingBranch: true } ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/fix-bug-0', expect.objectContaining({ preserveBranchOnDelete: true }) ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/fix-bug-0', branch: 'refs/heads/fix/bug-0' }) }) }) it('suffixes only the path when an existing local branch checkout path already exists', async () => { const mainWorktree = { path: '/workspace/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } computeWorktreePathMock.mockImplementation((sanitizedName: string) => sanitizedName === 'fix-bug-0' ? process.cwd() : `/workspace/${sanitizedName}` ) listWorktreesMock .mockResolvedValueOnce([mainWorktree]) .mockResolvedValueOnce([mainWorktree]) .mockResolvedValueOnce([ mainWorktree, { path: '/workspace/fix-bug-0-2', head: 'abc123', branch: 'refs/heads/fix/bug-0', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix/bug-0', baseBranch: 'fix/bug-0', branchNameOverride: 'fix/bug-0' }) expect(getBranchConflictKindMock).not.toHaveBeenCalled() expect(getPRForBranchMock).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-bug-0-2', 'fix/bug-0', 'fix/bug-0', false, false, { checkoutExistingBranch: true } ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/fix-bug-0-2', branch: 'refs/heads/fix/bug-0' }) }) }) it('suffixes branchNameOverride without flattening slashes when the first branch collides', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/something' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/feature-something-2', head: 'abc123', branch: 'feature/something-2', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'feature/something', branchNameOverride: 'feature/something' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['check-ref-format', '--branch', 'feature/something-2'], { cwd: '/workspace/repo' } ) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-something-2', 'feature/something-2', 'origin/main', false ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/feature-something-2', branch: 'feature/something-2' }) }) }) it('persists a sanitized artifact title as the worktree display name', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', displayName: ' Fix: dashboards\nfor PRs\u0000 ' }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ displayName: 'Fix: dashboards for PRs' }) ) expect(result).toEqual({ worktree: expect.objectContaining({ displayName: 'Fix: dashboards for PRs' }) }) }) it('persists linked issue and PR metadata during local create', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', linkedIssue: 123, linkedPR: 456, linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ linkedIssue: 123, linkedPR: 456, linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) ) expect(result).toEqual({ worktree: expect.objectContaining({ linkedIssue: 123, linkedPR: 456, linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) }) }) it('persists the selected creation agent during local create', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', createdWithAgent: 'codex' }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ createdWithAgent: 'codex' }) ) expect(result).toEqual({ worktree: expect.objectContaining({ createdWithAgent: 'codex' }) }) }) it('configures a PR push target during local create', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', pushTarget: { remoteName: 'pr-prateek-orca', branchName: 'prateek/fix-sidebar-agents-toggle', remoteUrl: 'git@github.com:prateek/orca.git' } }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['remote', 'add', 'pr-prateek-orca', 'git@github.com:prateek/orca.git'], { cwd: '/workspace/repo' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'fetch', 'pr-prateek-orca', '+refs/heads/prateek/fix-sidebar-agents-toggle:refs/remotes/pr-prateek-orca/prateek/fix-sidebar-agents-toggle' ], { cwd: '/workspace/repo' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'branch', '--set-upstream-to', 'pr-prateek-orca/prateek/fix-sidebar-agents-toggle', 'improve-dashboard' ], { cwd: '/workspace/improve-dashboard' } ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ pushTarget: expect.objectContaining({ remoteName: 'pr-prateek-orca', branchName: 'prateek/fix-sidebar-agents-toggle', remoteUrl: 'git@github.com:prateek/orca.git', remoteCreated: true }) }) ) }) it('keeps the Orca-created marker when a new worktree reuses an Orca-created fork remote', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) const existingPushTarget = { remoteName: 'pr-contributor-orca', branchName: 'contributor/previous-fix', remoteUrl: 'https://github.com/contributor/orca.git', remoteCreated: true } store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/workspace/previous-fix': makeWorktreeMeta({ pushTarget: existingPushTarget }) }) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'remote' && args.length === 1) { return { stdout: 'pr-contributor-orca\n', stderr: '' } } if (args[0] === 'remote' && args[1] === 'get-url') { return { stdout: 'https://github.com/contributor/orca.git\n', stderr: '' } } return { stdout: '', stderr: '' } }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', pushTarget: { remoteName: 'pr-contributor-orca', branchName: 'contributor/new-fix', remoteUrl: 'https://github.com/contributor/orca.git' } }) expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( ['remote', 'add', expect.any(String), expect.any(String)], expect.any(Object) ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ pushTarget: expect.objectContaining({ remoteName: 'pr-contributor-orca', branchName: 'contributor/new-fix', remoteUrl: 'https://github.com/contributor/orca.git', remoteCreated: true }) }) ) }) it('returns the PR head push target when resolving a fork PR base', async () => { getPullRequestPushTargetMock.mockResolvedValue({ remoteName: 'pr-prateek-orca', branchName: 'prateek/fix-sidebar-agents-toggle', remoteUrl: 'git@github.com:prateek/orca.git' }) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse') { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 1738, headRefName: 'prateek/fix-sidebar-agents-toggle', isCrossRepository: true }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1738/head'], { cwd: '/workspace/repo' }) expect(result).toEqual({ baseBranch: 'abc123', pushTarget: { remoteName: 'pr-prateek-orca', branchName: 'prateek/fix-sidebar-agents-toggle', remoteUrl: 'git@github.com:prateek/orca.git' } }) }) it('resolves a fork PR base even when push-target discovery fails', async () => { getPullRequestPushTargetMock.mockRejectedValueOnce(new Error('lookup failed')) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse') { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 1849, headRefName: 'feat/onboarding-model-choice-782', isCrossRepository: true }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1849/head'], { cwd: '/workspace/repo' }) expect(result).toEqual({ baseBranch: 'abc123' }) }) it('falls back to refs/pull//head when branch fetch fails for a PR', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if ( args[0] === 'fetch' && args[2] === '+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782' ) { throw new Error( 'fatal: could not find remote ref refs/heads/feat/onboarding-model-choice-782' ) } if (args[0] === 'rev-parse') { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 1849, headRefName: 'feat/onboarding-model-choice-782' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'fetch', 'origin', '+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782' ], { cwd: '/workspace/repo' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1849/head'], { cwd: '/workspace/repo' }) expect(result).toEqual({ baseBranch: 'abc123' }) }) it('does not fall back to refs/pull//head when branch fetch hits a network failure', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if ( args[0] === 'fetch' && args[2] === '+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782' ) { throw new Error('fatal: unable to access repo: Could not resolve host: github.com') } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 1849, headRefName: 'feat/onboarding-model-choice-782' }) expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( ['fetch', 'origin', 'refs/pull/1849/head'], expect.anything() ) expect(result).toEqual({ error: 'Failed to fetch origin/feat/onboarding-model-choice-782: fatal: unable to access repo: Could not resolve host: github.com' }) }) it('delegates GitLab MR base resolution through the runtime implementation', async () => { runtimeStub.resolveManagedMrBase.mockResolvedValueOnce({ baseBranch: 'fork-mr-sha', pushTarget: { remoteName: 'origin', branchName: 'feature/mr' } }) const result = await handlers['worktrees:resolveMrBase'](null, { repoId: 'repo-1', mrIid: 42, sourceBranch: 'feature/mr', isCrossRepository: true }) expect(runtimeStub.resolveManagedMrBase).toHaveBeenCalledWith({ repoSelector: 'id:repo-1', mrIid: 42, sourceBranch: 'feature/mr', isCrossRepository: true }) expect(result).toEqual({ baseBranch: 'fork-mr-sha', pushTarget: { remoteName: 'origin', branchName: 'feature/mr' } }) }) it('persists linked issue, PR, and selected agent metadata during remote create', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'improve-dashboard', linkedIssue: 123, linkedPR: 456, createdWithAgent: 'codex', linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-ssh::/remote/improve-dashboard', expect.objectContaining({ linkedIssue: 123, linkedPR: 456, createdWithAgent: 'codex', linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) ) expect(result).toEqual({ worktree: expect.objectContaining({ linkedIssue: 123, linkedPR: 456, createdWithAgent: 'codex', linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) }) }) it('reads remote orca.yaml and returns a setup launch payload during SSH create', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse') { return { stdout: '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n setup: pnpm install\n', isBinary: false }), createDir: vi.fn().mockResolvedValue(undefined), writeFile: vi.fn().mockResolvedValue(undefined) } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getActiveMultiplexerMock.mockReturnValue(mux) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) parseOrcaYamlMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) shouldRunSetupForCreateMock.mockReturnValue(true) const result = await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'improve-dashboard', setupDecision: 'run' }) expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml') expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/improve-dashboard/orca.yaml') expect(provider.exec).toHaveBeenCalledWith( ['rev-parse', '--git-path', 'orca/setup-runner.sh'], '/remote/improve-dashboard' ) expect(fsProvider.createDir).toHaveBeenCalledWith( '/remote/repo/.git/worktrees/improve-dashboard/orca' ) expect(fsProvider.writeFile).toHaveBeenCalledWith( '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh', '#!/usr/bin/env bash\nset -e\npnpm install\n' ) expect(result).toEqual( expect.objectContaining({ setup: { runnerScriptPath: '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh', envVars: expect.objectContaining({ ORCA_ROOT_PATH: '/remote/repo', ORCA_WORKTREE_PATH: '/remote/improve-dashboard' }) } }) ) }) it('creates sparse checkout metadata and remote sparse config for SSH worktrees', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), removeWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/sparse-dashboard', head: 'abc123', branch: 'refs/heads/sparse-dashboard', isBare: false, isSparse: true, isMainWorktree: false } ]) } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) store.getSparsePresets.mockReturnValue([ { id: 'preset-1', repoId: 'repo-ssh', name: 'App', directories: ['apps/mobile', 'packages/shared'], createdAt: 1, updatedAt: 1 } ]) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'sparse-dashboard', sparseCheckout: { directories: [' apps/mobile ', 'packages/shared', 'apps/mobile'], presetId: 'preset-1' } }) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'sparse-dashboard', '/remote/repo/../sparse-dashboard', { base: 'origin/main', noCheckout: true } ) expect(provider.exec).toHaveBeenCalledWith( ['sparse-checkout', 'init', '--cone'], '/remote/repo/../sparse-dashboard' ) expect(provider.exec).toHaveBeenCalledWith( ['sparse-checkout', 'set', '--', 'apps/mobile', 'packages/shared'], '/remote/repo/../sparse-dashboard' ) expect(provider.exec).toHaveBeenCalledWith( ['checkout', 'sparse-dashboard'], '/remote/repo/../sparse-dashboard' ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-ssh::/remote/sparse-dashboard', expect.objectContaining({ sparseDirectories: ['apps/mobile', 'packages/shared'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) ) expect(result).toEqual({ worktree: expect.objectContaining({ isSparse: true, sparseDirectories: ['apps/mobile', 'packages/shared'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) }) }) it('does not create an SSH worktree when remote-tracking base refresh fails', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'fetch') { throw new Error('network unavailable') } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockRejectedValue(new Error('network unavailable')), addWorktree: vi.fn(), listWorktrees: vi.fn() } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) await expect( handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'improve-dashboard' }) ).rejects.toThrow( 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' ) expect(provider.addWorktree).not.toHaveBeenCalled() expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main' ) }) it('prunes stale child lineage after a successful SSH worktree scan proves the child is missing', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/live', head: 'abc123', branch: 'refs/heads/live', isBare: false, isMainWorktree: false }, { path: '/remote/live-child', head: 'def456', branch: 'refs/heads/live-child', isBare: false, isMainWorktree: false } ]) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) store.getAllWorktreeLineage.mockReturnValue({ 'repo-ssh::/remote/missing-child': { parentWorktreeId: 'repo-ssh::/remote/live' }, 'repo-ssh::/remote/live-child': { parentWorktreeId: 'repo-ssh::/remote/missing-parent', parentWorktreeInstanceId: 'old-parent-instance' }, 'repo-ssh::/remote/live': { parentWorktreeId: 'other-repo::/elsewhere' } }) store.getWorktreeMeta.mockImplementation((worktreeId: string) => worktreeId === 'repo-ssh::/remote/missing-parent' ? { instanceId: 'old-parent-instance' } : undefined ) await handlers['worktrees:list'](null, { repoId: 'repo-ssh' }) expect(store.removeWorktreeLineage).toHaveBeenCalledWith('repo-ssh::/remote/missing-child') expect(store.removeWorktreeLineage).not.toHaveBeenCalledWith('repo-ssh::/remote/live-child') expect(store.removeWorktreeLineage).not.toHaveBeenCalledWith('repo-ssh::/remote/live') expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-ssh::/remote/missing-parent', expect.objectContaining({ instanceId: expect.any(String) }) ) }) it('does not repeatedly rotate already-invalid missing parent metadata', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/live-child', head: 'def456', branch: 'refs/heads/live-child', isBare: false, isMainWorktree: false } ]) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) store.getAllWorktreeLineage.mockReturnValue({ 'repo-ssh::/remote/live-child': { parentWorktreeId: 'repo-ssh::/remote/missing-parent', parentWorktreeInstanceId: 'old-parent-instance' } }) store.getWorktreeMeta.mockReturnValue({ instanceId: 'rotated-parent-instance' }) await handlers['worktrees:list'](null, { repoId: 'repo-ssh' }) expect(store.setWorktreeMeta).not.toHaveBeenCalledWith( 'repo-ssh::/remote/missing-parent', expect.objectContaining({ instanceId: expect.any(String) }) ) }) it('awaits a cold refresh before creating from an existing remote-tracking base', async () => { const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } let resolveFetch!: () => void const pendingFetch = new Promise<{ ok: true }>((resolve) => { resolveFetch = () => resolve({ ok: true }) }) runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockReturnValue(pendingFetch) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'created-sha', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' }) const createPromise = handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) as Promise const earlyResult = await Promise.race([ createPromise.then(() => 'resolved'), new Promise((resolve) => setTimeout(() => resolve('pending'), 0)) ]) expect(earlyResult).toBe('pending') expect(addWorktreeMock).not.toHaveBeenCalled() expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( '/workspace/repo', remoteBase ) expect(runtimeStub.fetchRemoteWithCache).not.toHaveBeenCalled() resolveFetch() const result = await createPromise expect(addWorktreeMock).toHaveBeenCalled() expect(result).toEqual( expect.objectContaining({ worktree: expect.objectContaining({ id: 'repo-1::/workspace/improve-dashboard' }) }) ) }) it('does not create when the pre-create remote-tracking refresh fails', async () => { const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: false, errorKind: 'git_error' }) await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) ).rejects.toThrow( 'Could not refresh base ref "origin/main" from "origin". Check your network and try again.' ) expect(addWorktreeMock).not.toHaveBeenCalled() }) it('refreshes before create even when the remote-tracking base was recently fetched', async () => { const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'created-sha', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockResolvedValue({ stdout: 'created-sha\n', stderr: '' }) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( '/workspace/repo', remoteBase ) expect(result).toEqual( expect.objectContaining({ worktree: expect.objectContaining({ id: 'repo-1::/workspace/improve-dashboard' }) }) ) }) it('throws a clear error when no default base ref can be resolved', async () => { // Why: guard against regressing to a silent 'origin/main' fallback. When // getDefaultBaseRef returns null (e.g. a fresh repo with no origin/HEAD, // no origin/main, no origin/master, and no local main/master), we must // fail loudly with a message that prompts the user to pick a base // branch, not hand a non-existent ref to `git worktree add`. getDefaultBaseRefMock.mockReturnValue(null) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null }) await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) ).rejects.toThrow(/Could not resolve a default base ref/) expect(addWorktreeMock).not.toHaveBeenCalled() }) it('creates an issue-command runner for an existing repo/worktree pair', async () => { const result = await handlers['hooks:createIssueCommandRunner'](null, { repoId: 'repo-1', worktreePath: '/workspace/improve-dashboard', command: 'codex exec "long command"' }) expect(createIssueCommandRunnerScriptMock).toHaveBeenCalledWith( expect.objectContaining({ id: 'repo-1' }), '/workspace/improve-dashboard', 'codex exec "long command"' ) expect(result).toEqual({ runnerScriptPath: '/workspace/repo/.git/orca/issue-command-runner.sh', envVars: { ORCA_ROOT_PATH: '/workspace/repo', ORCA_WORKTREE_PATH: '/workspace/improve-dashboard' } }) }) it('lists a synthetic worktree for folder-mode repos', async () => { store.getRepos.mockReturnValue([ { id: 'repo-1', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' } ]) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' }) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(listed).toEqual([ expect.objectContaining({ id: 'repo-1::/workspace/folder', repoId: 'repo-1', path: '/workspace/folder', displayName: 'folder', branch: '', head: '', isMainWorktree: true }) ]) expect(listWorktreesMock).not.toHaveBeenCalled() }) it('returns reconstructed rows when an SSH provider is unavailable', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } store.getRepo.mockReturnValue(repo) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::/remote/feature-wt': makeWorktreeMeta({ displayName: 'Feature workspace', comment: 'persisted comment', linkedIssue: 123, linkedPR: 456, linkedLinearIssue: 'LIN-123', isArchived: true, isUnread: true, isPinned: true, sortOrder: 7, lastActivityAt: 42, workspaceStatus: 'blocked', diffComments: [ { id: 'comment-1', worktreeId: 'repo-ssh::/remote/feature-wt', filePath: 'src/app.ts', lineNumber: 10, body: 'check this', createdAt: 1, updatedAt: 1 } ], sparseDirectories: ['packages/web'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) }) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-ssh' }) expect(listed).toEqual([ expect.objectContaining({ id: 'repo-ssh::/remote/feature-wt', repoId: 'repo-ssh', path: '/remote/feature-wt', head: '', branch: '', isBare: false, isMainWorktree: false, isSparse: true, displayName: 'Feature workspace', comment: 'persisted comment', linkedIssue: 123, linkedPR: 456, linkedLinearIssue: 'LIN-123', isArchived: true, isUnread: true, isPinned: true, sortOrder: 7, lastActivityAt: 42, workspaceStatus: 'blocked', sparseDirectories: ['packages/web'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1', diffComments: [ expect.objectContaining({ id: 'comment-1', filePath: 'src/app.ts' }) ] }) ]) expect(store.getWorktreeMeta).not.toHaveBeenCalled() expect(store.setWorktreeMeta).not.toHaveBeenCalled() }) it('falls back to reconstructed SSH rows when provider listing throws', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } const provider = { listWorktrees: vi.fn().mockRejectedValue(new Error('connection lost')) } store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::/remote/feature-wt': makeWorktreeMeta({ displayName: 'Feature workspace', lastActivityAt: 42 }) }) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-ssh' }) expect(provider.listWorktrees).toHaveBeenCalledWith('/remote/repo') expect(listed).toEqual([ expect.objectContaining({ id: 'repo-ssh::/remote/feature-wt', displayName: 'Feature workspace', lastActivityAt: 42 }) ]) }) it('keeps local listing failure behavior as an empty list', async () => { listWorktreesMock.mockRejectedValue(new Error('filesystem denied')) store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/workspace/feature-wt': makeWorktreeMeta({ displayName: 'Should not appear' }) }) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(listed).toEqual([]) expect(store.getAllWorktreeMeta).not.toHaveBeenCalled() }) it('ignores malformed metadata keys during SSH fallback', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } store.getRepo.mockReturnValue(repo) store.getAllWorktreeMeta.mockReturnValue({ 'not-a-worktree-id': makeWorktreeMeta({ displayName: 'Bad row' }), 'repo-ssh::/remote/feature-wt': makeWorktreeMeta({ displayName: 'Good row' }) }) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-ssh' }) expect(listed).toEqual([ expect.objectContaining({ id: 'repo-ssh::/remote/feature-wt', displayName: 'Good row' }) ]) }) it('does not use the repo display name for sparse fallback rows with empty branches', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } store.getRepo.mockReturnValue(repo) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::/remote/custom-name': makeWorktreeMeta({ sparseDirectories: ['packages/web'] }) }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-ssh' })) as { displayName: string isSparse?: boolean sparseDirectories?: string[] }[] expect(listed[0]).toMatchObject({ displayName: 'custom-name', isSparse: true, sparseDirectories: ['packages/web'] }) }) it('uses path equivalence to mark the reconstructed SSH main worktree', async () => { const repo = { id: 'repo-ssh', path: 'C:\\Remote\\Repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } store.getRepo.mockReturnValue(repo) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::c:/remote/repo': makeWorktreeMeta() }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-ssh' })) as { isMainWorktree: boolean }[] expect(listed[0].isMainWorktree).toBe(true) }) it('includes SSH fallback rows in listAll alongside healthy local rows', async () => { const sshRepo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'SSH Repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } const localRepo = { id: 'repo-local', path: '/workspace/local', displayName: 'Local Repo', badgeColor: '#111', addedAt: 0 } store.getRepos.mockReturnValue([sshRepo, localRepo]) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::/remote/feature-wt': makeWorktreeMeta({ displayName: 'Remote cached' }) }) listWorktreesMock.mockResolvedValue([ { path: '/workspace/local', head: 'abc123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) const listed = await handlers['worktrees:listAll'](null, undefined) expect(store.getAllWorktreeMeta).toHaveBeenCalledTimes(1) expect(listed).toEqual( expect.arrayContaining([ expect.objectContaining({ id: 'repo-ssh::/remote/feature-wt', displayName: 'Remote cached' }), expect.objectContaining({ id: 'repo-local::/workspace/local', branch: 'refs/heads/main' }) ]) ) }) it('snapshots SSH fallback metadata once for listAll', async () => { const sshRepoA = { id: 'repo-ssh-a', path: '/remote/a', displayName: 'SSH A', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1' } const sshRepoB = { id: 'repo-ssh-b', path: '/remote/b', displayName: 'SSH B', badgeColor: '#111', addedAt: 0, connectionId: 'conn-2' } store.getRepos.mockReturnValue([sshRepoA, sshRepoB]) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh-a::/remote/a/one': makeWorktreeMeta({ displayName: 'One' }), 'repo-ssh-b::/remote/b/two': makeWorktreeMeta({ displayName: 'Two' }) }) const listed = await handlers['worktrees:listAll'](null, undefined) expect(store.getAllWorktreeMeta).toHaveBeenCalledTimes(1) expect(listed).toEqual([ expect.objectContaining({ id: 'repo-ssh-a::/remote/a/one' }), expect.objectContaining({ id: 'repo-ssh-b::/remote/b/two' }) ]) }) it('stamps lastActivityAt on first discovery so newly-added worktrees sort to the top of Recent', async () => { // Why: a worktree that exists on disk but has no persisted WorktreeMeta // (e.g. a folder repo just added, or a pre-existing worktree in a // newly-added git repo) would otherwise fall back to `lastActivityAt: 0` // and rank dead last in the Recent sort. listWorktreesMock.mockResolvedValue([ { path: '/workspace/discovered-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue(undefined) const stampedMeta = { lastActivityAt: 1_700_000_000_000 } store.setWorktreeMeta.mockReturnValue(stampedMeta) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as { id: string lastActivityAt: number }[] expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/discovered-wt', expect.objectContaining({ lastActivityAt: expect.any(Number) }) ) expect(listed[0]).toMatchObject({ id: 'repo-1::/workspace/discovered-wt', lastActivityAt: 1_700_000_000_000 }) }) it('does not re-stamp lastActivityAt when a worktree already has persisted meta', async () => { // Why: only the *first* discovery should stamp. Re-stamping on every list // would overwrite real activity and reshuffle the sidebar on refresh. listWorktreesMock.mockResolvedValue([ { path: '/workspace/existing-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue({ displayName: '', comment: '', linkedIssue: null, linkedPR: null, instanceId: 'existing-instance', isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 42 }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as { id: string lastActivityAt: number }[] expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(listed[0].lastActivityAt).toBe(42) }) it('backfills instanceId on discovery for persisted metadata from older profiles', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/existing-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue({ displayName: '', comment: '', linkedIssue: null, linkedPR: null, isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 42 }) store.setWorktreeMeta.mockReturnValue({ instanceId: 'new-instance', lastActivityAt: 42 }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as { id: string instanceId?: string }[] expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/existing-wt', expect.objectContaining({ instanceId: expect.any(String) }) ) expect(listed[0].instanceId).toBe('new-instance') }) it('stamps lastActivityAt on first discovery for folder-mode repos', async () => { // Why: folder repos produce a synthetic worktree that flows through the // same list path. Without the stamp, adding a folder puts its card at the // bottom of Recent even though the user just added it. store.getRepos.mockReturnValue([ { id: 'repo-1', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' } ]) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' }) store.getWorktreeMeta.mockReturnValue(undefined) store.setWorktreeMeta.mockReturnValue({ lastActivityAt: 1_700_000_000_000 }) await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/folder', expect.objectContaining({ lastActivityAt: expect.any(Number) }) ) }) it('stamps lastActivityAt on first discovery via worktrees:listAll', async () => { // Why: the stamping logic lives in both worktrees:list and worktrees:listAll. // Without a dedicated test, a regression in the listAll loop would silently // bury newly-discovered worktrees from the multi-repo sidebar view. listWorktreesMock.mockResolvedValue([ { path: '/workspace/discovered-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue(undefined) store.setWorktreeMeta.mockReturnValue({ lastActivityAt: 1_700_000_000_000 }) const listed = (await handlers['worktrees:listAll'](null, undefined)) as { id: string lastActivityAt: number }[] expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/discovered-wt', expect.objectContaining({ lastActivityAt: expect.any(Number) }) ) expect(listed[0]).toMatchObject({ id: 'repo-1::/workspace/discovered-wt', lastActivityAt: 1_700_000_000_000 }) }) it('skips past a suffix that already belongs to a PR after an initial branch conflict', async () => { // Why: `gh pr list` is network-bound and previously fired on every single // create, adding 1–3s to the happy path. We now only probe PR conflicts // from suffix=2 onward — once a local/remote branch collision has already // forced us past the first candidate and uniqueness matters enough to // justify the GitHub round-trip. This test covers that delayed path: // suffix=1 is a branch conflict, suffix=2 is owned by an old PR, so the // loop lands on suffix=3. getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'improve-dashboard' ? 'remote' : null ) getPRForBranchMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'improve-dashboard-2' ? { number: 3127, title: 'Existing PR', state: 'merged', url: 'https://example.com/pr/3127', checksStatus: 'success', updatedAt: '2026-04-01T00:00:00Z', mergeable: 'UNKNOWN' } : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard-3', head: 'abc123', branch: 'improve-dashboard-3', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard-3', 'improve-dashboard-3', 'origin/main', false ) expect(result).toEqual({ worktree: expect.objectContaining({ path: '/workspace/improve-dashboard-3', branch: 'improve-dashboard-3' }) }) }) it('does not call `gh pr list` on the happy path (no branch conflict)', async () => { // Why: guards the speed optimization. If a future refactor accidentally // reintroduces the PR probe on the first iteration, the happy path will // silently regain a 1–3s GitHub round-trip per click; this test fails // loudly instead. listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) expect(getPRForBranchMock).not.toHaveBeenCalled() }) const createdWorktreeList = [ { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ] it('returns a setup launch payload when setup should run', async () => { listWorktreesMock.mockResolvedValue(createdWorktreeList) getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) shouldRunSetupForCreateMock.mockReturnValue(true) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', setupDecision: 'run' }) expect(createSetupRunnerScriptMock).toHaveBeenCalledWith( expect.objectContaining({ id: 'repo-1' }), '/workspace/improve-dashboard', 'pnpm worktree:setup' ) expect(result).toEqual({ worktree: expect.objectContaining({ repoId: 'repo-1', path: '/workspace/improve-dashboard', branch: 'improve-dashboard' }), setup: { runnerScriptPath: '/workspace/repo/.git/orca/setup-runner.sh', envVars: { ORCA_ROOT_PATH: '/workspace/repo', ORCA_WORKTREE_PATH: '/workspace/improve-dashboard' } } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', 'origin/main', false ) }) it('launches setup even when primary and worktree orca.yaml scripts diverge', async () => { // Why: regression for a silent skip introduced by the #1280 content-equality // gate. Benign divergence (whitespace, comments, or any setup edit that // landed on the base branch but not yet in the primary checkout) must not // disable setup — repo-level trust already gates execution. listWorktreesMock.mockResolvedValue(createdWorktreeList) getEffectiveHooksMock.mockImplementation((_repo, worktreePath?: string) => ({ scripts: { setup: worktreePath ? 'pnpm worktree:setup # worktree' : 'pnpm worktree:setup' } })) shouldRunSetupForCreateMock.mockReturnValue(true) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', setupDecision: 'run' }) expect(createSetupRunnerScriptMock).toHaveBeenCalledWith( expect.objectContaining({ id: 'repo-1' }), '/workspace/improve-dashboard', 'pnpm worktree:setup # worktree' ) expect(result).toEqual( expect.objectContaining({ setup: expect.objectContaining({ runnerScriptPath: '/workspace/repo/.git/orca/setup-runner.sh' }) }) ) }) it('creates a sparse worktree and persists its sparse metadata', async () => { listWorktreesMock.mockResolvedValue([ { ...createdWorktreeList[0], isSparse: true } ]) store.setWorktreeMeta.mockReturnValue({ sparseDirectories: ['packages/web', 'apps/api'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) store.getSparsePresets.mockReturnValue([ { id: 'preset-1', repoId: 'repo-1', name: 'Frontend and API', directories: ['packages/web', 'apps/api'], createdAt: 1, updatedAt: 1 } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', sparseCheckout: { directories: [' packages/web ', 'apps\\api\\', 'packages/web/'], presetId: 'preset-1' } }) expect(addWorktreeMock).not.toHaveBeenCalled() expect(addSparseWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', ['packages/web', 'apps/api'], 'origin/main', false ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ sparseDirectories: ['packages/web', 'apps/api'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) ) expect(result).toEqual({ worktree: expect.objectContaining({ repoId: 'repo-1', path: '/workspace/improve-dashboard', sparseDirectories: ['packages/web', 'apps/api'], sparseBaseRef: 'origin/main', sparsePresetId: 'preset-1' }) }) }) it('clears sparse preset attribution when the preset id does not belong to the repo', async () => { listWorktreesMock.mockResolvedValue([ { ...createdWorktreeList[0], isSparse: true } ]) store.getSparsePresets.mockReturnValue([ { id: 'preset-2', repoId: 'repo-1', name: 'Other preset', directories: ['packages/web'], createdAt: 1, updatedAt: 1 } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', sparseCheckout: { directories: ['packages/web'], presetId: 'preset-1' } }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ sparseDirectories: ['packages/web'], sparseBaseRef: 'origin/main', sparsePresetId: undefined }) ) }) it('clears sparse preset attribution when normalized directories do not match', async () => { listWorktreesMock.mockResolvedValue([ { ...createdWorktreeList[0], isSparse: true } ]) store.getSparsePresets.mockReturnValue([ { id: 'preset-1', repoId: 'repo-1', name: 'Frontend and API', directories: ['packages/web', 'apps/api'], createdAt: 1, updatedAt: 1 } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', sparseCheckout: { directories: ['packages/web'], presetId: 'preset-1' } }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ sparseDirectories: ['packages/web'], sparseBaseRef: 'origin/main', sparsePresetId: undefined }) ) }) it('rejects sparse checkout directories that traverse above the repo root', async () => { await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', sparseCheckout: { directories: ['packages/web', '../secrets'] } }) ).rejects.toThrow('Sparse checkout directories must be repo-relative paths.') expect(addSparseWorktreeMock).not.toHaveBeenCalled() expect(addWorktreeMock).not.toHaveBeenCalled() }) it.each(['/Users/me/repo/packages/web', 'C:\\repo\\packages\\web', '\\\\server\\share\\repo'])( 'rejects absolute sparse checkout directory before normalization: %s', async (directory) => { await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', sparseCheckout: { directories: ['packages/web', directory] } }) ).rejects.toThrow('Sparse checkout directories must be repo-relative paths.') expect(addSparseWorktreeMock).not.toHaveBeenCalled() expect(addWorktreeMock).not.toHaveBeenCalled() } ) it('still returns the created worktree when setup runner generation fails', async () => { listWorktreesMock.mockResolvedValue(createdWorktreeList) getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) shouldRunSetupForCreateMock.mockReturnValue(true) createSetupRunnerScriptMock.mockImplementation(() => { throw new Error('disk full') }) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', setupDecision: 'run' }) expect(result).toEqual({ worktree: expect.objectContaining({ repoId: 'repo-1', path: '/workspace/improve-dashboard', branch: 'improve-dashboard' }) }) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('prunes git worktree tracking when removing an orphaned worktree', async () => { mockKnownFeatureWorktree() const orphanError = Object.assign(new Error('git worktree remove failed'), { stderr: "fatal: '/workspace/feature-wt' is not a working tree" }) removeWorktreeMock.mockRejectedValue(orphanError) getEffectiveHooksMock.mockReturnValue(null) gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) // Should have called git worktree prune to clean up stale tracking expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: '/workspace/repo' }) expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith('repo-1::/workspace/feature-wt') expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('refuses to delete the root workspace for folder-mode repos', async () => { store.getRepo.mockReturnValue({ id: 'repo-folder', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' }) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-folder::/workspace/folder' }) ).rejects.toThrow('Cannot delete the project root workspace') expect(store.removeWorktreeMeta).not.toHaveBeenCalled() expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled() }) it('kills PTYs before removing additional folder workspace metadata', async () => { const ptyProvider = {} as never const worktreeId = 'repo-folder::/workspace/folder::workspace:child-1' store.getRepo.mockReturnValue({ id: 'repo-folder', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' }) getLocalPtyProviderMock.mockReturnValue(ptyProvider) await handlers['worktrees:remove'](null, { worktreeId }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, { runtime: runtimeStub, localProvider: ptyProvider }) expect(killAllProcessesForWorktreeMock.mock.invocationCallOrder[0]).toBeLessThan( store.removeWorktreeMeta.mock.invocationCallOrder[0] ) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-folder' }) }) it('runs the archive hook on remove when skipArchive is not set', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) runHookMock.mockResolvedValue({ success: true, output: '' }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(runHookMock).toHaveBeenCalledWith( 'archive', '/workspace/feature-wt', expect.objectContaining({ id: 'repo-1' }) ) expect(removeWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-wt', false ) }) it('skips the archive hook on remove when skipArchive is true', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) runHookMock.mockResolvedValue({ success: true, output: '' }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', skipArchive: true }) expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-wt', false ) }) it('runs the archive hook before removing an SSH worktree', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const callOrder: string[] = [] const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/remote/feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockImplementation(async () => { callOrder.push('remove') }), execNonInteractive: vi.fn().mockImplementation(async () => { callOrder.push('archive') return { stdout: '', stderr: '', exitCode: 0, timedOut: false } }) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: echo archived\n', isBinary: false }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml') expect(provider.execNonInteractive).toHaveBeenCalledWith( '/bin/bash', ['-lc', 'echo archived'], '/remote/feature-wt', 120_000, undefined, expect.objectContaining({ ORCA_ROOT_PATH: '/remote/repo', ORCA_WORKTREE_PATH: '/remote/feature-wt' }) ) expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) expect(callOrder).toEqual(['archive', 'remove']) expect(runHookMock).not.toHaveBeenCalled() }) it('continues SSH worktree removal when the archive hook fails', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/remote/feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue(undefined), execNonInteractive: vi.fn().mockResolvedValue({ stdout: '', stderr: 'cleanup failed', exitCode: 7, timedOut: false }) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: exit 7\n', isBinary: false }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'exit 7' } }) try { await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) expect(consoleErrorSpy).toHaveBeenCalledWith( '[hooks] archive hook failed for /remote/feature-wt:', expect.stringContaining('archive hook exited 7') ) } finally { consoleErrorSpy.mockRestore() } }) it('continues SSH worktree removal when archive hook execution rejects', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/remote/feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue(undefined), execNonInteractive: vi.fn().mockRejectedValue(new Error('relay disconnected')) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: echo archived\n', isBinary: false }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) try { await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) expect(consoleErrorSpy).toHaveBeenCalledWith( '[hooks] archive hook failed for /remote/feature-wt:', 'relay disconnected' ) } finally { consoleErrorSpy.mockRestore() } }) it('uses cmd.exe for archive hooks on Windows-like SSH worktree paths', async () => { const repo = { id: 'repo-ssh', path: 'C:\\remote\\repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: 'C:\\remote\\repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: 'C:\\remote\\feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue(undefined), execNonInteractive: vi.fn().mockResolvedValue({ stdout: '', stderr: '', exitCode: 0, timedOut: false }) } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: echo archived\n', isBinary: false }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::C:\\remote\\feature-wt' }) expect(fsProvider.readFile).toHaveBeenCalledWith('C:\\remote\\repo\\orca.yaml') expect(provider.execNonInteractive).toHaveBeenCalledWith( 'cmd.exe', ['/d', '/s', '/c', 'echo archived'], 'C:\\remote\\feature-wt', 120_000, undefined, expect.objectContaining({ ORCA_ROOT_PATH: 'C:\\remote\\repo', ORCA_WORKTREE_PATH: 'C:\\remote\\feature-wt' }) ) }) it('skips the archive hook before removing an SSH worktree when skipArchive is true', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/remote/feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue(undefined), execNonInteractive: vi.fn() } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: echo archived\n', isBinary: false }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt', skipArchive: true }) expect(provider.execNonInteractive).not.toHaveBeenCalled() expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) }) it('preserves the branch on remove for worktrees created from an existing local branch', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ preserveBranchOnDelete: true })) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(removeWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-wt', false, { deleteBranch: false } ) }) it('removes an unused Orca-created fork remote after deleting its worktree', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) const pushTarget = { remoteName: 'pr-contributor-orca', branchName: 'feature/from-fork', remoteUrl: 'https://github.com/contributor/orca.git', remoteCreated: true } store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ pushTarget })) store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/workspace/feature-wt': makeWorktreeMeta({ pushTarget }) }) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'config') { throw new Error('no branch config') } if (args[0] === 'remote' && args[1] === 'get-url') { return { stdout: 'https://github.com/contributor/orca.git\n', stderr: '' } } return { stdout: '', stderr: '' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['remote', 'remove', 'pr-contributor-orca'], { cwd: '/workspace/repo' }) }) it('keeps an Orca-created fork remote while another worktree still uses it', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) const pushTarget = { remoteName: 'pr-contributor-orca', branchName: 'feature/from-fork', remoteUrl: 'https://github.com/contributor/orca.git', remoteCreated: true } store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ pushTarget })) store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/workspace/feature-wt': makeWorktreeMeta({ pushTarget }), 'repo-1::/workspace/other-wt': makeWorktreeMeta({ pushTarget: { ...pushTarget, branchName: 'other-branch' } }) }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( ['remote', 'remove', 'pr-contributor-orca'], expect.any(Object) ) }) it('ignores matching push targets from other repos when deciding fork remote cleanup', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue(undefined) const pushTarget = { remoteName: 'pr-contributor-orca', branchName: 'feature/from-fork', remoteUrl: 'https://github.com/contributor/orca.git', remoteCreated: true } store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ pushTarget })) store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/workspace/feature-wt': makeWorktreeMeta({ pushTarget }), 'repo-2::/workspace/other-wt': makeWorktreeMeta({ pushTarget: { ...pushTarget, branchName: 'other-branch' } }) }) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'config') { throw new Error('no branch config') } if (args[0] === 'remote' && args[1] === 'get-url') { return { stdout: 'https://github.com/contributor/orca.git\n', stderr: '' } } return { stdout: '', stderr: '' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['remote', 'remove', 'pr-contributor-orca'], { cwd: '/workspace/repo' }) }) it('rejects unregistered delete paths before teardown, hooks, or git removal', async () => { mockKnownFeatureWorktree('/workspace/real-feature') getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/not-a-worktree' }) ).rejects.toThrow('Refusing to delete unregistered worktree path') expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('treats forced deletion of an already-missing unregistered worktree as cleanup', async () => { mockKnownFeatureWorktree('/workspace/real-feature') await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/already-deleted-wt', force: true }) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith( 'repo-1::/workspace/already-deleted-wt' ) expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/already-deleted-wt') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith( 'repo-1::/workspace/already-deleted-wt' ) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('coalesces concurrent deletes for the same worktree id', async () => { mockKnownFeatureWorktree() deleteWorktreeHistoryDirMock.mockClear() let removalStarted!: () => void let finishRemoval!: () => void const started = new Promise((resolve) => { removalStarted = resolve }) removeWorktreeMock.mockImplementation( () => new Promise((resolve) => { removalStarted() finishRemoval = resolve }) ) const first = handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) as Promise const second = handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) as Promise await started await Promise.resolve() expect(removeWorktreeMock).toHaveBeenCalledTimes(1) finishRemoval() await expect(Promise.all([first, second])).resolves.toEqual([undefined, undefined]) expect(store.removeWorktreeMeta).toHaveBeenCalledTimes(1) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledTimes(1) expect(mainWindow.webContents.send).toHaveBeenCalledTimes(1) }) it('rejects concurrent deletes for the same worktree id with different options', async () => { mockKnownFeatureWorktree() let removalStarted!: () => void let finishRemoval!: () => void const started = new Promise((resolve) => { removalStarted = resolve }) removeWorktreeMock.mockImplementation( () => new Promise((resolve) => { removalStarted() finishRemoval = resolve }) ) const first = handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) as Promise await started await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) ).rejects.toThrow('Worktree deletion already in progress') expect(removeWorktreeMock).toHaveBeenCalledTimes(1) finishRemoval() await expect(first).resolves.toBeUndefined() }) it('still rejects forced unregistered delete paths that exist on disk', async () => { mockKnownFeatureWorktree('/workspace/real-feature') await expect( handlers['worktrees:remove'](null, { worktreeId: `repo-1::${process.cwd()}`, force: true }) ).rejects.toThrow('Refusing to delete unregistered worktree path') expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('rejects the main worktree before teardown, hooks, or git removal', async () => { mockKnownFeatureWorktree() await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/repo' }) ).rejects.toThrow('Refusing to delete protected worktree path') expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('rejects deleting a worktree that contains another registered worktree before teardown, hooks, or git removal', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/workspace/parent', head: 'parent', branch: 'parent', isBare: false, isMainWorktree: false }, { path: '/workspace/parent/child', head: 'child', branch: 'child', isBare: false, isMainWorktree: false } ]) getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/parent', force: true }) ).rejects.toThrow( 'Refusing to delete worktree because it contains another registered worktree: /workspace/parent/child' ) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('IPC-initiated delete kills PTYs BEFORE git-level removal (design §4.3)', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) const callOrder: string[] = [] assertWorktreeCleanForRemovalMock.mockImplementation(async () => { callOrder.push('preflight') }) killAllProcessesForWorktreeMock.mockImplementation(async () => { callOrder.push('kill') return { runtimeStopped: 1, providerStopped: 0, registryStopped: 0 } }) removeWorktreeMock.mockImplementation(async () => { callOrder.push('git') }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.objectContaining({ localProvider: expect.anything() }) ) expect(removeWorktreeMock).toHaveBeenCalled() expect(callOrder).toEqual(['preflight', 'kill', 'git']) }) it('fails dirty non-force deletes before PTY teardown', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) assertWorktreeCleanForRemovalMock.mockRejectedValue( Object.assign(new Error('Worktree has uncommitted or untracked changes.'), { stdout: '?? scratch.txt\n' }) ) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('Failed to delete worktree at /workspace/feature-wt. ?? scratch.txt') expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('formats preflight subprocess failures and does not tear down PTYs', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) assertWorktreeCleanForRemovalMock.mockRejectedValue( Object.assign(new Error('status failed'), { stderr: 'fatal: unable to read current working directory\n' }) ) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow( 'Failed to delete worktree at /workspace/feature-wt. fatal: unable to read current working directory' ) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('falls through to orphan cleanup when preflight reports missing/non-repo worktree', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) assertWorktreeCleanForRemovalMock.mockRejectedValue( Object.assign(new Error('status failed'), { stderr: 'fatal: not a git repository (or any of the parent directories): .git\n' }) ) removeWorktreeMock.mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: "fatal: '/workspace/feature-wt' is not a working tree" }) ) gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).toHaveBeenCalled() expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: '/workspace/repo' }) }) it('skips the PTY teardown for SSH-backed repos (design §6 out-of-scope)', async () => { // Why: SSH-backed PTYs live on the remote host and are handled by the // remote provider's own teardown. The local-host helper must not run for // SSH repos, because it would sweep registry entries for other worktrees. const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) // The test can't easily mock the SSH provider without more plumbing — the // call will throw about 'no git provider for connection'. What matters // here is that the kill helper was NOT called for the SSH branch. await ( handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) as Promise ).catch(() => {}) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() }) it('rejects ask-policy creates before mutating git state when setup decision is missing', async () => { getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) shouldRunSetupForCreateMock.mockImplementation(() => { throw new Error('Setup decision required for this repository') }) await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) ).rejects.toThrow('Setup decision required for this repository') expect(addWorktreeMock).not.toHaveBeenCalled() expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(createSetupRunnerScriptMock).not.toHaveBeenCalled() }) })