mirror of
https://github.com/stablyai/orca.git
synced 2026-10-19 00:03:20 +00:00
* fix(orchestration): complete worker tasks and improve coordinator UX
* Fix orchestration lifecycle sender resolution and peek/check compat hand
- Lifecycle sends (worker_done/heartbeat) now use ORCA_TERMINAL_HANDLE
verbatim, skipping the liveness probe and pane remint that could
block delivery during restarts or mismatch stale-runtime assignee
handles.
- --peek now round-trips as {peek:true, unread:false} so older runtimes
that strip unknown params degrade to non-destructive "all" instead of
mark-read, with client-side filtering to restore peek semantics and a
clear error when --peek --wait can't be honored.
- Reject combined read-mode flags (--unread/--peek/--all) before calling
the runtime.
- Distinguish suppressed (already-consumed) lifecycle messages from
ignored ones so send doesn't wake --wait waiters for stale heartbeats.
- Fix task summary truncation to avoid splitting UTF-16 surrogate pairs
and to not misreport whitespace normalization as truncation.
* Add shared helper to abbreviate orchestration task specs for brief listi
- Normalizes whitespace and caps spec length at 160 chars, flagging
truncation separately from whitespace-only changes
- Truncates on UTF-16 code point boundaries to avoid splitting
surrogate pairs and emitting malformed strings
* Add pane-key identity to worker_done/heartbeat reconciliation and server
- Records the sender's pane key on messages and dispatch contexts so
worker_done/heartbeat ownership can be verified by the remint-stable
pane leaf instead of the terminal handle, which is reissued across
restarts.
- Rejects lifecycle messages from a genuinely foreign pane while still
tolerating handle remints, tab break-outs, and older CLIs that lack
pane identity.
- Moves task-spec abbreviation server-side (orchestration.taskList
--brief) so full specs no longer cross SSH/relay transports, with a
client-side fallback for older runtimes; consolidates the shared
abbreviation helper under src/shared.
- Adds a stderr warning when a pre-peek runtime's --peek response hits
the 100-row cap, since older unread messages may be missing.
* Isolate ORCA_PANE_KEY in CLI test beforeEach to fix leaked senderPaneKey
Co-authored-by: Orca <help@stably.ai>
* Fix pane-key remint bypassing dispatch mutual-exclusion lock
- Dispatch locking only matched on assignee_handle, so a reminted
terminal handle (tab break-out) could open a second concurrent
dispatch on the same pane.
- Add leaf-UUID-based pane key comparison (parsePaneKey) as a
secondary lock, falling back to exact handle match for legacy
rows without pane keys.
* Update orchestration skill docs for lifecycle authority and CLI flag add
- Clarify that dispatch lifecycle is tied to taskId+dispatchId verified against
the dispatched pane, not the terminal handle, since handles can be reminted
after restart
- Document new `check --peek`/`--all` and `task-list --brief` flags, with
fallback guidance for older CLIs that reject them
- Note that a valid worker_done auto-completes the task/dispatch, so workers
shouldn't also call task-update manually
---------
Co-authored-by: Orca <help@stably.ai>
832 lines
28 KiB
TypeScript
832 lines
28 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { RpcDispatcher } from '../dispatcher'
|
|
import type { RpcRequest } from '../core'
|
|
import type { OrcaRuntimeService } from '../../orca-runtime'
|
|
import { WORKTREE_METHODS } from './worktree'
|
|
import { createAutomationDispatchToken } from '../../../automations/dispatch-tokens'
|
|
|
|
const repo = {
|
|
id: 'repo-1',
|
|
path: '/workspace/repo',
|
|
displayName: 'repo',
|
|
badgeColor: '#000',
|
|
addedAt: 1,
|
|
kind: 'git' as const,
|
|
executionHostId: 'ssh:ssh-target-1' as const
|
|
}
|
|
|
|
function makeRequest(method: string, params?: unknown): RpcRequest {
|
|
return { id: 'req-1', authToken: 'tok', method, params }
|
|
}
|
|
|
|
describe('worktree RPC methods', () => {
|
|
it('routes mobile session-only activation without notifying desktop clients', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
activateManagedWorktree: vi
|
|
.fn()
|
|
.mockResolvedValue({ repoId: 'repo-1', worktreeId: 'wt-1', activated: true })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.activate', {
|
|
worktree: 'id:wt-1',
|
|
notifyClients: false
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.activateManagedWorktree).toHaveBeenCalledWith('id:wt-1', {
|
|
notifyClients: false,
|
|
clientKind: undefined
|
|
})
|
|
})
|
|
|
|
it('forwards the mobile clientKind to the runtime on session-only activation', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
activateManagedWorktree: vi
|
|
.fn()
|
|
.mockResolvedValue({ repoId: 'repo-1', worktreeId: 'wt-1', activated: true })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
// The mobile WebSocket path always uses dispatchStreaming, which threads the
|
|
// authenticated device scope as clientKind even for non-streaming methods.
|
|
const replies: string[] = []
|
|
await dispatcher.dispatchStreaming(
|
|
makeRequest('worktree.activate', { worktree: 'id:wt-1', notifyClients: false }),
|
|
(response) => replies.push(response),
|
|
{ clientKind: 'mobile' }
|
|
)
|
|
|
|
expect(runtime.activateManagedWorktree).toHaveBeenCalledWith('id:wt-1', {
|
|
notifyClients: false,
|
|
clientKind: 'mobile'
|
|
})
|
|
})
|
|
|
|
it('routes dirty-file force to the runtime server', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
removeManagedWorktree: vi.fn().mockResolvedValue({})
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.rm', {
|
|
worktree: 'id:wt-1',
|
|
force: true,
|
|
runHooks: false
|
|
})
|
|
)
|
|
|
|
expect(runtime.removeManagedWorktree).toHaveBeenCalledWith('id:wt-1', true, false)
|
|
expect(response).toMatchObject({ ok: true, result: { removed: true } })
|
|
})
|
|
|
|
it('routes create options to the runtime server', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'feature',
|
|
branchNameOverride: 'feature/something',
|
|
baseBranch: 'origin/main',
|
|
compareBaseRef: undefined,
|
|
setupDecision: 'skip',
|
|
displayName: 'Feature title',
|
|
telemetrySource: 'sidebar',
|
|
workspaceStatus: 'in-review',
|
|
manualOrder: 123_456,
|
|
linkedIssue: 123,
|
|
linkedPR: 456,
|
|
linkedGitLabIssue: 789,
|
|
linkedGitLabMR: 321,
|
|
sparseCheckout: { directories: ['src'], presetId: 'preset-1' },
|
|
pushTarget: { remoteName: 'fork', branchName: 'feature' },
|
|
parentWorktree: 'id:parent'
|
|
})
|
|
)
|
|
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith({
|
|
repoSelector: 'repo-1',
|
|
name: 'feature',
|
|
branchNameOverride: 'feature/something',
|
|
baseBranch: 'origin/main',
|
|
linkedIssue: 123,
|
|
linkedPR: 456,
|
|
linkedLinearIssue: undefined,
|
|
linkedLinearIssueWorkspaceId: undefined,
|
|
linkedLinearIssueOrganizationUrlKey: undefined,
|
|
linkedGitLabIssue: 789,
|
|
linkedGitLabMR: 321,
|
|
linkedBitbucketPR: undefined,
|
|
linkedAzureDevOpsPR: undefined,
|
|
linkedGiteaPR: undefined,
|
|
comment: undefined,
|
|
displayName: 'Feature title',
|
|
telemetrySource: 'sidebar',
|
|
workspaceStatus: 'in-review',
|
|
manualOrder: 123_456,
|
|
sparseCheckout: { directories: ['src'], presetId: 'preset-1' },
|
|
pushTarget: { remoteName: 'fork', branchName: 'feature' },
|
|
runHooks: false,
|
|
activate: false,
|
|
setupDecision: 'skip',
|
|
createdWithAgent: undefined,
|
|
automationProvenance: undefined,
|
|
startup: undefined,
|
|
startupDraft: undefined,
|
|
lineage: {
|
|
parentWorktree: 'id:parent',
|
|
noParent: false,
|
|
callerTerminalHandle: undefined,
|
|
orchestrationContext: undefined
|
|
}
|
|
})
|
|
})
|
|
|
|
it('mints automation provenance from a valid dispatch request on worktree creation', async () => {
|
|
const dispatchToken = createAutomationDispatchToken('automation-1', 'run-1')
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
showAutomation: vi.fn(() => ({
|
|
id: 'automation-1',
|
|
name: 'Nightly review',
|
|
projectId: 'legacy-repo-1',
|
|
runContext: {
|
|
projectId: 'project-1',
|
|
repoId: 'repo-1',
|
|
hostId: 'ssh:ssh-target-1'
|
|
},
|
|
workspaceMode: 'new_per_run',
|
|
executionTargetType: 'ssh',
|
|
executionTargetId: 'ssh-target-1'
|
|
})),
|
|
listAutomationRuns: vi.fn(() => [
|
|
{
|
|
id: 'run-1',
|
|
automationId: 'automation-1',
|
|
title: 'Nightly review run',
|
|
status: 'dispatching',
|
|
workspaceId: null
|
|
}
|
|
]),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'automation-workspace',
|
|
automationProvenanceRequest: {
|
|
automationId: 'automation-1',
|
|
automationRunId: 'run-1',
|
|
dispatchToken,
|
|
createRequestId: 'create-request-1'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
const replay = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'automation-workspace-replay',
|
|
automationProvenanceRequest: {
|
|
automationId: 'automation-1',
|
|
automationRunId: 'run-1',
|
|
dispatchToken,
|
|
createRequestId: 'create-request-1'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(replay).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(1)
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
repoSelector: 'repo-1',
|
|
name: 'automation-workspace',
|
|
automationProvenance: expect.objectContaining({
|
|
kind: 'created-by-automation',
|
|
automationId: 'automation-1',
|
|
automationNameSnapshot: 'Nightly review',
|
|
automationRunId: 'run-1',
|
|
automationRunTitleSnapshot: 'Nightly review run',
|
|
executionTargetType: 'ssh',
|
|
executionTargetId: 'ssh-target-1',
|
|
projectId: 'project-1',
|
|
repoId: 'repo-1',
|
|
hostId: 'ssh:ssh-target-1'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('stamps automation provenance with the persisted runtime host from run context', async () => {
|
|
const dispatchToken = createAutomationDispatchToken('automation-runtime', 'run-runtime')
|
|
const runtimeLocalRepo = {
|
|
id: 'repo-runtime',
|
|
path: '/workspace/repo',
|
|
displayName: 'repo',
|
|
badgeColor: '#000',
|
|
addedAt: 1,
|
|
kind: 'git' as const
|
|
}
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(runtimeLocalRepo),
|
|
showAutomation: vi.fn(() => ({
|
|
id: 'automation-runtime',
|
|
name: 'Runtime review',
|
|
projectId: 'legacy-repo-runtime',
|
|
runContext: {
|
|
projectId: 'project-runtime',
|
|
repoId: 'repo-runtime',
|
|
hostId: 'runtime:owner-runtime'
|
|
},
|
|
workspaceMode: 'new_per_run',
|
|
executionTargetType: 'runtime',
|
|
executionTargetId: 'owner-runtime'
|
|
})),
|
|
listAutomationRuns: vi.fn(() => [
|
|
{
|
|
id: 'run-runtime',
|
|
automationId: 'automation-runtime',
|
|
title: 'Runtime review run',
|
|
status: 'dispatching',
|
|
workspaceId: null
|
|
}
|
|
]),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-runtime' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-runtime',
|
|
name: 'runtime-automation-workspace',
|
|
automationProvenanceRequest: {
|
|
automationId: 'automation-runtime',
|
|
automationRunId: 'run-runtime',
|
|
dispatchToken,
|
|
createRequestId: 'create-request-runtime'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
automationProvenance: expect.objectContaining({
|
|
automationId: 'automation-runtime',
|
|
automationRunId: 'run-runtime',
|
|
repoId: 'repo-runtime',
|
|
hostId: 'runtime:owner-runtime'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('validates and stamps automation provenance from the dispatching run snapshot', async () => {
|
|
const dispatchToken = createAutomationDispatchToken('automation-edited', 'run-edited')
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
showAutomation: vi.fn(() => ({
|
|
id: 'automation-edited',
|
|
name: 'Edited review',
|
|
projectId: 'legacy-repo-edited',
|
|
runContext: {
|
|
projectId: 'project-after-edit',
|
|
repoId: 'repo-after-edit',
|
|
hostId: 'runtime:after-edit'
|
|
},
|
|
workspaceMode: 'new_per_run',
|
|
executionTargetType: 'runtime',
|
|
executionTargetId: 'after-edit'
|
|
})),
|
|
listAutomationRuns: vi.fn(() => [
|
|
{
|
|
id: 'run-edited',
|
|
automationId: 'automation-edited',
|
|
title: 'Pre-edit run',
|
|
runContext: {
|
|
projectId: 'project-before-edit',
|
|
repoId: 'repo-1',
|
|
hostId: 'runtime:before-edit'
|
|
},
|
|
status: 'dispatching',
|
|
workspaceId: null
|
|
}
|
|
]),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-edited' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'edited-automation-workspace',
|
|
automationProvenanceRequest: {
|
|
automationId: 'automation-edited',
|
|
automationRunId: 'run-edited',
|
|
dispatchToken,
|
|
createRequestId: 'create-request-edited'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
automationProvenance: expect.objectContaining({
|
|
automationId: 'automation-edited',
|
|
automationRunId: 'run-edited',
|
|
projectId: 'project-before-edit',
|
|
repoId: 'repo-1',
|
|
hostId: 'runtime:before-edit'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('allows the same automation provenance request to retry after a failed create attempt', async () => {
|
|
const dispatchToken = createAutomationDispatchToken('automation-retry', 'run-retry')
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
showAutomation: vi.fn(() => ({
|
|
id: 'automation-retry',
|
|
name: 'Nightly retry',
|
|
projectId: 'repo-1',
|
|
workspaceMode: 'new_per_run',
|
|
executionTargetType: 'ssh',
|
|
executionTargetId: 'ssh-target-1'
|
|
})),
|
|
listAutomationRuns: vi.fn(() => [
|
|
{
|
|
id: 'run-retry',
|
|
automationId: 'automation-retry',
|
|
title: 'Nightly retry run',
|
|
status: 'dispatching',
|
|
workspaceId: null
|
|
}
|
|
]),
|
|
createManagedWorktree: vi
|
|
.fn()
|
|
.mockRejectedValueOnce(new Error('Branch "automation-workspace" already exists.'))
|
|
.mockResolvedValueOnce({ worktree: { id: 'wt-retry' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
const automationProvenanceRequest = {
|
|
automationId: 'automation-retry',
|
|
automationRunId: 'run-retry',
|
|
dispatchToken,
|
|
createRequestId: 'create-request-retry'
|
|
}
|
|
|
|
const firstResponse = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'automation-workspace',
|
|
automationProvenanceRequest
|
|
})
|
|
)
|
|
const retryResponse = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'automation-workspace-2',
|
|
automationProvenanceRequest
|
|
})
|
|
)
|
|
|
|
expect(firstResponse).toMatchObject({ ok: false })
|
|
expect(retryResponse).toMatchObject({ ok: true })
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledTimes(2)
|
|
expect(runtime.createManagedWorktree).toHaveBeenLastCalledWith(
|
|
expect.objectContaining({
|
|
name: 'automation-workspace-2',
|
|
automationProvenance: expect.objectContaining({
|
|
automationId: 'automation-retry',
|
|
automationRunId: 'run-retry'
|
|
})
|
|
})
|
|
)
|
|
})
|
|
|
|
it('rejects forged automation provenance requests on worktree creation', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
showAutomation: vi.fn(() => ({
|
|
id: 'automation-1',
|
|
name: 'Nightly review',
|
|
projectId: 'repo-1',
|
|
workspaceMode: 'new_per_run',
|
|
executionTargetType: 'local',
|
|
executionTargetId: 'local'
|
|
})),
|
|
listAutomationRuns: vi.fn(() => [
|
|
{
|
|
id: 'run-1',
|
|
automationId: 'automation-1',
|
|
title: 'Nightly review run',
|
|
status: 'dispatching',
|
|
workspaceId: null
|
|
}
|
|
]),
|
|
createManagedWorktree: vi.fn()
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'manual-workspace',
|
|
automationProvenanceRequest: {
|
|
automationId: 'automation-1',
|
|
automationRunId: 'run-1',
|
|
dispatchToken: 'forged-token',
|
|
createRequestId: 'create-request-forged'
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
|
expect(runtime.createManagedWorktree).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('forwards startup command and env to runtime worktree creation', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({
|
|
worktree: { id: 'wt-1' },
|
|
startupTerminal: { spawned: true, handle: 'term_agent' }
|
|
})
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'agent-startup',
|
|
startupAgent: 'codex',
|
|
startupCommand: "codex 'summarize repo'",
|
|
startupCommandDelivery: 'shell-ready',
|
|
startupEnv: { ORCA_AGENT_MODE: 'direct' },
|
|
startupLaunchConfig: {
|
|
agentCommand: 'codex',
|
|
agentArgs: '--model gpt-5',
|
|
agentEnv: { ORCA_AGENT_MODE: 'direct' }
|
|
},
|
|
activate: true
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({
|
|
ok: true,
|
|
result: { agentTerminalHandle: 'term_agent' }
|
|
})
|
|
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
repoSelector: 'repo-1',
|
|
name: 'agent-startup',
|
|
activate: true,
|
|
startupAgent: 'codex',
|
|
startup: {
|
|
command: "codex 'summarize repo'",
|
|
startupCommandDelivery: 'shell-ready',
|
|
env: { ORCA_AGENT_MODE: 'direct' },
|
|
launchConfig: {
|
|
agentCommand: 'codex',
|
|
agentArgs: '--model gpt-5',
|
|
agentEnv: { ORCA_AGENT_MODE: 'direct' }
|
|
}
|
|
}
|
|
})
|
|
)
|
|
})
|
|
|
|
it('drops invalid startup launch config env at the runtime RPC boundary', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'agent-startup',
|
|
startupCommand: "codex 'summarize repo'",
|
|
startupLaunchConfig: {
|
|
agentCommand: 'codex',
|
|
agentArgs: '--model gpt-5',
|
|
agentEnv: { ['__proto__']: 'polluted' }
|
|
}
|
|
})
|
|
)
|
|
|
|
expect(response.ok).toBe(true)
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
startup: expect.objectContaining({
|
|
command: "codex 'summarize repo'"
|
|
})
|
|
})
|
|
)
|
|
expect(vi.mocked(runtime.createManagedWorktree).mock.calls[0]?.[0].startup).not.toHaveProperty(
|
|
'launchConfig'
|
|
)
|
|
})
|
|
|
|
it('forwards task startup drafts to runtime worktree creation', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'issue-123',
|
|
startupDraft: 'https://github.com/stablyai/orca/issues/123',
|
|
createdWithAgent: 'codex',
|
|
activate: true
|
|
})
|
|
)
|
|
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
repoSelector: 'repo-1',
|
|
name: 'issue-123',
|
|
activate: true,
|
|
createdWithAgent: 'codex',
|
|
startup: undefined,
|
|
startupDraft: 'https://github.com/stablyai/orca/issues/123'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('routes create-base prefetches to the runtime server', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
prefetchManagedWorktreeCreateBase: vi.fn().mockResolvedValue(undefined)
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.prefetchCreateBase', {
|
|
repo: 'repo-1',
|
|
baseBranch: 'origin/main'
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true, result: null })
|
|
expect(runtime.prefetchManagedWorktreeCreateBase).toHaveBeenCalledWith({
|
|
repoSelector: 'repo-1',
|
|
baseBranch: 'origin/main'
|
|
})
|
|
})
|
|
|
|
it('maps unknown telemetry sources to the runtime default instead of rejecting create', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
showRepo: vi.fn().mockResolvedValue(repo),
|
|
createManagedWorktree: vi.fn().mockResolvedValue({ worktree: { id: 'wt-1' } })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'feature',
|
|
telemetrySource: 'future_surface'
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.createManagedWorktree).toHaveBeenCalledWith(
|
|
expect.objectContaining({
|
|
repoSelector: 'repo-1',
|
|
name: 'feature',
|
|
telemetrySource: undefined
|
|
})
|
|
)
|
|
})
|
|
|
|
it('rejects worktree.create when both parent and no-parent are supplied', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
createManagedWorktree: vi.fn()
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.create', {
|
|
repo: 'repo-1',
|
|
name: 'child',
|
|
parentWorktree: 'id:parent',
|
|
noParent: true
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: false })
|
|
expect(JSON.stringify(response)).toContain('Choose either one parent selector or --no-parent')
|
|
expect(runtime.createManagedWorktree).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('passes explicit repo selectors to PR base resolution and preserves start-point fields', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
resolveManagedPrBase: vi.fn().mockResolvedValue({
|
|
baseBranch: 'abc123',
|
|
headSha: 'abc123',
|
|
branchNameOverride: 'feature/pr-head',
|
|
pushTarget: { remoteName: 'origin', branchName: 'feature/pr-head' }
|
|
})
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.resolvePrBase', {
|
|
repo: 'id:repo-1',
|
|
prNumber: 42,
|
|
headRefName: 'feature/pr-head',
|
|
isCrossRepository: false
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(response).toMatchObject({
|
|
result: {
|
|
baseBranch: 'abc123',
|
|
headSha: 'abc123',
|
|
branchNameOverride: 'feature/pr-head',
|
|
pushTarget: { remoteName: 'origin', branchName: 'feature/pr-head' }
|
|
}
|
|
})
|
|
expect(runtime.resolveManagedPrBase).toHaveBeenCalledWith({
|
|
repoSelector: 'id:repo-1',
|
|
prNumber: 42,
|
|
headRefName: 'feature/pr-head',
|
|
isCrossRepository: false
|
|
})
|
|
})
|
|
|
|
it('passes explicit repo selectors to MR base resolution', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
resolveManagedMrBase: vi.fn().mockResolvedValue({ baseBranch: 'origin/mr-head' })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.resolveMrBase', {
|
|
repo: 'id:repo-1',
|
|
mrIid: 42,
|
|
sourceBranch: 'feature/mr-head',
|
|
isCrossRepository: false
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.resolveManagedMrBase).toHaveBeenCalledWith({
|
|
repoSelector: 'id:repo-1',
|
|
mrIid: 42,
|
|
sourceBranch: 'feature/mr-head',
|
|
isCrossRepository: false
|
|
})
|
|
})
|
|
|
|
it('forwards Linear metadata through worktree.set', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
updateManagedWorktreeMeta: vi.fn().mockResolvedValue({ id: 'wt-1' })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.set', {
|
|
worktree: 'id:wt-1',
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: 'stably'
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.updateManagedWorktreeMeta).toHaveBeenCalledWith(
|
|
'id:wt-1',
|
|
expect.objectContaining({
|
|
linkedLinearIssue: 'STA-335',
|
|
linkedLinearIssueWorkspaceId: null,
|
|
linkedLinearIssueOrganizationUrlKey: 'stably'
|
|
})
|
|
)
|
|
})
|
|
|
|
it('forwards push target clears through worktree.set', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
updateManagedWorktreeMeta: vi.fn().mockResolvedValue({ id: 'wt-1' })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.set', {
|
|
worktree: 'id:wt-1',
|
|
linkedPR: null,
|
|
pushTarget: null
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: true })
|
|
expect(runtime.updateManagedWorktreeMeta).toHaveBeenCalledWith(
|
|
'id:wt-1',
|
|
expect.objectContaining({
|
|
linkedPR: null,
|
|
pushTarget: null
|
|
})
|
|
)
|
|
})
|
|
|
|
it('rejects worktree.set when both parent and no-parent are supplied', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
updateManagedWorktreeMeta: vi.fn()
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.set', {
|
|
worktree: 'id:child',
|
|
parentWorktree: 'id:parent',
|
|
noParent: true
|
|
})
|
|
)
|
|
|
|
expect(response).toMatchObject({ ok: false })
|
|
expect(JSON.stringify(response)).toContain('Choose either --parent-worktree or --no-parent')
|
|
expect(runtime.updateManagedWorktreeMeta).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('lists raw worktree lineage through the runtime server', async () => {
|
|
const lineage = {
|
|
'repo::/child': {
|
|
worktreeId: 'repo::/child',
|
|
worktreeInstanceId: 'child-instance',
|
|
parentWorktreeId: 'repo::/missing-parent',
|
|
parentWorktreeInstanceId: 'parent-instance',
|
|
origin: 'manual',
|
|
capture: { source: 'manual-action', confidence: 'explicit' },
|
|
createdAt: 1
|
|
}
|
|
}
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
listWorktreeLineage: vi.fn().mockResolvedValue(lineage),
|
|
listWorkspaceLineage: vi.fn().mockResolvedValue({})
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(makeRequest('worktree.lineageList'))
|
|
|
|
expect(runtime.listWorktreeLineage).toHaveBeenCalled()
|
|
expect(runtime.listWorkspaceLineage).toHaveBeenCalled()
|
|
expect(response).toMatchObject({ ok: true, result: { lineage, workspaceLineage: {} } })
|
|
})
|
|
|
|
it('persists smart sort order on the runtime server', async () => {
|
|
const runtime = {
|
|
getRuntimeId: () => 'test-runtime',
|
|
persistManagedWorktreeSortOrder: vi.fn().mockReturnValue({ updated: 2 })
|
|
} as unknown as OrcaRuntimeService
|
|
const dispatcher = new RpcDispatcher({ runtime, methods: WORKTREE_METHODS })
|
|
|
|
const response = await dispatcher.dispatch(
|
|
makeRequest('worktree.persistSortOrder', { orderedIds: ['wt-1', 'wt-2'] })
|
|
)
|
|
|
|
expect(runtime.persistManagedWorktreeSortOrder).toHaveBeenCalledWith(['wt-1', 'wt-2'])
|
|
expect(response).toMatchObject({ ok: true, result: { updated: 2 } })
|
|
})
|
|
})
|