Browse Source

fix(ui): align trajectory checks with inspection ledger

_Kerman 2 months ago
parent
commit
fb3e4effba

+ 3 - 3
packages/client/ui-primitives/src/markdown/plain-text.ts

@@ -1,6 +1,6 @@
 /**
  * Markdown-to-plain-text projection for compact summaries and labels.
- * Parsing shares the renderer's GFM grammar; raw HTML remains literal, links
+ * Parsing shares the renderer's GFM grammar; raw HTML is omitted, links
  * keep their labels, images keep alt text, and code keeps its source text.
  */
 
@@ -36,7 +36,7 @@ function inlineText(node: MarkdownNode): string {
     case 'break':
       return '\n'
     case 'html':
-      return node.value ?? ''
+      return ''
     case 'thematicBreak':
     case 'definition':
       return ''
@@ -70,7 +70,7 @@ function blockText(node: MarkdownNode): string {
     case 'tableCell':
       return compactInline(inlineText(node))
     case 'html':
-      return node.value?.trim() ?? ''
+      return ''
     case 'thematicBreak':
     case 'definition':
       return ''

+ 3 - 3
packages/client/ui-trajectory/tests/layout.spec.tsx

@@ -139,7 +139,7 @@ describe('deriveTrajectoryLayout', () => {
       },
     ] as unknown as ConversationSnapshot['nodes']
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
-    expect(turns[0]?.groups[0]?.description).toBe('3s bash×2')
+    expect(turns[0]?.groups[0]?.description).toBe('3 s bash×2')
   })
 
   it('assigns each user message to its enclosing turn instead of pooling into Turn 1', () => {
@@ -172,7 +172,7 @@ describe('deriveTrajectoryLayout', () => {
     const turns = deriveTrajectoryLayout({ codeDispatches: new Map(), nodes, partial: null, runningCalls: [] })
     const message = turns[0]?.groups.flatMap(g => g.cells).find(c => c.kind === 'message')
     expect(message).toMatchObject({
-      text: '仅推理输出', input: 11, output: 22, think: 3,
+      text: '…', input: 11, output: 22, think: 3,
     })
   })
 
@@ -236,7 +236,7 @@ describe('run_code sub-dispatch cells', () => {
     const turns = deriveTrajectoryLayout({ codeDispatches, nodes: runCodeNodes, partial: null, runningCalls: [] })
     const cells = turns[0]!.groups.flatMap(g => g.cells)
     expect(cells.map(c => c.kind)).toEqual(['message', 'tool', 'subtool', 'subtool'])
-    expect(cells[0]?.text).toBe('请求调用 run_code')
+    expect(cells[0]?.text).toBe('Tool call only')
     // Sequential indexes across the interleave; durations from the pair times.
     expect(cells.map(c => c.index)).toEqual([1, 2, 3, 4])
     expect(cells[2]).toMatchObject({ text: 'bash · {"x":1}', timeSeconds: 1 })

+ 20 - 16
packages/client/ui-trajectory/tests/table.spec.tsx

@@ -62,9 +62,9 @@ const FOLD_PROPS = {
 
 describe('TrajectoryTable', () => {
   it('shows assistant timing facts after keyboard selection', () => {
-    render(<TrajectoryTable turns={TURNS} collapsed={false} />)
-    fireEvent.keyDown(screen.getByRole('row', { name: /记录 1,ASSISTANT/ }), { key: 'Enter' })
-    fireEvent.click(screen.getByRole('tab', { name: '计时' }))
+    render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
+    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()
@@ -84,24 +84,28 @@ describe('TrajectoryTable', () => {
   })
 
   it('keeps running and failure semantics distinct from record roles', () => {
-    const view = render(<TrajectoryTable turns={TURNS} collapsed={false} />)
+    const view = render(<TrajectoryTable turns={TURNS} {...FOLD_PROPS} />)
     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: /记录 2,TOOL/ }))
-    expect(screen.getByText('进行中')).toBeTruthy()
-    fireEvent.click(screen.getByRole('row', { name: /记录 3,TOOL/ }))
-    expect(screen.getByText('失败')).toBeTruthy()
-    fireEvent.click(screen.getByRole('tab', { name: '输出' }))
+    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()
+    fireEvent.click(screen.getByRole('tab', { name: 'Result' }))
     expect(screen.getByText('ToolError: non_zero_exit')).toBeTruthy()
   })
 
-  it('retains the ledger header and record count when collapsed', () => {
-    render(<TrajectoryTable turns={TURNS} collapsed />)
-    expect(screen.getByRole('columnheader', { name: '事件' })).toBeTruthy()
-    expect(screen.queryByRole('columnheader', { name: 'Tokens' })).toBeNull()
-    expect(screen.queryByRole('columnheader', { name: '耗时' })).toBeNull()
-    expect(screen.getByText('3 条记录已收起')).toBeTruthy()
-    expect(screen.queryByRole('row', { name: /记录 1,ASSISTANT/ })).toBeNull()
+  it('keeps the first row and a compact summary when a turn is collapsed', () => {
+    render(
+      <TrajectoryTable
+        turns={TURNS}
+        {...FOLD_PROPS}
+        collapsedTurns={new Set([1])}
+      />,
+    )
+    expect(screen.queryByRole('columnheader')).toBeNull()
+    expect(screen.getByRole('row', { name: /ASSISTANT/ })).toBeTruthy()
+    expect(screen.getByRole('row', { name: /Collapsed turn summary/ })).toBeTruthy()
   })
 })

+ 18 - 25
packages/client/ui-trajectory/tests/views.spec.tsx

@@ -178,16 +178,12 @@ describe('tab switching in ConversationRoot', () => {
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
     expect(screen.queryByText(/turns ·/)).toBeNull()
     expect(view.container.querySelectorAll('tr[data-turn-start="true"]')).toHaveLength(2)
-    expect(screen.getAllByLabelText('Step 1')).toHaveLength(2)
-    expect(screen.getByRole('columnheader', { name: '事件' })).toBeTruthy()
-    expect(screen.getByRole('columnheader', { name: '内容' })).toBeTruthy()
-    expect(screen.queryByRole('columnheader', { name: 'Tokens' })).toBeNull()
-    expect(screen.queryByRole('columnheader', { name: '耗时' })).toBeNull()
-    expect(screen.getByRole('toolbar', { name: '轨迹工具栏' }).textContent).toContain('4 条记录')
-    fireEvent.click(screen.getByRole('button', { name: '收起记录' }))
-    expect(screen.getByText('4 条记录已收起')).toBeTruthy()
-    fireEvent.click(screen.getByRole('button', { name: '展开记录' }))
-    expect(screen.getByRole('row', { name: /记录 1,USER/ })).toBeTruthy()
+    expect(screen.queryByRole('columnheader')).toBeNull()
+    expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
+    fireEvent.click(screen.getByRole('button', { name: 'Collapse turns' }))
+    expect(view.container.querySelector('[data-collapsed-summary="turn"]')).toBeTruthy()
+    fireEvent.click(screen.getByRole('button', { name: 'Expand turns' }))
+    expect(screen.getByRole('row', { name: /USER/ })).toBeTruthy()
     expect(screen.queryByTestId('chat-body')).toBeNull()
   })
 
@@ -196,19 +192,14 @@ describe('tab switching in ConversationRoot', () => {
     mount(b.slots)
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
 
-    fireEvent.keyDown(screen.getByRole('row', { name: /记录 3,TOOL/ }), { key: 'Enter' })
-    expect(screen.getByRole('complementary', { name: '记录详情' })).toBeTruthy()
-    expect(screen.getByText('记录 #3')).toBeTruthy()
+    fireEvent.keyDown(screen.getByRole('row', { name: /TOOL/ }), { key: 'Enter' })
+    expect(screen.getByRole('complementary', { name: 'Event details' })).toBeTruthy()
     expect(screen.getByText('Turn 1 · Step 1')).toBeTruthy()
-    expect(screen.getByText('完成')).toBeTruthy()
+    expect(screen.getByText('Completed')).toBeTruthy()
+    expect(screen.getByRole('tab', { name: 'Result' })).toBeTruthy()
 
-    fireEvent.click(screen.getByRole('tab', { name: '输入' }))
-    expect(screen.getByText('这条记录没有输入载荷')).toBeTruthy()
-    fireEvent.click(screen.getByRole('tab', { name: '输出' }))
-    expect(screen.getByText('[]')).toBeTruthy()
-
-    fireEvent.click(screen.getByRole('button', { name: '关闭详情' }))
-    expect(screen.queryByRole('complementary', { name: '记录详情' })).toBeNull()
+    fireEvent.click(screen.getByRole('button', { name: 'Close details' }))
+    expect(screen.queryByRole('complementary', { name: 'Event details' })).toBeNull()
   })
 
   it('waterfall renders bars and switching back to chat does not collapse it', async () => {
@@ -226,7 +217,8 @@ describe('tab switching in ConversationRoot', () => {
     const b = await bench()
     mount(b.slots, [])
     fireEvent.click(screen.getByRole('tab', { name: 'Trajectory' }))
-    expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
+    expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
+    expect(screen.queryByRole('row')).toBeNull()
     expect(screen.queryByText(/turns ·/)).toBeNull()
   })
 })
@@ -253,7 +245,8 @@ describe('span derivation', () => {
     expect(container.firstChild).toBeNull()
     render(createElement(TrajectoryView as FC<ConvViewProps>,
       standaloneProps([])))
-    expect(screen.getByText('暂无轨迹数据')).toBeTruthy()
+    expect(screen.getByRole('toolbar', { name: 'Trajectory toolbar' })).toBeTruthy()
+    expect(screen.queryByRole('row')).toBeNull()
   })
 })
 
@@ -261,7 +254,7 @@ describe('WaterfallView standalone branches', () => {
   it('empty window renders the placeholder copy', () => {
     render(createElement(WaterfallView as FC<ConvViewProps>,
       standaloneProps([])))
-    expect(screen.getByText('暂无瀑布数据')).toBeTruthy()
+    expect(screen.getByText('No timing data')).toBeTruthy()
   })
 
   it('a turn without tool calls renders the node bar only', () => {
@@ -361,7 +354,7 @@ describe('deriveSubSpans (waterfall lanes)', () => {
     const lane = view.container.querySelector('[data-subspan]')
     expect(lane).not.toBeNull()
     expect(lane!.textContent).toContain('bash')
-    expect(lane!.querySelector('[title*="1.80s"]')).not.toBeNull()
+    expect(lane!.querySelector('[title*="1.80 s"]')).not.toBeNull()
     expect(lane!.querySelector('[data-timing="measured"]')).not.toBeNull()
   })
 

+ 5 - 0
packages/compact/compact-basic/tests/compact-basic.spec.ts

@@ -1107,6 +1107,11 @@ describe('default one-shot summarizer', () => {
 
     expect(output).toEqual({
       summary: [{ type: 'text', text: 'public summary' }],
+      rawOutput: [
+        { type: 'reasoning', text: 'private' },
+        { type: 'text', text: 'public summary' },
+        { type: 'tool-call', id: CallId('unexpected'), name: 'x', arguments: '{}' },
+      ],
       provider: MODEL,
       model: MODEL,
       maxTokens: 321,