// @vitest-environment jsdom /** Trajectory ledger selection, details, status, and fold behavior. */ import { afterEach, describe, expect, it, vi } from 'vitest' import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx' import type { TrajectoryTurnModel } from '../src/client/layout.ts' afterEach(cleanup) const TURNS: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', description: '1.5s bash×2', cells: [ { index: 1, kind: 'message', text: 'Checking files', outputDetail: 'Checking files', input: 10, output: 20, think: 5, timeSeconds: 1.5, assistantMetrics: { timingRecorded: true, stepStartTime: 1_000, firstTokenTime: 1_500, completedTime: 2_500, usageProvided: true, outputTokens: 20, }, }, { index: 2, kind: 'tool', text: 'bash · {"command":"pwd"}', inputDetail: '{"command":"pwd"}', timeSeconds: null, }, { index: 3, kind: 'tool', text: 'bash · {"command":"false"}', inputDetail: '{"command":"false"}', outputDetail: 'ToolError: non_zero_exit', result: 'non_zero_exit', isError: true, timeSeconds: 0.2, }, ], }], }] const FOLD_PROPS = { collapsedTurns: new Set(), onToggleTurn: () => {}, collapsedAssistants: new Set(), onToggleAssistant: () => {}, } describe('TrajectoryTable', () => { it('shows assistant timing facts after keyboard selection', () => { render() fireEvent.keyDown(screen.getByRole('row', { name: /ASSISTANT/ }), { key: 'Enter' }) fireEvent.click(screen.getByRole('button', { name: 'Timing' })) expect(screen.getByText('500 ms')).toBeTruthy() expect(screen.getByText('1.00 s')).toBeTruthy() expect(screen.getByText('20.0 tok/s')).toBeTruthy() }) it('breaks output tokens into labeled reasoning and content rows', () => { render() fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ })) expect(screen.getByText('Tokens')).toBeTruthy() expect(screen.getByText('20 tok')).toBeTruthy() expect(screen.getByText('Reasoning')).toBeTruthy() expect(screen.getByText('5 tok')).toBeTruthy() expect(screen.getByText('Content')).toBeTruthy() expect(screen.getByText('15 tok')).toBeTruthy() }) it('keeps long thinking collapsed until the user asks to render it', () => { const thinking = 'private chain '.repeat(1_000) const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [{ index: 1, kind: 'message', text: 'private chain…', thinkingDetail: thinking, timeSeconds: 1, }], }], }] render() fireEvent.click(screen.getByRole('row', { name: /ASSISTANT/ })) const toggle = screen.getByRole('button', { name: 'Thinking ...' }) expect(screen.queryByText(thinking)).toBeNull() fireEvent.click(toggle) expect(toggle.parentElement?.textContent?.length).toBeGreaterThan(thinking.length) }) it('keeps raw HTML tags in a Markdown-derived context preview', () => { const html = [ '', 'Command: pnpm test', 'Exit code: 0', '', ].join('\n') const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Message', cells: [{ index: 1, kind: 'context', text: '', inputDetail: html, timeSeconds: 0, }], }], }] render() expect(screen.getByText( ' Command: pnpm test Exit code: 0 ', )).toBeTruthy() }) it('clears the selected row when ledger whitespace is clicked', () => { const onClearSelection = vi.fn() render( , ) const row = screen.getByRole('row', { name: /ASSISTANT/ }) fireEvent.click(row) expect(row.getAttribute('aria-selected')).toBe('true') expect(screen.getByRole('complementary', { name: 'Event details' })).toBeTruthy() const tablePane = screen.getByRole('table').parentElement expect(tablePane).not.toBeNull() fireEvent.click(tablePane as HTMLElement) expect(row.getAttribute('aria-selected')).toBe('false') expect(screen.queryByRole('complementary', { name: 'Event details' })).toBeNull() expect(onClearSelection).toHaveBeenCalledOnce() }) it('keeps running and failure semantics distinct from record roles', () => { const view = render() expect(view.container.querySelector('tr[data-kind="tool"][data-running="true"]')).toBeTruthy() expect(view.container.querySelector('tr[data-kind="tool"][data-error="true"]')).toBeTruthy() fireEvent.click(screen.getByRole('row', { name: /TOOL, bash \{"command":"pwd"\}/ })) expect(screen.getByText('Pending')).toBeTruthy() fireEvent.click(screen.getByRole('row', { name: /TOOL, bash \{"command":"false"\}/ })) expect(screen.getByText('Failed')).toBeTruthy() expect(screen.getByText('Failed').className).toContain('error') fireEvent.click(screen.getByRole('tab', { name: 'Result' })) const errorResult = screen.getByText('ToolError: non_zero_exit') expect(errorResult.closest('[class*="errorPayload"]')).toBeTruthy() }) it('renders responsive role icons with a custom tooltip', () => { const view = render() const toolTag = view.container.querySelector('[data-role-kind="tool"]') expect(toolTag).not.toBeNull() expect(toolTag?.getAttribute('title')).toBeNull() expect(toolTag?.querySelector('[data-role-icon="wrench"]')).toBeTruthy() fireEvent.mouseEnter(toolTag as HTMLElement) expect(screen.getByRole('tooltip').textContent).toBe('TOOL') fireEvent.mouseLeave(toolTag as HTMLElement) expect(screen.queryByRole('tooltip')).toBeNull() }) it('uses information and compression glyphs for injected and compacted context', () => { const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Context', cells: [ { index: 1, kind: 'context', text: 'Workspace context', timeSeconds: 0 }, { index: 2, kind: 'compacted', text: 'Compacted history', timeSeconds: 0 }, ], }], }] const view = render() expect(view.container.querySelector( '[data-role-kind="context"] [data-role-icon="information"]', )).toBeTruthy() expect(view.container.querySelector( '[data-role-kind="compacted"] [data-role-icon="compacted"]', )).toBeTruthy() }) it('keeps a compact turn label available for narrow layouts', () => { render() const turnLabel = screen.getByLabelText('Turn 1') expect(turnLabel.textContent).toContain('Turn 1') expect(turnLabel.textContent).toContain('#1') }) it('renders a single-text JSON tool result as a JSON tree', () => { const turns: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [{ index: 1, kind: 'tool', text: 'read {"path":"result.json"}', outputDetail: '{"value":1,"nested":{"ok":true}}', outputBlocks: [{ type: 'text', content: '{"value":1,"nested":{"ok":true}}', }], timeSeconds: 0.1, }], }], }] render() fireEvent.click(screen.getByRole('row', { name: /TOOL/ })) fireEvent.click(screen.getByRole('tab', { name: 'Result' })) expect(screen.getByRole('tree', { name: 'Result JSON' })).toBeTruthy() expect(screen.getByText('value:')).toBeTruthy() }) it('keeps the first row and a compact summary when a turn is collapsed', () => { render( , ) expect(screen.queryByRole('columnheader')).toBeNull() expect(screen.getByRole('row', { name: /ASSISTANT/ })).toBeTruthy() expect(screen.getByRole('row', { name: /Collapsed turn summary/ })).toBeTruthy() }) const CALL_TURNS: readonly TrajectoryTurnModel[] = [{ turn: 1, groups: [{ title: 'Step 1', cells: [{ index: 1, kind: 'tool', text: 'bash · {"command":"pwd"}', inputDetail: '{"command":"pwd"}', callId: 'call-1', timeSeconds: 0.1, }], }], }] it('an inspect target opens the matching record and acknowledges once', () => { const onInspectApplied = vi.fn() render( , ) expect(screen.getByRole('row', { name: /TOOL/ }).getAttribute('aria-selected')).toBe('true') expect(screen.getByRole('complementary', { name: 'Event details' })).toBeTruthy() expect(onInspectApplied).toHaveBeenCalledOnce() }) it('an unmatched inspect target stays pending without acknowledgement', () => { const onInspectApplied = vi.fn() render( , ) expect(screen.getByRole('row', { name: /TOOL/ }).getAttribute('aria-selected')).toBe('false') expect(onInspectApplied).not.toHaveBeenCalled() }) })