mirror of
https://github.com/stablyai/orca.git
synced 2026-11-01 01:27:35 +00:00
test: share native chat render cost setup (#29316)
* test: batch lint fixtures and remove repeated runtime scan * test(cli): consolidate owner coverage and remove false decoding proof * test(renderer): retain IPC lifecycle behavior without copied inventories * test: share floating panel behavior suite startup * test: retire synthetic runtime constructor probes * test: share saved terminal title runtime setup * test: share native chat render cost setup --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
This commit is contained in:
+116
-1
@@ -3,7 +3,7 @@
|
||||
import '@testing-library/jest-dom/vitest'
|
||||
|
||||
import { cleanup, fireEvent, render, screen } from '@testing-library/react'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Why: these fixtures have neither inline visual owners nor sender navigation.
|
||||
const unusedChatLeaves = vi.hoisted(() => ({
|
||||
@@ -24,6 +24,7 @@ afterEach(() => {
|
||||
expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled()
|
||||
expect(unusedChatLeaves.openSender).not.toHaveBeenCalled()
|
||||
})
|
||||
import type * as NativeChatWorkRunModule from '../../../../shared/native-chat-work-run'
|
||||
import type * as NativeChatProseModule from '../../../../shared/native-chat-prose'
|
||||
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import { projectStructuredAgentSessionMessages } from '../../../../shared/structured-agent-session-message-projection'
|
||||
@@ -62,6 +63,20 @@ vi.mock('../../../../shared/native-chat-unified-patch', async (importOriginal) =
|
||||
}
|
||||
})
|
||||
|
||||
// Counts each run rebuilding its lines and thoughts: a rebuilt transcript hands every run a fresh
|
||||
// member list, and only the row's memo keeps a settled run from redoing that work every frame.
|
||||
const entriesCalls = vi.hoisted(() => ({ count: 0 }))
|
||||
vi.mock('../../../../shared/native-chat-work-run', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NativeChatWorkRunModule>()
|
||||
return {
|
||||
...actual,
|
||||
nativeChatWorkRunEntries: (...args: Parameters<typeof actual.nativeChatWorkRunEntries>) => {
|
||||
entriesCalls.count += 1
|
||||
return actual.nativeChatWorkRunEntries(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { NativeChatMessageList } = await import('./NativeChatMessageList')
|
||||
|
||||
let restoreViewport = (): void => {}
|
||||
@@ -70,6 +85,12 @@ beforeAll(() => {
|
||||
})
|
||||
afterAll(() => restoreViewport())
|
||||
afterEach(cleanup)
|
||||
beforeEach(() => {
|
||||
proseCalls.count = 0
|
||||
patchCalls.detailed = 0
|
||||
patchCalls.summary = 0
|
||||
entriesCalls.count = 0
|
||||
})
|
||||
|
||||
const TRANSCRIPT_LENGTH = 120
|
||||
|
||||
@@ -201,3 +222,97 @@ describe('native chat transcript re-render cost during a streaming turn', () =>
|
||||
expect(screen.getByText('new')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
|
||||
let clock = 0
|
||||
function row(
|
||||
id: string,
|
||||
role: NativeChatMessage['role'],
|
||||
blocks: NativeChatMessage['blocks'],
|
||||
extra: Partial<NativeChatMessage> = {}
|
||||
): NativeChatMessage {
|
||||
clock += 1
|
||||
return { id, role, blocks, timestamp: clock, source: 'transcript', ...extra }
|
||||
}
|
||||
const thought = (id: string) =>
|
||||
row(id, 'reasoning', [{ type: 'text', text: `thinking ${id}` }], {
|
||||
state: 'completed',
|
||||
completedAt: clock + 1
|
||||
})
|
||||
const command = (id: string, output: string) =>
|
||||
row(id, 'assistant', [
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: id }, state: 'completed', callId: id },
|
||||
{ type: 'tool-result', output, callId: id }
|
||||
])
|
||||
|
||||
const prompt = row('user-1', 'user', [{ type: 'text', text: 'go' }])
|
||||
// Three settled runs, each closed by a reply, then the live run.
|
||||
const settled = [1, 2, 3].flatMap((turn) => [
|
||||
thought(`r${turn}a`),
|
||||
command(`c${turn}a`, 'ok'),
|
||||
thought(`r${turn}b`),
|
||||
command(`c${turn}b`, 'ok'),
|
||||
row(`reply-${turn}`, 'assistant', [{ type: 'text', text: `Step ${turn} done.` }])
|
||||
])
|
||||
const liveHead = [thought('r4'), command('c4', 'ok')]
|
||||
|
||||
function journal(rows: readonly NativeChatMessage[]): AgentJournalRenderItem[] {
|
||||
return [
|
||||
{
|
||||
itemId: prompt.id,
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 1,
|
||||
body: { kind: 'message', role: 'user', blocks: prompt.blocks }
|
||||
},
|
||||
{
|
||||
itemId: 'turn-1',
|
||||
revision: 1,
|
||||
sequence: 2,
|
||||
observedAt: 2,
|
||||
body: { kind: 'turn', turnId: 'turn-1', state: 'running', userItemId: prompt.id }
|
||||
},
|
||||
...rows.map((message, index) => ({
|
||||
itemId: message.id,
|
||||
revision: 1,
|
||||
sequence: index + 3,
|
||||
observedAt: index + 3,
|
||||
body: { kind: 'message' as const, role: message.role, blocks: message.blocks }
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
function list(tail: NativeChatMessage): React.JSX.Element {
|
||||
const rows = [...settled, ...liveHead, tail]
|
||||
const session: NativeChatLiveSession = {
|
||||
messages: [prompt, ...rows],
|
||||
status: 'working',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
hasMore: false,
|
||||
loadingEarlier: false,
|
||||
olderHistoryGeneration: 0,
|
||||
loadEarlier: vi.fn(),
|
||||
readPhase: 'ready'
|
||||
}
|
||||
return (
|
||||
<NativeChatMessageList
|
||||
session={session}
|
||||
journalItems={journal(rows)}
|
||||
isWorking
|
||||
expandSignal={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('settled work runs while the live run streams', () => {
|
||||
it('rebuilds only the live run on each frame', () => {
|
||||
const tail = (frame: number) => ({ ...command('c5', `line ${frame}`), id: 'c5' })
|
||||
const { rerender, container } = render(list(tail(0)))
|
||||
expect(container.querySelectorAll('[data-native-chat-tool-run-state]')).toHaveLength(4)
|
||||
entriesCalls.count = 0
|
||||
for (let frame = 1; frame <= 5; frame += 1) {
|
||||
rerender(list(tail(frame)))
|
||||
}
|
||||
expect(entriesCalls.count).toBe(5)
|
||||
})
|
||||
})
|
||||
@@ -1,146 +0,0 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { cleanup, render } from '@testing-library/react'
|
||||
import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
|
||||
// Why: these fixtures have neither inline visual owners nor sender navigation.
|
||||
const unusedChatLeaves = vi.hoisted(() => ({
|
||||
inlineVisual: vi.fn(() => {
|
||||
throw new Error('Unexpected inline visual in transcript fixture')
|
||||
}),
|
||||
openSender: vi.fn(() => {
|
||||
throw new Error('Unexpected sender navigation in transcript fixture')
|
||||
})
|
||||
}))
|
||||
vi.mock('./NativeChatInlineVisual', () => ({
|
||||
NativeChatInlineVisual: unusedChatLeaves.inlineVisual
|
||||
}))
|
||||
vi.mock('@/lib/open-agent-message-sender', () => ({
|
||||
openAgentMessageSender: unusedChatLeaves.openSender
|
||||
}))
|
||||
afterEach(() => {
|
||||
expect(unusedChatLeaves.inlineVisual).not.toHaveBeenCalled()
|
||||
expect(unusedChatLeaves.openSender).not.toHaveBeenCalled()
|
||||
})
|
||||
import type * as NativeChatWorkRunModule from '../../../../shared/native-chat-work-run'
|
||||
import type { AgentJournalRenderItem } from '../../../../shared/agent-session-journal-types'
|
||||
import type { NativeChatMessage } from '../../../../shared/native-chat-types'
|
||||
import type { NativeChatLiveSession } from './use-native-chat-live-session'
|
||||
import { installNativeChatMessageListTestViewport } from './native-chat-message-list-test-viewport'
|
||||
|
||||
// Counts each run rebuilding its lines and thoughts: a rebuilt transcript hands every run a fresh
|
||||
// member list, and only the row's memo keeps a settled run from redoing that work every frame.
|
||||
const entriesCalls = vi.hoisted(() => ({ count: 0 }))
|
||||
vi.mock('../../../../shared/native-chat-work-run', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof NativeChatWorkRunModule>()
|
||||
return {
|
||||
...actual,
|
||||
nativeChatWorkRunEntries: (...args: Parameters<typeof actual.nativeChatWorkRunEntries>) => {
|
||||
entriesCalls.count += 1
|
||||
return actual.nativeChatWorkRunEntries(...args)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
const { NativeChatMessageList } = await import('./NativeChatMessageList')
|
||||
|
||||
let restoreViewport = (): void => {}
|
||||
beforeAll(() => {
|
||||
restoreViewport = installNativeChatMessageListTestViewport()
|
||||
})
|
||||
afterAll(() => restoreViewport())
|
||||
afterEach(cleanup)
|
||||
|
||||
let clock = 0
|
||||
function row(
|
||||
id: string,
|
||||
role: NativeChatMessage['role'],
|
||||
blocks: NativeChatMessage['blocks'],
|
||||
extra: Partial<NativeChatMessage> = {}
|
||||
): NativeChatMessage {
|
||||
clock += 1
|
||||
return { id, role, blocks, timestamp: clock, source: 'transcript', ...extra }
|
||||
}
|
||||
const thought = (id: string) =>
|
||||
row(id, 'reasoning', [{ type: 'text', text: `thinking ${id}` }], {
|
||||
state: 'completed',
|
||||
completedAt: clock + 1
|
||||
})
|
||||
const command = (id: string, output: string) =>
|
||||
row(id, 'assistant', [
|
||||
{ type: 'tool-call', name: 'Bash', input: { command: id }, state: 'completed', callId: id },
|
||||
{ type: 'tool-result', output, callId: id }
|
||||
])
|
||||
|
||||
const prompt = row('user-1', 'user', [{ type: 'text', text: 'go' }])
|
||||
// Three settled runs, each closed by a reply, then the live run.
|
||||
const settled = [1, 2, 3].flatMap((turn) => [
|
||||
thought(`r${turn}a`),
|
||||
command(`c${turn}a`, 'ok'),
|
||||
thought(`r${turn}b`),
|
||||
command(`c${turn}b`, 'ok'),
|
||||
row(`reply-${turn}`, 'assistant', [{ type: 'text', text: `Step ${turn} done.` }])
|
||||
])
|
||||
const liveHead = [thought('r4'), command('c4', 'ok')]
|
||||
|
||||
function journal(rows: readonly NativeChatMessage[]): AgentJournalRenderItem[] {
|
||||
return [
|
||||
{
|
||||
itemId: prompt.id,
|
||||
revision: 1,
|
||||
sequence: 1,
|
||||
observedAt: 1,
|
||||
body: { kind: 'message', role: 'user', blocks: prompt.blocks }
|
||||
},
|
||||
{
|
||||
itemId: 'turn-1',
|
||||
revision: 1,
|
||||
sequence: 2,
|
||||
observedAt: 2,
|
||||
body: { kind: 'turn', turnId: 'turn-1', state: 'running', userItemId: prompt.id }
|
||||
},
|
||||
...rows.map((message, index) => ({
|
||||
itemId: message.id,
|
||||
revision: 1,
|
||||
sequence: index + 3,
|
||||
observedAt: index + 3,
|
||||
body: { kind: 'message' as const, role: message.role, blocks: message.blocks }
|
||||
}))
|
||||
]
|
||||
}
|
||||
|
||||
function list(tail: NativeChatMessage): React.JSX.Element {
|
||||
const rows = [...settled, ...liveHead, tail]
|
||||
const session: NativeChatLiveSession = {
|
||||
messages: [prompt, ...rows],
|
||||
status: 'working',
|
||||
sessionId: 'session-1',
|
||||
agent: 'codex',
|
||||
hasMore: false,
|
||||
loadingEarlier: false,
|
||||
olderHistoryGeneration: 0,
|
||||
loadEarlier: vi.fn(),
|
||||
readPhase: 'ready'
|
||||
}
|
||||
return (
|
||||
<NativeChatMessageList
|
||||
session={session}
|
||||
journalItems={journal(rows)}
|
||||
isWorking
|
||||
expandSignal={false}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
describe('settled work runs while the live run streams', () => {
|
||||
it('rebuilds only the live run on each frame', () => {
|
||||
const tail = (frame: number) => ({ ...command('c5', `line ${frame}`), id: 'c5' })
|
||||
const { rerender, container } = render(list(tail(0)))
|
||||
expect(container.querySelectorAll('[data-native-chat-tool-run-state]')).toHaveLength(4)
|
||||
entriesCalls.count = 0
|
||||
for (let frame = 1; frame <= 5; frame += 1) {
|
||||
rerender(list(tail(frame)))
|
||||
}
|
||||
expect(entriesCalls.count).toBe(5)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user