test: remove 184 low-value mobile, CLI and ACP tests (#38733)

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
OrcaWin
2026-11-05 16:02:19 -08:00
committed by GitHub
co-authored by OrcaWin
parent 925cc80507
commit 27b1ed858c
21 changed files with 8 additions and 5375 deletions
+6 -5
View File
@@ -4769,7 +4769,7 @@
"providers": ["local", "daemon", "ssh"],
"coveredPlatforms": ["macos"],
"coveredProviders": ["local", "daemon", "ssh"],
"coverageNotes": "Deterministic unit coverage exercises activation gating, single-instance ownership, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, the promoted renderer's agent-resume accounting, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. Supervisor settlement coverage prevents a late handoff-completion failure from rearming termination after the replacement child exits. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.",
"coverageNotes": "Audit1589 removes mock-only CLI forwarding, canned provisioning/catalog/error projections and private inventories without equivalent replacement coverage. Retained cases exercise actual local markdown/paste/reconciliation, stream framing/backpressure/output-drain, temporary directory cleanup and persisted handoff with injected host/provider/process boundaries. No real Jira, provisioning, remote launch, native pipe/process-tree authority or deployed provider catalog proof. Assertion references use the surviving handoff registration titles; changed-selector historical receipts are retired and unaffected receipts remain unchanged. Historical scope before this audit: Deterministic unit coverage exercises activation gating, single-instance ownership, local/remote CLI status, headless binding persistence, local daemon identity, SSH identity transfer, the promoted renderer's agent-resume accounting, and the macOS serve update handoff from staged installer through atomic bundle replacement and target-version readiness. Supervisor settlement coverage prevents a late handoff-completion failure from rearming termination after the replacement child exits. A macOS Electron journey covers headless promotion and persistent PTY identity. A disposable locally signed Electron canary exercised real ShipIt and a temporary LaunchAgent with the compiled production supervisor; full packaged Orca and Linux/Windows serve updates remain uncollected. Audit1383: src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts reduced. Lose command-list/receipt/default/background activation matrices whose provider spawn returns canned IDs. Retained test proves actual tab allocation separation, not terminal startup. No equivalent replacement coverage is claimed. Existing exact unaffected receipts and their platform/provider/budget claims remain scoped to unchanged selectors. Changed optional selectors have no new execution result. Audit1401: src/main/runtime/orca-runtime-tests/mobile-session-tabs-duplicate-pty-repair.spec.ts removed. Loses listing-on-already-repaired-data alarm; it never established runtime repair invocation. src/main/runtime/orca-runtime-tests/mobile-session-tabs-split-agent-tab.spec.ts removed. Loses actual mobile split sibling shell versus recorded-agent resume command alarms; no equivalent coverage asserted. src/main/runtime/orca-runtime-tests/worktree-native-chat-setup.spec.ts removed. Loses separate setup/startup tab result in that manually assembled path. src/main/runtime/orca-runtime.test.ts reduced. Driver registration/import of a whole-deleted selected spec is removed; directly authored cases remain unchanged. No extra authored removal credit. No equivalent replacement coverage is claimed. Existing exact unaffected receipts remain unchanged and apply only to their unchanged selectors. Changed optional selectors have no new execution result.",
"motivatingLinks": [
"https://github.com/stablyai/orca/issues/8457",
"https://github.com/stablyai/orca/issues/9563"
@@ -4812,10 +4812,10 @@
{
"file": "src/cli/runtime/launch.test.ts",
"assertions": [
"the CLI parent remains alive after the old serving child exits instead of letting launchd respawn it",
"an atomic app-bundle replacement starts one target-version serve child with the original arguments",
"handoff completes only after the replacement reports target-version runtime readiness",
"a replacement version mismatch or readiness timeout is persisted and exits without an in-process retry loop"
"waits past startup status lines for valid recipe JSON",
"preserves UTF-8 recipe JSON split across Buffer chunks",
"rejects when the server exits without valid recipe JSON",
"accepts valid recipe JSON at exit without a trailing newline"
]
},
{
@@ -4910,6 +4910,7 @@
"Add an Electron SSH promotion journey in addition to the deterministic identity-transfer unit contract."
],
"knownGaps": [
"Audit1589 removes mock-only CLI forwarding, canned provisioning/catalog/error projections and private inventories without equivalent replacement coverage. Retained cases exercise actual local markdown/paste/reconciliation, stream framing/backpressure/output-drain, temporary directory cleanup and persisted handoff with injected host/provider/process boundaries. No real Jira, provisioning, remote launch, native pipe/process-tree authority or deployed provider catalog proof. Assertion references use the surviving handoff registration titles; changed-selector historical receipts are retired and unaffected receipts remain unchanged.",
"The Electron journey uses an isolated development bundle rather than the installed application so it cannot disturb a real user session.",
"The live ShipIt/LaunchAgent canary used a disposable minimal Electron bundle plus the compiled production supervisor; a full packaged Orca update has not yet been run.",
"Linux and Windows single-instance activation have unit coverage but no live Electron evidence yet.",
@@ -410,23 +410,15 @@
"src/cli/index-automation-setup.test.ts": 466,
"src/cli/index-environment-commands.test.ts": 351,
"src/cli/index-file-open-headless.test.ts": 291,
"src/cli/index-jira.test.ts": 435,
"src/cli/index-local-command-routing-flags.test.ts": 907,
"src/cli/index-omitted-host-scope-selectors.test.ts": 6719,
"src/cli/index-orchestration.test.ts": 659,
"src/cli/index-ports.test.ts": 344,
"src/cli/index-project-setup.test.ts": 339,
"src/cli/index-reference-jira.test.ts": 517,
"src/cli/index-reference.test.ts": 789,
"src/cli/index-terminal-commands.test.ts": 466,
"src/cli/index-vm-recipe-doctor.test.ts": 459,
"src/cli/index-workspace-owner-choice-scaling.test.ts": 10154,
"src/cli/index-workspace-owner-choice.test.ts": 1072,
"src/cli/index-worktree-create-agent.test.ts": 582,
"src/cli/index-worktree-create-host-scoped-repo.test.ts": 881,
"src/cli/index-worktree-create-parent.test.ts": 665,
"src/cli/index-worktree-create-target.test.ts": 957,
"src/cli/index-worktree-selector-resolution.test.ts": 774,
"src/cli/index-worktree-set.test.ts": 541,
"src/cli/index-worktree-wsl-selection.test.ts": 712,
"src/cli/index.test.ts": 1237,
@@ -1,26 +1,6 @@
import { describe, expect, it } from 'vitest'
import { parseMobileMarkdownTree } from './mobile-markdown-tree'
function inlineNodes(source: string) {
const [paragraph] = parseMobileMarkdownTree(source, false).children
return paragraph?.type === 'paragraph' ? paragraph.children : []
}
describe('CJK emphasis in mobile markdown', () => {
it('closes bold that ends in punctuation right before CJK text', () => {
expect(
inlineNodes('**单算 Runtime 执行引擎,平均每题 29.78 秒。**边界是开始执行到产出结果。')
).toContainEqual({
type: 'strong',
children: [{ type: 'text', value: '单算 Runtime 执行引擎,平均每题 29.78 秒。' }]
})
})
it('keeps CommonMark flanking rules between non-CJK characters', () => {
expect(inlineNodes('**Note.**Next')).toEqual([{ type: 'text', value: '**Note.**Next' }])
})
})
describe('GitHub <details> sections', () => {
it('nests the Markdown between the tags under its summary', () => {
const source = [
@@ -85,10 +65,4 @@ describe('GitHub <details> sections', () => {
).children.map((node) => node.type)
expect(types).toEqual(['details', 'paragraph'])
})
it('leaves details markup inside an HTML comment alone', () => {
expect(
parseMobileMarkdownTree('<!-- <details><summary>x</summary> -->\n\nVisible', false).children
).toEqual([{ type: 'paragraph', children: [{ type: 'text', value: 'Visible' }] }])
})
})
@@ -103,30 +103,4 @@ describe('pasting into the mobile composer in the paired web page', () => {
expect(onAttachPastedText).toHaveBeenCalledTimes(1)
expect(onPasteImages).not.toHaveBeenCalled()
})
it('keeps a long paste as text when the host stores no phone files', async () => {
await render({ structured: null })
expect(paste('a'.repeat(40 * 1024)).defaultPrevented).toBe(false)
})
it('attaches a pasted image and leaves its caption to the field', async () => {
await render()
const event = paste('a caption', new File([], 'image.png', { type: 'image/png' }))
expect(onPasteImages).toHaveBeenCalledTimes(1)
expect(event.defaultPrevented).toBe(false)
})
it('reads the pasteboard when Paste brought neither text nor image bytes', async () => {
await render()
expect(paste('').defaultPrevented).toBe(true)
expect(onPasteImages).toHaveBeenCalledTimes(1)
})
it('leaves every paste to the field while a queued edit saves', async () => {
await render({ editSession: { cancel: vi.fn(), refuseCommand: () => false, locked: true } })
expect(paste('a'.repeat(40 * 1024)).defaultPrevented).toBe(false)
expect(paste('').defaultPrevented).toBe(false)
expect(onAttachPastedText).not.toHaveBeenCalled()
expect(onPasteImages).not.toHaveBeenCalled()
})
})
@@ -1,60 +0,0 @@
import { describe, expect, it } from 'vitest'
import { mobileNativeChatHostOutage } from './mobile-native-chat-host-outage'
const connected = { kind: 'normal', label: 'Connected' } as const
const reconnect = (): void => {}
const unreachable = { kind: 'unreachable', label: 'Unreachable', reason: 'stale' } as const
describe('mobileNativeChatHostOutage', () => {
it.each([
[
'an offline desktop offers Reconnect',
{ pairedHostState: 'reconnecting', pairedVerdict: unreachable, reconnect },
{ text: 'connection.connectionHealth.hostOffline', onReconnect: reconnect }
],
[
'a desktop still retrying offers nothing yet',
{
pairedHostState: 'reconnecting',
pairedVerdict: { kind: 'normal', label: 'Reconnecting…', dialing: true },
reconnect: null
},
{ text: 'chat.hostOutage.reconnecting' }
],
[
'a stopped server distro is text only',
{
pairedHostState: 'connected',
pairedVerdict: connected,
reconnect,
server: { state: 'connecting', name: 'Ubuntu', desktopReach: 'stopped' }
},
{ text: 'chat.hostOutage.stopped' }
],
[
'a server the desktop has not brought up points at the computer',
{
pairedHostState: 'connected',
pairedVerdict: connected,
reconnect,
server: { state: 'connecting', name: 'Ubuntu', desktopReach: 'unavailable' }
},
{ text: 'chat.hostOutage.unavailable' }
],
[
'an offline server cannot be redialled from the phone',
{
pairedHostState: 'connected',
pairedVerdict: connected,
reconnect,
server: { state: 'disconnected', name: 'Build box' }
},
{ text: 'connection.connectionHealth.hostOffline' }
]
] as const)('%s', (_name, args, expected) => {
// Why keys: the test pins which message and action each outage gets, not its wording.
expect(mobileNativeChatHostOutage({ hostName: 'Studio Mac', ...args }, (key) => key)).toEqual(
expected
)
})
})
@@ -93,38 +93,3 @@ it('links only workspace paths the host confirms, asking once for the whole repl
act(() => linked(tree)[0]?.props.onPress())
expect(onOpenFile).toHaveBeenCalledWith('src/app.ts:12')
})
it('links paths like before on a host too old to check them', async () => {
const sendRequest = vi.fn(async (): Promise<RpcResponse> => ({
id: '1',
ok: false,
error: {
code: 'forbidden',
message: 'Method files.pathsExist is not available to mobile clients'
},
_meta: { runtimeId: 'runtime-1' }
}))
const fileLinks = createMobileNativeChatFileLinkExistence(
{ sendRequest },
'repo-1::/work',
'/work'
)
const tree = await renderChat('Edited src/app.ts and the docs folder at src/notes today.', {
fileLinks,
githubRepo: null
})
expect(linked(tree).map(label)).toEqual(['src/app.ts'])
})
it('opens a bare #123 on the repo bare refs point to', async () => {
const tree = await renderChat('Fixed in #123.', {
fileLinks: null,
githubRepo: { owner: 'stablyai', repo: 'orca' }
})
expect(linked(tree).map(label)).toEqual(['#123'])
act(() => linked(tree)[0]?.props.onPress())
expect(openURL).toHaveBeenCalledWith('https://github.com/stablyai/orca/issues/123')
})
@@ -1,126 +0,0 @@
import { createElement, type ReactElement } from 'react'
import { act, create } from 'react-test-renderer'
import { describe, expect, it, vi } from 'vitest'
import type { AgentJournalThreadGoal } from '../../../src/shared/agent-session/journal/agent-session-journal-types'
import { WorkspaceServerContext } from '../transport/workspace-server-context'
import { structuredAgentSessionHostSupport } from './mobile-structured-agent-session-host-support'
import type { MobileStructuredAgentMutate } from './use-mobile-structured-agent-mutation'
import {
useMobileStructuredThreadGoal,
type MobileStructuredThreadGoal
} from './use-mobile-structured-thread-goal'
const PHONE_GOALS = 'agent-session.thread-goal.phone.v1'
const GOAL: AgentJournalThreadGoal = {
objective: 'Ship the parser',
status: 'active',
tokenBudget: null,
tokensUsed: 0,
timeUsedSeconds: 0,
createdAt: 1_000,
updatedAt: 1_000
}
type Route = {
report: 'host' | 'agent'
phoneThreadGoal: boolean
/** Undefined: the phone reaches the host directly, not through a paired desktop. */
desktopRelays?: boolean
}
function mount(route: Route) {
const mutate = vi.fn(async () => ({ status: 'accepted' as const, value: {}, sameFence: true }))
const send = vi.fn(async () => 'accepted' as const)
let goal: MobileStructuredThreadGoal | null = null
function Probe(): null {
const result = useMobileStructuredThreadGoal(
{
hostSupport: structuredAgentSessionHostSupport(route.phoneThreadGoal ? [PHONE_GOALS] : []),
enabled: true,
connected: true,
onSendError: () => {}
},
{ items: [], epoch: null, hostClock: undefined },
{
sessionKey: 'chat-1',
report:
route.report === 'host'
? { support: { current: GOAL }, reported: undefined }
: { support: undefined, reported: { current: GOAL } },
turnId: null,
workingStartedAt: null,
working: false,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the hook reads only the result status.
mutate: mutate as unknown as MobileStructuredAgentMutate,
commands: {}
}
)
result.sendRef.current = send
goal = result.threadGoal
return null
}
const tree = (children: ReactElement): ReactElement =>
route.desktopRelays === undefined
? children
: createElement(
WorkspaceServerContext.Provider,
{
value: {
client: null,
state: 'connected',
desktopCapabilities: route.desktopRelays ? [PHONE_GOALS] : []
}
},
children
)
act(() => {
create(tree(createElement(Probe)))
})
return { goal: () => goal!, mutate, send }
}
describe('useMobileStructuredThreadGoal', () => {
it.each([
{ route: { report: 'host', phoneThreadGoal: true }, changes: 'host' },
{ route: { report: 'host', phoneThreadGoal: true, desktopRelays: true }, changes: 'host' },
// An older paired desktop refuses to forward the method to a newer server.
{ route: { report: 'host', phoneThreadGoal: true, desktopRelays: false }, changes: null },
// An older host reports the goal but refuses the phone's goal changes.
{ route: { report: 'host', phoneThreadGoal: false }, changes: null },
{ route: { report: 'agent', phoneThreadGoal: false }, changes: 'agent' }
] as const)('offers $changes changes for $route', ({ route, changes }) => {
expect(mount(route).goal().changes).toBe(changes)
})
it('pauses a host goal through the host goal method', async () => {
const { goal, mutate, send } = mount({ report: 'host', phoneThreadGoal: true })
await act(async () => {
expect(await goal().change({ kind: 'status', status: 'paused' })).toBe(true)
})
expect(mutate).toHaveBeenCalledWith('agentSession.threadGoal', 'agentSession.threadGoal', {
change: { kind: 'status', status: 'paused' }
})
expect(send).not.toHaveBeenCalled()
})
it('clears an agent-owned goal with its own command and cannot pause it', async () => {
const { goal, mutate, send } = mount({ report: 'agent', phoneThreadGoal: true })
await act(async () => {
expect(await goal().change({ kind: 'status', status: 'paused' })).toBe(false)
expect(await goal().change({ kind: 'clear' })).toBe(true)
})
expect(send).toHaveBeenCalledTimes(1)
expect(send).toHaveBeenCalledWith('/goal clear')
expect(mutate).not.toHaveBeenCalled()
})
it('sends nothing when the host refuses the phone goal changes', async () => {
const { goal, mutate, send } = mount({ report: 'host', phoneThreadGoal: false })
await act(async () => {
expect(await goal().change({ kind: 'clear' })).toBe(false)
})
expect(mutate).not.toHaveBeenCalled()
expect(send).not.toHaveBeenCalled()
})
})
-439
View File
@@ -1,439 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock(mocks)
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: mocks.addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: mocks.listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(mocks.spawnMock)
})
import { main } from './index'
import { okFixture } from './test-fixtures'
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
import { JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY } from '../shared/jira/jira-agent-capability'
import { RuntimeClientError } from './runtime/types'
const site = {
id: 'site-1',
siteUrl: 'https://example.atlassian.net',
email: 'test@example.com',
displayName: 'Jira',
accountId: 'user-1'
}
const issue = {
id: '1001',
key: 'ENG-123',
siteId: site.id,
title: 'Fix login',
url: `${site.siteUrl}/browse/ENG-123`,
project: { id: '100', key: 'ENG', name: 'Engineering' },
issueType: { id: '10001', name: 'Bug' },
status: { id: '1', name: 'Open', categoryName: 'To Do', categoryKey: 'new' },
labels: ['existing'],
updatedAt: '2026-10-13',
createdAt: '2026-10-12'
}
const transition = {
id: '31',
name: 'Start progress',
to: { ...issue.status, name: 'In Progress' }
}
function responses(overrides: Record<string, unknown> = {}): void {
const values: Record<string, unknown> = {
'status.get': { capabilities: [JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY] },
'jira.status': { connected: true, viewer: null, sites: [site], selectedSiteId: site.id },
'jira.getIssue': issue,
'jira.listIssues': [],
'jira.searchIssues': [],
'jira.issueComments': [],
'jira.listProjects': [issue.project],
'jira.listIssueTypes': [issue.issueType],
'jira.listCreateFields': [],
'jira.listTransitions': [transition],
'jira.listPriorities': [],
'jira.listAssignableUsers': [],
'jira.createIssue': {
ok: true,
id: '1002',
key: 'ENG-124',
url: `${site.siteUrl}/browse/ENG-124`
},
'jira.updateIssue': { ok: true },
'jira.addIssueComment': { ok: true, id: 'comment-1' },
...overrides
}
mocks.callMock.mockImplementation(async (method: string) => {
if (!(method in values)) {
throw new Error(`Unexpected RPC: ${method}`)
}
return okFixture('req-jira', values[method])
})
}
async function run(args: string[], overrides: Record<string, unknown> = {}): Promise<void> {
responses(overrides)
await main(['jira', ...args, '--json'], '/tmp/workspace')
}
function output() {
const calls = vi.mocked(console.log).mock.calls
return JSON.parse(String(calls.at(-1)?.[0]))
}
describe('Jira CLI', () => {
const priorExitCode = process.exitCode
afterEach(() => {
process.exitCode = priorExitCode
})
useWorktreeAwarenessEnvironment(mocks)
beforeEach(() => {
process.exitCode = undefined
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
pairRuntimeEnvironment(mocks.listEnvironmentsMock, 'remote')
})
it('reads a URL on its exact saved site through a selected remote host', async () => {
await run(['issue', `${site.siteUrl}/browse/eng-123`, '--environment', 'remote'], {
'jira.status': { sites: [site, { ...site, id: 'other', siteUrl: 'https://other.example' }] }
})
expect(mocks.runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'remote')
expect(mocks.callMock).toHaveBeenCalledWith('jira.getIssue', {
key: 'ENG-123',
siteId: site.id,
strict: true
})
expect(output()).toMatchObject({ ok: true, result: issue })
})
it('reports saved connections without claiming verified authentication', async () => {
responses()
await main(['jira', 'connection'], '/tmp/workspace')
expect(mocks.callMock).toHaveBeenCalledTimes(1)
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('does not verify access'))
expect(console.log).toHaveBeenCalledWith(
expect.stringContaining('site-1\thttps://example.atlassian.net')
)
})
it('rejects old hosts before any Jira read or write', async () => {
await run(['comment', 'add', 'ENG-123', '--body', 'Ready'], {
'status.get': { capabilities: [] }
})
expect(output()).toMatchObject({ error: { code: 'incompatible_runtime' } })
expect(mocks.callMock.mock.calls).toEqual([['status.get']])
})
it('refuses ambiguous keys even when activeSiteId happens to name the first site', async () => {
await run(['label', 'set', 'ENG-123', '--label', 'bug'], {
'jira.status': {
sites: [site, { ...site, id: 'other' }],
selectedSiteId: 'all',
activeSiteId: site.id
}
})
expect(output()).toMatchObject({ error: { code: 'invalid_argument' } })
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.updateIssue', expect.anything())
})
it('honors explicit site selection without changing saved settings', async () => {
await run(['issue', 'eng-123', '--site', site.id], {
'jira.status': { sites: [site, { ...site, id: 'other' }], selectedSiteId: 'all' }
})
expect(mocks.callMock).toHaveBeenCalledWith('jira.getIssue', {
key: 'ENG-123',
siteId: site.id,
strict: true
})
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.selectSite', expect.anything())
})
it.each([
'https://example.atlassian.net.evil.example/browse/ENG-123',
'https://example.atlassian.net/jira/browse/ENG-123'
])('rejects a different origin or context path: %s', async (input) => {
await run(['issue', input])
expect(output()).toMatchObject({ error: { code: 'invalid_argument' } })
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.getIssue', expect.anything())
})
it('rejects a conflicting URL and --site', async () => {
await run(['issue', `${site.siteUrl}/browse/ENG-123`, '--site', 'other'], {
'jira.status': { sites: [site, { ...site, id: 'other', siteUrl: 'https://other.example' }] }
})
expect(output()).toMatchObject({ error: { code: 'invalid_argument' } })
})
it.each(['not-a-key', 'https://example.atlassian.net/projects/ENG'])(
'rejects malformed issue targets before network access: %s',
async (key) => {
await run(['issue', key])
expect(mocks.callMock).not.toHaveBeenCalled()
expect(output()).toMatchObject({ error: { code: 'invalid_argument' } })
}
)
it.each([null, { ...issue, siteId: 'other' }, { ...issue, key: 'ENG-99' }])(
'does not claim a missing or wrong issue was read',
async (result) => {
await run(['issue', 'ENG-123'], { 'jira.getIssue': result })
expect(output()).toMatchObject({ error: { code: 'jira_issue_unavailable' } })
}
)
it('preserves successful empty comment results', async () => {
await run(['comment', 'list', 'ENG-123'])
expect(output()).toMatchObject({ ok: true, result: [] })
expect(console.error).not.toHaveBeenCalled()
})
it('reads built-in filters and custom JQL on one site', async () => {
await run(['list', '--filter', 'all', '--limit', '8'])
expect(mocks.callMock).toHaveBeenCalledWith('jira.listIssues', {
filter: 'all',
limit: 8,
siteId: site.id,
strict: true
})
await run(['search', 'project = ENG AND updated >= -30d', '--limit', '5'])
expect(mocks.callMock).toHaveBeenCalledWith('jira.searchIssues', {
jql: 'project = ENG AND updated >= -30d',
limit: 5,
siteId: site.id,
strict: true
})
})
it.each([
['list', '--filter', 'bad'],
['list', '--limit', '0'],
['search', ' ']
])('rejects invalid searches before access: %s', async (...args) => {
await run(args)
expect(output()).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(mocks.callMock).not.toHaveBeenCalled()
})
it('resolves a project key to its ID and preserves required custom create fields', async () => {
await run([
'create',
'--project',
'eng',
'--type',
'10001',
'--title',
'Fix login',
'--body',
'Details\nnext line',
'--fields-json',
'{"reporter":"user-9","customfield_1":{"id":"option-1"}}',
'--user-field',
'reporter'
])
expect(mocks.callMock).toHaveBeenCalledWith('jira.createIssue', {
siteId: site.id,
projectId: '100',
issueTypeId: '10001',
title: 'Fix login',
description: 'Details\nnext line',
customFields: { reporter: 'user-9', customfield_1: { id: 'option-1' } },
userFieldKeys: ['reporter']
})
expect(output()).toMatchObject({ ok: true, result: { key: 'ENG-124' } })
})
it.each(['[]', '{bad}', '{"project":{"id":"other"}}'])(
'rejects invalid or conflicting custom fields before writes: %s',
async (fields) => {
await run([
'create',
'--project',
'ENG',
'--type',
'10001',
'--title',
'Fix',
'--fields-json',
fields
])
expect(output()).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(mocks.callMock).not.toHaveBeenCalled()
}
)
it('rejects unknown projects without creating an issue', async () => {
await run(['create', '--project', 'UNKNOWN', '--type', '10001', '--title', 'Fix'])
expect(output()).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: expect.stringContaining('Project did not match exactly one project')
}
})
expect(mocks.callMock).toHaveBeenCalledWith('jira.listProjects', {
siteId: site.id,
strict: true
})
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.createIssue', expect.anything())
})
it('writes comments and returns their ID', async () => {
await run(['comment', 'add', 'ENG-123', '--body', 'Ready\nhttps://example.com/review/123'])
expect(mocks.callMock).toHaveBeenCalledWith('jira.addIssueComment', {
key: issue.key,
siteId: site.id,
body: 'Ready\nhttps://example.com/review/123'
})
expect(output()).toMatchObject({ result: { ok: true, id: 'comment-1' } })
})
it.each([
['--body', ' '],
['--body', 'a', '--body-file', 'b']
])('refuses unsafe comment input before access: %s', async (...flags) => {
await run(['comment', 'add', 'ENG-123', ...flags])
expect(output()).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(mocks.callMock).not.toHaveBeenCalled()
})
it.each([[], ['--to', 'In Progress', '--to-id', '31'], ['--to', ' '], ['--to-id', ' ']])(
'requires exactly one nonblank transition selector before access: %s',
async (...flags) => {
await run(['status', 'set', 'ENG-123', ...flags])
expect(output()).toMatchObject({
error: {
code: 'invalid_argument',
message: 'Pass exactly one of --to <name> or --to-id <transitionId>.'
}
})
expect(mocks.callMock).not.toHaveBeenCalled()
}
)
it('resolves status names using transitions of this exact issue and site', async () => {
await run(['status', 'set', 'eng-123', '--to', 'In Progress'])
expect(mocks.callMock).toHaveBeenCalledWith('jira.listTransitions', {
key: issue.key,
siteId: site.id,
strict: true
})
expect(mocks.callMock).toHaveBeenCalledWith('jira.updateIssue', {
key: issue.key,
siteId: site.id,
updates: { transitionId: '31' }
})
})
it.each([{ transitions: [] }, { transitions: [transition, { ...transition, id: '32' }] }])(
'refuses absent or ambiguous transitions',
async ({ transitions }) => {
await run(['status', 'set', 'ENG-123', '--to', 'In Progress'], {
'jira.listTransitions': transitions
})
expect(output()).toMatchObject({
ok: false,
error: {
code: 'invalid_argument',
message: expect.stringContaining('Expected one available transition')
}
})
expect(mocks.callMock).toHaveBeenCalledWith('jira.listTransitions', {
key: issue.key,
siteId: site.id,
strict: true
})
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.updateIssue', expect.anything())
}
)
it.each([
['assignee', 'set', ['--to-id', 'account-9'], { assigneeAccountId: 'account-9' }],
['assignee', 'clear', [], { assigneeAccountId: null }],
['priority', 'set', ['--to-id', '2'], { priorityId: '2' }],
['priority', 'clear', [], { priorityId: null }],
['label', 'set', ['--label', 'bug', '--label', 'urgent'], { labels: ['bug', 'urgent'] }],
['label', 'add', ['--label', 'bug'], { labelOperations: { add: ['bug'] } }],
['label', 'rm', ['--label', 'bug'], { labelOperations: { remove: ['bug'] } }]
])('maps %s %s to a site-pinned mutation', async (noun, verb, flags, updates) => {
await run([noun, verb, 'ENG-123', ...flags])
expect(mocks.callMock).toHaveBeenCalledWith('jira.updateIssue', {
key: issue.key,
siteId: site.id,
updates
})
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.getIssue', expect.anything())
expect(output()).toMatchObject({
ok: true,
result: { ok: true, key: issue.key, siteId: site.id }
})
})
it('does not report rejected mutations as successful', async () => {
await run(['label', 'add', 'ENG-123', '--label', 'bug'], {
'jira.updateIssue': { ok: false, error: 'Permission denied' }
})
expect(output()).toMatchObject({
error: {
code: 'jira_write_failed',
message: expect.stringContaining('Permission denied')
}
})
})
it('preserves uncertainty when the host returns a failed comment result', async () => {
await run(['comment', 'add', 'ENG-123', '--body', 'Ready'], {
'jira.addIssueComment': { ok: false, error: 'Connection closed while reading the response' }
})
expect(output()).toMatchObject({
error: {
code: 'jira_write_failed',
message: expect.stringContaining('Verify in Jira before retrying')
}
})
expect(
mocks.callMock.mock.calls.filter(([method]) => method === 'jira.addIssueComment')
).toHaveLength(1)
})
it('does not retry a timed-out write or claim it did not happen', async () => {
responses()
const previous = mocks.callMock.getMockImplementation()
mocks.callMock.mockImplementation(async (method: string, ...args: unknown[]) => {
if (method === 'jira.addIssueComment') {
throw new RuntimeClientError('runtime_timeout', 'Timed out')
}
return previous?.(method, ...args)
})
await main(['jira', 'comment', 'add', 'ENG-123', '--body', 'Ready', '--json'], '/tmp/workspace')
expect(output()).toMatchObject({
error: {
code: 'jira_write_unverified',
message: expect.stringContaining('Verify in Jira before retrying')
}
})
expect(
mocks.callMock.mock.calls.filter(([method]) => method === 'jira.addIssueComment')
).toHaveLength(1)
})
})
@@ -1,348 +0,0 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
removeEnvironmentMock,
resolveEnvironmentMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
removeEnvironmentMock: vi.fn(),
resolveEnvironmentMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: removeEnvironmentMock,
resolveEnvironment: resolveEnvironmentMock
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { okFixture, queueFixtures } from './test-fixtures'
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
const SSH_TARGET = { id: 'ssh-1777360569033-yvz2mp', label: 'openclaw', remotePlatform: 'win32' }
function queueSshTargetLookups(count: number): void {
queueFixtures(
callMock,
...Array.from({ length: count }, () => okFixture('req_ssh_targets', { targets: [SSH_TARGET] }))
)
}
/** Mirror the settings.update envelope and publish the effective name through status.get. */
function fakeMachineNameRuntime(detectedName: string): { updates: unknown[] } {
const updates: unknown[] = []
let override = ''
callMock.mockImplementation(async (method: string, params?: unknown) => {
if (method === 'status.get') {
return okFixture('req_status', {
machineName: override || detectedName,
hostPlatform: 'darwin'
})
}
if (method === 'settings.update') {
updates.push(params)
const requested =
typeof params === 'object' && params !== null && 'machineName' in params
? params.machineName
: ''
override = String(requested).trim()
return okFixture('req_settings', { settings: { machineName: override } })
}
throw new Error(`unexpected call ${method}`)
})
return { updates }
}
describe('runtime-selector flags on locally pinned CLI commands', () => {
const priorExitCode = process.exitCode
afterEach(() => {
process.exitCode = priorExitCode
})
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('answers `host list` from this machine and stamps the runtime that actually answered', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
queueSshTargetLookups(1)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'list', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed._meta.runtimeId).toBe('local')
expect(printed.result.hosts.map((host: { id: string }) => host.id)).toEqual([
'local',
SSH_TARGET.id,
'env-m4air'
])
expect(
printed.result.hosts.find((host: { id: string }) => host.id === SSH_TARGET.id).platform
).toBe('win32')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, null)
})
it('reads and updates the name the answering runtime publishes', async () => {
const runtime = fakeMachineNameRuntime('m4airs-Air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'name', '--json'], '/tmp/repo')
await main(['host', 'name', '--name', ' build-server ', '--json'], '/tmp/repo')
await main(['host', 'name', '--name', '', '--json'], '/tmp/repo')
await main(['host', 'name'], '/tmp/repo')
const [first, second, third] = logSpy.mock.calls
.slice(0, 3)
.map((call) => JSON.parse(String(call[0])))
expect(first.result).toEqual({ machineName: 'm4airs-Air', platform: 'darwin' })
expect(second.result).toEqual({ machineName: 'build-server', platform: 'darwin' })
expect(second._meta.runtimeId).toBe('runtime-1')
expect(third.result.machineName).toBe('m4airs-Air')
expect(logSpy.mock.calls[3]?.[0]).toBe('m4airs-Air (darwin)')
expect(runtime.updates).toEqual([{ machineName: ' build-server ' }, { machineName: '' }])
})
it('routes `host name --environment` and stamps the runtime that answered', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
fakeMachineNameRuntime('M4 Air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
runtimeClientConstructorMock.mockClear()
await main(['host', 'name', '--environment', 'm4air', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(true)
expect(printed.result.machineName).toBe('M4 Air')
expect(printed._meta.runtimeId).toBe('runtime-1')
expect(printed._meta.runtimeId).not.toBe('local')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'm4air')
})
it('connects to the server an --environment name in another case resolved to', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'M4Air')
fakeMachineNameRuntime('M4 Air')
vi.spyOn(console, 'log').mockImplementation(() => {})
runtimeClientConstructorMock.mockClear()
await main(['host', 'name', '--environment', 'm4air', '--json'], '/tmp/repo')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'env-m4air')
})
function agentSelectors(): void {
process.env.ORCA_ENVIRONMENT = ''
process.env.ORCA_PAIRING_CODE = ''
process.env.ORCA_REMOTE_PAIRING = ''
}
it("keeps explicit --environment routing under an agent's empty selectors", async () => {
agentSelectors()
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
fakeMachineNameRuntime('M4 Air')
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'name', '--environment', 'm4air', '--json'], '/tmp/repo')
expect(JSON.parse(String(log.mock.calls[0]?.[0])).result.machineName).toBe('M4 Air')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'm4air')
})
it("keeps --host runtime:<id> routing under an agent's empty selectors", async () => {
agentSelectors()
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
queueFixtures(callMock, okFixture('req_project_setups', { setups: [] }))
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'runtime:gpu', '--json'], '/tmp/repo')
expect(JSON.parse(String(log.mock.calls[0]?.[0]))).toMatchObject({
ok: true,
result: { setups: [] }
})
expect(callMock).toHaveBeenCalledWith('projectHostSetup.list')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'gpu')
})
it('routes `host name` through an ambient ORCA_ENVIRONMENT, unlike the pinned `host list`', async () => {
process.env.ORCA_ENVIRONMENT = 'm4air'
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
fakeMachineNameRuntime('M4 Air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
runtimeClientConstructorMock.mockClear()
await main(['host', 'name', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(true)
expect(printed.result.machineName).toBe('M4 Air')
expect(printed._meta.runtimeId).toBe('runtime-1')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, undefined)
expect(runtimeClientConstructorMock).not.toHaveBeenCalledWith(null, null)
})
it('refuses to rename a runtime that does not publish a machine name', async () => {
// Missing status support lets the CLI reject the rename before an older settings schema rejects it.
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
callMock.mockImplementation(async (method: string) => {
if (method === 'status.get') {
return okFixture('req_status', { hostPlatform: 'darwin' })
}
throw new Error(`unexpected call ${method}`)
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['host', 'name', '--name', 'build-server', '--environment', 'm4air', '--json'],
'/tmp/repo'
)
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(false)
expect(printed.error.code).toBe('incompatible_runtime')
expect(printed.error.message).toMatch(/does not support machine names/)
expect(callMock).not.toHaveBeenCalledWith('settings.update', expect.anything())
expect(process.exitCode).toBe(1)
})
it('reports an unreachable runtime as an error instead of inventing a name', async () => {
const { RuntimeClientError } = await import('./runtime/types.js')
callMock.mockRejectedValue(new RuntimeClientError('runtime_unavailable', 'Orca is not running'))
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'name', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(false)
expect(printed.error.code).toBe('runtime_unavailable')
expect(process.exitCode).toBe(1)
})
it('rejects `host list --environment` instead of answering with a half-routed listing', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
runtimeClientConstructorMock.mockClear()
await main(['host', 'list', '--environment', 'm4air', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(false)
expect(printed.error.code).toBe('invalid_argument')
expect(printed.error.message).toContain('`--environment` does not retarget `orca host list`')
expect(process.exitCode).toBe(1)
expect(callMock).not.toHaveBeenCalled()
expect(runtimeClientConstructorMock).not.toHaveBeenCalledWith(null, 'm4air')
})
it('rejects `environment list --environment` rather than repeating the local answer', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['environment', 'list', '--environment', 'm4air', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(false)
expect(printed.error.code).toBe('invalid_argument')
expect(printed.error.message).toContain(
'`--environment` does not retarget `orca environment list`'
)
})
it('rejects `--pairing-code` on both listings for the same reason', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'list', '--pairing-code', 'orca://pair?code=x', '--json'], '/tmp/repo')
await main(
['environment', 'list', '--pairing-code', 'orca://pair?code=x', '--json'],
'/tmp/repo'
)
expect(logSpy).toHaveBeenCalledTimes(2)
for (const call of logSpy.mock.calls) {
const printed = JSON.parse(String(call[0]))
expect(printed.ok).toBe(false)
expect(printed.error.code).toBe('invalid_argument')
expect(printed.error.message).toContain('`--pairing-code` does not retarget')
}
expect(callMock).not.toHaveBeenCalled()
})
it('keeps `host list` local when ORCA_ENVIRONMENT is set ambiently', async () => {
process.env.ORCA_ENVIRONMENT = 'm4air'
pairRuntimeEnvironment(listEnvironmentsMock, 'env-m4air', 'm4air')
queueSshTargetLookups(1)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['host', 'list', '--json'], '/tmp/repo')
const printed = JSON.parse(String(logSpy.mock.calls[0]?.[0]))
expect(printed.ok).toBe(true)
expect(printed.result.hosts.some((host: { id: string }) => host.id === SSH_TARGET.id)).toBe(
true
)
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, null)
expect(runtimeClientConstructorMock).not.toHaveBeenCalledWith(undefined, undefined)
})
it('still treats --environment as the selector argument on `environment show` and `rm`', async () => {
const environment = {
id: 'env-m4air',
name: 'm4air',
createdAt: 1,
updatedAt: 1,
lastUsedAt: null,
runtimeId: null,
endpoints: [],
preferredEndpointId: null
}
resolveEnvironmentMock.mockReturnValue(environment)
removeEnvironmentMock.mockReturnValue(environment)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['environment', 'show', '--environment', 'm4air', '--json'], '/tmp/repo')
await main(['environment', 'rm', '--environment', 'm4air', '--json'], '/tmp/repo')
expect(resolveEnvironmentMock).toHaveBeenCalledExactlyOnceWith('/tmp/orca-user-data', 'm4air')
expect(removeEnvironmentMock).toHaveBeenCalledExactlyOnceWith('/tmp/orca-user-data', 'm4air')
expect(logSpy).toHaveBeenCalledTimes(2)
expect(logSpy.mock.calls.map(([output]) => JSON.parse(String(output)).result)).toEqual([
{ environment },
{ removed: environment }
])
expect(callMock).not.toHaveBeenCalled()
})
})
-372
View File
@@ -1,372 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
import type { WorkspacePort } from '../shared/workspace-ports'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { RuntimeClientError, RuntimeRpcFailureError } from './runtime-client'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
const FEATURE_ID = 'repo::/tmp/repo/feature'
function workspacePort(
port: number,
worktreeId: string,
extra: { advertisedUrl?: string; confidence?: 'cwd' | 'command' } = {}
): WorkspacePort {
return {
id: `127.0.0.1:${port}:${port + 1}`,
bindHost: '127.0.0.1',
connectHost: 'localhost',
port,
pid: port + 1,
processName: 'node',
protocol: 'http',
kind: 'workspace',
owner: {
worktreeId,
repoId: 'repo',
displayName: worktreeId.endsWith('feature') ? 'feature' : 'other',
path: worktreeId.split('::')[1],
confidence: extra.confidence ?? 'cwd'
},
...(extra.advertisedUrl ? { advertisedUrl: extra.advertisedUrl } : {})
}
}
const externalPort: WorkspacePort = {
id: '0.0.0.0:5432:77',
bindHost: '0.0.0.0',
connectHost: 'localhost',
port: 5432,
pid: 77,
processName: 'postgres',
protocol: 'unknown',
kind: 'external'
}
const hostScan = okFixture('req_scan', {
executionHostId: 'local',
platform: 'darwin',
scannedAt: 1,
ports: [
workspacePort(5173, FEATURE_ID, { advertisedUrl: 'http://dev.test:5173' }),
workspacePort(3000, FEATURE_ID, { confidence: 'command' }),
workspacePort(4000, 'repo::/tmp/repo/other'),
externalPort
]
})
const noFolders = okFixture('req_folders', { folderWorkspaces: [] })
function folderCatalog(folderPath: string): ReturnType<typeof okFixture>[] {
return [
okFixture('req_folders', {
folderWorkspaces: [{ id: 'f1', folderPath, projectGroupId: 'local' }]
}),
okFixture('req_groups', { groups: [{ id: 'local' }] }),
okFixture('req_repos', { repos: [] })
]
}
function selectorNotFound(): RuntimeRpcFailureError {
return new RuntimeRpcFailureError({
id: 'req_show',
ok: false,
error: { code: 'selector_not_found', message: 'selector_not_found' },
_meta: { runtimeId: 'test-runtime' }
})
}
function captureOutput(): () => string {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
return () => [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
}
describe('orca ports', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('scans the host of the worktree containing cwd and keeps only its ports', async () => {
queueFixtures(
callMock,
noFolders,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main'),
buildWorktree('/tmp/repo/feature', 'feature/foo')
]),
okFixture('req_show', { worktree: buildWorktree('/tmp/repo/feature', 'feature/foo') }),
hostScan
)
const output = captureOutput()
await main(['ports', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenCalledTimes(4)
expect(callMock).toHaveBeenNthCalledWith(3, 'worktree.show', { worktree: `id:${FEATURE_ID}` })
expect(callMock).toHaveBeenNthCalledWith(4, 'workspacePorts.scanHost', {
worktree: `id:${FEATURE_ID}`
})
const printed = JSON.parse(output())
expect(printed.result.executionHostId).toBe('local')
expect(printed.result.ports.map((port: WorkspacePort) => port.port)).toEqual([3000, 5173])
expect(printed.result.ports[1].advertisedUrl).toBe('http://dev.test:5173')
})
it('prints one row per port with pid, process, host, protocol, match, and url', async () => {
queueFixtures(
callMock,
noFolders,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_show', { worktree: buildWorktree('/tmp/repo/feature', 'feature/foo') }),
hostScan
)
const output = captureOutput()
await main(['ports'], '/tmp/repo/feature')
expect(output()).toBe(
[
'3000 pid=3001 node localhost http match=command',
'5173 pid=5174 node localhost http match=cwd url=http://dev.test:5173',
'',
'host=local'
].join('\n')
)
})
it('resolves an explicit selector to its worktree id before filtering', async () => {
queueFixtures(
callMock,
okFixture('req_show', { worktree: buildWorktree('/tmp/repo/other', 'other') }),
hostScan
)
const output = captureOutput()
await main(['ports', '--worktree', 'branch:other', '--json'], '/tmp')
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.show', { worktree: 'branch:other' })
expect(callMock).toHaveBeenNthCalledWith(2, 'workspacePorts.scanHost', {
worktree: 'branch:other'
})
expect(JSON.parse(output()).result.ports.map((port: WorkspacePort) => port.port)).toEqual([
4000
])
})
it('lists every listener on the host with its owner under --all', async () => {
queueFixtures(
callMock,
noFolders,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
hostScan
)
const output = captureOutput()
await main(['ports', '--all'], '/tmp/repo/feature')
expect(callMock).not.toHaveBeenCalledWith('worktree.show', expect.anything())
expect(output().split('\n').slice(0, 4)).toEqual([
'3000 workspace=feature pid=3001 node localhost http match=command',
'4000 workspace=other pid=4001 node localhost http match=cwd',
'5173 workspace=feature pid=5174 node localhost http match=cwd url=http://dev.test:5173',
'5432 workspace=(none) pid=77 postgres localhost unknown'
])
})
it.each([
['a standalone native folder', '/tmp/folder', '/tmp/folder/src'],
['a native folder nested in a git worktree', '/tmp/repo/feature/docs', '/tmp/repo/feature/docs']
])(
'refuses %s containing cwd unless --all asks for the host',
async (_label, folderPath, cwd) => {
queueFixtures(callMock, ...folderCatalog(folderPath))
const output = captureOutput()
const priorExitCode = process.exitCode
await main(['ports', '--json'], cwd)
expect(callMock).not.toHaveBeenCalledWith('workspacePorts.scanHost', expect.anything())
expect(output()).toContain('does not tie ports to native folder workspaces')
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
queueFixtures(callMock, ...folderCatalog(folderPath), hostScan)
await main(['ports', '--all'], cwd)
expect(callMock).toHaveBeenCalledWith('workspacePorts.scanHost', { worktree: 'id:folder:f1' })
expect(callMock).not.toHaveBeenCalledWith('worktree.list', expect.anything())
expect(output()).toContain('5432 workspace=(none) pid=77 postgres')
}
)
it('recognises a path selector naming a native folder', async () => {
queueFixtures(
callMock,
okFixture('req_folders', {
folderWorkspaces: [{ id: 'f1', folderPath: '/tmp/folder', projectGroupId: 'local' }]
})
)
callMock.mockRejectedValueOnce(selectorNotFound())
const output = captureOutput()
const priorExitCode = process.exitCode
await main(['ports', '--worktree', 'path:/tmp/folder'], '/tmp')
expect(callMock).not.toHaveBeenCalledWith('workspacePorts.scanHost', expect.anything())
expect(output()).toContain('does not tie ports to native folder workspaces')
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
queueFixtures(
callMock,
okFixture('req_folders', {
folderWorkspaces: [{ id: 'f1', folderPath: '/tmp/folder', projectGroupId: 'local' }]
})
)
callMock.mockRejectedValueOnce(selectorNotFound())
queueFixtures(callMock, hostScan)
await main(['ports', '--worktree', 'path:/tmp/folder', '--all'], '/tmp')
expect(callMock).toHaveBeenCalledWith('workspacePorts.scanHost', { worktree: 'id:folder:f1' })
})
it('refuses a path that a worktree and a folder workspace share', async () => {
queueFixtures(
callMock,
okFixture('req_folders', {
folderWorkspaces: [{ id: 'f1', folderPath: '/srv/app', projectGroupId: 'ssh' }]
}),
okFixture('req_show', { worktree: buildWorktree('/srv/app', 'main') })
)
const output = captureOutput()
const priorExitCode = process.exitCode
await main(['ports', '--worktree', 'path:/srv/app', '--all'], '/tmp')
expect(callMock).not.toHaveBeenCalledWith('workspacePorts.scanHost', expect.anything())
expect(output()).toContain('id:repo::/srv/app, folder:f1')
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('keeps the ports of a folder workspace instance that shares its path with the owner', async () => {
const instance = {
...buildWorktree('/tmp/repo/feature', 'feature/foo'),
id: `${FEATURE_ID}::workspace:second`
}
queueFixtures(callMock, okFixture('req_show', { worktree: instance }), hostScan)
const output = captureOutput()
await main(['ports', '--worktree', `id:${instance.id}`, '--json'], '/tmp')
expect(JSON.parse(output()).result.ports.map((port: WorkspacePort) => port.port)).toEqual([
3000, 5173
])
})
it('adds the worktree id to owner names shared by two workspaces under --all', async () => {
const twin = workspacePort(4000, 'repo2::/tmp/twin/feature')
queueFixtures(
callMock,
okFixture('req_scan', {
executionHostId: 'local',
platform: 'darwin',
scannedAt: 1,
ports: [workspacePort(3000, FEATURE_ID), twin, externalPort]
})
)
const output = captureOutput()
await main(['ports', '--worktree', 'branch:feature', '--all'], '/tmp')
expect(output().split('\n').slice(0, 3)).toEqual([
`3000 workspace=feature (${FEATURE_ID}) pid=3001 node localhost http match=cwd`,
'4000 workspace=feature (repo2::/tmp/twin/feature) pid=4001 node localhost http match=cwd',
'5432 workspace=(none) pid=77 postgres localhost unknown'
])
})
it('shows why the scan could not run', async () => {
queueFixtures(
callMock,
okFixture('req_scan', {
executionHostId: 'ssh:box',
platform: 'unknown',
scannedAt: 1,
ports: [],
unavailableReason: 'Could not scan the SSH host.'
})
)
const output = captureOutput()
await main(['ports', '--worktree', 'folder:f1', '--all'], '/tmp')
expect(callMock).toHaveBeenCalledTimes(1)
expect(output()).toBe('Port scan unavailable: Could not scan the SSH host.\nhost=ssh:box')
})
it('asks for an Orca update when the runtime predates the host-scoped scan', async () => {
callMock.mockRejectedValueOnce(
new RuntimeClientError('method_not_found', 'Unknown method: workspacePorts.scanHost')
)
const output = captureOutput()
const priorExitCode = process.exitCode
await main(['ports', '--worktree', 'id:repo::/tmp/repo/feature', '--all'], '/tmp')
expect(output()).toContain('cannot list workspace ports yet. Update Orca')
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
})
-873
View File
@@ -1,873 +0,0 @@
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { okFixture, queueFixtures } from './test-fixtures'
import { PROJECT_HOST_SETUP_HOST_TARGET_RUNTIME_CAPABILITY } from '../shared/worktree-runtime-capabilities'
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
const ORCA_PROJECT = {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
}
const PROJECT_LIST_FIXTURE = okFixture('req_project_list', { projects: [ORCA_PROJECT] })
describe('orca cli worktree awareness', () => {
const priorExitCode = process.exitCode
beforeEach(() => {
process.exitCode = undefined
})
afterEach(() => {
process.exitCode = priorExitCode
})
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('resolves repo.add paths against the invoking cli cwd', async () => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: path.resolve('/tmp/repo/apps/web'),
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['repo', 'add', '--path', './apps/web', '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: path.resolve('/tmp/repo/apps/web')
})
})
it('lists projects through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_list', {
projects: [
{
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
providerIdentity: {
provider: 'github',
owner: 'stablyai',
repo: 'orca'
},
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
},
{
id: 'folder:notes',
displayName: 'Notes',
kind: 'folder',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'list'], '/tmp/repo')
expect(callMock).toHaveBeenCalledExactlyOnceWith('project.list')
expect(logSpy).toHaveBeenCalledExactlyOnceWith(
'github:stablyai/orca Orca github:stablyai/orca\nfolder:notes Notes no-provider'
)
})
it('routes a runtime host filter to that paired server and keeps its own local rows', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-local',
path: '/tmp/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-remote',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: 'repo-remote',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['project', 'setups', '--project', 'github:stablyai/orca', '--host', 'runtime:gpu'],
'/tmp/repo'
)
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'gpu')
expect(callMock).toHaveBeenCalledWith('projectHostSetup.list')
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-remote')
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-local')
})
it('keeps --host local a filter on the selected environment rather than a second selector', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'prod')
queueFixtures(
callMock,
okFixture('req_project_setups', {
setups: [
{
id: 'setup-on-box',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-on-box',
path: '/srv/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-by-client',
projectId: 'github:stablyai/orca',
hostId: 'runtime:prod',
repoId: 'repo-by-client',
path: '/srv/orca-2',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'setups', '--environment', 'prod', '--host', 'local'], '/tmp/repo')
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(undefined, 'prod')
expect(logSpy.mock.calls[0]?.[0]).toContain('setup-on-box')
expect(logSpy.mock.calls[0]?.[0]).not.toContain('setup-by-client')
})
it('names the version gap when the server predates project host setup', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'old-server')
const { RuntimeClientError } = await import('./runtime/types.js')
callMock.mockRejectedValueOnce(
new RuntimeClientError('method_not_found', 'Unknown method: projectHostSetup.list')
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'runtime:old-server', '--json'], '/tmp/repo')
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({
ok: false,
error: { code: 'incompatible_runtime' }
})
expect(printed).toContain('does not support project host setup yet')
expect(printed).not.toContain('Unknown method')
expect(process.exitCode).toBe(1)
})
it('rejects a runtime host id that no paired server owns instead of answering empty', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'runtime:not-a-real-env', '--json'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalledWith('projectHostSetup.list')
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(printed).toContain('no paired Orca server is named or has id not-a-real-env')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(JSON.parse(printed).error.data.knownEnvironments).toEqual([])
expect(process.exitCode).toBe(1)
})
it('rejects an unknown ssh host instead of answering empty', async () => {
queueFixtures(callMock, okFixture('req_ssh_targets', { targets: [] }))
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'ssh:openclaw', '--json'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalledWith('projectHostSetup.list')
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain('no SSH target named or with id openclaw')
expect(process.exitCode).toBe(1)
})
it('tells a caller reaching for a paired server by ssh that it is an environment', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'awin')
queueFixtures(callMock, okFixture('req_ssh_targets', { targets: [] }))
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'ssh:awin', '--json'], '/tmp/repo')
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain('--environment awin')
expect(process.exitCode).toBe(1)
})
it('resolves an ssh label to its target id before filtering', async () => {
queueFixtures(
callMock,
okFixture('req_ssh_targets', { targets: [{ id: 'ssh-123-abc', label: 'openclaw' }] }),
okFixture('req_project_setups', {
setups: [
{
id: 'setup-openclaw',
projectId: 'github:stablyai/orca',
hostId: 'ssh:ssh-123-abc',
repoId: 'repo-openclaw',
path: '/home/me/orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
},
{
id: 'setup-other-host',
projectId: 'github:stablyai/orca',
hostId: 'ssh:other-target',
repoId: 'repo-other',
path: '/home/me/other',
displayName: 'Other',
setupState: 'ready',
setupMethod: 'legacy-repo',
createdAt: 1,
updatedAt: 1
}
]
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'ssh:openclaw'], '/tmp/repo')
expect(logSpy).toHaveBeenCalledExactlyOnceWith(
'setup-openclaw project:github:stablyai/orca host:ssh:ssh-123-abc ready /home/me/orca'
)
})
it('never lets an environment name reach a persisted host id', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'env-uuid-1', 'awin')
queueFixtures(
callMock,
PROJECT_LIST_FIXTURE,
okFixture('req_project_setup_create', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-awin',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: '',
path: '',
displayName: 'awin',
setupState: 'setting-up',
setupMethod: 'provisioned',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-create',
'--project',
'github:stablyai/orca',
'--host',
'runtime:awin',
'--json'
],
'/tmp/repo'
)
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'env-uuid-1')
expect(callMock).toHaveBeenCalledWith(
'projectHostSetup.create',
expect.objectContaining({ hostId: 'runtime:env-uuid-1' })
)
})
it('rejects a malformed --host value before contacting any runtime', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(['project', 'setups', '--host', 'runtime:', '--json'], '/tmp/repo')
expect(callMock).not.toHaveBeenCalled()
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain('Invalid --host value: runtime:')
expect(process.exitCode).toBe(1)
})
it('refuses a runtime host id alongside an unrelated --pairing-code connection', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['project', 'setups', '--host', 'runtime:gpu', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain('use either --host runtime:<id> or --pairing-code, not both')
expect(process.exitCode).toBe(1)
})
it('sets up an existing project folder with a path resolved against the local cli cwd', async () => {
queueFixtures(
callMock,
PROJECT_LIST_FIXTURE,
okFixture('req_project_setup', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: ['repo-1'],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-local',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-1',
path: path.resolve('/tmp/orca'),
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 1
},
repo: {
id: 'repo-1',
path: path.resolve('/tmp/orca'),
displayName: 'Orca',
badgeColor: '#7c3aed',
addedAt: 1
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-existing-folder',
'--project',
'github:stablyai/orca',
'--host',
'local',
'--path',
'..',
'--kind',
'git',
'--display-name',
'Orca',
'--json'
],
'/tmp/orca/worktrees/feature'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.setupExistingFolder', {
projectId: 'github:stablyai/orca',
hostId: 'local',
path: path.resolve('/tmp/orca/worktrees'),
kind: 'git',
displayName: 'Orca'
})
})
it('rejects remote project setup relative paths instead of resolving against client cwd', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'project',
'setup-existing-folder',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--path',
'./orca',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain(
'Remote project setup requires --path to be an absolute path on the remote server.'
)
expect(process.exitCode).toBe(1)
})
it('rejects SSH project setup relative paths, which name the client filesystem', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'project',
'setup-existing-folder',
'--project',
'github:stablyai/orca',
'--host',
'ssh:openclaw',
'--path',
'./orca',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain(
'Remote project setup requires --path to be an absolute path on the remote server.'
)
expect(process.exitCode).toBe(1)
})
it('rejects remote repo.add relative paths instead of resolving against client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['repo', 'add', '--path', './apps/web', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
const printed = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(JSON.parse(printed)).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(printed).toContain(
'Remote repo add requires --path to be an absolute path on the remote server.'
)
expect(process.exitCode).toBe(1)
})
it('sends remote repo.add absolute paths unchanged', async () => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: '/srv/orca/web',
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['repo', 'add', '--path', '/srv/orca/web', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: '/srv/orca/web'
})
})
it.each(['C:\\repo', 'C:/repo', '\\\\server\\share\\repo', '//server/share/repo'])(
'sends remote repo.add server absolute path %s unchanged',
async (serverPath) => {
queueFixtures(
callMock,
okFixture('req_repo_add', {
repo: {
id: 'repo-1',
path: serverPath,
displayName: 'web'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['repo', 'add', '--path', serverPath, '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('repo.add', {
path: serverPath
})
}
)
it('sends a windows destination to the routed host instead of joining it to the local cwd', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'awin')
queueFixtures(
callMock,
PROJECT_LIST_FIXTURE,
okFixture('req_project_setup_clone', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-awin',
projectId: 'github:stablyai/orca',
hostId: 'local',
repoId: 'repo-awin',
path: 'C:\\orca-probe\\orca',
displayName: 'Orca',
setupState: 'ready',
setupMethod: 'cloned',
createdAt: 1,
updatedAt: 1
},
repo: {
id: 'repo-awin',
path: 'C:\\orca-probe\\orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
addedAt: 1
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-clone',
'--project',
'github:stablyai/orca',
'--host',
'runtime:awin',
'--url',
'https://github.com/stablyai/orca.git',
'--destination',
'C:\\orca-probe',
'--json'
],
'/Users/nwparker/orca/workspaces/orca/IME-koko'
)
expect(runtimeClientConstructorMock).toHaveBeenCalledWith(null, 'awin')
expect(callMock).toHaveBeenCalledWith('projectHostSetup.clone', {
projectId: 'github:stablyai/orca',
hostId: 'runtime:awin',
url: 'https://github.com/stablyai/orca.git',
destination: 'C:\\orca-probe',
displayName: undefined
})
})
it('updates project host setup metadata through the project-first runtime API', async () => {
queueFixtures(
callMock,
okFixture('req_project_setup_update', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '/srv/orca',
displayName: 'GPU VM',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-update',
'--setup',
'setup-gpu',
'--display-name',
'GPU VM',
'--path',
'/srv/orca',
'--worktree-base-path',
'../worktrees',
'--state',
'ready',
'--method',
'imported-existing-folder',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.update', {
setupId: 'setup-gpu',
updates: {
displayName: 'GPU VM',
path: path.resolve('/tmp/repo', '/srv/orca'),
worktreeBasePath: '../worktrees',
gitUsername: undefined,
kind: undefined,
setupState: 'ready',
setupMethod: 'imported-existing-folder'
}
})
})
it('creates independent project host setup metadata through the project-first runtime API', async () => {
pairRuntimeEnvironment(listEnvironmentsMock, 'gpu')
queueFixtures(
callMock,
PROJECT_LIST_FIXTURE,
okFixture('req_project_setup_create', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '',
displayName: 'GPU VM',
setupState: 'setting-up',
setupMethod: 'provisioned',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
[
'project',
'setup-create',
'--project',
'github:stablyai/orca',
'--host',
'runtime:gpu',
'--setup-id',
'setup-gpu',
'--display-name',
'GPU VM',
'--state',
'setting-up',
'--method',
'provisioned',
'--json'
],
'/tmp/repo'
)
expect(callMock).toHaveBeenCalledWith('projectHostSetup.create', {
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
setupId: 'setup-gpu',
path: undefined,
kind: undefined,
displayName: 'GPU VM',
worktreeBasePath: undefined,
gitUsername: undefined,
setupState: 'setting-up',
setupMethod: 'provisioned'
})
})
it.each([
{ flags: [], force: false, hostId: undefined },
{ flags: ['--force'], force: true, hostId: undefined },
{ flags: ['--host', 'local'], force: false, hostId: 'local' }
])('deletes project host setup metadata with $flags', async ({ flags, force, hostId }) => {
queueFixtures(
callMock,
...(hostId ? [hostTargetStatusFixture(true)] : []),
okFixture('req_project_setup_delete', {
result: {
project: {
id: 'github:stablyai/orca',
displayName: 'Orca',
badgeColor: '#7c3aed',
sourceRepoIds: [],
createdAt: 1,
updatedAt: 1
},
setup: {
id: 'setup-gpu',
projectId: 'github:stablyai/orca',
hostId: 'runtime:gpu',
repoId: '',
path: '/srv/orca',
displayName: 'GPU VM',
setupState: 'ready',
setupMethod: 'imported-existing-folder',
createdAt: 1,
updatedAt: 2
}
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['project', 'setup-delete', '--setup', 'setup-gpu', ...flags, '--json'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('projectHostSetup.delete', {
setupId: 'setup-gpu',
hostId,
force
})
})
it('refuses --host on a server that would ignore it and act on the first same-id twin', async () => {
queueFixtures(callMock, hostTargetStatusFixture(false))
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['project', 'setup-delete', '--setup', 'dup', '--host', 'local', '--force', '--json'],
'/tmp/repo'
)
expect(callMock.mock.calls).toEqual([['status.get']])
expect(logSpy).toHaveBeenCalledTimes(1)
expect(JSON.parse(String(logSpy.mock.calls[0]?.[0]))).toMatchObject({
ok: false,
error: {
code: 'incompatible_runtime',
message: expect.stringContaining('cannot pick a project setup by host yet')
}
})
expect(process.exitCode).toBe(1)
})
})
function hostTargetStatusFixture(supported: boolean) {
return okFixture('req_status', {
capabilities: supported ? [PROJECT_HOST_SETUP_HOST_TARGET_RUNTIME_CAPABILITY] : []
})
}
-410
View File
@@ -1,410 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const mocks = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock(mocks)
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: mocks.addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: mocks.listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(mocks.spawnMock)
})
import { main } from './index'
import { pairRuntimeEnvironment, useWorktreeAwarenessEnvironment } from './index-test-harness'
import { buildWorktree, okFixture } from './test-fixtures'
import { RuntimeClientError } from './runtime/types'
import { JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY } from '../shared/jira/jira-agent-capability'
import { WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY } from '../shared/workspace-attachments/workspace-attachment-capabilities'
const site = {
id: 'site-1',
siteUrl: 'https://example.atlassian.net',
email: 'test@example.com',
displayName: 'Jira',
accountId: 'user-1'
}
const issue = {
id: '1001',
key: 'ENG-123',
siteId: site.id,
title: 'Fix login',
url: `${site.siteUrl}/browse/ENG-123`,
project: { id: '100', key: 'ENG', name: 'Engineering' },
issueType: { id: '10001', name: 'Bug' },
status: { id: '1', name: 'Open', categoryName: 'To Do', categoryKey: 'new' },
labels: [],
updatedAt: '2026-10-13',
createdAt: '2026-10-12'
}
const jiraReference = {
provider: 'jira' as const,
type: 'issue' as const,
number: 0,
identifier: 'ENG-123',
jiraIdentifier: 'ENG-123',
url: issue.url
}
const attachment = { ...jiraReference, title: issue.title }
const reviewUrl = 'https://gitlab.com/acme/api/-/merge_requests/12'
const review = { provider: 'gitlab' as const, type: 'mr' as const, number: 12, url: reviewUrl }
const workspace = { ...buildWorktree('/tmp/api', 'api'), kind: 'worktree', name: 'api' }
const addArgs = ['reference', 'add', 'eng-123', '--worktree', 'name:api']
const createArgs = [
'worktree',
'create',
'--name',
'fix',
'--repo',
'id:repo',
'--no-parent',
'--reference',
'eng-123',
'--json'
]
function responses(overrides: Record<string, unknown> = {}): void {
const values: Record<string, unknown> = {
'status.get': {
capabilities: [
JIRA_AGENT_ACCESS_RUNTIME_CAPABILITY,
WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY
]
},
'jira.status': { sites: [site], selectedSiteId: site.id },
'jira.lookupIssueSummary': issue,
'reference.list': { worktree: workspace, references: [] },
'worktree.create': { worktree: workspace },
...overrides
}
mocks.callMock.mockImplementation(async (method: string) => {
const result = values[method]
if (result instanceof Error) {
throw result
}
return okFixture(method, result)
})
}
function expectNoWrite(): void {
expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('worktree.set')
expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('worktree.create')
expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain(
'folderWorkspace.update'
)
}
describe('Jira reference keys', () => {
const priorExitCode = process.exitCode
beforeEach(() => {
process.exitCode = undefined
})
afterEach(() => {
process.exitCode = priorExitCode
})
useWorktreeAwarenessEnvironment(mocks)
beforeEach(() => {
mocks.runtimeClientConstructorMock.mockClear()
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
pairRuntimeEnvironment(mocks.listEnvironmentsMock, 'remote')
responses()
})
it('adds a lowercase Jira key with its title using existing delta storage', async () => {
await main(addArgs, '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: site.id
})
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
worktree: 'id:repo::/tmp/api',
linkedItemsBase: [],
linkedItems: [attachment],
linkedItemsSelectionChanged: false
})
)
})
it('uses the selected execution host for Jira lookup and workspace mutation', async () => {
await main([...addArgs, '--environment', 'remote'], '/tmp/api')
expect(mocks.runtimeClientConstructorMock).toHaveBeenCalledExactlyOnceWith(undefined, 'remote')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: site.id
})
expect(mocks.callMock).toHaveBeenCalledWith('reference.list', { worktree: 'name:api' })
expect(mocks.callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/api',
linkedItemsBase: [],
linkedItems: [attachment],
linkedItemsSelectionChanged: false
})
})
it('refuses ambiguous sites rather than using activeSiteId', async () => {
responses({
'jira.status': {
sites: [site, { ...site, id: 'other' }],
selectedSiteId: 'all',
activeSiteId: site.id
}
})
await main(addArgs, '/tmp/api')
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('--reference-site'))
expectNoWrite()
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.lookupIssueSummary', expect.anything())
})
it.each(['all', 'missing'])('rejects invalid explicit site %s before lookup', async (id) => {
await main([...addArgs, '--reference-site', id], '/tmp/api')
expect(console.error).toHaveBeenCalledExactlyOnceWith(
id === 'all'
? 'Choose one Jira site ID; --reference-site all is unsupported.'
: 'No matching connected Jira site. Check orca jira connection and Orca Jira settings.'
)
expect(process.exitCode).toBe(1)
expect(mocks.callMock).not.toHaveBeenCalledWith('jira.lookupIssueSummary', expect.anything())
expectNoWrite()
})
it('selects an explicit site including a Server context path', async () => {
const server = {
...site,
id: 'server',
siteUrl: 'https://jira.example/jira',
authType: 'server'
}
const serverIssue = { ...issue, siteId: server.id, url: `${server.siteUrl}/browse/ENG-123` }
responses({
'jira.status': { sites: [site, server], selectedSiteId: 'all' },
'jira.lookupIssueSummary': serverIssue
})
await main([...addArgs, '--reference-site', server.id], '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: server.id
})
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItems: [{ ...attachment, url: serverIssue.url }]
})
)
})
it.each([
null,
{ ...issue, key: 'ENG-124' },
{ ...issue, siteId: 'other' },
{ ...issue, url: 'https://example.atlassian.net.evil.example/browse/ENG-123' },
{ ...issue, url: `${site.siteUrl}/other/browse/ENG-123` },
{ ...issue, url: `${site.siteUrl}/browse/ENG-124` },
{ ...issue, title: '' }
])('rejects an absent or mismatched summary %#', async (summary) => {
responses({ 'jira.lookupIssueSummary': summary })
await main(addArgs, '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: site.id
})
expect(console.error).toHaveBeenCalledExactlyOnceWith(
'Jira did not return issue ENG-123 with a title on the selected site.'
)
expect(process.exitCode).toBe(1)
expectNoWrite()
})
it('preserves Jira lookup errors without reading or updating workspace storage', async () => {
responses({
'jira.lookupIssueSummary': new RuntimeClientError('rpc_error', 'jira_summary_lookup:auth')
})
await main(addArgs, '/tmp/api')
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('jira_summary_lookup:auth'))
expect(mocks.callMock).not.toHaveBeenCalledWith('reference.list', expect.anything())
expectNoWrite()
})
it('validates mixed URLs and keys before any request', async () => {
await main(
['reference', 'add', 'ENG-123', reviewUrl, 'invalid', '--worktree', 'name:api'],
'/tmp/api'
)
expect(mocks.callMock).not.toHaveBeenCalled()
})
it('deduplicates a key and its URL without losing the resolved title', async () => {
await main(
['reference', 'add', reviewUrl, 'eng-123', issue.url, 'ENG-123', '--worktree', 'name:api'],
'/tmp/api'
)
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItems: [review, attachment]
})
)
expect(
mocks.callMock.mock.calls.filter(([method]) => method === 'jira.lookupIssueSummary')
).toHaveLength(1)
})
it('keeps stored metadata for an already linked issue and produces no write', async () => {
const stored = { ...attachment, title: 'Existing title' }
responses({
'reference.list': {
worktree: workspace,
references: [
{
...stored,
key: '["jira","issue","example.atlassian.net","/browse/ENG-123"]',
selected: false
}
]
}
})
await main([...addArgs, '--json'], '/tmp/api')
expectNoWrite()
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes[0]).toMatchObject({
changed: false,
reason: 'already_linked'
})
})
it('adds to a folder through the existing folder update', async () => {
responses({
'reference.list': {
worktree: { ...workspace, id: 'folder:folder1', kind: 'folder' },
references: []
}
})
await main(addArgs, '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('folderWorkspace.update', {
folderWorkspaceId: 'folder1',
updates: {
linkedItemsBase: [],
linkedItems: [attachment],
linkedItemsSelectionChanged: false
}
})
})
it('resolves create references before workspace creation', async () => {
await main(createArgs, '/tmp/api')
const methods = mocks.callMock.mock.calls.map(([method]) => method)
expect(methods).toContain('jira.lookupIssueSummary')
expect(methods.indexOf('jira.lookupIssueSummary')).toBeLessThan(
methods.indexOf('worktree.create')
)
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.create',
expect.objectContaining({ linkedItems: [attachment] })
)
})
it('creates nothing when the Jira reference cannot resolve', async () => {
responses({ 'jira.lookupIssueSummary': null })
await main(createArgs, '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: site.id
})
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({
ok: false,
error: {
code: 'jira_issue_unavailable',
message: 'Jira did not return issue ENG-123 with a title on the selected site.'
}
})
expect(process.exitCode).toBe(1)
expectNoWrite()
})
it('creates with an explicitly selected Jira site', async () => {
responses({
'jira.status': {
sites: [site, { ...site, id: 'other' }],
selectedSiteId: 'all'
}
})
await main([...createArgs, '--reference-site', site.id], '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-123',
siteId: site.id
})
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.create',
expect.objectContaining({ linkedItems: [attachment] })
)
})
it('writes nothing if a later Jira key does not resolve', async () => {
await main(['reference', 'add', 'ENG-123', 'ENG-124', '--worktree', 'name:api'], '/tmp/api')
expect(mocks.callMock).toHaveBeenCalledWith('jira.lookupIssueSummary', {
key: 'ENG-124',
siteId: site.id
})
expect(
mocks.callMock.mock.calls
.filter(([method]) => method === 'jira.lookupIssueSummary')
.map(([, params]) => params)
).toEqual([
{ key: 'ENG-123', siteId: site.id },
{ key: 'ENG-124', siteId: site.id }
])
expect(console.error).toHaveBeenCalledExactlyOnceWith(
'Jira did not return issue ENG-124 with a title on the selected site.'
)
expect(process.exitCode).toBe(1)
expectNoWrite()
})
it('validates required create flags before Jira lookup', async () => {
await main(
['worktree', 'create', '--repo', 'id:repo', '--no-parent', '--reference', 'ENG-123'],
'/tmp/api'
)
expect(mocks.callMock).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('--name'))
})
it('keeps full Jira URLs usable without a saved Jira connection', async () => {
responses({ 'status.get': { capabilities: [WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY] } })
await main(['reference', 'add', issue.url, '--worktree', 'name:api'], '/tmp/api')
expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('jira.status')
expect(mocks.callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItems: [jiraReference]
})
)
})
it('requires Jira agent capability before resolving keys', async () => {
responses({ 'status.get': { capabilities: [WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY] } })
await main(addArgs, '/tmp/api')
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('Update Orca'))
expect(mocks.callMock.mock.calls.map(([method]) => method)).not.toContain('jira.status')
expectNoWrite()
})
})
-436
View File
@@ -1,436 +0,0 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { buildWorktree, okFixture } from './test-fixtures'
import { getWorkspaceReferenceIdentity } from '../shared/workspace-attachments/workspace-reference-identity'
import type { WorkspaceAttachment } from '../shared/worktree/types'
import { WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY } from '../shared/workspace-attachments/workspace-attachment-capabilities'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
const PR = 'https://github.com/acme/api/pull/5123'
const TASK = 'https://linear.app/acme/issue/STA-1234'
const PR_KEY = '["github","pr","github.com","/acme/api/pull/5123"]'
const parsedPR: WorkspaceAttachment = { provider: 'github', type: 'pr', number: 5123, url: PR }
const parsedTask: WorkspaceAttachment = {
provider: 'linear',
type: 'issue',
number: 0,
identifier: 'STA-1234',
linearIdentifier: 'STA-1234',
linearOrganizationUrlKey: 'acme',
url: TASK
}
function listResult(references = [parsedPR], kind: 'worktree' | 'folder' = 'worktree') {
return {
worktree: {
...buildWorktree('/tmp/api', 'api'),
id: kind === 'folder' ? 'folder:folder1' : 'repo::/tmp/api',
name: 'api',
kind,
repo: 'repo'
},
references: references.map((item) => ({
...item,
key: getWorkspaceReferenceIdentity(item),
selected: true
}))
}
}
function mockCalls(
before = listResult(),
after = before,
capabilities = [WORKTREE_LINKED_ITEMS_DELTA_RUNTIME_CAPABILITY]
) {
let lists = 0
callMock.mockImplementation(async (method: string) => {
if (method === 'status.get') {
return okFixture('status', { capabilities })
}
if (method === 'reference.list') {
return okFixture('list', lists++ === 0 ? before : after)
}
if (method === 'reference.find') {
return okFixture('find', { kind: 'reference_matches', matches: [], truncated: false })
}
return okFixture('mutation', { worktree: before.worktree })
})
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
}
describe('reference CLI', () => {
const priorExitCode = process.exitCode
beforeEach(() => {
process.exitCode = undefined
})
afterEach(() => {
process.exitCode = priorExitCode
})
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('adds several URLs through the existing delta merge and never invents origins', async () => {
mockCalls(listResult([]), listResult([parsedPR, parsedTask]))
await main(['reference', 'add', PR, TASK, '--worktree', 'name:api', '--json'], '/tmp/api')
expect(callMock).toHaveBeenCalledWith('worktree.set', {
worktree: 'id:repo::/tmp/api',
linkedItemsBase: [],
linkedItems: [parsedPR, parsedTask],
linkedItemsSelectionChanged: false
})
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toEqual([
{ key: PR_KEY, operation: 'add', changed: true },
{
key: '["linear","issue","linear.app","/acme/issue/STA-1234"]',
operation: 'add',
changed: true
}
])
})
it('validates every URL before any request', async () => {
mockCalls()
await main(['reference', 'add', PR, 'github:pr:4', '--worktree', 'name:api'], '/tmp/api')
expect(callMock).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalled()
})
it('requires the delta capability even if the host supports linked items', async () => {
mockCalls(listResult(), listResult(), ['worktree.linked-items.v1'])
await main(['reference', 'add', TASK, '--worktree', 'name:api'], '/tmp/api')
expect(callMock).toHaveBeenCalledExactlyOnceWith('status.get')
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('cannot merge'))
})
it('duplicate adds preserve current metadata and produce a no-op', async () => {
mockCalls(listResult([{ ...parsedPR, title: 'Saved title' }]))
await main(['reference', 'add', PR, PR, '--worktree', 'name:api', '--json'], '/tmp/api')
expect(callMock.mock.calls.some(([method]) => method === 'worktree.set')).toBe(false)
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.references[0].title).toBe(
'Saved title'
)
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toEqual([
{
key: PR_KEY,
operation: 'add',
changed: false,
reason: 'already_linked'
}
])
})
it('removes only requested keys from folders via existing folder storage', async () => {
mockCalls(listResult([parsedPR, parsedTask], 'folder'), listResult([parsedTask], 'folder'))
await main(['reference', 'remove', '--key', PR_KEY, '--worktree', 'folder:folder1'], '/tmp/api')
expect(callMock).toHaveBeenCalledWith('folderWorkspace.update', {
folderWorkspaceId: 'folder1',
updates: {
linkedItemsBase: [parsedPR, parsedTask],
linkedItems: [parsedTask],
linkedItemsSelectionChanged: false
}
})
})
it('removes a URL without touching the same review number in another repository', async () => {
const other = { ...parsedPR, url: 'https://github.com/acme/other/pull/5123' }
mockCalls(listResult([parsedPR, other]), listResult([other]))
await main(['reference', 'remove', PR, '--worktree', 'name:api'], '/tmp/api')
expect(callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({ linkedItemsBase: [parsedPR, other], linkedItems: [other] })
)
})
const localTwin = {
...parsedPR,
title: 'Local',
taskSourceContext: {
kind: 'task-source' as const,
provider: 'github' as const,
projectId: 'p1',
hostId: 'local' as const
}
}
const remoteTwin = {
...parsedPR,
title: 'Remote',
taskSourceContext: {
...localTwin.taskSourceContext,
hostId: 'runtime:other' as const
}
}
it('keeps same-URL twins with different source contexts on an unrelated add', async () => {
mockCalls(listResult([localTwin, remoteTwin]))
await main(['reference', 'add', TASK, '--worktree', 'name:api'], '/tmp/api')
expect(callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItemsBase: [localTwin, remoteTwin],
linkedItems: [localTwin, remoteTwin, parsedTask]
})
)
})
it('removing a URL removes every twin that shares its identity', async () => {
mockCalls(listResult([localTwin, parsedTask, remoteTwin]), listResult([parsedTask]))
await main(['reference', 'remove', PR, '--worktree', 'name:api'], '/tmp/api')
expect(callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItemsBase: [localTwin, parsedTask, remoteTwin],
linkedItems: [parsedTask]
})
)
})
it('removes by the host-provided key even when the local identity differs', async () => {
const before = listResult([parsedPR, parsedTask])
before.references[0].key = 'host-v2:pr-key'
mockCalls(before, listResult([parsedTask]))
await main(
['reference', 'remove', '--key', 'host-v2:pr-key', '--worktree', 'name:api', '--json'],
'/tmp/api'
)
expect(callMock).toHaveBeenCalledWith(
'worktree.set',
expect.objectContaining({
linkedItemsBase: [parsedPR, parsedTask],
linkedItems: [parsedTask]
})
)
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes).toEqual([
{ key: 'host-v2:pr-key', operation: 'remove', changed: true }
])
})
it('removing an absent URL is a no-op', async () => {
mockCalls()
await main(['reference', 'remove', TASK, '--worktree', 'name:api', '--json'], '/tmp/api')
expect(callMock.mock.calls.some(([method]) => method === 'worktree.set')).toBe(false)
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0]).result.changes[0]).toMatchObject({
changed: false,
reason: 'not_linked'
})
})
it('rejects remote current without resolving the local cwd on the paired host', async () => {
mockCalls()
await main(
['reference', 'find', 'STA-1234', '--worktree', 'current', '--pairing-code', 'remote'],
'/tmp/api'
)
expect(callMock).not.toHaveBeenCalled()
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('remote host'))
})
it('forwards local caller path context so WSL selectors resolve without a scan', async () => {
mockCalls()
const cwd = String.raw`\\wsl$\Ubuntu\home\me\repo`
await main(['reference', 'find', 'STA-1234', '--worktree', 'path:/home/me/repo'], cwd)
expect(callMock).toHaveBeenCalledExactlyOnceWith(
'reference.find',
expect.objectContaining({ worktree: 'path:/home/me/repo', cwd })
)
})
it('passes explicit paired-host selectors unchanged', async () => {
mockCalls()
await main(
['reference', 'find', 'STA-1234', '--worktree', 'path:/srv/api', '--pairing-code', 'remote'],
'/tmp/api'
)
expect(callMock).toHaveBeenCalledExactlyOnceWith(
'reference.find',
expect.objectContaining({ worktree: 'path:/srv/api' })
)
expect(callMock.mock.calls[0][1]).not.toHaveProperty('cwd')
})
it('uses metadata current lookup instead of worktree.list or Git scanning', async () => {
mockCalls()
await main(
[
'reference',
'find',
'STA-1234',
'--worktree',
'current',
'--include-archived',
'--limit',
'10'
],
'/tmp/api/subdir'
)
expect(callMock).toHaveBeenCalledExactlyOnceWith('reference.find', {
query: 'STA-1234',
worktree: 'current',
cwd: '/tmp/api/subdir',
repo: undefined,
includeArchived: true,
limit: 10
})
})
it.each([
{ args: ['5123'], message: 'Expected a full reference URL (https://…).' },
{
args: ['STA-1234', '--worktree', 'name:api', '--repo', 'id:repo'],
message: 'Pass --worktree or --repo, not both.'
},
{ args: ['STA-1234', '--limit', '0'], message: 'Invalid positive integer for --limit' }
])('rejects invalid find arguments $args before querying', async ({ args, message }) => {
mockCalls()
await main(['reference', 'find', ...args, '--json'], '/tmp/api')
expect(callMock).not.toHaveBeenCalled()
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({
ok: false,
error: { code: 'invalid_argument', message }
})
expect(process.exitCode).toBe(1)
})
it('seeds repeated references in the create request before setup or agent startup', async () => {
mockCalls()
await main(
[
'worktree',
'create',
'--name',
'fix',
'--repo',
'id:repo',
'--no-parent',
'--reference',
PR,
'--reference',
TASK
],
'/tmp/api'
)
expect(callMock).toHaveBeenCalledWith(
'worktree.create',
expect.objectContaining({ linkedItems: [parsedPR, parsedTask] })
)
})
it.each(['pr', 'issue', 'linear-issue', 'gitlab-issue', 'gitlab-mr'])(
'rejects create --reference mixed with --%s',
async (flag) => {
mockCalls()
await main(
[
'worktree',
'create',
'--name',
'fix',
'--repo',
'id:repo',
'--no-parent',
'--reference',
PR,
`--${flag}`,
'4',
'--json'
],
'/tmp/api'
)
expect(callMock).not.toHaveBeenCalled()
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({
ok: false,
error: { code: 'invalid_argument', message: `Cannot combine --reference with --${flag}.` }
})
expect(process.exitCode).toBe(1)
}
)
// Why: hosts built before reference.* still advertise the delta capability.
it.each([
[['reference', 'list', '--worktree', 'name:api']],
[['reference', 'find', '--query', 'STA-1234']],
[['reference', 'add', TASK, '--worktree', 'name:api']]
])('names the version gap when the host lacks reference RPCs: %j', async (argv) => {
mockCalls()
const { RuntimeClientError } = await import('./runtime/types.js')
const base = callMock.getMockImplementation()
callMock.mockImplementation(async (method: string, ...rest: unknown[]) => {
if (method.startsWith('reference.')) {
throw new RuntimeClientError('method_not_found', `Unknown method: ${method}`)
}
return base?.(method, ...rest)
})
await main([...argv, '--json'], '/tmp/api')
expect(JSON.parse(vi.mocked(console.log).mock.calls[0][0])).toMatchObject({
ok: false,
error: {
code: 'incompatible_runtime',
message:
'This Orca host does not support reference commands yet. Update Orca on the execution host.'
}
})
expect(process.exitCode).toBe(1)
})
it('validates create flags locally before probing reference capability', async () => {
mockCalls()
await main(
['worktree', 'create', '--repo', 'id:repo', '--no-parent', '--reference', PR],
'/tmp/api'
)
expect(callMock).not.toHaveBeenCalledWith('status.get', expect.anything())
expect(callMock).not.toHaveBeenCalledWith('status.get')
expect(console.error).toHaveBeenCalledWith(expect.stringContaining('--name'))
})
})
-236
View File
@@ -47,8 +47,6 @@ vi.mock('child_process', async () => {
})
import { main } from './index'
import { encodePairingOffer } from '../shared/remote-runtime/pairing/pairing'
import { PAIRING_OFFER_VERSION } from '../shared/remote-runtime/pairing/mobile-relay-pairing-offer'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
describe('orca cli worktree awareness', () => {
@@ -106,238 +104,4 @@ describe('orca cli worktree awareness', () => {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('warns when vm recipe doctor finds no cleanup hook', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-'))
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(path.join(repoPath, 'scripts', 'orca-vm', 'start.sh'), '#!/bin/sh\n')
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: manual-sandbox',
' name: Manual Sandbox',
' create: ./scripts/orca-vm/start.sh'
].join('\n')
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['vm', 'recipe', 'doctor', 'manual-sandbox', '--repo-path', repoPath, '--json'])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string; remediation?: string }[]
}
if (!output.ok) {
throw new Error(JSON.stringify(output))
}
expect(output.ok).toBe(true)
expect(output.checks).toContainEqual(
expect.objectContaining({
id: 'recipe.destroy',
status: 'warn',
remediation: 'Add destroy or explicitly set destroy: none.'
})
)
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('runs vm recipe doctor provision mode and invokes cleanup', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-provision-'))
const pairingCode = encodePairingOffer({
v: PAIRING_OFFER_VERSION,
endpoint: 'ws://sandbox.example.com:6767',
deviceToken: 'token',
publicKeyB64: 'public-key'
})
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(
path.join(repoPath, 'scripts', 'orca-vm', 'start.js'),
[
'console.log(JSON.stringify({',
' schemaVersion: 1,',
` pairingCode: ${JSON.stringify(pairingCode)},`,
" projectRoot: '/workspace/repo'",
'}))'
].join('\n')
)
writeFileSync(
path.join(repoPath, 'scripts', 'orca-vm', 'cleanup.js'),
[
"const fs = require('fs')",
"const input = fs.readFileSync(0, 'utf8')",
'const payload = JSON.parse(input)',
"fs.writeFileSync('cleanup-ran.json', JSON.stringify(payload))"
].join('\n')
)
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
` create: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/start.js`)}`,
` destroy: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/cleanup.js`)}`
].join('\n')
)
const { EventEmitter } = await import('node:events')
const startChild = Object.assign(new EventEmitter(), {
stdout: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stderr: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stdin: { write: vi.fn(), end: vi.fn() },
kill: vi.fn()
})
const cleanupChild = Object.assign(new EventEmitter(), {
stdout: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stderr: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stdin: { write: vi.fn(), end: vi.fn() },
kill: vi.fn()
})
spawnMock
.mockImplementationOnce(() => {
process.nextTick(() => {
startChild.stdout.emit(
'data',
Buffer.from(
JSON.stringify({
schemaVersion: 1,
pairingCode,
projectRoot: '/workspace/repo'
})
)
)
startChild.emit('exit', 0, null)
startChild.emit('close', 0, null)
})
return startChild
})
.mockImplementationOnce(() => {
process.nextTick(() => {
cleanupChild.emit('exit', 0, null)
cleanupChild.emit('close', 0, null)
})
return cleanupChild
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main([
'vm',
'recipe',
'doctor',
'cloud-sandbox',
'--repo-path',
repoPath,
'--provision',
'--json'
])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string }[]
provisionTranscript?: {
provision: { exitCode: number | null; stdout: string; stderr: string }
destroy?: { exitCode: number | null; stdout: string; stderr: string }
}
}
if (!output.ok) {
throw new Error(JSON.stringify(output))
}
expect(output.ok).toBe(true)
expect(output.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'recipe.provision', status: 'pass' }),
expect.objectContaining({ id: 'recipe.result.endpoint.public_ws', status: 'warn' }),
expect.objectContaining({ id: 'recipe.result.project_root', status: 'pass' }),
expect.objectContaining({ id: 'recipe.destroy.run', status: 'pass' })
])
)
expect(output.provisionTranscript?.provision.exitCode).toBe(0)
expect(output.provisionTranscript?.destroy?.exitCode).toBe(0)
const cleanupPayload = JSON.parse(
String(vi.mocked(cleanupChild.stdin.end).mock.calls[0]?.[0])
) as { recipeId: string; recipeResult: { projectRoot: string } }
expect(cleanupPayload).toMatchObject({
recipeId: 'cloud-sandbox',
recipeResult: { projectRoot: '/workspace/repo' }
})
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
it('returns the full create transcript when provision fails so the agent can self-diagnose', async () => {
const repoPath = mkdtempSync(path.join(tmpdir(), 'orca-vm-doctor-provision-fail-'))
try {
mkdirSync(path.join(repoPath, 'scripts', 'orca-vm'), { recursive: true })
writeFileSync(path.join(repoPath, 'scripts', 'orca-vm', 'start.js'), 'process.exit(0)')
writeFileSync(
path.join(repoPath, 'orca.yaml'),
[
'environmentRecipes:',
' - id: cloud-sandbox',
' name: Cloud Sandbox',
` create: ${JSON.stringify(`${process.execPath} ./scripts/orca-vm/start.js`)}`,
' destroy: none'
].join('\n')
)
const { EventEmitter } = await import('node:events')
const startChild = Object.assign(new EventEmitter(), {
stdout: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stderr: Object.assign(new EventEmitter(), { setEncoding: vi.fn() }),
stdin: { write: vi.fn(), end: vi.fn() },
kill: vi.fn()
})
spawnMock.mockImplementationOnce(() => {
process.nextTick(() => {
startChild.stdout.emit('data', Buffer.from('Provisioning sandbox...\n'))
startChild.stderr.emit('data', Buffer.from('vercel: error: missing scope\n'))
startChild.emit('exit', 0, null)
startChild.emit('close', 0, null)
})
return startChild
})
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main([
'vm',
'recipe',
'doctor',
'cloud-sandbox',
'--repo-path',
repoPath,
'--provision',
'--json'
])
const output = JSON.parse(String(logSpy.mock.calls[0][0])) as {
ok: boolean
checks: { id: string; status: string }[]
provisionTranscript?: {
provision: {
exitCode: number | null
stdout: string
stderr: string
parseError?: string
}
}
}
expect(output.ok).toBe(false)
expect(output.checks).toEqual(
expect.arrayContaining([
expect.objectContaining({ id: 'recipe.provision', status: 'fail' })
])
)
expect(output.provisionTranscript?.provision.stdout).toContain('Provisioning sandbox...')
expect(output.provisionTranscript?.provision.stderr).toContain('missing scope')
expect(output.provisionTranscript?.provision.parseError).toBeTruthy()
process.exitCode = priorExitCode
} finally {
rmSync(repoPath, { recursive: true, force: true })
}
})
})
@@ -1,592 +0,0 @@
import { describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main } from './index'
import { RuntimeRpcFailureError } from './runtime-client'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
describe('orca cli worktree awareness', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('passes an explicit parent through worktree.create without cwd inference', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: {
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
parentWorktreeId: 'repo-1::/tmp/repo/parent',
lineage: {
worktreeId: 'repo-1::/tmp/repo/child',
worktreeInstanceId: 'child-instance',
parentWorktreeId: 'repo-1::/tmp/repo/parent',
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
}
},
lineage: {
worktreeId: 'repo-1::/tmp/repo/child',
worktreeInstanceId: 'child-instance',
parentWorktreeId: 'repo-1::/tmp/repo/parent',
parentWorktreeInstanceId: 'parent-instance',
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'id:repo-1::/tmp/repo/parent',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'id:repo-1::/tmp/repo/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes traditional parent-worktree selectors through parentWorktree', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: {
...buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
parentWorktreeId: 'repo-1::/tmp/repo/parent'
},
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'branch:feature/parent',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'branch:feature/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes workspace-key parent-worktree selectors through parentWorkspace', async () => {
const cases = [
{ selector: 'folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
},
{ selector: 'id:folder:folder-1', parentWorkspace: 'folder:folder-1' },
{
selector: 'id:worktree:repo-1::/tmp/repo/parent',
parentWorkspace: 'worktree:repo-1::/tmp/repo/parent'
}
]
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const testCase of cases) {
callMock.mockReset()
queueFixtures(
callMock,
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: testCase.parentWorkspace,
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
testCase.selector,
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: testCase.parentWorkspace,
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
}
})
it('passes folder workspace environment lineage through worktree.create', async () => {
process.env.ORCA_WORKSPACE_ID = 'folder:folder-1'
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo', 'main', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'env-workspace', confidence: 'inferred' },
createdAt: 1
},
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--json'],
'/tmp/repo'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
envParentWorkspace: 'folder:folder-1',
cwdParentWorktree: 'id:repo-1::/tmp/repo',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('resolves current for explicit parent-worktree on create', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/parent', 'feature/parent', 'abc', 'repo-1')]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'current',
'--json'
],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: 'id:repo-1::/tmp/repo/parent',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
it('routes active/current folder workspace parent selectors through parentWorkspace on create', async () => {
const folderWorkspace = {
...buildWorktree('/tmp/folder', '', '', 'folder-workspace:group-1'),
id: 'folder:folder-1',
repoId: 'folder-workspace:group-1',
displayName: 'Folder'
}
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
for (const parentSelector of ['current', 'active']) {
callMock.mockReset()
queueFixtures(
callMock,
worktreeListFixture([folderWorkspace]),
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
workspaceLineage: {
childWorkspaceKey: 'worktree:repo-1::/tmp/repo/child',
childInstanceId: 'child-instance',
parentWorkspaceKey: 'folder:folder-1',
parentInstanceId: null,
origin: 'cli',
capture: { source: 'explicit-cli-flag', confidence: 'explicit' },
createdAt: 1
},
warnings: []
})
)
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
parentSelector,
'--json'
],
'/tmp/folder/src'
)
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:folder-1',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
}
})
it('rejects contradictory parent flags on worktree.create before resolving selectors', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'current',
'--no-parent',
'--json'
],
'/tmp/not-managed'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Choose either one parent selector or --no-parent.'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('rejects removed parent-workspace on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
const outputModes = [[], ['--json']]
for (const outputArgs of outputModes) {
logSpy.mockClear()
errSpy.mockClear()
process.exitCode = priorExitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-workspace',
'folder:folder-1',
...outputArgs
],
'/tmp/repo'
)
const output = [...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')
expect(output).toContain('Unknown flag --parent-workspace for command: worktree create')
expect(callMock).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
}
process.exitCode = priorExitCode
})
it('rejects bare parent-worktree on worktree.create', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'--json'
],
'/tmp/repo'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'Missing required --parent-worktree'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('reports runtime parent selector failures without hidden flag guidance', async () => {
callMock.mockRejectedValueOnce(
new RuntimeRpcFailureError({
id: 'req_create',
ok: false,
error: {
code: 'LINEAGE_PARENT_NOT_FOUND',
message: 'Parent selector was not found.',
data: {
nextSteps: [
'Pass a valid --parent-worktree selector such as folder:<id>, worktree:<worktreeId>, id:<repo-id>::<path>, branch:<branch>, issue:<number>, path:<absolute-path>, or active/current.',
'Retry with --no-parent to create without lineage.'
]
}
},
_meta: { runtimeId: 'runtime-1' }
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
[
'worktree',
'create',
'--repo',
'id:repo-1',
'--name',
'child',
'--parent-worktree',
'folder:missing',
'--json'
],
'/tmp/repo'
)
const output = String(logSpy.mock.calls[0][0])
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
parentWorkspace: 'folder:missing',
noParent: false,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
expect(output).toContain('"ok": false')
expect(output).toContain('Parent selector was not found.')
expect(output).toContain('--parent-worktree selector')
expect(output).not.toContain('--parent-workspace')
expect(errSpy).not.toHaveBeenCalled()
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('passes no-parent through worktree.create and skips cwd inference', async () => {
queueFixtures(
callMock,
okFixture('req_create', {
worktree: buildWorktree('/tmp/repo/child', 'child', 'abc', 'repo-1'),
lineage: null,
warnings: []
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
vi.spyOn(console, 'error').mockImplementation(() => {})
await main(
['worktree', 'create', '--repo', 'id:repo-1', '--name', 'child', '--no-parent', '--json'],
'/tmp/repo/parent/src'
)
expect(callMock).toHaveBeenCalledTimes(1)
expect(callMock).toHaveBeenCalledWith('worktree.create', {
repo: 'id:repo-1',
name: 'child',
displayName: 'child',
displayNameKind: 'user',
baseBranch: undefined,
linkedIssue: undefined,
comment: undefined,
runHooks: false,
activate: false,
parentWorktree: undefined,
noParent: true,
callerTerminalHandle: undefined,
cliProvenanceRequest: {}
})
})
})
@@ -1,296 +0,0 @@
import path from 'node:path'
import { describe, expect, it, vi } from 'vitest'
const {
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
} = vi.hoisted(() => ({
callMock: vi.fn(),
runtimeClientConstructorMock: vi.fn(),
serveOrcaAppMock: vi.fn(),
getDefaultUserDataPathMock: vi.fn(() => '/tmp/orca-user-data'),
addEnvironmentFromPairingCodeMock: vi.fn(),
listEnvironmentsMock: vi.fn(),
spawnMock: vi.fn()
}))
vi.mock('./runtime-client', async () => {
const { createRuntimeClientModuleMock } = await import('./index-test-harness.js')
return createRuntimeClientModuleMock({
callMock,
runtimeClientConstructorMock,
serveOrcaAppMock,
getDefaultUserDataPathMock
})
})
vi.mock('./runtime/environments', () => ({
addEnvironmentFromPairingCode: addEnvironmentFromPairingCodeMock,
removeEnvironment: vi.fn(),
resolveEnvironment: vi.fn()
}))
vi.mock('../shared/remote-runtime/host-store/runtime-environment-store', () => ({
listEnvironments: listEnvironmentsMock
}))
vi.mock('child_process', async () => {
const { createChildProcessModuleMock } = await import('./index-test-harness.js')
return createChildProcessModuleMock(spawnMock)
})
import { main, normalizeWorktreeSelector } from './index'
import { buildWorktree, okFixture, queueFixtures, worktreeListFixture } from './test-fixtures'
import { useWorktreeAwarenessEnvironment } from './index-test-harness'
describe('orca cli worktree awareness', () => {
useWorktreeAwarenessEnvironment({
callMock,
serveOrcaAppMock,
getDefaultUserDataPathMock,
addEnvironmentFromPairingCodeMock,
listEnvironmentsMock,
spawnMock
})
it('normalizes active/current worktree selectors to cwd', () => {
const resolved = path.resolve('/tmp/repo/feature')
expect(normalizeWorktreeSelector('active', '/tmp/repo/feature')).toBe(`path:${resolved}`)
expect(normalizeWorktreeSelector('current', '/tmp/repo/feature')).toBe(`path:${resolved}`)
expect(normalizeWorktreeSelector('branch:feature/foo', '/tmp/repo/feature')).toBe(
'branch:feature/foo'
)
})
it('shows the enclosing worktree for `worktree current`', async () => {
queueFixtures(
callMock,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main'),
buildWorktree('/tmp/repo/feature', 'feature/foo'),
buildWorktree('/tmp/repo/feature', 'feature/foo', 'abc', 'duplicate-repo')
]),
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'current', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenNthCalledWith(1, 'worktree.list', {
limit: 10_000
})
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
expect(logSpy).toHaveBeenCalledExactlyOnceWith(
JSON.stringify(
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
}),
null,
2
)
)
})
it('resolves the invocation cwd from ORCA_CLI_CWD when no cwd is passed', async () => {
// ORCA_CLI_CWD carries the remote shell's cwd when the bridge executes the CLI on the host.
process.env.ORCA_CLI_CWD = '/tmp/repo/feature/src'
try {
queueFixtures(
callMock,
worktreeListFixture([
buildWorktree('/tmp/repo', 'main'),
buildWorktree('/tmp/repo/feature', 'feature/foo')
]),
okFixture('req_1', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'current', '--json'])
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
} finally {
delete process.env.ORCA_CLI_CWD
}
})
it.skipIf(process.platform === 'win32')(
'prepares and starts Claude Agent Teams in the current Orca terminal',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
vi.stubEnv('ELECTRON_RUN_AS_NODE', '1')
vi.stubEnv('TMUX_PANE', '%99')
try {
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(['claude-teams'], '/tmp/repo')
expect(callMock).toHaveBeenCalledWith('agentTeams.prepareLaunch', {
paneKey: 'tab-1:11111111-1111-4111-8111-111111111111',
prepareAuth: true,
env: expect.objectContaining({
ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111'
})
})
expect(spawnMock).toHaveBeenCalledWith('claude', ['--teammate-mode', 'auto'], {
stdio: 'inherit',
env: expect.objectContaining({
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin',
ORCA_PANE_KEY: 'tab-1:11111111-1111-4111-8111-111111111111'
})
})
expect(callMock.mock.calls[0][1].env).not.toHaveProperty('ELECTRON_RUN_AS_NODE')
expect(spawnMock.mock.calls[0][2].env).not.toHaveProperty('ELECTRON_RUN_AS_NODE')
} finally {
vi.unstubAllEnvs()
}
}
)
it.skipIf(process.platform === 'win32')(
'passes Claude Agent Teams arguments through to Claude Code',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(
['claude-teams', '--resume', 'session-1', '--model', 'sonnet', 'review this'],
'/tmp/repo'
)
expect(spawnMock).toHaveBeenCalledWith(
'claude',
['--teammate-mode', 'auto', '--resume', 'session-1', '--model', 'sonnet', 'review this'],
{
stdio: 'inherit',
env: expect.objectContaining({
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX_PANE: '%1'
})
}
)
}
)
it.skipIf(process.platform === 'win32')(
'does not duplicate an explicit Claude teammate mode',
async () => {
process.env.ORCA_PANE_KEY = 'tab-1:11111111-1111-4111-8111-111111111111'
queueFixtures(
callMock,
okFixture('req_agent_teams_prepare', {
launch: {
env: {
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
TMUX: '/tmp/orca-claude-agent-teams/team-1,0,1',
TMUX_PANE: '%1',
PATH: '/tmp/orca-shim:/usr/bin'
}
}
})
)
await main(['claude-teams', '--teammate-mode', 'in-process'], '/tmp/repo')
expect(spawnMock).toHaveBeenCalledWith(
'claude',
['--teammate-mode', 'in-process'],
expect.objectContaining({ stdio: 'inherit' })
)
}
)
it('rejects remote `worktree current` without listing worktrees from client cwd', async () => {
const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {})
const errSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
const priorExitCode = process.exitCode
await main(
['worktree', 'current', '--pairing-code', 'remote-runtime', '--json'],
'/tmp/repo/src'
)
expect(callMock).not.toHaveBeenCalled()
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'current is a local cwd shortcut and cannot be resolved against a remote runtime.'
)
expect([...logSpy.mock.calls, ...errSpy.mock.calls].flat().join('\n')).toContain(
'id:<repo-id>::<path>'
)
expect(process.exitCode).toBe(1)
process.exitCode = priorExitCode
})
it('uses the resolved enclosing worktree for other worktree consumers', async () => {
queueFixtures(
callMock,
worktreeListFixture([buildWorktree('/tmp/repo/feature', 'feature/foo')]),
okFixture('req_show', {
worktree: {
id: 'repo::/tmp/repo/feature',
branch: 'feature/foo',
path: '/tmp/repo/feature'
}
})
)
vi.spyOn(console, 'log').mockImplementation(() => {})
await main(['worktree', 'show', '--worktree', 'current', '--json'], '/tmp/repo/feature/src')
expect(callMock).toHaveBeenNthCalledWith(2, 'worktree.show', {
worktree: 'id:repo::/tmp/repo/feature'
})
})
})
+1 -410
View File
@@ -3,7 +3,6 @@ import { mkdir, mkdtemp, readFile, rename, rm, writeFile } from 'node:fs/promise
import { tmpdir } from 'node:os'
import { join, resolve } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { headlessServeOzonePlatformSwitches } from '../../shared/headless-serve-ozone-platform'
import { encodePairingOffer } from '../../shared/remote-runtime/pairing/pairing'
import { PAIRING_OFFER_VERSION } from '../../shared/remote-runtime/pairing/mobile-relay-pairing-offer'
import {
@@ -24,9 +23,7 @@ vi.mock('child_process', () => ({
spawn: spawnMock,
spawnSync: spawnSyncMock
}))
import { launchOrcaApp, serveOrcaApp } from './launch'
import { getExecutableAppArgs, stripElectronRunAsNode } from './orca-app-executable'
import { serveOrcaApp } from './launch'
class FakeChildProcess extends EventEmitter {
stdout = new EventEmitter()
@@ -74,24 +71,6 @@ const SSH_RECIPE_JSON = JSON.stringify({
const INVALID_SSH_RECIPE_JSON = SSH_RECIPE_JSON.replace('/workspace/repo', 'relative/repo')
const IGNORED_NON_RECIPE_STDOUT = '[serve] ignored non-recipe stdout'
const PROFILE = resolve('/profiles/serve')
const PIN_PROFILE = `--user-data-dir=${PROFILE}`
const HOST_SERVE_OZONE_SWITCHES = headlessServeOzonePlatformSwitches(process.platform)
function restoreEnv(key: string, value: string | undefined): void {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
function firstServeSpawnArgs(): string[] {
const args: unknown = spawnMock.mock.calls[0]?.[1]
if (!Array.isArray(args)) {
throw new Error('orca serve did not spawn its server')
}
return args.map(String)
}
function startRecipeJsonServer() {
const child = new FakeChildProcess()
@@ -128,77 +107,6 @@ describe('serveOrcaApp', () => {
)
})
it('names what to run when this orca is not an Orca app (an SSH host server CLI)', () => {
delete process.env.ORCA_APP_EXECUTABLE
const previous = process.env.ELECTRON_RUN_AS_NODE
delete process.env.ELECTRON_RUN_AS_NODE
try {
expect(() => serveOrcaApp({ supervise: true })).toThrow(
/Install the Orca app on this machine/
)
expect(spawnMock).not.toHaveBeenCalled()
} finally {
restoreEnv('ELECTRON_RUN_AS_NODE', previous)
}
})
it('launches an isolated profile with the keychain mocked, so serve is not blocked on it', async () => {
const previousUserData = process.env.ORCA_E2E_USER_DATA_DIR
const previousHome = process.env.ORCA_E2E_HOME_DIR
process.env.ORCA_E2E_USER_DATA_DIR = '/tmp/orca-e2e-profile'
try {
const { child, result } = startRecipeJsonServer()
queueMicrotask(() => child.stdout.emit('data', `${RECIPE_JSON}\n`))
await expect(result).resolves.toBe(0)
const args = firstServeSpawnArgs()
expect(args).toEqual(
expect.arrayContaining(['--password-store=basic', '--use-mock-keychain'])
)
} finally {
restoreEnv('ORCA_E2E_USER_DATA_DIR', previousUserData)
restoreEnv('ORCA_E2E_HOME_DIR', previousHome)
}
})
it('keeps the real keychain when only HOME is overridden', async () => {
const previousUserData = process.env.ORCA_E2E_USER_DATA_DIR
const previousHome = process.env.ORCA_E2E_HOME_DIR
delete process.env.ORCA_E2E_USER_DATA_DIR
process.env.ORCA_E2E_HOME_DIR = '/tmp/orca-e2e-home-only'
try {
const { child, result } = startRecipeJsonServer()
queueMicrotask(() => child.stdout.emit('data', `${RECIPE_JSON}\n`))
await expect(result).resolves.toBe(0)
const args = firstServeSpawnArgs()
expect(args).not.toContain('--use-mock-keychain')
expect(args).not.toContain('--password-store=basic')
} finally {
restoreEnv('ORCA_E2E_USER_DATA_DIR', previousUserData)
restoreEnv('ORCA_E2E_HOME_DIR', previousHome)
}
})
it('leaves a normal profile on the real keychain', async () => {
const previousUserData = process.env.ORCA_E2E_USER_DATA_DIR
const previousHome = process.env.ORCA_E2E_HOME_DIR
delete process.env.ORCA_E2E_USER_DATA_DIR
delete process.env.ORCA_E2E_HOME_DIR
try {
const { child, result } = startRecipeJsonServer()
queueMicrotask(() => child.stdout.emit('data', `${RECIPE_JSON}\n`))
await expect(result).resolves.toBe(0)
const args = firstServeSpawnArgs()
expect(args).not.toContain('--use-mock-keychain')
expect(args).not.toContain('--password-store=basic')
} finally {
restoreEnv('ORCA_E2E_USER_DATA_DIR', previousUserData)
restoreEnv('ORCA_E2E_HOME_DIR', previousHome)
}
})
it.runIf(process.platform === 'darwin')(
'keeps the serve supervisor alive until the installed target version can take ownership',
async () => {
@@ -425,216 +333,6 @@ describe('serveOrcaApp', () => {
}
)
it('pins the Electron child cwd to the app root instead of the caller cwd', async () => {
const child = {
kill: vi.fn(),
once: vi.fn(
(event: string, handler: (code: number | null, signal: string | null) => void) => {
if (event === 'exit') {
queueMicrotask(() => handler(0, null))
}
return child
}
)
}
spawnMock.mockReturnValue(child)
await expect(serveOrcaApp({ json: true })).resolves.toBe(0)
expect(spawnMock).toHaveBeenCalledWith(
'/Applications/Orca.app/Contents/MacOS/Orca',
[PIN_PROFILE, '--serve', '--serve-json', ...HOST_SERVE_OZONE_SWITCHES],
expect.objectContaining({
cwd: resolve(__dirname, '../../..')
})
)
})
it('passes mobile pairing through to the foreground server child', async () => {
const child = {
kill: vi.fn(),
once: vi.fn(
(event: string, handler: (code: number | null, signal: string | null) => void) => {
if (event === 'exit') {
queueMicrotask(() => handler(0, null))
}
return child
}
)
}
spawnMock.mockReturnValue(child)
await expect(
serveOrcaApp({
json: true,
port: '6768',
pairingAddress: '100.64.1.20',
mobilePairing: true
})
).resolves.toBe(0)
expect(spawnMock).toHaveBeenCalledWith(
'/Applications/Orca.app/Contents/MacOS/Orca',
[
PIN_PROFILE,
'--serve',
'--serve-json',
'--serve-port',
'6768',
'--serve-pairing-address',
'100.64.1.20',
'--serve-mobile-pairing',
...HOST_SERVE_OZONE_SWITCHES
],
expect.objectContaining({
cwd: resolve(__dirname, '../../..')
})
)
})
it('passes the app root before serve flags for dev Electron executables', async () => {
process.env.ORCA_APP_EXECUTABLE = '/repo/node_modules/.bin/electron'
process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT = '1'
const child = {
kill: vi.fn(),
once: vi.fn(
(event: string, handler: (code: number | null, signal: string | null) => void) => {
if (event === 'exit') {
queueMicrotask(() => handler(0, null))
}
return child
}
)
}
spawnMock.mockReturnValue(child)
await expect(serveOrcaApp({ json: true, port: '6768' })).resolves.toBe(0)
expect(spawnMock).toHaveBeenCalledWith(
'/repo/node_modules/.bin/electron',
[
resolve(__dirname, '../../..'),
PIN_PROFILE,
'--serve',
'--serve-json',
'--serve-port',
'6768',
...HOST_SERVE_OZONE_SWITCHES
],
expect.objectContaining({
cwd: resolve(__dirname, '../../..')
})
)
})
it("re-applies a direct launch's --no-sandbox to the Electron it starts, not to terminals", () => {
process.env.ORCA_ELECTRON_NO_SANDBOX = '1'
try {
expect(getExecutableAppArgs('/opt/Orca/orca-ide')).toEqual(['--no-sandbox'])
expect(stripElectronRunAsNode(process.env)).not.toHaveProperty('ORCA_ELECTRON_NO_SANDBOX')
} finally {
delete process.env.ORCA_ELECTRON_NO_SANDBOX
}
})
it.each([
{ probe: 'exits nonzero', result: { status: 1 }, expectedPrefix: ['--no-sandbox'] },
{ probe: 'succeeds', result: { status: 0 }, expectedPrefix: [] },
{
probe: 'times out',
result: {
status: null,
error: Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' })
},
expectedPrefix: ['--no-sandbox']
},
{
probe: 'cannot start',
result: { status: null, error: Object.assign(new Error('missing'), { code: 'ENOENT' }) },
expectedPrefix: ['--no-sandbox']
}
])(
'uses the extracted AppImage sandbox fallback when the userns probe $probe',
async ({ result: userNamespaceResult, expectedPrefix }) => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
const getuidDescriptor = Object.getOwnPropertyDescriptor(process, 'getuid')
const root = await mkdtemp(join(tmpdir(), 'orca-extracted-appimage-'))
temporaryDirectories.push(root)
const executable = join(root, 'orca-ide')
await writeFile(join(root, 'AppRun'), '', { mode: 0o755 })
process.env.ORCA_APP_EXECUTABLE = executable
Object.defineProperty(process, 'platform', { value: 'linux' })
Object.defineProperty(process, 'getuid', { configurable: true, value: () => 1000 })
spawnSyncMock.mockReturnValue(userNamespaceResult)
const child = new FakeChildProcess()
spawnMock.mockReturnValue(child)
try {
const result = serveOrcaApp({ json: true })
queueMicrotask(() => child.emit('exit', 0, null))
await expect(result).resolves.toBe(0)
expect(spawnSyncMock).toHaveBeenCalledWith(
'unshare',
['-Ur', 'true'],
expect.objectContaining({ stdio: 'ignore', timeout: 2_000 })
)
expect(spawnMock).toHaveBeenCalledWith(
executable,
[...expectedPrefix, PIN_PROFILE, '--serve', '--serve-json', '--ozone-platform=x11'],
// Foreground serve must share POSIX job-control signals with its CLI supervisor.
expect.objectContaining({ detached: false })
)
} finally {
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor)
}
if (getuidDescriptor) {
Object.defineProperty(process, 'getuid', getuidDescriptor)
} else {
Reflect.deleteProperty(process, 'getuid')
}
}
}
)
it('prints recipe JSON from a detached server child and exits', async () => {
const child = new FakeChildProcess()
spawnMock.mockReturnValue(child)
const writeSpy = vi.spyOn(process.stdout, 'write').mockImplementation(() => true)
const result = serveOrcaApp({
pairingAddress: 'wss://sandbox.example.com',
recipeJson: true,
projectRoot: '/workspace/repo'
})
queueMicrotask(() => {
child.stdout.emit('data', `${RECIPE_JSON}\n`)
})
await expect(result).resolves.toBe(0)
expect(spawnMock).toHaveBeenCalledWith(
'/Applications/Orca.app/Contents/MacOS/Orca',
[
PIN_PROFILE,
'--serve',
'--serve-pairing-address',
'wss://sandbox.example.com',
'--serve-recipe-json',
'--serve-project-root',
'/workspace/repo',
...HOST_SERVE_OZONE_SWITCHES
],
expect.objectContaining({
cwd: resolve(__dirname, '../../..'),
detached: true,
stdio: ['ignore', 'pipe', 'inherit']
})
)
expect(writeSpy).toHaveBeenCalledWith(`${RECIPE_JSON}\n`)
expect(child.unref).toHaveBeenCalled()
})
it('waits past startup status lines for valid recipe JSON', async () => {
const { child, result, stdoutSpy, stderrSpy } = startRecipeJsonServer()
queueMicrotask(() => {
@@ -716,111 +414,4 @@ describe('serveOrcaApp', () => {
expect(stdoutSpy).toHaveBeenCalledWith(`${RECIPE_JSON}\n`)
expect(child.unref).toHaveBeenCalledOnce()
})
it('runs a Windows command-shim Electron executable through cmd.exe without a raw shell', async () => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
Object.defineProperty(process, 'platform', { value: 'win32' })
process.env.ORCA_APP_EXECUTABLE = 'C:\\repo\\node_modules\\.bin\\electron.cmd'
const child = {
kill: vi.fn(),
once: vi.fn(
(event: string, handler: (code: number | null, signal: string | null) => void) => {
if (event === 'exit') {
queueMicrotask(() => handler(0, null))
}
return child
}
)
}
spawnMock.mockReturnValue(child)
try {
await expect(serveOrcaApp({ json: true })).resolves.toBe(0)
const [program, [commandLine], options] = spawnMock.mock.calls[0]
expect(program).toMatch(/cmd\.exe$/i)
expect(commandLine).toContain('C:\\repo\\node_modules\\.bin\\electron.cmd')
expect(commandLine).toContain('--serve-json')
expect(options).toEqual(
expect.objectContaining({ shell: false, windowsHide: true, windowsVerbatimArguments: true })
)
} finally {
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor)
}
}
})
})
describe('launchOrcaApp', () => {
beforeEach(() => {
spawnMock.mockReset()
spawnSyncMock.mockReset()
process.env.ORCA_USER_DATA_PATH = PROFILE
})
afterEach(() => {
delete process.env.ORCA_USER_DATA_PATH
delete process.env.ORCA_OPEN_COMMAND
delete process.env.ORCA_APP_EXECUTABLE
delete process.env.ORCA_APP_EXECUTABLE_NEEDS_APP_ROOT
})
it('handles asynchronous detached spawn errors without throwing', async () => {
process.env.ORCA_APP_EXECUTABLE = '/missing/Orca'
const child = new FakeChildProcess()
spawnMock.mockReturnValue(child)
launchOrcaApp()
child.emit('error', new Error('ENOENT'))
await Promise.resolve()
expect(child.unref).toHaveBeenCalled()
})
it('adds the extracted-AppImage sandbox fallback for open launches', async () => {
const platformDescriptor = Object.getOwnPropertyDescriptor(process, 'platform')
const getuidDescriptor = Object.getOwnPropertyDescriptor(process, 'getuid')
const root = await mkdtemp(join(tmpdir(), 'orca-open-extracted-appimage-'))
const executable = join(root, 'orca-ide')
try {
await writeFile(join(root, 'AppRun'), '')
process.env.ORCA_APP_EXECUTABLE = executable
process.env.ELECTRON_RUN_AS_NODE = '1'
Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' })
Object.defineProperty(process, 'getuid', { configurable: true, value: () => 1000 })
spawnSyncMock.mockReturnValue({ status: 1 })
const child = new FakeChildProcess()
spawnMock.mockReturnValue(child)
launchOrcaApp()
expect(spawnSyncMock).toHaveBeenCalledWith(
'unshare',
['-Ur', 'true'],
expect.objectContaining({ stdio: 'ignore', timeout: 2_000 })
)
expect(spawnMock).toHaveBeenCalledWith(
executable,
['--no-sandbox', PIN_PROFILE],
expect.objectContaining({
detached: true,
stdio: 'ignore',
env: expect.not.objectContaining({ ELECTRON_RUN_AS_NODE: '1' })
})
)
expect(child.unref).toHaveBeenCalledOnce()
} finally {
await rm(root, { recursive: true, force: true })
delete process.env.ELECTRON_RUN_AS_NODE
if (platformDescriptor) {
Object.defineProperty(process, 'platform', platformDescriptor)
}
if (getuidDescriptor) {
Object.defineProperty(process, 'getuid', getuidDescriptor)
} else {
Reflect.deleteProperty(process, 'getuid')
}
}
})
})
+1 -222
View File
@@ -1,4 +1,3 @@
import { AcpLaunchGate } from './acp-launch-gate'
import { PassThrough } from 'node:stream'
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { PipedProcessSpawner } from '@orca/process-host/process-spec'
@@ -14,8 +13,6 @@ import {
import { AcpAgentExitedError, AcpAuthRequiredError } from './acp-errors'
import { acpStartFailure } from './acp-startup-outcome'
import captures from '../native-chat/agent-session-wire/__fixtures__/startup-protocol-rejections.json'
import { PROVIDER_SPAWN_FAILURE_MARKER } from '../provider-process/provider-spawn-failure-report'
import { providerExecutableMissing } from '../provider-process/provider-executable-missing'
import { AcpScriptedAgent, deferred, tick } from './acp-scripted-agent.test-support'
const teardown = vi.hoisted(() =>
@@ -24,8 +21,7 @@ const teardown = vi.hoisted(() =>
vi.mock('../provider-process/provider-process-teardown', () => ({
terminateProviderProcessTree: teardown
}))
const actual = await vi.importActual<{
await vi.importActual<{
terminateProviderProcessTree: typeof terminateProviderProcessTree
}>('../provider-process/provider-process-teardown')
@@ -34,12 +30,6 @@ const grace =
process.platform === 'win32' ? ROOT_ONLY_GRACEFUL_EXIT_MS : PROVIDER_SUPERVISOR_MAX_STOP_MS
const start = { cwd: '/execution-host/folder', mcpServers: [] }
const prompt = [{ type: 'text', text: 'hello' }] as const
/** What the supervisor writes when it cannot spawn the agent's CLI. */
const SPAWN_ENOENT_REPORT = `${PROVIDER_SPAWN_FAILURE_MARKER}${JSON.stringify({
thrown: false,
code: 'ENOENT',
message: 'spawn fixture-acp-agent ENOENT'
})}\n`
function fixture(
options: AcpAgentConnectionOptions = {},
@@ -87,49 +77,6 @@ afterEach(async () => {
describe('ACP process-owning connection', () => {
// The supervisor (POSIX only) reports the failed spawn on stderr and exits 127.
it.skipIf(process.platform === 'win32')(
'marks the exit of a CLI the supervisor could not spawn as a missing executable',
async () => {
const onExit = vi.fn()
const { connection, child } = fixture({ onExit }, { cwd: process.cwd() })
child.stderr.write(SPAWN_ENOENT_REPORT)
await tick()
child.emit('exit', 127, null)
child.emit('close', 127, null)
await vi.waitFor(() => expect(onExit).toHaveBeenCalledOnce())
expect(providerExecutableMissing(onExit.mock.calls[0][0])).toBe(true)
expect(connection.spawnAbsence).toBe('executable')
}
)
it('spawns through the supervisor with the host launch and exposes typed session calls', async () => {
const { connection, spawn, child, agent } = fixture()
await connection.spawned
expect(connection.pid).toBe(child.pid)
expect(connection.rootVerdict).toBe('live')
expect(connection.lastCloseResult).toBeNull()
expect(spawn).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({
cwd: start.cwd,
env: expect.objectContaining({ ACP_ACCOUNT_HOME: '/host/account' }),
stdio: ['pipe', 'pipe', 'pipe', ...(process.platform === 'win32' ? [] : ['ipc'])]
})
)
expect(spawn.mock.calls[0][0].env).not.toHaveProperty('STRIPPED')
expect(await connection.start(start)).toMatchObject({
sessionId: 'session-1'
})
agent.on('session/prompt', (frame) => agent.reply(frame, { stopReason: 'end_turn' }))
expect(await connection.prompt([...prompt])).toEqual({
stopReason: 'end_turn'
})
connection.pauseReading()
expect(child.stdout.isPaused()).toBe(true)
connection.resumeReading()
expect(child.stdout.isPaused()).toBe(false)
expect(await connection.close()).toBe(true)
expect(connection.lastCloseResult).toEqual({ root: 'exited', tree: null })
})
it('settles prompts and permission signals on proven exit with stdout still open', async () => {
const statusDuringExit: unknown[] = []
@@ -252,30 +199,6 @@ describe('ACP process-owning connection', () => {
expect(vi.getTimerCount()).toBe(0)
})
it('joins physical close and retries unproven exit without spawning a replacement', async () => {
vi.useFakeTimers()
const onExit = vi.fn()
const { connection, child, spawn } = fixture({ onExit }, { exitOnEnd: false })
const first = connection.close()
const joined = connection.close()
await vi.advanceTimersByTimeAsync(grace + 1_000)
expect(await first).toBe(false)
expect(await joined).toBe(false)
expect(connection.rootVerdict).toBe('live')
expect(teardown).toHaveBeenCalledOnce()
teardown.mockImplementationOnce(async () => {
child.emit('exit', null, 'SIGKILL')
return 'unverifiable'
})
const retry = connection.close()
await vi.advanceTimersByTimeAsync(grace + 1_000)
expect(await retry).toBe(true)
expect(connection.processTreeUnproven).toBe(true)
expect(spawn).toHaveBeenCalledOnce()
expect(onExit.mock.calls[0][1].expected).toBe(true)
expect(vi.getTimerCount()).toBe(0)
})
it('ends notification draining at the bounded close result even when exit remains unproven', async () => {
vi.useFakeTimers()
const onExtensionNotification = vi.fn()
@@ -293,24 +216,6 @@ describe('ACP process-owning connection', () => {
expect(agent.stdout.listenerCount('data')).toBe(0)
})
it.each(['live', 'unverifiable'] as const)(
'retains %s tree evidence when root exits between completed close attempts',
async (tree) => {
vi.useFakeTimers()
const { connection, child } = fixture({}, { exitOnEnd: false })
teardown.mockResolvedValueOnce(tree)
const close = connection.close()
await vi.advanceTimersByTimeAsync(grace + 1_000)
expect(await close).toBe(false)
child.emit('exit', null, 'SIGKILL')
expect(connection.processTreeUnproven).toBe(true)
expect(await connection.close()).toBe(true)
expect(connection.processTreeUnproven).toBe(true)
expect(connection.lastCloseResult).toEqual({ root: 'live', tree })
expect(teardown).toHaveBeenCalledOnce()
}
)
it('keeps the final answer an agent wrote just before it exited, and only then reads its exit', async () => {
const { response } = captures.acp
const onExit = vi.fn()
@@ -338,30 +243,6 @@ describe('ACP process-owning connection', () => {
expect(acpStartFailure(await loaded).retry).toBe('safe')
})
it('isolates early and late exit subscribers and continues delivering exit', () => {
const onDiagnostic = vi.fn(() => {
throw new Error('diagnostic')
})
const { connection, child } = fixture({ onDiagnostic })
const failing = (): void => {
throw new Error('observer')
}
const early = vi.fn()
connection.onExit(failing)
connection.onExit(early)
expect(() => child.emit('exit', 0, null)).not.toThrow()
expect(early).toHaveBeenCalledExactlyOnceWith(
{ code: 0, signal: null, processless: false },
false,
undefined
)
expect(() => connection.onExit(failing)).not.toThrow()
const late = vi.fn()
connection.onExit(late)
expect(late).toHaveBeenCalledOnce()
expect(onDiagnostic).toHaveBeenCalledTimes(2)
})
it('settles a failed spawn but requires processless close evidence before reporting exit', async () => {
const onExit = vi.fn()
const { connection, child, agent } = fixture({ onExit }, { pid: null, exitOnEnd: false })
@@ -377,33 +258,6 @@ describe('ACP process-owning connection', () => {
expect(onExit.mock.calls[0][1].exit.processless).toBe(true)
expect(await connection.close()).toBe(true)
})
it('owns stderr failure cleanup and isolates failing exit callbacks', async () => {
const { connection, child } = fixture({
onExit: () => {
throw new Error('listener')
}
})
await connection.start(start)
const rejected = expect(connection.prompt([...prompt])).rejects.toThrow('stderr failure')
child.stderr.destroy(new Error('stderr failure'))
await rejected
await tick()
expect(connection.exited).toBe(true)
expect(await connection.close()).toBe(true)
})
it('rejects invalid peer options before starting any process', () => {
const spawn = vi.fn<PipedProcessSpawner>()
expect(() =>
createAcpAgentConnection(
{ command: 'fixture', args: [] },
{ peer: { maxPendingRequests: 0 } },
spawn
)
).toThrow('positive finite integers')
expect(spawn).not.toHaveBeenCalled()
})
})
it.each([true, false])(
@@ -446,78 +300,3 @@ it('closes a recognized stdout prompt over 16 MiB without inventing a signed-out
await vi.waitFor(() => expect(connection.closed).toBe(true))
expect(answered).toHaveBeenCalledTimes(1)
})
it.each([true, false])(
'runs setup through the retained launch gate with initial kill delivery=%s',
async (initialDelivery) => {
vi.useFakeTimers()
const { connection, child } = fixture({}, { exitOnEnd: false })
let permitted = initialDelivery
let writerGone = false
const deliver = () => {
if (!permitted || writerGone) {
throw Object.assign(new Error('writer group unavailable'), { code: 'ESRCH' })
}
writerGone = true
}
teardown.mockImplementation((held, deps) =>
actual.terminateProviderProcessTree(held, {
...deps,
captureDescendants: async () => ({
rootPgid: null,
capturedAtMs: 1,
descendants: [
{
pid: child.pid! + 1,
ppid: child.pid!,
pgid: child.pid! + 1,
startedAt: 'Mon Jan 1 00:00:00 2026'
}
]
}),
terminateDescendants: async () => 'unverifiable',
signalProcessGroup: deliver,
signalProcess: () => {},
terminateWindowsTree: async () => {
try {
deliver()
return true
} catch {
return false
}
}
})
)
const gate = new AcpLaunchGate('Antigravity')
gate.retain(connection)
const first = connection.close()
await vi.advanceTimersByTimeAsync(grace + 1000)
expect(await first).toBe(initialDelivery)
const work = vi.fn(async () => 'setup completed')
if (!initialDelivery) {
const denied = expect(gate.run(async () => {}, work)).rejects.toThrow('could not be stopped')
await vi.advanceTimersByTimeAsync(grace + 1000)
await denied
expect(work).not.toHaveBeenCalled()
permitted = true
const retry = gate.run(async () => {}, work)
await vi.advanceTimersByTimeAsync(grace + 1000)
await expect(retry).resolves.toBe('setup completed')
} else {
await expect(gate.run(async () => {}, work)).resolves.toBe('setup completed')
}
expect(work).toHaveBeenCalledOnce()
const attempts = initialDelivery ? 1 : 3
expect(teardown).toHaveBeenCalledTimes(attempts)
await expect(gate.run(async () => {}, work)).resolves.toBe('setup completed')
expect(work).toHaveBeenCalledTimes(2)
expect(teardown).toHaveBeenCalledTimes(attempts)
const next = gate.enter(undefined)
expect(next.signal.aborted).toBe(false)
next.leave()
expect(connection.lastCloseResult).toMatchObject({
root: 'live',
killDelivered: true
})
}
)
-45
View File
@@ -80,49 +80,4 @@ describe('OMP ask forms', () => {
})
expect(presentation.reply(null)).toEqual({ action: 'cancel' })
})
it('tells OMP which questions past the fourth were never shown', () => {
const properties = Object.fromEntries(
[0, 1, 2, 3, 4].map((n) => [
`q${n}__other`,
{ type: 'string', title: 'Other (type your own)' }
])
)
const presentation = acpFormElicitationPresentation({
...ask,
message: 'Answer 5 questions',
requestedSchema: { type: 'object', properties }
})
const answers = [1, 2, 3, 4].map((n) => ({
questionId: `q${n}`,
optionIds: [],
other: `a${n}`
}))
expect(presentation.reply({ kind: 'answers', answers })).toEqual({
action: 'accept',
content: {
q0__other: 'a1',
q1__other: 'a2',
q2__other: 'a3',
q3__other: 'a4',
q4__other: expect.any(String)
}
})
})
it('answers a confirm form with a boolean', () => {
const confirm = acpFormElicitationPresentation({
mode: 'form',
sessionId: 'session-1',
message: 'Switch model?',
requestedSchema: {
type: 'object',
properties: { value: { type: 'boolean' } },
required: ['value']
}
})
expect(
confirm.reply({ kind: 'answers', answers: [{ questionId: 'q1', optionIds: ['o1'] }] })
).toEqual({ action: 'accept', content: { value: true } })
})
})
-43
View File
@@ -8,7 +8,6 @@ import {
AcpRpcError
} from './acp-errors'
import { AcpScriptedAgent, deferred, tick } from './acp-scripted-agent.test-support'
import { providerRequestTimeoutOf } from '../provider-process/provider-request-timeout'
import captures from '../native-chat/agent-session-wire/__fixtures__/startup-protocol-rejections.json'
const peers: AcpJsonRpcPeer[] = []
@@ -88,29 +87,6 @@ describe('ACP JSON-RPC peer', () => {
})
})
it('returns method-not-found and preserves explicit handler error objects', async () => {
const { agent } = fixture({
onRequest: (method) => {
if (method === '_denied') {
throw new AcpRpcError(-32005, 'Denied', { reason: 'policy' })
}
if (method === '_crash') {
throw new Error('Handler failed')
}
throw new AcpRpcError(-32601, 'Unknown method')
}
})
expect(await agent.request('missing', '_missing', {})).toMatchObject({
error: { code: -32601 }
})
expect(await agent.request('denied', '_denied', {})).toMatchObject({
error: { code: -32005, message: 'Denied', data: { reason: 'policy' } }
})
expect(await agent.request('crash', '_crash', {})).toMatchObject({
error: { code: -32603, message: 'Handler failed' }
})
})
it('allows the agent to reuse a request id after receiving its response', async () => {
const { agent } = fixture({ onRequest: () => ({ answer: true }) })
expect(await agent.request('reused', '_question', {})).toMatchObject({
@@ -220,17 +196,6 @@ describe('ACP JSON-RPC peer', () => {
expect(agent.frames.map((frame) => frame.method)).toEqual(['wait', 'next'])
})
it('names the request and its deadline on a timeout, so a failed start can say which stalled', async () => {
vi.useFakeTimers()
const { peer } = fixture()
const timedOut = peer.request('session/new', {}, { timeoutMs: 300 }).catch((error) => error)
await vi.advanceTimersByTimeAsync(300)
expect(providerRequestTimeoutOf(await timedOut)).toEqual({
method: 'session/new',
timeoutMs: 300
})
})
it('bounds incoming requests without expiring user decisions', async () => {
vi.useFakeTimers()
const signal = deferred<AbortSignal>()
@@ -361,14 +326,6 @@ describe('ACP JSON-RPC peer', () => {
input.destroy()
})
it('fails pending requests on an output stream error', async () => {
const { peer, agent } = fixture()
const rejected = expect(peer.request('wait', {})).rejects.toThrow('Broken pipe')
agent.stdin.emit('error', new Error('Broken pipe'))
await rejected
expect(peer.closed).toBe(true)
})
it('handles asynchronous write callback errors without an unhandled stream error', async () => {
const input = new PassThrough()
const output = new Writable({
@@ -9,18 +9,9 @@ import {
} from './acp-model-catalog-probe'
import { AcpSessionRuntime } from './acp-session-runtime'
import { AcpScriptedAgent } from './acp-scripted-agent.test-support'
import { AgentModelCatalogListingStoppedError } from '../native-chat/agent-model-catalog/agent-model-catalog-failures'
import type { ProviderProcessLaunch } from '../provider-process/provider-process-launch'
import { openCodeAcpAccountBinding } from '../opencode/opencode-structured-account-home'
import authRequired from './__fixtures__/session-new-auth-required.json'
import recordedEfforts from './fixtures/opencode-v2.0.21-model-efforts.json'
import { SetSessionConfigOptionRequestSchema } from './generated/acp-protocol.generated'
const PREFIX = ['run', '--', 'agent']
const launcherSettings = (agent: string) => ({
agentCmdOverrides: { [agent]: `"${process.execPath}" ${PREFIX.join(' ')}` }
})
const GROK = acpLaunchSpecFor('grok')!
// Bypass the app's managed-account service for this fixture.
const OPENCODE: AcpLaunchSpec = {
...acpLaunchSpecFor('opencode')!,
@@ -30,28 +21,6 @@ const OPENCODE: AcpLaunchSpec = {
environmentForAccount: () => ({})
}))
}
const GROK_HOME = { variable: 'GROK_HOME', path: '/homes/grok-a' }
const MODEL_STATE = {
currentModelId: 'grok-build',
availableModels: [
{
modelId: 'grok-build',
name: 'Grok Build',
_meta: {
supportsReasoningEffort: true,
// Reported model defaults take precedence over the menu's recommendation.
reasoningEffort: 'low',
reasoningEfforts: [
{ value: 'low', id: 'fast', label: 'Fast' },
{ value: 'high', id: 'deep', default: true }
]
}
},
{ modelId: 'grok-mini', name: 'Grok Mini', _meta: { supportsReasoningEffort: true } },
{ modelId: 'grok-chat', name: 'Grok Chat', _meta: {} }
]
}
const agents: AcpScriptedAgent[] = []
afterEach(() => {
@@ -61,47 +30,6 @@ afterEach(() => {
}
})
function scriptedGrok(initializeMeta: Record<string, unknown>): {
agent: AcpScriptedAgent
launches: ProviderProcessLaunch[]
closed: () => number
deps: AcpModelCatalogProbeDeps
} {
const agent = new AcpScriptedAgent()
agents.push(agent)
agent.on('initialize', (frame) =>
agent.reply(frame, { protocolVersion: 1, agentCapabilities: {}, _meta: initializeMeta })
)
agent.on('x.ai/models/list', (frame) => agent.reply(frame, { result: MODEL_STATE }))
const launches: ProviderProcessLaunch[] = []
let closes = 0
return {
agent,
launches,
closed: () => closes,
deps: {
resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }),
dataDirectory: '/orca-data',
resolveLaunchEnv: () => ({ XAI_API_KEY: 'from-settings' }),
resolveCommand: () => '/opt/grok/bin/grok',
homePath: '/home/user',
connect: (launch) => {
launches.push(launch)
const runtime = new AcpSessionRuntime(agent.stdout, agent.stdin)
return {
initialize: () => runtime.initialize(),
requestSessionFreeExtension: (method, params) =>
runtime.requestSessionFreeExtension(method, params),
close: async () => {
closes++
runtime.close()
}
}
}
}
}
}
function scriptedOpenCode2(canDelete: boolean): {
agent: AcpScriptedAgent
launches: ProviderProcessLaunch[]
@@ -230,239 +158,6 @@ describe('ACP model catalog probes', () => {
}
})
it.each([
{ selectedModels: undefined },
{ selectedModels: ['opencode/beam-free'] },
{ selectedModels: ['opencode/beam-free', 'google/gemini-3.8-flash'] }
])(
'discovers actual model-owned menus before deleting its session ($selectedModels)',
async ({ selectedModels }) => {
const { agent, deps, closed } = scriptedOpenCode2(true)
agent.on('session/new', (frame) =>
agent.reply(frame, { sessionId: 'probe-session', ...recordedEfforts.new })
)
agent.on('session/set_config_option', (frame) => {
const { value } = SetSessionConfigOptionRequestSchema.parse(frame.params)
agent.reply(
frame,
value === 'opencode/beam-free'
? recordedEfforts['opencode/beam-free']
: value === 'google/gemini-3.8-flash'
? recordedEfforts['google/gemini-3.8-flash']
: recordedEfforts.new
)
})
const result = await createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{
workspacePath: '/work/project',
...(selectedModels ? { selectedModels } : {})
}
)
expect(
result.models.find((row) => row.id === 'opencode/crouton-alpha-free')?.efforts
).toHaveLength(7)
expect(result.models.find((row) => row.id === 'opencode/beam-free')?.efforts).toHaveLength(6)
expect(
result.models.find((row) => row.id === 'google/gemini-3.8-flash')?.efforts
).toHaveLength(!selectedModels || selectedModels.includes('google/gemini-3.8-flash') ? 4 : 0)
expect(agent.frames.at(-1)?.method).toBe('session/delete')
expect(
agent.frames.filter((frame) => frame.method === 'session/set_config_option')
).toHaveLength(selectedModels?.length ?? 2)
expect(result.attemptedModels?.toSorted()).toEqual(
(selectedModels ?? ['opencode/beam-free', 'google/gemini-3.8-flash']).toSorted()
)
expect(closed()).toBe(1)
}
)
it('keeps a rejected or mismatched target unknown and an observed empty menu known', async () => {
const { agent, deps } = scriptedOpenCode2(true)
agent.on('session/new', (frame) =>
agent.reply(frame, { sessionId: 'probe-session', ...recordedEfforts.new })
)
const probe = () =>
createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project', selectedModels: ['opencode/beam-free'] }
)
expect((await probe()).models.find((row) => row.id === 'opencode/beam-free')).toMatchObject({
effortsKnown: false,
efforts: []
})
agent.on('session/set_config_option', (frame) => agent.reply(frame, recordedEfforts.new))
expect((await probe()).models.find((row) => row.id === 'opencode/beam-free')).toMatchObject({
effortsKnown: false,
efforts: []
})
agent.on('session/new', (frame) =>
agent.reply(frame, {
sessionId: 'probe-session',
...recordedEfforts['opencode/longcat-2.5-preview-free']
})
)
expect((await probe()).models).toMatchObject([
{ id: 'opencode/longcat-2.5-preview-free', efforts: [] }
])
expect((await probe()).models[0]?.effortsKnown).toBeUndefined()
})
it('projects an agent-authored auth-required session/new error, retaining unknown for other failures', async () => {
const { agent, deps } = scriptedOpenCode2(true)
const { code, message, data } = authRequired.reply.error
agent.on('session/new', (frame) => agent.fail(frame, code, message, data))
const list = () =>
createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
await expect(list()).rejects.toMatchObject({ unavailable: { reason: 'notSignedIn' } })
agent.on('session/new', (frame) => agent.fail(frame, -32603, 'Internal error'))
await expect(list()).rejects.toMatchObject({ code: -32603 })
})
it('lists Grok from initialize alone, under its pinned account, and closes the child', async () => {
const { agent, deps, launches, closed } = scriptedGrok({ modelState: MODEL_STATE })
deps.resolveCommandSettings = () => launcherSettings('grok')
const success = await createAcpModelCatalogProbe(GROK, deps)(GROK_HOME)
expect(agent.frames.map((frame) => frame.method)).toEqual(['initialize'])
expect(agent.frames[0]!.params).toMatchObject({
clientCapabilities: { fs: { readTextFile: false, writeTextFile: false }, terminal: false }
})
expect(launches).toEqual([
expect.objectContaining({
command: process.execPath,
args: [...PREFIX, 'agent', 'stdio'],
env: expect.objectContaining({ GROK_HOME: '/homes/grok-a', XAI_API_KEY: 'from-settings' })
})
])
expect(closed()).toBe(1)
expect(success.models).toEqual([
{
id: 'grok-build',
label: 'Grok Build',
isDefault: false,
efforts: [
{ value: 'fast', label: 'Fast' },
{ value: 'deep', label: 'Deep' }
],
defaultEffort: 'deep',
declaredDefaultEffort: 'fast'
},
{
id: 'grok-mini',
label: 'Grok Mini',
isDefault: false,
declaredDefaultEffort: null,
efforts: ['minimal', 'low', 'medium', 'high', 'xhigh'].map((value) => ({
value,
label: expect.any(String)
}))
},
{
id: 'grok-chat',
label: 'Grok Chat',
isDefault: false,
declaredDefaultEffort: null,
efforts: []
}
])
})
it('falls back to x.ai/models/list and never authenticates or opens a session', async () => {
const { agent, deps } = scriptedGrok({})
const success = await createAcpModelCatalogProbe(GROK, deps)(GROK_HOME)
expect(agent.frames.map((frame) => frame.method)).toEqual(['initialize', 'x.ai/models/list'])
expect(success.models.map((model) => model.id)).toEqual([
'grok-build',
'grok-mini',
'grok-chat'
])
})
it('fails, closing the child, when Grok lists nothing', async () => {
const { agent, deps, closed } = scriptedGrok({})
agent.on('x.ai/models/list', (frame) => agent.reply(frame, { error: 'not signed in' }))
await expect(createAcpModelCatalogProbe(GROK, deps)(GROK_HOME)).rejects.toThrow(/did not list/)
expect(closed()).toBe(1)
expect(agent.frames.some((frame) => frame.method === 'authenticate')).toBe(false)
})
it('lists OpenCode with `models --verbose` under its scrubbed launch env, with no ACP call', async () => {
const runListing = vi.fn(async () => 'opencode/big-pickle\n{\n "name": "Big Pickle"\n}\n')
const connect = vi.fn()
const probe = createAcpModelCatalogProbe(OPENCODE, {
resolveEnvironment: async () => ({ PATH: '/usr/bin', HOME: '/home/user' }),
dataDirectory: '/orca-data',
resolveCommandSettings: () => launcherSettings('opencode'),
probeVersion: async () => ({ version: '1.18.35' }),
homePath: '/home/user',
runListing,
connect
})
const success = await probe(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
expect(connect).not.toHaveBeenCalled()
expect(runListing).toHaveBeenCalledWith(
expect.objectContaining({
command: process.execPath,
args: [...PREFIX, 'models', '--verbose'],
cwd: '/work/project',
env: expect.objectContaining({ OPENCODE_CLIENT: 'acp' })
}),
expect.objectContaining({ site: 'opencode-model-catalog-probe' })
)
expect(success.models).toEqual([
{
id: 'opencode/big-pickle',
label: 'opencode/Big Pickle',
isDefault: false,
declaredDefaultEffort: null,
efforts: []
}
])
})
it('lists OpenCode 2.x from a session it opens and deletes, with no listing command', async () => {
const { agent, deps, launches } = scriptedOpenCode2(true)
deps.resolveCommandSettings = () => launcherSettings('opencode')
const success = await createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
expect(agent.frames.map((frame) => frame.method)).toEqual([
'initialize',
'session/new',
'session/set_config_option',
'session/delete'
])
// In the asking chat's folder, so OpenCode records no project the person never opened.
expect(agent.frames[1]!.params).toMatchObject({ cwd: '/work/project' })
expect(agent.frames.at(-1)!.params).toEqual({ sessionId: 'probe-session' })
expect(launches).toEqual([
expect.objectContaining({ command: process.execPath, args: [...PREFIX, 'acp'] })
])
expect(deps.runListing).not.toHaveBeenCalled()
expect(success.models.map((model) => [model.id, model.label])).toEqual([
['opencode/big-pickle', 'Big Pickle'],
['opencode/exo-free', 'Exo Free']
])
})
it('opens no session on an OpenCode 2.x that cannot delete one', async () => {
const { agent, deps } = scriptedOpenCode2(false)
await expect(
createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
).rejects.toThrow(/cannot delete a session/)
expect(agent.frames.map((frame) => frame.method)).toEqual(['initialize'])
})
it('lists OMP from a session kept in a folder it removes, in the chat folder', async () => {
const { agent, deps, launches } = scriptedOpenCode2(false)
const dataDirectory = await mkdtemp(join(tmpdir(), 'orca-omp-listing-'))
@@ -495,36 +190,6 @@ describe('ACP model catalog probes', () => {
}
})
it('opens no session on OpenCode 2.x without a chat folder, as at a prewarm', async () => {
const { agent, deps, launches } = scriptedOpenCode2(true)
const probeVersion = vi.fn(deps.probeVersion)
await expect(
createAcpModelCatalogProbe(OPENCODE, { ...deps, probeVersion })({
kind: 'opencode',
locator: { kind: 'unmanaged' }
})
).rejects.toBeInstanceOf(AgentModelCatalogListingStoppedError)
expect(launches).toEqual([])
expect(agent.frames).toEqual([])
expect(probeVersion).not.toHaveBeenCalled()
})
it('keeps the models, and says so, when OpenCode 2.x refuses to delete the listing session', async () => {
const { agent, deps } = scriptedOpenCode2(true)
agent.on('session/delete', (frame) => agent.fail(frame, -32603, 'locked'))
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
const success = await createAcpModelCatalogProbe(OPENCODE, deps)(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
expect(success.models).toHaveLength(2)
expect(warn).toHaveBeenCalledWith(
'[acp-model-catalog] opencode kept its listing session:',
expect.anything()
)
warn.mockRestore()
})
// An interrupted create can save a session without returning its ID.
it('stops a listing cut off while OpenCode 2.x creates its session, without deleting', async () => {
const { agent, deps, closed } = scriptedOpenCode2(true)
@@ -542,36 +207,4 @@ describe('ACP model catalog probes', () => {
expect(closed()).toBe(1)
expect(agent.frames.map((frame) => frame.method)).not.toContain('session/delete')
})
it('asks an OpenCode 2 chat nothing, not even its version, without a chat folder', async () => {
const { deps, launches } = scriptedOpenCode2(true)
const probeVersion = vi.fn(deps.probeVersion)
const spec = { ...acpLaunchSpecFor('opencode2')!, account: OPENCODE.account }
await expect(
createAcpModelCatalogProbe(spec, { ...deps, probeVersion })({
kind: 'opencode',
locator: { kind: 'unmanaged' }
})
).rejects.toBeInstanceOf(AgentModelCatalogListingStoppedError)
expect(probeVersion).not.toHaveBeenCalled()
expect(launches).toEqual([])
})
it('lists nothing for a release that runs no structured chat', async () => {
const runListing = vi.fn()
const probe = createAcpModelCatalogProbe(OPENCODE, {
resolveEnvironment: async () => ({ PATH: '/usr/bin' }),
dataDirectory: '/orca-data',
resolveCommand: () => '/opt/opencode/bin/opencode',
probeVersion: async () => ({ version: '1.0.0' }),
runListing
})
await expect(
probe(
{ kind: 'opencode', locator: { kind: 'unmanaged' } },
{ workspacePath: '/work/project' }
)
).rejects.toThrow(/does not run structured chats/)
expect(runListing).not.toHaveBeenCalled()
})
})