Files
orca/src/shared/native-chat-transcript-projection.test.ts
T
Brennan Benson 028f609b5a Keep chat thinking before its answer across history loads (#33628)
* fix(chat): preserve transcript source order across history windows

* test(chat): use template literal for mobile transcript fixture

* test(chat): place composed transcript contracts across project boundaries

* test(chat): move reader-to-phone contract across project boundaries

* test(chat): reuse transcript input setup and caller contracts

* test(chat): align mobile recordings with source-order capability

* test(chat): scope phone transforms and await history snapshot
2026-10-26 18:45:21 -07:00

180 lines
6.5 KiB
TypeScript

import './native-chat-reasoning-order.spec'
import { describe, expect, it } from 'vitest'
import type {
AgentJournalItemBody,
AgentJournalRenderItem,
AgentJournalTurnScope
} from './agent-session-journal-types'
import type { NativeChatMessage } from './native-chat-types'
import {
projectNativeChatTranscript,
projectNativeChatTranscriptMessages
} from './native-chat-transcript-projection'
let sequence = 0
function row(
id: string,
blocks: NativeChatMessage['blocks'],
overrides: Partial<NativeChatMessage> = {}
): NativeChatMessage {
sequence += 1
return {
id,
role: 'assistant',
blocks,
timestamp: 1,
source: 'transcript',
journalPosition: { sequence, index: 0 },
...overrides
}
}
const say = (value: string) => [{ type: 'text' as const, text: value }]
const call = (name: string) => [{ type: 'tool-call' as const, name, input: {} }]
const child = { agentId: 'task-1', producerKind: 'agent' as const }
/** A journal item as a host that states each row's turn writes it; `turnItemId` absent: none. */
function item(
itemId: string,
body: AgentJournalItemBody,
turnItemId?: string,
agentId?: string
): AgentJournalRenderItem {
const turnScope: AgentJournalTurnScope =
turnItemId === undefined ? { kind: 'thread' } : { kind: 'turn', turnItemId }
return {
itemId,
body,
sequence: 0,
observedAt: 0,
revision: 1,
turnScope,
...(agentId === undefined ? {} : { agentId })
}
}
const message = (role: 'user' | 'assistant'): AgentJournalItemBody => ({
kind: 'message',
role,
blocks: []
})
const turn = (turnId: string, userItemId: string): AgentJournalItemBody => ({
kind: 'turn',
turnId,
state: 'completed',
userItemId,
startedAt: 0
})
describe("a subagent's rows are not the conversation's", () => {
// The shape a background subagent leaves: its rows interleave with its parent's.
const transcript = [
row('ask', say('review the PR'), { role: 'user' }),
row('delegate', say('Delegating the review.')),
row('child-look', say('Looking at the diff.'), child),
row('parent-read', call('Read')),
row('child-grep', call('Grep'), child),
row('child-verdict', say('The PR is CLEAN.'), child),
row('answer', say('The review found nothing.'))
]
it("keeps the session's own rows as the conversation, and none of the subagent's", () => {
const { conversation, subagentRows } = projectNativeChatTranscript(transcript)
expect(conversation.map((message) => message.id)).toEqual(['ask', 'delegate', 'answer'])
expect(conversation[1]?.blocks).toEqual([...say('Delegating the review.'), ...call('Read')])
expect(
subagentRows.get('task-1')?.map(({ message, turnKey }) => [message.id, turnKey])
).toEqual([
['child-look', 'ask'],
['child-verdict', 'ask']
])
expect(projectNativeChatTranscriptMessages(transcript)).toEqual(conversation)
})
it("folds the subagent's calls into its own run however its parent's interleave", () => {
const [look] = projectNativeChatTranscript(transcript).subagentRows.get('task-1') ?? []
expect(look?.message.blocks).toEqual([...say('Looking at the diff.'), ...call('Grep')])
})
it('splits a subagent run at the turn it crossed, so each call stays in its own turn', () => {
const rows = projectNativeChatTranscript([
row('first', say('go'), { role: 'user' }),
row('child-start', say('Starting.'), child),
row('second', say('and then'), { role: 'user' }),
row('child-edit', call('Edit'), child)
]).subagentRows.get('task-1')
expect(rows?.map(({ message, turnKey }) => [message.id, turnKey])).toEqual([
['child-start', 'first'],
['child-edit', 'second']
])
})
// Turn records own rows; a mid-turn send cannot claim work before its own turn opens.
it("takes each row's turn from the journal's turn records, as the conversation's rows do", () => {
const rows = [
row('first', say('go'), { role: 'user' }),
row('child-start', say('Starting.'), child),
row('second', say('and then'), { role: 'user' }),
row('child-edit', call('Edit'), child),
row('child-woke', call('Write'), child)
]
const journal = {
items: [
item('first', message('user')),
item('turn-1', turn('1', 'first')),
item('child-start', message('assistant'), 'turn-1', 'task-1'),
item('second', message('user')),
item('child-edit', message('assistant'), 'turn-1', 'task-1'),
item('turn-2', turn('2', 'second')),
// Its opener is outside the loaded window, so the turn keys to its own record.
item('turn-3', turn('3', 'older-send')),
item('child-woke', message('assistant'), 'turn-3', 'task-1')
],
submissions: []
}
const projected = projectNativeChatTranscript(rows, undefined, journal).subagentRows.get(
'task-1'
)
expect(projected?.map(({ message, turnKey }) => [message.id, turnKey])).toEqual([
['child-start', 'first'],
['child-woke', 'turn-3']
])
expect(projected?.[0]?.message.blocks).toEqual([...say('Starting.'), ...call('Edit')])
})
it("never lets a subagent's own prompt open a conversation turn", () => {
const transcript = [
row('ask', say('go'), { role: 'user' }),
row('child-prompt', say('Review the diff.'), { ...child, role: 'user' }),
row('child-look', say('Looking.'), child)
]
const rows = projectNativeChatTranscript(transcript).subagentRows.get('task-1')
expect(rows?.every(({ turnKey }) => turnKey === 'ask')).toBe(true)
// A host that states turns scopes a prompt written after the turn ended to none.
const journal = {
items: [
item('ask', message('user')),
item('turn-1', turn('1', 'ask')),
item('child-prompt', message('user'), undefined, 'task-1'),
item('child-look', message('assistant'), 'turn-1', 'task-1')
],
submissions: []
}
const scoped = projectNativeChatTranscript(transcript, undefined, journal).subagentRows
expect(scoped.get('task-1')?.map(({ turnKey }) => turnKey)).toEqual([undefined, 'ask'])
})
it('projects a transcript that names no producer exactly as before', () => {
const plain = transcript.map(({ agentId: _agentId, producerKind: _kind, ...rest }) => rest)
const { conversation, subagentRows } = projectNativeChatTranscript(plain)
expect(subagentRows.size).toBe(0)
expect(conversation.map((message) => message.id)).toEqual([
'ask',
'delegate',
'child-look',
'child-verdict',
'answer'
])
})
})