/* eslint-disable max-lines */ import { beforeEach, describe, expect, it, vi } from 'vitest' import type * as GitUsernameModule from '../git/git-username' import { lstat, mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import { join, resolve } from 'node:path' import type { CreateWorktreeResult, GitWorktreeInfo, Repo, Worktree } from '../../shared/types' import type { ProviderRequestId } from '../../shared/detected-worktree-provider-contract' import { LOCAL_EXECUTION_HOST_ID, toSshExecutionHostId } from '../../shared/execution-host' import * as localWorktreeFilesystem from '../local-worktree-filesystem' const ORIGINAL_PLATFORM = process.platform const removeWorktreeLinkedPathsMock = vi.hoisted(() => vi.fn()) const findExistingWorktreeSymlinkPathsMock = vi.hoisted(() => vi.fn()) function setPlatform(platform: NodeJS.Platform): void { Object.defineProperty(process, 'platform', { configurable: true, value: platform }) } const { handleMock, removeHandlerMock, listWorktreesMock, parseWorktreeListMock, assertWorktreeCleanForRemovalMock, addWorktreeMock, addSparseWorktreeMock, removeWorktreeMock, forceDeleteLocalBranchMock, resolveLocalGitUsernameMock, getBaseRefDefaultMock, resolveDefaultBaseRefWithLocalGitMock, resolveDefaultBaseRefViaExecMock, getDefaultRemoteMock, getBranchConflictKindMock, getPRForBranchMock, getHostedReviewForBranchMock, getWorkItemMock, getPullRequestPushTargetMock, getEffectiveHooksMock, createIssueCommandRunnerScriptMock, createSetupRunnerScriptMock, getEffectiveHooksFromConfigMock, getDefaultTabsLaunchMock, parseOrcaYamlMock, shouldRunSetupForCreateMock, buildPosixRunnerScriptMock, buildWindowsRunnerScriptMock, getSetupRunnerEnvVarsMock, resolveSetupRunnerShellMock, runHookMock, hasHooksFileMock, loadHooksMock, computeWorktreePathMock, ensurePathWithinWorkspaceMock, gitExecFileAsyncMock, getSshGitProviderMock, getSshFilesystemProviderMock, getActiveMultiplexerMock } = vi.hoisted(() => ({ handleMock: vi.fn(), removeHandlerMock: vi.fn(), listWorktreesMock: vi.fn(), parseWorktreeListMock: vi.fn((output: string) => output .trim() .split(/\n\s*\n/) .filter(Boolean) .map((block, index) => { const lines = block.split(/\r?\n/) const path = lines.find((line) => line.startsWith('worktree '))?.slice(9) ?? '' const branch = lines.find((line) => line.startsWith('branch '))?.slice(7) ?? '' return { path, branch, head: String(index), isBare: false, isMainWorktree: index === 0 } }) ), assertWorktreeCleanForRemovalMock: vi.fn(), addWorktreeMock: vi.fn(), addSparseWorktreeMock: vi.fn(), removeWorktreeMock: vi.fn(), forceDeleteLocalBranchMock: vi.fn(), resolveLocalGitUsernameMock: vi.fn(), getBaseRefDefaultMock: vi.fn(), resolveDefaultBaseRefWithLocalGitMock: vi.fn(), resolveDefaultBaseRefViaExecMock: vi.fn(), getDefaultRemoteMock: vi.fn(), getBranchConflictKindMock: vi.fn(), getPRForBranchMock: vi.fn(), getHostedReviewForBranchMock: vi.fn(), getWorkItemMock: vi.fn(), getPullRequestPushTargetMock: vi.fn(), getEffectiveHooksMock: vi.fn(), createIssueCommandRunnerScriptMock: vi.fn(), createSetupRunnerScriptMock: vi.fn(), getEffectiveHooksFromConfigMock: vi.fn(), getDefaultTabsLaunchMock: vi.fn(), parseOrcaYamlMock: vi.fn(), shouldRunSetupForCreateMock: vi.fn(), buildPosixRunnerScriptMock: vi.fn(), buildWindowsRunnerScriptMock: vi.fn(), getSetupRunnerEnvVarsMock: vi.fn(), resolveSetupRunnerShellMock: 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, listWorktreesStrict: listWorktreesMock, parseWorktreeList: parseWorktreeListMock, assertWorktreeCleanForRemoval: assertWorktreeCleanForRemovalMock, addWorktree: addWorktreeMock, addSparseWorktree: addSparseWorktreeMock, removeWorktree: removeWorktreeMock, forceDeleteLocalBranch: forceDeleteLocalBranchMock })) vi.mock('../git/runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock, gitExecFileSync: vi.fn() })) vi.mock('../git/repo', () => ({ getBaseRefDefault: getBaseRefDefaultMock, resolveDefaultBaseRefWithLocalGit: resolveDefaultBaseRefWithLocalGitMock, resolveDefaultBaseRefViaExec: resolveDefaultBaseRefViaExecMock, getDefaultRemote: getDefaultRemoteMock, getBranchConflictKind: getBranchConflictKindMock })) vi.mock('../git/git-username', async () => { const actual = await vi.importActual('../git/git-username') return { ...actual, resolveLocalGitUsername: resolveLocalGitUsernameMock } }) vi.mock('../github/client', () => ({ getPRForBranch: getPRForBranchMock, getWorkItem: getWorkItemMock, getPullRequestPushTarget: getPullRequestPushTargetMock })) vi.mock('../source-control/hosted-review', () => ({ getHostedReviewForBranch: getHostedReviewForBranchMock })) vi.mock('../providers/ssh-git-dispatch', () => ({ getSshGitProviderGeneration: () => 0, 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('./worktree-symlinks', () => ({ createWorktreeCopiedPaths: vi.fn(), createWorktreeLinkedPaths: vi.fn(), findExistingWorktreeSymlinkPaths: findExistingWorktreeSymlinkPathsMock, removeWorktreeLinkedPaths: removeWorktreeLinkedPathsMock })) vi.mock('./ssh', () => ({ getActiveMultiplexer: getActiveMultiplexerMock })) vi.mock('../hooks', () => ({ buildPosixRunnerScript: buildPosixRunnerScriptMock, buildWindowsRunnerScript: buildWindowsRunnerScriptMock, createIssueCommandRunnerScript: createIssueCommandRunnerScriptMock, createSetupRunnerScript: createSetupRunnerScriptMock, getEffectiveHooks: getEffectiveHooksMock, getEffectiveHooksFromConfig: getEffectiveHooksFromConfigMock, getDefaultTabsLaunch: getDefaultTabsLaunchMock, getSetupRunnerEnvVars: getSetupRunnerEnvVarsMock, loadHooks: loadHooksMock, parseOrcaYaml: parseOrcaYamlMock, resolveSetupRunnerShell: resolveSetupRunnerShellMock, 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-deletion', () => ({ deleteWorktreeHistoryDir: deleteWorktreeHistoryDirMock })) const { advertisedUrlWatcherForgetWorktreeMock } = vi.hoisted(() => ({ advertisedUrlWatcherForgetWorktreeMock: vi.fn() })) vi.mock('../ports/advertised-url-watcher', () => ({ advertisedUrlWatcher: { forgetWorktree: advertisedUrlWatcherForgetWorktreeMock } })) const { pruneCleanupScanSnapshotMock, pruneSpaceAnalysisSnapshotMock } = vi.hoisted(() => ({ pruneCleanupScanSnapshotMock: vi.fn().mockResolvedValue(undefined), pruneSpaceAnalysisSnapshotMock: vi.fn().mockResolvedValue(undefined) })) vi.mock('../workspace-cleanup-scan-snapshot', () => ({ pruneWorkspaceCleanupScanSnapshot: pruneCleanupScanSnapshotMock })) vi.mock('../workspace-space-analysis-snapshot', () => ({ pruneWorkspaceSpaceAnalysisSnapshot: pruneSpaceAnalysisSnapshotMock })) const { killAllProcessesForWorktreeMock, clearProviderPtyStateMock, getLocalPtyProviderMock, getSshPtyProviderMock } = vi.hoisted(() => ({ killAllProcessesForWorktreeMock: vi.fn(), clearProviderPtyStateMock: vi.fn(), getLocalPtyProviderMock: vi.fn(), getSshPtyProviderMock: vi.fn() })) vi.mock('../runtime/worktree-teardown', () => ({ killAllProcessesForWorktree: killAllProcessesForWorktreeMock })) vi.mock('./pty', () => ({ clearProviderPtyState: clearProviderPtyStateMock, getLocalPtyProvider: getLocalPtyProviderMock, getSshPtyProvider: getSshPtyProviderMock })) import { __resetSshWorktreeCreateFetchCacheForTests, notifyWorktreesChanged } from './worktree-remote' import { invalidateAuthorizedRootsCache, registerWorktreeRootsForRepo, resolveRegisteredWorktreePath } from './filesystem-auth' import { _resetTracerForTests, setActiveSink } from '../observability/tracer' import type { RedactableSpan } from '../observability/redactor' import { reviewHeadRemoteRefComponent, REVIEW_HEAD_FETCH_TIMEOUT_MS } from '../../shared/review-head-tracking-ref' // Why: durable review-head refs are scoped by remote identity (name + URL hash). const ORIGIN_REMOTE_URL = 'git@github.com:org/repo.git' const ORIGIN_HEAD_COMPONENT = reviewHeadRemoteRefComponent('origin', ORIGIN_REMOTE_URL) import { DETECTED_WORKTREE_PROVIDER_TIMEOUT_MS, LINEAGE_HYDRATION_TIMEOUT_MS, __getDetectedWorktreeScanCacheStatsForTests, __resetDetectedWorktreeScanCacheForTests, registerWorktreeHandlers } from './worktrees' import { clearConfiguredWorktreeSharedDirectoriesCacheForTests } from '../git/worktree-shared-directories' import { getSshProviderAuthority, resetSshProviderAuthorities, rotateSshProviderAuthority } from '../ssh/ssh-provider-authority' type HandlerMap = Record unknown> describe('registerWorktreeHandlers', () => { const handlers: HandlerMap = {} const mainWindow = { isDestroyed: () => false, webContents: { send: vi.fn() } } const ipcEvent = { sender: { id: 1 } } const store = { getProfileStorageDirectory: vi.fn(() => '/profile-a'), getRepos: vi.fn(), getRepo: vi.fn(), getProjects: vi.fn(), getSparsePresets: vi.fn(), getSettings: vi.fn(), getWorktreeMeta: vi.fn(), getAllWorktreeMeta: vi.fn(), setWorktreeMeta: vi.fn(), getProjectHostSetups: vi.fn(), removeWorktreeMeta: vi.fn(), removeWorkspaceSessionStateForWorktree: vi.fn(), getAllWorktreeLineage: vi.fn(), removeWorktreeLineage: vi.fn(), getAllWorkspaceLineage: vi.fn(), getFolderWorkspaces: vi.fn(), getProjectGroups: vi.fn() } let runtimeStub: { resolveRemoteTrackingBase: ReturnType hasRemoteTrackingRef: ReturnType getOrStartRemoteTrackingBaseRefresh: ReturnType getOrStartRemoteFetch: ReturnType fetchRemoteWithCache: ReturnType emitWorktreeBaseStatus: ReturnType recordOptimisticReconcileToken: ReturnType reconcileWorktreeBaseStatus: ReturnType clearOptimisticReconcileToken: ReturnType resolveManagedMrBase: ReturnType createTerminal: ReturnType splitTerminal: ReturnType notifyWorktreesChangedForRemoteClients: ReturnType closeFileWatchersForRemoval: ReturnType acquireFileWatcherRemoval: ReturnType hydrateInferredWorktreeLineage: ReturnType } beforeEach(() => { setPlatform(ORIGINAL_PLATFORM) clearConfiguredWorktreeSharedDirectoriesCacheForTests() __resetSshWorktreeCreateFetchCacheForTests() __resetDetectedWorktreeScanCacheForTests() resetSshProviderAuthorities() invalidateAuthorizedRootsCache() for (const m of [ handleMock, removeHandlerMock, listWorktreesMock, assertWorktreeCleanForRemovalMock, addWorktreeMock, addSparseWorktreeMock, removeWorktreeMock, forceDeleteLocalBranchMock, resolveLocalGitUsernameMock, getBaseRefDefaultMock, resolveDefaultBaseRefWithLocalGitMock, resolveDefaultBaseRefViaExecMock, getDefaultRemoteMock, getBranchConflictKindMock, getPRForBranchMock, getHostedReviewForBranchMock, getWorkItemMock, getPullRequestPushTargetMock, getEffectiveHooksMock, getEffectiveHooksFromConfigMock, getDefaultTabsLaunchMock, parseOrcaYamlMock, createIssueCommandRunnerScriptMock, createSetupRunnerScriptMock, buildPosixRunnerScriptMock, buildWindowsRunnerScriptMock, getSetupRunnerEnvVarsMock, resolveSetupRunnerShellMock, shouldRunSetupForCreateMock, runHookMock, hasHooksFileMock, loadHooksMock, computeWorktreePathMock, ensurePathWithinWorkspaceMock, gitExecFileAsyncMock, getSshGitProviderMock, getSshFilesystemProviderMock, getActiveMultiplexerMock, mainWindow.webContents.send, store.getRepos, store.getRepo, store.getProjects, store.getSparsePresets, store.getSettings, store.getWorktreeMeta, store.getAllWorktreeMeta, store.setWorktreeMeta, store.getProjectHostSetups, store.removeWorktreeMeta, store.removeWorkspaceSessionStateForWorktree, store.getAllWorktreeLineage, store.removeWorktreeLineage, store.getAllWorkspaceLineage, store.getFolderWorkspaces, store.getProjectGroups, killAllProcessesForWorktreeMock, clearProviderPtyStateMock, getLocalPtyProviderMock, getSshPtyProviderMock, deleteWorktreeHistoryDirMock, advertisedUrlWatcherForgetWorktreeMock, findExistingWorktreeSymlinkPathsMock, removeWorktreeLinkedPathsMock ]) { m.mockReset() } killAllProcessesForWorktreeMock.mockResolvedValue({ runtimeStopped: 0, providerStopped: 0, registryStopped: 0 }) assertWorktreeCleanForRemovalMock.mockResolvedValue(undefined) findExistingWorktreeSymlinkPathsMock.mockResolvedValue([]) getLocalPtyProviderMock.mockReturnValue({} as never) getSshPtyProviderMock.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.getProjects.mockReturnValue([]) 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.getProjectHostSetups.mockReturnValue([ { id: 'repo-1', projectId: 'repo:repo-1', hostId: 'local', repoId: 'repo-1', path: '/workspace/repo', displayName: 'repo', setupState: 'ready', setupMethod: 'legacy-repo', createdAt: 0, updatedAt: 0 } ]) store.getAllWorktreeLineage.mockReturnValue({}) store.getAllWorkspaceLineage.mockReturnValue({}) store.getFolderWorkspaces.mockReturnValue([]) store.getProjectGroups.mockReturnValue([]) resolveLocalGitUsernameMock.mockResolvedValue('') getBaseRefDefaultMock.mockResolvedValue('origin/main') resolveDefaultBaseRefWithLocalGitMock.mockResolvedValue('origin/main') resolveDefaultBaseRefViaExecMock.mockResolvedValue('origin/main') getDefaultRemoteMock.mockResolvedValue('origin') getBranchConflictKindMock.mockResolvedValue(null) getPRForBranchMock.mockResolvedValue(null) getHostedReviewForBranchMock.mockResolvedValue(null) getWorkItemMock.mockResolvedValue(null) getPullRequestPushTargetMock.mockResolvedValue(null) // Why: createLocalWorktree can still hit the legacy git fetch fallback here; resolve so catch/then chains don't trip on undefined. gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' }) getEffectiveHooksMock.mockReturnValue(null) getEffectiveHooksFromConfigMock.mockImplementation(() => getEffectiveHooksMock()) getDefaultTabsLaunchMock.mockReturnValue(undefined) 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) resolveSetupRunnerShellMock.mockReturnValue(undefined) 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([]) forceDeleteLocalBranchMock.mockResolvedValue(undefined) // Why: minimal stub keeps these tests on create-flow semantics; full fetchRemoteWithCache 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' }), createTerminal: vi.fn().mockResolvedValue({ handle: 'term-startup', worktreeId: 'repo-1::/workspace/improve-dashboard', title: null, surface: 'visible' }), splitTerminal: vi.fn().mockResolvedValue({ handle: 'term-setup', tabId: 'tab-startup', paneRuntimeId: -1 }), notifyWorktreesChangedForRemoteClients: vi.fn(), closeFileWatchersForRemoval: vi.fn().mockResolvedValue(undefined), acquireFileWatcherRemoval: vi.fn(), hydrateInferredWorktreeLineage: vi.fn().mockResolvedValue(undefined) } runtimeStub.acquireFileWatcherRemoval.mockImplementation( async (worktreePath: string, connectionId?: string) => { await ( runtimeStub.closeFileWatchersForRemoval as ( worktreePath: string, connectionId?: string ) => Promise )(worktreePath, connectionId) return { finish: vi.fn().mockResolvedValue(undefined) } } ) 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() }) it('clears the branch rename failure-output handler before re-registering IPC handlers', () => { expect(removeHandlerMock).toHaveBeenCalledWith('worktrees:getBranchRenameFailureOutput') expect(handlers['worktrees:getBranchRenameFailureOutput']).toBeDefined() }) it('persistSortOrder only reorders existing worktrees and never mints meta for a stale id', () => { const liveId = 'repo-1::/workspace/repo' const staleId = 'removed-repo::/workspace/gone' // Only the live worktree has meta; the stale id (e.g. a removed repo the // renderer still lists) has none and must be skipped, not created. store.getWorktreeMeta.mockImplementation((id: string) => id === liveId ? ({ instanceId: 'x' } as never) : undefined ) handlers['worktrees:persistSortOrder'](null, { orderedIds: [liveId, staleId] }) const orderedTargets = store.setWorktreeMeta.mock.calls.map((call) => call[0]) expect(orderedTargets).toContain(liveId) expect(orderedTargets).not.toContain(staleId) }) it('persistSortOrder skips ranks that already represent the requested order', () => { const firstId = 'repo-1::/workspace/first' const secondId = 'repo-1::/workspace/second' store.getWorktreeMeta.mockImplementation((id: string) => ({ sortOrder: id === firstId ? 200 : 100 })) handlers['worktrees:persistSortOrder'](null, { orderedIds: [firstId, secondId] }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() }) it('prefetches the local default create base through the runtime refresh cache', async () => { const repo = { id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: 'origin/master' } const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } store.getRepo.mockReturnValue(repo) runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => baseBranch === 'origin/main' ? remoteBase : null ) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1' }) expect(getBaseRefDefaultMock).toHaveBeenCalledWith('/workspace/repo') expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( '/workspace/repo', 'origin/master' ) expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( '/workspace/repo', 'origin/main' ) expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( '/workspace/repo', remoteBase ) expect(addWorktreeMock).not.toHaveBeenCalled() }) it('uses the runtime remote fetch cache when prefetching a local branch base', async () => { runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1', baseBranch: 'main' }) expect(runtimeStub.fetchRemoteWithCache).toHaveBeenCalledWith('/workspace/repo', 'origin') expect(addWorktreeMock).not.toHaveBeenCalled() }) it('prefetches origin for local branch bases containing slashes', async () => { runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1', baseBranch: 'Jinwoo-H/vm-improve-2' }) expect(runtimeStub.fetchRemoteWithCache).toHaveBeenCalledWith('/workspace/repo', 'origin') expect(runtimeStub.fetchRemoteWithCache).not.toHaveBeenCalledWith('/workspace/repo', 'Jinwoo-H') expect(addWorktreeMock).not.toHaveBeenCalled() }) it('does not prefetch the whole remote for an existing commit SHA base', async () => { const sha = 'a'.repeat(40) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-1', baseBranch: sha }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], { cwd: '/workspace/repo' } ) expect(runtimeStub.resolveRemoteTrackingBase).not.toHaveBeenCalled() expect(runtimeStub.fetchRemoteWithCache).not.toHaveBeenCalled() expect(addWorktreeMock).not.toHaveBeenCalled() }) it('skips the broad remote fetch when creating from an existing commit SHA base', async () => { const sha = 'a'.repeat(40) listWorktreesMock.mockResolvedValue([ { path: '/workspace/pr-title', head: sha, branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'pr-title', baseBranch: sha, branchNameOverride: 'feature/fix' }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['rev-parse', '--verify', '--quiet', `${sha}^{commit}`], { cwd: '/workspace/repo' } ) expect(runtimeStub.fetchRemoteWithCache).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/pr-title', 'feature/fix', sha, false ) }) it('keeps the broad remote fetch fallback when a commit SHA base is missing locally', async () => { const sha = 'b'.repeat(40) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes(`${sha}^{commit}`)) { throw new Error('missing object') } return { stdout: '', stderr: '' } }) listWorktreesMock.mockResolvedValue([ { path: '/workspace/pr-title', head: sha, branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'pr-title', baseBranch: sha, branchNameOverride: 'feature/fix' }) expect(runtimeStub.fetchRemoteWithCache).toHaveBeenCalledWith('/workspace/repo', 'origin') expect(addWorktreeMock).toHaveBeenCalled() }) it('fetches origin when creating from a local branch base containing slashes', async () => { runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null) listWorktreesMock.mockResolvedValue([ { path: '/workspace/slash-base', head: 'created-sha', branch: 'refs/heads/slash-base', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'slash-base', baseBranch: 'Jinwoo-H/vm-improve-2', branchNameOverride: 'slash-base' }) expect(runtimeStub.fetchRemoteWithCache).toHaveBeenCalledWith('/workspace/repo', 'origin') expect(runtimeStub.fetchRemoteWithCache).not.toHaveBeenCalledWith('/workspace/repo', 'Jinwoo-H') expect(addWorktreeMock).toHaveBeenCalled() }) function mockKnownFeatureWorktree( path = '/workspace/feature-wt', repoPath = '/workspace/repo' ): GitWorktreeInfo[] { const worktrees: GitWorktreeInfo[] = [ { path: repoPath, head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path, head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false } ] listWorktreesMock.mockResolvedValue(worktrees) return worktrees } 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 } } function mockSelectedWslProjectRuntime(): void { setPlatform('win32') store.getProjects.mockReturnValue([ { id: 'project-1', displayName: 'repo', badgeColor: '#000', sourceRepoIds: ['repo-1'], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ]) } it('strips Orca provenance fields from renderer metadata updates', () => { store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) const result = handlers['worktrees:updateMeta'](null, { worktreeId: 'repo-1::/workspace/feature-wt', updates: { comment: 'keep me', isPinned: true, orcaCreatedAt: 123, orcaCreationSource: 'desktop', orcaCreationWorkspaceLayout: { path: '/workspace', nestWorkspaces: false } } }) expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt', { comment: 'keep me', isPinned: true }) expect(result).toMatchObject({ comment: 'keep me', isPinned: true }) }) it('pushes a remote-client invalidation for renames but not read-state updates', () => { store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) handlers['worktrees:updateMeta'](null, { worktreeId: 'repo-1::/workspace/feature-wt', updates: { isUnread: false } }) // Why: per-click isUnread writes must stay event-free (PR #209), while a rename must reach paired remote clients that no longer poll for titles. expect(runtimeStub.notifyWorktreesChangedForRemoteClients).not.toHaveBeenCalled() handlers['worktrees:updateMeta'](null, { worktreeId: 'repo-1::/workspace/feature-wt', updates: { displayName: 'Renamed workspace' } }) expect(runtimeStub.notifyWorktreesChangedForRemoteClients).toHaveBeenCalledWith('repo-1') }) it('does not trust renderer-authored automation provenance during local create', async () => { store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) 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', automationProvenance: { kind: 'created-by-automation', automationId: 'automation-1', automationNameSnapshot: 'Forged', automationRunId: 'run-1', automationRunTitleSnapshot: 'Forged run', createdAt: 123, executionTargetType: 'local', executionTargetId: 'local', projectId: 'repo-1' } }) const persistedMeta = store.setWorktreeMeta.mock.calls.find( ([worktreeId]) => worktreeId === 'repo-1::/workspace/improve-dashboard' )?.[1] expect(persistedMeta).toBeDefined() expect(persistedMeta).not.toHaveProperty('automationProvenance') }) 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 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' })) as CreateWorktreeResult expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard-2', 'improve-dashboard-2', 'origin/main', false ) expect(result).toMatchObject({ worktree: expect.objectContaining({ path: '/workspace/improve-dashboard-2', branch: 'improve-dashboard-2' }) }) }) it('keeps an emoji-only display name while using safe branch and path names', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/rocket', head: 'abc123', branch: 'rocket', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: '๐Ÿš€' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/rocket', 'rocket', 'origin/main', false ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/rocket', expect.objectContaining({ displayName: '๐Ÿš€' }) ) }) it('uses a repo-specific worktree base path when creating local worktrees', async () => { store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null, worktreeBasePath: '../worktrees' }) listWorktreesMock.mockResolvedValue([ { path: '../worktrees/feature', head: 'abc123', branch: 'feature', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'feature' }) expect(computeWorktreePathMock).toHaveBeenCalledWith('feature', '/workspace/repo', { nestWorkspaces: false, workspaceDir: '../worktrees' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '../worktrees/feature', 'feature', 'origin/main', false ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::../worktrees/feature', expect.objectContaining({ orcaCreationWorkspaceLayout: { path: '../worktrees', nestWorkspaces: false } }) ) }) it('registers local worktree roots immediately after create', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'base', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) const listWorktreesCallsAfterCreate = listWorktreesMock.mock.calls.length await expect( resolveRegisteredWorktreePath('/workspace/improve-dashboard', store as never) ).resolves.toBe(resolve('/workspace/improve-dashboard')) expect(listWorktreesMock).toHaveBeenCalledTimes(listWorktreesCallsAfterCreate) }) it('uses branchNameOverride for the git branch while keeping the sanitized worktree path', async () => { store.getSettings.mockReturnValue({ branchPrefix: 'git-username', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: false, workspaceDir: '/workspace' }) resolveLocalGitUsernameMock.mockResolvedValue('unused-user') 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(resolveLocalGitUsernameMock).not.toHaveBeenCalled() expect(result).toMatchObject({ 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('spawns a startup terminal and setup terminal after local worktree registration', async () => { addWorktreeMock.mockResolvedValue({}) listWorktreesMock.mockResolvedValueOnce([ { path: '/workspace/improve-dashboard', head: 'def', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) loadHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) shouldRunSetupForCreateMock.mockReturnValue(true) const result = (await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', createdWithAgent: 'claude', startup: { command: 'claude --prefill test', env: { ORCA_AGENT_MODE: 'direct' }, viewMode: 'chat', telemetry: { agent_kind: 'claude', launch_source: 'new_workspace_composer', request_kind: 'new' } } })) as { setup?: unknown startupTerminal?: { spawned: boolean; surface?: string } timing?: { phases: { phase: string }[] } } expect(runtimeStub.createTerminal).toHaveBeenNthCalledWith( 1, 'id:repo-1::/workspace/improve-dashboard', { claudeAgentTeamsSourceCommand: 'claude --prefill test', command: 'claude --prefill test', env: { ORCA_AGENT_MODE: 'direct' }, launchAgent: 'claude', viewMode: 'chat', startupCommandDelivery: undefined, telemetry: { agent_kind: 'claude', launch_source: 'new_workspace_composer', request_kind: 'new' }, activate: true } ) expect(runtimeStub.createTerminal).toHaveBeenNthCalledWith( 2, 'id:repo-1::/workspace/improve-dashboard', { title: 'Setup', command: expect.stringContaining('bash /workspace/repo/.git/orca/setup-runner.sh'), env: { ORCA_ROOT_PATH: '/workspace/repo', ORCA_WORKTREE_PATH: '/workspace/improve-dashboard' }, activate: false } ) const startupCreateCall = runtimeStub.createTerminal.mock.calls[0] const setupCreateCall = runtimeStub.createTerminal.mock.calls[1] if (!startupCreateCall || !setupCreateCall) { throw new Error('expected startup and setup terminal calls') } const startupCommand = (startupCreateCall[1] as { command: string }).command const setupCommand = (setupCreateCall[1] as { command: string }).command expect(startupCommand).toBe('claude --prefill test') expect(setupCommand).toBe('bash /workspace/repo/.git/orca/setup-runner.sh') expect(result.setup).toBeUndefined() expect(result.startupTerminal).toEqual({ spawned: true, surface: 'visible' }) expect(result.timing?.phases.map((phase) => phase.phase)).toEqual( expect.arrayContaining([ 'git_worktree_add', 'list_created_worktree', 'resolve_worktreeinclude', 'prepare_setup', 'spawn_startup_terminal' ]) ) }) it('returns the wrapped setup command when startup spawned but setup creation failed', async () => { addWorktreeMock.mockResolvedValue({}) listWorktreesMock.mockResolvedValueOnce([ { path: '/workspace/improve-dashboard', head: 'def', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) loadHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) shouldRunSetupForCreateMock.mockReturnValue(true) createSetupRunnerScriptMock.mockReturnValueOnce({ runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh', shell: { family: 'posix', executable: 'wsl.exe' }, envVars: { ORCA_ROOT_PATH: 'C:\\workspace\\repo', ORCA_WORKTREE_PATH: 'C:\\workspace\\improve-dashboard' }, waitForAgentStartup: true }) runtimeStub.createTerminal .mockResolvedValueOnce({ handle: 'term-startup', surface: 'visible' }) .mockRejectedValueOnce(new Error('setup creation failed')) const result = (await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', createdWithAgent: 'claude', startup: { command: 'claude --prefill test', env: { ORCA_AGENT_MODE: 'direct' }, telemetry: { agent_kind: 'claude', launch_source: 'new_workspace_composer', request_kind: 'new' } } })) as { setup?: { command?: string; runnerScriptPath: string } } expect(result.setup).toEqual( expect.objectContaining({ runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh', command: expect.stringContaining('bash /mnt/c/workspace/repo/.git/orca/setup-runner.sh') }) ) expect(result.setup?.command).toContain('printf') }) 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).toMatchObject({ worktree: expect.objectContaining({ path: '/workspace/fix-bug-0', branch: 'refs/heads/fix/bug-0' }) }) }) it('reuses an existing local branch when the worktree folder is renamed (#5181)', async () => { // Why: reuse keeps branchNameOverride on the selected branch though the folder is renamed; backend must check out that branch (no -b). 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/my-folder', head: 'abc123', branch: 'refs/heads/fix/bug-0', isBare: false, isMainWorktree: false } ]) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'my-folder', baseBranch: 'fix/bug-0', branchNameOverride: 'fix/bug-0' }) expect(getBranchConflictKindMock).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/my-folder', 'fix/bug-0', 'fix/bug-0', false, false, { checkoutExistingBranch: true } ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/my-folder', expect.objectContaining({ preserveBranchOnDelete: true }) ) expect(result).toMatchObject({ worktree: expect.objectContaining({ path: '/workspace/my-folder', 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).toMatchObject({ worktree: expect.objectContaining({ path: '/workspace/fix-bug-0-2', branch: 'refs/heads/fix/bug-0' }) }) }) it('suffixes branchNameOverride when the requested 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: 'refs/heads/feature/something-2', isBare: false, isMainWorktree: false } ]) 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 ) }) it('allows a resolver-provided PR branch override to match its remote push target', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/fix' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) getPRForBranchMock.mockResolvedValueOnce({ number: 42, title: 'Selected PR', state: 'open', url: 'https://example.com/pr/42', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', compareBaseRef: 'refs/remotes/origin/main', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title', 'feature/fix', 'abc123', false ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['branch', '--set-upstream-to', 'origin/feature/fix', 'feature/fix'], { cwd: '/workspace/fix-title' } ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/fix-title', expect.objectContaining({ baseRef: 'refs/remotes/origin/main', linkedPR: 42 }) ) expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix') }) it('persists an explicit compare base ahead of the checkout remote-tracking base', async () => { runtimeStub.resolveRemoteTrackingBase.mockResolvedValueOnce({ base: 'origin/source-branch', remote: 'origin', branch: 'source-branch', ref: 'refs/remotes/origin/source-branch' }) runtimeStub.hasRemoteTrackingRef.mockResolvedValueOnce(true) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'origin/source-branch', compareBaseRef: 'refs/remotes/origin/main', branchNameOverride: 'feature/fix', linkedGitLabMR: 7, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/fix-title', expect.objectContaining({ baseRef: 'refs/remotes/origin/main', linkedGitLabMR: 7 }) ) }) it('allows a selected Bitbucket PR branch override to match its remote push target', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/bitbucket' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/bitbucket-title', head: 'abc123', branch: 'refs/heads/feature/bitbucket', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) getHostedReviewForBranchMock.mockResolvedValueOnce({ provider: 'bitbucket', number: 11, title: 'Bitbucket PR', state: 'open', url: 'https://bitbucket.org/team/repo/pull-requests/11', status: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'bitbucket-title', baseBranch: 'abc123', branchNameOverride: 'feature/bitbucket', linkedBitbucketPR: 11, pushTarget: { remoteName: 'origin', branchName: 'feature/bitbucket' } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/bitbucket-title', 'feature/bitbucket', 'abc123', false ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/bitbucket-title', expect.objectContaining({ linkedBitbucketPR: 11 }) ) expect(getHostedReviewForBranchMock).toHaveBeenCalledWith( expect.objectContaining({ repoPath: '/workspace/repo', branch: 'feature/bitbucket', linkedBitbucketPR: 11 }) ) expect(getPRForBranchMock).not.toHaveBeenCalled() }) it('suffixes a selected Bitbucket PR branch when the existing PR is different', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/bitbucket' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/bitbucket-title-2', head: 'abc123', branch: 'refs/heads/feature/bitbucket-2', isBare: false, isMainWorktree: false } ]) getHostedReviewForBranchMock.mockResolvedValueOnce({ provider: 'bitbucket', number: 12, title: 'Different Bitbucket PR', state: 'open', url: 'https://bitbucket.org/team/repo/pull-requests/12', status: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'bitbucket-title', baseBranch: 'abc123', branchNameOverride: 'feature/bitbucket', linkedBitbucketPR: 11, pushTarget: { remoteName: 'origin', branchName: 'feature/bitbucket' } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/bitbucket-title-2', 'feature/bitbucket-2', 'abc123', false ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/bitbucket-title-2', expect.objectContaining({ linkedBitbucketPR: 11 }) ) }) it('suffixes a matching push target branch without selected PR metadata', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/fix' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title-2', 'feature/fix-2', 'abc123', false ) }) it('suffixes a matching push target branch when selected PR metadata has no PR number', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/fix' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: null, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title-2', 'feature/fix-2', 'abc123', false ) }) it('suffixes a matching push target branch when the existing PR is different', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/fix' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } ]) getPRForBranchMock.mockResolvedValueOnce({ number: 43, title: 'Different PR', state: 'open', url: 'https://example.com/pr/43', checksStatus: 'success', updatedAt: '2026-05-21T00:00:00Z', mergeable: 'UNKNOWN' }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix') expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title-2', 'feature/fix-2', 'abc123', false ) }) it('suffixes a selected PR remote conflict when the PR lookup fails', async () => { getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) => branch === 'feature/fix' ? 'remote' : null ) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } ]) getPRForBranchMock.mockRejectedValueOnce(new Error('gh unavailable')) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith('/workspace/repo', 'feature/fix') expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title-2', 'feature/fix-2', 'abc123', false ) }) it('checks out an unused existing PR branch only when it is at the resolved head SHA', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) 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-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) expect(getBranchConflictKindMock).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title', 'feature/fix', 'abc123', false, false, { checkoutExistingBranch: true } ) }) it('suffixes an existing PR branch when its tip differs from the resolved head SHA', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { return { stdout: 'old123\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }) getBranchConflictKindMock.mockResolvedValueOnce('local') listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix-2', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title-2', 'feature/fix-2', 'abc123', false ) }) 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).toMatchObject({ 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).toMatchObject({ 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).toMatchObject({ 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('threads explicit origin preference into dual-remote PR head resolution', async () => { store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, issueSourcePreference: 'origin', worktreeBaseRef: null }) getPullRequestPushTargetMock.mockResolvedValue({ pushTarget: { 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] === 'remote' && args[1] === 'get-url') { const url = args[2] === 'origin' ? ORIGIN_REMOTE_URL : 'git@github.com:org/upstream-repo.git' return { stdout: `${url}\n`, stderr: '' } } if (args[0] === 'remote') { return { stdout: 'origin\nupstream\n', stderr: '' } } 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', '--no-tags', 'origin', `+refs/pull/1738/head:refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/1738` ], { cwd: '/workspace/repo', timeout: REVIEW_HEAD_FETCH_TIMEOUT_MS } ) expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( ['remote', 'get-url', 'upstream'], expect.anything() ) expect(getPullRequestPushTargetMock).toHaveBeenCalledWith( '/workspace/repo', 1738, null, {}, 'origin' ) expect(result).toMatchObject({ baseBranch: 'abc123', headSha: 'abc123', branchNameOverride: 'prateek/fix-sidebar-agents-toggle', pushTarget: { remoteName: 'pr-prateek-orca', branchName: 'prateek/fix-sidebar-agents-toggle', remoteUrl: 'git@github.com:prateek/orca.git' } }) }) it('returns the same-repo PR head SHA and exact branch override when resolving a PR base', async () => { gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse') { return { stdout: 'def456\n', stderr: '' } } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 42, headRefName: 'feature/add-feature', isCrossRepository: false }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'fetch', 'origin', '+refs/heads/feature/add-feature:refs/remotes/origin/feature/add-feature' ], { cwd: '/workspace/repo' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['rev-parse', '--verify', 'origin/feature/add-feature'], { cwd: '/workspace/repo' } ) expect(result).toMatchObject({ baseBranch: 'def456', headSha: 'def456', branchNameOverride: 'feature/add-feature', pushTarget: { remoteName: 'origin', branchName: 'feature/add-feature' } }) }) it('routes local worktree creation through the selected WSL project runtime', async () => { mockSelectedWslProjectRuntime() listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'base', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', 'origin/main', false, false, { wslDistro: 'Ubuntu' } ) expect(resolveDefaultBaseRefWithLocalGitMock).toHaveBeenCalledWith({ cwd: '/workspace/repo', wslDistro: 'Ubuntu' }) expect(getBranchConflictKindMock).toHaveBeenCalledWith( '/workspace/repo', 'improve-dashboard', 'origin/main', { wslDistro: 'Ubuntu' } ) expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' }) }) it('routes fork push target setup through the selected WSL project runtime', async () => { mockSelectedWslProjectRuntime() listWorktreesMock.mockResolvedValue([ { path: '/workspace/wsl-fork', head: 'abc123', branch: 'refs/heads/wsl-fork', isBare: false, isMainWorktree: false } ]) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'wsl-fork', pushTarget: { remoteName: 'pr-contributor-orca', branchName: 'contributor/wsl-fork', remoteUrl: 'git@github.com:contributor/orca.git' } }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['check-ref-format', '--branch', 'contributor/wsl-fork'], { cwd: '/workspace/repo', wslDistro: 'Ubuntu' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['remote', 'add', 'pr-contributor-orca', 'git@github.com:contributor/orca.git'], { cwd: '/workspace/repo', wslDistro: 'Ubuntu' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'fetch', 'pr-contributor-orca', '+refs/heads/contributor/wsl-fork:refs/remotes/pr-contributor-orca/contributor/wsl-fork' ], { cwd: '/workspace/repo', wslDistro: 'Ubuntu' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['branch', '--set-upstream-to', 'pr-contributor-orca/contributor/wsl-fork', 'wsl-fork'], { cwd: '/workspace/wsl-fork', wslDistro: 'Ubuntu' } ) }) it('routes selected PR branch conflict lookup through the selected WSL project runtime', async () => { mockSelectedWslProjectRuntime() getBranchConflictKindMock.mockResolvedValueOnce('remote') getPRForBranchMock.mockResolvedValueOnce({ number: 42, title: 'Selected PR', state: 'open', url: 'https://example.com/pr/42', checksStatus: 'success', updatedAt: '2026-06-16T00:00:00.000Z', mergeable: 'UNKNOWN' }) listWorktreesMock.mockResolvedValue([ { path: '/workspace/fix-title', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix', linkedPR: 42, pushTarget: { remoteName: 'origin', branchName: 'feature/fix' } }) expect(getPRForBranchMock).toHaveBeenCalledWith( '/workspace/repo', 'feature/fix', null, null, null, { localGitExecOptions: { wslDistro: 'Ubuntu' } } ) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/fix-title', 'feature/fix', 'abc123', false, false, { wslDistro: 'Ubuntu' } ) }) it('routes PR base git calls through the selected WSL project runtime', async () => { setPlatform('win32') store.getProjects.mockReturnValue([ { id: 'project-1', displayName: 'repo', badgeColor: '#000', sourceRepoIds: ['repo-1'], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ]) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse') { return { stdout: 'def456\n', stderr: '' } } return { stdout: '', stderr: '' } }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 42, headRefName: 'feature/add-feature', isCrossRepository: false }) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( [ 'fetch', 'origin', '+refs/heads/feature/add-feature:refs/remotes/origin/feature/add-feature' ], { cwd: '/workspace/repo', wslDistro: 'Ubuntu' } ) expect(gitExecFileAsyncMock).toHaveBeenCalledWith( ['rev-parse', '--verify', 'origin/feature/add-feature'], { cwd: '/workspace/repo', wslDistro: 'Ubuntu' } ) expect(getDefaultRemoteMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' }) expect(result).toMatchObject({ baseBranch: 'def456', headSha: 'def456', branchNameOverride: 'feature/add-feature', pushTarget: { remoteName: 'origin', branchName: 'feature/add-feature' } }) }) it('lists detected worktrees through the selected WSL project runtime', async () => { setPlatform('win32') store.getProjects.mockReturnValue([ { id: 'project-1', displayName: 'repo', badgeColor: '#000', sourceRepoIds: ['repo-1'], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ]) listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'def456', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) const result = await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' }) expect(result).toMatchObject({ repoId: 'repo-1', authoritative: true, source: 'git', worktrees: [expect.objectContaining({ path: '/workspace/repo' })] }) }) it('selects the exact SSH repo owner when repo IDs collide across hosts', async () => { const sshHostId = toSshExecutionHostId('target-a') const localRepo = { id: 'shared-repo', path: '/local/repo', displayName: 'local repo', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', displayName: 'remote repo', connectionId: 'target-a' } const provider = { listWorktrees: vi.fn().mockResolvedValue([]) } store.getRepos.mockImplementation(() => [{ ...localRepo }, { ...sshRepo }]) getSshGitProviderMock.mockImplementation((targetId) => targetId === 'target-a' ? provider : undefined ) const expectedAuthority = getSshProviderAuthority('target-a') const result = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'shared-repo', executionHostId: sshHostId, expectedAuthority }) expect(provider.listWorktrees).toHaveBeenCalledWith('/remote/repo', { signal: expect.any(AbortSignal) }) expect(result).toEqual({ status: 'complete', providerRequestId: 'request-1', repoId: 'shared-repo', authority: { kind: 'direct-ssh', executionHostId: sshHostId, ...expectedAuthority }, result: { repoId: 'shared-repo', authoritative: true, source: 'git', worktrees: [] } }) }) it('rejects malformed and contradictory repo host provenance', async () => { const provider = { listWorktrees: vi.fn().mockResolvedValue([]) } getSshGitProviderMock.mockReturnValue(provider) const request = { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'repo-1', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') } const baseRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([{ ...baseRepo, executionHostId: 'ssh:%' }]) const malformed = await handlers['worktrees:listDetected'](ipcEvent, request) store.getRepos.mockReturnValue([ { ...baseRepo, executionHostId: toSshExecutionHostId('target-b') } ]) const contradictory = await handlers['worktrees:listDetected'](ipcEvent, request) expect(malformed).toMatchObject({ status: 'rejected', providerRequestId: 'request-1', executionHostId: 'ssh:target-a' }) expect(contradictory).toMatchObject({ status: 'rejected', providerRequestId: 'request-1', executionHostId: 'ssh:target-a' }) expect(provider.listWorktrees).not.toHaveBeenCalled() }) it('returns a local discriminant without SSH authority fields', async () => { listWorktreesMock.mockResolvedValue([]) const result = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'repo-1', executionHostId: 'local' }) expect(result).toEqual({ status: 'complete', providerRequestId: 'request-1', repoId: 'repo-1', authority: { kind: 'local', executionHostId: 'local' }, result: { repoId: 'repo-1', authoritative: true, source: 'git', worktrees: [] } }) }) it('includes the full SSH authority on non-authoritative data', async () => { const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo]) getSshGitProviderMock.mockReturnValue(undefined) const expectedAuthority = getSshProviderAuthority('target-a') const result = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) expect(result).toEqual({ status: 'non-authoritative', providerRequestId: 'request-1', repoId: 'repo-1', authority: { kind: 'direct-ssh', executionHostId: 'ssh:target-a', ...expectedAuthority }, result: { repoId: 'repo-1', authoritative: false, source: 'metadata-fallback', worktrees: [] } }) }) it('lists every persisted SSH worktree without accessing the live provider', async () => { const sshHostId = toSshExecutionHostId('target-a') const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } const metaById = { 'repo-1::/remote/repo': makeWorktreeMeta({ displayName: 'main', hostId: sshHostId }), 'repo-1::/remote/queued': makeWorktreeMeta({ displayName: 'queued', hostId: sshHostId }), 'repo-1::/remote/other-host': makeWorktreeMeta({ displayName: 'other host', hostId: toSshExecutionHostId('target-b') }) } store.getRepos.mockReturnValue([sshRepo]) store.getProjectHostSetups.mockReturnValue([]) store.getAllWorktreeMeta.mockReturnValue(metaById) store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId]) const result = await handlers['worktrees:listKnownForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: sshHostId }) expect(result).toMatchObject({ status: 'complete', repoId: sshRepo.id, executionHostId: sshHostId, result: { repoId: sshRepo.id, authoritative: false, source: 'metadata-fallback', worktrees: [ expect.objectContaining({ path: '/remote/repo', isMainWorktree: true }), expect.objectContaining({ path: '/remote/queued', isMainWorktree: false }) ] } }) expect(getSshGitProviderMock).not.toHaveBeenCalled() expect(listWorktreesMock).not.toHaveBeenCalled() }) it('lists SSH folder workspaces at the folder path, not the instance-suffixed id', async () => { const sshHostId = toSshExecutionHostId('target-a') const folderRepo = { id: 'repo-1', path: '/remote/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, connectionId: 'target-a', kind: 'folder' as const } const rootId = `${folderRepo.id}::${folderRepo.path}` const instanceId = `${rootId}::workspace:11111111-2222-3333-4444-555555555555` const metaById = { [rootId]: makeWorktreeMeta({ displayName: 'root', hostId: sshHostId }), [instanceId]: makeWorktreeMeta({ displayName: 'second workspace', hostId: sshHostId, instanceId: '11111111-2222-3333-4444-555555555555' }) } store.getRepos.mockReturnValue([folderRepo]) store.getProjectHostSetups.mockReturnValue([]) store.getAllWorktreeMeta.mockReturnValue(metaById) store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId]) const result = (await handlers['worktrees:listKnownForExecutionHost'](null, { repoId: folderRepo.id, executionHostId: sshHostId })) as { status: string; result: { authoritative: boolean; worktrees: Worktree[] } } expect(result.status).toBe('complete') expect(result.result.authoritative).toBe(false) // Why: the git-worktree synthesizer would read the "::workspace:" tail as a directory. expect(result.result.worktrees).toEqual([ expect.objectContaining({ id: rootId, path: folderRepo.path, isMainWorktree: true }), expect.objectContaining({ id: instanceId, path: folderRepo.path, isMainWorktree: false }) ]) expect(getSshGitProviderMock).not.toHaveBeenCalled() }) it('stops listing SSH worktrees once an authoritative scan retires their metadata', async () => { const sshHostId = toSshExecutionHostId('target-a') const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } const metaById: Record> = { 'repo-1::/remote/repo': makeWorktreeMeta({ displayName: 'main', hostId: sshHostId }), 'repo-1::/remote/deleted': makeWorktreeMeta({ displayName: 'deleted', hostId: sshHostId }), 'repo-1::/remote/other-host': makeWorktreeMeta({ displayName: 'other host', hostId: toSshExecutionHostId('target-b') }), 'repo-2::/remote/other-repo': makeWorktreeMeta({ displayName: 'other repo' }) } store.getRepos.mockReturnValue([sshRepo]) store.getProjectHostSetups.mockReturnValue([]) store.getAllWorktreeMeta.mockReturnValue(metaById) store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId]) store.removeWorktreeMeta.mockImplementation((worktreeId: string) => { delete metaById[worktreeId] }) const forgotten = await handlers['worktrees:forgetRemovedForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: sshHostId, // Only the first is this host's row; the rest must survive an over-broad request. worktreeIds: [ 'repo-1::/remote/deleted', 'repo-1::/remote/other-host', 'repo-2::/remote/other-repo', 'repo-1::/remote/never-persisted' ] }) expect(forgotten).toEqual({ forgottenWorktreeIds: ['repo-1::/remote/deleted'] }) expect(store.removeWorktreeMeta).toHaveBeenCalledTimes(1) expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/remote/deleted', sshHostId) // Why: the renderer's suppression memory is session-scoped, so the next launch must not re-list the row. const relisted = (await handlers['worktrees:listKnownForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: sshHostId })) as { result: { worktrees: { path: string }[] } } expect(relisted.result.worktrees.map((worktree) => worktree.path)).toEqual(['/remote/repo']) }) it('never retires folder workspace metadata, which is the workspace record itself', async () => { const sshHostId = toSshExecutionHostId('target-a') const folderRepo = { id: 'repo-1', path: '/remote/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, connectionId: 'target-a', kind: 'folder' as const } const instanceId = `${folderRepo.id}::${folderRepo.path}::workspace:11111111-2222-3333-4444-555555555555` store.getRepos.mockReturnValue([folderRepo]) store.getAllWorktreeMeta.mockReturnValue({ [instanceId]: makeWorktreeMeta({ displayName: 'second workspace', hostId: sshHostId }) }) const forgotten = await handlers['worktrees:forgetRemovedForExecutionHost'](null, { repoId: folderRepo.id, executionHostId: sshHostId, worktreeIds: [instanceId] }) expect(forgotten).toEqual({ forgottenWorktreeIds: [] }) expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('refuses to retire metadata for non-SSH hosts and unowned repos', async () => { const sshRepo = { id: 'repo-1', path: '/remote/repo-a', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo, { ...sshRepo, path: '/remote/repo-b' }]) store.getAllWorktreeMeta.mockReturnValue({ 'repo-1::/remote/deleted': makeWorktreeMeta({}) }) expect( await handlers['worktrees:forgetRemovedForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: LOCAL_EXECUTION_HOST_ID, worktreeIds: ['repo-1::/remote/deleted'] }) ).toEqual({ forgottenWorktreeIds: [] }) // Ambiguous SSH ownership fails closed the same way the metadata read does. expect( await handlers['worktrees:forgetRemovedForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), worktreeIds: ['repo-1::/remote/deleted'] }) ).toEqual({ forgottenWorktreeIds: [] }) expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('rejects metadata-only reads for non-SSH execution hosts', async () => { const result = await handlers['worktrees:listKnownForExecutionHost'](null, { repoId: 'repo-1', executionHostId: LOCAL_EXECUTION_HOST_ID }) expect(result).toEqual({ status: 'rejected', repoId: 'repo-1', executionHostId: LOCAL_EXECUTION_HOST_ID }) expect(store.getRepos).not.toHaveBeenCalled() }) it('rejects ambiguous metadata-only SSH owners', async () => { const sshHostId = toSshExecutionHostId('target-a') const sshRepo = { id: 'repo-1', path: '/remote/repo-a', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo, { ...sshRepo, path: '/remote/repo-b' }]) const result = await handlers['worktrees:listKnownForExecutionHost'](null, { repoId: sshRepo.id, executionHostId: sshHostId }) expect(result).toEqual({ status: 'rejected', repoId: sshRepo.id, executionHostId: sshHostId }) expect(store.getAllWorktreeMeta).not.toHaveBeenCalled() expect(getSshGitProviderMock).not.toHaveBeenCalled() }) it('fails closed for duplicate exact owners and ambiguous legacy repo IDs', async () => { const sshRepo = { id: 'shared-repo', path: '/remote/repo-a', displayName: 'remote repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } const duplicateSshRepo = { ...sshRepo, path: '/remote/repo-b' } const localRepo = { ...sshRepo, path: '/local/repo', connectionId: undefined } store.getRepos.mockReturnValue([sshRepo, duplicateSshRepo, localRepo]) const expectedAuthority = getSshProviderAuthority('target-a') const qualified = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'shared-repo', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) const legacy = await handlers['worktrees:listDetected'](null, { repoId: 'shared-repo' }) expect(qualified).toMatchObject({ status: 'ambiguous-owner', providerRequestId: 'request-1', executionHostId: 'ssh:target-a' }) expect(legacy).toEqual({ repoId: 'shared-repo', authoritative: false, source: 'metadata-fallback', worktrees: [] }) expect(getSshGitProviderMock).not.toHaveBeenCalled() expect(listWorktreesMock).not.toHaveBeenCalled() }) it('does not prune another host lineage when repo IDs collide', async () => { const sshARepo = { id: 'shared-repo', path: '/remote/repo-a', displayName: 'remote repo A', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } const sshBRepo = { ...sshARepo, path: '/remote/repo-b', displayName: 'remote repo B', connectionId: 'target-b' } const childId = 'shared-repo::/remote/repo-b/feature' store.getRepos.mockReturnValue([sshARepo, sshBRepo]) store.getWorktreeMeta.mockImplementation((worktreeId: string) => worktreeId === childId ? makeWorktreeMeta({ hostId: toSshExecutionHostId('target-b') }) : undefined ) store.getAllWorktreeLineage.mockReturnValue({ [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: 'shared-repo::/remote/repo-b', parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 0 } }) getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn().mockResolvedValue([]) }) const expectedAuthority = getSshProviderAuthority('target-a') const result = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'shared-repo', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) expect(result).toMatchObject({ status: 'complete' }) expect(store.removeWorktreeLineage).not.toHaveBeenCalled() expect(store.setWorktreeMeta).not.toHaveBeenCalled() }) it('preserves conflicting host metadata instead of backfilling it', async () => { const sshARepo = { id: 'shared-repo', path: '/remote/repo-a', displayName: 'remote repo A', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } const sshBRepo = { ...sshARepo, path: '/remote/repo-b', connectionId: 'target-b' } const worktreePath = '/remote/shared-feature' store.getRepos.mockReturnValue([sshARepo, sshBRepo]) store.getWorktreeMeta.mockImplementation((worktreeId: string) => worktreeId === `shared-repo::${worktreePath}` ? makeWorktreeMeta({ hostId: toSshExecutionHostId('target-b') }) : undefined ) getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn().mockResolvedValue([ { path: worktreePath, head: 'head-a', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) }) const result = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: 'shared-repo', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) expect(result).toMatchObject({ status: 'non-authoritative', result: { authoritative: false, worktrees: [] } }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('rejects runtime hosts, wrong SSH targets, and missing authority', async () => { const requestId = 'request-1' as ProviderRequestId const expectedAuthority = getSshProviderAuthority('target-a') const runtimeResult = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: requestId, repoId: 'repo-1', executionHostId: 'runtime:runtime-a' }) const wrongTargetResult = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: requestId, repoId: 'repo-1', executionHostId: toSshExecutionHostId('target-b'), expectedAuthority }) const missingAuthorityResult = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: requestId, repoId: 'repo-1', executionHostId: toSshExecutionHostId('target-a') }) const zeroOwnerResult = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: requestId, repoId: 'repo-1', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) store.getRepos.mockReturnValue([ { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-b', executionHostId: toSshExecutionHostId('target-a') } ]) const wrongProviderOwnerResult = await handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: requestId, repoId: 'repo-1', executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) expect(runtimeResult).toMatchObject({ status: 'rejected' }) expect(wrongTargetResult).toMatchObject({ status: 'rejected' }) expect(missingAuthorityResult).toMatchObject({ status: 'rejected' }) expect(zeroOwnerResult).toMatchObject({ status: 'ambiguous-owner' }) expect(wrongProviderOwnerResult).toMatchObject({ status: 'rejected' }) expect(getSshGitProviderMock).not.toHaveBeenCalled() expect(listWorktreesMock).not.toHaveBeenCalled() }) it('rejects a provider replacement during the SSH await without durable mutations', async () => { let resolveList: (worktrees: GitWorktreeInfo[]) => void = () => {} const firstProvider = { listWorktrees: vi.fn( () => new Promise((resolve) => { resolveList = resolve }) ) } const replacementProvider = { listWorktrees: vi.fn().mockResolvedValue([]) } let currentProvider = firstProvider const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo]) getSshGitProviderMock.mockImplementation(() => currentProvider) const expectedAuthority = getSshProviderAuthority('target-a') const pending = handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority }) await Promise.resolve() currentProvider = replacementProvider resolveList([ { path: '/remote/repo', head: 'stale-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) await expect(pending).resolves.toMatchObject({ status: 'stale' }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it.each([ ['malformed', 'ssh:%'], ['contradictory', toSshExecutionHostId('target-b')] ])( 'rejects %s repo provenance introduced during the SSH await', async (_caseName, invalidExecutionHostId) => { let resolveList: (worktrees: GitWorktreeInfo[]) => void = () => {} const provider = { listWorktrees: vi.fn( () => new Promise((resolve) => { resolveList = resolve }) ) } const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } let repos: Repo[] = [sshRepo] store.getRepos.mockImplementation(() => repos) getSshGitProviderMock.mockReturnValue(provider) const pending = handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: `request-${_caseName}` as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) await Promise.resolve() repos = [ sshRepo, { ...sshRepo, path: '/remote/conflicting-repo', executionHostId: invalidExecutionHostId as Repo['executionHostId'] } ] resolveList([ { path: '/remote/repo', head: 'stale-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) await expect(pending).resolves.toMatchObject({ status: 'stale' }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() } ) it('aborts all old-authority SSH calls on rotation with target isolation', async () => { const repos = [ { id: 'repo-a', path: '/remote/repo-a', displayName: 'repo A', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' }, { id: 'repo-b', path: '/remote/repo-b', displayName: 'repo B', badgeColor: '#000', addedAt: 0, connectionId: 'target-b' } ] const resolveA: ((worktrees: GitWorktreeInfo[]) => void)[] = [] let resolveB: (worktrees: GitWorktreeInfo[]) => void = () => {} const signalsA: AbortSignal[] = [] let signalB: AbortSignal | undefined const abortsA = [vi.fn(), vi.fn()] const abortB = vi.fn() const providerA = { listWorktrees: vi.fn((_path: string, options?: { signal?: AbortSignal }) => { const index = signalsA.length const signal = options?.signal if (signal) { signalsA.push(signal) signal.addEventListener('abort', abortsA[index]) } return new Promise((resolve) => { resolveA.push(resolve) }) }) } const providerB = { listWorktrees: vi.fn((_path: string, options?: { signal?: AbortSignal }) => { signalB = options?.signal signalB?.addEventListener('abort', abortB) return new Promise((resolve) => { resolveB = resolve }) }) } store.getRepos.mockReturnValue(repos) getSshGitProviderMock.mockImplementation((targetId) => targetId === 'target-a' ? providerA : providerB ) const authorityA = getSshProviderAuthority('target-a') const authorityB = getSshProviderAuthority('target-b') const request = ( repo: (typeof repos)[number], providerRequestId: ProviderRequestId, expectedAuthority: ReturnType ) => handlers['worktrees:listDetected'](ipcEvent, { providerRequestId, repoId: repo.id, executionHostId: toSshExecutionHostId(repo.connectionId), expectedAuthority }) const pendingA1 = request(repos[0], 'request-a1' as ProviderRequestId, authorityA) const pendingA2 = request(repos[0], 'request-a2' as ProviderRequestId, authorityA) const pendingB = request(repos[1], 'request-b' as ProviderRequestId, authorityB) await Promise.resolve() rotateSshProviderAuthority('target-a') rotateSshProviderAuthority('target-a') expect(signalsA).toHaveLength(2) expect(signalsA.every((signal) => signal.aborted)).toBe(true) expect(abortsA[0]).toHaveBeenCalledOnce() expect(abortsA[1]).toHaveBeenCalledOnce() expect(signalB?.aborted).toBe(false) expect(abortB).not.toHaveBeenCalled() await expect(Promise.all([pendingA1, pendingA2])).resolves.toEqual([ expect.objectContaining({ status: 'canceled', providerRequestId: 'request-a1' }), expect.objectContaining({ status: 'canceled', providerRequestId: 'request-a2' }) ]) resolveB([]) await expect(pendingB).resolves.toMatchObject({ status: 'complete', providerRequestId: 'request-b' }) rotateSshProviderAuthority('target-b') expect(abortB).not.toHaveBeenCalled() store.setWorktreeMeta.mockClear() store.removeWorktreeLineage.mockClear() for (const resolve of resolveA) { resolve([ { path: '/remote/repo-a', head: 'late-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) } await Promise.resolve() await Promise.resolve() expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('cancels an SSH provider request by sender-scoped provider request ID', async () => { let providerSignal: AbortSignal | undefined const provider = { listWorktrees: vi.fn( (_repoPath: string, options?: { signal?: AbortSignal }) => new Promise((_resolve, reject) => { providerSignal = options?.signal providerSignal?.addEventListener( 'abort', () => reject(new DOMException('Canceled', 'AbortError')), { once: true } ) }) ) } const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo]) getSshGitProviderMock.mockReturnValue(provider) const pending = handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) await Promise.resolve() handlers['worktrees:cancelListDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId }) expect(providerSignal?.aborted).toBe(true) await expect(pending).resolves.toMatchObject({ status: 'canceled', providerRequestId: 'request-1' }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('settles a noncooperative SSH provider at the main-owned deadline and cleans up', async () => { vi.useFakeTimers() try { let providerSignal: AbortSignal | undefined let rejectLateRequest: (error: Error) => void = () => {} const provider = { listWorktrees: vi.fn((_repoPath: string, options?: { signal?: AbortSignal }) => { if (provider.listWorktrees.mock.calls.length > 1) { return Promise.resolve([]) } return new Promise((_resolve, reject) => { rejectLateRequest = reject // Why: this provider intentionally ignores abort to exercise the main-owned deadline. if (options?.signal) { providerSignal = options?.signal } }) }) } const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } store.getRepos.mockReturnValue([sshRepo]) getSshGitProviderMock.mockReturnValue(provider) const pending = handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) await Promise.resolve() await vi.advanceTimersByTimeAsync(DETECTED_WORKTREE_PROVIDER_TIMEOUT_MS - 1) let settled = false void Promise.resolve(pending).finally(() => { settled = true }) await Promise.resolve() expect(settled).toBe(false) await vi.advanceTimersByTimeAsync(1) expect(providerSignal?.aborted).toBe(true) await expect(pending).resolves.toMatchObject({ status: 'timed-out', providerRequestId: 'request-1' }) expect(vi.getTimerCount()).toBe(0) await expect( handlers['worktrees:listDetected'](ipcEvent, { providerRequestId: 'request-1' as ProviderRequestId, repoId: sshRepo.id, executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) ).resolves.toMatchObject({ status: 'complete', providerRequestId: 'request-1' }) store.setWorktreeMeta.mockClear() store.removeWorktreeLineage.mockClear() rejectLateRequest(new Error('late provider failure')) await Promise.resolve() expect(store.setWorktreeMeta).not.toHaveBeenCalled() expect(store.removeWorktreeLineage).not.toHaveBeenCalled() } finally { vi.useRealTimers() } }) it('filters worktree and folder lineage to one exact SSH host', async () => { const repos = [ { id: 'duplicate', path: '/a/repo', displayName: 'a', badgeColor: '#000', addedAt: 0, connectionId: 'target-a', projectGroupId: 'group-a' }, { id: 'duplicate', path: '/b/repo', displayName: 'b', badgeColor: '#000', addedAt: 0, connectionId: 'target-b', projectGroupId: 'group-b' } ] const aParent = 'duplicate::/a/repo' const aChild = 'duplicate::/a/child' const bParent = 'duplicate::/b/repo' const bChild = 'duplicate::/b/child' const runtimeParent = 'duplicate::/runtime/parent' const runtimeChild = 'duplicate::/runtime/child' const worktreeLineage = { [aChild]: { worktreeId: aChild, worktreeInstanceId: 'a-child', parentWorktreeId: aParent, parentWorktreeInstanceId: 'a-parent', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 1 }, [bChild]: { worktreeId: bChild, worktreeInstanceId: 'b-child', parentWorktreeId: bParent, parentWorktreeInstanceId: 'b-parent', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 2 }, [runtimeChild]: { worktreeId: runtimeChild, worktreeInstanceId: 'runtime-child', parentWorktreeId: runtimeParent, parentWorktreeInstanceId: 'runtime-parent', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 3 } } const folderLineage = { 'folder:folder-a-child': { childWorkspaceKey: 'folder:folder-a-child', parentWorkspaceKey: 'folder:folder-a-parent', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 3 }, 'folder:folder-b-child': { childWorkspaceKey: 'folder:folder-b-child', parentWorkspaceKey: 'folder:folder-b-parent', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 4 } } store.getRepos.mockReturnValue(repos) store.getWorktreeMeta.mockImplementation((id: string) => id.includes('/runtime/') ? { hostId: 'ssh:target-a', runtimeOwnerEnvironmentId: 'environment-a' } : { hostId: id.includes('/a/') || id.endsWith('/a/repo') ? 'ssh:target-a' : 'ssh:target-b' } ) store.getAllWorktreeLineage.mockReturnValue(worktreeLineage) store.getAllWorkspaceLineage.mockReturnValue(folderLineage) store.getProjectGroups.mockReturnValue([ { id: 'group-a', connectionId: 'target-a' }, { id: 'group-b', connectionId: 'target-b' } ]) store.getFolderWorkspaces.mockReturnValue([ { id: 'folder-a-child', projectGroupId: 'group-a', folderPath: '/a/child', connectionId: 'target-a' }, { id: 'folder-a-parent', projectGroupId: 'group-a', folderPath: '/a', connectionId: 'target-a' }, { id: 'folder-b-child', projectGroupId: 'group-b', folderPath: '/b/child', connectionId: 'target-b' }, { id: 'folder-b-parent', projectGroupId: 'group-b', folderPath: '/b', connectionId: 'target-b' } ]) const provider = { listWorktrees: vi.fn() } getSshGitProviderMock.mockImplementation((targetId: string) => targetId === 'target-a' ? provider : undefined ) const result = await handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: getSshProviderAuthority('target-a') }) expect(result).toMatchObject({ authoritative: true, authority: { kind: 'direct-ssh', executionHostId: 'ssh:target-a', targetId: 'target-a' }, worktreeLineageById: { [aChild]: worktreeLineage[aChild] }, workspaceLineageByChildKey: { 'folder:folder-a-child': folderLineage['folder:folder-a-child'] } }) expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('snapshots lineage catalogs once and memoizes repeated owner resolution', async () => { const worktreeIds = Array.from( { length: 101 }, (_, index) => `repo-1::/workspace/repo-${index}` ) const lineage = Object.fromEntries( worktreeIds.slice(1).map((worktreeId, index) => [ worktreeId, { worktreeId, worktreeInstanceId: `child-${index}`, parentWorktreeId: worktreeIds[index], parentWorktreeInstanceId: `parent-${index}`, origin: 'cli', capture: { source: 'cwd-context', confidence: 'inferred' }, createdAt: index } ]) ) store.getAllWorktreeLineage.mockReturnValue(lineage) store.getRepos.mockClear() store.getFolderWorkspaces.mockClear() store.getProjectGroups.mockClear() store.getWorktreeMeta.mockClear() const result = await handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: 'local' }) expect(result).toMatchObject({ authoritative: true }) expect( Object.keys((result as { worktreeLineageById: Record }).worktreeLineageById) ).toHaveLength(100) expect(store.getRepos).toHaveBeenCalledOnce() expect(store.getFolderWorkspaces).toHaveBeenCalledOnce() expect(store.getProjectGroups).toHaveBeenCalledOnce() expect(store.getWorktreeMeta).toHaveBeenCalledTimes(101) }) it('preserves ambiguous legacy lineage instead of guessing among duplicate repo owners', async () => { const child = 'duplicate::/child' const parent = 'duplicate::/parent' store.getRepos.mockReturnValue([ { id: 'duplicate', path: '/local', displayName: 'local', badgeColor: '#000', addedAt: 0 }, { id: 'duplicate', path: '/remote', displayName: 'remote', badgeColor: '#000', addedAt: 0, connectionId: 'target-a' } ]) store.getAllWorktreeLineage.mockReturnValue({ [child]: { worktreeId: child, worktreeInstanceId: 'child', parentWorktreeId: parent, parentWorktreeInstanceId: 'parent', origin: 'cli', capture: { source: 'cwd-context', confidence: 'inferred' }, createdAt: 1 } }) await expect( handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: 'local' }) ).resolves.toEqual({ authoritative: false, executionHostId: 'local', reason: 'ambiguous-owner' }) expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('rejects runtime lineage reads and stale SSH authority after hydration', async () => { await expect( handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: 'runtime:environment-a' }) ).resolves.toMatchObject({ authoritative: false, reason: 'rejected' }) expect(runtimeStub.hydrateInferredWorktreeLineage).not.toHaveBeenCalled() let finishHydration: () => void = () => {} runtimeStub.hydrateInferredWorktreeLineage.mockImplementation( () => new Promise((resolve) => { finishHydration = resolve }) ) getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn() }) const authority = getSshProviderAuthority('target-a') const pending = handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: authority }) await Promise.resolve() rotateSshProviderAuthority('target-a') finishHydration() await expect(pending).resolves.toMatchObject({ authoritative: false, reason: 'stale' }) expect(store.removeWorktreeLineage).not.toHaveBeenCalled() }) it('bounds noncooperative lineage hydration and permits a later same-authority read', async () => { vi.useFakeTimers() try { runtimeStub.hydrateInferredWorktreeLineage.mockReturnValue(new Promise(() => {})) getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn() }) const authority = getSshProviderAuthority('target-a') const pending = handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: authority }) await vi.advanceTimersByTimeAsync(LINEAGE_HYDRATION_TIMEOUT_MS - 1) let settled = false void Promise.resolve(pending).finally(() => { settled = true }) await Promise.resolve() expect(settled).toBe(false) await vi.advanceTimersByTimeAsync(1) await expect(pending).resolves.toMatchObject({ authoritative: false, reason: 'unavailable' }) expect(vi.getTimerCount()).toBe(0) runtimeStub.hydrateInferredWorktreeLineage.mockResolvedValue(undefined) await expect( handlers['worktrees:listLineageForHost'](ipcEvent, { executionHostId: toSshExecutionHostId('target-a'), expectedAuthority: authority }) ).resolves.toMatchObject({ authoritative: true }) } finally { vi.useRealTimers() } }) it('hydrates detected worktrees with instance-validated legacy lineage after an update', async () => { const parentPath = '/workspace/assigned-issues' const childPath = '/workspace/issue-9276-nested-ssh-runtime-routing' const parentId = `repo-1::${parentPath}` const childId = `repo-1::${childPath}` const metaById: Record = { [parentId]: { instanceId: 'parent-instance' }, [childId]: { instanceId: 'child-instance' } } store.getWorktreeMeta.mockImplementation((id: string) => metaById[id]) store.setWorktreeMeta.mockImplementation((id: string, updates: object) => ({ ...metaById[id], ...updates })) store.getAllWorktreeLineage.mockReturnValue({ [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 1 } }) listWorktreesMock.mockResolvedValue([ { path: childPath, head: 'child-head', branch: 'refs/heads/child', isBare: false, isMainWorktree: false }, { path: parentPath, head: 'parent-head', branch: 'refs/heads/parent', isBare: false, isMainWorktree: false } ]) const result = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: (Worktree & { lineage?: unknown; parentWorktreeId?: string | null })[] } expect(result.worktrees).toEqual([ expect.objectContaining({ id: childId, parentWorktreeId: parentId, lineage: expect.objectContaining({ parentWorktreeInstanceId: 'parent-instance' }) }), expect.objectContaining({ id: parentId, parentWorktreeId: null, childWorktreeIds: [childId], lineage: null }) ]) }) it('hydrates folder-repo detected rows with instance-validated legacy lineage', async () => { const folderRepo = { id: 'repo-1', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' as const } const parentId = `${folderRepo.id}::${folderRepo.path}` const childId = `${parentId}::workspace:child-instance` const metaById: Record> = { [parentId]: makeWorktreeMeta({ instanceId: 'parent-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }), [childId]: makeWorktreeMeta({ instanceId: 'child-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) } store.getRepos.mockReturnValue([folderRepo]) store.getRepo.mockReturnValue(folderRepo) store.getAllWorktreeMeta.mockReturnValue(metaById) store.getWorktreeMeta.mockImplementation((worktreeId: string) => metaById[worktreeId]) store.getAllWorktreeLineage.mockReturnValue({ [childId]: { worktreeId: childId, worktreeInstanceId: 'child-instance', parentWorktreeId: parentId, parentWorktreeInstanceId: 'parent-instance', origin: 'cli', capture: { source: 'explicit-cli-flag', confidence: 'explicit' }, createdAt: 1 } }) const result = (await handlers['worktrees:listDetected'](null, { repoId: folderRepo.id })) as { worktrees: (Worktree & { lineage?: unknown; parentWorktreeId?: string | null })[] } expect(result.worktrees).toEqual([ expect.objectContaining({ id: parentId, parentWorktreeId: null, childWorktreeIds: [childId], lineage: null }), expect.objectContaining({ id: childId, parentWorktreeId: parentId, lineage: expect.objectContaining({ parentWorktreeInstanceId: 'parent-instance' }) }) ]) }) it('hides agent scratch created inside a linked checkout from desktop listings', async () => { const linkedCheckoutPath = '/workspace/feature-x' const scratchPath = `${linkedCheckoutPath}/.claude/worktrees/agent-a04ccaaa` listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: linkedCheckoutPath, head: 'feature-head', branch: 'refs/heads/feature-x', isBare: false, isMainWorktree: false }, { path: scratchPath, head: 'scratch-head', branch: 'refs/heads/worktree-agent-a04ccaaa', isBare: false, isMainWorktree: false } ]) const detected = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: (Worktree & { ownership: string; visible: boolean })[] } const visible = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as Worktree[] expect(detected.worktrees.find((worktree) => worktree.path === scratchPath)).toMatchObject({ ownership: 'agent-scratch', visible: false }) expect(visible.map((worktree) => worktree.path)).toEqual([ '/workspace/repo', linkedCheckoutPath ]) }) it('does not reuse host detected worktree scans for a selected WSL runtime', async () => { listWorktreesMock .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'host-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'wsl-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) const hostResult = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } setPlatform('win32') store.getProjects.mockReturnValue([ { id: 'project-1', displayName: 'repo', badgeColor: '#000', sourceRepoIds: ['repo-1'], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ]) const wslResult = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(hostResult.worktrees[0].head).toBe('host-head') expect(wslResult.worktrees[0].head).toBe('wsl-head') expect(listWorktreesMock).toHaveBeenCalledTimes(2) expect(listWorktreesMock).toHaveBeenNthCalledWith(1, '/workspace/repo') expect(listWorktreesMock).toHaveBeenNthCalledWith(2, '/workspace/repo', { wslDistro: 'Ubuntu' }) }) it('reuses a recent authoritative detected worktree scan', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) const first = await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) const second = await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) expect(first).toEqual(second) expect(listWorktreesMock).toHaveBeenCalledTimes(1) }) it('coalesces concurrent authoritative detected worktree scans', async () => { listWorktreesMock.mockImplementation(async () => { await Promise.resolve() return [ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ] }) await Promise.all([ handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }), handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }), handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) ]) expect(listWorktreesMock).toHaveBeenCalledTimes(1) }) it('rechecks detected worktree metadata while reusing a cached raw scan', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) let currentMeta = makeWorktreeMeta({ isPinned: false }) store.getWorktreeMeta.mockImplementation(() => currentMeta) store.setWorktreeMeta.mockImplementation(() => currentMeta) const first = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } currentMeta = makeWorktreeMeta({ isPinned: true }) const second = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(first.worktrees[0].isPinned).toBe(false) expect(second.worktrees[0].isPinned).toBe(true) expect(listWorktreesMock).toHaveBeenCalledTimes(1) }) it('rescans detected worktrees after the scan cache TTL expires', async () => { vi.useFakeTimers() try { listWorktreesMock .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/new-worktree', head: 'feature-head', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await vi.advanceTimersByTimeAsync(5_001) const second = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(second.worktrees.map((worktree) => worktree.path)).toEqual([ '/workspace/repo', '/workspace/new-worktree' ]) expect(listWorktreesMock).toHaveBeenCalledTimes(2) } finally { vi.useRealTimers() } }) it('starts the detected scan cache TTL after a slow scan completes', async () => { vi.useFakeTimers() try { listWorktreesMock .mockImplementationOnce( () => new Promise((resolve) => { setTimeout( () => resolve([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]), 6_000 ) }) ) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/new-worktree', head: 'feature-head', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) const first = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await vi.advanceTimersByTimeAsync(6_000) await first const second = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(second.worktrees.map((worktree) => worktree.path)).toEqual(['/workspace/repo']) expect(listWorktreesMock).toHaveBeenCalledTimes(1) } finally { vi.useRealTimers() } }) it('invalidates the detected scan cache before worktree change notifications', async () => { listWorktreesMock .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/new-worktree', head: 'feature-head', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) notifyWorktreesChanged(mainWindow as never, 'repo-1') const second = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(second.worktrees).toHaveLength(2) expect(listWorktreesMock).toHaveBeenCalledTimes(2) }) it('rescans detected worktrees after the local create flow notifies worktree changes', async () => { listWorktreesMock .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/improve-dashboard', head: 'feature-head', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/improve-dashboard', head: 'feature-head', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]) await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) const detected = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(detected.worktrees.map((worktree) => worktree.path)).toEqual([ '/workspace/repo', '/workspace/improve-dashboard' ]) expect(listWorktreesMock).toHaveBeenCalledTimes(3) }) it('does not run fresh-scan side effects from a detected scan invalidated while in flight', async () => { let resolveScan: (worktrees: GitWorktreeInfo[]) => void = () => {} listWorktreesMock.mockImplementation( () => new Promise((resolve) => { resolveScan = resolve as (worktrees: GitWorktreeInfo[]) => void }) ) store.getAllWorktreeLineage.mockReturnValue({ 'repo-1::/workspace/new-worktree': { worktreeId: 'repo-1::/workspace/new-worktree', worktreeInstanceId: 'child-instance', parentWorktreeId: 'repo-1::/workspace/repo', parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 0 } }) const pendingList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() notifyWorktreesChanged(mainWindow as never, 'repo-1') resolveScan([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) await pendingList expect(store.removeWorktreeLineage).not.toHaveBeenCalled() expect(listWorktreesMock).toHaveBeenCalledTimes(1) }) it('does not retain invalidated detected scans after they settle', async () => { let resolveScan: (worktrees: GitWorktreeInfo[]) => void = () => {} listWorktreesMock.mockImplementation( () => new Promise((resolve) => { resolveScan = resolve as (worktrees: GitWorktreeInfo[]) => void }) ) const pendingList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() expect(__getDetectedWorktreeScanCacheStatsForTests()).toMatchObject({ cacheSize: 0, inFlightSize: 1 }) notifyWorktreesChanged(mainWindow as never, 'repo-1') expect(__getDetectedWorktreeScanCacheStatsForTests()).toMatchObject({ cacheSize: 0, inFlightSize: 0 }) resolveScan([ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) await pendingList expect(__getDetectedWorktreeScanCacheStatsForTests()).toMatchObject({ cacheSize: 0, inFlightSize: 0 }) }) it('does not accumulate scan bookkeeping across prolonged repository churn', async () => { listWorktreesMock.mockImplementation(async (repoPath: string) => [ { path: repoPath, head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) for (let index = 0; index < 128; index += 1) { const repoId = `repo-${index}` store.getRepos.mockReturnValue([ { id: repoId, path: `/workspace/${repoId}`, displayName: repoId, badgeColor: '#000', addedAt: 0, worktreeBaseRef: null } ]) await handlers['worktrees:listDetected'](null, { repoId }) notifyWorktreesChanged(mainWindow as never, repoId) } expect(__getDetectedWorktreeScanCacheStatsForTests()).toEqual({ cacheSize: 0, inFlightSize: 0 }) expect(listWorktreesMock).toHaveBeenCalledTimes(128) }) it('keeps a replacement scan current after an older scan settles first', async () => { const resolvers: ((worktrees: GitWorktreeInfo[]) => void)[] = [] listWorktreesMock.mockImplementation( () => new Promise((resolve) => { resolvers.push(resolve as (worktrees: GitWorktreeInfo[]) => void) }) ) const result = [ { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ] const staleList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() notifyWorktreesChanged(mainWindow as never, 'repo-1') const replacementList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() resolvers[0](result) await staleList expect(__getDetectedWorktreeScanCacheStatsForTests()).toEqual({ cacheSize: 0, inFlightSize: 1 }) resolvers[1](result) await replacementList expect(__getDetectedWorktreeScanCacheStatsForTests()).toEqual({ cacheSize: 1, inFlightSize: 0 }) }) it('does not let an older scan overwrite a replacement that settles first', async () => { const resolvers: ((worktrees: GitWorktreeInfo[]) => void)[] = [] listWorktreesMock.mockImplementation( () => new Promise((resolve) => { resolvers.push(resolve as (worktrees: GitWorktreeInfo[]) => void) }) ) store.getAllWorktreeLineage.mockReturnValue({ 'repo-1::/workspace/fresh-worktree': { worktreeId: 'repo-1::/workspace/fresh-worktree', worktreeInstanceId: 'child-instance', parentWorktreeId: 'repo-1::/workspace/repo', parentWorktreeInstanceId: 'parent-instance', origin: 'manual', capture: { source: 'manual-action', confidence: 'explicit' }, createdAt: 0 } }) const mainWorktree: GitWorktreeInfo = { path: '/workspace/repo', head: 'main-head', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } const staleList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() notifyWorktreesChanged(mainWindow as never, 'repo-1') const replacementList = handlers['worktrees:listDetected'](null, { repoId: 'repo-1' }) await Promise.resolve() resolvers[1]([ mainWorktree, { path: '/workspace/fresh-worktree', head: 'fresh-head', branch: 'refs/heads/fresh-worktree', isBare: false, isMainWorktree: false } ]) await replacementList resolvers[0]([ mainWorktree, { path: '/workspace/stale-worktree', head: 'stale-head', branch: 'refs/heads/stale-worktree', isBare: false, isMainWorktree: false } ]) await staleList const cached = (await handlers['worktrees:listDetected'](null, { repoId: 'repo-1' })) as { worktrees: Worktree[] } expect(cached.worktrees.map((worktree) => worktree.path)).toEqual([ '/workspace/repo', '/workspace/fresh-worktree' ]) expect(store.removeWorktreeLineage).not.toHaveBeenCalled() await expect( resolveRegisteredWorktreePath('/workspace/fresh-worktree', store as never) ).resolves.toBe(resolve('/workspace/fresh-worktree')) await expect( resolveRegisteredWorktreePath('/workspace/stale-worktree', store as never) ).rejects.toThrow('Access denied: unknown repository or worktree path') expect(listWorktreesMock).toHaveBeenCalledTimes(2) expect(__getDetectedWorktreeScanCacheStatsForTests()).toEqual({ cacheSize: 1, inFlightSize: 0 }) }) it('fetches the same-repo PR head via the SSH tracking-ref RPC, not git.exec', async () => { const fetchRemoteTrackingRef = vi.fn(async () => {}) const exec = vi.fn(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse') { return { stdout: 'def456\n', stderr: '' } } return { stdout: '', stderr: '' } }) getSshGitProviderMock.mockReturnValue({ exec, fetchRemoteTrackingRef }) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 42, headRefName: 'feature/add-feature', isCrossRepository: false }) expect(fetchRemoteTrackingRef).toHaveBeenCalledWith( '/workspace/repo', 'origin', 'feature/add-feature', 'refs/remotes/origin/feature/add-feature' ) expect(exec).not.toHaveBeenCalledWith(expect.arrayContaining(['fetch']), expect.anything()) expect(result).toMatchObject({ baseBranch: 'def456', headSha: 'def456', branchNameOverride: 'feature/add-feature', pushTarget: { remoteName: 'origin', branchName: 'feature/add-feature' } }) }) it('fetches a fork PR head via the SSH pull-head RPC, not git.exec', async () => { const durableLocalRef = `refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/42` const fetchGitHubPullRequestHead = vi.fn(async () => durableLocalRef) const exec = vi.fn(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args[2] === `${durableLocalRef}^{commit}`) { return { stdout: 'fork-head-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) getSshGitProviderMock.mockReturnValue({ exec, fetchGitHubPullRequestHead, fetchRemoteTrackingRef: vi.fn() }) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 42, headRefName: 'contributor/fix', isCrossRepository: true }) expect(fetchGitHubPullRequestHead).toHaveBeenCalledWith('/workspace/repo', 'origin', 42) expect(exec).not.toHaveBeenCalledWith(expect.arrayContaining(['fetch']), expect.anything()) expect(result).toMatchObject({ baseBranch: 'fork-head-sha', headSha: 'fork-head-sha', branchNameOverride: 'contributor/fix' }) }) it('fetches a fork PR head from origin, not the first remote, over SSH', async () => { const durableLocalRef = `refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/42` const fetchGitHubPullRequestHead = vi.fn(async () => durableLocalRef) // Why: `fork` is listed first, but fork PR heads live on the hosting remote (origin). const exec = vi.fn(async (args: string[]) => { if (args[0] === 'remote' && args[1] === 'get-url') { return { stdout: `git@github.com:org/${args[2] === 'origin' ? 'repo' : 'fork'}.git\n`, stderr: '' } } if (args[0] === 'remote') { return { stdout: 'fork\norigin\n', stderr: '' } } if (args[0] === 'rev-parse' && args[2] === `${durableLocalRef}^{commit}`) { return { stdout: 'fork-head-sha\n', stderr: '' } } throw new Error(`unexpected git call: ${args.join(' ')}`) }) getSshGitProviderMock.mockReturnValue({ exec, fetchGitHubPullRequestHead, fetchRemoteTrackingRef: vi.fn() }) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null }) const result = await handlers['worktrees:resolvePrBase'](null, { repoId: 'repo-1', prNumber: 42, headRefName: 'contributor/fix', isCrossRepository: true }) expect(fetchGitHubPullRequestHead).toHaveBeenCalledWith('/workspace/repo', 'origin', 42) expect(result).toMatchObject({ baseBranch: 'fork-head-sha', headSha: 'fork-head-sha', branchNameOverride: 'contributor/fix' }) }) 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] === 'remote' && args[1] === 'get-url') { return { stdout: `${ORIGIN_REMOTE_URL}\n`, stderr: '' } } 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', '--no-tags', 'origin', `+refs/pull/1849/head:refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/1849` ], { cwd: '/workspace/repo', timeout: REVIEW_HEAD_FETCH_TIMEOUT_MS } ) expect(result).toEqual({ baseBranch: 'abc123', headSha: 'abc123', branchNameOverride: 'feat/onboarding-model-choice-782' }) }) 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] === 'remote' && args[1] === 'get-url') { return { stdout: `${ORIGIN_REMOTE_URL}\n`, stderr: '' } } 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', '--no-tags', 'origin', `+refs/pull/1849/head:refs/orca/pull/${ORIGIN_HEAD_COMPONENT}/1849` ], { cwd: '/workspace/repo', timeout: REVIEW_HEAD_FETCH_TIMEOUT_MS } ) expect(result).toEqual({ baseBranch: 'abc123', headSha: 'abc123', branchNameOverride: 'feat/onboarding-model-choice-782' }) }) 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( expect.arrayContaining(['fetch', '--no-tags']), expect.anything() ) expect(result).toMatchObject({ 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).toMatchObject({ 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/repo', head: 'base123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/remote/repo-improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } 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(provider.exec).not.toHaveBeenCalledWith( ['config', '--get', 'github.user'], '/remote/repo' ) expect(provider.exec).not.toHaveBeenCalledWith( ['config', '--get', 'user.username'], '/remote/repo' ) expect(provider.listWorktrees).toHaveBeenCalledTimes(1) expect(provider.worktreeIsClean).not.toHaveBeenCalled() expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-ssh::/remote/repo-improve-dashboard', expect.objectContaining({ linkedIssue: 123, linkedPR: 456, createdWithAgent: 'codex', linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) ) expect(result).toMatchObject({ worktree: expect.objectContaining({ linkedIssue: 123, linkedPR: 456, createdWithAgent: 'codex', linkedLinearIssue: 'ENG-123', manualOrder: 123_456 }) }) }) it('returns SSH local base refresh skip status when the owning worktree is dirty', 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] === 'merge-base') { return { stdout: '', stderr: '' } } if (args[0] === 'log') { return { stdout: 'commit-a\ncommit-b\ncommit-c\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi .fn() .mockResolvedValueOnce([ { path: '/remote/repo', head: 'base123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/remote/repo-improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: false, stdout: ' M package.json\n' }), refreshLocalBaseRefForWorktreeCreate: vi.fn().mockResolvedValue(undefined) } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getSettings.mockReturnValue({ branchPrefix: 'none', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: true, workspaceDir: '/workspace' }) 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' })) as CreateWorktreeResult expect(provider.exec).toHaveBeenCalledWith( ['merge-base', '--is-ancestor', 'refs/heads/main', 'refs/remotes/origin/main'], '/remote/repo' ) expect(provider.exec).toHaveBeenCalledWith( ['log', '--format=%H', 'refs/heads/main..refs/remotes/origin/main'], '/remote/repo' ) expect(provider.worktreeIsClean).toHaveBeenCalledWith('/remote/repo', { includeUntracked: false }) expect(provider.exec).not.toHaveBeenCalledWith( ['reset', '--hard', 'refs/remotes/origin/main'], expect.any(String) ) expect(provider.refreshLocalBaseRefForWorktreeCreate).not.toHaveBeenCalled() expect(result).toEqual( expect.objectContaining({ localBaseRefRefresh: { status: 'skipped_dirty_worktree', baseRef: 'origin/main', localBranch: 'main', ownerWorktreePath: '/remote/repo' } }) ) }) it('refreshes SSH local base through the narrow relay RPC when the setting is on', 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] === 'merge-base') { return { stdout: '', stderr: '' } } if (args[0] === 'log') { return { stdout: 'commit-a\ncommit-b\n', stderr: '' } } throw new Error(`unexpected generic exec: ${args.join(' ')}`) }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi .fn() .mockResolvedValueOnce([ { path: '/remote/repo', head: 'base123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/remote/repo-improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), refreshLocalBaseRefForWorktreeCreate: vi.fn().mockResolvedValue(undefined) } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getSettings.mockReturnValue({ branchPrefix: 'none', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: true, workspaceDir: '/workspace' }) 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' })) as CreateWorktreeResult expect(provider.exec).toHaveBeenCalledWith( ['merge-base', '--is-ancestor', 'refs/heads/main', 'refs/remotes/origin/main'], '/remote/repo' ) expect(provider.exec).toHaveBeenCalledWith( ['log', '--format=%H', 'refs/heads/main..refs/remotes/origin/main'], '/remote/repo' ) expect(provider.refreshLocalBaseRefForWorktreeCreate).toHaveBeenCalledWith({ repoPath: '/remote/repo', fullRef: 'refs/heads/main', remoteTrackingRef: 'refs/remotes/origin/main', ownerWorktreePath: '/remote/repo' }) expect(provider.exec).not.toHaveBeenCalledWith( ['reset', '--hard', 'refs/remotes/origin/main'], expect.any(String) ) expect(provider.exec).not.toHaveBeenCalledWith( ['update-ref', 'refs/heads/main', 'refs/remotes/origin/main'], expect.any(String) ) expect(result).toEqual( expect.objectContaining({ localBaseRefRefresh: { status: 'updated', baseRef: 'origin/main', localBranch: 'main', ownerWorktreePath: '/remote/repo' } }) ) }) it('returns SSH local base update suggestion when a full local base ref is safely behind', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'refs/remotes/origin/main' } let registeredRoots = false const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'merge-base' || args[0] === 'log') { if (!registeredRoots) { throw new Error('Path outside authorized workspace') } return { stdout: args[0] === 'log' ? 'commit-a\ncommit-b\ncommit-c\ncommit-d\n' : '', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockImplementation(async () => { if (!registeredRoots) { throw new Error('No workspace roots registered yet') } return [ { path: '/remote/repo', head: 'base123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/remote/repo-improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ] }), worktreeIsClean: vi.fn().mockImplementation(async () => { if (!registeredRoots) { throw new Error('Path outside authorized workspace') } return { clean: true } }), refreshLocalBaseRefForWorktreeCreate: vi.fn().mockResolvedValue(undefined) } const mux = { request: vi.fn().mockImplementation(async (method: string) => { if (method === 'session.registerRoot') { registeredRoots = true } }), 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' }) expect(provider.exec).toHaveBeenCalledWith( ['merge-base', '--is-ancestor', 'refs/heads/main', 'refs/remotes/origin/main'], '/remote/repo' ) expect(provider.exec).toHaveBeenCalledWith( ['log', '--format=%H', 'refs/heads/main..refs/remotes/origin/main'], '/remote/repo' ) expect(provider.listWorktrees).toHaveBeenCalledWith('/remote/repo') expect(provider.worktreeIsClean).toHaveBeenCalledWith('/remote/repo', { includeUntracked: false }) expect(provider.refreshLocalBaseRefForWorktreeCreate).toHaveBeenCalledWith({ repoPath: '/remote/repo', fullRef: 'refs/heads/main', remoteTrackingRef: 'refs/remotes/origin/main', ownerWorktreePath: '/remote/repo', checkOnly: true }) expect(provider.exec).not.toHaveBeenCalledWith( ['reset', '--hard', 'refs/remotes/origin/main'], expect.any(String) ) expect(result).toEqual( expect.objectContaining({ localBaseRefUpdateSuggestion: { baseRef: 'origin/main', localBranch: 'main', behind: 4 } }) ) }) it('does not suggest SSH local base updates when the relay cannot refresh local refs', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'refs/remotes/origin/main' } const methodNotFound = Object.assign( new Error('Method not found: git.refreshLocalBaseRefForWorktreeCreate'), { code: -32601 } ) const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'merge-base') { return { stdout: '', stderr: '' } } if (args[0] === 'log') { return { stdout: 'commit-a\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi .fn() .mockResolvedValueOnce([ { path: '/remote/repo', head: 'base123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/remote/repo-improve-dashboard', head: 'abc123', branch: 'refs/heads/improve-dashboard', isBare: false, isMainWorktree: false } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), refreshLocalBaseRefForWorktreeCreate: vi.fn().mockRejectedValue(methodNotFound) } 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' })) as CreateWorktreeResult expect(provider.refreshLocalBaseRefForWorktreeCreate).toHaveBeenCalledWith({ repoPath: '/remote/repo', fullRef: 'refs/heads/main', remoteTrackingRef: 'refs/remotes/origin/main', ownerWorktreePath: '/remote/repo', checkOnly: true }) expect(result.localBaseRefUpdateSuggestion).toBeUndefined() }) 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' && args[1] === '--git-path') { return { stdout: '/remote/repo/.git/worktrees/repo-improve-dashboard/orca/setup-runner.sh\n', stderr: '' } } if (args[0] === 'rev-parse') { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-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/repo-improve-dashboard/orca.yaml') expect(provider.exec).toHaveBeenCalledWith( ['rev-parse', '--git-path', 'orca/setup-runner.sh'], '/remote/repo-improve-dashboard' ) expect(fsProvider.createDir).toHaveBeenCalledWith( '/remote/repo/.git/worktrees/repo-improve-dashboard/orca' ) expect(fsProvider.writeFile).toHaveBeenCalledWith( '/remote/repo/.git/worktrees/repo-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/repo-improve-dashboard/orca/setup-runner.sh', envVars: expect.objectContaining({ ORCA_ROOT_PATH: '/remote/repo', ORCA_WORKTREE_PATH: '/remote/repo-improve-dashboard' }) } }) ) }) it('keeps Windows SSH setup runners independent from the local Git Bash setting', async () => { const repo = { id: 'repo-ssh', path: 'C:\\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' && args[1] === '--git-path') { return { stdout: 'C:\\remote\\repo\\.git\\worktrees\\improve-dashboard\\orca\\setup-runner.cmd\n', stderr: '' } } if (args[0] === 'rev-parse') { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: 'C:\\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) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) store.getSettings.mockReturnValue({ branchPrefix: 'none', nestWorkspaces: false, refreshLocalBaseRefOnWorktreeCreate: false, terminalWindowsShell: 'git-bash', workspaceDir: 'C:\\workspace' }) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) getActiveMultiplexerMock.mockReturnValue({ request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() }) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) parseOrcaYamlMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } }) shouldRunSetupForCreateMock.mockReturnValue(true) resolveSetupRunnerShellMock.mockReturnValue({ family: 'posix' }) const result = await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'improve-dashboard', setupDecision: 'run' }) expect(provider.exec).toHaveBeenCalledWith( ['rev-parse', '--git-path', 'orca/setup-runner.cmd'], 'C:\\remote\\improve-dashboard' ) expect(fsProvider.writeFile).toHaveBeenCalledWith( 'C:\\remote\\repo\\.git\\worktrees\\improve-dashboard\\orca\\setup-runner.cmd', 'pnpm install' ) expect(resolveSetupRunnerShellMock).not.toHaveBeenCalled() expect(result).toEqual( expect.objectContaining({ setup: { runnerScriptPath: 'C:\\remote\\repo\\.git\\worktrees\\improve-dashboard\\orca\\setup-runner.cmd', envVars: expect.objectContaining({ ORCA_ROOT_PATH: 'C:\\remote\\repo', ORCA_WORKTREE_PATH: 'C:\\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/repo-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/repo-sparse-dashboard', expect.objectContaining({ sparseDirectories: ['apps/mobile', 'packages/shared'], baseRef: 'refs/remotes/origin/main', sparseBaseRef: 'refs/remotes/origin/main', sparsePresetId: 'preset-1' }) ) expect(result).toMatchObject({ worktree: expect.objectContaining({ isSparse: true, sparseDirectories: ['apps/mobile', 'packages/shared'], sparseBaseRef: 'refs/remotes/origin/main', sparsePresetId: 'preset-1' }) }) }) it('suffixes only the SSH worktree path when an exact PR branch checkout path exists', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'abc123' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('abc123^{commit}')) { return { stdout: 'abc123\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), removeWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi .fn() .mockResolvedValueOnce([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]) .mockResolvedValueOnce([ { path: '/remote/repo-fix-title-2', head: 'abc123', branch: 'refs/heads/feature/fix', isBare: false, isMainWorktree: false } ]) } const fsProvider = { stat: vi.fn().mockImplementation(async (pathValue: string) => { if (pathValue === '/remote/repo-fix-title') { return { size: 0, type: 'directory', mtime: 0 } } const error = new Error('missing') as Error & { code: string } error.code = 'ENOENT' throw error }), readFile: vi.fn().mockRejectedValue(new Error('missing')) } 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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'fix-title', baseBranch: 'abc123', branchNameOverride: 'feature/fix' }) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'feature/fix', '/remote/repo-fix-title-2', { checkoutExistingBranch: true } ) expect(mux.request).toHaveBeenCalledWith('session.registerRoot', { rootPath: '/remote/repo-fix-title-2' }) }) it('suffixes SSH worktree creation when the requested branch already exists on a remote', 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] === 'branch' && args.includes('feature/something')) { return { stdout: '', stderr: '' } } if (args[0] === 'for-each-ref') { return { stdout: 'refs/remotes/origin/feature/something\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/something^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/something-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), removeWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-feature-something-2', head: 'abc123', branch: 'refs/heads/feature/something-2', 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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'feature-something', branchNameOverride: 'feature/something' }) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'feature/something-2', '/remote/repo-feature-something-2', { base: 'origin/main' } ) }) it('suffixes SSH worktree creation when a slashed remote owns the requested branch', 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\nfoo/bar\n', stderr: '' } } if (args[0] === 'for-each-ref') { return { stdout: 'refs/remotes/foo/bar/feature/something\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/something^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/something-2^{commit}')) { throw new Error('missing local branch') } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), removeWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-feature-something-2', head: 'abc123', branch: 'refs/heads/feature/something-2', 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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'feature-something', branchNameOverride: 'feature/something' }) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'feature/something-2', '/remote/repo-feature-something-2', { base: 'origin/main' } ) }) it('unsets SSH branch base config before removing a sparse worktree after setup failure', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } const setupError = new Error('sparse init failed') const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'sparse-checkout' && args[1] === 'init') { throw setupError } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), removeWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn() } const mux = { request: vi.fn().mockResolvedValue(undefined), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) store.getSparsePresets.mockReturnValue([]) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) await expect( handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'sparse-dashboard', sparseCheckout: { directories: ['apps/mobile'] } }) ).rejects.toThrow('sparse init failed') expect(provider.exec).toHaveBeenCalledWith( ['config', '--local', '--unset-all', 'branch.sparse-dashboard.base'], '/remote/repo-sparse-dashboard' ) expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/repo-sparse-dashboard', true, { deleteBranch: true, forceBranchDelete: true }) }) it('keeps an explicit SSH base strict when refresh fails and no local base ref exists', 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', baseBranch: 'origin/master' }) ).rejects.toThrow( 'Could not refresh base ref "origin/master" from "origin". Check your network and try again.' ) expect(provider.addWorktree).not.toHaveBeenCalled() expect(resolveDefaultBaseRefViaExecMock).not.toHaveBeenCalled() expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'master', 'refs/remotes/origin/master', { skipAutoMaintenance: true } ) }) it('creates an SSH worktree from the detected default base when the persisted base is stale', async () => { // Regression: a stale persisted repo base must fall back to the detected primary default instead of blocking creation. const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/master' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: `${'a'.repeat(40)}\n`, stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValueOnce([ { path: '/remote/repo-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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'improve-dashboard' }) expect(resolveDefaultBaseRefViaExecMock).toHaveBeenCalled() expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main', { skipAutoMaintenance: true } ) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'improve-dashboard', '/remote/repo-improve-dashboard', { base: 'origin/main' } ) }) it('keeps a usable SSH persisted local branch base after registering the repo root', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'develop' } let repoRootRegistered = false const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'config') { return { stdout: '', stderr: '' } } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { return { stdout: repoRootRegistered ? 'develop-sha\n' : '', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-local-branch-base', head: 'develop-sha', branch: 'refs/heads/local-branch-base', isBare: false, isMainWorktree: false } ]) } const mux = { request: vi .fn() .mockImplementation(async (_method: string, payload: { rootPath: string }) => { if (payload.rootPath === repo.path) { repoRootRegistered = true } }), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'local-branch-base' }) expect(mux.request).toHaveBeenCalledWith('session.registerRoot', { rootPath: repo.path }) expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalled() expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'local-branch-base', '/remote/repo-local-branch-base', { base: 'develop' } ) }) it('keeps a usable SSH slash-named local branch base that matches a remote prefix', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'team/feature' } let repoRootRegistered = false const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'config') { return { stdout: '', stderr: '' } } if (args[0] === 'remote') { return { stdout: 'team\norigin\n', stderr: '' } } if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main')) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { return { stdout: repoRootRegistered ? 'team-feature-sha\n' : '', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-slash-local-base', head: 'team-feature-sha', branch: 'refs/heads/slash-local-base', isBare: false, isMainWorktree: false } ]) } const mux = { request: vi .fn() .mockImplementation(async (_method: string, payload: { rootPath: string }) => { if (payload.rootPath === repo.path) { repoRootRegistered = true } }), 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: 'slash-local-base' })) as CreateWorktreeResult expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalledWith( '/remote/repo', 'team', 'feature', 'refs/remotes/team/feature' ) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'slash-local-base', '/remote/repo-slash-local-base', { base: 'team/feature' } ) expect(result.baseFallback).toEqual({ requestedRef: 'team/feature', localRef: 'team/feature' }) }) it('reuses a fresh SSH remote-tracking base refresh for repeated creates', 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() .mockResolvedValueOnce([ { path: '/remote/repo-first-worktree', head: 'abc123', branch: 'refs/heads/first-worktree', isBare: false, isMainWorktree: false } ]) .mockResolvedValueOnce([ { path: '/remote/repo-second-worktree', head: 'def456', branch: 'refs/heads/second-worktree', 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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'first-worktree' }) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'second-worktree' }) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledWith( '/remote/repo', 'origin', 'main', 'refs/remotes/origin/main', { skipAutoMaintenance: true } ) expect(provider.addWorktree).toHaveBeenCalledTimes(2) }) it('skips broad SSH remote fetch for an existing commit SHA base', async () => { const sha = 'c'.repeat(40) const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'for-each-ref') { return { stdout: '', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/heads/feature/fix^{commit}')) { throw new Error('missing local branch') } if (args[0] === 'rev-parse' && args.includes(`${sha}^{commit}`)) { return { stdout: `${sha}\n`, stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-fix-title', head: sha, branch: 'refs/heads/feature/fix', 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) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'fix-title', baseBranch: sha, branchNameOverride: 'feature/fix' }) expect(provider.exec).not.toHaveBeenCalledWith(['fetch', 'origin'], '/remote/repo') expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalled() expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'feature/fix', '/remote/repo-fix-title', { base: sha } ) }) it('shares an in-flight SSH create-base prefetch with create', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } let resolveFetch!: () => void const pendingFetch = new Promise((resolve) => { resolveFetch = resolve }) const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockReturnValue(pendingFetch), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-prefetched-worktree', head: 'abc123', branch: 'refs/heads/prefetched-worktree', 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 prefetch = handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-ssh' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) const create = handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'prefetched-worktree' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) expect(provider.addWorktree).not.toHaveBeenCalled() resolveFetch() await prefetch await create expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) expect(provider.addWorktree).toHaveBeenCalledTimes(1) }) it('registers the SSH repo root before create-base prefetch probes refs', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/master' } const registeredRoots = new Set() const events: string[] = [] const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { events.push(`exec:${args[0]}:${registeredRoots.has('/remote/repo')}`) if (!registeredRoots.has('/remote/repo')) { throw new Error('root not registered') } if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/master^{commit}')) { throw new Error('missing stale base') } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { return { stdout: 'main-sha\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-prefetched-worktree', head: 'abc123', branch: 'refs/heads/prefetched-worktree', isBare: false, isMainWorktree: false } ]) } const mux = { request: vi .fn() .mockImplementation(async (_method: string, payload: { rootPath: string }) => { events.push(`register:${payload.rootPath}`) registeredRoots.add(payload.rootPath) }), notify: vi.fn() } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue(mux) store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-ssh' }) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'prefetched-worktree' }) expect(events[0]).toBe('register:/remote/repo') expect(events).not.toContain('exec:symbolic-ref:false') expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'prefetched-worktree', '/remote/repo-prefetched-worktree', { base: 'origin/main' } ) }) it('does not let SSH prefetch turn a persisted slash-named local branch into a remote-tracking base', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'team/feature' } const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'symbolic-ref') { return { stdout: 'refs/remotes/origin/main\n', stderr: '' } } if (args[0] === 'remote') { return { stdout: 'team\norigin\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main')) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { throw new Error('missing remote-tracking ref') } if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { return { stdout: 'team-feature-sha\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-slash-local-base', head: 'team-feature-sha', branch: 'refs/heads/slash-local-base', 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) await handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-ssh' }) await handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'slash-local-base' }) expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalledWith( '/remote/repo', 'team', 'feature', 'refs/remotes/team/feature' ) expect(provider.addWorktree).toHaveBeenCalledWith( '/remote/repo', 'slash-local-base', '/remote/repo-slash-local-base', { base: 'team/feature' } ) }) it('shares in-flight SSH create-base resolution with create', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } let resolveRemoteList!: () => void const pendingRemoteList = new Promise<{ stdout: string; stderr: string }>((resolve) => { resolveRemoteList = () => resolve({ stdout: 'origin\n', stderr: '' }) }) const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return pendingRemoteList } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-prefetched-worktree', head: 'abc123', branch: 'refs/heads/prefetched-worktree', 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 prefetch = handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-ssh' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.exec.mock.calls.filter(([args]) => args[0] === 'remote')).toHaveLength(1) const create = handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'prefetched-worktree' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.exec.mock.calls.filter(([args]) => args[0] === 'remote')).toHaveLength(1) expect(provider.fetchRemoteTrackingRef).not.toHaveBeenCalled() expect(provider.addWorktree).not.toHaveBeenCalled() resolveRemoteList() await prefetch await create expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) expect(provider.addWorktree).toHaveBeenCalledTimes(1) }) it('queues different SSH create-base fetch shapes on the same remote', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: 'origin/main' } let resolveExactFetch!: () => void const pendingExactFetch = new Promise((resolve) => { resolveExactFetch = resolve }) const provider = { exec: vi.fn().mockImplementation(async (args: string[]) => { if (args[0] === 'remote') { return { stdout: 'origin\n', stderr: '' } } return { stdout: '', stderr: '' } }), fetchRemoteTrackingRef: vi.fn().mockReturnValue(pendingExactFetch), addWorktree: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo-local-base-worktree', head: 'abc123', branch: 'refs/heads/local-base-worktree', 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 prefetch = handlers['worktrees:prefetchCreateBase'](null, { repoId: 'repo-ssh' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.fetchRemoteTrackingRef).toHaveBeenCalledTimes(1) const create = handlers['worktrees:create'](null, { repoId: 'repo-ssh', name: 'local-base-worktree', baseBranch: 'local-base' }) as Promise await new Promise((resolve) => setTimeout(resolve, 0)) expect(provider.exec.mock.calls.filter(([args]) => args[0] === 'fetch')).toHaveLength(0) expect(provider.addWorktree).not.toHaveBeenCalled() resolveExactFetch() await vi.waitFor(() => expect(provider.exec.mock.calls.filter(([args]) => args[0] === 'fetch')).toHaveLength(1) ) await prefetch await create expect(provider.addWorktree).toHaveBeenCalledTimes(1) }) 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.mockImplementation(async (args: string[]) => { if ( args[0] === 'rev-parse' && (args.includes('refs/remotes/origin/master^{commit}') || args.includes('refs/heads/origin/master^{commit}')) ) { throw new Error('missing ref') } return { 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) as CreateWorktreeResult expect(addWorktreeMock).toHaveBeenCalled() expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard') expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ baseRef: 'refs/remotes/origin/main' }) ) }) it('creates from the detected default base when the persisted base is stale', async () => { // Regression: a stale persisted repo base must fall back to the detected primary default instead of blocking creation. const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: 'origin/master' }) runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => baseBranch === 'origin/main' ? remoteBase : null ) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: true, errorKind: 'git_error' }) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'created-sha', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if ( args[0] === 'rev-parse' && (args.includes('refs/remotes/origin/master^{commit}') || args.includes('refs/heads/origin/master^{commit}')) ) { throw new Error('missing ref') } return { stdout: 'created-sha\n', stderr: '' } }) const result = (await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' })) as CreateWorktreeResult expect(addWorktreeMock).toHaveBeenCalled() expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( '/workspace/repo', 'origin/master' ) expect(runtimeStub.resolveRemoteTrackingBase).toHaveBeenCalledWith( '/workspace/repo', 'origin/main' ) expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( '/workspace/repo', remoteBase ) expect(result.worktree.id).toBe('repo-1::/workspace/improve-dashboard') }) it('keeps a usable persisted local branch base when a detected default exists', async () => { store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: 'develop' }) runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(null) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: false, errorKind: 'git_error' }) listWorktreesMock.mockResolvedValue([ { path: '/workspace/improve-dashboard', head: 'created-sha', branch: 'improve-dashboard', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes('refs/heads/develop^{commit}')) { return { stdout: 'develop-sha\n', stderr: '' } } if (args[0] === 'fetch') { throw new Error('network unavailable') } return { stdout: 'created-sha\n', stderr: '' } }) await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' }) ).resolves.toEqual(expect.objectContaining({ worktree: expect.any(Object) })) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', 'develop', false ) }) it('keeps a usable persisted slash-named local branch base that matches a remote prefix', async () => { const remoteBase = { remote: 'team', branch: 'feature', ref: 'refs/remotes/team/feature', base: 'team/feature' } store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: 'team/feature' }) runtimeStub.resolveRemoteTrackingBase.mockImplementation(async (_repoPath, baseBranch) => baseBranch === 'team/feature' ? remoteBase : null ) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) listWorktreesMock.mockResolvedValue([ { path: '/workspace/slash-local-base', head: 'created-sha', branch: 'slash-local-base', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes('refs/remotes/team/feature^{commit}')) { throw new Error('missing remote-tracking ref') } if (args[0] === 'rev-parse' && args.includes('refs/heads/team/feature^{commit}')) { return { stdout: 'team-feature-sha\n', stderr: '' } } if (args[0] === 'fetch') { throw new Error('network unavailable') } return { stdout: 'created-sha\n', stderr: '' } }) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'slash-local-base' }) expect(result).toEqual(expect.objectContaining({ worktree: expect.any(Object) })) expect((result as CreateWorktreeResult).baseFallback).toEqual({ requestedRef: 'team/feature', localRef: 'team/feature' }) expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/slash-local-base', 'slash-local-base', 'team/feature', false ) }) it('uses a local branch when its missing remote-tracking base cannot refresh', async () => { const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) listWorktreesMock.mockResolvedValue([ { path: '/workspace/offline-local-main', head: 'created-sha', branch: 'offline-local-main', isBare: false, isMainWorktree: false } ]) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/main^{commit}')) { throw new Error('missing remote-tracking ref') } if (args[0] === 'rev-parse' && args.includes('refs/heads/main^{commit}')) { return { stdout: 'main-sha\n', stderr: '' } } if (args[0] === 'rev-parse') { return { stdout: '', stderr: '' } } return { stdout: 'created-sha\n', stderr: '' } }) const result = await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'offline-local-main', baseBranch: 'origin/main' }) expect(result).toEqual(expect.objectContaining({ worktree: expect.any(Object) })) expect((result as CreateWorktreeResult).baseFallback).toEqual({ requestedRef: 'origin/main', localRef: 'main' }) expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).not.toHaveBeenCalled() expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/offline-local-main', 'offline-local-main', 'main', false ) }) it('keeps an explicit base strict when the pre-create refresh fails', async () => { const remoteBase = { remote: 'origin', branch: 'master', ref: 'refs/remotes/origin/master', base: 'origin/master' } runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(false) runtimeStub.getOrStartRemoteTrackingBaseRefresh.mockResolvedValue({ ok: false, errorKind: 'git_error' }) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: 'origin/main' }) await expect( handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard', baseBranch: 'origin/master' }) ).rejects.toThrow( 'Could not refresh base ref "origin/master" from "origin". Check your network and try again.' ) expect(addWorktreeMock).not.toHaveBeenCalled() expect(resolveDefaultBaseRefViaExecMock).not.toHaveBeenCalled() }) it('delegates remote-tracking base freshness to the runtime before create', 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' })) as CreateWorktreeResult expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).toHaveBeenCalledWith( '/workspace/repo', remoteBase ) expect(result).toEqual( expect.objectContaining({ worktree: expect.objectContaining({ id: 'repo-1::/workspace/improve-dashboard' }) }) ) }) it('threads the local base update suggestion from local create results', async () => { const remoteBase = { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } runtimeStub.resolveRemoteTrackingBase.mockResolvedValue(remoteBase) runtimeStub.hasRemoteTrackingRef.mockResolvedValue(true) addWorktreeMock.mockResolvedValue({ localBaseRefUpdateSuggestion: { baseRef: 'origin/main', localBranch: 'main', behind: 2 } }) 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' })) as CreateWorktreeResult expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', 'origin/main', false, false, { suggestLocalBaseRefUpdate: true, remoteTrackingBase: { remote: 'origin', branch: 'main', ref: 'refs/remotes/origin/main', base: 'origin/main' } } ) expect(store.setWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/workspace/improve-dashboard', expect.objectContaining({ baseRef: 'refs/remotes/origin/main' }) ) expect(result.localBaseRefUpdateSuggestion).toEqual({ baseRef: 'origin/main', localBranch: 'main', behind: 2 }) }) it('throws a clear error when no default base ref can be resolved', async () => { // Why: guard against regressing to a silent 'origin/main' fallback; an unresolved default base must fail loudly, not hand a non-existent ref to `git worktree add`. resolveDefaultBaseRefWithLocalGitMock.mockResolvedValue(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"', {}, // Why: issue runners take the resolved setup shell; it is undefined off Windows. undefined ) expect(result).toMatchObject({ 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 () => { const rootWorktreeId = 'repo-1::/workspace/folder' const priorWorktreeIds = ['repo-1::/workspace/old-folder'] const rootMeta = makeWorktreeMeta({ instanceId: 'folder-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', priorWorktreeIds }) 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.getAllWorktreeMeta.mockReturnValue({ [rootWorktreeId]: rootMeta }) store.getWorktreeMeta.mockImplementation((worktreeId: string) => worktreeId === rootWorktreeId ? rootMeta : undefined ) const listed = await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(listed).toEqual([ expect.objectContaining({ id: rootWorktreeId, repoId: 'repo-1', path: '/workspace/folder', displayName: 'folder', branch: '', head: '', isMainWorktree: true, priorWorktreeIds }) ]) 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).toHaveBeenCalledWith('repo-ssh::/remote/feature-wt', { projectId: 'repo:repo-ssh', hostId: 'ssh:conn-1', projectHostSetupId: 'repo-ssh' }) }) 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 on disk with no persisted WorktreeMeta would otherwise fall back to lastActivityAt: 0 and rank dead last in Recent. listWorktreesMock.mockResolvedValue([ { path: '/workspace/discovered-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue(undefined) const stampedMeta = { projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', 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), projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) ) expect(listed[0]).toMatchObject({ id: 'repo-1::/workspace/discovered-wt', lastActivityAt: 1_700_000_000_000 }) }) it('backfills project-host ownership without re-stamping lastActivityAt for existing meta', async () => { // Why: only first discovery stamps (re-stamping would reshuffle the sidebar); host ownership is still backfilled since it derives from repo setup. 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 }) store.setWorktreeMeta.mockReturnValue({ instanceId: 'existing-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', lastActivityAt: 42 }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as { id: string lastActivityAt: number projectId?: string hostId?: string projectHostSetupId?: string }[] expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/existing-wt', { projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) expect(listed[0].lastActivityAt).toBe(42) expect(listed[0]).toMatchObject({ projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) }) it('repairs legacy project ids when discovery now resolves the same host setup to a logical project', async () => { // Why: provider identity can arrive after metadata was written; existing workspaces must move to the logical project ID without losing activity ordering. listWorktreesMock.mockResolvedValue([ { path: '/workspace/existing-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getProjectHostSetups.mockReturnValue([ { id: 'repo-1', projectId: 'github:stablyai/orca', hostId: 'local', repoId: 'repo-1', path: '/workspace/repo', displayName: 'repo', setupState: 'ready', setupMethod: 'legacy-repo', createdAt: 0, updatedAt: 0 } ]) store.getWorktreeMeta.mockReturnValue({ displayName: '', comment: '', linkedIssue: null, linkedPR: null, instanceId: 'existing-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 42 }) store.setWorktreeMeta.mockReturnValue({ instanceId: 'existing-instance', projectId: 'github:stablyai/orca', hostId: 'local', projectHostSetupId: 'repo-1', lastActivityAt: 42 }) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-1' })) as { id: string lastActivityAt: number projectId?: string hostId?: string projectHostSetupId?: string }[] expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/existing-wt', { projectId: 'github:stablyai/orca' }) expect(listed[0]).toMatchObject({ id: 'repo-1::/workspace/existing-wt', projectId: 'github:stablyai/orca', hostId: 'local', projectHostSetupId: 'repo-1', lastActivityAt: 42 }) }) it('does not repair ownership when discovery points at a different project-host setup', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/existing-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getProjectHostSetups.mockReturnValue([ { id: 'repo-1', projectId: 'github:stablyai/orca', hostId: 'local', repoId: 'repo-1', path: '/workspace/repo', displayName: 'repo', setupState: 'ready', setupMethod: 'legacy-repo', createdAt: 0, updatedAt: 0 } ]) store.getWorktreeMeta.mockReturnValue({ displayName: '', comment: '', linkedIssue: null, linkedPR: null, instanceId: 'existing-instance', projectId: 'github:other/project', hostId: 'ssh:ssh-target-1', projectHostSetupId: 'repo-other-host', isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 42 }) await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() }) it('repairs legacy project ids when SSH worktree listing falls back to persisted metadata', async () => { const repo = { id: 'repo-ssh', path: '/remote/orca', displayName: 'orca', badgeColor: '#000', addedAt: 0, connectionId: 'ssh-target-1' } store.getRepo.mockReturnValue(repo) store.getAllWorktreeMeta.mockReturnValue({ 'repo-ssh::/remote/orca': makeWorktreeMeta({ instanceId: 'existing-instance', projectId: 'repo:repo-ssh', hostId: 'ssh:ssh-target-1', projectHostSetupId: 'repo-ssh', lastActivityAt: 42 }) }) store.getProjectHostSetups.mockReturnValue([ { id: 'repo-ssh', projectId: 'github:stablyai/orca', hostId: 'ssh:ssh-target-1', repoId: 'repo-ssh', path: '/remote/orca', displayName: 'orca', setupState: 'ready', setupMethod: 'imported-existing-folder', createdAt: 0, updatedAt: 0 } ]) store.setWorktreeMeta.mockReturnValue( makeWorktreeMeta({ instanceId: 'existing-instance', projectId: 'github:stablyai/orca', hostId: 'ssh:ssh-target-1', projectHostSetupId: 'repo-ssh', lastActivityAt: 42 }) ) const listed = (await handlers['worktrees:list'](null, { repoId: 'repo-ssh' })) as { id: string projectId?: string hostId?: string projectHostSetupId?: string lastActivityAt: number }[] expect(getSshGitProviderMock).toHaveBeenCalledWith('ssh-target-1') expect(store.setWorktreeMeta).toHaveBeenCalledWith('repo-ssh::/remote/orca', { projectId: 'github:stablyai/orca' }) expect(listed).toEqual([ expect.objectContaining({ id: 'repo-ssh::/remote/orca', projectId: 'github:stablyai/orca', hostId: 'ssh:ssh-target-1', projectHostSetupId: 'repo-ssh', lastActivityAt: 42 }) ]) }) it('does not rewrite discovery metadata when instance and project-host ownership already exist', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/existing-wt', head: 'abc123', branch: 'refs/heads/feature', isBare: false, isMainWorktree: false } ]) store.getWorktreeMeta.mockReturnValue({ instanceId: 'existing-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', displayName: '', comment: '', linkedIssue: null, linkedPR: null, isArchived: false, isUnread: false, isPinned: false, sortOrder: 0, lastActivityAt: 42 }) await handlers['worktrees:list'](null, { repoId: 'repo-1' }) expect(store.setWorktreeMeta).not.toHaveBeenCalled() }) 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', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', 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), projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) ) expect(listed[0]).toMatchObject({ instanceId: 'new-instance', projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) }) it('stamps lastActivityAt on first discovery for folder-mode repos', async () => { // Why: folder repos produce a synthetic worktree; without the stamp a just-added folder sorts to the bottom of Recent. 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({ projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1', 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), projectId: 'repo:repo-1', hostId: 'local', projectHostSetupId: 'repo-1' }) ) }) it('stamps lastActivityAt on first discovery via worktrees:listAll', async () => { // Why: stamping logic is duplicated in worktrees:list and worktrees:listAll; a listAll regression would silently bury newly-discovered worktrees. 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('omits prunable worktrees from worktrees:listAll', async () => { // Why: a prunable registration has no working directory (issue #8389), so surfacing it yields repeated pty/fs failures and a blank pane. listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'abc123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true }, { path: '/workspace/stale-wt', head: 'def456', branch: 'refs/heads/stale', isBare: false, prunable: true, prunableReason: 'gitdir file points to non-existent location', isMainWorktree: false }, { path: '/workspace/live-wt', head: 'fed789', branch: 'refs/heads/live', 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 }[] const listedIds = listed.map((worktree) => worktree.id) expect(listedIds).toContain('repo-1::/workspace/live-wt') expect(listedIds).not.toContain('repo-1::/workspace/stale-wt') }) it('limits concurrent repo scans in worktrees:listAll while preserving order', async () => { const repos = Array.from({ length: 10 }, (_, index) => ({ id: `repo-${index}`, path: `/workspace/repo-${index}`, displayName: `repo-${index}`, badgeColor: '#000', addedAt: 0 })) store.getRepos.mockReturnValue(repos) let activeScans = 0 let maxActiveScans = 0 let notifyScanStarted: (() => void) | undefined const waitForScanCount = async (count: number): Promise => { while (listWorktreesMock.mock.calls.length < count) { await new Promise((resolve, reject) => { const timeout = setTimeout( () => reject(new Error(`Timed out waiting for ${count} scans`)), 1000 ) notifyScanStarted = () => { clearTimeout(timeout) resolve() } }) } } const pendingScans: (() => void)[] = [] listWorktreesMock.mockImplementation( async ( repoPath: string ): Promise< { path: string; head: string; branch: string; isBare: false; isMainWorktree: true }[] > => { activeScans += 1 maxActiveScans = Math.max(maxActiveScans, activeScans) await new Promise((resolve) => { pendingScans.push(resolve) notifyScanStarted?.() notifyScanStarted = undefined }) activeScans -= 1 return [ { path: repoPath, head: 'abc123', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ] } ) const listPromise = handlers['worktrees:listAll'](null, undefined) as Promise< { path: string }[] > await Promise.resolve() expect(listWorktreesMock).toHaveBeenCalledTimes(8) expect(maxActiveScans).toBe(8) for (const resolve of pendingScans.splice(0)) { resolve() } await waitForScanCount(10) expect(listWorktreesMock).toHaveBeenCalledTimes(10) for (const resolve of pendingScans.splice(0)) { resolve() } const listed = await listPromise expect(maxActiveScans).toBe(8) expect(listed.map((worktree) => worktree.path)).toEqual(repos.map((repo) => repo.path)) }) it('skips past a suffix that already belongs to a PR after an initial branch conflict', async () => { // Why: the PR-conflict probe (network-bound, 1โ€“3s) only runs from suffix=2 onward, after a branch collision already forced past the first candidate. 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).toMatchObject({ 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: guard against a refactor reintroducing the PR probe on the happy path (1โ€“3s GitHub round-trip per click). 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', undefined, undefined ) expect(result).toMatchObject({ 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('routes setup runner generation through the selected WSL project runtime', async () => { setPlatform('win32') store.getProjects.mockReturnValue([ { id: 'project-1', displayName: 'repo', badgeColor: '#000', sourceRepoIds: ['repo-1'], localWindowsRuntimePreference: { kind: 'wsl', distro: 'Ubuntu' }, createdAt: 0, updatedAt: 0 } ]) listWorktreesMock.mockResolvedValue(createdWorktreeList) getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup' } }) shouldRunSetupForCreateMock.mockReturnValue(true) 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', { wslDistro: 'Ubuntu' }, undefined ) expect(addWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/improve-dashboard', 'improve-dashboard', 'origin/main', false, false, { wslDistro: 'Ubuntu' } ) }) it('launches setup even when primary and worktree orca.yaml scripts diverge', async () => { // Why: benign orca.yaml divergence must not disable setup (regression from #1280 content-equality gate); repo trust already gates execution. listWorktreesMock.mockResolvedValue(createdWorktreeList) getEffectiveHooksMock.mockImplementation((_repo, worktreePath?: string) => ({ scripts: { setup: worktreePath ? 'pnpm worktree:setup # worktree' : 'pnpm worktree:setup' } })) getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm worktree:setup # worktree' } }) 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', undefined, undefined ) 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).toMatchObject({ 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).toMatchObject({ 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 the persisted cleanup and space snapshots on removal', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) // A removed workspace must never resurrect from the cached scan snapshots. expect(pruneCleanupScanSnapshotMock).toHaveBeenCalledWith( '/profile-a', 'repo-1::/workspace/feature-wt', 'local' ) expect(pruneSpaceAnalysisSnapshotMock).toHaveBeenCalledWith( '/profile-a', 'repo-1::/workspace/feature-wt', 'local' ) }) it('traces the removal as worktree.remove with a stage sub-span tree', async () => { const records: RedactableSpan[] = [] setActiveSink({ push: (record) => records.push(record as RedactableSpan), flush: () => {}, close: () => {} }) try { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) const parent = records.find((record) => record.name === 'worktree.remove') expect(parent).toBeDefined() expect(parent?.attributes).toMatchObject({ kind: 'worktree', 'worktree.stage': 'remove', 'worktree.path': '/workspace/feature-wt' }) const stages = records.filter((record) => record.name.startsWith('worktree.remove.')) expect(stages.map((record) => record.name)).toEqual( expect.arrayContaining([ 'worktree.remove.watcher_gate', 'worktree.remove.pty_sweep', 'worktree.remove.git_remove', 'worktree.remove.metadata_purge', 'worktree.remove.cache_invalidation' ]) ) // Stages must hang off the removal span, not float as roots, or a freeze can't be attributed. for (const stage of stages) { expect(stage.parentSpanId).toBe(parent?.spanId) expect(stage.attributes).toMatchObject({ kind: 'worktree', 'worktree.flow': 'local' }) } } finally { _resetTracerForTests() } }) it('traces a local archive hook as flow local, not remote', async () => { const records: RedactableSpan[] = [] setActiveSink({ push: (record) => records.push(record as RedactableSpan), flush: () => {}, close: () => {} }) try { mockKnownFeatureWorktree() // The archive hook block is shared by both flows, so a local repo must not land under 'remote'. getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'pnpm worktree:archive' } }) runHookMock.mockResolvedValue({ success: true, output: '' }) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) const archiveStage = records.find((record) => record.name === 'worktree.remove.archive_hook') expect(archiveStage?.attributes).toMatchObject({ kind: 'worktree', 'worktree.flow': 'local' }) } finally { _resetTracerForTests() } }) 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', 'local') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith('repo-1::/workspace/feature-wt') expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('recovers forced Windows long-path worktree removal through local deletion and prune', async () => { setPlatform('win32') const parentDir = await mkdtemp(join(tmpdir(), 'orca-ipc-long-path-')) const repoPath = join(parentDir, 'repo') const worktreePath = join(parentDir, 'feature-wt') await mkdir(worktreePath, { recursive: true }) await writeFile(join(worktreePath, 'scratch.txt'), 'delete me') const registeredWorktrees = mockKnownFeatureWorktree(worktreePath, repoPath) listWorktreesMock .mockResolvedValueOnce(registeredWorktrees) .mockResolvedValueOnce(registeredWorktrees) .mockResolvedValue([]) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) const longPathError = Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) removeWorktreeMock.mockRejectedValue(longPathError) const worktreeId = `repo-1::${worktreePath}` try { const result = await handlers['worktrees:remove'](null, { worktreeId, force: true }) expect(result).toEqual({ preservedBranch: { branchName: 'feature', head: 'feature' } }) if (ORIGINAL_PLATFORM === 'win32') { await expect(lstat(worktreePath)).rejects.toMatchObject({ code: 'ENOENT' }) } expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: '/workspace/repo' }) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('does not create a preserved-branch target when long-path recovery preserves branch by policy', async () => { setPlatform('win32') const registeredWorktrees = mockKnownFeatureWorktree() listWorktreesMock .mockResolvedValueOnce(registeredWorktrees) .mockResolvedValueOnce(registeredWorktrees) .mockResolvedValue([]) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ preserveBranchOnDelete: true })) removeWorktreeMock.mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) ) const result = await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) expect(result).toEqual({}) await expect( handlers['worktrees:forceDeletePreservedBranch'](null, { worktreeId: 'repo-1::/workspace/feature-wt', branchName: 'feature', expectedHead: 'feature' }) ).rejects.toThrow('No preserved branch cleanup is pending') }) it('does not recover Windows long-path worktree removal without force', async () => { setPlatform('win32') mockKnownFeatureWorktree() const longPathError = Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) removeWorktreeMock.mockRejectedValue(longPathError) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('Failed to delete worktree at /workspace/feature-wt.') expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('refuses Windows recovery while Git still reports the row and keeps metadata', async () => { setPlatform('win32') mockKnownFeatureWorktree() store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) const removePathSpy = vi .spyOn(localWorktreeFilesystem, 'removeLocalWorktreePath') .mockResolvedValue(undefined) removeWorktreeMock.mockRejectedValue( Object.assign(new Error('git worktree remove failed'), { stderr: 'error: failed to delete deep/file.txt: Filename too long' }) ) try { await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) ).rejects.toThrow( 'Failed to force delete worktree at /workspace/feature-wt. error: failed to delete deep/file.txt: Filename too long' ) expect(removePathSpy).not.toHaveBeenCalled() expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith( ['worktree', 'prune'], expect.anything() ) expect(store.removeWorktreeMeta).not.toHaveBeenCalled() expect(mainWindow.webContents.send).not.toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) } finally { removePathSpy.mockRestore() } }) it('retries stale Git registration cleanup after prior local filesystem recovery', async () => { setPlatform('win32') const missingWorktreePath = 'C:\\workspace\\already-removed' const worktreeId = `repo-1::${missingWorktreePath}` const registeredWorktrees = mockKnownFeatureWorktree(missingWorktreePath) listWorktreesMock.mockResolvedValueOnce(registeredWorktrees).mockResolvedValue([]) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) const result = await handlers['worktrees:remove'](null, { worktreeId, force: true }) expect(result).toEqual({ preservedBranch: { branchName: 'feature', head: 'feature' } }) expect(runHookMock).not.toHaveBeenCalled() expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: '/workspace/repo' }) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') }) it('preserves a locked missing registration even with force', async () => { setPlatform('win32') const missingWorktreePath = 'C:\\workspace\\locked-already-removed' const worktreeId = `repo-1::${missingWorktreePath}` const registeredWorktrees: GitWorktreeInfo[] = [ { path: '/workspace/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: missingWorktreePath, head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false, locked: true, lockReason: 'active agent session' } ] listWorktreesMock.mockResolvedValue(registeredWorktrees) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) removeWorktreeMock.mockResolvedValue({}) await expect(handlers['worktrees:remove'](null, { worktreeId, force: true })).rejects.toThrow( 'Worktree is locked by Git. Lock reason: active agent session' ) expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) 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, resolvedWorktreeId: worktreeId, localProvider: ptyProvider, onPtyStopped: clearProviderPtyStateMock }) expect(killAllProcessesForWorktreeMock.mock.invocationCallOrder[0]).toBeLessThan( store.removeWorktreeMeta.mock.invocationCallOrder[0] ) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-folder' }) }) // Folder projects can be SSH-backed, and folder workspace ids are `repoId::path::workspace:` // โ€” reusable across hosts โ€” so the sweep must name the owning connection. it('fences an SSH folder workspace PTY sweep to the owning connection', async () => { const sshPtyProvider = { id: 'ssh-pty-provider' } as never const worktreeId = 'repo-folder::/remote/folder::workspace:child-1' store.getRepo.mockReturnValue({ id: 'repo-folder', path: '/remote/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder', connectionId: 'conn-1' }) getSshPtyProviderMock.mockReturnValue(sshPtyProvider) // One global meta key can describe the same-id local copy; the resolved repo still owns this delete. store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ hostId: 'local' })) await handlers['worktrees:remove'](null, { worktreeId }) expect(getSshPtyProviderMock).toHaveBeenCalledWith('conn-1') expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, { runtime: runtimeStub, resolvedWorktreeId: worktreeId, resolvedConnectionId: 'conn-1', localProvider: sshPtyProvider, onPtyStopped: clearProviderPtyStateMock, includeProviderInventory: true, includeLocalRegistry: false }) }) it('fences a mirrored runtime folder workspace sweep to its environment', async () => { const runtimePtyProvider = {} as never const worktreeId = 'repo-folder::/runtime/folder::workspace:child-1' store.getRepo.mockReturnValue({ id: 'repo-folder', path: '/runtime/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder', executionHostId: 'runtime:env-1' }) getLocalPtyProviderMock.mockReturnValue(runtimePtyProvider) await handlers['worktrees:remove'](null, { worktreeId, hostId: 'runtime:env-1' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, { runtime: runtimeStub, resolvedWorktreeId: worktreeId, resolvedRuntimeEnvironmentId: 'env-1', localProvider: runtimePtyProvider, onPtyStopped: clearProviderPtyStateMock, includeProviderInventory: false, includeLocalRegistry: false }) expect(getSshPtyProviderMock).not.toHaveBeenCalled() }) 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' }), undefined, {} ) expect(removeWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-wt', false, expect.objectContaining({ knownRemovedWorktree: expect.objectContaining({ branch: 'feature', head: 'feature', path: '/workspace/feature-wt' }) }) ) }) it('passes project shared links through the IPC removal preflight and cleanup', async () => { mockKnownFeatureWorktree() loadHooksMock.mockReturnValue({ worktree: { sharedDirectories: ['node_modules'] } }) findExistingWorktreeSymlinkPathsMock.mockResolvedValue(['node_modules']) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(findExistingWorktreeSymlinkPathsMock).toHaveBeenCalledWith('/workspace/feature-wt', [ 'node_modules' ]) expect(assertWorktreeCleanForRemovalMock).toHaveBeenCalledWith('/workspace/feature-wt', false, { ignoredUntrackedPaths: ['node_modules'] }) expect(removeWorktreeLinkedPathsMock).toHaveBeenCalledWith('/workspace/feature-wt', [ 'node_modules' ]) // Why order matters: linked-path deletion is destructive, so PTYs must release every handle // before Windows or WSL filesystem cleanup starts (mirrors the runtime removal path). expect(killAllProcessesForWorktreeMock).toHaveBeenCalled() // Latest PTY sweep vs earliest deletion: a later sweep would mean handles were still open. expect(Math.max(...killAllProcessesForWorktreeMock.mock.invocationCallOrder)).toBeLessThan( Math.min(...removeWorktreeLinkedPathsMock.mock.invocationCallOrder) ) }) it('does not remove a worktree when watcher teardown cannot release it', async () => { mockKnownFeatureWorktree() store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, symlinkPaths: ['node_modules'] }) runtimeStub.closeFileWatchersForRemoval.mockRejectedValue( new Error('file watcher process did not exit after termination deadline') ) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('file watcher process did not exit after termination deadline') expect(removeWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('releases the watcher-install fence when worktree deletion fails', async () => { mockKnownFeatureWorktree() const finish = vi.fn().mockResolvedValue(undefined) runtimeStub.acquireFileWatcherRemoval.mockResolvedValueOnce({ finish }) removeWorktreeMock.mockRejectedValueOnce(new Error('delete failed')) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('delete failed') expect(finish).toHaveBeenCalledWith(false) expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) 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, expect.objectContaining({ knownRemovedWorktree: expect.objectContaining({ branch: 'feature', head: 'feature', path: '/workspace/feature-wt' }) }) ) }) 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[] = [] runtimeStub.closeFileWatchersForRemoval.mockImplementationOnce(async () => { callOrder.push('watchers') }) 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') }), worktreeIsClean: vi.fn().mockImplementation(async () => { callOrder.push('preflight') return { clean: true } }), 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(runtimeStub.closeFileWatchersForRemoval).toHaveBeenCalledWith( '/remote/feature-wt', 'conn-1' ) expect(callOrder).toEqual(['archive', 'preflight', 'watchers', 'remove']) expect(runHookMock).not.toHaveBeenCalled() }) it('runs SSH archive hooks before failing dirty non-force removal', 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') }), worktreeIsClean: vi.fn().mockImplementation(async () => { callOrder.push('preflight') return { clean: false, stdout: ' M src/file.ts\n?? scratch.txt\n' } }), 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 expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) ).rejects.toThrow('Worktree has uncommitted or untracked changes.') expect(callOrder).toEqual(['archive', 'preflight']) expect(provider.removeWorktree).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('skips SSH dirty preflight for force removal', 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), worktreeIsClean: vi.fn(), 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::/remote/feature-wt', force: true }) expect(provider.worktreeIsClean).not.toHaveBeenCalled() expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', true) }) 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), 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('uses the workspace host when duplicate repo ids exist across local and SSH', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null } const sshRepo = { ...localRepo, path: '/remote/repo', displayName: 'ssh', connectionId: 'conn-1' } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: sshRepo.path, 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } store.getRepo.mockReturnValue(localRepo) store.getRepos.mockReturnValue([localRepo, sshRepo]) getSshGitProviderMock.mockReturnValue(provider) await handlers['worktrees:remove'](null, { worktreeId: 'repo-shared::/remote/feature-wt', hostId: 'ssh:conn-1' }) expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/feature-wt', undefined) expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('tears down the remote session when an ownerless remote worktree is deleted', async () => { const sshRepo = { id: 'repo-1', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: sshRepo.path, 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), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } store.getRepo.mockReturnValue(sshRepo) store.getRepos.mockReturnValue([sshRepo]) getSshGitProviderMock.mockReturnValue(provider) // Why: no WorktreeMeta means removal cannot read the owner back; the repo is the only host source. store.getWorktreeMeta.mockReturnValue(undefined) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/remote/feature-wt' }) // The whole point: without the repo's host the ownerless row would clear the local session instead. expect(store.removeWorktreeMeta).toHaveBeenCalledWith( 'repo-1::/remote/feature-wt', 'ssh:conn-1' ) }) it('fails closed when duplicate repo ids are deleted without a host', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', connectionId: 'conn-1' } store.getRepos.mockReturnValue([localRepo, sshRepo]) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-shared::/remote/feature-wt' }) ).rejects.toThrow('Repo not found: repo-shared') expect(removeWorktreeMock).not.toHaveBeenCalled() expect(getSshGitProviderMock).not.toHaveBeenCalled() }) it('inspects hooks on the requested host when repo ids collide', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', connectionId: 'conn-1' } const fsProvider = { readFile: vi.fn().mockResolvedValue({ content: 'scripts:\n archive: remote-cleanup', isBinary: false }) } store.getRepos.mockReturnValue([localRepo, sshRepo]) getSshFilesystemProviderMock.mockReturnValue(fsProvider) parseOrcaYamlMock.mockReturnValue({ scripts: { archive: 'remote-cleanup' } }) await expect( handlers['hooks:check'](null, { repoId: 'repo-shared', hostId: 'ssh:conn-1' }) ).resolves.toEqual({ status: 'ok', hasHooks: true, hooks: { scripts: { archive: 'remote-cleanup' } }, mayNeedUpdate: false }) expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml') expect(hasHooksFileMock).not.toHaveBeenCalled() }) it('fails hook inspection closed when duplicate repo ids omit the host', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', connectionId: 'conn-1' } store.getRepos.mockReturnValue([localRepo, sshRepo]) await expect(handlers['hooks:check'](null, { repoId: 'repo-shared' })).resolves.toEqual({ status: 'error', hasHooks: false, hooks: null, mayNeedUpdate: false }) expect(getSshFilesystemProviderMock).not.toHaveBeenCalled() expect(hasHooksFileMock).not.toHaveBeenCalled() }) it('inspects setup-script imports on the requested host when repo ids collide', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', connectionId: 'conn-1' } const fsProvider = { readFile: vi.fn(async (filePath: string) => { if (filePath === '/remote/repo/.superset/config.json') { return { content: '{"setup":"remote setup"}', isBinary: false } } throw Object.assign(new Error('missing'), { code: 'ENOENT' }) }), stat: vi.fn().mockRejectedValue(Object.assign(new Error('missing'), { code: 'ENOENT' })) } store.getRepos.mockReturnValue([localRepo, sshRepo]) store.getRepo.mockReturnValue(localRepo) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await expect( handlers['hooks:inspectSetupScriptImports'](null, { repoId: 'repo-shared', hostId: 'ssh:conn-1' }) ).resolves.toContainEqual( expect.objectContaining({ provider: 'superset', setup: 'remote setup' }) ) expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/.superset/config.json') }) it('does not coalesce forget requests for the same id on different hosts', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', connectionId: 'conn-1' } store.getRepos.mockReturnValue([localRepo, sshRepo]) let finishFirst!: () => void killAllProcessesForWorktreeMock .mockImplementationOnce( () => new Promise((resolve) => { finishFirst = () => resolve({ runtimeStopped: 0, providerStopped: 0, registryStopped: 0 }) }) ) .mockResolvedValueOnce({ runtimeStopped: 0, providerStopped: 0, registryStopped: 0 }) const first = handlers['worktrees:forgetLocal'](null, { worktreeId: 'repo-shared::/same/path', hostId: 'local' }) as Promise await vi.waitFor(() => expect(killAllProcessesForWorktreeMock).toHaveBeenCalledTimes(1)) await expect( handlers['worktrees:forgetLocal'](null, { worktreeId: 'repo-shared::/same/path', hostId: 'ssh:conn-1' }) ).resolves.toEqual({}) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledTimes(2) finishFirst() await expect(first).resolves.toEqual({}) }) 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, expect.objectContaining({ deleteBranch: false, knownRemovedWorktree: expect.objectContaining({ branch: 'feature', head: 'feature', path: '/workspace/feature-wt' }) }) ) }) it('force-deletes a branch that was preserved by safe worktree removal', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'def456' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) const result = await handlers['worktrees:forceDeletePreservedBranch'](null, { worktreeId: 'repo-1::/workspace/feature-wt', branchName: 'feature/test', expectedHead: 'def456' }) expect(result).toMatchObject({ deleted: true }) expect(forceDeleteLocalBranchMock).toHaveBeenCalledWith( '/workspace/repo', 'feature/test', 'def456' ) }) it('force-deletes an SSH branch that was preserved by safe worktree removal', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const worktreeId = 'repo-ssh::/remote/feature-wt' const provider = { exec: vi.fn().mockResolvedValue({ stdout: '', stderr: '' }), forceDeletePreservedBranch: vi.fn().mockResolvedValue(undefined), listWorktrees: vi.fn().mockResolvedValue([ { path: repo.path, head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/remote/feature-wt', head: 'def456', branch: 'feature/test', isBare: false, isMainWorktree: false } ]), removeWorktree: vi.fn().mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'def456' } }), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue(provider) getActiveMultiplexerMock.mockReturnValue({ request: vi.fn(), notify: vi.fn() }) await handlers['worktrees:remove'](null, { worktreeId }) const result = await handlers['worktrees:forceDeletePreservedBranch'](null, { worktreeId, branchName: 'feature/test', expectedHead: 'def456' }) expect(result).toMatchObject({ deleted: true }) expect(provider.forceDeletePreservedBranch).toHaveBeenCalledWith( '/remote/repo', 'feature/test', 'def456' ) expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled() }) it('rejects stale preserved-branch cleanup actions with an old head', async () => { mockKnownFeatureWorktree() removeWorktreeMock.mockResolvedValue({ preservedBranch: { branchName: 'feature/test', head: 'new456' } }) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) await expect( handlers['worktrees:forceDeletePreservedBranch'](null, { worktreeId: 'repo-1::/workspace/feature-wt', branchName: 'feature/test', expectedHead: 'old123' }) ).rejects.toThrow('No preserved branch cleanup is pending') expect(forceDeleteLocalBranchMock).not.toHaveBeenCalled() }) 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('reports already-missing 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( 'Worktree is no longer registered with Git and its directory is already gone.' ) 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', 'local' ) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith( 'repo-1::/workspace/already-deleted-wt' ) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('cleans up an already-missing unregistered worktree after force recovery', async () => { const worktreeId = 'repo-1::/workspace/already-deleted-wt' mockKnownFeatureWorktree('/workspace/real-feature') await expect(handlers['worktrees:remove'](null, { worktreeId })).rejects.toThrow( 'Worktree is no longer registered with Git and its directory is already gone.' ) await handlers['worktrees:remove'](null, { worktreeId, force: true }) expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('treats normal deletion of an already-missing unregistered worktree as cleanup', async () => { mockKnownFeatureWorktree('/workspace/real-feature') store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta()) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/already-deleted-wt' }) 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', 'local' ) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith( 'repo-1::/workspace/already-deleted-wt' ) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) }) it('force-removes a legacy Orca-created orphaned worktree directory after Git tracking is gone', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-ipc-orphan-')) const repoPath = join(parentDir, 'repo') const orphanPath = join(parentDir, 'orphan') const adminWorktreePath = join(repoPath, '.git', 'worktrees', 'orphan') const worktreeId = `repo-1::${orphanPath}` await mkdir(orphanPath, { recursive: true }) await mkdir(adminWorktreePath, { recursive: true }) await writeFile(join(orphanPath, '.git'), `gitdir: ${adminWorktreePath}\n`) await writeFile(join(adminWorktreePath, 'gitdir'), `${join(orphanPath, '.git')}\n`) const repo = { id: 'repo-1', path: repoPath, displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null } store.getRepo.mockReturnValue(repo) store.getRepos.mockReturnValue([repo]) mockKnownFeatureWorktree(join(parentDir, 'real-feature'), repoPath) store.getWorktreeMeta.mockReturnValue(makeWorktreeMeta({ createdAt: Date.now() })) try { await handlers['worktrees:remove'](null, { worktreeId, force: true }) await expect(lstat(orphanPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( worktreeId, expect.objectContaining({ requirePhysicalStop: true }) ) expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('prompts for force before removing an Orca-created orphaned worktree directory', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-ipc-orphan-')) const repoPath = join(parentDir, 'repo') const orphanPath = join(parentDir, 'orphan') const adminWorktreePath = join(repoPath, '.git', 'worktrees', 'orphan') await mkdir(orphanPath, { recursive: true }) await mkdir(adminWorktreePath, { recursive: true }) await writeFile(join(orphanPath, '.git'), `gitdir: ${adminWorktreePath}\n`) await writeFile(join(adminWorktreePath, 'gitdir'), `${join(orphanPath, '.git')}\n`) const repo = { id: 'repo-1', path: repoPath, displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null } store.getRepo.mockReturnValue(repo) store.getRepos.mockReturnValue([repo]) mockKnownFeatureWorktree(join(parentDir, 'real-feature'), repoPath) store.getWorktreeMeta.mockReturnValue( makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'runtime' }) ) try { await expect( handlers['worktrees:remove'](null, { worktreeId: `repo-1::${orphanPath}` }) ).rejects.toThrow('Worktree is no longer registered with Git but its directory remains.') await expect(lstat(orphanPath)).resolves.toBeTruthy() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('prompts then force-removes an Orca-created unregistered leftover directory with no git marker', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-ipc-leftover-')) const repoPath = join(parentDir, 'repo') const leftoverPath = join(parentDir, 'leftover') const worktreeId = `repo-1::${leftoverPath}` await mkdir(leftoverPath, { recursive: true }) await writeFile(join(leftoverPath, 'leftover.txt'), 'kept until force\n') store.getRepo.mockReturnValue({ id: 'repo-1', path: repoPath, displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null }) mockKnownFeatureWorktree(join(parentDir, 'real-feature'), repoPath) store.getWorktreeMeta.mockReturnValue( makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'runtime' }) ) gitExecFileAsyncMock.mockImplementation(async (args: string[]) => { if (args[0] === 'status') { throw new Error('fatal: not a git repository') } return { stdout: '', stderr: '' } }) try { await expect(handlers['worktrees:remove'](null, { worktreeId })).rejects.toThrow( 'Worktree is no longer registered with Git but its directory remains.' ) await expect(lstat(leftoverPath)).resolves.toBeTruthy() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() await expect( handlers['worktrees:remove'](null, { worktreeId, force: true }) ).resolves.toEqual({}) await expect(lstat(leftoverPath)).rejects.toMatchObject({ code: 'ENOENT' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( worktreeId, expect.objectContaining({ requirePhysicalStop: true }) ) expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('rejects an Orca-created unregistered local directory with a git directory', async () => { const parentDir = await mkdtemp(join(tmpdir(), 'orca-ipc-standalone-')) const repoPath = join(parentDir, 'repo') const standalonePath = join(parentDir, 'standalone') await mkdir(join(standalonePath, '.git'), { recursive: true }) store.getRepo.mockReturnValue({ id: 'repo-1', path: repoPath, displayName: 'repo', badgeColor: '#000', addedAt: 0, worktreeBaseRef: null }) mockKnownFeatureWorktree(join(parentDir, 'real-feature'), repoPath) store.getWorktreeMeta.mockReturnValue( makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'runtime' }) ) try { await expect( handlers['worktrees:remove'](null, { worktreeId: `repo-1::${standalonePath}`, force: true }) ).rejects.toThrow(`Refusing to delete unregistered worktree path: ${standalonePath}`) await expect(lstat(standalonePath)).resolves.toBeTruthy() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() } finally { await rm(parentDir, { recursive: true, force: true }) } }) it('does not inspect or delete a local path when SSH orphan cleanup has no filesystem provider', async () => { const localPath = await mkdtemp(join(tmpdir(), 'orca-ipc-ssh-missing-fs-')) const repo = { id: 'repo-ssh-missing-fs', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-missing-fs' } const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } store.getRepo.mockReturnValue(repo) store.getWorktreeMeta.mockReturnValue( makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'ssh' }) ) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(undefined) try { await expect( handlers['worktrees:remove'](null, { worktreeId: `${repo.id}::${localPath}`, force: true }) ).rejects.toThrow('SSH filesystem provider unavailable') await expect(lstat(localPath)).resolves.toBeTruthy() expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() } finally { await rm(localPath, { recursive: true, force: true }) } }) it('refuses SSH orphan cleanup when remote .git is a symlink', async () => { const repo = { id: 'repo-ssh-symlink-git', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-symlink-git' } const worktreePath = '/remote/orphan' const provider = { listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', head: 'main', branch: 'refs/heads/main', isBare: false, isMainWorktree: true } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) } const fsProvider = { lstat: vi.fn().mockResolvedValue({ type: 'symlink' }), stat: vi.fn().mockResolvedValue({ type: 'directory' }), readFile: vi.fn(), deletePath: vi.fn() } store.getRepo.mockReturnValue(repo) store.getWorktreeMeta.mockReturnValue( makeWorktreeMeta({ orcaCreatedAt: Date.now(), orcaCreationSource: 'ssh' }) ) getSshGitProviderMock.mockReturnValue(provider) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await expect( handlers['worktrees:remove'](null, { worktreeId: `${repo.id}::${worktreePath}`, force: true }) ).rejects.toThrow(`Refusing to delete unregistered worktree path: ${worktreePath}`) expect(fsProvider.lstat).toHaveBeenCalledWith(`${worktreePath}/.git`) expect(fsProvider.readFile).not.toHaveBeenCalled() expect(fsProvider.deletePath).not.toHaveBeenCalled() }) 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', hostId: 'local', force: true }) as Promise await started await Promise.resolve() expect(removeWorktreeMock).toHaveBeenCalledTimes(1) finishRemoval() await expect(Promise.all([first, second])).resolves.toEqual([{}, {}]) 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', hostId: 'local', force: true }) ).rejects.toThrow('Worktree deletion already in progress') expect(removeWorktreeMock).toHaveBeenCalledTimes(1) finishRemoval() await expect(first).resolves.toEqual({}) }) 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(), onPtyStopped: clearProviderPtyStateMock, requirePhysicalStop: true }) ) expect(removeWorktreeMock).toHaveBeenCalled() expect(callOrder).toEqual(['preflight', 'kill', 'git']) }) // Regression: `repoId::path` ids repeat across hosts, so an SSH delete used to reach the // runtime's same-id local (or other-connection) terminals and stop them. it('fences an SSH worktree delete PTY sweep to the owning connection', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const sshPtyProvider = { id: 'ssh-pty-provider' } as never store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getSshGitProviderMock.mockReturnValue({ 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({}), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }) }) getSshPtyProviderMock.mockReturnValue(sshPtyProvider) getEffectiveHooksFromConfigMock.mockReturnValue(null) await handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith('repo-ssh::/remote/feature-wt', { runtime: runtimeStub, resolvedWorktreeId: 'repo-ssh::/remote/feature-wt', resolvedConnectionId: 'conn-1', localProvider: sshPtyProvider, onPtyStopped: clearProviderPtyStateMock, requirePhysicalStop: true, includeLocalRegistry: false }) }) // The local counterpart still identifies itself by exact id so a selector that resolves // two hosts can no longer decide which workspace loses its terminals. it('pins a local worktree delete PTY sweep to the exact worktree id', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.objectContaining({ resolvedWorktreeId: 'repo-1::/workspace/feature-wt' }) ) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.not.objectContaining({ resolvedConnectionId: expect.anything() }) ) }) // Why (#11960): the PTY gate previously had no escape hatch at all, so a // workspace with an unprovable PTY was unremovable forever. it('forwards an explicit Force Delete to the PTY gate', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true, allowUnverifiedPtyStop: true }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.objectContaining({ requirePhysicalStop: true, allowUnverifiedStop: true }) ) }) // Why (#11960): the ordinary Delete confirmation already sets force:true to skip // the dirty-file prompt. Waiving PTY-stop proof off that signal would silently // disable the gate on the primary delete path. it('keeps the PTY gate strict for a confirmed delete that only sets force', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.not.objectContaining({ allowUnverifiedStop: true }) ) }) it('keeps the PTY gate strict for a plain delete', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.not.objectContaining({ allowUnverifiedStop: true }) ) }) it('does not start Git removal when physical PTY teardown cannot be proven', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) killAllProcessesForWorktreeMock.mockRejectedValueOnce( new Error('Timed out waiting for physical PTY teardown') ) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('Timed out waiting for physical PTY teardown') expect(removeWorktreeMock).not.toHaveBeenCalled() expect(store.removeWorktreeMeta).not.toHaveBeenCalled() }) it('routes local worktree removal through the selected WSL project runtime', async () => { mockSelectedWslProjectRuntime() mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) removeWorktreeMock.mockResolvedValue({}) await handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' }) expect(assertWorktreeCleanForRemovalMock).toHaveBeenCalledWith('/workspace/feature-wt', false, { wslDistro: 'Ubuntu' }) expect(removeWorktreeMock).toHaveBeenCalledWith( '/workspace/repo', '/workspace/feature-wt', false, expect.objectContaining({ wslDistro: 'Ubuntu' }) ) }) it('surfaces selected-runtime list failures during local worktree removal', async () => { mockSelectedWslProjectRuntime() const listError = new Error('wsl git list failed') listWorktreesMock.mockRejectedValue(listError) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('wsl git list failed') expect(listWorktreesMock).toHaveBeenCalledWith('/workspace/repo', { wslDistro: 'Ubuntu' }) expect(assertWorktreeCleanForRemovalMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('fails dirty non-force deletes before PTY teardown', async () => { mockKnownFeatureWorktree() const repoWithConfiguredRegularFile = { id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, symlinkPaths: ['scratch.txt'] } store.getRepo.mockReturnValue(repoWithConfiguredRegularFile) store.getRepos.mockReturnValue([repoWithConfiguredRegularFile]) 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(findExistingWorktreeSymlinkPathsMock).toHaveBeenCalledWith('/workspace/feature-wt', [ 'scratch.txt' ]) expect(assertWorktreeCleanForRemovalMock).toHaveBeenCalledWith('/workspace/feature-wt', false) expect(runtimeStub.closeFileWatchersForRemoval).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('propagates a timed-out removal preflight before watcher teardown', async () => { mockKnownFeatureWorktree() getEffectiveHooksMock.mockReturnValue(null) assertWorktreeCleanForRemovalMock.mockRejectedValue(new Error('git timed out.')) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt' }) ).rejects.toThrow('Failed to delete worktree at /workspace/feature-wt. git timed out.') expect(runtimeStub.closeFileWatchersForRemoval).not.toHaveBeenCalled() expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('fails locked dirty-force deletes before hooks, link cleanup, or PTY teardown', async () => { listWorktreesMock.mockResolvedValue([ { path: '/workspace/repo', head: 'main', branch: 'main', isBare: false, isMainWorktree: true }, { path: '/workspace/feature-wt', head: 'feature', branch: 'feature', isBare: false, isMainWorktree: false, locked: true, lockReason: 'active agent session' } ]) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, symlinkPaths: ['node_modules'] }) getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) ).rejects.toThrow( 'Failed to force delete worktree at /workspace/feature-wt. Worktree is locked by Git.' ) expect(assertWorktreeCleanForRemovalMock).not.toHaveBeenCalled() expect(runHookMock).not.toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('rechecks a local Git lock after the archive hook before teardown', async () => { const unlockedWorktrees = mockKnownFeatureWorktree() const lockedWorktrees = unlockedWorktrees.map((worktree) => worktree.path === '/workspace/feature-wt' ? { ...worktree, locked: true, lockReason: 'locked during archive' } : worktree ) listWorktreesMock .mockResolvedValueOnce(unlockedWorktrees) .mockResolvedValueOnce(lockedWorktrees) store.getRepo.mockReturnValue({ id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, symlinkPaths: ['node_modules'] }) getEffectiveHooksMock.mockReturnValue({ scripts: { archive: 'echo archived' } }) runHookMock.mockResolvedValue({ success: true, output: '' }) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-1::/workspace/feature-wt', force: true }) ).rejects.toThrow('Worktree is locked by Git') expect(runHookMock).toHaveBeenCalled() expect(removeWorktreeLinkedPathsMock).not.toHaveBeenCalled() expect(assertWorktreeCleanForRemovalMock).not.toHaveBeenCalled() 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).toHaveBeenCalledWith( 'repo-1::/workspace/feature-wt', expect.objectContaining({ requirePhysicalStop: true }) ) expect(removeWorktreeMock).toHaveBeenCalled() expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], { cwd: '/workspace/repo' }) }) it('fails closed when the SSH PTY provider is unavailable', async () => { 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) const removeWorktree = vi.fn() getSshGitProviderMock.mockReturnValue({ listWorktrees: vi.fn().mockResolvedValue([ { path: '/remote/repo', isMainWorktree: true }, { path: '/remote/feature-wt', head: 'feature', branch: 'feature', isMainWorktree: false } ]), worktreeIsClean: vi.fn().mockResolvedValue({ clean: true }), removeWorktree }) getSshFilesystemProviderMock.mockReturnValue({ readFile: vi.fn().mockRejectedValue(new Error('missing')) }) getSshPtyProviderMock.mockReturnValue(undefined) await expect( handlers['worktrees:remove'](null, { worktreeId: 'repo-ssh::/remote/feature-wt' }) ).rejects.toThrow('PTY provider unavailable for worktree deletion') expect(removeWorktree).not.toHaveBeenCalled() expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled() }) it('keeps SSH issue-command local overrides usable when shared read fails', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const fsProvider = { readFile: vi.fn(async (filePath: string) => { if (filePath.endsWith('/.orca/issue-command')) { return { content: 'local command\n', isBinary: false } } throw new Error('shared read failed') }) } store.getRepo.mockReturnValue(repo) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await expect( handlers['hooks:readIssueCommand'](null, { repoId: 'repo-ssh' }) ).resolves.toMatchObject({ status: 'ok', localContent: 'local command', sharedContent: null, effectiveContent: 'local command', source: 'local' }) }) it('writes SSH issue-command overrides without clobbering .gitignore on read failure', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const fsProvider = { createDir: vi.fn().mockResolvedValue(undefined), readFile: vi.fn().mockRejectedValue(new Error('ssh read failed')), writeFile: vi.fn().mockResolvedValue(undefined), deletePath: vi.fn().mockResolvedValue(undefined) } store.getRepo.mockReturnValue(repo) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await expect( handlers['hooks:writeIssueCommand'](null, { repoId: 'repo-ssh', content: 'orca issue command' }) ).rejects.toThrow('ssh read failed') expect(fsProvider.writeFile).not.toHaveBeenCalled() }) it('reads an issue-command override from the requested host when repo ids collide', async () => { const localRepo = { id: 'repo-shared', path: '/local/repo', displayName: 'local', badgeColor: '#000', addedAt: 0 } const sshRepo = { ...localRepo, path: '/remote/repo', displayName: 'ssh', connectionId: 'conn-1' } const fsProvider = { readFile: vi.fn(async (filePath: string) => { if (filePath.endsWith('/.orca/issue-command')) { return { content: 'remote command\n', isBinary: false } } throw Object.assign(new Error('missing'), { code: 'ENOENT' }) }) } store.getRepos.mockReturnValue([localRepo, sshRepo]) store.getRepo.mockReturnValue(localRepo) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await expect( handlers['hooks:readIssueCommand'](null, { repoId: 'repo-shared', hostId: 'ssh:conn-1' }) ).resolves.toMatchObject({ localContent: 'remote command', effectiveContent: 'remote command', source: 'local' }) expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/.orca/issue-command') }) it('creates remote .gitignore only when it is missing while writing SSH issue commands', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } const enoent = Object.assign(new Error('missing'), { code: 'ENOENT' }) const fsProvider = { createDir: vi.fn().mockResolvedValue(undefined), readFile: vi.fn().mockRejectedValue(enoent), writeFile: vi.fn().mockResolvedValue(undefined), deletePath: vi.fn().mockResolvedValue(undefined) } store.getRepo.mockReturnValue(repo) getSshFilesystemProviderMock.mockReturnValue(fsProvider) await handlers['hooks:writeIssueCommand'](null, { repoId: 'repo-ssh', content: 'orca issue command' }) expect(fsProvider.writeFile).toHaveBeenNthCalledWith(1, '/remote/repo/.gitignore', '.orca\n') expect(fsProvider.writeFile).toHaveBeenNthCalledWith( 2, '/remote/repo/.orca/issue-command', 'orca issue command\n' ) }) it('rejects SSH issue-command writes when the remote filesystem provider is unavailable', async () => { const repo = { id: 'repo-ssh', path: '/remote/repo', displayName: 'ssh', badgeColor: '#000', addedAt: 0, connectionId: 'conn-1', worktreeBaseRef: null } store.getRepo.mockReturnValue(repo) getSshFilesystemProviderMock.mockReturnValue(null) await expect( handlers['hooks:writeIssueCommand'](null, { repoId: 'repo-ssh', content: 'orca issue command' }) ).rejects.toThrow('Remote filesystem unavailable') }) 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() }) describe('worktrees:forgetLocal', () => { it('forgets a workspace pinned to a removed SSH target without touching the provider', async () => { const repo = { id: 'repo-1', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, connectionId: 'ssh-dead', worktreeBaseRef: null } const ptyProvider = {} as never const worktreeId = 'repo-1::/workspace/feature-wt' store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) getLocalPtyProviderMock.mockReturnValue(ptyProvider) // Why: a removed/disconnected SSH target has no live provider; forgetLocal must not reach for one. getSshGitProviderMock.mockReturnValue(undefined) getSshPtyProviderMock.mockReturnValue(undefined) const result = await handlers['worktrees:forgetLocal'](null, { worktreeId }) expect(result).toEqual({}) expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, { runtime: runtimeStub, // Without the exact id the sweep resolves a selector that no longer exists and stops nothing. resolvedWorktreeId: worktreeId, resolvedConnectionId: 'ssh-dead', localProvider: ptyProvider, onPtyStopped: clearProviderPtyStateMock, includeProviderInventory: false, includeLocalRegistry: false }) expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId) // The purge must be scoped to the same owner the sweep used, or the ssh:* partition keeps this worktree's session state. expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:ssh-dead') expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId) expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-1' }) // The whole point: local-only cleanup never dispatches to the SSH provider. expect(getSshGitProviderMock).not.toHaveBeenCalled() expect(getSshFilesystemProviderMock).not.toHaveBeenCalled() expect(listWorktreesMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('sweeps a connected SSH owner through its PTY provider', async () => { const worktreeId = 'repo-gone::/workspace/feature-wt' const sshProvider = {} as never store.getRepos.mockReturnValue([]) store.getRepo.mockReturnValue(undefined) store.getWorktreeMeta.mockReturnValue({ hostId: 'ssh:ssh-live' }) getSshPtyProviderMock.mockReturnValue(sshProvider) await expect( handlers['worktrees:forgetLocal'](null, { worktreeId, hostId: 'ssh:ssh-live' }) ).resolves.toEqual({}) expect(getSshPtyProviderMock).toHaveBeenCalledWith('ssh-live') expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, { runtime: runtimeStub, resolvedWorktreeId: worktreeId, resolvedConnectionId: 'ssh-live', localProvider: sshProvider, onPtyStopped: clearProviderPtyStateMock, includeProviderInventory: true, includeLocalRegistry: false }) expect(getLocalPtyProviderMock).not.toHaveBeenCalled() }) it('forgets an ownerless workspace after its repo is already gone', async () => { const worktreeId = 'repo-gone::/workspace/feature-wt' const worktreePath = '/workspace/feature-wt' // Seed authorized roots while the owning repo still exists, then let it disappear. store.getRepos.mockReturnValue([ { id: 'repo-gone', path: '/workspace/gone', displayName: 'gone', badgeColor: '#000', addedAt: 0 } ]) registerWorktreeRootsForRepo(store as never, 'repo-gone', [worktreePath]) await expect(resolveRegisteredWorktreePath(worktreePath, store as never)).resolves.toBe( resolve(worktreePath) ) store.getRepos.mockReturnValue([]) store.getRepo.mockReturnValue(undefined) await expect( handlers['worktrees:forgetLocal'](null, { worktreeId, hostId: 'runtime:env-1' }) ).resolves.toEqual({}) // The whole point: a forgotten workspace must not stay filesystem-authorized via cached roots. await expect(resolveRegisteredWorktreePath(worktreePath, store as never)).rejects.toThrow( 'Access denied: unknown repository or worktree path' ) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'runtime:env-1') expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId) expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', { repoId: 'repo-gone' }) expect(getSshGitProviderMock).not.toHaveBeenCalled() expect(removeWorktreeMock).not.toHaveBeenCalled() }) it('purges the SSH owner partition for a hostId-less forget whose worktreeMeta row is already gone', async () => { const repo = { id: 'repo-ssh', path: '/workspace/repo', displayName: 'repo', badgeColor: '#000', addedAt: 0, executionHostId: 'ssh:ssh-live' as const, connectionId: 'ssh-live', worktreeBaseRef: null } const worktreeId = 'repo-ssh::/workspace/feature-wt' store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) // The orphan case forget-local exists for: the meta row is lost, so only the repo still records ownership. store.getWorktreeMeta.mockReturnValue(undefined) getSshPtyProviderMock.mockReturnValue(undefined) getLocalPtyProviderMock.mockReturnValue({} as never) await expect(handlers['worktrees:forgetLocal'](null, { worktreeId })).resolves.toEqual({}) // Without the resolved owner the purge resolves to [local] only and ssh:ssh-live keeps tabsByWorktree et al. forever. expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'ssh:ssh-live') }) it('scopes the purge to a local folder workspace owner', async () => { const repo = { id: 'repo-folder-child', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' as const } const worktreeId = 'repo-folder-child::/workspace/folder/child' store.getRepos.mockReturnValue([repo]) store.getRepo.mockReturnValue(repo) store.getWorktreeMeta.mockReturnValue(undefined) getLocalPtyProviderMock.mockReturnValue({} as never) await expect(handlers['worktrees:forgetLocal'](null, { worktreeId })).resolves.toEqual({}) expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId, 'local') expect(getSshPtyProviderMock).not.toHaveBeenCalled() }) it('rejects forgetting a folder project root', async () => { const repo = { id: 'repo-folder', path: '/workspace/folder', displayName: 'folder', badgeColor: '#000', addedAt: 0, kind: 'folder' as const } store.getRepo.mockReturnValue(repo) await expect( handlers['worktrees:forgetLocal'](null, { worktreeId: `${repo.id}::${repo.path}` }) ).rejects.toThrow(/project root workspace/) expect(store.removeWorktreeMeta).not.toHaveBeenCalled() expect(deleteWorktreeHistoryDirMock).not.toHaveBeenCalled() }) }) })