1
0

code-inspector.client.spec.tsx 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. // @vitest-environment jsdom
  2. /** PTC source, output, and independent wrapping through the trajectory inspector. */
  3. import { afterEach, describe, expect, it, vi } from 'vitest'
  4. import { cleanup, fireEvent, render, waitFor, within } from '@testing-library/react'
  5. import { TrajectoryTable } from '../src/client/TrajectoryTable.tsx'
  6. import type { TrajectoryCellProps } from '../src/client/trajectory-record.ts'
  7. import { t } from './locale.client.ts'
  8. afterEach(() => {
  9. cleanup()
  10. vi.restoreAllMocks()
  11. vi.unstubAllGlobals()
  12. })
  13. const SOURCE = 'const value = await tools.bash({ command: "pwd" })\nreturn value\n'
  14. const ARGS = JSON.stringify({ code: SOURCE, description: 'Read the working directory' })
  15. const CELL: TrajectoryCellProps = {
  16. index: 1, kind: 'tool', toolName: 'run_code', text: `run_code · ${ARGS}`, callId: 'code-1',
  17. inputDetail: ARGS, outputDetail: 'printed line\n42', result: 'printed line', timeSeconds: 0.5,
  18. }
  19. function Harness({ cell = CELL, getDefault = () => false, setDefault = () => {} }: {
  20. cell?: TrajectoryCellProps
  21. getDefault?: () => boolean
  22. setDefault?: (value: boolean) => void
  23. }) {
  24. return <TrajectoryTable
  25. turns={[{ turn: 1, groups: [{ title: 'Step 1', cells: [cell] }] }]}
  26. renderImages={() => null} t={t}
  27. stringWrapping={{ label: 'Wrap lines', getDefault, setDefault }}
  28. collapsedTurns={new Set()} onToggleTurn={() => {}}
  29. collapsedAssistants={new Set()} onToggleAssistant={() => {}}
  30. />
  31. }
  32. describe('PTC trajectory inspector', () => {
  33. it('shows the description, numbered source, original arguments, and verbatim output', () => {
  34. const view = render(<Harness />)
  35. const ui = within(view.container)
  36. fireEvent.click(ui.getByRole('row', { name: /run_code Read the working directory/ }))
  37. expect(ui.getAllByRole('tab').map(tab => tab.textContent)).toEqual(['Summary', 'Code', 'Result', 'Schema', 'Timing'])
  38. expect(ui.getByRole('tabpanel').textContent).toContain('Read the working directory')
  39. expect(view.container.querySelector('[data-line-numbers] pre')?.textContent).toBe(SOURCE.trimEnd())
  40. expect(ui.getByRole('tabpanel').textContent).toContain('printed line\n42')
  41. fireEvent.click(ui.getByRole('button', { name: 'Code' }))
  42. expect(ui.getByRole('tab', { name: 'Code' }).getAttribute('aria-selected')).toBe('true')
  43. fireEvent.click(ui.getByRole('button', { name: 'Original JSON' }))
  44. expect(ui.getByRole('tree', { name: 'parameters JSON' }).textContent).toContain('description:')
  45. fireEvent.click(ui.getByRole('button', { name: 'Original JSON' }))
  46. expect(view.container.querySelector('[data-line-numbers] pre')?.textContent).toBe(SOURCE.trimEnd())
  47. })
  48. it('copies exact source, raw JSON, and output with feedback', async () => {
  49. const writeText = vi.fn().mockResolvedValue(undefined)
  50. vi.stubGlobal('navigator', { clipboard: { writeText } })
  51. const view = render(<Harness />)
  52. const ui = within(view.container)
  53. fireEvent.click(ui.getByRole('row', { name: /run_code Read/ }))
  54. fireEvent.click(ui.getByRole('button', { name: 'Copy code' }))
  55. await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith(SOURCE) })
  56. await waitFor(() => { expect(ui.getByRole('button', { name: 'Copied' })).toBeTruthy() })
  57. fireEvent.click(ui.getByRole('button', { name: 'Copy output' }))
  58. await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith('printed line\n42') })
  59. fireEvent.click(ui.getByRole('tab', { name: 'Code' }))
  60. fireEvent.click(ui.getByRole('button', { name: 'Original JSON' }))
  61. fireEvent.click(ui.getByRole('button', { name: 'Copy JSON' }))
  62. await waitFor(() => { expect(writeText).toHaveBeenLastCalledWith(ARGS) })
  63. })
  64. it('samples the last wrapping choice on open without changing another open inspector', () => {
  65. let preference = false
  66. const props = { getDefault: () => preference, setDefault: (value: boolean) => { preference = value } }
  67. const first = render(<Harness {...props} />)
  68. const second = render(<Harness {...props} />)
  69. const a = within(first.container)
  70. const b = within(second.container)
  71. fireEvent.click(a.getByRole('row', { name: /run_code Read/ }))
  72. fireEvent.click(b.getByRole('row', { name: /run_code Read/ }))
  73. fireEvent.click(a.getByRole('button', { name: 'Wrap lines' }))
  74. expect(preference).toBe(true)
  75. expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true')
  76. expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false')
  77. fireEvent.click(b.getByRole('tab', { name: 'Code' }))
  78. expect(b.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true')
  79. fireEvent.click(b.getByRole('button', { name: 'Wrap lines' }))
  80. expect(preference).toBe(false)
  81. expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('true')
  82. fireEvent.click(a.getByRole('button', { name: 'Close details' }))
  83. fireEvent.click(a.getByRole('row', { name: /run_code Read/ }))
  84. expect(a.getByRole('button', { name: 'Wrap lines' }).getAttribute('aria-pressed')).toBe('false')
  85. })
  86. it('shows pending execution and keeps a failed run’s captured output', () => {
  87. const pending = { ...CELL, timeSeconds: null }
  88. delete pending.result
  89. delete pending.outputDetail
  90. const view = render(<Harness cell={pending} />)
  91. const ui = within(view.container)
  92. fireEvent.click(ui.getByRole('row', { name: /run_code Read/ }))
  93. expect(ui.getByRole('tabpanel').textContent).toContain('Running…')
  94. view.rerender(<Harness cell={{ ...CELL, isError: true, outputDetail: 'Error: execution failed\nCaptured output:\nstep one' }} />)
  95. expect(ui.getByRole('tabpanel').textContent).toContain('Failed')
  96. expect(ui.getByRole('tabpanel').textContent).toContain('Error: execution failed\nCaptured output:\nstep one')
  97. })
  98. it('uses a tree for complete JSON output and generic input for malformed arguments', () => {
  99. const view = render(<Harness cell={{ ...CELL, outputDetail: '{"passed":12,"failed":0}' }} />)
  100. const ui = within(view.container)
  101. fireEvent.click(ui.getByRole('row', { name: /run_code Read/ }))
  102. expect(ui.getByRole('tree', { name: 'Result JSON' }).textContent).toContain('passed:12')
  103. view.rerender(<Harness cell={{ ...CELL, inputDetail: '{"code":3}' }} />)
  104. expect(ui.getByRole('tab', { name: 'Payload' })).toBeTruthy()
  105. expect(ui.queryByRole('tab', { name: 'Code' })).toBeNull()
  106. expect(ui.getByRole('tree', { name: 'Payload JSON' }).textContent).toContain('code:3')
  107. })
  108. })