mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 10:08:13 +00:00
194 lines
7.7 KiB
TypeScript
194 lines
7.7 KiB
TypeScript
import './rpc/unused-default-rpc-methods.test-fixture'
|
|
// A busy structured chat holds the orchestration pointer as a card in its own queue, sent when the
|
|
// turn ends, as it holds a message the person sends then; the queue does nothing else with it. End
|
|
// to end on the coordinator-mail rig.
|
|
|
|
import { describe, expect, it, vi } from 'vitest'
|
|
import { structuredAgentSessionMessageSendMutation } from '../../shared/structured-agent-session-send-mutation'
|
|
import { operationId, type FakeConnection } from './structured-chat-coordinator-fake-codex-fixture'
|
|
import { idOf } from './rpc/orchestration-session-caller-test-fixture'
|
|
import {
|
|
COORDINATOR,
|
|
WORKER_2_PANE,
|
|
WAIT,
|
|
call,
|
|
coordinatorRunAndTask,
|
|
db,
|
|
finishWorker,
|
|
host,
|
|
observationClock,
|
|
openChat,
|
|
ptyPointer,
|
|
queuedCardTexts,
|
|
runtime,
|
|
sendUserMessage,
|
|
settleTurn,
|
|
turnText
|
|
} from './structured-chat-coordinator-mail-rig.test-fixture'
|
|
|
|
/** The person's turn, started and still running; resolves to its end. */
|
|
async function runningUserTurn(chat: FakeConnection): Promise<() => Promise<void>> {
|
|
expect(await sendUserMessage(COORDINATOR, 'go')).toMatchObject({ ok: true })
|
|
await vi.waitFor(() => expect(chat.turns).toHaveLength(1), WAIT)
|
|
const notify = (method: string, params: unknown) => chat.handlers.onNotification?.(method, params)
|
|
notify('turn/started', { turn: { id: 'turn-1' } })
|
|
notify('item/completed', {
|
|
item: {
|
|
type: 'userMessage',
|
|
id: 'echo-go',
|
|
clientId: chat.turns[0]!.clientUserMessageId,
|
|
content: [{ type: 'text', text: 'go' }]
|
|
}
|
|
})
|
|
await host.flushStreamedEvents(COORDINATOR)
|
|
return async () => {
|
|
notify('turn/completed', { turn: { id: 'turn-1' } })
|
|
await host.flushStreamedEvents(COORDINATOR)
|
|
}
|
|
}
|
|
|
|
/** Idle edges with nothing owed: whatever they would send gets the time to show. */
|
|
async function idleEdgesSettled(): Promise<void> {
|
|
for (let edge = 0; edge < 3; edge += 1) {
|
|
runtime.onStructuredSessionStatusForMail({ sessionId: COORDINATOR, status: 'idle' })
|
|
await observationClock.observe(100)
|
|
}
|
|
}
|
|
|
|
/** The chat's queue as its journal stores it. */
|
|
function queuedRows() {
|
|
return host.collaboratorsForTests().sessions.get(COORDINATOR)?.journal.queuedMessages.list() ?? []
|
|
}
|
|
|
|
/** A second task, for a second worker result. */
|
|
async function secondTask(): Promise<string> {
|
|
return idOf(
|
|
(await call('orchestration.taskCreate', { spec: 'more' }, { sessionId: COORDINATOR })).task
|
|
)
|
|
}
|
|
|
|
describe("a busy chat's orchestration pointer waits in its queue", () => {
|
|
it('queues the pointer as a card, with who it is from, and sends it once when the turn ends', async () => {
|
|
observationClock.start()
|
|
const chat = await openChat(COORDINATOR)
|
|
const { runId, taskId } = await coordinatorRunAndTask()
|
|
const endTurn = await runningUserTurn(chat)
|
|
const dispatchId = await finishWorker(taskId)
|
|
// The report was accepted, so its dispatch settled before its mail was named.
|
|
expect(db.getDispatchContextById(dispatchId)?.status).toBe('completed')
|
|
await vi.waitFor(
|
|
async () => expect(await queuedCardTexts()).toEqual([ptyPointer(`run:${runId}`)]),
|
|
WAIT
|
|
)
|
|
expect(chat.turns).toHaveLength(1)
|
|
const [card] = queuedRows()
|
|
const [mail] = db.getAllMessages(`run:${runId}`)
|
|
const from = {
|
|
kind: 'agent',
|
|
senders: [
|
|
{
|
|
party: { address: 'term_worker', terminalHandle: 'term_worker', orcaSessionId: null },
|
|
// Named by the task of the dispatch it just finished, through the real runtime's naming.
|
|
name: 'build it'
|
|
}
|
|
],
|
|
orchestration: {
|
|
message: 'mail-notice',
|
|
mailbox: `run:${runId}`,
|
|
dispatchId: null,
|
|
messages: [{ messageId: mail!.id, runId, from: 'term_worker' }]
|
|
}
|
|
}
|
|
// On the card's body: the turn the queue sends carries it, and the provider never sees it.
|
|
expect(card?.body.from).toEqual(from)
|
|
|
|
await endTurn()
|
|
await vi.waitFor(() => expect(chat.turns).toHaveLength(2), WAIT)
|
|
expect(turnText(chat.turns[1]!)).toBe(ptyPointer(`run:${runId}`))
|
|
expect(JSON.stringify(chat.turns[1])).not.toContain('term_worker')
|
|
const sent = (await host.journalSnapshot(COORDINATOR)).items.filter(
|
|
(item) => item.body.kind === 'message' && item.body.from
|
|
)
|
|
expect(sent.map((item) => item.body.kind === 'message' && item.body.from)).toEqual([from])
|
|
expect(await queuedCardTexts()).toEqual([])
|
|
await settleTurn(COORDINATOR, 1)
|
|
await idleEdgesSettled()
|
|
expect(chat.turns).toHaveLength(2)
|
|
})
|
|
|
|
it('queues a second card for mail that arrives while the first waits, each counting its own mail', async () => {
|
|
observationClock.start()
|
|
const chat = await openChat(COORDINATOR)
|
|
const { runId, taskId } = await coordinatorRunAndTask()
|
|
const second = await secondTask()
|
|
const endTurn = await runningUserTurn(chat)
|
|
await finishWorker(taskId)
|
|
await vi.waitFor(async () => expect(await queuedCardTexts()).toHaveLength(1), WAIT)
|
|
await finishWorker(second, { handle: 'term_worker_2', paneKey: WORKER_2_PANE })
|
|
const pointer = ptyPointer(`run:${runId}`)
|
|
await vi.waitFor(async () => expect(await queuedCardTexts()).toEqual([pointer, pointer]), WAIT)
|
|
|
|
await endTurn()
|
|
await vi.waitFor(() => expect(chat.turns).toHaveLength(2), WAIT)
|
|
await settleTurn(COORDINATOR, 1)
|
|
await vi.waitFor(() => expect(chat.turns).toHaveLength(3), WAIT)
|
|
expect(turnText(chat.turns[2]!)).toBe(pointer)
|
|
await settleTurn(COORDINATOR, 2)
|
|
await idleEdgesSettled()
|
|
expect(chat.turns).toHaveLength(3)
|
|
expect(await queuedCardTexts()).toEqual([])
|
|
})
|
|
|
|
it("leaves the chat's own `check` as it is: the mail stays readable, and the card stays", async () => {
|
|
const chat = await openChat(COORDINATOR)
|
|
const { runId, taskId } = await coordinatorRunAndTask()
|
|
const endTurn = await runningUserTurn(chat)
|
|
await finishWorker(taskId)
|
|
await vi.waitFor(async () => expect(await queuedCardTexts()).toHaveLength(1), WAIT)
|
|
const [mail] = db.getAllMessages(`run:${runId}`)
|
|
expect(await call('orchestration.check', {}, { sessionId: COORDINATOR })).toMatchObject({
|
|
count: 1,
|
|
messages: [{ id: mail!.id }]
|
|
})
|
|
expect(await queuedCardTexts()).toEqual([ptyPointer(`run:${runId}`)])
|
|
await endTurn()
|
|
})
|
|
|
|
// The queue once refused every send past twenty cards, so a busy coordinator's mail locked the
|
|
// person out of their own chat.
|
|
it("still queues the person's message behind more than twenty mail cards", async () => {
|
|
const chat = await openChat(COORDINATOR)
|
|
const { runId, taskId } = await coordinatorRunAndTask()
|
|
await runningUserTurn(chat)
|
|
await finishWorker(taskId)
|
|
await vi.waitFor(async () => expect(await queuedCardTexts()).toHaveLength(1), WAIT)
|
|
for (let index = 1; index <= 24; index += 1) {
|
|
await call('orchestration.send', {
|
|
from: 'term_worker',
|
|
to: `run:${runId}`,
|
|
subject: `mail ${index}`
|
|
})
|
|
await vi.waitFor(async () => expect(await queuedCardTexts()).toHaveLength(index + 1), WAIT)
|
|
}
|
|
const sent = await host.send(
|
|
{ callerKey: 'test-surface' },
|
|
{
|
|
...structuredAgentSessionMessageSendMutation({
|
|
sessionId: COORDINATOR,
|
|
clientOperationId: operationId(),
|
|
expectedRuntimeFence: host.deps.store.getRecord(COORDINATOR)!.lease.runtimeFence,
|
|
body: {
|
|
kind: 'message',
|
|
role: 'user',
|
|
blocks: [{ type: 'text', text: 'human message' }]
|
|
},
|
|
delivery: 'queue-if-active'
|
|
}),
|
|
userSend: true
|
|
}
|
|
)
|
|
expect(sent).toMatchObject({ ok: true, value: { queued: { position: 26, state: 'waiting' } } })
|
|
expect((await queuedCardTexts()).at(-1)).toBe('human message')
|
|
})
|
|
})
|