From ffff2d9ea9923f09f43ca05664bbccd7350864d6 Mon Sep 17 00:00:00 2001 From: OrcaWin Date: Wed, 4 Nov 2026 23:08:45 -0800 Subject: [PATCH] test: remove low-value attachment and tool presentation tests (#38420) Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> --- ...NativeChatTextAttachmentConsumers.test.tsx | 502 ------------- ...iveChatTextAttachments.visibility.test.tsx | 293 -------- .../native-chat/NativeChatToolLine.test.tsx | 403 +---------- .../NativeChatToolRun.appearance.test.tsx | 267 ------- .../NativeChatToolRun.identity.test.tsx | 186 +---- .../native-chat/NativeChatToolRun.test.tsx | 674 ------------------ .../NativeChatTranscriptChrome.test.tsx | 221 +----- .../NativeChatTurnDiffRollup.test.tsx | 30 +- .../NativeChatUsageLimitActions.test.tsx | 172 +---- 9 files changed, 7 insertions(+), 2741 deletions(-) delete mode 100644 src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx delete mode 100644 src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx diff --git a/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx b/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx deleted file mode 100644 index 65c1e8cffc2..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatTextAttachmentConsumers.test.tsx +++ /dev/null @@ -1,502 +0,0 @@ -// @vitest-environment happy-dom -import { useRef, useMemo } from 'react' - -import { NativeChatMarkdownImageOwnerContext } from './native-chat-markdown-image-owner' - -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { afterEach, expect, it, vi } from 'vitest' - -import { NativeChatPromptEditor } from './NativeChatPromptEditor' -import { - NativeChatTextAttachmentScope, - NativeChatTextAttachments -} from './NativeChatTextAttachments' -import { useNativeChatPastedTextFile } from './use-native-chat-pasted-text-file' -import { - insertNativeChatPastedText, - type NativeChatComposerInput -} from './native-chat-composer-input' -import { promptEditor, promptValue } from './native-chat-prompt-editor.test-support' -import { - clearNativeChatComposerDraftsForTests, - readNativeChatComposerDraft, - updateNativeChatComposerDraft -} from './native-chat-composer-draft-store' -import { resolveNativeChatPastedText } from './native-chat-composer-pasted-text-references' -import { nativeChatPastedTextOperations } from './native-chat-pasted-text-reference' - -const mocks = vi.hoisted(() => ({ - save: vi.fn(), - rpc: vi.fn(), - write: vi.fn(), - resolved: false, - start: vi.fn(), - append: vi.fn(), - finish: vi.fn(), - cancel: vi.fn() -})) -vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc })) -vi.mock('./native-chat-visual-markdown-extension', () => ({ - useNativeChatVisualMarkdownExtension: () => undefined -})) -vi.mock('./use-native-chat-file-link-existence', () => ({ - useNativeChatFileLinkExists: () => undefined -})) -vi.mock('sonner', () => ({ toast: { success: vi.fn(), error: vi.fn() } })) -vi.mock('./NativeChatAutocompleteMenus', () => ({ - NativeChatMentionMenu: () => null, - NativeChatPickerMenu: () => null -})) -vi.mock('./use-native-chat-skills', () => ({ - useNativeChatSkills: () => ({ status: 'ready', skills: [], error: null, retry: () => {} }) -})) -const scope = 'agent-session:consumer35636' -const path = - '/host/agent-session-attachments/12345678-1234-1234-1234-123456789abc/orca-paste-123-abc.txt' -let paste: (text: string) => boolean -function Composer({ initialValue = '' }: { initialValue?: string }) { - const ref = useRef(null) - paste = useNativeChatPastedTextFile({ - scopeKey: scope, - scopeEditor: () => ref.current, - resolveAttachmentOwner: () => ({ kind: 'local' }), - insertTypedText: (text) => insertNativeChatPastedText(ref.current, text), - setNotice: () => {} - }) - const resolved = mocks.resolved - const owner = useMemo( - () => ({ - runtimeContext: resolved - ? { target: { kind: 'local' as const }, worktreeId: 'w', worktreePath: '/repo' } - : null, - isVisible: true, - fileLinkContext: null - }), - [resolved] - ) - return ( - - - {}} - onSelect={() => {}} - /> - - - ) -} -async function openSource() { - fireEvent.click(await screen.findByRole('button', { name: 'Pasted text' })) - await waitFor(() => - expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe( - false - ) - ) -} -afterEach(() => { - cleanup() - clearNativeChatComposerDraftsForTests() - nativeChatPastedTextOperations.clear() - vi.clearAllMocks() -}) - -it('opens draft and sent sources, exports exact bytes and scrolls a giant-line paste to the end', async () => { - const text = `\ufeff${Array.from({ length: 5000 }, (_, index) => `🙂paste${index}\r\n`).join('')}END35636` - const ending = Promise.withResolvers() - let heldRange = false - let viewportHeight = 600 - let viewportWidth = 390 - let horizontalOccupation = 0 - let rowHeight = 20 - const nativeRect = HTMLElement.prototype.getBoundingClientRect - vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation(function ( - this: HTMLElement - ) { - return this.classList.contains('invisible') - ? new DOMRect(0, 0, 8, rowHeight) - : nativeRect.call(this) - }) - const positions = new WeakMap() - const horizontal = new WeakMap() - const scrollWrites: HTMLElement[] = [] - let deliverScroll = true - const later = Promise.withResolvers() - const laterStarted = Promise.withResolvers() - let holdGiant = false - const arrayBuffer = Blob.prototype.arrayBuffer - vi.spyOn(Blob.prototype, 'arrayBuffer').mockImplementation(async function (this: Blob) { - if (holdGiant) { - laterStarted.resolve() - await later.promise - } - return arrayBuffer.call(this) - }) - vi.spyOn(HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(function ( - this: HTMLElement - ) { - return this.classList.contains('scrollbar-editor') - ? Number.parseFloat(this.style.width) || viewportWidth - : viewportWidth - }) - vi.spyOn(HTMLElement.prototype, 'scrollWidth', 'get').mockImplementation(function ( - this: HTMLElement - ) { - const track = this.lastElementChild - return Math.max( - this.clientWidth, - track instanceof HTMLElement ? Number.parseFloat(track.style.minWidth) || 0 : 0, - ...(track instanceof HTMLElement && track.style.overflow === 'clip' - ? [] - : Array.from(this.querySelectorAll('[data-text-segment]')).map( - (row) => Array.from(row.textContent || '').length * 8 - )) - ) - }) - vi.spyOn(HTMLElement.prototype, 'scrollLeft', 'get').mockImplementation(function ( - this: HTMLElement - ) { - const left = Math.min(horizontal.get(this) || 0, this.scrollWidth - this.clientWidth) - horizontal.set(this, left) - return left - }) - vi.spyOn(HTMLElement.prototype, 'scrollLeft', 'set').mockImplementation(function ( - this: HTMLElement, - value - ) { - horizontal.set(this, Math.max(0, Math.min(value, this.scrollWidth - this.clientWidth))) - }) - const measurements = new Set<() => void>() - vi.spyOn(HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(function ( - this: HTMLElement - ) { - return this.classList.contains('scrollbar-editor') - ? this.offsetHeight - (this.scrollWidth > this.clientWidth ? horizontalOccupation : 0) - : viewportHeight - }) - vi.spyOn(HTMLElement.prototype, 'offsetHeight', 'get').mockImplementation(function ( - this: HTMLElement - ) { - return Number.parseFloat(this.style.height) || viewportHeight - }) - vi.spyOn(HTMLElement.prototype, 'scrollHeight', 'get').mockImplementation(function ( - this: HTMLElement - ) { - const track = this.lastElementChild - return Math.max( - this.clientHeight, - track instanceof HTMLElement ? Number.parseFloat(track.style.height) || 0 : 0, - ...(track instanceof HTMLElement && track.style.overflow === 'clip' - ? [] - : Array.from(this.querySelectorAll('[data-text-segment]')).map( - (row) => Number.parseFloat(row.style.top) + Number.parseFloat(row.style.height) - )) - ) - }) - vi.spyOn(HTMLElement.prototype, 'scrollTop', 'get').mockImplementation(function ( - this: HTMLElement - ) { - const top = Math.min(positions.get(this) || 0, this.scrollHeight - this.clientHeight) - positions.set(this, top) - return top - }) - vi.spyOn(HTMLElement.prototype, 'scrollTop', 'set').mockImplementation(function ( - this: HTMLElement, - value - ) { - scrollWrites.push(this) - const top = Math.max(0, Math.min(value, this.scrollHeight - this.clientHeight)) - if (top !== this.scrollTop) { - positions.set(this, top) - if (deliverScroll) { - queueMicrotask(() => this.dispatchEvent(new Event('scroll'))) - } - } - }) - vi.stubGlobal( - 'ResizeObserver', - class { - readonly notify: () => void - constructor(callback: ResizeObserverCallback) { - this.notify = () => callback([], this) - measurements.add(this.notify) - } - observe() {} - unobserve() {} - disconnect() { - measurements.delete(this.notify) - } - } - ) - try { - mocks.start.mockResolvedValue({ canceled: false, transferId: 'output' }) - mocks.finish.mockResolvedValue({ canceled: false }) - mocks.resolved = false - const upload = Promise.withResolvers() - mocks.save.mockReturnValue(upload.promise) - mocks.rpc.mockImplementation(async (_target, method, request) => { - if (method === 'status.get') { - return { capabilities: ['native-chat.text-attachment-read.v1'] } - } - const bytes = Buffer.from(text) - if (request.offset > 0) { - heldRange = true - await ending.promise - } - const chunk = bytes.subarray(request.offset, request.offset + request.length) - return { - snapshot: 'a'.repeat(64), - size: bytes.length, - bytesRead: chunk.length, - contentBase64: chunk.toString('base64'), - eof: request.offset + chunk.length === bytes.length - } - }) - Object.defineProperty(window, 'api', { - configurable: true, - value: { - ui: { savePastedTextAsFile: mocks.save, writeClipboardText: mocks.write }, - fs: { - startDownloadedFile: mocks.start, - appendDownloadedFileChunk: mocks.append, - finishDownloadedFile: mocks.finish, - cancelDownloadedFile: mocks.cancel - } - } - }) - let view = render() - await act(async () => { - expect(paste(text)).toBe(true) - }) - expect(screen.getByRole('button', { name: 'Pasted text' })).toBeTruthy() - await openSource() - const dialog = screen.getByRole('dialog', { name: 'Pasted text' }) - expect(dialog.getAttribute('data-slot')).toBe('dialog-content') - expect(document.getElementById(dialog.getAttribute('aria-describedby')!)?.textContent).toBe( - `${Buffer.byteLength(text).toLocaleString('en-US')} bytes` - ) - await waitFor(() => expect(dialog.contains(document.activeElement)).toBe(true)) - fireEvent.click(screen.getByRole('button', { name: 'Copy contents' })) - await waitFor(() => expect(mocks.write).toHaveBeenCalledWith(text)) - expect(mocks.rpc).not.toHaveBeenCalled() - fireEvent.keyDown(document.activeElement!, { key: 'Escape' }) - await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) - mocks.resolved = true - await act(async () => upload.resolve(path)) - view.rerender() - expect(nativeChatPastedTextOperations.size).toBe(0) - fireEvent.click(screen.getByRole('button', { name: 'Pasted text' })) - await waitFor(() => - expect(mocks.rpc).toHaveBeenCalledWith( - expect.anything(), - 'agentSessionAttachment.readTextChunk', - expect.objectContaining({ path }), - expect.anything() - ) - ) - const measured = document.querySelector('.scrollbar-editor') - if (!(measured instanceof HTMLElement)) { - throw new Error('Expected real committed text scroller') - } - await waitFor(() => - expect(measured.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0) - ) - fireEvent.keyDown(measured, { key: 'End' }) - await waitFor(() => expect(heldRange).toBe(true)) - await act(async () => ending.resolve()) - await waitFor(() => { - expect(measured.textContent).toContain('END35636') - expect(measured.scrollTop + measured.clientHeight).toBe(measured.scrollHeight) - }) - const oldEnd = measured.scrollTop - viewportHeight = 3000 - expect.soft(measured.scrollTop).toBe(oldEnd) - act(() => measurements.forEach((notify) => notify())) - await waitFor(() => - expect(measured.scrollTop + measured.clientHeight).toBe(measured.scrollHeight) - ) - fireEvent.keyDown(measured, { key: 'Home' }) - await waitFor(() => { - expect(measured.scrollTop).toBe(0) - expect(measured.querySelector('[data-text-segment]')?.textContent).toContain('🙂paste0') - }) - fireEvent.keyDown(measured, { key: 'PageDown' }) - await waitFor(() => { - expect(measured.querySelector('[data-text-segment]')?.textContent).toBeTruthy() - expect(measured.querySelector('[data-text-segment]')?.textContent).not.toContain('🙂paste0') - }) - const visible = measured.querySelector('[data-text-segment]')?.textContent - viewportHeight = 400 - rowHeight = 24 - act(() => measurements.forEach((notify) => notify())) - await waitFor(() => - expect(measured.querySelector('[data-text-segment]')?.textContent).toBe(visible) - ) - fireEvent.keyDown(measured, { key: 'Home' }) - await waitFor(() => expect(measured.scrollTop).toBe(0)) - fireEvent.click(screen.getByRole('button', { name: 'Close' })) - await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull()) - const editor = promptEditor(screen.getByRole('textbox')) - const token = promptValue(screen.getByRole('textbox')).trim() - act(() => { - editor.commands.selectAll() - editor.commands.deleteSelection() - }) - expect(screen.queryByRole('button', { name: 'Pasted text' })).toBeNull() - await act(async () => { - editor.commands.undo() - }) - expect(screen.getByRole('button', { name: 'Pasted text' })).toBeTruthy() - const saved = structuredClone(readNativeChatComposerDraft(scope)) - expect(saved.text).toContain(token) - const sent = resolveNativeChatPastedText(scope, token) - expect(sent).toBe(`@${path}`) - view.unmount() - clearNativeChatComposerDraftsForTests() - updateNativeChatComposerDraft( - scope, - { text: saved.text, document: saved.document, pastedTextFiles: saved.pastedTextFiles }, - 'immediate' - ) - expect(readNativeChatComposerDraft(scope).text).toBe(saved.text) - view = render( - {content}} - /> - ) - await openSource() - fireEvent.click(screen.getByRole('button', { name: 'Copy contents' })) - await waitFor(() => expect(mocks.write).toHaveBeenCalledTimes(2)) - expect(mocks.write.mock.calls[1]).toEqual([text]) - await waitFor(() => - expect(screen.getByRole('button', { name: 'Save file' }).hasAttribute('disabled')).toBe(false) - ) - fireEvent.click(screen.getByRole('button', { name: 'Save file' })) - await waitFor(() => expect(mocks.finish).toHaveBeenCalledOnce()) - expect( - Buffer.concat( - mocks.append.mock.calls.map(([args]) => Buffer.from(args.contentBase64, 'base64')) - ).equals(Buffer.from(text)) - ).toBe(true) - view.unmount() - const giant = `${'x'.repeat(25 * 1024 * 1024)}TAIL` - mocks.resolved = false - const heldUpload = Promise.withResolvers() - mocks.save.mockReturnValueOnce(heldUpload.promise) - viewportHeight = 600 - view = render() - await act(async () => { - expect(paste(giant)).toBe(true) - }) - fireEvent.click(screen.getByRole('button', { name: 'Pasted text' })) - await waitFor(() => - expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe( - true - ) - ) - const scroller = document.querySelector('.scrollbar-editor') - if (!(scroller instanceof HTMLElement)) { - throw new Error('Expected real text scroller') - } - await waitFor(() => - expect(scroller.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0) - ) - const track = scroller.lastElementChild - if (!(track instanceof HTMLElement)) { - throw new Error('Expected virtual track') - } - expect(Number.parseFloat(track.style.height)).toBeLessThan(8_000_000) - scroller.scrollLeft = 500 - holdGiant = true - await act(async () => { - scroller.scrollTop = 4000 - }) - await laterStarted.promise - expect.soft(scroller.scrollLeft).toBe(500) - deliverScroll = false - scroller.scrollTop += 80 - const liveTop = scroller.scrollTop - const writes = scrollWrites.filter((node) => node === scroller).length - holdGiant = false - await act(async () => later.resolve()) - await waitFor(() => - expect(scroller.querySelectorAll('[data-text-segment]').length).toBeGreaterThan(0) - ) - expect.soft(scrollWrites.filter((node) => node === scroller).length).toBe(writes) - expect.soft(scroller.scrollTop).toBe(liveTop) - expect.soft(scroller.scrollLeft).toBe(500) - deliverScroll = true - fireEvent.scroll(scroller) - - fireEvent.scroll(scroller, { - target: { scrollTop: Number.parseFloat(track.style.height) - 600 } - }) - await waitFor(() => expect(scroller.textContent).toContain('TAIL')) - expect(scroller.querySelectorAll('[data-text-segment]').length).toBeLessThanOrEqual( - Math.ceil(viewportHeight / rowHeight) + 4 - ) - fireEvent.scroll(scroller, { target: { scrollTop: 0 } }) - await waitFor(() => - expect(scroller.querySelector('[data-text-segment]')?.textContent).toBe('x'.repeat(256)) - ) - viewportHeight = 3000 - rowHeight = 10 - act(() => measurements.forEach((measure) => measure())) - await waitFor(() => { - const rows = scroller.querySelectorAll('[data-text-segment]') - expect(rows.length).toBeGreaterThanOrEqual(Math.ceil(viewportHeight / rowHeight)) - expect(rows.length).toBeLessThanOrEqual(Math.ceil(viewportHeight / rowHeight) + 4) - expect(scroller.scrollTop).toBe(0) - }) - fireEvent.click(screen.getByRole('button', { name: 'Close' })) - await act(async () => heldUpload.resolve(path)) - view.unmount() - viewportHeight = 600 - viewportWidth = 400 - rowHeight = 20 - horizontalOccupation = 6 - const dense = `HEAD${'X'.repeat(56)}\n${'\n'.repeat(50 * 1024 * 1024 - 61)}` - mocks.save.mockReturnValueOnce(new Promise(() => {})) - view = render() - await act(async () => { - expect(paste(dense)).toBe(true) - }) - fireEvent.click(screen.getByRole('button', { name: 'Pasted text' })) - const classic = document.querySelector('.scrollbar-editor') - if (!(classic instanceof HTMLElement)) { - throw new Error('Expected native classic-scrollbar host') - } - await waitFor(() => - expect(classic.querySelector('[data-text-segment]')?.textContent).toContain('HEAD') - ) - act(() => measurements.forEach((notify) => notify())) - // Visit real source blocks through the shipping wheel/demand/read path, without a prefilled index. - for (let block = 1; block < 100; block++) { - await act(async () => { - fireEvent.wheel(classic, { deltaY: (block === 1 ? 15292 : 15352) * rowHeight }) - }) - } - fireEvent.keyDown(classic, { key: 'End' }) - await waitFor(() => expect(classic.scrollHeight).toBe(8_000_593)) - await waitFor(() => expect(classic.scrollTop).toBe(7_999_999)) - const classicEnd = classic.scrollTop - viewportWidth = 650 - expect.soft(classic.scrollTop).toBe(classicEnd) - act(() => measurements.forEach((notify) => notify())) - await waitFor(() => expect(classic.scrollTop + classic.clientHeight).toBe(classic.scrollHeight)) - view.unmount() - } finally { - ending.resolve() - later.resolve() - cleanup() - vi.restoreAllMocks() - vi.unstubAllGlobals() - } -}) diff --git a/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx b/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx deleted file mode 100644 index 44df2f93609..00000000000 --- a/src/renderer/src/components/native-chat/NativeChatTextAttachments.visibility.test.tsx +++ /dev/null @@ -1,293 +0,0 @@ -// @vitest-environment happy-dom -import type { ReactNode } from 'react' -import type { TerminalActivationController } from '../use-terminal-activation-actions' -import type { NativeChatStructuredViewProps } from './native-chat-view-types' -import { useContext, useState } from 'react' -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, expect, it, vi } from 'vitest' -import { useAppStore } from '@/store' -import { registerTabGroupBody } from '../tab-group/tab-group-body-geometry' -import { useTerminalKeyboardShortcuts } from '../use-terminal-keyboard-shortcuts' -import { useTerminalWorkspaceFoundation } from '../use-terminal-workspace-foundation' - -import StructuredAgentSessionPaneOverlayLayer from './StructuredAgentSessionPaneOverlayLayer' -import { - NativeChatTextAttachmentScope, - NativeChatTextAttachmentOpener -} from './NativeChatTextAttachments' -import { nativeChatFanoutFixture } from './native-chat-workspace-test-fixtures' -import { getDefaultSettings } from '../../../../shared/constants' - -const mocks = vi.hoisted(() => ({ - rpc: vi.fn(), - write: vi.fn(), - start: vi.fn(), - append: vi.fn(), - finish: vi.fn(), - cancel: vi.fn(), - fallback: false, - web: false -})) -vi.mock('@/runtime/runtime-rpc-client', () => ({ callRuntimeRpc: mocks.rpc })) -vi.mock('@/lib/web-client-location', () => ({ isWebClientLocation: () => mocks.web })) -vi.mock('sonner', () => ({ toast: { success: vi.fn() } })) -// Keep the production overlay, NativeChatView and its visibility provider; agent execution is outside this UI contract. -vi.mock('./NativeChatStructuredSession', () => ({ - NativeChatStructuredSession: (props: NativeChatStructuredViewProps) => -})) -vi.mock('./NativeChatPaneFileDropSurface', () => ({ - NativeChatPaneFileDropSurface: ({ children }: { children: ReactNode }) => <>{children} -})) -const path = '/host/agent-session-attachments/12345678-1234-1234-1234-123456789abc/log.txt' -const bytes = Buffer.from('selected source') -const chunk = () => ({ - snapshot: 'a'.repeat(64), - size: bytes.length, - contentBase64: bytes.toString('base64'), - bytesRead: bytes.length, - eof: true -}) -const ws = 'folder:workspace-0' -const initial = useAppStore.getInitialState() -let unregister: () => void -let anchor: HTMLElement - -function Editor({ tabId }: { tabId: string }) { - const [draft, setDraft] = useState('') - const open = useContext(NativeChatTextAttachmentOpener) - return ( - <> - setDraft(event.target.value)} - /> - - - ) -} -function ChatBody({ tabId }: NativeChatStructuredViewProps) { - return ( - - - - ) -} -function Host() { - const visible = useAppStore( - (state) => state.activeWorktreeId === ws && state.activeView === 'terminal' - ) - const controller: TerminalActivationController = { - ...useAppStore.getState(), - ...useTerminalWorkspaceFoundation(), - activeWorktreeId: ws, - keybindings: {}, - mobileEmulatorEnabled: false, - terminalShortcutPolicy: 'orca-first', - tabBarOrder: undefined, - activityTerminalPortals: [], - foregroundTerminalTabIds: [], - tabs: [], - titlebarTabsTarget: null, - worktreeFiles: [], - worktreeBrowserTabs: [], - worktreeClientHostedBrowserRows: [], - getEffectiveLayoutForWorktree: vi.fn(), - effectiveActiveLayout: undefined, - activeWorktreeBrowserTabIdsKey: '', - terminalProviderSnapshotCapabilityRevision: 0, - saveDialogFileId: null, - setSaveDialogFileId: vi.fn(), - saveDialogFile: null, - pendingEditorCloseQueueRef: { current: [] }, - closedReactionsRef: { current: new Map() }, - inFlightSaveFileIdRef: { current: null }, - isClosingRef: { current: false }, - closeDialogDebounceTimersRef: { current: new Set() }, - releaseCloseDialogGuardAfterDebounce: vi.fn(), - windowCloseDialogOpen: false, - setWindowCloseDialogOpen: vi.fn(), - windowCloseDialogKind: 'running', - windowCloseAfterDirtyRef: { current: null }, - confirmNativeWindowClose: vi.fn(), - proceedToNativeWindowClose: vi.fn(), - waitForFileClosed: vi.fn(), - settleQueuedClose: vi.fn(), - getNextQueuedEditorClose: vi.fn(), - advanceEditorCloseQueue: vi.fn(), - queueEditorCloseRequests: vi.fn(), - handleCloseFile: vi.fn(), - handleSaveDialogSave: vi.fn(), - handleSaveDialogDiscard: vi.fn(), - handleSaveDialogCancel: vi.fn(), - measurableBackgroundWorktreeTimersRef: { current: new Map() }, - backgroundMountRevision: 0, - setBackgroundMountRevision: vi.fn(), - terminalParkingRevision: 0, - setTerminalParkingRevision: vi.fn(), - browserGuestRetentionRevision: 0, - setBrowserGuestRetentionRevision: vi.fn(), - parkedTerminalWorktreeIds: new Set(), - setParkedTerminalWorktreeIds: vi.fn(), - manuallyParkedTerminalWorktreeIds: new Set(), - effectiveParkedTerminalWorktreeIds: new Set(), - forceParkedTerminalWorktreeIds: new Set(), - setForceParkedTerminalWorktreeIds: vi.fn(), - evictionExemptTerminalTabIds: new Set(), - setEvictionExemptTerminalTabIds: vi.fn(), - parkedCaptureDoneRef: { current: new Set() }, - backgroundMountTabIdsByWorktreeRef: { current: new Map() }, - activationDeferredMountTabIdsByWorktreeRef: { current: new Map() }, - lastActivationWorktreeIdRef: { current: null }, - prewarmLeasesRef: { current: new Map() }, - startupTerminalTabHoldRef: { current: null }, - activationDeferralPlanRevisionRef: { current: 0 }, - anyMountedWorktreeHasLayout: false, - activationDeferralPlanRevision: 0, - startupTerminalTabHold: null, - handleCloseTab: vi.fn(), - handleCloseBrowserTab: vi.fn(), - handlePtyExit: vi.fn(), - closeTabBarTabs: vi.fn(), - handleCloseOthers: vi.fn(), - handleCloseTabsToRight: vi.fn(), - handleCloseTabsToLeft: vi.fn(), - handleCloseAllFiles: vi.fn(), - handleNewBrowserTab: vi.fn(), - handleNewBrowserTabSplitRight: vi.fn(), - handleNewFile: vi.fn(), - handleNewSimulatorTab: vi.fn(), - handleNewTab: vi.fn(), - handleOpenEntry: vi.fn(), - handleDuplicateBrowserTab: vi.fn(), - handleActivateTab: vi.fn(), - handleTogglePaneExpand: vi.fn(), - handleActivateBrowserTab: vi.fn() - } - useTerminalKeyboardShortcuts(controller) - return -} -beforeEach(() => { - vi.stubGlobal('reportError', (error: unknown) => { - throw error - }) - vi.stubGlobal('__ORCA_WEB_CLIENT__', true) - vi.stubGlobal( - 'ResizeObserver', - class { - observe() {} - unobserve() {} - disconnect() {} - } - ) - vi.spyOn(navigator, 'userAgent', 'get').mockReturnValue('Linux') - anchor = document.createElement('div') - anchor.getBoundingClientRect = () => new DOMRect(0, 32, 800, 568) - document.body.append(anchor) - unregister = registerTabGroupBody('g', anchor) - const fixture = nativeChatFanoutFixture(2, 1, 0) - fixture.unifiedTabsByWorktree[ws] = fixture.unifiedTabsByWorktree[ws].map((tab) => ({ - ...tab, - groupId: 'g' - })) - useAppStore.setState( - { - ...initial, - ...fixture, - settings: getDefaultSettings('/test35636'), - activeWorktreeId: ws, - activeWorkspaceExecutionHostId: 'local', - activeView: 'terminal', - activeTabType: 'agent-session', - groupsByWorktree: { - [ws]: [{ id: 'g', worktreeId: ws, activeTabId: 'chat-0', tabOrder: ['chat-0', 'chat-1'] }] - }, - activeGroupIdByWorktree: { [ws]: 'g' } - }, - true - ) - mocks.web = false - mocks.write.mockReset().mockResolvedValue(undefined) - mocks.start.mockReset().mockResolvedValue({ canceled: false, transferId: 'output35636' }) - mocks.append.mockReset().mockResolvedValue(undefined) - mocks.finish.mockReset().mockResolvedValue({ canceled: false }) - mocks.cancel.mockReset().mockResolvedValue(undefined) - mocks.rpc - .mockReset() - .mockImplementation(async (_target, method) => - method === 'status.get' - ? { runtimeId: 'host', capabilities: ['native-chat.text-attachment-read.v1'] } - : chunk() - ) - Object.defineProperty(window, 'api', { - configurable: true, - value: { - ui: { writeClipboardText: mocks.write }, - fs: { - startDownloadedFile: mocks.start, - appendDownloadedFileChunk: mocks.append, - finishDownloadedFile: mocks.finish, - cancelDownloadedFile: mocks.cancel - } - } - }) -}) -afterEach(() => { - cleanup() - unregister() - anchor.remove() - useAppStore.setState(initial, true) - vi.restoreAllMocks() - vi.unstubAllGlobals() -}) - -it('retires delayed Copy on a genuine keyboard tab hide, preserves the editor and reopens', async () => { - const view = render() - const input = screen.getByRole('textbox', { name: 'draft-chat-0' }) - fireEvent.change(input, { target: { value: 'editor state survives hide' } }) - input.focus() - const open = () => fireEvent.click(screen.getByRole('button', { name: 'Open source' })) - const ready = () => - waitFor(() => - expect(screen.getByRole('button', { name: 'Copy contents' }).hasAttribute('disabled')).toBe( - false - ) - ) - open() - await ready() - const delayed = Promise.withResolvers() - const waiting = Promise.withResolvers() - mocks.rpc.mockImplementationOnce(() => { - waiting.resolve() - return delayed.promise - }) - fireEvent.click(screen.getByRole('button', { name: 'Copy contents' })) - await waiting.promise - const dispatched = mocks.rpc.mock.calls.length - fireEvent.keyDown(screen.getByRole('dialog'), { - key: ']', - code: 'BracketRight', - ctrlKey: true, - altKey: true - }) - expect(useAppStore.getState().getActiveTab(ws)?.id).toBe('chat-1') - await act(async () => {}) - expect( - view.container - .querySelector('[data-structured-agent-session-overlay-tab-id="chat-0"]') - ?.hasAttribute('inert') - ).toBe(true) - expect(screen.queryByRole('dialog')).toBeNull() - await act(async () => delayed.resolve(chunk())) - expect(mocks.write).not.toHaveBeenCalled() - expect(mocks.rpc).toHaveBeenCalledTimes(dispatched) - fireEvent.keyDown(document, { key: '[', code: 'BracketLeft', ctrlKey: true, altKey: true }) - const returned = screen.getByRole('textbox', { name: 'draft-chat-0' }) - expect(returned).toBe(input) - expect(returned.value).toBe('editor state survives hide') - expect(screen.queryByRole('dialog')).toBeNull() - open() - await ready() - fireEvent.click(screen.getByRole('button', { name: 'Copy contents' })) - await waitFor(() => expect(mocks.write).toHaveBeenCalledExactlyOnceWith('selected source')) -}) diff --git a/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx index 4e0cefb74f3..7cfc39704cc 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolLine.test.tsx @@ -1,10 +1,8 @@ // @vitest-environment happy-dom import '@testing-library/jest-dom/vitest' -import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { NativeChatToolLine } from './NativeChatToolLine' -import { NativeChatToolRun } from './NativeChatToolRun' -import type { NativeChatToolCallBlock } from '../../../../shared/native-chat/native-chat-types' vi.mock('@/lib/syntax-highlighting/oniguruma', async () => ({ loadOniguruma: (await import('@/lib/syntax-highlighting/oniguruma-test-harness')) @@ -14,166 +12,6 @@ vi.mock('@/lib/syntax-highlighting/oniguruma', async () => ({ afterEach(cleanup) describe('tool sentence rows', () => { - it.each([ - ['Bash', { command: 'pnpm test' }, 'Ran', 'pnpm test', false], - ['Read', { file_path: '/repo/src/main.ts' }, 'Read', 'main.ts', true], - ['Edit', { file_path: 'C:\\repo\\main.ts' }, 'Edited', 'main.ts', true], - ['Write', { file_path: '/repo/new.ts' }, 'Edited', 'new.ts', true], - ['MultiEdit', { file_path: '/repo/main.ts' }, 'Edited', 'main.ts', true], - ['Grep', { pattern: 'TODO', path: '/repo' }, 'Searched', 'TODO', true], - ['Glob', { pattern: '**/*.ts' }, 'Searched', '**/*.ts', true], - ['search', { query: 'settings', command: 'rg settings' }, 'Searched', 'settings', true], - ['web_search', { query: 'react docs' }, 'Searched the web', 'react docs', true], - ['WebFetch', { url: 'https://example.com/docs' }, 'Fetched', 'example.com/docs', true], - ['CreateWidget', { description: 'a widget' }, 'CreateWidget', 'a widget', true], - ['list', { directory: '/repo' }, 'Listed', '/repo', true], - ['exec', { command: 'git diff' }, 'Ran', 'git diff', false], - ['local_shell', { command: 'pwd' }, 'Ran', 'pwd', false] - ])('describes %s with a verb and target', (name, input, verb, target, verbShown) => { - const { container } = render( - - ) - // A command is its own row: the run's header already says it ran. - expect(screen.queryByText(verb, { selector: 'span:not(.sr-only)' }) !== null).toBe(verbShown) - expect(screen.getByText(target, { selector: 'span:not(.sr-only)' })).toHaveClass( - 'text-chat-foreground' - ) - expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(name)) - expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(verb)) - expect(container.querySelector('.font-semibold')).toBeNull() - }) - - it('retains full paths in titles and accessible targets', () => { - render( - - ) - expect(screen.getByTitle('/repo/src/main.ts')).toHaveTextContent('main.ts') - expect(screen.getByTitle('/repo/src/main.ts')).toHaveAttribute('aria-hidden', 'true') - expect(screen.getByRole('button')).toHaveAccessibleName(/Read.*\/repo\/src\/main.ts/) - }) - - it('keeps integration identity in plain text even for a command-shaped tool name', () => { - render( - - ) - expect(screen.getByText('My server')).toBeInTheDocument() - expect(screen.getByText('shell')).toBeInTheDocument() - expect(screen.queryByText('Ran')).toBeNull() - expect(screen.getByText('inspect')).not.toHaveClass('font-mono') - expect(screen.getByText('inspect')).not.toHaveAttribute('data-native-chat-code-content') - }) - - it('counts changed lines rather than unchanged lines in an edit', () => { - const block: NativeChatToolCallBlock = { - type: 'tool-call', - name: 'Edit', - input: { file_path: 'main.ts', old_string: 'same\nold\n', new_string: 'same\nnew\nextra\n' }, - state: 'completed' - } - render() - expect(screen.getByRole('button', { name: /^Edited main\.ts/ })).toBeInTheDocument() - expect(screen.getByText('+2')).toBeInTheDocument() - expect(screen.getByText('-1')).toBeInTheDocument() - }) - - it('omits counts when an edit supplies no diff', () => { - render( - - ) - expect(screen.queryByText(/^[+-]\d+$/)).toBeNull() - }) - - it.each([ - ['Bash', { command: 'pnpm test' }, 'Running'], - ['exec', { command: 'git diff' }, 'Running'], - ['local_shell', { command: 'pwd' }, 'Running'], - ['Read', { file_path: '/repo/a.ts' }, 'Reading'], - ['Write', { file_path: '/repo/a.ts', content: 'new' }, 'Editing'], - ['Grep', { pattern: 'TODO' }, 'Searching'], - ['list', { directory: '/repo' }, 'Listing'], - ['web_search', { query: 'docs' }, 'Searching the web'], - ['WebFetch', { url: 'https://example.com/docs' }, 'Fetching'] - ])('uses present tense for a running %s', (name, input, verb) => { - render() - expect(screen.getByText(verb)).toBeInTheDocument() - }) - - it.each(['failed', undefined] as const)('never claims an edit landed with state %s', (state) => { - render( - - ) - expect(screen.getByRole('button')).toHaveAccessibleName( - state === 'failed' ? 'Edit Tried to edit /repo/a.ts' : 'Edit /repo/a.ts' - ) - expect(screen.queryByText('Edited')).toBeNull() - expect(screen.queryByText(/^[+-]\d+$/)).toBeNull() - }) - - it('does not treat an error result as a completed edit', () => { - render( - - ) - expect(screen.getByRole('button')).toHaveAccessibleName('Edit Tried to edit /repo/a.ts') - expect(screen.queryByText('Edited')).toBeNull() - }) - - it('uses a successful legacy result as completion evidence', () => { - render( - - ) - expect(screen.getByText('Ran')).toBeInTheDocument() - }) - - it.each([false, true])( - 'keeps a long Unicode fetch target unchanged (JSON input: %s)', - (jsonInput) => { - const url = `https://example.com/日本語/${'segment/'.repeat(25)}end` - const input = jsonInput ? JSON.stringify({ url }) : { url } - render( - - ) - expect(screen.getByTitle(url)).toHaveTextContent(url.slice('https://'.length)) - expect(screen.getByTitle(url)).not.toHaveTextContent('%E2%80%A6') - } - ) - it('keeps metadata and errors while toggling output behind the sentence', () => { render( { fireEvent.click(button) expect(screen.queryByText('command failed')).toBeNull() }) - - it.each([ - ['Bash', { type: 'tool-result', output: 'failed', isError: true }, undefined], - ['exec', { type: 'tool-result', output: 'failed', isError: true }, 1], - ['local_shell', undefined, 2], - ['Bash', { type: 'tool-result', output: 'ok' }, undefined] - ] as const)( - 'says Ran for %s with execution evidence, including failures', - (name, result, exitCode) => { - render( - - ) - expect(screen.getByText('Ran')).toBeInTheDocument() - expect(screen.queryByText('Running')).toBeNull() - } - ) - - it.each([ - ['Read', { file_path: '/repo/a.ts' }, 'Tried to read'], - ['Grep', { pattern: 'TODO' }, 'Tried to search'], - ['list', { directory: '/repo' }, 'Tried to list'], - ['WebFetch', { url: 'https://example.com' }, 'Tried to fetch'], - ['web_search', { query: 'docs' }, 'Tried to search the web'] - ])('describes a failed %s without claiming success', (name, input, verb) => { - render( - - ) - expect(screen.getByText(verb)).toBeInTheDocument() - }) - - it.each([ - ['Bash', { command: 'pnpm test' }], - ['exec', JSON.stringify({ cmd: 'pnpm test' })], - ['Bash', 'pnpm test'] - ])('omits duplicate input for a complete %s command row', (name, input) => { - const { container } = render( - - ) - fireEvent.click(screen.getByRole('button')) - expect(container.querySelectorAll('pre')).toHaveLength(1) - expect(container.querySelector('pre')).toHaveTextContent('all passed') - }) - - it.each([ - [{ command: 'pnpm test', description: 'Run the suite' }], - [{ command: ['printf', '%s', 'a b'] }], - [{ command: '/bin/zsh -lc "pnpm test"' }] - ])('opens a command to its output alone, never its raw arguments', (input) => { - const { container } = render( - - ) - fireEvent.click(screen.getByRole('button')) - expect([...container.querySelectorAll('pre')].map((pre) => pre.textContent)).toEqual([ - 'all passed' - ]) - }) - - it('does not offer empty detail for a short command without output', () => { - const { container } = render( - - ) - expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded') - expect(container.querySelector('pre')).toBeNull() - }) - - it.each([false, true])( - 'shows the full abbreviated command as plain text (JSON: %s)', - (jsonInput) => { - const command = `pnpm test ${'src/renderer/tests/long-path/'.repeat(170)}end.test.ts` - const input = jsonInput ? JSON.stringify({ command }) : { command } - const { container } = render( - - ) - expect(screen.getByTitle(command)).toHaveTextContent(/…$/) - expect(container.querySelector('pre')).toBeNull() - fireEvent.click(screen.getByRole('button')) - const detail = container.querySelector('pre') - expect(detail?.textContent).toBe(command) - expect(detail).toHaveClass('font-mono', 'bg-chat-code-surface') - expect(container.querySelectorAll('pre')).toHaveLength(2) - } - ) - - it.each([ - ['PowerShell', 'Get-ChildItem', true], - ['execute', 'Get-ChildItem', false] - ])('colors the full %s command only when its shell is known', async (name, verb, colored) => { - const command = `${verb} -Path 'C:\\repo\\src\\renderer' -Recurse -File | Where-Object { $_.Length -gt 10 }` - const { container } = render( - - ) - fireEvent.click(screen.getByRole('button')) - const detail = container.querySelector('pre') - - if (colored) { - // The first PowerShell block in this file loads the grammar and its regex engine. - await waitFor( - () => { - expect(detail?.querySelector('span[style*="--syntax-dark"]')).not.toBeNull() - }, - { timeout: 10_000 } - ) - } else { - expect(detail?.querySelector('span')).toBeNull() - } - expect(detail?.textContent).toBe(command) - }) - - it('keeps a long plain command with surrounding whitespace complete', () => { - const command = ` pnpm test ${'src/renderer/tests/long-path/'.repeat(170)}end.test.ts ` - const { container } = render( - - ) - fireEvent.click(screen.getByRole('button')) - expect(container.querySelector('pre')?.textContent).toBe(command) - }) - - it('opens an edit that has not landed to the change it proposes', () => { - const { container } = render( - - ) - expect(container.textContent).not.toContain('now') - fireEvent.click(screen.getByRole('button')) - expect(container.textContent).toContain('now') - }) - - it.each(['exec', 'shell', 'Bash'])('offers nothing to open on a running %s command', (name) => { - render( - - ) - expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded') - }) - - it('offers nothing to open on a call that has produced no output', () => { - render( - - ) - expect(screen.getByRole('button')).not.toHaveAttribute('aria-expanded') - }) - - it('shows a read as its output alone, not the arguments its row already names', () => { - const { container } = render( - - ) - fireEvent.click(screen.getByRole('button')) - expect([...container.querySelectorAll('pre')].map((pre) => pre.textContent)).toEqual([ - 'const a = 1' - ]) - }) - - it.each(['Task', 'Agent'])( - 'uses the unchanged description for live and settled %s calls', - (name) => { - const description = `explore settings search entries ${'without rewriting '.repeat(8)}end` - const input = { - description, - prompt: 'longer private instructions', - query: 'not the description' - } - const { rerender } = render( - - ) - expect(screen.getByText('Subagent')).toBeInTheDocument() - expect(screen.getByTitle(description).textContent).toBe(description) - rerender( - - ) - expect(screen.getByText('Subagent')).toBeInTheDocument() - expect(screen.getByTitle(description).textContent).toBe(description) - expect(screen.getByRole('button')).toHaveAccessibleName(new RegExp(name)) - } - ) - - it('preserves integration identity for an integration named Agent', () => { - render( - - ) - expect(screen.getByText('My server')).toBeInTheDocument() - expect(screen.queryByText('Subagent')).toBeNull() - }) - - it('still renders result-only rows', () => { - render() - fireEvent.click(screen.getByRole('button', { name: /Result/ })) - expect(screen.getByText('first second', { selector: 'pre' })).toHaveClass( - 'text-chat-foreground' - ) - }) }) diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx index 1a416114ed7..93844d513bc 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.appearance.test.tsx @@ -10,8 +10,6 @@ import type { NativeChatBlock } from '../../../../shared/native-chat/native-chat import { NativeChatToolRun } from './NativeChatToolRun' -import { nativeChatAppearanceStyle } from './native-chat-appearance-style' - import { openToolRunMembers } from './native-chat-tool-run-members-test-support' import { @@ -20,43 +18,8 @@ import { useNativeChatDisclosures } from './native-chat-disclosure-store' -import { revealNativeChatToolRunMember } from './NativeChatToolRunMembers' - import { i18n } from '@/i18n/i18n' -{ - describe('tool-run summary in a matching chat', () => { - afterEach(cleanup) - - it('keeps a long command available in the expanded detail', () => { - const command = `printf ${'x'.repeat(5000)}` - const style = nativeChatAppearanceStyle({ - terminalFontFamily: 'Menlo', - nativeChatAppearance: { matchTerminalInterface: true } - }) - const { container } = render( -
- -
- ) - - const summary = container.querySelector('span.line-clamp-2') - expect(summary).toHaveClass('line-clamp-2', 'break-words') - expect(summary?.textContent).toContain('printf') - expect(summary?.textContent?.length).toBeLessThan(command.length) - expect(container.querySelector('pre')).toBeNull() - - fireEvent.click(screen.getByRole('button')) - openToolRunMembers() - - expect(container.querySelector('pre')).toHaveTextContent(command) - }) - }) -} - { const QUESTION = 'What would you like me to do next in this repo?' const ASK_INPUT = { questions: [{ question: QUESTION }] } @@ -107,29 +70,6 @@ import { i18n } from '@/i18n/i18n' describe('NativeChatToolRun awaiting-input row', () => { afterEach(cleanup) - it('does not revive stale tool state after a turn stops', () => { - render( - - ) - expect(screen.queryByText('Awaiting user input:')).toBeNull() - expect(screen.getByText('Asked:')).toBeInTheDocument() - }) - - it('keeps a pending question visible alongside another active tool', () => { - render( - - ) - expect(screen.getByText('Awaiting user input:')).toBeInTheDocument() - expect(screen.getByText('Reading 1 file')).toBeInTheDocument() - expect(screen.getByText('Read a.ts')).toBeInTheDocument() - }) it('preserves errors from failed question calls', () => { render( @@ -147,79 +87,6 @@ import { i18n } from '@/i18n/i18n' expect(screen.queryByText('Asked:')).toBeNull() expect(screen.getAllByText('Question rejected').length).toBeGreaterThan(0) }) - it('replaces a running ask call with the awaiting row', () => { - const { container } = render( - - ) - - expect(screen.getByText('Awaiting user input:')).toHaveClass( - 'animate-pulse', - 'motion-reduce:animate-none' - ) - expect(screen.getByText(QUESTION)).toBeInTheDocument() - expect(container.querySelector('.lucide-message-square-more')).toBeInTheDocument() - expect(screen.queryByText(/Running AskUserQuestion/)).toBeNull() - expect(screen.queryByText(/AskUserQuestion/)).toBeNull() - }) - - it('reports a settled ask without the pulse or a tool-count header', () => { - const { container } = render( - - ) - - expect(screen.getByText('Asked:')).not.toHaveClass('animate-pulse') - expect(screen.getByText(QUESTION)).toBeInTheDocument() - expect(container.querySelector('[data-native-chat-tool-run-state]')).toBeNull() - }) - - it('keeps a question that fits on its line collapsed but expandable', () => { - render( - - ) - expect(screen.getByText(QUESTION)).toHaveClass('truncate') - expect(screen.getByRole('button', { name: /Asked:/ })).toHaveAttribute( - 'aria-expanded', - 'false' - ) - }) - - it('opens a clipped question below the toggle and folds it back', () => { - const restore = clipEveryLine() - try { - render( - - ) - const toggle = screen.getByRole('button', { name: /Asked:/ }) - expect(toggle).toHaveAttribute('aria-expanded', 'false') - - fireEvent.click(toggle) - expect(toggle).toHaveAttribute('aria-expanded', 'true') - const full = screen.getByText(QUESTION) - expect(full).not.toHaveClass('truncate') - // Outside the button, so it selects like prose and a click in it keeps it open. - expect(full.closest('button')).toBeNull() - fireEvent.click(full) - expect(toggle).toHaveAttribute('aria-expanded', 'true') - - fireEvent.click(toggle) - expect(toggle).toHaveAttribute('aria-expanded', 'false') - expect(screen.getByText(QUESTION)).toHaveClass('truncate') - } finally { - restore() - } - }) it('keeps an opened question open when the windowed row remounts', () => { const restore = clipEveryLine() @@ -244,51 +111,6 @@ import { i18n } from '@/i18n/i18n' restore() } }) - - it('offers no expansion when the row names only a question count', () => { - const restore = clipEveryLine() - try { - const input = { questions: [{ question: 'First?' }, { question: 'Second?' }] } - render( - - ) - expect(screen.getByText('2 questions')).toBeInTheDocument() - expect(screen.queryByRole('button', { name: /Asked:/ })).toBeNull() - } finally { - restore() - } - }) - - it('counts only the work that ran in the header beside the ask', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'Read', input: { file_path: 'a.ts' }, state: 'completed' }, - { type: 'tool-call', name: 'AskUserQuestion', input: ASK_INPUT, state: 'running' } - ] - - render() - - expect(screen.getByText('Awaiting user input:')).toBeInTheDocument() - // Waiting for an answer settles the work summary; the question itself adds no call count. - expect(screen.getByText('Read 1 file')).toBeInTheDocument() - expect(screen.queryByText('Reading 1 file')).toBeNull() - }) - - it('draws the row from the tool name when the payload names no question', () => { - render( - - ) - - expect(screen.getByText('Awaiting user input')).toBeInTheDocument() - expect(screen.queryByText(/request_user_input/)).toBeNull() - }) }) } @@ -463,57 +285,6 @@ import { i18n } from '@/i18n/i18n' fireEvent.click(returnedHeader) expect(membersBox(container).scrollTop).toBe(0) }) - - it('opens a settled run at its first call', () => { - layOut({ current: 600 }) - const { container } = render( - - ) - expect(membersBox(container).scrollTop).toBe(0) - }) - - it('brings a revealed member to the top of the box and reports the box', () => { - layOut({ current: 600 }) - const { container } = render( - - ) - const box = membersBox(container) - const member = box.querySelectorAll('button')[2]! - vi.spyOn(box, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 40, 300, VIEWPORT)) - vi.spyOn(member, 'getBoundingClientRect').mockReturnValue(new DOMRect(0, 340, 300, 26)) - - expect(revealNativeChatToolRunMember(member)).toBe(box) - expect(box.scrollTop).toBe(300) - }) - }) - - describe('NativeChatToolRun asides', () => { - afterEach(() => { - cleanup() - vi.restoreAllMocks() - }) - it('draws the rows it is handed among its calls, in order', () => { - const blocks = calls(2) - const { container } = render( - between

]]]), - after: [

last

] - }} - expandSignal - activeTurnIsWorking={false} - /> - ) - const rows = [...membersBox(container).querySelectorAll('button, p')].map( - (row) => row.textContent ?? '' - ) - expect(rows).toHaveLength(4) - expect(rows[0]).toContain('file-0.ts') - expect(rows[1]).toBe('between') - expect(rows[2]).toContain('file-1.ts') - expect(rows[3]).toBe('last') - }) }) } @@ -531,44 +302,6 @@ import { i18n } from '@/i18n/i18n' cleanup() await i18n.changeLanguage('en') }) - it.each(['Task', 'Agent'])('separates failure counts in command and %s runs', (name) => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'Bash', input: { command: 'false' }, state: 'failed' }, - { type: 'tool-result', output: 'exit 1', isError: true }, - { - type: 'tool-call', - name, - input: { description: 'explored settings search entries' }, - state: 'completed' - } - ] - const { container } = render() - const header = runHeader(container) - expect(header).toHaveTextContent('Ran 1 command and ran 1 agent · 1 failed') - expect(header).toHaveAccessibleName(/Failed tool calls: 1/) - expect(header).not.toHaveAccessibleName(/·/) - fireEvent.click(header) - expect(screen.getByText('Subagent')).toBeInTheDocument() - expect(screen.getByTitle('explored settings search entries')).toBeInTheDocument() - }) - - it('uses the agent glyph for a lone Agent transcript call', () => { - const { container } = render( - - ) - expect(runHeader(container)).toHaveTextContent('Ran 1 agent') - expect(container.querySelectorAll('button .lucide-bot')).toHaveLength(2) - }) it('relabels an already-mounted run and its rows when the UI language changes', async () => { const blocks: NativeChatBlock[] = [ diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx index dbf85af6af1..0fbb34ef8db 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.identity.test.tsx @@ -2,11 +2,7 @@ import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import { NativeChatToolRun } from './NativeChatToolRun' -import type { - NativeChatBlock, - NativeChatToolCallBlock, - NativeChatToolResultBlock -} from '../../../../shared/native-chat/native-chat-types' +import type { NativeChatToolCallBlock } from '../../../../shared/native-chat/native-chat-types' import { NativeChatDisclosureContext, useNativeChatDisclosures @@ -17,11 +13,6 @@ vi.mock('./NativeChatDiffCard', () => ({ NativeChatDiffCard: () => null })) vi.mock('./NativeChatDiffView', () => ({ NativeChatDiffView: () => null })) const disclosureWrite = vi.fn() -const capturedDisclosures = { - read: (_key: string) => undefined, - write: (key: string, open: boolean) => disclosureWrite(key, open), - runScroll: new Map() -} afterEach(() => { cleanup() @@ -37,12 +28,6 @@ const shell: NativeChatToolCallBlock = { durationMs: 400 } -const shellResult: NativeChatToolResultBlock = { - type: 'tool-result', - output: 'command not found', - isError: true -} - function ToolRunDisclosureHarness({ expandSignal }: { expandSignal: boolean }) { const disclosures = useNativeChatDisclosures() return ( @@ -73,175 +58,6 @@ describe('inline tool annotations', () => { screen.getByRole('button', { name: /missing-command/ }).getAttribute('aria-expanded') ).toBe('false') }) - - it('resynchronizes a standalone run when the toolbar signal flips', () => { - const { rerender } = render( - - ) - expect(screen.getByRole('button').getAttribute('aria-expanded')).toBe('false') - - rerender() - - expect( - screen.getAllByRole('button', { name: /missing-command/ })[0].getAttribute('aria-expanded') - ).toBe('true') - }) - - it('uses provider call identities for byte-identical line disclosure keys', () => { - const blocks: NativeChatBlock[] = [ - { ...shell, callId: 'call-a' }, - shellResult, - { ...shell, callId: 'call-b' }, - shellResult - ] - render( - - - - ) - - fireEvent.click(screen.getAllByRole('button')[2]!) - - expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith('line:message-1:call:call-b', true) - }) - - it('keeps occurrence identity as the fallback for calls without provider IDs', () => { - render( - - - - ) - - fireEvent.click(screen.getAllByRole('button')[2]!) - - expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith( - 'line:message-1:tool-call:shell:{"command":"missing-command"}:1', - true - ) - }) - - it('keeps occurrence identity for whitespace-only provider IDs', () => { - render( - - - - ) - - fireEvent.click(screen.getAllByRole('button')[2]!) - - expect(disclosureWrite).toHaveBeenCalledExactlyOnceWith( - 'line:message-1:tool-call:shell:{"command":"missing-command"}:1', - true - ) - }) - - it('keeps command completion annotations on the collapsed tool line', () => { - render( - - ) - expect(screen.getByText('exit 127').closest('button')).toBe( - screen.getByText('400ms').closest('button') - ) - expect(screen.getByText('exit 127').closest('button')?.getAttribute('aria-expanded')).toBe( - 'false' - ) - expect(screen.queryByText('0s')).toBeNull() - }) - it('renders a legacy command without invented metadata', () => { - render( - - ) - expect(screen.queryByText(/exit \d/)).toBeNull() - expect(screen.queryByText(/\d+ms/)).toBeNull() - }) - it('shows distinct MCP names while retaining the raw identifier', () => { - const name = 'mcp__linear__list_issues' - render() - expect(screen.getByText('Linear')).toBeTruthy() - expect(screen.getByText('list issues')).toBeTruthy() - expect(screen.getByTitle(name)).toBeTruthy() - }) - it('reveals safe result links only inside row disclosure and routes clicks through chat', () => { - const onLinkClick = vi.fn((event) => event.preventDefault()) - const block: NativeChatToolCallBlock = { - type: 'tool-call', - name: 'web_search', - input: { query: 'docs' }, - state: 'completed', - webSearchResults: [{ title: 'Reference docs', url: 'https://example.com/docs' }] - } - render() - expect(screen.queryByRole('link')).toBeNull() - fireEvent.click(screen.getByText('Searched the web').closest('button')!) - const link = screen.getByRole('link', { name: /Reference docs/ }) - expect(link.getAttribute('href')).toBe('https://example.com/docs') - expect(link.closest('button')).toBeNull() - fireEvent.click(link) - expect(onLinkClick).toHaveBeenCalledWith(expect.anything(), 'https://example.com/docs') - fireEvent(link, new MouseEvent('auxclick', { button: 1, bubbles: true })) - expect(onLinkClick).toHaveBeenCalledTimes(2) - }) -}) - -it.each(['tools/read', 'browser.open', 'package.lock', 'linear/list_issues'])( - 'keeps an ordinary tool name %s intact', - (name) => { - render() - expect(screen.getByText(name)).toBeTruthy() - expect(document.querySelector('.lucide-plug')).toBeNull() - } -) - -it.each(['running', 'completed'] as const)( - 'renders provider MCP identity on %s rows and headers', - (state) => { - const name = 'my_server/ns.tool' - render( - - ) - expect(screen.getByText('My server')).toBeTruthy() - expect(screen.getByText('ns.tool')).toBeTruthy() - expect(screen.getByTitle(name)).toBeTruthy() - // Run and member glyphs must agree on MCP identity. - expect(document.querySelectorAll('.lucide-plug')).toHaveLength(2) - } -) - -it.each([ - [{ exitCode: 0 }, 'exit 0', '400ms'], - [{ durationMs: 400 }, '400ms', 'exit 0'] -])('renders independently optional command metadata %j', (metadata, present, absent) => { - render( - - ) - expect(screen.getByText(present)).toBeTruthy() - expect(screen.queryByText(absent)).toBeNull() }) it('filters untrusted persisted result URLs at the renderer boundary', () => { diff --git a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx index 14f13ce9d1b..d43ddbab860 100644 --- a/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatToolRun.test.tsx @@ -13,15 +13,6 @@ import { openToolRunMembers } from './native-chat-tool-run-members-test-support' afterEach(cleanup) /** Read lucide classes so identity-changing glyph swaps remain observable. */ -function leadingGlyphs(container: HTMLElement): (string | null)[] { - return [...container.querySelectorAll('button')].map( - (button) => - button - .querySelector('svg') - ?.getAttribute('class') - ?.match(/lucide-[a-z0-9-]+/)?.[0] ?? null - ) -} /** The first button is the header; summary members render as separate pills. */ function runHeader(container: HTMLElement): HTMLElement { @@ -33,49 +24,6 @@ function runHeader(container: HTMLElement): HTMLElement { } describe('NativeChatToolRun', () => { - it('uses the shared clean label for a desktop tool row', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'Read', - input: '{"file_path":"src/index.ts","offset":10}' - } - ] - - render() - - expect(screen.getByTitle('src/index.ts')).toHaveTextContent('index.ts') - expect(screen.queryByTitle('{"file_path":"src/index.ts","offset":10}')).toBeNull() - }) - - it('renders structured apply_patch changes as a reviewable diff instead of JSON', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'apply_patch', - // This lane carries its patch on the call; provider completion proves the edit landed. - state: 'completed', - input: { - changes: [ - { - path: '/repo/src/app.ts', - kind: { type: 'update', move_path: null }, - diff: '@@ -1 +1 @@\n-before\n+after' - } - ] - } - } - ] - - const { container } = render() - openToolRunMembers() - - expect(screen.getByText('after')).toBeInTheDocument() - expect(screen.getByText('before')).toBeInTheDocument() - expect(screen.getByText('Edited')).toBeInTheDocument() - expect(container.querySelector('pre')).toBeNull() - }) - it('renders evidence-shaped projected patches as colored diffs without changes JSON', () => { const item: AgentJournalRenderItem = { itemId: 'apply-patch', @@ -134,103 +82,6 @@ describe('NativeChatToolRun', () => { expect(body).toHaveClass('text-destructive') }) - it('leaves a `git diff` command as a command row rather than an edit card', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'exec', input: { command: 'git diff' }, state: 'completed' }, - { - type: 'tool-result', - output: 'diff --git a/a.ts b/a.ts\n--- a/a.ts\n+++ b/a.ts\n@@ -1 +1 @@\n-was\n+now' - } - ] - - const { container } = render() - - const commandRow = screen.getByRole('button', { name: /^exec Ran git diff/ }) - expect(within(commandRow).queryByText('Edited')).toBeNull() - expect(container).toHaveTextContent('git diff') - }) - - it('shows no gutter number for a snippet edit, which cannot locate itself', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'Edit', - input: { file_path: '/repo/a.ts', old_string: 'was', new_string: 'now' }, - state: 'completed' - }, - { type: 'tool-result', output: 'ok' } - ] - - render() - openToolRunMembers() - - // Exact, because a snippet-relative number would sit ahead of the marker. - expect(screen.getByText('now').closest('div')?.textContent).toBe('+now') - expect(screen.getByText('was').closest('div')?.textContent).toBe('-was') - }) - - it('separates two regions of a file so the gutter jump is accounted for', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'Edit', - input: { file_path: '/repo/a.ts' }, - state: 'completed' - }, - { - type: 'tool-result', - output: 'ok', - editPatch: { - filePath: '/repo/a.ts', - hunks: [ - { oldStart: 42, oldLines: 1, newStart: 42, newLines: 1, lines: ['-was', '+now'] }, - { oldStart: 310, oldLines: 1, newStart: 310, newLines: 1, lines: ['-old', '+new'] } - ] - } - } - ] - - render() - openToolRunMembers() - - const separators = screen.getAllByRole('separator') - expect(separators).toHaveLength(1) - expect(separators[0]).toHaveAccessibleName('Lines not shown') - }) - - it('offers no empty body for a delete, which names the file and nothing else', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'apply_patch', - input: { input: '*** Begin Patch\n*** Delete File: gone.ts\n*** End Patch' }, - state: 'completed' - } - ] - - render() - - expect(screen.getByTitle('gone.ts')).toBeInTheDocument() - expect(screen.getByText('Deleted').closest('button')).not.toHaveAttribute('aria-expanded') - }) - - it('says a diff was clipped even while the card is collapsed', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'Diff', - input: { path: 'src/a.ts' }, - state: 'completed' - }, - { type: 'tool-result', output: '@@ -1,3 +1,3 @@\n ctx\n-was\n+now\n… (48210 bytes)' } - ] - - render() - - expect(screen.getByText('Diff truncated')).toBeInTheDocument() - expect(screen.queryByText('was')).toBeNull() - }) - it('copies the diff as signed rows, with the region breaks left out', () => { const writeClipboardText = vi.fn() Object.assign(window, { api: { ui: { writeClipboardText } } }) @@ -260,151 +111,6 @@ describe('NativeChatToolRun', () => { expect(writeClipboardText).toHaveBeenCalledWith(' ctx\n-was\n+now\n+tail') }) - describe('reading a batch as a group', () => { - const batch: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'mcp__linear__list_issues', - input: { query: 'todo' }, - state: 'completed', - mcpIdentity: { server: 'linear', tool: 'list_issues' } - }, - { type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' }, - { type: 'tool-call', name: 'read', input: { file_path: 'README.md' }, state: 'completed' } - ] - - // Summarize categories; separators also occur inside tool names and make joined call lists ambiguous. - it('summarizes a run by category, in the order the run first used each one', () => { - const { container } = render() - - expect(runHeader(container)).toHaveTextContent( - 'Used 1 integration, ran 1 command, and read 1 file' - ) - }) - - it('names no individual call in the header, however wide the run', () => { - const { container } = render() - - const header = runHeader(container) - expect(header).not.toHaveTextContent('mcp__linear__list_issues') - expect(header).not.toHaveTextContent('ls -la') - expect(header).not.toHaveTextContent('README.md') - }) - - it('counts every call rather than capping the list and marking a remainder', () => { - const wide: NativeChatBlock[] = [ - ...batch, - { type: 'tool-call', name: 'Grep', input: { pattern: 'todo' }, state: 'completed' }, - { type: 'tool-call', name: 'Write', input: { file_path: 'a.ts' }, state: 'completed' } - ] - - const { container } = render() - - expect(runHeader(container)).not.toHaveTextContent('more') - expect(runHeader(container)).toHaveTextContent( - 'Used 1 integration, ran 1 command, read 1 file, searched 1 time, and edited 1 file' - ) - }) - - it('joins exactly two categories with "and", and no comma', () => { - const pair: NativeChatBlock[] = [ - { type: 'tool-call', name: 'Bash', input: { command: 'ls' }, state: 'completed' }, - { type: 'tool-call', name: 'Bash', input: { command: 'pwd' }, state: 'completed' }, - { type: 'tool-call', name: 'read', input: { file_path: 'a.ts' }, state: 'completed' } - ] - - const { container } = render() - - expect(runHeader(container)).toHaveTextContent('Ran 2 commands and read 1 file') - }) - - it('prints the split MCP name on the opened row', () => { - render() - - expect(screen.getByText('Linear')).toBeInTheDocument() - }) - - it('gives a run whose tool it cannot name the generic category', () => { - const { container } = render( - - ) - - expect(runHeader(container)).toHaveTextContent('Used 1 tool') - }) - - it('keeps a lone command as the header, unwrapped from its login shell', () => { - const { container } = render( - - ) - - expect(runHeader(container)).toHaveTextContent('git push --force-with-lease') - expect(runHeader(container)).not.toHaveTextContent('/bin/zsh') - }) - }) - - it('keeps a grouped active run to one stable row showing only the latest tool', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'shell', input: { command: 'date' }, state: 'completed' }, - { type: 'tool-call', name: 'shell', input: { command: 'pwd' }, state: 'completed' }, - { type: 'tool-call', name: 'shell', input: { command: 'cat package.json' }, state: 'running' } - ] - - const { container } = render() - - const header = runHeader(container) - expect(header).toHaveTextContent('Running 3 commands') - expect(header).toHaveTextContent('cat package.json') - expect(header).not.toHaveTextContent('date') - expect(header).not.toHaveTextContent('pwd') - expect(screen.getByText('Running 3 commands')).toHaveClass( - 'animate-pulse', - 'motion-reduce:animate-none' - ) - expect(header.querySelector('.lucide-check')).toBeNull() - expect(container.querySelector('.animate-spin')).toBeNull() - }) - - it('treats legacy tool calls without lifecycle state as active while the turn works', () => { - render( - - ) - - expect(screen.getByText('Running 1 command')).toBeInTheDocument() - expect(screen.getByText('sleep 5')).toBeInTheDocument() - }) - - it('keeps a completed tool payload collapsed until the run is expanded', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'shell', - input: { command: 'printf hello' }, - state: 'completed' - }, - { type: 'tool-result', output: 'hello' } - ] - - render() - expect(screen.queryByText('hello')).toBeNull() - }) - // Call transitions must preserve one turn-owned header, its count, hover and status mark. it('keeps one header element from a call starting until its turn ends', () => { const running: NativeChatBlock[] = [ @@ -451,109 +157,6 @@ describe('NativeChatToolRun', () => { expect(header.querySelector('.animate-pulse')).toBeNull() }) - it('settles a run the agent has moved past even while its last call still reports', () => { - const { container } = render( - - ) - - const header = runHeader(container) - expect(header).toHaveAttribute('data-native-chat-tool-run-state', 'settled') - expect(header).not.toHaveTextContent('Running') - expect(header.querySelector('.animate-pulse')).toBeNull() - expect(header.querySelector('.lucide-check')).toBeNull() - }) - - it('never animates a settled tool row with its completion check', () => { - const { container } = render( - - ) - - const settledRow = runHeader(container) - expect(settledRow).toHaveTextContent('pnpm test') - expect(settledRow.querySelector('.lucide-check')).toBeInTheDocument() - // Windowing remounts settled rows on scroll; a mark that faded in would replay. - expect(settledRow.querySelector('.lucide-check')).not.toHaveClass('animate-in') - expect(settledRow.querySelector('.animate-pulse')).toBeNull() - expect(container.querySelector('.animate-pulse')).toBeNull() - }) - - it('refuses the completion mark to a collapsed run whose call failed', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'shell', input: { command: 'false' }, state: 'failed' }, - { type: 'tool-result', output: 'exit 1', isError: true } - ] - - const { container } = render() - - expect(container.querySelector('.lucide-check')).toBeNull() - expect(runHeader(container)).toHaveTextContent(' · 1 failed') - expect(runHeader(container)).toHaveAccessibleName(/Failed tool calls: 1/) - expect(container.querySelector('.lucide-circle-alert')).toBeNull() - expect(container.querySelector('[class*="destructive"]')).toBeNull() - expect(screen.queryByText('exit 1')).toBeNull() - }) - - it('counts every failed call in a run, not just the last one', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'shell', input: { command: 'a' }, state: 'failed' }, - { type: 'tool-result', output: 'exit 1', isError: true }, - { type: 'tool-call', name: 'shell', input: { command: 'b' }, state: 'failed' }, - { type: 'tool-result', output: 'exit 2', isError: true }, - { type: 'tool-call', name: 'shell', input: { command: 'c' }, state: 'completed' }, - { type: 'tool-result', output: 'ok' } - ] - - const { container } = render() - - expect(runHeader(container)).toHaveTextContent('2 failed') - expect(container.querySelector('.lucide-check')).toBeNull() - }) - - it('says nothing and keeps the mark when every call in the run succeeded', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'shell', input: { command: 'a' }, state: 'completed' }, - { type: 'tool-result', output: 'ok' } - ] - - const { container } = render() - - expect(runHeader(container)).not.toHaveTextContent('failed') - expect(container.querySelector('.lucide-check')).toBeInTheDocument() - }) - - // Keep output behind each call’s disclosure to avoid doubling expanded runs with Result rows. - it('keeps a call output behind the call row, not beside it', () => { - const blocks: NativeChatBlock[] = [ - { type: 'tool-call', name: 'shell', input: { command: 'git log -1' }, state: 'failed' }, - { type: 'tool-result', output: 'exit 128', isError: true } - ] - - render() - - expect(screen.queryByText('Result')).toBeNull() - expect(screen.queryByText('exit 128')).toBeNull() - - // Click the member, since this single-call header names the same command. - const rows = screen.getAllByText('git log -1') - fireEvent.click(rows.at(-1)!.closest('button')!) - - expect(screen.getByText('exit 128')).toBeInTheDocument() - }) - it('keeps a post-turn running call neutral until the item itself settles', () => { const blocks: NativeChatBlock[] = [ { type: 'tool-call', name: 'shell', input: { command: 'sleep 1' }, state: 'running' } @@ -578,284 +181,7 @@ describe('NativeChatToolRun', () => { expect(container.querySelector('.lucide-check')).toBeInTheDocument() }) - it('shows the category glyph beside the word a classified row is named by', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'read', - input: { command: "sed -n '1,200p' notes.txt", path: 'notes.txt' }, - state: 'completed' - } - ] - - const { container } = render() - - const glyph = container.querySelector('.lucide-eye') - expect(glyph).toBeInTheDocument() - expect(glyph).toHaveAttribute('aria-hidden') - expect(screen.getByText('Read')).toBeInTheDocument() - }) - - it('holds one glyph for a category across running, completed, and failed', () => { - const searchCall = (state: 'running' | 'completed' | 'failed'): NativeChatBlock[] => [ - { type: 'tool-call', name: 'search', input: { query: 'beta' }, state } - ] - const { container, rerender } = render( - - ) - - expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search']) - - for (const settled of ['completed', 'failed'] as const) { - rerender( - - ) - - expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search']) - } - }) - - it('falls back to the generic tool glyph, not the terminal, for an unmodelled row', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'CreateWidget', - input: { prompt: 'which?' }, - state: 'completed' - } - ] - - const { container } = render() - - expect(container.querySelector('.lucide-square-terminal')).toBeNull() - expect(container.querySelector('.lucide-wrench')).toBeInTheDocument() - }) - - it('agrees between the header and the row it names for an unmodelled tool', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'CreateWidget', - input: { prompt: 'which?' }, - state: 'completed' - } - ] - - const { container } = render() - - expect(leadingGlyphs(container)).toEqual(['lucide-wrench', 'lucide-wrench']) - }) - - // Unpaired results have translated labels rather than a tool name to classify. - it('leaves an unpaired result row without a category glyph', () => { - const blocks: NativeChatBlock[] = [{ type: 'tool-result', output: 'first line' }] - - const { container } = render() - - expect(runHeader(container)).toHaveAccessibleName('Result') - const resultRow = screen.getByRole('button', { name: 'Result first line' }) - // Keying a category off 'Result' would resolve a different glyph per locale. - expect( - [...(resultRow?.querySelectorAll('svg') ?? [])].map( - (svg) => svg.getAttribute('class')?.match(/lucide-[a-z0-9-]+/)?.[0] - ) - ).toEqual(['lucide-chevron-right']) - }) - - it('heads a projected diff run with the file-change glyph, not the generic one', () => { - const projected = projectStructuredItemToNativeChat({ - itemId: 'file-change', - revision: 1, - sequence: 1, - observedAt: 1, - body: { - kind: 'diff', - path: 'src/a.ts', - patch: { - head: '@@ -1 +1 @@\n-was\n+now', - truncated: false, - byteLength: 24, - digest: 'a'.repeat(64) - } - } - }) - - const { container } = render( - - ) - - expect(container.querySelector('.lucide-pencil')).toBeInTheDocument() - expect(container.querySelector('.lucide-wrench')).toBeNull() - }) - - describe('the settled header glyph over a whole run', () => { - // The header glyph describes the summarized run, not its last-finishing call. - const call = (name: string, input: unknown): NativeChatBlock => ({ - type: 'tool-call', - name, - input, - state: 'completed' - }) - - it('heads a run that is all reads with the read glyph', () => { - const blocks: NativeChatBlock[] = [ - call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' }), - call('read', { command: "sed -n '1,50p' b.ts", path: 'b.ts' }) - ] - - const { container } = render( - - ) - - expect(leadingGlyphs(container)).toEqual(['lucide-eye', 'lucide-eye', 'lucide-eye']) - }) - - it('heads a run that is all shell with the terminal glyph, whatever each is named', () => { - const blocks: NativeChatBlock[] = [ - call('shell', { command: 'npm test' }), - call('Bash', { command: 'git status' }) - ] - - const { container } = render( - - ) - - expect(leadingGlyphs(container)).toEqual([ - 'lucide-square-terminal', - 'lucide-square-terminal', - 'lucide-square-terminal' - ]) - }) - - it('heads a run spanning categories with the generic tool glyph', () => { - const blocks: NativeChatBlock[] = [ - call('shell', { command: 'npm test' }), - call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' }) - ] - - const { container } = render( - - ) - - expect(leadingGlyphs(container)).toEqual([ - 'lucide-wrench', - 'lucide-square-terminal', - 'lucide-eye' - ]) - }) - - it('heads a single-call run with that call\u2019s own glyph', () => { - const { container } = render( - - ) - - expect(leadingGlyphs(container)).toEqual(['lucide-search', 'lucide-search']) - }) - - it.each([1, 3])('labels a run of %s unpaired results without inventing tool calls', (count) => { - const blocks: NativeChatBlock[] = Array.from({ length: count }, (_, index) => ({ - type: 'tool-result', - output: `first line ${index}` - })) - - const { container } = render( - - ) - - expect(runHeader(container)).toHaveAccessibleName('Result') - expect(runHeader(container)).not.toHaveTextContent(/tool calls?/) - - expect(leadingGlyphs(container)).toEqual([ - 'lucide-check', - ...blocks.map(() => 'lucide-chevron-right') - ]) - }) - - it('holds the run glyph across live and settled', () => { - const blocks: NativeChatBlock[] = [ - call('read', { command: "sed -n '1,50p' a.ts", path: 'a.ts' }), - { type: 'tool-call', name: 'shell', input: { command: 'npm test' }, state: 'running' } - ] - - const { container, rerender } = render( - - ) - - // Keep the run’s identity glyph across state transitions instead of using the latest call’s category. - expect(leadingGlyphs(container)[0]).toBe('lucide-wrench') - rerender() - expect(leadingGlyphs(container)[0]).toBe('lucide-wrench') - }) - }) - - it('labels a bare list row by the command it ran rather than an invented path', () => { - const blocks: NativeChatBlock[] = [ - { - type: 'tool-call', - name: 'list', - input: { command: 'ls', cwd: '/repo' }, - state: 'completed' - } - ] - - const { container } = render() - - expect(container.querySelector('.lucide-folder')).toBeInTheDocument() - expect(screen.getByTitle('ls')).toHaveTextContent('ls') - }) - // Unnamed group-hover matches the surrounding message too, revealing every member caret at once. - describe('hover reveal', () => { - const run: NativeChatBlock[] = [ - { type: 'tool-call', name: 'Bash', input: { command: 'ls -la' }, state: 'completed' }, - { type: 'tool-result', output: 'a\nb' }, - { type: 'tool-call', name: 'Bash', input: { command: 'pwd' }, state: 'completed' }, - { type: 'tool-result', output: '/tmp' } - ] - - /** Read class attributes because SVG className is an SVGAnimatedString. */ - function hoverRevealed(container: HTMLElement): { element: Element; classes: string }[] { - return [...container.querySelectorAll('[class*="group-hover"]')].map((element) => ({ - element, - classes: element.getAttribute('class') ?? '' - })) - } - - it('scopes a row’s reveal to that row rather than the whole message', () => { - // Open run, collapsed children — the state the turn caret leaves behind. - const { container } = render() - - const revealed = hoverRevealed(container) - expect(revealed.length).toBeGreaterThan(0) - revealed.forEach(({ classes }) => { - expect(classes).not.toMatch(/(^|\s)group-hover:/) - }) - }) - - it('puts every reveal inside the row button that governs it', () => { - const { container } = render() - - hoverRevealed(container).forEach(({ element, classes }) => { - const scope = /group-hover\/([a-z-]+):/.exec(classes)?.[1] - expect(scope).toBeDefined() - expect(element.closest(`.group\\/${scope}`)).not.toBeNull() - }) - }) - - it('keeps every row’s chevron in view at the row’s right edge', () => { - const { container } = render() - - const chevrons = [...container.querySelectorAll('.border-l button svg.lucide-chevron-right')] - expect(chevrons.length).toBeGreaterThan(1) - chevrons.forEach((chevron) => { - expect(chevron.getAttribute('class')).not.toContain('opacity-0') - }) - }) - }) }) describe('NativeChatToolRun task lists', () => { diff --git a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx index 2c1aac283eb..f0eb86ec481 100644 --- a/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatTranscriptChrome.test.tsx @@ -1,7 +1,7 @@ // @vitest-environment happy-dom import { act, createElement } from 'react' -import { fireEvent, screen, within } from '@testing-library/react' + import { createRoot } from 'react-dom/client' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types' @@ -146,103 +146,6 @@ describe('NativeChatImageAttachments', () => { } }) - it('pools visibility observation across image refs', async () => { - class FakeIntersectionObserver { - static instances: FakeIntersectionObserver[] = [] - readonly observe = vi.fn() - readonly unobserve = vi.fn() - readonly disconnect = vi.fn() - - constructor(_callback: IntersectionObserverCallback) { - FakeIntersectionObserver.instances.push(this) - } - } - vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver) - - const container = document.createElement('div') - const root = createRoot(container) - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks: [ - { type: 'image-ref' as const, path: '/repo/one.png' }, - { type: 'image-ref' as const, path: '/repo/two.png' }, - { type: 'image-ref' as const, path: '/repo/three.png' } - ], - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - - expect(FakeIntersectionObserver.instances).toHaveLength(1) - expect(FakeIntersectionObserver.instances[0]?.observe).toHaveBeenCalledTimes(3) - - root.unmount() - expect(FakeIntersectionObserver.instances[0]?.unobserve).toHaveBeenCalledTimes(3) - expect(FakeIntersectionObserver.instances[0]?.disconnect).toHaveBeenCalledOnce() - }) - - it('offers the full-size file of a sent image to the chat copy menu', async () => { - const container = document.createElement('div') - const root = createRoot(container) - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }], - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - - expect(container.querySelector('button')?.getAttribute('data-native-chat-copy-image-src')).toBe( - 'blob:owner-1' - ) - - root.unmount() - }) - - it.each([ - 'data:image/png;base64,AA==', - 'blob:sent-original', - 'http://example.test/original.png', - 'https://example.test/original.svg' - ])( - 'offers the original displayed source %s in the thumbnail and full-size preview', - async (src) => { - const container = document.body.appendChild(document.createElement('div')) - const root = createRoot(container) - try { - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks: [{ type: 'image-ref', url: src, alt: 'Sent image' }], - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - const thumbnail = within(container).getByRole('button', { name: 'View image: Sent image' }) - expect(thumbnail.getAttribute('data-native-chat-copy-image-src')).toBe(src) - fireEvent.click(thumbnail) - const preview = screen.getByRole('dialog', { name: 'Sent image' }) - expect( - within(preview) - .getByTestId('pan-zoom-image-canvas') - .closest('[data-native-chat-copy-image-src]') - ?.getAttribute('data-native-chat-copy-image-src') - ).toBe(src) - expect(window.api.fs.readFile).not.toHaveBeenCalled() - fireEvent.click(within(preview).getByRole('button', { name: 'Close' })) - expect(screen.queryByRole('dialog')).toBeNull() - } finally { - await act(async () => root.unmount()) - container.remove() - } - } - ) - it('preserves same-image errors but retries when the runtime owner changes', async () => { const container = document.createElement('div') const root = createRoot(container) @@ -292,62 +195,6 @@ describe('NativeChatImageAttachments', () => { root.unmount() }) - it('keeps the shown image when an equal runtime context is rebuilt', async () => { - const container = document.createElement('div') - const root = createRoot(container) - const blocks = [{ type: 'image-ref' as const, path: '/repo/image.png' }] - - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks, - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - const img = container.querySelector('img') - expect(img?.getAttribute('src')).toBe('blob:owner-1') - - for (let update = 0; update < 3; update += 1) { - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks, - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - } - - expect(URL.revokeObjectURL).not.toHaveBeenCalled() - expect(window.api.fs.readFile).toHaveBeenCalledOnce() - expect(container.querySelector('img')).toBe(img) - expect(img?.getAttribute('src')).toBe('blob:owner-1') - - root.unmount() - }) - - it('shows a remounted row from the cache without re-reading the image', async () => { - const props = { - blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }], - runtimeContext: runtimeContext('wt-1') - } - for (let mount = 0; mount < 2; mount += 1) { - const container = document.createElement('div') - const root = createRoot(container) - await act(async () => { - root.render(createElement(NativeChatImageAttachments, props)) - await flushPromises() - }) - expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-1') - await act(async () => root.unmount()) - } - expect(window.api.fs.readFile).toHaveBeenCalledOnce() - expect(URL.revokeObjectURL).not.toHaveBeenCalled() - }) - it('retries a failed thumbnail after the image cache refreshes', async () => { const container = document.createElement('div') const root = createRoot(container) @@ -375,70 +222,4 @@ describe('NativeChatImageAttachments', () => { expect(container.querySelector('img')?.getAttribute('src')).toBe('blob:owner-2') root.unmount() }) - - it('keeps the observed element stable while a preview is materialized', async () => { - let callback: IntersectionObserverCallback | undefined - class FakeIntersectionObserver { - readonly observe = vi.fn() - readonly unobserve = vi.fn() - readonly disconnect = vi.fn() - - constructor(nextCallback: IntersectionObserverCallback) { - callback = nextCallback - } - } - vi.stubGlobal('IntersectionObserver', FakeIntersectionObserver) - - const container = document.createElement('div') - const root = createRoot(container) - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks: [{ type: 'image-ref' as const, path: '/repo/image.png' }], - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - - const observedElement = container.firstElementChild - expect(observedElement).not.toBeNull() - if (!observedElement || !callback) { - throw new Error('image preview did not register visibility observation') - } - const notifyVisibility = callback - await act(async () => { - notifyVisibility( - [{ target: observedElement, isIntersecting: true } as IntersectionObserverEntry], - {} as IntersectionObserver - ) - await flushPromises() - }) - - expect(container.firstElementChild).toBe(observedElement) - root.unmount() - }) - - it.each([ - ['a pasted screenshot in the temp folder', '/tmp/orca-paste-1.png'], - ['an agent image outside the project', '/Users/me/.codex/generated/plot.png'] - ])('reads %s as a chat image, whoever sent it', async (_label, path) => { - const container = document.createElement('div') - const root = createRoot(container) - await act(async () => { - root.render( - createElement(NativeChatImageAttachments, { - blocks: [{ type: 'image-ref' as const, path }], - runtimeContext: runtimeContext('wt-1') - }) - ) - await flushPromises() - }) - - expect(vi.mocked(window.api.fs.readFile).mock.calls[0]?.[0]).toMatchObject({ - filePath: path, - access: { kind: 'chat-image' } - }) - root.unmount() - }) }) diff --git a/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx b/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx index a4b1e1071c8..c037a6970c0 100644 --- a/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatTurnDiffRollup.test.tsx @@ -3,9 +3,9 @@ import '@testing-library/jest-dom/vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { afterEach, beforeEach, expect, it, vi } from 'vitest' import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types' -import { nativeChatTurnDiffs } from '../../../../shared/native-chat/tools/native-chat-turn-diffs' + import { TooltipProvider } from '../ui/tooltip' -import { NativeChatTurnDiffRollup } from './NativeChatTurnDiffRollup' + import { NativeChatMessageList } from './NativeChatMessageList' import { session, stubLayout, stubResizeObserver } from './native-chat-windowing-test-harness' @@ -96,29 +96,3 @@ it('keeps landed files visible after work folds, opens a patch in one click and expect(screen.getByRole('button', { name: /status.ts/ })).toBeVisible() expect(screen.queryByText('diagnostic-output')).toBeNull() }) - -it('mounts a bounded file prefix and no patch bodies until a file is selected', () => { - const many: NativeChatMessage[] = Array.from({ length: 200 }, (_, index) => ({ - id: `e${index}`, - role: 'assistant', - source: 'transcript', - timestamp: index, - blocks: [ - { type: 'tool-call', name: 'Diff', input: { path: `file-${index}.ts` } }, - { type: 'tool-result', output: `@@ -1 +1 @@\n-old-${index}\n+new-${index}` } - ] - })) - const diff = nativeChatTurnDiffs( - many, - many.map(() => 'u') - ).get('u')! - const { container } = render() - expect(screen.getAllByRole('button', { name: /file-\d+.ts/ })).toHaveLength(20) - expect(container.querySelectorAll('[data-native-chat-code-content]')).toHaveLength(0) - fireEvent.click(screen.getByRole('button', { name: /file-0.ts/ })) - expect(screen.getByText('new-0')).toBeVisible() - expect(screen.queryByText('new-1')).toBeNull() - expect(container.querySelectorAll('[data-native-chat-code-content]')).toHaveLength(1) - fireEvent.click(screen.getByRole('button', { name: /Show 180 more/ })) - expect(screen.getByRole('button', { name: /file-199.ts/ })).toBeVisible() -}) diff --git a/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx b/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx index 7aeb9b96862..d616995c84c 100644 --- a/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx +++ b/src/renderer/src/components/native-chat/NativeChatUsageLimitActions.test.tsx @@ -7,13 +7,12 @@ import { NativeChatUsageLimitActionsProvider } from './NativeChatUsageLimitActio import { NativeChatNoticeRow } from './NativeChatNoticeRow' import type { NativeChatOrcaStopView } from './native-chat-orca-stop-context' import { TooltipProvider } from '@/components/ui/tooltip' -import { useAppStore } from '@/store' -import { buildAgentSessionContinuationPrompt } from '@/lib/agent-session-continuation' + import type { NativeChatMessage } from '../../../../shared/native-chat/native-chat-types' import type { AgentType } from '../../../../shared/agent-status/contracts/agent-status-types' import type { RuntimeClientTarget } from '../../../../shared/runtime-client-target' import type { AgentSessionStatusSummary } from '../../../../shared/agent-session/agent-session-wire' -import { useNativeChatUsageLimitAutoContinue } from './use-native-chat-usage-limit-auto-continue' +import type { useNativeChatUsageLimitAutoContinue } from './use-native-chat-usage-limit-auto-continue' const mocks = vi.hoisted(() => ({ readSnapshot: { @@ -184,104 +183,6 @@ function expectInlineActions(accountName: string | null) { expect(account.parentElement).toHaveClass('w-32', 'shrink-0') } } -function snapshot(count: number) { - return { - claude: { - activeAccountId: 'a0', - accounts: Array.from({ length: count }, (_, index) => ({ - id: `a${index}`, - email: `account${index}@example.com`, - authMethod: 'subscription-oauth', - createdAt: 1, - updatedAt: 1, - lastAuthenticatedAt: 1 - })) - } - } -} - -it('offers host-owned recovery actions for the current usage-limit cut', async () => { - const target: RuntimeClientTarget = { kind: 'environment', environmentId: 'chat-host' } - mocks.snapshot.mockResolvedValue(snapshot(2)) - mocks.select.mockResolvedValue(snapshot(2).claude) - const original = useAppStore.getState() - const openSettingsTarget = vi.fn() - useAppStore.setState({ openSettingsTarget, openSettingsPage: vi.fn() }) - try { - const view = render( - row(target, true, 'claude', [], { readCurrent: false, canSwitchAccount: undefined }) - ) - expectInlineActions('Switch Account') - expect(screen.getByRole('button', { name: 'Switch Account' })).toBeDisabled() - expect(screen.getByRole('button', { name: 'Hand Off to Another Agent' })).toBeDisabled() - view.rerender(row(target)) - expect(screen.getByRole('button', { name: 'Switch Account' })).toBeDisabled() - await waitFor(() => - expect(screen.getByRole('button', { name: 'Switch Account' })).toBeEnabled() - ) - expectInlineActions('Switch Account') - fireEvent.pointerDown(screen.getByRole('button', { name: 'Switch Account' }), { - button: 0, - ctrlKey: false, - pointerType: 'mouse' - }) - fireEvent.click(await screen.findByRole('menuitem', { name: 'account1@example.com' })) - await waitFor(() => - expect(mocks.select).toHaveBeenCalledWith(target, { - accountId: 'a1', - runtime: 'host', - wslDistro: null - }) - ) - fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' })) - await waitFor(() => expect(mocks.dialog).toHaveBeenCalled()) - expect(mocks.dialog.mock.lastCall?.[0].request).toMatchObject({ - worktreeId: 'folder', - avoidSourceAgent: true, - launchSource: 'native_chat', - source: { transcriptPath: '/work/chat.jsonl', sourceAgent: 'claude', sourceTitle: 'My chat' } - }) - view.rerender(row(target, false)) - expect(screen.queryByRole('button', { name: 'Continue Now' })).toBeNull() - expect(screen.queryByText('Continue this chat without waiting:')).toBeNull() - cleanup() - mocks.snapshot.mockResolvedValue(snapshot(1)) - render(row(target)) - await waitFor(() => expect(screen.queryByRole('button', { name: /Account/ })).toBeNull()) - expect( - screen.getByText('Continue this chat without waiting:').parentElement?.querySelector('.w-32') - ).toHaveClass('empty:hidden') - const watcher = mocks.watch.mock.lastCall?.[1] - watcher.onSnapshot(snapshot(2)) - await waitFor(() => - expect(screen.getByRole('button', { name: 'Switch Account' })).toBeEnabled() - ) - expectInlineActions('Switch Account') - cleanup() - const local: RuntimeClientTarget = { kind: 'local' } - render(row(local)) - fireEvent.click(await screen.findByRole('button', { name: 'Add Account…' })) - expect(openSettingsTarget).toHaveBeenCalledWith({ - pane: 'accounts', - repoId: null, - sectionId: 'accounts-claude', - hostScope: local, - intent: 'native-accounts' - }) - cleanup() - mocks.follows = false - render(row(target, true, 'codex')) - expect(screen.getByRole('button', { name: 'Hand Off to Another Agent' })).toBeEnabled() - expect(screen.queryByRole('button', { name: /Account/ })).toBeNull() - expectInlineActions(null) - } finally { - useAppStore.setState({ - openSettingsPage: original.openSettingsPage, - openSettingsTarget: original.openSettingsTarget - }) - } -}) - it('admits one preparation and discards it after Continue supersedes the cut', async () => { let answer: (value: typeof mocks.readSnapshot) => void = () => {} mocks.options.mockImplementation( @@ -311,72 +212,3 @@ it('admits one preparation and discards it after Continue supersedes the cut', a fireEvent.click(screen.getByText('Cancel hand-off')) expect(screen.queryByText('Cancel hand-off')).toBeNull() }) - -it('hands off database-backed chats using labelled recent messages when no native file exists', async () => { - mocks.options.mockResolvedValue({ canSwitchAccount: false }) - render( - row({ kind: 'local' }, true, 'opencode', [ - { - id: 'recent', - role: 'user', - timestamp: 1, - source: 'transcript', - blocks: [{ type: 'text', text: 'Keep the recent task context' }] - } - ]) - ) - fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' })) - await waitFor(() => expect(mocks.dialog).toHaveBeenCalled()) - const source = mocks.dialog.mock.lastCall?.[0].request.source - expect(source.transcriptPath).toBeUndefined() - expect(buildAgentSessionContinuationPrompt(source, 'focused')).toContain( - 'A saved session transcript was unavailable, so use these recent chat messages:' - ) - expect(source.capturedText).toContain('user: Keep the recent task context') - expect(buildAgentSessionContinuationPrompt(source, 'full')).toBeNull() -}) - -it('withdraws the source schedule after hand-off starts without waiting for cancellation', async () => { - const target: RuntimeClientTarget = { kind: 'environment', environmentId: 'chat-host' } - const resetsAt = 1_800_000_000_000 - vi.useFakeTimers({ toFake: ['Date'] }) - vi.setSystemTime(resetsAt - 60_000) - status.set('chat', { usageLimitContinue: { turnItemId: 'cut', resetsAt, state: 'scheduled' } }) - mocks.follows = false - function ScheduledRow() { - const automatic = useNativeChatUsageLimitAutoContinue(target, 'chat', { - turnItemId: 'cut', - resetsAt, - turnEndedAt: resetsAt - 120_000 - }) - return row(target, true, 'codex', [], undefined, automatic) - } - const view = render() - fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' })) - fireEvent.click(await screen.findByText('Cancel hand-off')) - expect( - mocks.options.mock.calls.some((call) => call[1] === 'agentSession.cancelUsageLimitContinue') - ).toBe(false) - fireEvent.click(screen.getByRole('button', { name: 'Hand Off to Another Agent' })) - await screen.findByText('Start hand-off') - let finish = () => {} - mocks.options.mockImplementation( - () => - new Promise((resolve) => { - finish = () => resolve({}) - }) - ) - fireEvent.click(screen.getByText('Start hand-off')) - expect(screen.queryByText('Cancel hand-off')).toBeNull() - expect(mocks.options).toHaveBeenLastCalledWith(target, 'agentSession.cancelUsageLimitContinue', { - sessionId: 'chat', - turnItemId: 'cut', - resetsAt - }) - status = new Map([ - ['chat', { usageLimitContinue: { turnItemId: 'cut', resetsAt, state: 'cancelled' } }] - ]) - view.rerender() - expect(screen.getByText("Orca won't continue this chat automatically.")).toBeInTheDocument() - finish() -})