mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
fix(ui): align trajectory checks with inspection ledger
This commit is contained in:
@@ -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 ''
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user