From 9ee7ee79bfaea6af831f5a745bb7afbc2dc8f75d Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Fri, 16 Oct 2026 02:21:29 -0700 Subject: [PATCH] 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> --- ...ativeChatMessageList.render-cost.test.tsx} | 117 +++++++++++++- ...tiveChatMessageList.work-run-memo.test.tsx | 146 ------------------ 2 files changed, 116 insertions(+), 147 deletions(-) rename src/renderer/src/components/native-chat/{NativeChatMessageList.stream-render.perf.test.tsx => NativeChatMessageList.render-cost.test.tsx} (65%) delete mode 100644 src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.render-cost.test.tsx similarity index 65% rename from src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx rename to src/renderer/src/components/native-chat/NativeChatMessageList.render-cost.test.tsx index 1fb38fed9a4..5c9d9557f72 100644 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.stream-render.perf.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatMessageList.render-cost.test.tsx @@ -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() + return { + ...actual, + nativeChatWorkRunEntries: (...args: Parameters) => { + 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 { + 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 ( + + ) +} + +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) + }) +}) diff --git a/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx b/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx deleted file mode 100644 index 3919fded6b0..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatMessageList.work-run-memo.test.tsx +++ /dev/null @@ -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() - return { - ...actual, - nativeChatWorkRunEntries: (...args: Parameters) => { - 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 { - 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 ( - - ) -} - -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) - }) -})