orca/src/main/ipc/worktrees.test.ts

8622 lines
273 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* 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, Worktree } from '../../shared/types'
import * as localWorktreeFilesystem from '../local-worktree-filesystem'
const ORIGINAL_PLATFORM = process.platform
const removeWorktreeLinkedPathsMock = 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,
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(),
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<typeof GitUsernameModule>('../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', () => ({
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', () => ({
createWorktreeLinkedPaths: vi.fn(),
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,
runHook: runHookMock,
hasHooksFile: hasHooksFileMock,
shouldRunSetupForCreate: shouldRunSetupForCreateMock
}))
vi.mock('./worktree-logic', async (importOriginal) => {
const actual = (await importOriginal()) as Record<string, unknown>
return {
...actual,
computeWorktreePath: computeWorktreePathMock,
ensurePathWithinWorkspace: ensurePathWithinWorkspaceMock
}
})
const { deleteWorktreeHistoryDirMock } = vi.hoisted(() => ({
deleteWorktreeHistoryDirMock: vi.fn()
}))
vi.mock('../terminal-history', () => ({
deleteWorktreeHistoryDir: deleteWorktreeHistoryDirMock
}))
const { advertisedUrlWatcherForgetWorktreeMock } = vi.hoisted(() => ({
advertisedUrlWatcherForgetWorktreeMock: vi.fn()
}))
vi.mock('../ports/advertised-url-watcher', () => ({
advertisedUrlWatcher: {
forgetWorktree: advertisedUrlWatcherForgetWorktreeMock
}
}))
const { killAllProcessesForWorktreeMock, clearProviderPtyStateMock, getLocalPtyProviderMock } =
vi.hoisted(() => ({
killAllProcessesForWorktreeMock: vi.fn(),
clearProviderPtyStateMock: vi.fn(),
getLocalPtyProviderMock: vi.fn()
}))
vi.mock('../runtime/worktree-teardown', () => ({
killAllProcessesForWorktree: killAllProcessesForWorktreeMock
}))
vi.mock('./pty', () => ({
clearProviderPtyState: clearProviderPtyStateMock,
getLocalPtyProvider: getLocalPtyProviderMock
}))
import {
__resetSshWorktreeCreateFetchCacheForTests,
notifyWorktreesChanged
} from './worktree-remote'
import { invalidateAuthorizedRootsCache, resolveRegisteredWorktreePath } from './filesystem-auth'
import {
__getDetectedWorktreeScanCacheStatsForTests,
__resetDetectedWorktreeScanCacheForTests,
registerWorktreeHandlers
} from './worktrees'
type HandlerMap = Record<string, (_event: unknown, args: unknown) => unknown>
describe('registerWorktreeHandlers', () => {
const handlers: HandlerMap = {}
const mainWindow = {
isDestroyed: () => false,
webContents: {
send: vi.fn()
}
}
const store = {
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(),
getAllWorktreeLineage: vi.fn(),
removeWorktreeLineage: vi.fn()
}
let runtimeStub: {
resolveRemoteTrackingBase: ReturnType<typeof vi.fn>
hasRemoteTrackingRef: ReturnType<typeof vi.fn>
getOrStartRemoteTrackingBaseRefresh: ReturnType<typeof vi.fn>
getOrStartRemoteFetch: ReturnType<typeof vi.fn>
fetchRemoteWithCache: ReturnType<typeof vi.fn>
emitWorktreeBaseStatus: ReturnType<typeof vi.fn>
recordOptimisticReconcileToken: ReturnType<typeof vi.fn>
reconcileWorktreeBaseStatus: ReturnType<typeof vi.fn>
clearOptimisticReconcileToken: ReturnType<typeof vi.fn>
resolveManagedMrBase: ReturnType<typeof vi.fn>
createTerminal: ReturnType<typeof vi.fn>
splitTerminal: ReturnType<typeof vi.fn>
notifyWorktreesChangedForRemoteClients: ReturnType<typeof vi.fn>
}
beforeEach(() => {
setPlatform(ORIGINAL_PLATFORM)
__resetSshWorktreeCreateFetchCacheForTests()
__resetDetectedWorktreeScanCacheForTests()
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,
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.getAllWorktreeLineage,
store.removeWorktreeLineage,
killAllProcessesForWorktreeMock,
clearProviderPtyStateMock,
getLocalPtyProviderMock,
deleteWorktreeHistoryDirMock,
advertisedUrlWatcherForgetWorktreeMock,
removeWorktreeLinkedPathsMock
]) {
m.mockReset()
}
killAllProcessesForWorktreeMock.mockResolvedValue({
runtimeStopped: 0,
providerStopped: 0,
registryStopped: 0
})
assertWorktreeCleanForRemovalMock.mockResolvedValue(undefined)
getLocalPtyProviderMock.mockReturnValue({} as never)
for (const key of Object.keys(handlers)) {
delete handlers[key]
}
handleMock.mockImplementation((channel, handler) => {
handlers[channel] = handler
})
const repo = {
id: 'repo-1',
path: '/workspace/repo',
displayName: 'repo',
badgeColor: '#000',
addedAt: 0
}
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue({ ...repo, worktreeBaseRef: null })
store.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({})
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 legacy git fetch fallback in
// narrow unit harnesses. Return a resolved promise so catch/then chains
// don't trip on undefined.
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
getEffectiveHooksMock.mockReturnValue(null)
getEffectiveHooksFromConfigMock.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)
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: createLocalWorktree routes `git fetch` through
// `runtime.fetchRemoteWithCache` (§3.3 Lifecycle). A minimal stub
// keeps these tests focused on create-flow semantics; the full
// cache behavior is covered by fetch-remote-cache.test.ts.
runtimeStub = {
resolveRemoteTrackingBase: vi.fn().mockResolvedValue(null),
hasRemoteTrackingRef: vi.fn().mockResolvedValue(false),
getOrStartRemoteTrackingBaseRefresh: vi.fn().mockResolvedValue({ ok: true }),
getOrStartRemoteFetch: vi.fn().mockResolvedValue({ ok: true }),
fetchRemoteWithCache: vi.fn().mockResolvedValue(undefined),
emitWorktreeBaseStatus: vi.fn(),
recordOptimisticReconcileToken: vi.fn().mockReturnValue('token-1'),
reconcileWorktreeBaseStatus: vi.fn(),
clearOptimisticReconcileToken: vi.fn(),
resolveManagedMrBase: vi.fn().mockResolvedValue({ baseBranch: 'origin/mr-branch' }),
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()
}
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('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<string, unknown> = {}): Record<string, unknown> {
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 and forcing the user back to the name picker.
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'improve-dashboard' ? 'remote' : null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard-2',
head: 'abc123',
branch: 'improve-dashboard-2',
isBare: false,
isMainWorktree: false
}
])
const result = (await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})) 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('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' },
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',
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',
'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: '/workspace/repo/.git/orca/setup-runner.sh',
envVars: {
ORCA_ROOT_PATH: '/workspace/repo',
ORCA_WORKTREE_PATH: '/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: '/workspace/repo/.git/orca/setup-runner.sh',
command: expect.stringContaining('bash /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: the reuse checkbox keeps branchNameOverride pinned to the selected
// branch while the worktree folder is named independently. The backend must
// still check out that exact branch (no -b) into the renamed folder.
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('returns the PR head push target when resolving a fork PR base', async () => {
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] === 'rev-parse') {
return { stdout: 'abc123\n', stderr: '' }
}
return { stdout: '', stderr: '' }
})
const result = await handlers['worktrees:resolvePrBase'](null, {
repoId: 'repo-1',
prNumber: 1738,
headRefName: 'prateek/fix-sidebar-agents-toggle',
isCrossRepository: true
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1738/head'], {
cwd: '/workspace/repo'
})
expect(result).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('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 () => {
store.getRepo.mockImplementation((repoId: string) => ({
id: repoId,
path: `/workspace/${repoId}`,
displayName: repoId,
badgeColor: '#000',
addedAt: 0,
worktreeBaseRef: null
}))
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}`
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('resolves a fork PR base even when push-target discovery fails', async () => {
getPullRequestPushTargetMock.mockRejectedValueOnce(new Error('lookup failed'))
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (args[0] === 'rev-parse') {
return { stdout: 'abc123\n', stderr: '' }
}
return { stdout: '', stderr: '' }
})
const result = await handlers['worktrees:resolvePrBase'](null, {
repoId: 'repo-1',
prNumber: 1849,
headRefName: 'feat/onboarding-model-choice-782',
isCrossRepository: true
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1849/head'], {
cwd: '/workspace/repo'
})
expect(result).toEqual({
baseBranch: 'abc123',
headSha: 'abc123',
branchNameOverride: 'feat/onboarding-model-choice-782'
})
})
it('falls back to refs/pull/<N>/head when branch fetch fails for a PR', async () => {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (
args[0] === 'fetch' &&
args[2] ===
'+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782'
) {
throw new Error(
'fatal: could not find remote ref refs/heads/feat/onboarding-model-choice-782'
)
}
if (args[0] === 'rev-parse') {
return { stdout: 'abc123\n', stderr: '' }
}
return { stdout: '', stderr: '' }
})
const result = await handlers['worktrees:resolvePrBase'](null, {
repoId: 'repo-1',
prNumber: 1849,
headRefName: 'feat/onboarding-model-choice-782'
})
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(
[
'fetch',
'origin',
'+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782'
],
{ cwd: '/workspace/repo' }
)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['fetch', 'origin', 'refs/pull/1849/head'], {
cwd: '/workspace/repo'
})
expect(result).toEqual({
baseBranch: 'abc123',
headSha: 'abc123',
branchNameOverride: 'feat/onboarding-model-choice-782'
})
})
it('does not fall back to refs/pull/<N>/head when branch fetch hits a network failure', async () => {
gitExecFileAsyncMock.mockImplementation(async (args: string[]) => {
if (
args[0] === 'fetch' &&
args[2] ===
'+refs/heads/feat/onboarding-model-choice-782:refs/remotes/origin/feat/onboarding-model-choice-782'
) {
throw new Error('fatal: unable to access repo: Could not resolve host: github.com')
}
return { stdout: '', stderr: '' }
})
const result = await handlers['worktrees:resolvePrBase'](null, {
repoId: 'repo-1',
prNumber: 1849,
headRefName: 'feat/onboarding-model-choice-782'
})
expect(gitExecFileAsyncMock).not.toHaveBeenCalledWith(
['fetch', 'origin', 'refs/pull/1849/head'],
expect.anything()
)
expect(result).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/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/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/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/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/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/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/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/improve-dashboard',
head: 'abc123',
branch: 'refs/heads/improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
}
const fsProvider = {
readFile: vi.fn().mockResolvedValue({
content: 'scripts:\n setup: pnpm install\n',
isBinary: false
}),
createDir: vi.fn().mockResolvedValue(undefined),
writeFile: vi.fn().mockResolvedValue(undefined)
}
const mux = {
request: vi.fn().mockResolvedValue(undefined),
notify: vi.fn()
}
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue(repo)
getSshGitProviderMock.mockReturnValue(provider)
getSshFilesystemProviderMock.mockReturnValue(fsProvider)
getActiveMultiplexerMock.mockReturnValue(mux)
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
parseOrcaYamlMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
shouldRunSetupForCreateMock.mockReturnValue(true)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-ssh',
name: 'improve-dashboard',
setupDecision: 'run'
})
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/repo/orca.yaml')
expect(fsProvider.readFile).toHaveBeenCalledWith('/remote/improve-dashboard/orca.yaml')
expect(provider.exec).toHaveBeenCalledWith(
['rev-parse', '--git-path', 'orca/setup-runner.sh'],
'/remote/improve-dashboard'
)
expect(fsProvider.createDir).toHaveBeenCalledWith(
'/remote/repo/.git/worktrees/improve-dashboard/orca'
)
expect(fsProvider.writeFile).toHaveBeenCalledWith(
'/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh',
'#!/usr/bin/env bash\nset -e\npnpm install\n'
)
expect(result).toEqual(
expect.objectContaining({
setup: {
runnerScriptPath: '/remote/repo/.git/worktrees/improve-dashboard/orca/setup-runner.sh',
envVars: expect.objectContaining({
ORCA_ROOT_PATH: '/remote/repo',
ORCA_WORKTREE_PATH: '/remote/improve-dashboard'
})
}
})
)
})
it('creates sparse checkout metadata and remote sparse config for SSH worktrees', async () => {
const repo = {
id: 'repo-ssh',
path: '/remote/repo',
displayName: 'ssh',
badgeColor: '#000',
addedAt: 0,
connectionId: 'conn-1',
worktreeBaseRef: 'origin/main'
}
const provider = {
exec: vi.fn().mockImplementation(async (args: string[]) => {
if (args[0] === 'remote') {
return { stdout: 'origin\n', stderr: '' }
}
return { stdout: '', stderr: '' }
}),
fetchRemoteTrackingRef: vi.fn().mockResolvedValue(undefined),
addWorktree: vi.fn().mockResolvedValue(undefined),
removeWorktree: vi.fn().mockResolvedValue(undefined),
listWorktrees: vi.fn().mockResolvedValue([
{
path: '/remote/sparse-dashboard',
head: 'abc123',
branch: 'refs/heads/sparse-dashboard',
isBare: false,
isSparse: true,
isMainWorktree: false
}
])
}
const mux = {
request: vi.fn().mockResolvedValue(undefined),
notify: vi.fn()
}
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue(repo)
store.getSparsePresets.mockReturnValue([
{
id: 'preset-1',
repoId: 'repo-ssh',
name: 'App',
directories: ['apps/mobile', 'packages/shared'],
createdAt: 1,
updatedAt: 1
}
])
getSshGitProviderMock.mockReturnValue(provider)
getActiveMultiplexerMock.mockReturnValue(mux)
store.setWorktreeMeta.mockImplementation((_worktreeId, meta) => meta)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-ssh',
name: 'sparse-dashboard',
sparseCheckout: {
directories: [' apps/mobile ', 'packages/shared', 'apps/mobile'],
presetId: 'preset-1'
}
})
expect(provider.addWorktree).toHaveBeenCalledWith(
'/remote/repo',
'sparse-dashboard',
'/remote/sparse-dashboard',
{ base: 'origin/main', noCheckout: true }
)
expect(provider.exec).toHaveBeenCalledWith(
['sparse-checkout', 'init', '--cone'],
'/remote/sparse-dashboard'
)
expect(provider.exec).toHaveBeenCalledWith(
['sparse-checkout', 'set', '--', 'apps/mobile', 'packages/shared'],
'/remote/sparse-dashboard'
)
expect(provider.exec).toHaveBeenCalledWith(
['checkout', 'sparse-dashboard'],
'/remote/sparse-dashboard'
)
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-ssh::/remote/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/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/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/fix-title-2',
{ checkoutExistingBranch: true }
)
expect(mux.request).toHaveBeenCalledWith('session.registerRoot', {
rootPath: '/remote/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/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/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/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/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/sparse-dashboard'
)
expect(provider.removeWorktree).toHaveBeenCalledWith('/remote/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/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/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/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/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/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)
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/slash-local-base',
{
base: '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/first-worktree',
head: 'abc123',
branch: 'refs/heads/first-worktree',
isBare: false,
isMainWorktree: false
}
])
.mockResolvedValueOnce([
{
path: '/remote/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/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/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<void>((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/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<void>
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<unknown>
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<string>()
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/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/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/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/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/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<void>
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<unknown>
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<void>((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/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<void>
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<unknown>
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<unknown>
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: '' }
})
await expect(
handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'slash-local-base'
})
).resolves.toEqual(expect.objectContaining({ worktree: expect.any(Object) }))
expect(runtimeStub.getOrStartRemoteTrackingBaseRefresh).not.toHaveBeenCalled()
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/slash-local-base',
'slash-local-base',
'team/feature',
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. When
// default-base resolution returns null (e.g. a fresh repo with no origin/HEAD,
// no origin/main, no origin/master, and no local main/master), we must
// fail loudly with a message that prompts the user to pick a base
// branch, not hand a non-existent ref to `git worktree add`.
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"',
{}
)
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 that exists on disk but has no persisted WorktreeMeta
// (e.g. a folder repo just added, or a pre-existing worktree in a
// newly-added git repo) would otherwise fall back to `lastActivityAt: 0`
// and rank dead last in the Recent sort.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/discovered-wt',
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
])
store.getWorktreeMeta.mockReturnValue(undefined)
const stampedMeta = {
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 the *first* discovery should stamp. Re-stamping on every list
// would overwrite real activity and reshuffle the sidebar on refresh. Host
// ownership can still be filled because it is derived from the 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 become available after metadata was written.
// Existing workspaces should move from repo-scoped IDs 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 that flows through the
// same list path. Without the stamp, adding a folder puts its card at the
// bottom of Recent even though the user just added it.
store.getRepos.mockReturnValue([
{
id: 'repo-1',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
}
])
store.getRepo.mockReturnValue({
id: 'repo-1',
path: '/workspace/folder',
displayName: 'folder',
badgeColor: '#000',
addedAt: 0,
kind: 'folder'
})
store.getWorktreeMeta.mockReturnValue(undefined)
store.setWorktreeMeta.mockReturnValue({
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: the stamping logic lives in both worktrees:list and worktrees:listAll.
// Without a dedicated test, a regression in the listAll loop would silently
// bury newly-discovered worktrees from the multi-repo sidebar view.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/discovered-wt',
head: 'abc123',
branch: 'refs/heads/feature',
isBare: false,
isMainWorktree: false
}
])
store.getWorktreeMeta.mockReturnValue(undefined)
store.setWorktreeMeta.mockReturnValue({ lastActivityAt: 1_700_000_000_000 })
const listed = (await handlers['worktrees:listAll'](null, undefined)) as {
id: string
lastActivityAt: number
}[]
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
'repo-1::/workspace/discovered-wt',
expect.objectContaining({ lastActivityAt: expect.any(Number) })
)
expect(listed[0]).toMatchObject({
id: 'repo-1::/workspace/discovered-wt',
lastActivityAt: 1_700_000_000_000
})
})
it('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<void> => {
while (listWorktreesMock.mock.calls.length < count) {
await new Promise<void>((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<void>((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: `gh pr list` is network-bound and previously fired on every single
// create, adding 13s to the happy path. We now only probe PR conflicts
// from suffix=2 onward — once a local/remote branch collision has already
// forced us past the first candidate and uniqueness matters enough to
// justify the GitHub round-trip. This test covers that delayed path:
// suffix=1 is a branch conflict, suffix=2 is owned by an old PR, so the
// loop lands on suffix=3.
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'improve-dashboard' ? 'remote' : null
)
getPRForBranchMock.mockImplementation(async (_repoPath: string, branch: string) =>
branch === 'improve-dashboard-2'
? {
number: 3127,
title: 'Existing PR',
state: 'merged',
url: 'https://example.com/pr/3127',
checksStatus: 'success',
updatedAt: '2026-04-01T00:00:00Z',
mergeable: 'UNKNOWN'
}
: null
)
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard-3',
head: 'abc123',
branch: 'improve-dashboard-3',
isBare: false,
isMainWorktree: false
}
])
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expect(addWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/improve-dashboard-3',
'improve-dashboard-3',
'origin/main',
false
)
expect(result).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: guards the speed optimization. If a future refactor accidentally
// reintroduces the PR probe on the first iteration, the happy path will
// silently regain a 13s GitHub round-trip per click; this test fails
// loudly instead.
listWorktreesMock.mockResolvedValue([
{
path: '/workspace/improve-dashboard',
head: 'abc123',
branch: 'improve-dashboard',
isBare: false,
isMainWorktree: false
}
])
await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard'
})
expect(getPRForBranchMock).not.toHaveBeenCalled()
})
const createdWorktreeList = [
{
path: '/workspace/improve-dashboard',
head: 'abc123',
branch: 'improve-dashboard',
isBare: false,
isMainWorktree: false
}
]
it('returns a setup launch payload when setup should run', async () => {
listWorktreesMock.mockResolvedValue(createdWorktreeList)
getEffectiveHooksMock.mockReturnValue({
scripts: {
setup: 'pnpm worktree:setup'
}
})
shouldRunSetupForCreateMock.mockReturnValue(true)
const result = await handlers['worktrees:create'](null, {
repoId: 'repo-1',
name: 'improve-dashboard',
setupDecision: 'run'
})
expect(createSetupRunnerScriptMock).toHaveBeenCalledWith(
expect.objectContaining({ id: 'repo-1' }),
'/workspace/improve-dashboard',
'pnpm worktree:setup'
)
expect(result).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' }
)
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: regression for a silent skip introduced by the #1280 content-equality
// gate. Benign divergence (whitespace, comments, or any setup edit that
// landed on the base branch but not yet in the primary checkout) must not
// disable setup — repo-level trust already gates execution.
listWorktreesMock.mockResolvedValue(createdWorktreeList)
getEffectiveHooksMock.mockImplementation((_repo, worktreePath?: string) => ({
scripts: {
setup: worktreePath ? 'pnpm worktree:setup # worktree' : 'pnpm worktree:setup'
}
}))
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'
)
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 git worktree tracking when removing an orphaned worktree', async () => {
mockKnownFeatureWorktree()
const orphanError = Object.assign(new Error('git worktree remove failed'), {
stderr: "fatal: '/workspace/feature-wt' is not a working tree"
})
removeWorktreeMock.mockRejectedValue(orphanError)
getEffectiveHooksMock.mockReturnValue(null)
gitExecFileAsyncMock.mockResolvedValue({ stdout: '', stderr: '' })
await handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt'
})
// Should have called git worktree prune to clean up stale tracking
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
expect(store.removeWorktreeMeta).toHaveBeenCalledWith('repo-1::/workspace/feature-wt')
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith('repo-1::/workspace/feature-wt')
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-1'
})
})
it('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)
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)
})
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,
localProvider: ptyProvider,
onPtyStopped: clearProviderPtyStateMock
})
expect(killAllProcessesForWorktreeMock.mock.invocationCallOrder[0]).toBeLessThan(
store.removeWorktreeMeta.mock.invocationCallOrder[0]
)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId)
expect(advertisedUrlWatcherForgetWorktreeMock).toHaveBeenCalledWith(worktreeId)
expect(deleteWorktreeHistoryDirMock).toHaveBeenCalledWith(worktreeId)
expect(mainWindow.webContents.send).toHaveBeenCalledWith('worktrees:changed', {
repoId: 'repo-folder'
})
})
it('runs the archive hook on remove when skipArchive is not set', async () => {
mockKnownFeatureWorktree()
removeWorktreeMock.mockResolvedValue(undefined)
getEffectiveHooksMock.mockReturnValue({
scripts: {
archive: 'echo archived'
}
})
runHookMock.mockResolvedValue({ success: true, output: '' })
await handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt'
})
expect(runHookMock).toHaveBeenCalledWith(
'archive',
'/workspace/feature-wt',
expect.objectContaining({ id: 'repo-1' }),
undefined,
{}
)
expect(removeWorktreeMock).toHaveBeenCalledWith(
'/workspace/repo',
'/workspace/feature-wt',
false,
expect.objectContaining({
knownRemovedWorktree: expect.objectContaining({
branch: 'feature',
head: 'feature',
path: '/workspace/feature-wt'
})
})
)
})
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[] = []
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(callOrder).toEqual(['archive', 'preflight', '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('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('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<unknown>
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')
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)
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')
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).not.toHaveBeenCalled()
expect(runHookMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId)
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).not.toHaveBeenCalled()
expect(runHookMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId)
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<void>((resolve) => {
removalStarted = resolve
})
removeWorktreeMock.mockImplementation(
() =>
new Promise<void>((resolve) => {
removalStarted()
finishRemoval = resolve
})
)
const first = handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
force: true
}) as Promise<unknown>
const second = handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt',
hostId: 'local',
force: true
}) as Promise<unknown>
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<void>((resolve) => {
removalStarted = resolve
})
removeWorktreeMock.mockImplementation(
() =>
new Promise<void>((resolve) => {
removalStarted()
finishRemoval = resolve
})
)
const first = handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt'
}) as Promise<unknown>
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
})
)
expect(removeWorktreeMock).toHaveBeenCalled()
expect(callOrder).toEqual(['preflight', 'kill', 'git'])
})
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()
getEffectiveHooksMock.mockReturnValue(null)
assertWorktreeCleanForRemovalMock.mockRejectedValue(
Object.assign(new Error('Worktree has uncommitted or untracked changes.'), {
stdout: '?? scratch.txt\n'
})
)
await expect(
handlers['worktrees:remove'](null, {
worktreeId: 'repo-1::/workspace/feature-wt'
})
).rejects.toThrow('Failed to delete worktree at /workspace/feature-wt. ?? scratch.txt')
expect(killAllProcessesForWorktreeMock).not.toHaveBeenCalled()
expect(removeWorktreeMock).not.toHaveBeenCalled()
})
it('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).not.toHaveBeenCalled()
expect(removeWorktreeMock).toHaveBeenCalled()
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['worktree', 'prune'], {
cwd: '/workspace/repo'
})
})
it('skips the PTY teardown for SSH-backed repos (design §6 out-of-scope)', async () => {
// Why: SSH-backed PTYs live on the remote host and are handled by the
// remote provider's own teardown. The local-host helper must not run for
// SSH repos, because it would sweep registry entries for other worktrees.
const repo = {
id: 'repo-ssh',
path: '/remote/repo',
displayName: 'ssh',
badgeColor: '#000',
addedAt: 0,
connectionId: 'conn-1',
worktreeBaseRef: null
}
store.getRepos.mockReturnValue([repo])
store.getRepo.mockReturnValue(repo)
// The test can't easily mock the SSH provider without more plumbing — the
// call will throw about 'no git provider for connection'. What matters
// here is that the kill helper was NOT called for the SSH branch.
await (
handlers['worktrees:remove'](null, {
worktreeId: 'repo-ssh::/remote/feature-wt'
}) as Promise<unknown>
).catch(() => {})
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.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)
const result = await handlers['worktrees:forgetLocal'](null, { worktreeId })
expect(result).toEqual({})
expect(killAllProcessesForWorktreeMock).toHaveBeenCalledWith(worktreeId, {
runtime: runtimeStub,
localProvider: ptyProvider,
onPtyStopped: clearProviderPtyStateMock
})
expect(runtimeStub.clearOptimisticReconcileToken).toHaveBeenCalledWith(worktreeId)
expect(store.removeWorktreeMeta).toHaveBeenCalledWith(worktreeId)
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('throws when the repo is missing', async () => {
store.getRepo.mockReturnValue(undefined)
await expect(
handlers['worktrees:forgetLocal'](null, {
worktreeId: 'repo-gone::/workspace/feature-wt'
})
).rejects.toThrow(/Repo not found/)
expect(store.removeWorktreeMeta).not.toHaveBeenCalled()
expect(getSshGitProviderMock).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()
})
})
})