mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
138 lines
5.7 KiB
TypeScript
138 lines
5.7 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { render, screen } from '@testing-library/react'
|
|
import type { ReactElement } from 'react'
|
|
import type { ChatToolCall, ToolCallBlock } from '@/chat/types'
|
|
import { ToolDetailDialogContent } from '@/components/ToolCard/ToolCard'
|
|
import { I18nProvider } from '@/lib/i18n-context'
|
|
|
|
function renderWithI18n(ui: ReactElement) {
|
|
return render(<I18nProvider>{ui}</I18nProvider>)
|
|
}
|
|
|
|
function makeBlock(tool: Partial<ChatToolCall>): ToolCallBlock {
|
|
return {
|
|
kind: 'tool-call',
|
|
id: 'tool-1',
|
|
localId: null,
|
|
createdAt: 0,
|
|
tool: {
|
|
id: 'tool-1',
|
|
name: 'Bash',
|
|
state: 'completed',
|
|
input: { command: 'ls' },
|
|
createdAt: 0,
|
|
startedAt: 0,
|
|
completedAt: 0,
|
|
execStartedAt: null,
|
|
execCompletedAt: null,
|
|
description: null,
|
|
result: 'ok',
|
|
...tool,
|
|
},
|
|
children: [],
|
|
}
|
|
}
|
|
|
|
describe('ToolDetailDialogContent — execution timing', () => {
|
|
it('shows a Duration row for a completed tool', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({ state: 'completed', startedAt: 1000, completedAt: 3500 })} metadata={null} />)
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.getByText('2.5s')).toBeTruthy()
|
|
})
|
|
|
|
it('shows a Duration row for an error tool that completed', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({ state: 'error', startedAt: 1000, completedAt: 1800 })} metadata={null} />)
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.getByText('0.8s')).toBeTruthy()
|
|
})
|
|
|
|
it('shows start and live duration but no finish while running', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({ state: 'running', startedAt: 1000, completedAt: null })} metadata={null} />)
|
|
expect(screen.getByText('Started')).toBeTruthy()
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.queryByText('Finished')).toBeNull()
|
|
})
|
|
|
|
it('does not show a Duration row on clock skew (completedAt precedes startedAt)', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({ state: 'completed', startedAt: 3500, completedAt: 1000 })} metadata={null} />)
|
|
expect(screen.queryByText('Duration')).toBeNull()
|
|
})
|
|
|
|
it('does not show a Duration row while pending (no startedAt, no completedAt)', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({ state: 'pending', startedAt: null, completedAt: null })} metadata={null} />)
|
|
expect(screen.queryByText('Duration')).toBeNull()
|
|
})
|
|
|
|
it('coexists with the Trace section summary on a completed Task tool call', () => {
|
|
// Task/CodexAgent tool calls render their own Trace section summary
|
|
// (children count/tokens/duration, self-reported by the tool result) in
|
|
// the same dialog. This guards against the two duration sources
|
|
// (hub wall-clock vs. tool-self-reported) silently clashing or crashing
|
|
// when both are present.
|
|
const child = makeBlock({ id: 'child-1', name: 'Read', state: 'completed' })
|
|
const block: ToolCallBlock = {
|
|
kind: 'tool-call',
|
|
id: 'task-1',
|
|
localId: null,
|
|
createdAt: 0,
|
|
children: [child],
|
|
tool: {
|
|
id: 'task-1',
|
|
name: 'Task',
|
|
state: 'completed',
|
|
input: { subagent_type: 'Explore' },
|
|
createdAt: 0,
|
|
startedAt: 1000,
|
|
completedAt: 3500,
|
|
execStartedAt: null,
|
|
execCompletedAt: null,
|
|
description: null,
|
|
result: { totalDurationMs: 2400, totalTokens: 1000, totalToolUseCount: 1 },
|
|
},
|
|
}
|
|
|
|
renderWithI18n(<ToolDetailDialogContent block={block} metadata={null} />)
|
|
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.getByText('2.5s')).toBeTruthy()
|
|
expect(screen.getByText('Trace')).toBeTruthy()
|
|
})
|
|
|
|
it('prefers the claude execution-machine timestamps over hub receive time when both are present', () => {
|
|
// Hub receipt shows an inflated 2.5s window (hub queue/transport
|
|
// overhead); the claude entries themselves show the true 2.0s.
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({
|
|
state: 'completed',
|
|
startedAt: 1000,
|
|
completedAt: 3500,
|
|
execStartedAt: 1100,
|
|
execCompletedAt: 3100,
|
|
})} metadata={null} />)
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.getByText('2.0s')).toBeTruthy()
|
|
})
|
|
|
|
it('falls back to the hub receive time when exec timestamps are absent (non-Claude agent, no regression)', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({
|
|
state: 'completed',
|
|
startedAt: 1000,
|
|
completedAt: 3500,
|
|
execStartedAt: null,
|
|
execCompletedAt: null,
|
|
})} metadata={null} />)
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
expect(screen.getByText('2.5s')).toBeTruthy()
|
|
})
|
|
|
|
it('shows start and finish timestamps for a completed tool', () => {
|
|
renderWithI18n(<ToolDetailDialogContent block={makeBlock({
|
|
state: 'completed',
|
|
startedAt: 1_700_000_000_000,
|
|
completedAt: 1_700_000_002_500,
|
|
})} metadata={null} />)
|
|
expect(screen.getByText('Started')).toBeTruthy()
|
|
expect(screen.getByText('Finished')).toBeTruthy()
|
|
expect(screen.getByText('Duration')).toBeTruthy()
|
|
})
|
|
})
|