fix(trajectory): show exact millisecond durations consistently
Drop the details-panel Duration toggle: Duration rows always show integer milliseconds, matching the cell time column. Timeline labels (Total/TTFT/Decoding) and step-group descriptions previously fell back to second labels at or above one second; they now also show exact milliseconds via the shared formatDurationMillis formatter.
This commit is contained in:
@@ -10,20 +10,33 @@ import {
|
||||
TrajectoryCell,
|
||||
type TrajectoryCellKind,
|
||||
} from '../src/client/TrajectoryCell.tsx'
|
||||
import { formatDurationMillis } from '../src/client/trajectory-record.ts'
|
||||
|
||||
afterEach(cleanup)
|
||||
|
||||
describe('formatDurationMillis', () => {
|
||||
it('formats exact millisecond labels with thousands separators', () => {
|
||||
expect(formatDurationMillis(0)).toBe('0 ms')
|
||||
expect(formatDurationMillis(29)).toBe('29 ms')
|
||||
expect(formatDurationMillis(500)).toBe('500 ms')
|
||||
expect(formatDurationMillis(1_500)).toBe('1,500 ms')
|
||||
expect(formatDurationMillis(235_200)).toBe('235,200 ms')
|
||||
expect(formatDurationMillis(null)).toBe('—')
|
||||
expect(formatDurationMillis(Number.NaN)).toBe('—')
|
||||
})
|
||||
})
|
||||
|
||||
describe('formatElapsedSeconds', () => {
|
||||
it('formats known durations and uses an em dash when absent', () => {
|
||||
expect(formatElapsedSeconds(null)).toBe('—')
|
||||
expect(formatElapsedSeconds(235)).toBe('235 s')
|
||||
expect(formatElapsedSeconds(235.0)).toBe('235 s')
|
||||
expect(formatElapsedSeconds(235.2)).toBe('235.2 s')
|
||||
expect(formatElapsedSeconds(235.25)).toBe('235.3 s')
|
||||
expect(formatElapsedSeconds(235)).toBe('235,000 ms')
|
||||
expect(formatElapsedSeconds(235.0)).toBe('235,000 ms')
|
||||
expect(formatElapsedSeconds(235.2)).toBe('235,200 ms')
|
||||
expect(formatElapsedSeconds(235.25)).toBe('235,250 ms')
|
||||
expect(formatElapsedSeconds(0)).toBe('0 ms')
|
||||
expect(formatElapsedSeconds(0.029)).toBe('29 ms')
|
||||
expect(formatElapsedSeconds(0.5)).toBe('500 ms')
|
||||
expect(formatElapsedSeconds(1.5)).toBe('1.5 s')
|
||||
expect(formatElapsedSeconds(1.5)).toBe('1,500 ms')
|
||||
expect(formatElapsedSeconds(Number.NaN)).toBe('—')
|
||||
})
|
||||
})
|
||||
@@ -41,7 +54,7 @@ describe('TrajectoryCell', () => {
|
||||
expect(screen.getByText('#6')).toBeTruthy()
|
||||
expect(screen.getByText('Tool')).toBeTruthy()
|
||||
expect(screen.getByText('bash · Read src/index.ts')).toBeTruthy()
|
||||
expect(screen.getByText('5 s')).toBeTruthy()
|
||||
expect(screen.getByText('5,000 ms')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('Message rows expose Input / Output / Think metric columns before time', () => {
|
||||
@@ -60,11 +73,11 @@ describe('TrajectoryCell', () => {
|
||||
expect(screen.getByText('136')).toBeTruthy()
|
||||
expect(screen.getByText('381')).toBeTruthy()
|
||||
expect(screen.getByText('155')).toBeTruthy()
|
||||
expect(screen.getByText('235.2 s')).toBeTruthy()
|
||||
expect(screen.getByText('235,200 ms')).toBeTruthy()
|
||||
const texts = [...container.querySelectorAll('span')].map(el => el.textContent)
|
||||
expect(texts.indexOf('136')).toBeLessThan(texts.indexOf('381'))
|
||||
expect(texts.indexOf('381')).toBeLessThan(texts.indexOf('155'))
|
||||
expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('235.2 s'))
|
||||
expect(texts.indexOf('155')).toBeLessThan(texts.indexOf('235,200 ms'))
|
||||
})
|
||||
|
||||
it('selected marks the row for the brand-primary inset ring', () => {
|
||||
|
||||
@@ -207,7 +207,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('3 s bash×2')
|
||||
expect(turns[0]?.groups[0]?.description).toBe('3,000 ms bash×2')
|
||||
})
|
||||
|
||||
it('assigns each user message to its enclosing turn instead of pooling into Turn 1', () => {
|
||||
|
||||
@@ -97,7 +97,7 @@ describe('TrajectoryTable', () => {
|
||||
expect(screen.getByText('20.0 tok/s')).toBeTruthy()
|
||||
})
|
||||
|
||||
it('toggles a tool record Duration between readable and exact milliseconds', () => {
|
||||
it('shows a tool record Duration as exact milliseconds', () => {
|
||||
const turns: readonly TrajectoryTurnModel[] = [{
|
||||
turn: 1,
|
||||
groups: [{
|
||||
@@ -115,11 +115,7 @@ describe('TrajectoryTable', () => {
|
||||
render(<TrajectoryTable turns={turns} {...FOLD_PROPS} />)
|
||||
fireEvent.click(screen.getByRole('row', { name: /TOOL/ }))
|
||||
|
||||
const readable = screen.getByRole('button', { name: '1.5 s' })
|
||||
fireEvent.click(readable)
|
||||
expect(screen.getByRole('button', { name: '1500 ms' })).toBeTruthy()
|
||||
fireEvent.click(screen.getByRole('button', { name: '1500 ms' }))
|
||||
expect(screen.getByRole('button', { name: '1.5 s' })).toBeTruthy()
|
||||
expect(screen.getByText('1,500 ms', { selector: 'dd' })).toBeTruthy()
|
||||
})
|
||||
|
||||
it('breaks output tokens into labeled reasoning and content rows', () => {
|
||||
|
||||
@@ -580,9 +580,9 @@ describe('timeline projection', () => {
|
||||
expect(view.container.querySelector('[role="tooltip"]')).toBeNull()
|
||||
act(() => { vi.advanceTimersByTime(1) })
|
||||
const tooltip = view.container.querySelector<HTMLElement>('[role="tooltip"]')
|
||||
expect(tooltip?.textContent).toContain('Total 2.0 s')
|
||||
expect(tooltip?.textContent).toContain('Total 2,000 ms')
|
||||
expect(tooltip?.textContent).toContain('TTFT 500 ms')
|
||||
expect(tooltip?.textContent).toContain('Decoding 1.5 s')
|
||||
expect(tooltip?.textContent).toContain('Decoding 1,500 ms')
|
||||
} finally {
|
||||
vi.useRealTimers()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user