Files
orca/src/main/runtime/rpc/methods/agent-session.test.ts
T
Neil e8e0803672 fix(wsl): hold main's background WSL start before the cwd probe; let the user's runtime start check a WSL folder (#35971)
* fix(wsl): hold main's background WSL start in the shared preflight; folder launch scope honours the user's act

Main's background start (no wslUserAct) is now held in resolvePtySpawnStartupTarget, before the
startup cwd probe and any provider. A runtime start the user asked for (CLI, window, phone, split)
checks a WSL folder workspace's path as their act, so it may start the distro; background work is
refused with the stopped-distro message instead of folder_workspace_path_unavailable.

* fix(wsl): agent-session create and user resume check a WSL folder as the user's act; background holds with Start

* fix(wsl): agent-session ensure and a failed start's kept pane pass the user's act to the WSL folder check

* test(e2e): a prepared terminal.create dedupes as the caller's act
2026-10-29 13:31:17 -07:00

384 lines
13 KiB
TypeScript

import '../unused-default-rpc-methods.test-fixture'
import { describe, expect, it, vi } from 'vitest'
import {
AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY,
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION,
RUNTIME_CAPABILITIES,
RUNTIME_PROTOCOL_VERSION
} from '../../../../shared/protocol-version'
import {
AGENT_SESSION_RPC_ERROR_CODES,
AGENT_SESSION_OPERATION_FUTURE_SKEW_MS
} from '../../../../shared/agent-session-host-authority'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { RpcRequest, RpcResponse } from '../core'
import { RpcDispatcher } from '../dispatcher'
import { AGENT_SESSION_METHODS } from './agent-session'
import { TERMINAL_METHODS } from './terminal'
function request(method: string, params: unknown): RpcRequest {
return { id: 'request-1', authToken: 'token', method, params }
}
function terminalResult(disposition: 'created' | 'adopted' | 'replayed' = 'created') {
return {
terminal: {
handle: 'term_1',
worktreeId: 'worktree-1',
title: null
},
disposition
}
}
function runtimeStub() {
return {
getRuntimeId: () => 'runtime-1',
ensureAgentSession: vi.fn().mockResolvedValue(terminalResult()),
createAgentSession: vi.fn().mockResolvedValue(terminalResult())
}
}
describe('agent session RPC methods', () => {
it('dispatches an explicit structured resume without an authoritative command', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.ensureAgentSession', {
kind: 'explicit',
worktree: 'id:worktree-1',
agent: 'omp',
providerSession: { key: 'session_id', id: 'provider-session-1' },
ompResumeFilePath: '/custom/omp/project/session.jsonl',
agentArgs: '--profile review',
launchPreferences: { model: 'gpt-5', effort: 'high' },
presentation: 'focused',
placement: { tabId: 'tab-1', leafId: 'leaf-1' }
})
)
expect(response).toMatchObject({ ok: true, result: { disposition: 'created' } })
expect(runtime.ensureAgentSession).toHaveBeenCalledWith(
{
kind: 'explicit',
worktree: 'id:worktree-1',
agent: 'omp',
providerSession: { key: 'session_id', id: 'provider-session-1' },
ompResumeFilePath: '/custom/omp/project/session.jsonl',
agentArgs: '--profile review',
launchPreferences: { model: 'gpt-5', effort: 'high' },
presentation: 'focused',
placement: { tabId: 'tab-1', leafId: 'leaf-1' }
},
{}
)
})
it.each([
{
method: 'terminal.createAgentSession',
params: {
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'codex',
presentation: 'focused'
},
runtimeMethod: 'createAgentSession' as const
},
{
method: 'terminal.ensureAgentSession',
params: {
kind: 'explicit',
worktree: 'id:worktree-1',
agent: 'codex',
providerSession: { key: 'session_id', id: 'provider-session-1' },
presentation: 'focused'
},
runtimeMethod: 'ensureAgentSession' as const
}
])('keeps $method presentation viewer-local for paired clients', async (testCase) => {
for (const clientKind of ['runtime', 'mobile'] as const) {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const replies: RpcResponse[] = []
await dispatcher.dispatchStreaming(
request(testCase.method, testCase.params),
(response) => replies.push(JSON.parse(response) as RpcResponse),
{ pairedDeviceId: `paired-${clientKind}`, clientKind }
)
expect(replies).toHaveLength(1)
expect(replies[0]).toMatchObject({ ok: true })
expect(runtime[testCase.runtimeMethod]).toHaveBeenCalledWith(
{ ...testCase.params, presentation: 'background' },
{ clientId: `paired-${clientKind}`, clientKind, terminalImageViewerSupported: false }
)
}
})
it('keeps automatic authority checkpoint-only', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.ensureAgentSession', {
kind: 'automatic',
sleepingCheckpointId: 'checkpoint_123456789012345678901',
worktree: 'id:forged',
placement: { tabId: 'forged' }
})
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(runtime.ensureAgentSession).not.toHaveBeenCalled()
})
it('rejects mismatched agent/provider identity before runtime mutation', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.ensureAgentSession', {
kind: 'explicit',
worktree: 'id:worktree-1',
agent: 'antigravity',
providerSession: { key: 'session_id', id: 'provider-session-1' }
})
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(runtime.ensureAgentSession).not.toHaveBeenCalled()
})
it('rejects opaque fresh-launch authority and malformed operation IDs', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const opaque = await dispatcher.dispatch(
request('terminal.createAgentSession', {
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'codex',
command: 'codex resume provider-session-1'
})
)
const malformed = await dispatcher.dispatch(
request('terminal.createAgentSession', {
clientOperationId: 'not-time-sortable',
worktree: 'id:worktree-1',
agent: 'codex'
})
)
expect(opaque).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(malformed).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(runtime.createAgentSession).not.toHaveBeenCalled()
})
it('preserves legacy agent-bearing terminal.create requests for mixed-version clients', async () => {
const createTerminal = vi.fn().mockResolvedValue({ handle: 'term-1' })
const dedupeTerminalCreate = vi.fn(
async (
_clientIdentity: string,
worktree: string | undefined,
_clientMutationId: string | undefined,
_reconcileExisting: boolean,
run: (worktree: string | undefined, handle: string | undefined) => Promise<unknown>
) => run(worktree, undefined)
)
const runtime = {
getRuntimeId: () => 'runtime-1',
createTerminal,
dedupeTerminalCreate
}
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: TERMINAL_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.create', {
worktree: 'id:worktree-1',
command: 'codex resume provider-session-1',
launchAgent: 'codex'
})
)
expect(response.ok).toBe(true)
expect(dedupeTerminalCreate).toHaveBeenCalledWith(
'local',
'id:worktree-1',
undefined,
false,
expect.any(Function),
{ wslUserAct: true }
)
expect(createTerminal).toHaveBeenCalledWith('id:worktree-1', {
wslUserAct: true,
command: 'codex resume provider-session-1',
startupCommandDelivery: undefined,
env: undefined,
launchAgent: 'codex',
title: undefined,
focus: false,
activate: false,
presentation: undefined,
tabId: undefined,
leafId: undefined
})
})
it('rejects future-dated operation IDs before runtime mutation', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const now = 1_752_883_200_000
const dateNow = vi.spyOn(Date, 'now').mockReturnValue(now)
const response = await dispatcher.dispatch(
request('terminal.createAgentSession', {
clientOperationId: `${now + AGENT_SESSION_OPERATION_FUTURE_SKEW_MS + 1}-0123456789abcdef0123456789abcdef`,
worktree: 'id:worktree-1',
agent: 'codex'
})
)
dateNow.mockRestore()
expect(response).toMatchObject({
ok: false,
error: { code: 'agent_session_operation_invalid' }
})
expect(runtime.createAgentSession).not.toHaveBeenCalled()
})
it('passes authenticated caller identity outside the request payload', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const replies: RpcResponse[] = []
await dispatcher.dispatchStreaming(
request('terminal.createAgentSession', {
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'codex',
prompt: 'Fix the race',
promptDelivery: 'draft',
agentArgs: '--profile review',
launchPreferences: { model: 'gpt-5', effort: 'high' },
viewMode: 'chat'
}),
(response) => replies.push(JSON.parse(response) as RpcResponse),
{ clientId: 'authenticated-device', clientKind: 'runtime' }
)
expect(replies).toHaveLength(1)
expect(replies[0]).toMatchObject({ ok: true, result: { disposition: 'created' } })
expect(runtime.createAgentSession).toHaveBeenCalledWith(
{
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'codex',
prompt: 'Fix the race',
promptDelivery: 'draft',
agentArgs: '--profile review',
launchPreferences: { model: 'gpt-5', effort: 'high' },
viewMode: 'chat'
},
{
clientId: 'authenticated-device',
clientKind: 'runtime',
terminalImageViewerSupported: false
}
)
})
it('rejects draft delivery without a non-empty prompt', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.createAgentSession', {
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'claude',
prompt: ' ',
promptDelivery: 'draft'
})
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(runtime.createAgentSession).not.toHaveBeenCalled()
})
it('rejects oversized structured agent arguments before runtime mutation', async () => {
const runtime = runtimeStub()
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.createAgentSession', {
clientOperationId: '1752883200000-0123456789abcdef0123456789abcdef',
worktree: 'id:worktree-1',
agent: 'codex',
agentArgs: 'a'.repeat(16 * 1024 + 1)
})
)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
expect(runtime.createAgentSession).not.toHaveBeenCalled()
})
it.each(AGENT_SESSION_RPC_ERROR_CODES)('preserves stable runtime error %s', async (code) => {
const runtime = runtimeStub()
runtime.ensureAgentSession.mockRejectedValueOnce(new Error(code))
const dispatcher = new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: AGENT_SESSION_METHODS
})
const response = await dispatcher.dispatch(
request('terminal.ensureAgentSession', {
kind: 'automatic',
sleepingCheckpointId: 'checkpoint_123456789012345678901'
})
)
expect(response).toMatchObject({ ok: false, error: { code } })
})
it('advertises the capability without moving the mixed-version protocol fence', () => {
expect(RUNTIME_PROTOCOL_VERSION).toBe(3)
expect(MIN_COMPATIBLE_RUNTIME_CLIENT_VERSION).toBe(2)
expect(RUNTIME_CAPABILITIES).toContain(AGENT_SESSION_HOST_AUTHORITY_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY)
})
})