mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 16:02:50 +00:00
779 lines
26 KiB
TypeScript
779 lines
26 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { resolve } from 'node:path'
|
|
import type { CreateWorktreeResult } from '../../shared/worktree/create-types'
|
|
import { resolveRegisteredWorktreePath } from './registered-worktree-roots-cache'
|
|
import {
|
|
listWorktreesMock,
|
|
describeCreatedWorktreeMock,
|
|
addWorktreeMock,
|
|
resolveLocalGitUsernameMock,
|
|
getBaseRefDefaultMock,
|
|
resolveDefaultBaseRefWithLocalGitMock,
|
|
getBranchConflictKindMock,
|
|
getEffectiveHooksMock,
|
|
createSetupRunnerScriptMock,
|
|
getEffectiveHooksFromConfigMock,
|
|
shouldRunSetupForCreateMock,
|
|
loadHooksMock,
|
|
computeWorktreePathMock,
|
|
gitExecFileAsyncMock
|
|
} from './worktrees-test-module-mocks'
|
|
import { handlers, mainWindow, setupWorktreeHandlers, store } from './worktrees-test-harness'
|
|
import type { WorktreeRuntimeStub } from './worktrees-test-runtime-stub'
|
|
|
|
vi.mock('electron', async () =>
|
|
(await import('./worktrees-test-module-mocks')).electronModuleMock()
|
|
)
|
|
vi.mock('../git/worktree', async () =>
|
|
(await import('./worktrees-test-module-mocks')).gitWorktreeModuleMock()
|
|
)
|
|
vi.mock('../git/runner', async () =>
|
|
(await import('./worktrees-test-module-mocks')).gitRunnerModuleMock()
|
|
)
|
|
vi.mock('../git/repo', async () =>
|
|
(await import('./worktrees-test-module-mocks')).gitRepoModuleMock()
|
|
)
|
|
vi.mock('../git/git-username', async (importOriginal) => ({
|
|
...(await importOriginal<Record<string, unknown>>()),
|
|
resolveLocalGitUsername: (await import('./worktrees-test-module-mocks'))
|
|
.resolveLocalGitUsernameMock
|
|
}))
|
|
vi.mock('../github/client', async () =>
|
|
(await import('./worktrees-test-module-mocks')).githubClientModuleMock()
|
|
)
|
|
vi.mock('../source-control/hosted-review', async () =>
|
|
(await import('./worktrees-test-module-mocks')).hostedReviewModuleMock()
|
|
)
|
|
vi.mock('../providers/ssh-git-dispatch', async () =>
|
|
(await import('./worktrees-test-module-mocks')).sshGitDispatchModuleMock()
|
|
)
|
|
vi.mock('../providers/ssh-filesystem-dispatch', async () =>
|
|
(await import('./worktrees-test-module-mocks')).sshFilesystemDispatchModuleMock()
|
|
)
|
|
vi.mock('./worktree-symlinks', async () =>
|
|
(await import('./worktrees-test-module-mocks')).worktreeSymlinksModuleMock()
|
|
)
|
|
vi.mock('./ssh', async () => (await import('./worktrees-test-module-mocks')).sshModuleMock())
|
|
vi.mock('../ssh/ssh-target-registry', async () =>
|
|
(await import('./worktrees-test-module-mocks')).sshTargetRegistryModuleMock()
|
|
)
|
|
vi.mock('../hooks', async () => (await import('./worktrees-test-module-mocks')).hooksModuleMock())
|
|
vi.mock('../setup-runner-script-text', async (importOriginal) =>
|
|
(await import('./worktrees-test-module-mocks')).setupRunnerScriptTextModuleMock(
|
|
(await importOriginal()) as Record<string, unknown>
|
|
)
|
|
)
|
|
vi.mock('../worktree-runner-script', async (importOriginal) =>
|
|
(await import('./worktrees-test-module-mocks')).worktreeRunnerScriptModuleMock(
|
|
(await importOriginal()) as Record<string, unknown>
|
|
)
|
|
)
|
|
vi.mock('../effective-hook-config', async (importOriginal) =>
|
|
(await import('./worktrees-test-module-mocks')).effectiveHookConfigModuleMock(
|
|
(await importOriginal()) as Record<string, unknown>
|
|
)
|
|
)
|
|
vi.mock('../setup-hook-env-vars', async (importOriginal) =>
|
|
(await import('./worktrees-test-module-mocks')).setupHookEnvVarsModuleMock(
|
|
(await importOriginal()) as Record<string, unknown>
|
|
)
|
|
)
|
|
vi.mock('./worktree-logic', async (importOriginal) =>
|
|
(await import('./worktrees-test-module-mocks')).worktreeLogicModuleMock(
|
|
(await importOriginal()) as Record<string, unknown>
|
|
)
|
|
)
|
|
vi.mock('../terminal-history-deletion', async () =>
|
|
(await import('./worktrees-test-module-mocks')).terminalHistoryDeletionModuleMock()
|
|
)
|
|
vi.mock('../ports/advertised-url-watcher', async () =>
|
|
(await import('./worktrees-test-module-mocks')).advertisedUrlWatcherModuleMock()
|
|
)
|
|
vi.mock('../workspace-cleanup-scan-snapshot', async () =>
|
|
(await import('./worktrees-test-module-mocks')).workspaceCleanupScanSnapshotModuleMock()
|
|
)
|
|
vi.mock('../workspace-space-analysis-snapshot', async () =>
|
|
(await import('./worktrees-test-module-mocks')).workspaceSpaceAnalysisSnapshotModuleMock()
|
|
)
|
|
vi.mock('../workspace-cleanup-removal-snapshot-prune', async () =>
|
|
(await import('./worktrees-test-module-mocks')).workspaceCleanupRemovalSnapshotPruneModuleMock()
|
|
)
|
|
vi.mock('../runtime/worktree-teardown', async () =>
|
|
(await import('./worktrees-test-module-mocks')).worktreeTeardownModuleMock()
|
|
)
|
|
vi.mock('./pty', async () => (await import('./worktrees-test-module-mocks')).ptyModuleMock())
|
|
|
|
describe('registerWorktreeHandlers', () => {
|
|
let runtimeStub: WorktreeRuntimeStub
|
|
|
|
beforeEach(() => {
|
|
runtimeStub = setupWorktreeHandlers()
|
|
})
|
|
|
|
it('starts username and base-ref probes concurrently', async () => {
|
|
const events: string[] = []
|
|
let resolveUsername!: (value: string) => void
|
|
let resolveBase!: (value: string | null) => void
|
|
store.getSettings.mockReturnValue({
|
|
branchPrefix: 'git-username',
|
|
nestWorkspaces: false,
|
|
refreshLocalBaseRefOnWorktreeCreate: false,
|
|
workspaceDir: '/workspace'
|
|
})
|
|
resolveLocalGitUsernameMock.mockImplementation(
|
|
() =>
|
|
new Promise<string>((resolve) => {
|
|
events.push('username-start')
|
|
resolveUsername = resolve
|
|
})
|
|
)
|
|
resolveDefaultBaseRefWithLocalGitMock.mockImplementation(
|
|
() =>
|
|
new Promise<string | null>((resolve) => {
|
|
events.push('base-start')
|
|
resolveBase = resolve
|
|
})
|
|
)
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '/workspace/concurrent-probe',
|
|
head: 'created-sha',
|
|
branch: 'jdoe/concurrent-probe',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
|
|
const creation = handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'concurrent-probe'
|
|
})
|
|
await Promise.resolve()
|
|
|
|
expect(events).toEqual(['username-start', 'base-start'])
|
|
resolveUsername('jdoe')
|
|
resolveBase('origin/main')
|
|
await expect(creation).resolves.toMatchObject({
|
|
worktree: expect.objectContaining({ branch: 'jdoe/concurrent-probe' })
|
|
})
|
|
})
|
|
|
|
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()
|
|
})
|
|
|
|
it('auto-suffixes the branch name when the first choice collides with a remote branch', async () => {
|
|
// Why: new-workspace flow should silently try improve-dashboard-2, -3, … rather than failing back to the name picker.
|
|
getBranchConflictKindMock.mockImplementation(async (_repoPath: string, branch: string) =>
|
|
branch === 'improve-dashboard' ? 'remote' : null
|
|
)
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '/workspace/improve-dashboard-2',
|
|
head: 'abc123',
|
|
branch: 'improve-dashboard-2',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
|
|
const result = (await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'improve-dashboard'
|
|
})) as CreateWorktreeResult
|
|
|
|
expect(addWorktreeMock).toHaveBeenCalledWith(
|
|
'/workspace/repo',
|
|
'/workspace/improve-dashboard-2',
|
|
'improve-dashboard-2',
|
|
'origin/main',
|
|
false
|
|
)
|
|
expect(result).toMatchObject({
|
|
worktree: expect.objectContaining({
|
|
path: '/workspace/improve-dashboard-2',
|
|
branch: 'improve-dashboard-2'
|
|
})
|
|
})
|
|
})
|
|
|
|
it('keeps an emoji-only display name while using safe branch and path names', async () => {
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '/workspace/rocket',
|
|
head: 'abc123',
|
|
branch: 'rocket',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
|
|
await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: '🚀'
|
|
})
|
|
|
|
expect(addWorktreeMock).toHaveBeenCalledWith(
|
|
'/workspace/repo',
|
|
'/workspace/rocket',
|
|
'rocket',
|
|
'origin/main',
|
|
false
|
|
)
|
|
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
|
|
'repo-1::/workspace/rocket',
|
|
expect.objectContaining({ displayName: '🚀' })
|
|
)
|
|
})
|
|
|
|
it('uses a repo-specific worktree base path when creating local worktrees', async () => {
|
|
store.getRepo.mockReturnValue({
|
|
id: 'repo-1',
|
|
path: '/workspace/repo',
|
|
displayName: 'repo',
|
|
badgeColor: '#000',
|
|
addedAt: 0,
|
|
worktreeBaseRef: null,
|
|
worktreeBasePath: '../worktrees'
|
|
})
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '../worktrees/feature',
|
|
head: 'abc123',
|
|
branch: 'feature',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
|
|
await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'feature'
|
|
})
|
|
|
|
expect(computeWorktreePathMock).toHaveBeenCalledWith('feature', '/workspace/repo', {
|
|
nestWorkspaces: false,
|
|
workspaceDir: '../worktrees'
|
|
})
|
|
expect(addWorktreeMock).toHaveBeenCalledWith(
|
|
'/workspace/repo',
|
|
'../worktrees/feature',
|
|
'feature',
|
|
'origin/main',
|
|
false
|
|
)
|
|
expect(store.setWorktreeMeta).toHaveBeenCalledWith(
|
|
'repo-1::../worktrees/feature',
|
|
expect.objectContaining({
|
|
orcaCreationWorkspaceLayout: { path: '../worktrees', nestWorkspaces: false }
|
|
})
|
|
)
|
|
})
|
|
|
|
it('registers local worktree roots immediately after create', async () => {
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '/workspace/repo',
|
|
head: 'base',
|
|
branch: 'refs/heads/main',
|
|
isBare: false,
|
|
isMainWorktree: true
|
|
},
|
|
{
|
|
path: '/workspace/improve-dashboard',
|
|
head: 'abc123',
|
|
branch: 'refs/heads/improve-dashboard',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
|
|
await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'improve-dashboard'
|
|
})
|
|
|
|
const listWorktreesCallsAfterCreate = listWorktreesMock.mock.calls.length
|
|
await expect(
|
|
resolveRegisteredWorktreePath('/workspace/improve-dashboard', store as never)
|
|
).resolves.toBe(resolve('/workspace/improve-dashboard'))
|
|
expect(listWorktreesMock).toHaveBeenCalledTimes(listWorktreesCallsAfterCreate)
|
|
})
|
|
|
|
it('completes a create the listing failed and keeps sibling worktrees authorized', async () => {
|
|
const sibling = {
|
|
path: '/workspace/existing-sibling',
|
|
head: 'sib123',
|
|
branch: 'refs/heads/existing-sibling',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
listWorktreesMock.mockResolvedValue([
|
|
{
|
|
path: '/workspace/repo',
|
|
head: 'base',
|
|
branch: 'refs/heads/main',
|
|
isBare: false,
|
|
isMainWorktree: true
|
|
},
|
|
sibling
|
|
])
|
|
await handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'existing-sibling' })
|
|
|
|
// The create Git could no longer list, recovered by reading the worktree directly.
|
|
listWorktreesMock.mockRejectedValue(new Error('git worktree list timed out.'))
|
|
describeCreatedWorktreeMock.mockResolvedValue({
|
|
path: '/workspace/improve-dashboard',
|
|
head: 'abc123',
|
|
branch: 'refs/heads/improve-dashboard',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
})
|
|
|
|
await expect(
|
|
handlers['worktrees:create'](null, { repoId: 'repo-1', name: 'improve-dashboard' })
|
|
).resolves.toMatchObject({ worktree: { path: '/workspace/improve-dashboard' } })
|
|
// Why: registration replaces the repo's root set, so a one-row recovery must not revoke it.
|
|
await expect(
|
|
resolveRegisteredWorktreePath('/workspace/existing-sibling', store as never)
|
|
).resolves.toBe(resolve('/workspace/existing-sibling'))
|
|
// Why: the recovered create is still a real worktree, so its own path must be usable at once.
|
|
await expect(
|
|
resolveRegisteredWorktreePath('/workspace/improve-dashboard', store as never)
|
|
).resolves.toBe(resolve('/workspace/improve-dashboard'))
|
|
})
|
|
|
|
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' },
|
|
setupAgentStartupPolicy: 'wait-for-setup'
|
|
})
|
|
getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
|
getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
|
shouldRunSetupForCreateMock.mockReturnValue(true)
|
|
expect(createSetupRunnerScriptMock).not.toHaveBeenCalled()
|
|
|
|
const result = (await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'improve-dashboard',
|
|
createdWithAgent: 'claude',
|
|
startup: {
|
|
command: 'claude --prefill test',
|
|
env: { ORCA_AGENT_MODE: 'direct' },
|
|
viewMode: 'chat',
|
|
telemetry: {
|
|
agent_kind: 'claude',
|
|
launch_source: 'new_workspace_composer',
|
|
request_kind: 'new'
|
|
}
|
|
}
|
|
})) as {
|
|
setup?: unknown
|
|
startupTerminal?: { spawned: boolean; surface?: string }
|
|
timing?: {
|
|
phases: { phase: string }[]
|
|
preparedCheckout?: { status: string; reason?: string }
|
|
}
|
|
}
|
|
expect(createSetupRunnerScriptMock).toHaveBeenCalledWith(
|
|
expect.objectContaining({ id: 'repo-1' }),
|
|
'/workspace/improve-dashboard',
|
|
'pnpm install',
|
|
undefined,
|
|
undefined,
|
|
'wait-for-setup'
|
|
)
|
|
|
|
expect(runtimeStub.createTerminal).toHaveBeenNthCalledWith(
|
|
1,
|
|
'id:repo-1::/workspace/improve-dashboard',
|
|
{
|
|
claudeAgentTeamsSourceCommand: 'claude --prefill test',
|
|
command: 'claude --prefill test',
|
|
env: { ORCA_AGENT_MODE: 'direct' },
|
|
launchAgent: 'claude',
|
|
viewMode: 'chat',
|
|
startupCommandDelivery: undefined,
|
|
telemetry: {
|
|
agent_kind: 'claude',
|
|
launch_source: 'new_workspace_composer',
|
|
request_kind: 'new'
|
|
},
|
|
activate: true
|
|
}
|
|
)
|
|
expect(runtimeStub.createTerminal).toHaveBeenNthCalledWith(
|
|
2,
|
|
'id:repo-1::/workspace/improve-dashboard',
|
|
{
|
|
title: 'Setup',
|
|
command: expect.stringContaining('bash /workspace/repo/.git/orca/setup-runner.sh'),
|
|
env: {
|
|
ORCA_ROOT_PATH: '/workspace/repo',
|
|
ORCA_WORKTREE_PATH: '/workspace/improve-dashboard'
|
|
},
|
|
activate: false
|
|
}
|
|
)
|
|
const startupCreateCall = runtimeStub.createTerminal.mock.calls[0]
|
|
const setupCreateCall = runtimeStub.createTerminal.mock.calls[1]
|
|
if (!startupCreateCall || !setupCreateCall) {
|
|
throw new Error('expected startup and setup terminal calls')
|
|
}
|
|
const startupCommand = (startupCreateCall[1] as { command: string }).command
|
|
const setupCommand = (setupCreateCall[1] as { command: string }).command
|
|
expect(startupCommand).toBe('claude --prefill test')
|
|
expect(setupCommand).toBe('bash /workspace/repo/.git/orca/setup-runner.sh')
|
|
expect(result.setup).toBeUndefined()
|
|
expect(result.startupTerminal).toEqual({ spawned: true, surface: 'visible' })
|
|
expect(result.timing?.phases.map((phase) => phase.phase)).toEqual(
|
|
expect.arrayContaining([
|
|
'git_worktree_add',
|
|
'list_created_worktree',
|
|
'resolve_worktreeinclude',
|
|
'prepare_setup',
|
|
'spawn_startup_terminal'
|
|
])
|
|
)
|
|
// Nothing warmed this repo, so the create must report the cold path rather than stay silent.
|
|
expect(result.timing?.preparedCheckout).toEqual({ status: 'miss', reason: 'none_armed' })
|
|
})
|
|
|
|
it('returns the wrapped setup command when startup spawned but setup creation failed', async () => {
|
|
addWorktreeMock.mockResolvedValue({})
|
|
listWorktreesMock.mockResolvedValueOnce([
|
|
{
|
|
path: '/workspace/improve-dashboard',
|
|
head: 'def',
|
|
branch: 'improve-dashboard',
|
|
isBare: false,
|
|
isMainWorktree: false
|
|
}
|
|
])
|
|
loadHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
|
getEffectiveHooksMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
|
getEffectiveHooksFromConfigMock.mockReturnValue({ scripts: { setup: 'pnpm install' } })
|
|
shouldRunSetupForCreateMock.mockReturnValue(true)
|
|
createSetupRunnerScriptMock.mockReturnValueOnce({
|
|
runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh',
|
|
shell: { family: 'posix', executable: 'wsl.exe' },
|
|
envVars: {
|
|
ORCA_ROOT_PATH: 'C:\\workspace\\repo',
|
|
ORCA_WORKTREE_PATH: 'C:\\workspace\\improve-dashboard'
|
|
},
|
|
waitForAgentStartup: true
|
|
})
|
|
runtimeStub.createTerminal
|
|
.mockResolvedValueOnce({ handle: 'term-startup', surface: 'visible' })
|
|
.mockRejectedValueOnce(new Error('setup creation failed'))
|
|
|
|
const result = (await handlers['worktrees:create'](null, {
|
|
repoId: 'repo-1',
|
|
name: 'improve-dashboard',
|
|
createdWithAgent: 'claude',
|
|
startup: {
|
|
command: 'claude --prefill test',
|
|
env: { ORCA_AGENT_MODE: 'direct' },
|
|
telemetry: {
|
|
agent_kind: 'claude',
|
|
launch_source: 'new_workspace_composer',
|
|
request_kind: 'new'
|
|
}
|
|
}
|
|
})) as { setup?: { command?: string; runnerScriptPath: string } }
|
|
|
|
expect(result.setup).toEqual(
|
|
expect.objectContaining({
|
|
runnerScriptPath: 'C:\\workspace\\repo\\.git\\orca\\setup-runner.sh',
|
|
command: expect.stringContaining('bash /mnt/c/workspace/repo/.git/orca/setup-runner.sh')
|
|
})
|
|
)
|
|
expect(result.setup?.command).toContain('printf')
|
|
})
|
|
|
|
it('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()
|
|
})
|
|
})
|