Files
orca/src/main/runtime/rpc/methods/structured-agent-session.test.ts
T
Brennan BensonandMerge Sim c49345d358 Fix native chat completion sorting and restored activity timestamps (#19144)
* Fix structured native chat completion sorting and timestamps

* Preserve native chat activity across settled updates and host upgrades

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 17:41:56 -07:00

828 lines
28 KiB
TypeScript

// The wire boundary: who may see `agentSession.*` at all, and what shapes it
// accepts once they can.
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types'
import type { AgentSessionJournal } from '../../../native-chat/agent-session-journal/journal-store'
import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host'
import { setStructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-registry'
import {
StructuredAgentSessionStatusFeed,
type StructuredAgentSessionStatusSubscriber
} from '../../../native-chat/agent-session-wire/structured-agent-session-status-feed'
import {
RUNTIME_CAPABILITIES,
RUNTIME_PROTOCOL_VERSION,
STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_REVEAL_RUNTIME_CAPABILITY,
STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY
} from '../../../../shared/protocol-version'
import type { OrcaRuntimeService } from '../../orca-runtime'
import type { RpcRequest, RpcResponse } from '../core'
import { RpcDispatcher } from '../dispatcher'
import { ALL_RPC_METHODS } from './index'
import { STRUCTURED_AGENT_SESSION_METHODS } from './structured-agent-session'
import { computeAgentSessionPayloadFingerprint } from '../../../../shared/agent-session-mutation-envelope'
const SESSION = 'session-alpha'
const FINGERPRINT = 'f'.repeat(64)
const OPERATION = '1800000000000-00000000000000000000000000000001'
function envelope(overrides: Record<string, unknown> = {}) {
return {
sessionId: SESSION,
clientOperationId: OPERATION,
expectedRuntimeFence: 1,
payloadFingerprint: FINGERPRINT,
...overrides
}
}
function sendParams(overrides: Record<string, unknown> = {}) {
return {
envelope: envelope(),
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'hi' }] },
...overrides
}
}
function attachParams(overrides: Record<string, unknown> = {}) {
return {
envelope: envelope({ expectedRuntimeFence: null }),
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'git-worktree'
},
provider: 'codex',
agent: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' },
runtimeKind: 'native',
providerHandle: { kind: 'codex', threadId: 'thread-1' },
...overrides
}
}
function request(method: string, params: unknown): RpcRequest {
return { id: 'request-1', authToken: 'token', method, params }
}
let hostCalls: Record<string, ReturnType<typeof vi.fn>>
let runtimeCalls: Record<string, ReturnType<typeof vi.fn>>
const STATUS_SESSION = 'session-status'
const STATUS_ITEMS: AgentJournalRenderItem[] = [
{
itemId: 'user-1',
sequence: 1,
revision: 1,
observedAt: 1,
body: { kind: 'message', role: 'user', blocks: [{ type: 'text', text: 'write a poem' }] }
},
{
itemId: 'turn-1',
sequence: 2,
revision: 1,
observedAt: 2,
body: { kind: 'status', text: 'Working', turnLifecycle: { turnId: 'turn-1', state: 'running' } }
}
]
/** One indexed session over a journal that reads back fixed items; the projection is real. */
function statusFeed(): StructuredAgentSessionStatusFeed {
return new StructuredAgentSessionStatusFeed({
sessions: new Map([
[
STATUS_SESSION,
{
journal: {
isReadOnly: false,
lastActivityAt: () => 2,
snapshot: () => ({ items: STATUS_ITEMS })
} as unknown as AgentSessionJournal,
params: { location: { workspaceId: 'workspace-1' }, provider: 'codex' as const }
}
]
]),
getRecord: () => null,
now: () => 1_000
})
}
function hostStub(): StructuredAgentSessionHost {
hostCalls = {
attach: vi.fn(async () => ({
ok: true,
replayed: false,
fence: 1,
cursor: { epoch: 'epoch-a', sequence: 0 },
value: {
sessionId: SESSION,
fence: 1,
page: {
sessionId: SESSION,
epoch: 'epoch-a',
direction: 'tail',
items: [],
removedItemIds: [],
submissions: [],
window: {
oldest: null,
newest: null,
nextCursor: { epoch: 'epoch-a', sequence: 0 }
},
liveCursor: { epoch: 'epoch-a', sequence: 0 },
hasOlder: false,
hasNewer: false
},
unconfirmedClientMessageIds: []
}
})),
send: vi.fn(async () => ({ ok: true, replayed: false })),
cancel: vi.fn(async () => ({ ok: true, replayed: false })),
close: vi.fn(async () => undefined),
revealSession: vi.fn(async () => ({
sessionId: SESSION,
workspaceId: 'workspace-1',
agent: 'codex' as const,
readable: true
})),
setSessionTabVisibility: vi.fn(async () => undefined),
respondToPrompt: vi.fn(async () => ({ ok: true, replayed: false })),
setOption: vi.fn(async () => ({ ok: true, replayed: false })),
requestHandoff: vi.fn(async () => ({
ok: true,
replayed: false,
fence: 1,
cursor: { epoch: 'epoch-a', sequence: 0 },
value: {
status: {
owner: 'native',
direction: null,
phase: 'idle',
stage: null,
operationId: null
}
}
})),
supportsCreate: vi.fn(() => true),
handoffStatus: vi.fn(async () => ({ owner: 'native' })),
readOptions: vi.fn(async () => ({
models: [{ id: 'gpt-live', label: 'GPT Live', isDefault: true, efforts: [] }],
current: { model: 'gpt-live' }
})),
history: vi.fn(() => ({ ok: true, page: { items: [] } })),
subscribe: vi.fn(() => () => undefined),
// A real feed, so the snapshot this method hands back is a genuine projection rather
// than a shape the stub restated.
subscribeStatus: vi.fn((subscriber: StructuredAgentSessionStatusSubscriber) =>
statusFeed().subscribe(subscriber)
),
unsubscribe: vi.fn()
}
return hostCalls as unknown as StructuredAgentSessionHost
}
function dispatcher(runtimeOverrides: Record<string, unknown> = {}): RpcDispatcher {
runtimeCalls = {
getStructuredAgentSessionCreateSupport: vi.fn(async () => ({ supported: true })),
resolveStructuredAgentSessionCreateIntent: vi.fn(async (params) => ({
envelope: params.envelope,
location: {
executionHostId: 'local',
wslDistro: null,
workspaceId: 'workspace-1',
workspaceKind: 'git-worktree'
},
provider: params.agent,
agent: params.agent,
accountHome: {
variable: params.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME',
path: params.agent === 'claude' ? '/host/.claude' : '/host/.codex'
},
options:
params.agent === 'claude'
? { model: 'opus', effort: 'high' }
: { model: 'gpt-5.6-sol', effort: 'medium' },
runtimeKind: 'native'
})),
publishStructuredAgentSessionTab: vi.fn()
}
const runtime = {
getRuntimeId: () => 'runtime-1',
registerSubscriptionCleanup: vi.fn(),
cleanupSubscription: vi.fn(),
cleanupSubscriptionsByPrefix: vi.fn(),
...runtimeCalls,
...runtimeOverrides
}
return new RpcDispatcher({
runtime: runtime as unknown as OrcaRuntimeService,
methods: STRUCTURED_AGENT_SESSION_METHODS
})
}
/** The reply path is the only one that carries a client's negotiated identity,
* which is exactly what the capability gate reads. */
async function call(
method: string,
params: unknown,
client?: {
clientId?: string
clientKind?: 'mobile' | 'runtime'
clientCapabilities?: string[]
},
runtimeOverrides: Record<string, unknown> = {}
): Promise<RpcResponse> {
const replies: RpcResponse[] = []
await dispatcher(runtimeOverrides).dispatchStreaming(
request(method, params),
(raw) => replies.push(JSON.parse(raw) as RpcResponse),
client
)
const first = replies[0]
if (!first) {
throw new Error(`no reply for ${method}`)
}
return first
}
const STRUCTURED_CLIENT = {
clientKind: 'runtime' as const,
clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]
}
const STRUCTURED_MOBILE_CLIENT = {
clientKind: 'mobile' as const,
clientCapabilities: [STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY]
}
beforeEach(() => {
setStructuredAgentSessionHost(hostStub())
})
afterEach(() => {
setStructuredAgentSessionHost(null)
})
describe('agentSession.reveal', () => {
it.each(['codex', 'claude'] as const)('republishes a persisted %s chat tab', async (agent) => {
hostCalls.revealSession.mockResolvedValueOnce({
sessionId: SESSION,
workspaceId: 'workspace-1',
agent,
readable: true
})
const response = await call('agentSession.reveal', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(hostCalls.revealSession).toHaveBeenCalledWith(SESSION)
expect(response).toMatchObject({ ok: true, result: { ok: true, agent } })
expect(runtimeCalls.publishStructuredAgentSessionTab).toHaveBeenCalledWith(
expect.objectContaining({ agent, activate: true })
)
})
it('publishes the workspace the host reported, not one the client could assert', async () => {
// The client sends only a session id, so a stale or forged one cannot aim the publish at
// another workspace.
hostCalls.revealSession.mockResolvedValueOnce({
sessionId: SESSION,
workspaceId: 'workspace-from-record',
agent: 'claude',
readable: true
})
await call('agentSession.reveal', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(runtimeCalls.publishStructuredAgentSessionTab).toHaveBeenCalledWith({
workspaceId: 'workspace-from-record',
sessionId: SESSION,
agent: 'claude',
activate: true
})
})
it('publishes the tab even when the journal could not be read', async () => {
// A pre-SQLite chat restores to nothing, but attach still recovers it, so the tab is worth
// publishing and the pane's hold finishes the job. Refusing here would strand it forever.
hostCalls.revealSession.mockResolvedValueOnce({
sessionId: SESSION,
workspaceId: 'workspace-1',
agent: 'codex',
readable: false
})
const response = await call('agentSession.reveal', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: true, result: { ok: true, readable: false } })
expect(runtimeCalls.publishStructuredAgentSessionTab).toHaveBeenCalledOnce()
})
it('refuses rather than throws when the host holds no such record', async () => {
hostCalls.revealSession.mockRejectedValueOnce(new Error('agent_session_identity_required'))
const response = await call('agentSession.reveal', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(response).toMatchObject({
ok: true,
result: { ok: false, refusal: { code: 'agent_session_identity_required' } }
})
expect(runtimeCalls.publishStructuredAgentSessionTab).not.toHaveBeenCalled()
})
it('does not launder an unrelated fault into a refusal', async () => {
hostCalls.revealSession.mockRejectedValueOnce(new Error('EACCES: journal directory'))
const response = await call('agentSession.reveal', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: false })
})
it('is refused for a client that cannot read structured sessions', async () => {
const response = await call(
'agentSession.reveal',
{ sessionId: SESSION },
{ clientKind: 'runtime', clientCapabilities: [] }
)
expect(response).toMatchObject({ ok: false })
expect(hostCalls.revealSession).not.toHaveBeenCalled()
})
})
describe('capability gating', () => {
it('clears durable tab visibility when closing through the agent-session RPC', async () => {
const response = await call('agentSession.close', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: true, result: { ok: true } })
expect(hostCalls.close).toHaveBeenCalledWith(SESSION)
expect(hostCalls.setSessionTabVisibility).toHaveBeenCalledWith(SESSION, false)
expect(hostCalls.setSessionTabVisibility.mock.invocationCallOrder[0]).toBeLessThan(
hostCalls.close.mock.invocationCallOrder[0]!
)
})
it('does not stop the provider when durable tab retirement fails', async () => {
hostCalls.setSessionTabVisibility.mockRejectedValueOnce(new Error('visibility write failed'))
const response = await call('agentSession.close', { sessionId: SESSION }, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: false })
expect(hostCalls.close).not.toHaveBeenCalled()
})
it('advertises the capability without bumping the protocol version', () => {
expect(RUNTIME_CAPABILITIES).toContain(STRUCTURED_AGENT_SESSION_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(STRUCTURED_AGENT_SESSION_HOLD_RUNTIME_CAPABILITY)
expect(RUNTIME_CAPABILITIES).toContain(STRUCTURED_AGENT_SESSION_REVEAL_RUNTIME_CAPABILITY)
// Additive methods do not break an old client; bumping would strand every
// paired device that has not updated.
expect(RUNTIME_PROTOCOL_VERSION).toBe(3)
})
it('registers every structured method on the runtime manifest', () => {
const names = new Set(ALL_RPC_METHODS.map((method) => method.name))
for (const method of STRUCTURED_AGENT_SESSION_METHODS) {
expect(names).toContain(method.name)
}
// Bump deliberately: the whole agentSession.* surface is behind the structured capability,
// so an additive method is invisible to old clients and needs no protocol bump.
expect(STRUCTURED_AGENT_SESSION_METHODS).toHaveLength(19)
})
it('hides the surface from a declared client that did not advertise it', async () => {
const response = await call('agentSession.send', sendParams(), {
clientKind: 'runtime',
clientCapabilities: ['terminal.stream.v1']
})
expect(response).toMatchObject({
ok: false,
error: { message: expect.stringContaining('structured_agent_session_unsupported') }
})
expect(hostCalls.send).not.toHaveBeenCalled()
})
it('rejects create intent before resolving host-owned fields for an old client', async () => {
const worktree = 'id:workspace-1'
const response = await call(
'agentSession.create',
{
envelope: envelope({
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION,
fields: { worktree, agent: 'codex' }
})
}),
worktree,
agent: 'codex'
},
{ clientKind: 'runtime', clientCapabilities: [] }
)
expect(response).toMatchObject({
ok: false,
error: { message: expect.stringContaining('structured_agent_session_unsupported') }
})
expect(runtimeCalls.resolveStructuredAgentSessionCreateIntent).not.toHaveBeenCalled()
})
it('serves a client that advertised it', async () => {
const response = await call('agentSession.send', sendParams(), STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: true })
expect(hostCalls.send).toHaveBeenCalledTimes(1)
})
it('requires the host structured-chat setting for mobile clients', async () => {
const response = await call('agentSession.send', sendParams(), STRUCTURED_MOBILE_CLIENT, {
getClientSettings: () => ({ experimentalStructuredNativeChat: false })
})
expect(response).toMatchObject({
ok: false,
error: { message: expect.stringContaining('structured_agent_session_unsupported') }
})
expect(hostCalls.send).not.toHaveBeenCalled()
})
it('serves mobile clients only after capability and setting negotiation', async () => {
const response = await call('agentSession.send', sendParams(), STRUCTURED_MOBILE_CLIENT, {
getClientSettings: () => ({ experimentalStructuredNativeChat: true })
})
expect(response).toMatchObject({ ok: true })
expect(hostCalls.send).toHaveBeenCalledTimes(1)
})
it('serves an in-process caller, which negotiates no capabilities at all', async () => {
const response = await call('agentSession.send', sendParams())
expect(response).toMatchObject({ ok: true })
})
it('reports the surface as absent when no host is installed', async () => {
setStructuredAgentSessionHost(null)
const response = await call('agentSession.send', sendParams(), STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: false })
})
})
describe('method routing', () => {
it('creates from a client intent while the host resolves paths and provider identity', async () => {
const worktree = 'id:workspace-1'
const params = {
envelope: envelope({
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION,
fields: { worktree, agent: 'codex' }
})
}),
worktree,
agent: 'codex'
}
const created = await call('agentSession.create', params, STRUCTURED_CLIENT)
expect(created).toMatchObject({ ok: true, result: { ok: true } })
expect(runtimeCalls.resolveStructuredAgentSessionCreateIntent).toHaveBeenCalledWith(params)
expect(hostCalls.attach).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' },
options: { model: 'gpt-5.6-sol', effort: 'medium' }
})
)
expect(hostCalls.attach.mock.calls[0]?.[1]).not.toHaveProperty('providerHandle')
expect(runtimeCalls.publishStructuredAgentSessionTab).toHaveBeenCalledWith(
expect.objectContaining({ sessionId: SESSION, activate: true })
)
})
it('routes Claude create support and create through the provider-aware runtime', async () => {
const worktree = 'id:workspace-1'
const support = await call(
'agentSession.createSupport',
{ worktree, agent: 'claude' },
STRUCTURED_CLIENT
)
expect(support).toMatchObject({ ok: true, result: { supported: true } })
expect(runtimeCalls.getStructuredAgentSessionCreateSupport).toHaveBeenCalledWith(
worktree,
'claude'
)
const params = {
envelope: envelope({
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION,
fields: { worktree, agent: 'claude' }
})
}),
worktree,
agent: 'claude'
}
const created = await call('agentSession.create', params, STRUCTURED_CLIENT)
expect(created).toMatchObject({ ok: true, result: { ok: true } })
expect(runtimeCalls.resolveStructuredAgentSessionCreateIntent).toHaveBeenCalledWith(params)
expect(hostCalls.attach).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
accountHome: { variable: 'CLAUDE_CONFIG_DIR', path: '/host/.claude' }
})
)
expect(runtimeCalls.publishStructuredAgentSessionTab).toHaveBeenCalledWith(
expect.objectContaining({
sessionId: SESSION,
activate: true,
agent: 'claude'
})
)
})
it('reports an unknown create outcome when attach commits before tab publication fails', async () => {
const worktree = 'id:workspace-1'
const params = {
envelope: envelope({
expectedRuntimeFence: null,
payloadFingerprint: computeAgentSessionPayloadFingerprint({
method: 'agentSession.create',
sessionId: SESSION,
fields: { worktree, agent: 'codex' }
})
}),
worktree,
agent: 'codex'
}
const response = await call('agentSession.create', params, STRUCTURED_CLIENT, {
publishStructuredAgentSessionTab: vi.fn(async () => {
throw new Error('publish failed')
})
})
expect(hostCalls.attach).toHaveBeenCalledOnce()
expect(response).toMatchObject({
ok: true,
result: {
ok: false,
refusal: { code: 'agent_session_operation_unknown' }
}
})
})
it('separates create from ensure by the fence the client may declare', async () => {
const created = await call('agentSession.create', attachParams())
expect(created).toMatchObject({ ok: true })
const fenced = await call('agentSession.create', attachParams({ envelope: envelope() }))
expect(fenced).toMatchObject({ ok: false })
const ensured = await call('agentSession.ensure', attachParams({ envelope: envelope() }))
expect(ensured).toMatchObject({ ok: true })
})
/** A client-supplied location skips the worktree-resolving support check, so both attach-shaped
* entries must ask the executing host directly or a host that cannot fence a provider child
* would create one anyway. */
it('returns a refusal envelope when create cannot support a client-supplied location', async () => {
hostCalls.supportsCreate.mockReturnValue(false)
const refused = await call('agentSession.create', attachParams())
expect(refused).toMatchObject({
ok: true,
result: {
ok: false,
refusal: { code: 'structured_agent_session_unsupported' }
}
})
expect(hostCalls.attach).not.toHaveBeenCalled()
expect(hostCalls.supportsCreate).toHaveBeenCalledWith(attachParams().location, 'codex')
})
it('keeps ensure failures as top-level errors for an unsupported client location', async () => {
hostCalls.supportsCreate.mockReturnValue(false)
const refused = await call('agentSession.ensure', attachParams())
expect(refused).toMatchObject({
ok: false,
error: { message: expect.stringContaining('structured_agent_session_unsupported') }
})
expect(hostCalls.attach).not.toHaveBeenCalled()
expect(hostCalls.supportsCreate).toHaveBeenCalledWith(attachParams().location, 'codex')
})
it('tags the prompt kind from the method name, not from the client', async () => {
const params = {
envelope: envelope(),
itemId: 'item-1',
expectedRevision: 1,
optionId: 'allow'
}
await call('agentSession.respondToApproval', params, STRUCTURED_CLIENT)
await call('agentSession.respondToQuestion', params, STRUCTURED_CLIENT)
expect(hostCalls.respondToPrompt.mock.calls.map((invocation) => invocation[1].kind)).toEqual([
'approval',
'question'
])
})
it('routes an optional background task id through cancellation', async () => {
const params = {
envelope: envelope(),
turnId: 'background-tasks',
scope: 'background-tasks' as const,
taskId: 'task-2'
}
const response = await call('agentSession.cancel', params, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: true })
expect(hostCalls.cancel).toHaveBeenCalledWith(expect.anything(), params)
})
it('routes the structured handoff mutation through the host', async () => {
const response = await call('agentSession.requestHandoff', {
envelope: envelope(),
direction: 'to-tui',
mode: 'now',
action: 'start'
})
expect(response).toMatchObject({ ok: true })
expect(hostCalls.requestHandoff).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ direction: 'to-tui', mode: 'now', action: 'start' })
)
})
})
describe('parameter validation', () => {
const rejects = async (method: string, params: unknown): Promise<void> => {
const response = await call(method, params, STRUCTURED_CLIENT)
expect(response).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
}
it('rejects an unknown key rather than dropping it', async () => {
await rejects('agentSession.send', { ...sendParams(), replyToItemId: 'item-1' })
await rejects('agentSession.send', {
...sendParams(),
envelope: { ...envelope(), priority: 'high' }
})
})
it('rejects invalid or unscoped background task ids', async () => {
await rejects('agentSession.cancel', {
envelope: envelope(),
turnId: 'background-tasks',
scope: 'background-tasks',
taskId: ' task-2'
})
await rejects('agentSession.cancel', {
envelope: envelope(),
turnId: 'turn-1',
taskId: 'task-2'
})
expect(hostCalls.cancel).not.toHaveBeenCalled()
})
it('refuses to let a client author anything but a user turn', async () => {
await rejects(
'agentSession.send',
sendParams({
body: { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text: 'hi' }] }
})
)
await rejects(
'agentSession.send',
sendParams({
body: { kind: 'message', role: 'user', blocks: [{ type: 'tool-call', name: 'Bash' }] }
})
)
})
it('rejects a journal-only opaque provider handle', async () => {
await rejects(
'agentSession.create',
attachParams({ providerHandle: { kind: 'opaque', agent: 'codex', value: 'thread-1' } })
)
})
it('requires a sha256 fingerprint and a positive fence', async () => {
await rejects(
'agentSession.send',
sendParams({ envelope: envelope({ payloadFingerprint: 'f' }) })
)
await rejects(
'agentSession.send',
sendParams({ envelope: envelope({ payloadFingerprint: 'F'.repeat(64) }) })
)
await rejects(
'agentSession.send',
sendParams({ envelope: envelope({ expectedRuntimeFence: 0 }) })
)
})
it('requires the item revision on a prompt answer', async () => {
await rejects('agentSession.respondToApproval', {
envelope: envelope(),
itemId: 'item-1',
optionId: 'allow'
})
})
it('accepts the maximum fully encoded Claude choice group and retains a finite bound', async () => {
const maximumSelections = Array.from({ length: 4 }, (_, questionIndex) => ({
questionId: `q${questionIndex + 1}`,
optionIds: Array.from(
{ length: 4 },
(_, optionIndex) => `q${questionIndex + 1}:choice-${optionIndex + 1}`
)
}))
const optionId = `question-group:${encodeURIComponent(JSON.stringify(maximumSelections))}`
expect(optionId.length).toBe(610)
const response = await call(
'agentSession.respondToQuestion',
{
envelope: envelope(),
itemId: 'item-1',
expectedRevision: 1,
optionId
},
STRUCTURED_CLIENT
)
expect(response).toMatchObject({ ok: true })
expect(hostCalls.respondToPrompt).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({ optionId })
)
await rejects('agentSession.respondToQuestion', {
envelope: envelope(),
itemId: 'item-1',
expectedRevision: 1,
optionId: 'x'.repeat(1025)
})
})
it('bounds a history page and validates its cursor', async () => {
await rejects('agentSession.history', {
sessionId: SESSION,
direction: 'tail',
limit: 100_000
})
await rejects('agentSession.history', { sessionId: SESSION, direction: 'sideways' })
await rejects('agentSession.history', {
sessionId: SESSION,
direction: 'after',
cursor: { epoch: 'epoch-1', sequence: -1 }
})
})
it('accepts a well-formed history request', async () => {
const response = await call(
'agentSession.history',
{
sessionId: SESSION,
direction: 'after',
cursor: { epoch: 'epoch-1', sequence: 4 },
limit: 40
},
STRUCTURED_CLIENT
)
expect(response).toMatchObject({ ok: true })
})
})
describe('agentSession.subscribeStatus', () => {
it('is invisible to a client without the structured capability', async () => {
const reply = await call('agentSession.subscribeStatus', null, { clientKind: 'runtime' })
expect(reply.ok).toBe(false)
expect(hostCalls.subscribeStatus).not.toHaveBeenCalled()
})
it('opens the host status feed with a projected snapshot as its first reply', async () => {
const reply = await call('agentSession.subscribeStatus', null, STRUCTURED_CLIENT)
expect(reply).toMatchObject({
ok: true,
result: {
type: 'snapshot',
sessions: [
{
sessionId: STATUS_SESSION,
workspaceId: 'workspace-1',
agent: 'codex',
status: 'working',
latestPrompt: 'write a poem',
updatedAt: 2
}
]
}
})
expect(hostCalls.subscribeStatus).toHaveBeenCalledOnce()
})
})