Files
orca/src/main/runtime/runtime-local-worktree-create.test.ts
T

689 lines
27 KiB
TypeScript

import type * as RuntimeWorktreeCreateGit from './runtime-worktree-create-git'
import {
labelOnlyCreateRequests,
renameCreatedWorktreeOnFirstWork
} from '../worktree-create-first-work-test-harness'
import { isAutoGeneratedCreatureBranchName } from '../../shared/branch-name-from-work'
import { creatureNameTier } from '../../shared/worktree/retired-name-registry'
import { resolveWorkerWorktreeName } from './rpc/methods/orchestration/worker/worker-worktree-creation'
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
import { basename, resolve } from 'node:path'
import type { Repo } from '../../shared/repo-types'
import { getDefaultRepoHookSettings } from '../../shared/constants'
import type { AddWorktreeOptions } from '../git/worktree'
import {
acquireGitAdmission,
GitAdmissionScheduler,
_resetGitAdmissionForTests
} from '../git/command-runner/git-subprocess-admission'
const {
runtimeCreateMocks: mocks,
worktreePath,
pickedBranch,
writtenMeta,
readSparsePresets,
createWorktree,
resetRuntimeCreateMocks
} = await vi.hoisted(async () => await import('./runtime-local-worktree-create-test-harness'))
vi.mock('../text-generation/commit-message-text-generation', () => ({
resolveTextGenerationParams: () => ({
ok: true,
params: { agentId: 'claude', model: 'fixture' }
}),
generateBranchNameFromContext: async () => ({ success: true, slug: 'fix-auth' })
}))
vi.mock('../text-generation/commit-message-agent-environment', () => ({
prepareLocalCommitMessageAgentEnv: async () => ({ ok: true })
}))
vi.mock('../git/runner', () => ({ gitExecFileAsync: mocks.git }))
vi.mock('../project-runtime-git-options', () => ({
getLocalProjectGitExecOptions: () => ({ cwd: '/repo', ...mocks.routing() }),
getLocalProjectWorktreeGitOptions: mocks.routing,
getWorktreeMirrorDistro: () => undefined
}))
vi.mock('../git/repo', () => ({
getBaseRefDefault: mocks.defaultBase,
resolveDefaultBaseRefWithLocalGit: mocks.defaultBase,
getBranchConflictKind: mocks.branchConflict
}))
vi.mock('../git/git-username', () => ({ resolveLocalGitUsername: async () => '' }))
vi.mock('../hooks', () => ({ getEffectiveHooks: mocks.effectiveHooks }))
vi.mock('../git/worktree-base-ref-probe', () => ({ hasLocalWorktreeBaseRef: mocks.hasBase }))
vi.mock('./runtime-worktree-create-git', () => ({
resolveCreateBranchName: mocks.branchName,
canCheckoutExistingLocalBranch: mocks.canCheckout,
getLocalGitHubPrForBranch: mocks.githubPr,
getSelectedHostedReviewForBranch: vi.fn()
}))
vi.mock('./runtime-worktree-filesystem', () => ({ runtimePathExists: mocks.pathExists }))
vi.mock('../worktree-create-preparation', () => ({ consumePreparedWorktreeCreate: mocks.consume }))
vi.mock('../git/worktree', () => ({ addWorktree: mocks.add, addSparseWorktree: mocks.addSparse }))
vi.mock('../ipc/worktree-remote', () => ({ configureCreatedWorktreePushTarget: mocks.pushTarget }))
vi.mock('../ipc/created-worktree-reconciliation', () => ({ resolveCreatedWorktree: mocks.listing }))
vi.mock('../worktree-name-retirement', () => ({
failedWorktreeCreationNeedsRetirement: vi.fn(),
retireGeneratedWorktreeName: vi.fn()
}))
vi.mock('../git/worktree-shared-directories', () => ({
resolveWorktreeSharedDirectories: mocks.resolveShared
}))
vi.mock('../git/worktree-include-file', () => ({
resolveWorktreeIncludePaths: mocks.resolveInclude
}))
vi.mock('../ipc/worktree-symlinks', () => ({
createWorktreeCopiedPaths: mocks.copyPaths,
createWorktreeLinkedPaths: vi.fn(),
createWorktreeSharedPaths: vi.fn()
}))
import { WorktreeCreateSetupChoiceRefusedError } from './worktree-create-setup-choice-refusal'
import type { PreparationRearmHolder } from '../worktree-create-preparation'
import { createWorktreeCreateTimingRecorder } from '../worktree-create-timing'
import { withoutInFlightWorktreeCreates } from '../worktree-create-in-flight'
beforeEach(resetRuntimeCreateMocks)
describe('runtime create listing hold', () => {
const rows = [{ path: worktreePath }]
it('holds the checkout during Git creation and releases it once metadata is saved', async () => {
mocks.consume.mockImplementation(async () => {
expect(withoutInFlightWorktreeCreates('repo-1', rows)).toEqual([])
return { status: 'hit', result: {}, rearm: mocks.rearm }
})
mocks.resolveShared.mockImplementation(async () => {
expect(withoutInFlightWorktreeCreates('repo-1', rows)).toBe(rows)
return []
})
await createWorktree()
expect(mocks.resolveShared).toHaveBeenCalledOnce()
})
it('releases the checkout when Git creation fails', async () => {
mocks.consume.mockRejectedValue(new Error('checkout failed'))
await expect(createWorktree()).rejects.toThrow('checkout failed')
expect(withoutInFlightWorktreeCreates('repo-1', rows)).toBe(rows)
})
})
describe('runtime concurrent creates on one name', () => {
const addedPaths = (): string[] => mocks.add.mock.calls.map((call) => String(call[1]))
beforeEach(() => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'disabled', rearm: mocks.rearm })
let reached = 0
let bothAdding: () => void = () => {}
const gate = new Promise<void>((resolveGate) => (bothAdding = resolveGate))
// Hold both creates inside Git's add, where neither checkout exists on disk yet.
mocks.add.mockImplementation(async () => {
if (++reached === 2) {
bothAdding()
}
await gate
return {}
})
})
it.each([
{ request: { name: 'app' }, second: 'app-2' },
{ request: { name: 'nautilus', nameWasGenerated: true }, second: null }
])('gives the second create of $request.name its next candidate', async ({ request, second }) => {
await Promise.all([createWorktree(request), createWorktree(request)])
// Claims are ordered, but each create resolves its path's realpath before Git's add, so on a
// loaded runner either create can reach add first. Check the names, not the call order.
const names = addedPaths().map((path) => basename(path))
expect(names.filter((name) => name === request.name)).toHaveLength(1)
const other = names.find((name) => name !== request.name) ?? ''
if (second) {
expect(other).toBe(second)
} else {
expect(creatureNameTier(other)).toBe(2)
}
// Both claims settled with their creates, so the name is free to check again.
mocks.add.mockResolvedValue({})
await createWorktree(request)
expect(basename(addedPaths()[2])).toBe(request.name)
})
it('releases the claim when the create fails', async () => {
mocks.add.mockRejectedValueOnce(new Error('add failed'))
await expect(createWorktree({ name: 'app' })).rejects.toThrow('add failed')
mocks.add.mockResolvedValue({})
await createWorktree({ name: 'app' })
expect(addedPaths().map((path) => basename(path))).toEqual(['app', 'app'])
})
})
describe('runtime nested-workspaces layout', () => {
const addedPath = (): string => String(mocks.add.mock.calls[0]?.[1])
it('honors the client choice over the host default', async () => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'disabled', rearm: mocks.rearm })
await createWorktree({ nestWorkspaces: true })
expect(addedPath()).toBe(resolve('/worktrees', 'repo', 'app'))
})
it('falls back to the host setting when the client sends none', async () => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'disabled', rearm: mocks.rearm })
await createWorktree({})
expect(addedPath()).toBe(worktreePath)
})
})
describe('runtime keep-local-main-up-to-date choice', () => {
const addRefreshArg = (): unknown => mocks.add.mock.calls[0]?.[4]
const addOptions = (): AddWorktreeOptions | undefined => mocks.add.mock.calls[0]?.[6]
beforeEach(() => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'disabled', rearm: mocks.rearm })
mocks.remoteBase.mockResolvedValue({
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
})
})
it('refreshes local main when the client turned it on, over the host default', async () => {
await createWorktree({ refreshLocalBaseRefOnWorktreeCreate: true })
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({ refreshLocalBaseRef: true })
)
expect(addRefreshArg()).toBe(true)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBeUndefined()
})
it('suppresses the suggestion the client already dismissed', async () => {
await createWorktree({ localBaseRefSuggestionDismissed: true })
expect(addRefreshArg()).toBe(false)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBeUndefined()
})
it('keeps the host setting and suggestion when the client sends neither', async () => {
await createWorktree({})
expect(addRefreshArg()).toBe(false)
expect(addOptions()?.suggestLocalBaseRefUpdate).toBe(true)
})
})
describe('runtime prepared-worktree replenishment', () => {
it('keeps a failed claim reserved through the normal-add fallback', async () => {
mocks.consume.mockResolvedValue({
status: 'miss',
reason: 'finalize_failed',
rearm: mocks.rearm
})
const rearm: PreparationRearmHolder = { fire: () => {} }
await createWorktree({}, rearm)
expect(mocks.add).toHaveBeenCalledOnce()
expect(mocks.rearm).not.toHaveBeenCalled()
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
it('keeps the failed claim release available when the fallback also fails', async () => {
mocks.consume.mockResolvedValue({
status: 'miss',
reason: 'prepare_failed',
rearm: mocks.rearm
})
mocks.add.mockRejectedValue(new Error('normal add failed'))
const rearm: PreparationRearmHolder = { fire: () => {} }
await expect(createWorktree({}, rearm)).rejects.toThrow('normal add failed')
expect(mocks.rearm).not.toHaveBeenCalled()
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
it('leaves the re-arm holder armed but unfired once probes and include copies finish', async () => {
const rearm: PreparationRearmHolder = { fire: () => {} }
let finishProbe!: (paths: string[]) => void
mocks.resolveShared.mockImplementation(
() =>
new Promise<string[]>((resolve) => {
finishProbe = resolve
})
)
let finishCopy!: (paths: string[]) => void
mocks.copyPaths.mockImplementation(
() =>
new Promise<string[]>((resolve) => {
finishCopy = resolve
})
)
const creation = createWorktree({}, rearm)
await vi.waitFor(() => expect(mocks.resolveShared).toHaveBeenCalledOnce())
expect(mocks.rearm).not.toHaveBeenCalled()
finishProbe([])
await vi.waitFor(() => expect(mocks.copyPaths).toHaveBeenCalledOnce())
expect(mocks.rearm).not.toHaveBeenCalled()
finishCopy([])
await creation
// The caller launches terminals before arming, so create must not fire it itself.
expect(mocks.rearm).not.toHaveBeenCalled()
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
it('arms the holder even when materialization fails', async () => {
mocks.copyPaths.mockRejectedValue(new Error('copy failed'))
const rearm: PreparationRearmHolder = { fire: () => {} }
await expect(createWorktree({}, rearm)).rejects.toThrow('copy failed')
// The slot was consumed before the failure, so the caller's `finally` must find a real thunk.
rearm.fire()
expect(mocks.rearm).toHaveBeenCalledOnce()
})
})
describe('runtime create timing', () => {
it('times the add with the prepared-checkout consume inside it, and lists the worktrees', async () => {
mocks.listing.mockResolvedValue({
created: mocks.created,
worktrees: [mocks.created, mocks.created, mocks.created],
listingComplete: true
})
const timing = createWorktreeCreateTimingRecorder()
mocks.branchName.mockResolvedValue('main')
await createWorktree(pickedBranch, undefined, timing)
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({ timing, branch: 'main', baseBranch: 'origin/main' })
)
expect(mocks.add).not.toHaveBeenCalled()
expect(mocks.consume.mock.calls[0][0].options).toEqual({})
const finished = timing.finish()
expect(finished.phases.map((phase) => phase.phase)).toEqual(
expect.arrayContaining([
'resolve_name',
'git_worktree_add',
'list_created_worktree',
'persist_metadata',
'copy_worktreeinclude'
])
)
expect(finished).toMatchObject({ executionHost: 'local', worktreeCount: 3 })
})
it('records a sparse create as a miss without consulting the prepared checkout', async () => {
const timing = createWorktreeCreateTimingRecorder()
await createWorktree({ sparseCheckout: { directories: ['src'] } }, undefined, timing)
expect(mocks.consume).not.toHaveBeenCalled()
expect(timing.finish().preparedCheckout).toEqual({ status: 'miss', reason: 'sparse_checkout' })
})
})
describe('runtime create Git priority', () => {
it.each([undefined, 'Ubuntu'])(
'preserves interactive priority and routing on %s',
async (wslDistro) => {
const routing = wslDistro ? { wslDistro } : {}
mocks.routing.mockReturnValue(routing)
const options = routing
const target = { remoteName: 'origin', branchName: 'app' }
await createWorktree({ baseBranch: undefined, branchNameOverride: 'app', pushTarget: target })
expect(mocks.defaultBase).toHaveBeenCalledWith({ cwd: '/repo', ...options })
expect(mocks.branchName).toHaveBeenCalledWith(
'/repo',
'app',
'app',
expect.anything(),
'',
options
)
expect(mocks.canCheckout).toHaveBeenCalledWith('/repo', 'app', 'main', options)
expect(mocks.branchConflict).toHaveBeenCalledWith('/repo', 'app', 'main', options, undefined)
expect(mocks.githubPr).toHaveBeenCalledWith('/repo', 'app', routing)
expect(mocks.remoteBase).toHaveBeenCalledWith('/repo', 'main', options)
expect(mocks.hasBase).toHaveBeenCalledWith('/repo', 'main', options)
expect(mocks.consume).toHaveBeenCalledWith(expect.objectContaining({ options }))
expect(mocks.pushTarget).toHaveBeenCalledWith(worktreePath, 'app', target, options)
expect(mocks.listing).toHaveBeenCalledWith('/repo', worktreePath, 'app', options)
expect(mocks.resolveShared).toHaveBeenCalledWith('/repo', options)
expect(mocks.resolveInclude).toHaveBeenCalledWith('/repo', options)
}
)
it('creates through interactive headroom when regular Git capacity is occupied', async () => {
mocks.consume.mockResolvedValue({ status: 'miss', reason: 'none_armed' })
const scheduler = new GitAdmissionScheduler({ generalCap: 1, generalHeadroom: 1 })
_resetGitAdmissionForTests(scheduler)
const blocker = await acquireGitAdmission({ args: ['status'], cwd: '/repo' })
mocks.add.mockImplementation(
async (
_repo: string,
_path: string,
_branch: string,
_base: string,
_refresh: boolean,
_existing: boolean,
options?: AddWorktreeOptions
) => {
const grant = await acquireGitAdmission({
args: ['worktree', 'add'],
cwd: '/repo',
tier: options?.admissionTier,
signal: AbortSignal.timeout(200)
})
grant.release()
return {}
}
)
try {
await expect(createWorktree()).resolves.toHaveProperty('worktreePath', worktreePath)
expect(mocks.add).toHaveBeenCalledOnce()
} finally {
blocker.release()
_resetGitAdmissionForTests()
}
})
it.each([
['exactly its directories', ['packages/ui', 'apps/web'], 'preset-web'],
['an edited selection', ['apps/web'], undefined]
])('records the sparse preset only for %s', async (_case, directories, recorded) => {
writtenMeta.length = 0
await createWorktree({ sparseCheckout: { directories, presetId: 'preset-web' } })
expect(writtenMeta.find((meta) => meta.sparseDirectories)?.sparsePresetId).toBe(recorded)
})
it('still creates the worktree, with no sparse preset, when presets cannot be read', async () => {
writtenMeta.length = 0
readSparsePresets.mockImplementation(() => {
throw new Error('corrupt presets')
})
await createWorktree({
sparseCheckout: { directories: ['apps/web', 'packages/ui'], presetId: 'preset-web' }
})
const sparseMeta = writtenMeta.find((meta) => meta.sparseDirectories)
expect(sparseMeta?.sparseDirectories).toEqual(['apps/web', 'packages/ui'])
expect(sparseMeta?.sparsePresetId).toBeUndefined()
})
it('preserves priority for sparse creates and remote base refreshes', async () => {
const base = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
mocks.remoteBase.mockResolvedValue(base)
await createWorktree({ baseBranch: 'origin/main', sparseCheckout: { directories: ['src'] } })
const options = {}
expect(mocks.hasRemoteRef).toHaveBeenCalledWith('/repo', base, options)
expect(mocks.refresh).toHaveBeenCalledWith('/repo', base, options)
expect(mocks.addSparse).toHaveBeenCalledWith(
'/repo',
worktreePath,
'app',
['src'],
'origin/main',
false,
expect.objectContaining(options)
)
expect(mocks.consume).not.toHaveBeenCalled()
})
})
describe('runtime create base without a tracking ref', () => {
const originMain = {
remote: 'origin',
branch: 'main',
ref: 'refs/remotes/origin/main',
base: 'origin/main'
}
beforeEach(() => {
mocks.remoteBase.mockResolvedValue(originMain)
mocks.hasRemoteRef.mockResolvedValue(false)
// Only the local branch the remote names exists; `origin/main` itself does not resolve.
mocks.hasBase.mockImplementation(async (_repo: string, ref: string) => ref === 'main')
})
it('fetches the remote base first and creates from it when the fetch works', async () => {
mocks.refresh.mockImplementation(async () => {
mocks.hasRemoteRef.mockResolvedValue(true)
return { ok: true }
})
const result = await createWorktree({ baseBranch: 'origin/main' })
expect(mocks.refresh).toHaveBeenCalledOnce()
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({
baseBranch: 'origin/main',
options: expect.objectContaining({ remoteTrackingBase: originMain })
})
)
expect(result).not.toHaveProperty('baseFallback')
expect(result.worktree.baseRef).toBe('refs/remotes/origin/main')
})
it('names the workspace only after the fetch has settled', async () => {
let refreshSettled = false
mocks.refresh.mockImplementation(async () => {
await Promise.resolve()
mocks.hasRemoteRef.mockResolvedValue(true)
refreshSettled = true
return { ok: true }
})
const settledAtNaming: boolean[] = []
mocks.canCheckout.mockImplementation(async () => {
settledAtNaming.push(refreshSettled)
return false
})
await createWorktree({ baseBranch: 'origin/main', branchNameOverride: 'app' })
expect(settledAtNaming).toEqual([true])
expect(mocks.canCheckout).toHaveBeenCalledWith('/repo', 'app', 'origin/main', {})
expect(mocks.branchConflict).toHaveBeenCalledWith('/repo', 'app', 'origin/main', {}, undefined)
})
it('keeps the not-found error when the fetch works but the tracking ref is still missing', async () => {
mocks.hasBase.mockResolvedValue(false)
mocks.refresh.mockResolvedValue({ ok: true })
await expect(createWorktree({ baseBranch: 'origin/main' })).rejects.toThrow(
'Base ref "origin/main" was not found after fetching.'
)
expect(mocks.consume).not.toHaveBeenCalled()
})
it('creates from the local branch and reports it when the fetch fails', async () => {
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
const result = await createWorktree({ baseBranch: 'origin/main', allowLocalBaseFallback: true })
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({
baseBranch: 'main',
options: expect.not.objectContaining({ remoteTrackingBase: expect.anything() })
})
)
expect(result.baseFallback).toEqual({ requestedRef: 'origin/main', localRef: 'main' })
expect(result.addResult).not.toHaveProperty('baseFallback')
expect(result.worktree.baseRef).toBe('main')
})
it('decides the base before branch reuse and conflict checks', async () => {
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
await createWorktree({
baseBranch: 'origin/main',
branchNameOverride: 'app',
allowLocalBaseFallback: true
})
expect(mocks.refresh.mock.invocationCallOrder[0]).toBeLessThan(
mocks.canCheckout.mock.invocationCallOrder[0]
)
expect(mocks.canCheckout).toHaveBeenCalledWith('/repo', 'app', 'main', {})
expect(mocks.branchConflict).toHaveBeenCalledWith('/repo', 'app', 'main', {}, undefined)
})
it("keeps the network error when the fetch fails and there's no local branch", async () => {
mocks.hasBase.mockResolvedValue(false)
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
await expect(
createWorktree({ baseBranch: 'origin/main', allowLocalBaseFallback: true })
).rejects.toThrow(
'Could not refresh base ref "origin/main" from "origin". Check your network and try again.'
)
expect(mocks.branchName).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
})
it('keeps the network error for a create that did not opt into the fallback', async () => {
mocks.refresh.mockResolvedValue({ ok: false, errorKind: 'git_error' })
await expect(createWorktree({ baseBranch: 'origin/main' })).rejects.toThrow(
'Could not refresh base ref "origin/main" from "origin". Check your network and try again.'
)
expect(mocks.consume).not.toHaveBeenCalled()
})
it('uses a local ref of the requested name without fetching or reporting a fallback', async () => {
mocks.hasBase.mockResolvedValue(true)
const result = await createWorktree({ baseBranch: 'origin/main' })
expect(mocks.refresh).not.toHaveBeenCalled()
expect(mocks.consume).toHaveBeenCalledWith(
expect.objectContaining({ baseBranch: 'origin/main' })
)
expect(result).not.toHaveProperty('baseFallback')
})
})
describe('runtime create setup decision', () => {
const askRepo: Partial<Repo> = {
hookSettings: { ...getDefaultRepoHookSettings(), setupRunPolicy: 'ask' }
}
beforeEach(() => {
mocks.effectiveHooks.mockReturnValue({ scripts: { setup: 'pnpm install' } })
})
it('refuses an ask repo with no decision before any git work', async () => {
const timing = createWorktreeCreateTimingRecorder()
// No requested base, so a create that got past the check would resolve the default base.
const request = { baseBranch: undefined }
const refusal = await createWorktree(request, undefined, timing, askRepo).catch(
(error: unknown) => error
)
expect(refusal).toBeInstanceOf(WorktreeCreateSetupChoiceRefusedError)
expect(refusal).toMatchObject({ message: 'Setup decision required for this repository' })
// Hooks come from the main checkout (no worktree path): the new worktree doesn't exist yet.
expect(mocks.effectiveHooks.mock.calls).toEqual([[expect.objectContaining({ path: '/repo' })]])
for (const gitWork of [mocks.defaultBase, mocks.remoteBase, mocks.hasBase, mocks.refresh]) {
expect(gitWork).not.toHaveBeenCalled()
}
expect(mocks.fetch).not.toHaveBeenCalled()
expect(mocks.branchName).not.toHaveBeenCalled()
expect(mocks.consume).not.toHaveBeenCalled()
expect(mocks.add).not.toHaveBeenCalled()
// The refusal's failure telemetry still says where the create ran.
expect(timing.finish().executionHost).toBe('local')
})
it.each([
['an explicit decision', { setupDecision: 'skip' as const }],
['runHooks', { runHooks: true }]
])('creates an ask repo once the request carries %s', async (_label, request) => {
await createWorktree(request, undefined, undefined, askRepo)
expect(mocks.consume).toHaveBeenCalledOnce()
})
})
describe('runtime label-only creation through first work', () => {
let actual: typeof RuntimeWorktreeCreateGit
// Load the real module graph before any test timeout starts.
beforeAll(async () => {
actual = await vi.importActual<typeof RuntimeWorktreeCreateGit>('./runtime-worktree-create-git')
})
function useRealBranchSelection() {
mocks.branchName.mockImplementation(actual.resolveCreateBranchName)
mocks.listing.mockImplementation(async (_repoPath, path, branch) => ({
created: { ...mocks.created, path, branch: `refs/heads/${branch}` }
}))
}
it.each(labelOnlyCreateRequests)(
'creates and renames a creature branch for $name while projecting its label',
async (request) => {
useRealBranchSelection()
const { worktree } = await createWorktree(request, undefined, undefined, {}, true)
const branch = worktree.branch.replace(/^refs\/heads\//, '')
expect(isAutoGeneratedCreatureBranchName(branch)).toBe(true)
expect(worktree.displayName).toBe(request.name)
expect(worktree.displayNameMode).toBe(request.nameWasGenerated ? 'automatic' : 'fixed')
const renamed = await renameCreatedWorktreeOnFirstWork(worktree, writtenMeta[0], mocks.git)
expect(renamed).toMatchObject({
branch: 'refs/heads/fix-auth',
displayName: request.nameWasGenerated ? 'Fix auth' : request.name,
displayNameMode: 'fixed'
})
}
)
it('renames the workspace a nameless orchestration worker creates on its first work', async () => {
useRealBranchSelection()
const request = await resolveWorkerWorktreeName(
{
listManagedWorktrees: async () => ({ worktrees: [] }),
listRetiredWorktreeNames: async () => ({
retiredNamesByRepo: {},
retiredNameTiersByRepo: {}
})
},
'repo-1',
undefined
)
const { worktree } = await createWorktree(request, undefined, undefined, {}, true)
expect(writtenMeta[0]).toMatchObject({ orcaCreationSource: 'runtime' })
const renamed = await renameCreatedWorktreeOnFirstWork(worktree, writtenMeta[0], mocks.git)
expect(renamed).toMatchObject({ branch: 'refs/heads/fix-auth', displayName: 'Fix auth' })
})
it.each([
{
request: { name: 'typed-label', branchNameOverride: 'feature/exact' },
enabled: true,
branch: 'feature/exact'
},
{
request: {
name: 'cli-exact',
cliProvenance: { kind: 'created-by-cli' as const, createdAt: 1 }
},
enabled: true,
branch: 'cli-exact'
},
{ request: { name: 'typed-label' }, enabled: false, branch: 'typed-label' }
])(
'preserves branch choice $branch (automatic=$enabled)',
async ({ request, enabled, branch }) => {
useRealBranchSelection()
const { worktree } = await createWorktree(request, undefined, undefined, {}, enabled)
expect(worktree).toMatchObject({
branch: `refs/heads/${branch}`,
displayName: request.name,
displayNameMode: 'fixed'
})
}
)
})