Files
orca/src/main/runtime/structured-worker-terminal-read.test.ts
T

194 lines
8.5 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import type { AgentJournalRenderItem } from '../../shared/agent-session-journal-types'
import type { AgentSessionRecord } from '../../shared/agent-session-record'
const hostRef: { current: unknown } = { current: null }
vi.mock('../native-chat/agent-session-wire/structured-agent-session-registry', () => ({
getStructuredAgentSessionHost: () => hostRef.current
}))
const { readStructuredWorkerTerminal } = await import('./structured-worker-terminal-read')
const { OrcaRuntimeWithResolveTerminalPane } =
await import('./terminal/recovery/resolve-terminal-pane')
const {
mintStructuredWorkerHandle,
mintStructuredWorkerPaneKey,
structuredWorkerIdentities,
structuredWorkerProcessIncarnation
} = await import('./structured-worker-identity')
const SESSION_ID = 'a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d'
function message(id: string, text: string): AgentJournalRenderItem {
return {
itemId: id,
observedAt: 1,
body: { kind: 'message', role: 'assistant', blocks: [{ type: 'text', text }] }
} as unknown as AgentJournalRenderItem
}
function installHost(options: {
items?: readonly AgentJournalRenderItem[] | 'unreadable'
hasOlder?: boolean
lease?: { runtimeKind: string; claimStatus: string }
hasSession?: boolean
}): void {
const lease = options.lease ?? { runtimeKind: 'native', claimStatus: 'live' }
hostRef.current = {
deps: {
store: {
getRecord: (sessionId: string) =>
({
sessionId,
location: { executionHostId: 'local', wslDistro: null },
lease: { ...lease, runtimeFence: 1, deathEvidence: null }
}) as unknown as AgentSessionRecord
}
},
hasSession: () => options.hasSession ?? true,
history: () => {
if (options.items === 'unreadable') {
throw new Error('agent_session_ownership_unknown')
}
return { page: { items: options.items ?? [], hasOlder: options.hasOlder ?? false } }
}
}
}
function registerWorker(): string {
const handle = mintStructuredWorkerHandle()
structuredWorkerIdentities.register({
handle,
sessionId: SESSION_ID,
agent: 'claude',
paneKey: mintStructuredWorkerPaneKey(SESSION_ID),
processIncarnation: structuredWorkerProcessIncarnation(SESSION_ID),
worktreeId: 'wt_1',
hostScope: { kind: 'local', hostId: 'local' }
})
return handle
}
describe('reading a structured worker through the terminal-read path', () => {
beforeEach(() => {
structuredWorkerIdentities.clear()
hostRef.current = null
})
it('serves the journal as terminal lines, with no dispatch and no capability', async () => {
// The defect this pins: a peer has no dispatch id and no coordinator standing, so `worker-read`
// is closed to it, and `terminal read` threw `terminal_handle_stale` for a perfectly live
// worker. A peer could not see a structured agent's recent output at all.
const handle = registerWorker()
installHost({ items: [message('i1', 'first line\nsecond line'), message('i2', 'done')] })
const read = await readStructuredWorkerTerminal({ handle, db: null })
expect(read?.tail).toEqual(['[assistant] first line', 'second line', '[assistant] done'])
expect(read?.status).toBe('running')
expect(read?.truncated).toBe(false)
})
it('honours limit, and claims no cursor space it cannot honour', async () => {
const handle = registerWorker()
installHost({ items: [message('i1', 'a'), message('i2', 'b'), message('i3', 'c')] })
const read = await readStructuredWorkerTerminal({ handle, db: null, limit: 2 })
expect(read?.tail).toEqual(['[assistant] b', '[assistant] c'])
// No index is advertised: the next read re-projects a sliding window, so 0/length would name
// positions that address different lines by then.
expect(read?.nextCursor).toBeNull()
expect(read?.oldestCursor).toBeUndefined()
expect(read?.latestCursor).toBeUndefined()
})
it('refuses a cursor read rather than silently misdelivering lines', async () => {
// The PTY cursor indexes an append-only completed-line buffer with a monotone count. This
// window is a bounded tail re-projected every read, so a saved index addresses different lines
// as the journal grows — and `truncated` could never fire to say so, because it tests
// `cursor < oldestCursor` and `oldestCursor` was always 0. A poller would get wrong or
// duplicated lines with `truncated:false`.
const handle = registerWorker()
installHost({ items: [message('i1', 'a')] })
const refusal = await readStructuredWorkerTerminal({ handle, db: null, cursor: 0 }).then(
() => '',
(error: unknown) => (error instanceof Error ? error.message : String(error))
)
expect(refusal).toMatch(/not line-addressable/)
// Tells the caller what DOES work here. Polling a bounded newest-last tail and diffing fails
// safe — a harmless re-read — where a broken cursor fails unsafe, as a silent hole.
expect(refusal).toMatch(/poll it and diff/)
// And names no paging alternative, because there is none. It must never send a peer to
// `worker-read`: that verb needs a dispatch id and coordinator standing this caller does not
// have, and it is a window index over the same bounded page rather than an append-only anchor.
expect(refusal).not.toContain('worker-read')
})
it('reports dropped history as truncated rather than pretending the page is whole', async () => {
const handle = registerWorker()
installHost({ items: [message('i1', 'tail only')], hasOlder: true })
expect((await readStructuredWorkerTerminal({ handle, db: null }))?.truncated).toBe(true)
})
it('redacts dispatch capability tokens the same way the archive path does', async () => {
const handle = registerWorker()
const token = `dcap_${'a'.repeat(32)}`
installHost({ items: [message('i1', `token is ${token} here`)] })
const tail = (await readStructuredWorkerTerminal({ handle, db: null }))?.tail.join('\n') ?? ''
expect(tail).not.toContain(token)
expect(tail).toContain('[dispatch capability redacted]')
})
it('refuses when the session is not attached rather than answering an empty tail', async () => {
// An empty tail is the claim "this worker has produced no output", which is a different and
// false statement — and the one a caller cannot tell apart from a real silence.
const handle = registerWorker()
installHost({ items: 'unreadable' })
await expect(readStructuredWorkerTerminal({ handle, db: null })).rejects.toThrow(
'agent_session_ownership_unknown'
)
})
it('reports a session it cannot verify as unknown, never as running', async () => {
const handle = registerWorker()
installHost({ items: [message('i1', 'said something')], hasSession: false })
expect((await readStructuredWorkerTerminal({ handle, db: null }))?.status).toBe('unknown')
})
it('is what `terminal read` answers with, ahead of the PTY lookup', async () => {
// The real method through the real prototype, because the wiring IS the fix: the module below
// could be perfect and a peer would still get `terminal_handle_stale` if nothing called it.
const handle = registerWorker()
installHost({ items: [message('i1', 'hello')] })
const runtime: {
readTerminal: (
handle: string,
opts?: { cursor?: number; limit?: number; screen?: boolean }
) => Promise<{ tail: string[] }>
} = Object.assign(Object.create(OrcaRuntimeWithResolveTerminalPane.prototype), {
getOrchestrationDbIfAvailable: () => null,
getLivePtyForHandle: () => {
throw new Error('the PTY lookup must never be reached for a structured worker')
}
})
await expect(runtime.readTerminal(handle)).resolves.toMatchObject({
tail: ['[assistant] hello'],
source: 'stream'
})
// There is no rendered grid to screenshot, and saying so beats inventing one.
await expect(runtime.readTerminal(handle, { screen: true })).resolves.toMatchObject({
source: 'screen-unavailable'
})
})
it('leaves every handle that is not a live structured worker to the PTY path', async () => {
const handle = registerWorker()
installHost({ items: [message('i1', 'x')] })
expect(await readStructuredWorkerTerminal({ handle: 'term_abc', db: null })).toBeNull()
// A terminal owner an older build recorded loads conflicted: not this runtime's worker.
installHost({
items: [message('i1', 'x')],
lease: { runtimeKind: 'native', claimStatus: 'conflicted' }
})
expect(await readStructuredWorkerTerminal({ handle, db: null })).toBeNull()
})
})