fix(ui): align trajectory checks with inspection ledger

This commit is contained in:
_Kerman
2026-07-28 10:17:44 +08:00
parent a509a3f81f
commit fb3e4effba
5 changed files with 49 additions and 47 deletions

View File

@@ -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 ''

View File

@@ -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 })

View File

@@ -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: /记录 1ASSISTANT/ }), { 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: /记录 2TOOL/ }))
expect(screen.getByText('进行中')).toBeTruthy()
fireEvent.click(screen.getByRole('row', { name: /记录 3TOOL/ }))
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: /记录 1ASSISTANT/ })).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()
})
})

View File

@@ -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: /记录 1USER/ })).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: /记录 3TOOL/ }), { 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()
})

View File

@@ -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,