mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 08:01:56 +00:00
* feat(chat): support structured clear and compact commands * fix(chat): authorize mobile commands and bound clear-chain projection * fix(chat): localize conversation command send errors * fix(chat): retain clear pane identity with reopened history * test: account for combined structured session RPC additions --------- Co-authored-by: Merge Sim <sim@local>
96 lines
3.5 KiB
TypeScript
96 lines
3.5 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import type { RpcClient } from '../transport/rpc-client'
|
|
import { dispatchMobileStructuredCommand } from './mobile-structured-composer-command'
|
|
|
|
function setup() {
|
|
const sendRequest = vi.fn(async (_method: string, _params: unknown, _options: unknown) => ({
|
|
ok: true,
|
|
result: { ok: true, value: { command: 'compact', state: 'completed' } }
|
|
}))
|
|
const input: Parameters<typeof dispatchMobileStructuredCommand>[0] = {
|
|
text: '/compact',
|
|
hasAttachments: false,
|
|
client: { sendRequest } as unknown as RpcClient,
|
|
sessionId: 'session',
|
|
fence: 1,
|
|
sessionKey: 'session:1',
|
|
pending: { current: false },
|
|
operationIds: new Map(),
|
|
controller: {
|
|
agent: 'codex',
|
|
snapshot: [],
|
|
invokeAction: vi.fn(async () => true),
|
|
setOption: vi.fn(async () => true),
|
|
conversationCommands: ['clear', 'compact']
|
|
},
|
|
canRun: () => true,
|
|
onError: vi.fn(),
|
|
timeoutMs: 15000
|
|
}
|
|
return { input, sendRequest }
|
|
}
|
|
describe('mobile structured conversation commands', () => {
|
|
it.each(['/clear', '/compact'])(
|
|
'uses the command RPC for %s without an ordinary send',
|
|
async (text) => {
|
|
const { input, sendRequest } = setup()
|
|
expect(await dispatchMobileStructuredCommand({ ...input, text })).toBe('accepted')
|
|
expect(sendRequest).toHaveBeenCalledWith(
|
|
'agentSession.conversationCommand',
|
|
expect.objectContaining({ command: text.slice(1) }),
|
|
expect.anything()
|
|
)
|
|
expect(input.operationIds.size).toBe(0)
|
|
}
|
|
)
|
|
it('retains the exact operation ID after an unknown response', async () => {
|
|
const { input, sendRequest } = setup()
|
|
sendRequest.mockResolvedValueOnce({
|
|
ok: true,
|
|
result: { ok: true, value: { command: 'compact', state: 'unknown' } }
|
|
})
|
|
expect(await dispatchMobileStructuredCommand(input)).toBe('unknown')
|
|
expect(await dispatchMobileStructuredCommand(input)).toBe('accepted')
|
|
expect(sendRequest.mock.calls[0]?.[1]).toEqual(sendRequest.mock.calls[1]?.[1])
|
|
})
|
|
it('retains operation identity when the host explicitly reports an unknown ledger outcome', async () => {
|
|
const { input, sendRequest } = setup()
|
|
sendRequest.mockResolvedValueOnce({
|
|
ok: true,
|
|
result: {
|
|
ok: false,
|
|
refusal: { code: 'agent_session_operation_unknown', message: 'unconfirmed' }
|
|
}
|
|
} as never)
|
|
expect(await dispatchMobileStructuredCommand(input)).toBe('unknown')
|
|
expect(await dispatchMobileStructuredCommand(input)).toBe('accepted')
|
|
expect(sendRequest.mock.calls[0]?.[1]).toEqual(sendRequest.mock.calls[1]?.[1])
|
|
})
|
|
it.each(['attachments', 'old host', 'arguments', 'pending work'])(
|
|
'guards %s without provider dispatch',
|
|
async (reason) => {
|
|
const { input, sendRequest } = setup()
|
|
if (reason === 'attachments') {
|
|
input.hasAttachments = true
|
|
}
|
|
if (reason === 'old host') {
|
|
input.controller.conversationCommands = undefined
|
|
}
|
|
if (reason === 'arguments') {
|
|
input.text = '/compact instructions'
|
|
}
|
|
if (reason === 'pending work') {
|
|
input.canRun = () => false
|
|
}
|
|
expect(await dispatchMobileStructuredCommand(input)).toBe('rejected')
|
|
expect(sendRequest).not.toHaveBeenCalled()
|
|
expect(input.onError).toHaveBeenCalled()
|
|
}
|
|
)
|
|
it('keeps ordinary messages on the existing send path', async () => {
|
|
const { input, sendRequest } = setup()
|
|
expect(await dispatchMobileStructuredCommand({ ...input, text: 'hello' })).toBeNull()
|
|
expect(sendRequest).not.toHaveBeenCalled()
|
|
})
|
|
})
|