Files
orca/src/shared/structured-agent-question-projection.test.ts
T
Neil 89f9ce08de refactor(shared): organize session contracts and projections (#36979)
* refactor(shared): organize session contracts and projections

* chore: tighten dropped-path flaky pattern allowance

* docs: preserve current agent instructions with valid formatting
2026-11-01 03:14:46 -08:00

191 lines
7.0 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import type { AgentJournalRenderItem } from './agent-session/journal/agent-session-journal-types'
import { projectStructuredItemToNativeChat } from './agent-session/view/structured-agent-session-projection'
import {
projectStructuredQuestionMessages,
structuredQuestionTranscript
} from './structured-agent-question-projection'
function projectQuestion(item: AgentJournalRenderItem) {
return projectStructuredQuestionMessages([item])[0]
}
const PENDING = {
state: 'pending',
selectedOptionId: null,
resolvedBy: null,
resolvedAt: null
} as const
function item(itemId: string, body: AgentJournalRenderItem['body']): AgentJournalRenderItem {
return { itemId, sequence: 1, revision: 1, observedAt: 1, body }
}
describe('structured agent session ask-row projection', () => {
it('suppresses only a matching question tool and preserves failed or unmatched calls', () => {
const call = item('ask', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: { questions: [{ question: 'Which branch?' }] },
state: 'running'
})
const question = item('q', {
kind: 'question',
question: 'Which branch?',
options: [],
resolution: { ...PENDING }
})
const turn = item('turn', { kind: 'turn', turnId: 'turn', state: 'running' })
expect(projectStructuredQuestionMessages([call])).toHaveLength(1)
expect(projectStructuredQuestionMessages([turn, call, question]).map((row) => row.id)).toEqual([
'q'
])
expect(projectStructuredQuestionMessages([call, question])).toHaveLength(2)
const failed = item('failed', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: call.body.kind === 'tool-call' ? call.body.input : null,
state: 'failed',
output: { head: 'Denied', byteLength: 6, truncated: false, digest: 'a' }
})
expect(
projectStructuredQuestionMessages([turn, failed, question]).map((row) => row.id)
).toEqual(['failed', 'q'])
const user = item('user', {
kind: 'message',
role: 'user',
blocks: [{ type: 'text', text: 'Next turn' }]
})
expect(projectStructuredQuestionMessages([call, user, question])).toHaveLength(3)
const nextTurn = item('next-turn', { kind: 'turn', turnId: 'next-turn', state: 'running' })
expect(projectStructuredQuestionMessages([turn, call, nextTurn, question])).toHaveLength(2)
})
it('suppresses only as many duplicate calls as question items', () => {
const turn = item('turn', { kind: 'turn', turnId: 'turn', state: 'running' })
const firstCall = item('ask-1', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: { questions: [{ question: 'Which branch?' }] },
state: 'running'
})
const secondCall = item('ask-2', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: { questions: [{ question: 'Which branch?' }] },
state: 'running'
})
const question = item('q', {
kind: 'question',
question: 'Which branch?',
options: [],
resolution: { ...PENDING }
})
expect(projectStructuredQuestionMessages([turn, firstCall, secondCall, question])).toEqual([
expect.objectContaining({ id: 'ask-2' }),
expect.objectContaining({ id: 'q' })
])
})
it('folds a settled matching tool call into its resolved question receipt', () => {
const turn = item('turn', { kind: 'turn', turnId: 'turn', state: 'completed' })
const firstCall = item('ask-1', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: { questions: [{ question: 'Which branch?' }] },
state: 'completed',
output: { head: 'main', byteLength: 4, truncated: false, digest: 'a' }
})
const secondCall = item('ask-2', {
kind: 'tool-call',
name: 'AskUserQuestion',
input: { questions: [{ question: 'Which branch?' }] },
state: 'completed',
output: { head: 'main', byteLength: 4, truncated: false, digest: 'b' }
})
const question = item('q', {
kind: 'question',
question: 'Which branch?',
options: [{ id: 'main', label: 'main' }],
resolution: { ...PENDING, state: 'resolved', selectedOptionId: 'main' }
})
expect(
projectStructuredQuestionMessages([turn, firstCall, secondCall, question]).map(
(row) => row.id
)
).toEqual(['ask-2', 'q'])
})
it('counts adjacent pending questions and preserves each independently settled answer', () => {
const first = item('q1', {
kind: 'question',
question: 'Branch?',
options: [],
resolution: { ...PENDING }
})
const second = item('q2', {
kind: 'question',
question: 'Proceed?',
options: [],
resolution: { ...PENDING }
})
const pending = structuredQuestionTranscript([first, second])
const unrelated = item('status', { kind: 'status', text: 'Background work' })
const refreshed = structuredQuestionTranscript([first, second, unrelated])
expect(refreshed.messages[0]).toBe(pending.messages[0])
expect(refreshed.receipts.get('q1')).toBe(pending.receipts.get('q1'))
expect(pending.messages.map((row) => row.id)).toEqual(['q1'])
expect(pending.receipts.get('q1')).toMatchObject({
questions: [{ question: 'Branch?' }, { question: 'Proceed?' }]
})
const resolved = item('q1', {
kind: 'question',
question: 'Branch?',
options: [{ id: 'main', label: 'main' }],
resolution: { ...PENDING, state: 'resolved', selectedOptionId: 'main' }
})
const partial = structuredQuestionTranscript([resolved, second])
expect(partial.messages.map((row) => row.id)).toEqual(['q1', 'q2'])
expect(partial.receipts.get('q1')).toBe(resolved.body)
expect(partial.receipts.get('q2')).toBe(second.body)
})
it('keeps host projection unchanged and question revisions authoritative', () => {
const pending = item('q', {
kind: 'question',
question: 'Proceed?',
options: [],
resolution: { ...PENDING }
})
expect(projectStructuredItemToNativeChat(pending)).toBeNull()
const initial = projectStructuredQuestionMessages([pending])[0]
expect(projectStructuredQuestionMessages([pending])[0]).toBe(initial)
const resolved = item('q', {
kind: 'question',
question: 'Proceed?',
options: [],
resolution: { ...PENDING, state: 'resolved' }
})
expect(projectStructuredQuestionMessages([resolved])[0]).toMatchObject({ role: 'system' })
expect(projectStructuredQuestionMessages([resolved])[0]).not.toBe(initial)
})
})
describe("a subagent's pending question keeps its producer", () => {
it('carries the linkage onto the row that stands in for the question', () => {
const projected = projectQuestion({
...item('child-q', {
kind: 'question',
question: 'Which branch?',
options: [{ id: 'q1:main', label: 'main' }],
resolution: { ...PENDING }
}),
agentId: 'task-1',
producerKind: 'agent'
})
expect(projected).toMatchObject({ role: 'system', agentId: 'task-1' })
})
})