deepseek-harness/packages/client/ui-trajectory/tests/layout.spec.tsx
07akioni 3babb2cd42 feat(trajectory): implement trajectory step cell and layout with bilingual support
- Added TrajectoryCell, TrajectoryGroupHeader, and TrajectoryTurn components for rendering trajectory steps and groups.
- Introduced bilingual support with English and Chinese translations for trajectory notes.
- Updated conversation session models to include timestamps for various message types.
- Enhanced layout logic to handle expanded assistant blocks and tool results with duration metrics.
- Added CSS styles for new components to ensure proper display and alignment.
2026-07-24 13:33:35 +08:00

143 lines
5.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// @vitest-environment jsdom
/**
* Trajectory turn chrome and layout fold: expand blocks, usage on Message,
* tool own-duration, group wall-span descriptions, in-flight rows.
*/
import { afterEach, describe, expect, it } from 'vitest'
import { cleanup, render, screen } from '@testing-library/react'
import type { ConversationSnapshot } from '@deepseek-ai/dsh-client-runtime/client'
import { TrajectoryGroupHeader } from '../src/client/TrajectoryGroupHeader.tsx'
import { TrajectoryTurn } from '../src/client/TrajectoryTurn.tsx'
import { TrajectoryTurnHeader } from '../src/client/TrajectoryTurnHeader.tsx'
import { deriveTrajectoryLayout } from '../src/client/layout.ts'
afterEach(cleanup)
describe('TrajectoryTurnHeader', () => {
it('renders Turn N and the four metric column labels', () => {
render(<TrajectoryTurnHeader turn={1} />)
expect(screen.getByText('Turn 1')).toBeTruthy()
expect(screen.getByText('Input')).toBeTruthy()
expect(screen.getByText('Output')).toBeTruthy()
expect(screen.getByText('Think')).toBeTruthy()
expect(screen.getByText('Time')).toBeTruthy()
})
})
describe('TrajectoryGroupHeader', () => {
it('renders title and optional description', () => {
render(<TrajectoryGroupHeader title="Step 1" description="2.2s skill" />)
expect(screen.getByText('Step 1')).toBeTruthy()
expect(screen.getByText('2.2s skill')).toBeTruthy()
})
it('omits the description node when absent', () => {
const { container } = render(<TrajectoryGroupHeader title="Message" />)
expect(screen.getByText('Message')).toBeTruthy()
expect(container.querySelectorAll('span')).toHaveLength(1)
})
})
describe('TrajectoryTurn', () => {
it('wraps a sticky header and body children', () => {
render(
<TrajectoryTurn turn={3}>
<TrajectoryGroupHeader title="Message" description="49s" />
</TrajectoryTurn>,
)
expect(screen.getByText('Turn 3')).toBeTruthy()
expect(screen.getByText('Message')).toBeTruthy()
expect(screen.getByText('49s')).toBeTruthy()
})
})
describe('deriveTrajectoryLayout', () => {
it('expands assistant blocks, hangs usage on Message, and folds call+result into Tool', () => {
const nodes = [
{ kind: 'user', seq: 1, time: 1_000, content: [{ type: 'text', text: 'hello' }], source: null },
{
kind: 'assistant', seq: 2, time: 6_000, turn: 1, step: 1,
blocks: [
{ kind: 'reasoning', text: 'thinking…' },
{ kind: 'text', text: 'I will run bash' },
{ kind: 'tool-call', callId: 'c1', name: 'bash', argsRaw: '{"command":"ls"}' },
],
usage: { inputTokens: 10, outputTokens: 20, reasoningTokens: 5 },
},
{
kind: 'tool-result', seq: 3, time: 7_500, callId: 'c1',
call: { name: 'bash', argsRaw: '{"command":"ls"}' }, callTime: 6_200,
content: [{ type: 'text', text: 'a.txt' }], isError: false, callView: null, resultView: null,
},
] as unknown as ConversationSnapshot['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
expect(turns).toHaveLength(1)
expect(turns[0]?.turn).toBe(1)
const kinds = turns[0]?.groups.flatMap((g) => g.cells.map((c) => c.kind))
expect(kinds).toEqual(['user', 'message', 'tool'])
const message = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'message')
expect(message).toMatchObject({
input: 10, output: 20, think: 5, timeSeconds: 5,
})
const tool = turns[0]?.groups.flatMap((g) => g.cells).find((c) => c.kind === 'tool')
expect(tool?.text).toBe('bash · {"command":"ls"}')
expect(tool?.timeSeconds).toBe(1.3)
})
it('adds runningCalls not already present and leaves their time blank', () => {
const turns = deriveTrajectoryLayout({
nodes: [] as unknown as ConversationSnapshot['nodes'],
partial: null,
runningCalls: [{
callId: 'r1', name: 'bash', argsRaw: '{"command":"pwd"}',
turn: 1, step: 2, time: 9_000, callView: null,
}],
})
expect(turns[0]?.groups.map((g) => g.title)).toEqual(['Step 2'])
expect(turns[0]?.groups[0]?.cells[0]).toMatchObject({
kind: 'tool', text: 'bash · {"command":"pwd"}', timeSeconds: null,
})
})
it('omits duration when node times are missing instead of rendering NaN', () => {
const nodes = [
{ kind: 'user', seq: 1, content: [{ type: 'text', text: 'hi' }], source: null },
{
kind: 'assistant', seq: 2, turn: 1, step: 1,
blocks: [
{ kind: 'reasoning', text: '…' },
{ kind: 'text', text: 'ok' },
],
usage: { inputTokens: 1, outputTokens: 2, reasoningTokens: 3 },
},
] as unknown as ConversationSnapshot['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
const cells = turns[0]?.groups.flatMap((g) => g.cells) ?? []
expect(cells.find((c) => c.kind === 'message')?.timeSeconds).toBeNull()
expect(turns[0]?.groups.find((g) => g.title === 'Step 1')?.description).toBeUndefined()
})
it('builds a wall-span step description with a tool histogram', () => {
const nodes = [
{
kind: 'assistant', seq: 1, time: 1_000, turn: 1, step: 1,
blocks: [
{ kind: 'tool-call', callId: 'a', name: 'bash', argsRaw: '{}' },
{ kind: 'tool-call', callId: 'b', name: 'bash', argsRaw: '{}' },
],
},
{
kind: 'tool-result', seq: 2, time: 2_500, callId: 'a',
call: { name: 'bash', argsRaw: '{}' }, callTime: 1_100,
content: [], isError: false, callView: null, resultView: null,
},
{
kind: 'tool-result', seq: 3, time: 4_000, callId: 'b',
call: { name: 'bash', argsRaw: '{}' }, callTime: 2_600,
content: [], isError: false, callView: null, resultView: null,
},
] as unknown as ConversationSnapshot['nodes']
const turns = deriveTrajectoryLayout({ nodes, partial: null, runningCalls: [] })
expect(turns[0]?.groups[0]?.description).toBe('2.9s bash×2')
})
})