mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
fix(orchestration): resolve explicit worker worktrees directly (#14275)
* fix(orchestration): resolve explicit worker worktrees directly * fix(orchestration): share worker workspace resolution * fix(runtime): reject cross-host path ambiguity * fix(orchestration): share federated workspace resolution * refactor(runtime): share worktree host identity * test(orchestration): align worker lifecycle fixtures --------- Co-authored-by: Jinwoo-H <jinwoo0825@gmail.com>
This commit is contained in:
@@ -261,6 +261,7 @@ import { assertWorktreeUnlockedForRemoval } from '../../shared/worktree-removal'
|
||||
import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
getRepoExecutionHostId,
|
||||
getWorktreeExecutionHostId,
|
||||
parseExecutionHostId,
|
||||
toSshExecutionHostId,
|
||||
type ExecutionHostId
|
||||
@@ -2492,6 +2493,11 @@ type TerminalWorkspaceLaunchScope = {
|
||||
folderWorkspace: FolderWorkspace | null
|
||||
}
|
||||
|
||||
type ResolvedTerminalWorkspaceLaunchTarget = {
|
||||
scope: TerminalWorkspaceLaunchScope
|
||||
managedWorktree: ResolvedWorktree | null
|
||||
}
|
||||
|
||||
type WorktreeLineageInput = {
|
||||
parentWorkspace?: string
|
||||
envParentWorkspace?: string
|
||||
@@ -21363,6 +21369,14 @@ export class OrcaRuntimeService {
|
||||
return await this.resolveWorktreeSelector(worktreeSelector)
|
||||
}
|
||||
|
||||
async showManagedTerminalWorkspace(worktreeSelector: string) {
|
||||
const target = await this.resolveTerminalWorkspaceLaunchTarget(worktreeSelector)
|
||||
if (!target.managedWorktree) {
|
||||
throw new Error('selector_not_found')
|
||||
}
|
||||
return target.managedWorktree
|
||||
}
|
||||
|
||||
async scanWorkspacePorts(repoId?: string): Promise<WorkspacePortScanResult> {
|
||||
return scanWorkspacePortProbes(await this.getWorkspacePortProbes(repoId))
|
||||
}
|
||||
@@ -28630,7 +28644,7 @@ export class OrcaRuntimeService {
|
||||
|
||||
private async resolveFolderWorkspaceLaunchScope(
|
||||
selector: string
|
||||
): Promise<TerminalWorkspaceLaunchScope | null> {
|
||||
): Promise<(TerminalWorkspaceLaunchScope & { folderWorkspace: FolderWorkspace }) | null> {
|
||||
const workspace = this.resolveFolderWorkspaceSelector(selector)
|
||||
if (!workspace) {
|
||||
return null
|
||||
@@ -28710,23 +28724,35 @@ export class OrcaRuntimeService {
|
||||
private async resolveTerminalWorkspaceLaunchScope(
|
||||
selector: string
|
||||
): Promise<TerminalWorkspaceLaunchScope> {
|
||||
return (await this.resolveTerminalWorkspaceLaunchTarget(selector)).scope
|
||||
}
|
||||
|
||||
private async resolveTerminalWorkspaceLaunchTarget(
|
||||
selector: string
|
||||
): Promise<ResolvedTerminalWorkspaceLaunchTarget> {
|
||||
const floatingTerminalSelector =
|
||||
selector === FLOATING_TERMINAL_WORKTREE_ID ||
|
||||
selector === `id:${FLOATING_TERMINAL_WORKTREE_ID}`
|
||||
if (floatingTerminalSelector) {
|
||||
// Why: the floating sentinel is terminal-only — no backing repo/worktree record for other workspace APIs.
|
||||
return {
|
||||
id: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
path: homedir(),
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
scope: {
|
||||
id: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
path: homedir(),
|
||||
connectionId: null,
|
||||
repo: null,
|
||||
folderWorkspace: null
|
||||
},
|
||||
managedWorktree: null
|
||||
}
|
||||
}
|
||||
|
||||
const folderScope = await this.resolveFolderWorkspaceLaunchScope(selector)
|
||||
if (folderScope) {
|
||||
return folderScope
|
||||
return {
|
||||
scope: folderScope,
|
||||
managedWorktree: this.folderWorkspaceToResolvedWorktree(folderScope.folderWorkspace)
|
||||
}
|
||||
}
|
||||
|
||||
const workspaceSelector = selector.startsWith('id:') ? selector.slice(3) : selector
|
||||
@@ -28735,11 +28761,14 @@ export class OrcaRuntimeService {
|
||||
const worktree = await this.resolveWorktreeSelector(worktreeSelector)
|
||||
const repo = this.store?.getRepo(worktree.repoId) ?? null
|
||||
return {
|
||||
id: worktree.id,
|
||||
path: worktree.path,
|
||||
connectionId: repo?.connectionId ?? null,
|
||||
repo,
|
||||
folderWorkspace: null
|
||||
scope: {
|
||||
id: worktree.id,
|
||||
path: worktree.path,
|
||||
connectionId: repo?.connectionId ?? null,
|
||||
repo,
|
||||
folderWorkspace: null
|
||||
},
|
||||
managedWorktree: worktree
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28814,8 +28843,16 @@ export class OrcaRuntimeService {
|
||||
runtimePathsEqual(worktree.path, selector.slice(5))
|
||||
)
|
||||
if (candidates.length > 1) {
|
||||
// Why: the same physical path can appear under multiple repo IDs; a path selector is exact, so take the first row over a dup-registration ambiguity.
|
||||
candidates = [candidates[0]]
|
||||
const hostIds = new Set(
|
||||
candidates.map((worktree) => {
|
||||
const repo = this.store?.getRepo(worktree.repoId)
|
||||
return getWorktreeExecutionHostId(worktree, repo)
|
||||
})
|
||||
)
|
||||
// Why: duplicate registrations on one host describe one path; identical paths on different hosts do not.
|
||||
if (hostIds.size === 1) {
|
||||
candidates = [candidates[0]]
|
||||
}
|
||||
}
|
||||
} else if (selector.startsWith('branch:')) {
|
||||
const branchSelector = selector.slice(7)
|
||||
|
||||
@@ -0,0 +1,305 @@
|
||||
import { mkdtemp, rm } from 'node:fs/promises'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
const electronMocks = vi.hoisted(() => {
|
||||
const ipcMain = {
|
||||
on: vi.fn(() => ipcMain),
|
||||
removeListener: vi.fn(() => ipcMain),
|
||||
emit: vi.fn(() => true)
|
||||
}
|
||||
return {
|
||||
BrowserWindow: { fromId: vi.fn((): unknown => null) },
|
||||
webContents: { fromId: vi.fn((): unknown => null) },
|
||||
ipcMain,
|
||||
app: { getPath: vi.fn(() => '/tmp'), isPackaged: false }
|
||||
}
|
||||
})
|
||||
vi.mock('electron', () => electronMocks)
|
||||
|
||||
const scanLocalRepoWorktreesForResolution = vi.hoisted(() => vi.fn())
|
||||
vi.mock('./repo-worktree-resolution-scan', () => ({ scanLocalRepoWorktreesForResolution }))
|
||||
|
||||
const getSshGitProvider = vi.hoisted(() => vi.fn())
|
||||
vi.mock('../providers/ssh-git-dispatch', () => ({
|
||||
getSshGitProvider,
|
||||
getSshGitProviderGeneration: vi.fn(() => 0),
|
||||
requireSshGitProvider: (connectionId: string) => getSshGitProvider(connectionId)
|
||||
}))
|
||||
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
|
||||
import type { FolderWorkspace, ProjectGroup, Repo, WorktreeMeta } from '../../shared/types'
|
||||
import {
|
||||
registerSshFilesystemProvider,
|
||||
unregisterSshFilesystemProvider
|
||||
} from '../providers/ssh-filesystem-dispatch'
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
|
||||
const REPO_ID = 'repo-1'
|
||||
const REPO_PATH = '/repo'
|
||||
const WORKTREE_PATH = '/repo/feature'
|
||||
const WORKTREE_ID = `${REPO_ID}::${WORKTREE_PATH}`
|
||||
|
||||
function makeMeta(displayName: string, hostId?: WorktreeMeta['hostId']): WorktreeMeta {
|
||||
return {
|
||||
displayName,
|
||||
comment: '',
|
||||
linkedIssue: null,
|
||||
linkedPR: null,
|
||||
linkedLinearIssue: null,
|
||||
linkedGitLabMR: null,
|
||||
linkedGitLabIssue: null,
|
||||
isArchived: false,
|
||||
isUnread: false,
|
||||
isPinned: false,
|
||||
sortOrder: 0,
|
||||
lastActivityAt: 0,
|
||||
...(hostId ? { hostId } : {})
|
||||
}
|
||||
}
|
||||
|
||||
function makeStore(
|
||||
options: {
|
||||
repos?: Repo[]
|
||||
meta?: Record<string, WorktreeMeta>
|
||||
folderWorkspaces?: FolderWorkspace[]
|
||||
projectGroups?: ProjectGroup[]
|
||||
} = {}
|
||||
) {
|
||||
const repos = options.repos ?? [
|
||||
{
|
||||
id: REPO_ID,
|
||||
path: REPO_PATH,
|
||||
displayName: 'App',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1
|
||||
}
|
||||
]
|
||||
const meta = options.meta ?? { [WORKTREE_ID]: makeMeta('Feature') }
|
||||
const store = {
|
||||
getRepo: (id: string) => repos.find((repo) => repo.id === id),
|
||||
getRepos: () => repos,
|
||||
getAllWorktreeMeta: () => meta,
|
||||
getWorktreeMeta: (id: string) => meta[id],
|
||||
setWorktreeMeta: (id: string, patch: Partial<WorktreeMeta>) => {
|
||||
meta[id] = { ...(meta[id] ?? makeMeta('')), ...patch }
|
||||
return meta[id]
|
||||
},
|
||||
getAllWorktreeLineage: () => ({}),
|
||||
getAllWorkspaceLineage: () => ({}),
|
||||
removeWorktreeLineage: vi.fn(),
|
||||
removeWorkspaceLineage: vi.fn(),
|
||||
getGitHubCache: () => undefined,
|
||||
getSettings: () => ({
|
||||
workspaceDir: '/tmp/workspaces',
|
||||
nestWorkspaces: false,
|
||||
refreshLocalBaseRefOnWorktreeCreate: false,
|
||||
branchPrefix: 'none',
|
||||
branchPrefixCustom: ''
|
||||
}),
|
||||
getProjects: () => [],
|
||||
getFolderWorkspaces: () => options.folderWorkspaces ?? [],
|
||||
getProjectGroups: () => options.projectGroups ?? []
|
||||
}
|
||||
return store
|
||||
}
|
||||
|
||||
describe('orchestration worker workspace resolution', () => {
|
||||
const tempPaths: string[] = []
|
||||
|
||||
beforeEach(() => {
|
||||
scanLocalRepoWorktreesForResolution.mockReset().mockResolvedValue({
|
||||
ok: true,
|
||||
worktrees: [
|
||||
{
|
||||
path: WORKTREE_PATH,
|
||||
head: 'abc',
|
||||
branch: 'feature',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
]
|
||||
})
|
||||
getSshGitProvider.mockReset()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(tempPaths.splice(0).map((path) => rm(path, { recursive: true, force: true })))
|
||||
})
|
||||
|
||||
it.each([
|
||||
['full id', `id:${WORKTREE_ID}`],
|
||||
['path', `path:${WORKTREE_PATH}`],
|
||||
['name', 'name:Feature']
|
||||
])('resolves a local worktree by %s with one catalog scan', async (_label, selector) => {
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace(selector)).resolves.toMatchObject({
|
||||
id: WORKTREE_ID,
|
||||
path: WORKTREE_PATH
|
||||
})
|
||||
expect(scanLocalRepoWorktreesForResolution).toHaveBeenCalledOnce()
|
||||
})
|
||||
|
||||
it('preserves a disconnected SSH worktree with unknown legacy ownership', async () => {
|
||||
const remoteRepo = {
|
||||
id: REPO_ID,
|
||||
path: REPO_PATH,
|
||||
displayName: 'Remote app',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
} satisfies Repo
|
||||
const runtime = new OrcaRuntimeService(
|
||||
makeStore({
|
||||
repos: [remoteRepo],
|
||||
meta: { [WORKTREE_ID]: makeMeta('Remote feature') }
|
||||
}) as never
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace(`id:${WORKTREE_ID}`)).resolves.toMatchObject({
|
||||
id: WORKTREE_ID,
|
||||
hostId: 'ssh:ssh-1'
|
||||
})
|
||||
})
|
||||
|
||||
it('does not fall back from the floating terminal sentinel to another workspace', async () => {
|
||||
const runtime = new OrcaRuntimeService(makeStore() as never)
|
||||
|
||||
await expect(
|
||||
runtime.showManagedTerminalWorkspace(`id:${FLOATING_TERMINAL_WORKTREE_ID}`)
|
||||
).rejects.toThrow('selector_not_found')
|
||||
expect(scanLocalRepoWorktreesForResolution).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects an ambiguous worktree name', async () => {
|
||||
const secondPath = '/repo/other'
|
||||
scanLocalRepoWorktreesForResolution.mockResolvedValue({
|
||||
ok: true,
|
||||
worktrees: [
|
||||
{ path: WORKTREE_PATH, head: 'a', branch: 'one', isBare: false, isMainWorktree: false },
|
||||
{ path: secondPath, head: 'b', branch: 'two', isBare: false, isMainWorktree: false }
|
||||
]
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(
|
||||
makeStore({
|
||||
meta: {
|
||||
[WORKTREE_ID]: makeMeta('Duplicate'),
|
||||
[`${REPO_ID}::${secondPath}`]: makeMeta('Duplicate')
|
||||
}
|
||||
}) as never
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace('name:Duplicate')).rejects.toThrow(
|
||||
'selector_ambiguous'
|
||||
)
|
||||
})
|
||||
|
||||
it('rejects the same worktree path on different execution hosts', async () => {
|
||||
const remoteRepo = {
|
||||
id: 'repo-remote',
|
||||
path: '/remote-repo',
|
||||
displayName: 'Remote app',
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
connectionId: 'ssh-1'
|
||||
} satisfies Repo
|
||||
getSshGitProvider.mockReturnValue({
|
||||
listWorktrees: vi.fn().mockResolvedValue([
|
||||
{
|
||||
path: WORKTREE_PATH,
|
||||
head: 'remote',
|
||||
branch: 'remote-feature',
|
||||
isBare: false,
|
||||
isMainWorktree: false
|
||||
}
|
||||
])
|
||||
})
|
||||
const runtime = new OrcaRuntimeService(
|
||||
makeStore({
|
||||
repos: [makeStore().getRepos()[0], remoteRepo],
|
||||
meta: {
|
||||
[WORKTREE_ID]: makeMeta('Local feature'),
|
||||
[`${remoteRepo.id}::${WORKTREE_PATH}`]: makeMeta('Remote feature')
|
||||
}
|
||||
}) as never
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace(`path:${WORKTREE_PATH}`)).rejects.toThrow(
|
||||
'selector_ambiguous'
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves local and SSH folder workspaces without a Git catalog scan', async () => {
|
||||
const localPath = await mkdtemp(join(tmpdir(), 'orca-worker-local-folder-'))
|
||||
tempPaths.push(localPath)
|
||||
const group = { id: 'group-1', name: 'Group', parentPath: localPath } as ProjectGroup
|
||||
const localFolder = {
|
||||
id: 'local-folder',
|
||||
projectGroupId: group.id,
|
||||
name: 'Local folder',
|
||||
folderPath: localPath
|
||||
} as FolderWorkspace
|
||||
const remoteFolder = {
|
||||
...localFolder,
|
||||
id: 'remote-folder',
|
||||
name: 'Remote folder',
|
||||
folderPath: '/srv/app',
|
||||
connectionId: 'ssh-folder'
|
||||
}
|
||||
registerSshFilesystemProvider('ssh-folder', {
|
||||
stat: vi.fn().mockResolvedValue({ type: 'directory', size: 0, mtime: 1 })
|
||||
} as never)
|
||||
try {
|
||||
const runtime = new OrcaRuntimeService(
|
||||
makeStore({
|
||||
folderWorkspaces: [localFolder, remoteFolder],
|
||||
projectGroups: [group]
|
||||
}) as never
|
||||
)
|
||||
|
||||
await expect(
|
||||
runtime.showManagedTerminalWorkspace('folder:local-folder')
|
||||
).resolves.toMatchObject({
|
||||
id: 'folder:local-folder',
|
||||
path: localPath,
|
||||
hostId: 'local'
|
||||
})
|
||||
await expect(
|
||||
runtime.showManagedTerminalWorkspace('id:folder:remote-folder')
|
||||
).resolves.toMatchObject({ id: 'folder:remote-folder', hostId: 'ssh:ssh-folder' })
|
||||
} finally {
|
||||
unregisterSshFilesystemProvider('ssh-folder')
|
||||
}
|
||||
expect(scanLocalRepoWorktreesForResolution).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('rejects a folder workspace whose execution host is ambiguous', async () => {
|
||||
const folderPath = '/workspace'
|
||||
const group = { id: 'group-1', name: 'Group', parentPath: folderPath } as ProjectGroup
|
||||
const folder = {
|
||||
id: 'folder-1',
|
||||
projectGroupId: group.id,
|
||||
name: 'Ambiguous folder',
|
||||
folderPath
|
||||
} as FolderWorkspace
|
||||
const repos = [
|
||||
{ id: 'local', path: '/workspace/local', projectGroupId: group.id },
|
||||
{ id: 'remote', path: '/workspace/remote', projectGroupId: group.id, connectionId: 'ssh-1' }
|
||||
].map((repo) => ({
|
||||
displayName: repo.id,
|
||||
badgeColor: 'blue',
|
||||
addedAt: 1,
|
||||
...repo
|
||||
})) as Repo[]
|
||||
const runtime = new OrcaRuntimeService(
|
||||
makeStore({ repos, folderWorkspaces: [folder], projectGroups: [group] }) as never
|
||||
)
|
||||
|
||||
await expect(runtime.showManagedTerminalWorkspace('folder:folder-1')).rejects.toThrow(
|
||||
'folder_workspace_connection_ambiguous'
|
||||
)
|
||||
expect(scanLocalRepoWorktreesForResolution).not.toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
@@ -15,17 +15,17 @@ describe('federated worker agent launch', () => {
|
||||
vi.restoreAllMocks()
|
||||
})
|
||||
|
||||
it('creates the remote worker terminal from the agent id, never as a command', async () => {
|
||||
it('creates an exact folder worker terminal from the agent id, never as a command', async () => {
|
||||
db = new OrchestrationDb(':memory:')
|
||||
const runtime = new OrcaRuntimeService()
|
||||
runtime.setOrchestrationDb(db)
|
||||
vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {})
|
||||
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({
|
||||
id: 'repo::remote-worktree'
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({
|
||||
id: 'folder:remote-workspace'
|
||||
} as never)
|
||||
const createTerminal = vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
handle: 'term_remote_worker',
|
||||
worktreeId: 'repo::remote-worktree',
|
||||
worktreeId: 'folder:remote-workspace',
|
||||
title: 'worker'
|
||||
})
|
||||
vi.spyOn(runtime, 'waitForTerminal').mockResolvedValue({
|
||||
@@ -60,7 +60,7 @@ describe('federated worker agent launch', () => {
|
||||
taskId: 'task_remote',
|
||||
taskSpec: 'remote cursor worker',
|
||||
protocolVersion: 3,
|
||||
worktree: 'id:repo::remote-worktree',
|
||||
worktree: 'folder:remote-workspace',
|
||||
agent: 'cursor',
|
||||
model: 'gpt-5.3-codex',
|
||||
effort: 'high'
|
||||
@@ -91,14 +91,14 @@ describe('federated worker agent launch', () => {
|
||||
}
|
||||
})
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
'id:repo::remote-worktree',
|
||||
'id:folder:remote-workspace',
|
||||
expect.objectContaining({
|
||||
startupAgent: 'cursor',
|
||||
launchPreferences: { model: 'gpt-5.3-codex', effort: 'high' }
|
||||
})
|
||||
)
|
||||
expect(createTerminal).toHaveBeenCalledWith(
|
||||
'id:repo::remote-worktree',
|
||||
'id:folder:remote-workspace',
|
||||
expect.not.objectContaining({ command: expect.anything() })
|
||||
)
|
||||
})
|
||||
|
||||
@@ -129,7 +129,7 @@ export const ORCHESTRATION_FEDERATION_ATTACH_METHODS: RpcMethod[] = [
|
||||
)
|
||||
appendFederationSetupEffect(effects, setup)
|
||||
} else {
|
||||
worktree = await runtime.showManagedWorktree(params.worktree).catch(() => {
|
||||
worktree = await runtime.showManagedTerminalWorkspace(params.worktree).catch(() => {
|
||||
throw new OrchestrationError(
|
||||
'worktree_not_found_on_server',
|
||||
`Worktree ${params.worktree} was not found on the selected worker server.`
|
||||
|
||||
@@ -48,7 +48,7 @@ describe('orchestration worker release recovery', () => {
|
||||
vi.spyOn(runtime, 'showTerminal').mockImplementation(
|
||||
async (handle) => ({ handle, worktreeId: 'repo::worktree', status: 'running' }) as never
|
||||
)
|
||||
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({
|
||||
id: 'repo::worktree'
|
||||
} as never)
|
||||
vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
|
||||
@@ -61,7 +61,7 @@ describe('orchestration worker release', () => {
|
||||
vi.spyOn(runtime, 'showTerminal').mockImplementation(
|
||||
async (handle) => ({ handle, worktreeId: 'repo::worktree', status: 'running' }) as never
|
||||
)
|
||||
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({
|
||||
id: 'repo::worktree'
|
||||
} as never)
|
||||
vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
|
||||
@@ -60,21 +60,21 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
|
||||
const { agent, launch } = prepareLocalWorkerStart({ params, createsWorktree, runtime })
|
||||
|
||||
const coordinatorTerminal = await runtime.showTerminal(params.from)
|
||||
const coordinatorWorktree = await runtime.showManagedWorktree(
|
||||
`id:${coordinatorTerminal.worktreeId}`
|
||||
)
|
||||
if (createsWorktree) {
|
||||
const creationWorktree = createsWorktree
|
||||
? await runtime.showManagedWorktree(`id:${coordinatorTerminal.worktreeId}`)
|
||||
: undefined
|
||||
if (creationWorktree) {
|
||||
await assertOrchestrationWorktreeCreationSupported({
|
||||
runtime,
|
||||
repoSelector: params.repo ?? coordinatorWorktree.repoId,
|
||||
repoSelector: params.repo ?? creationWorktree.repoId,
|
||||
existingPlacement: 'current or an exact existing folder workspace'
|
||||
})
|
||||
}
|
||||
let resolvedWorktree = createsWorktree
|
||||
let resolvedWorktree = creationWorktree
|
||||
? undefined
|
||||
: requestedWorktree === 'current'
|
||||
? coordinatorWorktree
|
||||
: await runtime.showManagedWorktree(requestedWorktree)
|
||||
? await runtime.showManagedTerminalWorkspace(`id:${coordinatorTerminal.worktreeId}`)
|
||||
: await runtime.showManagedTerminalWorkspace(requestedWorktree)
|
||||
let explicitTerminal
|
||||
if (params.terminal) {
|
||||
explicitTerminal = await runtime.showTerminal(params.terminal)
|
||||
@@ -96,7 +96,7 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
|
||||
worktree: requestedWorktree,
|
||||
resolvedWorktreeId: resolvedWorktree?.id ?? null,
|
||||
name: params.name ?? null,
|
||||
repo: params.repo ?? (createsWorktree ? coordinatorWorktree.repoId : null),
|
||||
repo: params.repo ?? creationWorktree?.repoId ?? null,
|
||||
baseBranch: params.baseBranch ?? null,
|
||||
terminal: params.terminal ?? null,
|
||||
agent: agent ?? null,
|
||||
@@ -135,14 +135,14 @@ export const ORCHESTRATION_WORKER_START_METHODS: RpcMethod[] = [
|
||||
state: 'not_applicable'
|
||||
}
|
||||
try {
|
||||
if (createsWorktree) {
|
||||
if (creationWorktree) {
|
||||
failedStage = 'worktree_create'
|
||||
const created = await createWorkerWorktree({
|
||||
runtime,
|
||||
db,
|
||||
dispatchId: started.dispatch.id,
|
||||
requestedWorktree,
|
||||
coordinatorWorktree,
|
||||
coordinatorWorktree: creationWorktree,
|
||||
params,
|
||||
agent: agent as TuiAgent,
|
||||
launchPreferences: launch.preferences,
|
||||
|
||||
@@ -9,6 +9,7 @@ import { OrcaRuntimeService } from '../../orca-runtime'
|
||||
import type { RuntimeTerminalSummary } from '../../../../shared/runtime-types'
|
||||
import { ORCHESTRATION_ASK_MAX_TIMEOUT_MS } from '../../../../shared/orchestration-ask-timeout'
|
||||
import { ORCHESTRATION_CONTRACT_VERSION } from '../../../../shared/protocol-version'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
|
||||
function lifecycleGroupRecipientError(type: 'worker_done' | 'heartbeat'): string {
|
||||
return `${type} messages belong to one exact Dispatch and cannot target a group address.`
|
||||
@@ -2180,6 +2181,9 @@ describe('orchestration RPC methods', () => {
|
||||
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({
|
||||
id: 'repo::worktree'
|
||||
} as never)
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({
|
||||
id: 'repo::worktree'
|
||||
} as never)
|
||||
vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
handle: 'term_worker',
|
||||
worktreeId: 'repo::worktree',
|
||||
@@ -2381,17 +2385,25 @@ describe('orchestration RPC methods', () => {
|
||||
expect(runtime.sendTerminalAgentPrompt).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('starts a fresh agent in an exact existing worktree without replaying setup', async () => {
|
||||
it('starts in an exact existing worktree from a floating coordinator', async () => {
|
||||
setup()
|
||||
mockCurrentWorkerStart()
|
||||
const createWorktree = vi.spyOn(runtime, 'createManagedWorktree')
|
||||
vi.mocked(runtime.showManagedWorktree).mockImplementation(
|
||||
async (selector) =>
|
||||
({
|
||||
id: selector === 'id:repo::other' ? 'repo::other' : 'repo::worktree',
|
||||
repoId: 'repo'
|
||||
}) as never
|
||||
)
|
||||
vi.mocked(runtime.showTerminal).mockResolvedValue({
|
||||
handle: 'term_coord',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
status: 'running'
|
||||
} as never)
|
||||
vi.mocked(runtime.showManagedWorktree).mockImplementation(async (selector) => {
|
||||
if (selector === `id:${FLOATING_TERMINAL_WORKTREE_ID}`) {
|
||||
throw new Error('selector_not_found')
|
||||
}
|
||||
return { id: 'repo::other', repoId: 'repo' } as never
|
||||
})
|
||||
vi.mocked(runtime.showManagedTerminalWorkspace).mockResolvedValue({
|
||||
id: 'repo::other',
|
||||
repoId: 'repo'
|
||||
} as never)
|
||||
const task = db.createTask({ spec: 'existing worktree worker' })
|
||||
|
||||
const result = (await call('orchestration.workerStart', {
|
||||
@@ -2415,6 +2427,41 @@ describe('orchestration RPC methods', () => {
|
||||
expect.objectContaining({ startupAgent: 'codex', surfaceOwner: false })
|
||||
)
|
||||
expect(createWorktree).not.toHaveBeenCalled()
|
||||
expect(runtime.showTerminal).toHaveBeenCalledWith('term_coord')
|
||||
expect(runtime.showManagedWorktree).not.toHaveBeenCalledWith(
|
||||
`id:${FLOATING_TERMINAL_WORKTREE_ID}`
|
||||
)
|
||||
expect(runtime.showManagedTerminalWorkspace).toHaveBeenCalledOnce()
|
||||
expect(runtime.showManagedTerminalWorkspace).toHaveBeenCalledWith('id:repo::other')
|
||||
})
|
||||
|
||||
it('starts in an exact existing folder workspace from a floating coordinator', async () => {
|
||||
setup()
|
||||
mockCurrentWorkerStart()
|
||||
vi.mocked(runtime.showTerminal).mockResolvedValue({
|
||||
handle: 'term_coord',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
status: 'running'
|
||||
} as never)
|
||||
vi.mocked(runtime.showManagedWorktree).mockRejectedValue(new Error('selector_not_found'))
|
||||
vi.mocked(runtime.showManagedTerminalWorkspace).mockResolvedValue({
|
||||
id: 'folder:workspace-1',
|
||||
repoId: 'folder-workspace:group-1'
|
||||
} as never)
|
||||
const task = db.createTask({ spec: 'folder workspace worker' })
|
||||
|
||||
await expect(
|
||||
call('orchestration.workerStart', {
|
||||
task: task.id,
|
||||
from: 'term_coord',
|
||||
worktree: 'folder:workspace-1',
|
||||
agent: 'codex'
|
||||
})
|
||||
).resolves.toMatchObject({ state: 'ready' })
|
||||
expect(runtime.createTerminal).toHaveBeenCalledWith(
|
||||
'id:folder:workspace-1',
|
||||
expect.objectContaining({ startupAgent: 'codex', surfaceOwner: false })
|
||||
)
|
||||
})
|
||||
|
||||
it('reuses only an explicitly selected existing agent terminal', async () => {
|
||||
|
||||
@@ -269,7 +269,7 @@ async function releaseCompletedWorker(terminalState: 'running' | 'exited'): Prom
|
||||
: null
|
||||
)
|
||||
vi.spyOn(runtime, 'validateOrchestrationAgentLauncher').mockImplementation(() => {})
|
||||
vi.spyOn(runtime, 'showManagedWorktree').mockResolvedValue({ id: WORKTREE_ID } as never)
|
||||
vi.spyOn(runtime, 'showManagedTerminalWorkspace').mockResolvedValue({ id: WORKTREE_ID } as never)
|
||||
vi.spyOn(runtime, 'createTerminal').mockResolvedValue({
|
||||
handle: TERMINAL_HANDLE,
|
||||
worktreeId: WORKTREE_ID,
|
||||
|
||||
Reference in New Issue
Block a user