fix: distinguish missing historical timing from zero duration
This commit is contained in:
@@ -14,6 +14,13 @@ requests remain paused during reconnection. Startup preserves closed intervals,
|
||||
discards unclosed intervals, and marks them incomplete; old history is not
|
||||
backfilled by scanning messages or events.
|
||||
|
||||
An incomplete zero duration means no known timed interval, not a measured zero.
|
||||
Both conversation totals and task cards display "unavailable" in this case.
|
||||
Positive partial durations retain the incomplete warning; complete zero durations
|
||||
still display zero. The upgrade regression verifies that a new completed reply
|
||||
in an old conversation retains its accumulated milliseconds after reopening the
|
||||
database. This presentation fix does not restore pre-upgrade historical totals.
|
||||
|
||||
Schema 59 stores timing rows and adds a covering index for conversation
|
||||
summaries. The old historical aggregation, its caches, dedicated reader/worker,
|
||||
worker build entries, and five-second statistics polling have been removed.
|
||||
|
||||
@@ -240,6 +240,18 @@ describe('persisted running-state duration', () => {
|
||||
expect(raw().prepare('PRAGMA index_info(messages_summary_idx)').all().map(row => row.name))
|
||||
.toEqual(['conversation_id', 'sequence', 'role', 'created_at'])
|
||||
expect(raw().prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
|
||||
const replyId = task()
|
||||
database.startExecutionTiming(replyId)
|
||||
now += 7000
|
||||
database.updateTaskStatus(replyId, 'completed')
|
||||
database.endExecutionTiming(replyId)
|
||||
expect(stats()).toMatchObject({ durationMs: 7000, runningCount: 0, incomplete: true })
|
||||
database.close()
|
||||
now += 86_400_000
|
||||
database.initialize(directory)
|
||||
expect(stats()).toMatchObject({ durationMs: 7000, runningCount: 0, incomplete: true })
|
||||
expect(raw().prepare('SELECT duration_ms FROM execution_timing WHERE task_id = ?').get(replyId))
|
||||
.toEqual({ duration_ms: 7000 })
|
||||
})
|
||||
|
||||
it('deletes timing with its conversation and notifies snapshot subscribers', async () => {
|
||||
|
||||
@@ -454,6 +454,20 @@ describe('RightAssistantSidebar resizing', () => {
|
||||
expect(screen.getByText('Project task a').closest('button')).toHaveAttribute('aria-current', 'true')
|
||||
expect(screen.getByText('Project task b').closest('article')).not.toHaveTextContent('任务累计用时')
|
||||
|
||||
view.rerender(sidebarElement({ ...props,
|
||||
conversationStats: { ...props.conversationStats, replyDurationMs: 0 },
|
||||
taskDurations: new Map([['a', { durationMs: 0, incomplete: true }]])
|
||||
}))
|
||||
expect(stats.getByText('暂无统计')).toBeVisible()
|
||||
expect(stats.queryByText('00:00:00')).not.toBeInTheDocument()
|
||||
expect(screen.getByText('任务累计用时: 暂无统计 (部分记录)')).toBeVisible()
|
||||
view.rerender(sidebarElement({ ...props,
|
||||
conversationStats: { ...props.conversationStats, replyDurationMs: 500 },
|
||||
taskDurations: new Map([['a', { durationMs: 500, incomplete: true }]])
|
||||
}))
|
||||
expect(stats.getByText('00:00:00')).toBeVisible()
|
||||
expect(screen.getByText('任务累计用时: 00:00:00 (部分记录)')).toBeVisible()
|
||||
|
||||
view.rerender(sidebarElement({ ...props, activeConversationId: 'b' }))
|
||||
expect(stats.queryByText('Conversation A')).not.toBeInTheDocument()
|
||||
expect(stats.getAllByText('暂无统计')).toHaveLength(2)
|
||||
|
||||
@@ -838,7 +838,8 @@ function RightAssistantSidebarView({
|
||||
const currentStats = activeConversationId && conversationStats?.conversationId === activeConversationId
|
||||
? conversationStats
|
||||
: undefined
|
||||
const formatDuration = (durationMs: number): string => {
|
||||
const formatDuration = (durationMs: number, incomplete = false): string => {
|
||||
if (incomplete && durationMs === 0) return t('sidebar.tasks.stats.unavailable')
|
||||
const seconds = Math.floor(Math.max(0, durationMs) / 1000)
|
||||
return `${Math.floor(seconds / 3600).toString().padStart(2, '0')}:${Math.floor(seconds / 60 % 60).toString().padStart(2, '0')}:${(seconds % 60).toString().padStart(2, '0')}`
|
||||
}
|
||||
@@ -2105,7 +2106,7 @@ function RightAssistantSidebarView({
|
||||
? currentStats?.title ?? conversationTitles.get(activeConversationId) ?? t('sidebar.tasks.stats.currentConversation')
|
||||
: t('sidebar.tasks.stats.noConversation')}</h3>
|
||||
<dl className="task-center__stat-grid">
|
||||
<div><dt>{t('sidebar.tasks.stats.replyDuration')}</dt><dd>{currentStats ? formatDuration(currentStats.replyDurationMs) : t('sidebar.tasks.stats.unavailable')}</dd></div>
|
||||
<div><dt>{t('sidebar.tasks.stats.replyDuration')}</dt><dd>{currentStats ? formatDuration(currentStats.replyDurationMs, currentStats.incomplete) : t('sidebar.tasks.stats.unavailable')}</dd></div>
|
||||
<div><dt>{t('sidebar.tasks.stats.messages')}</dt><dd>{currentStats ? currentStats.messageCount.toLocaleString(locale) : t('sidebar.tasks.stats.unavailable')}</dd></div>
|
||||
</dl>
|
||||
{currentStats?.incomplete && <p className="task-center__incomplete">{t('sidebar.tasks.stats.incomplete')}</p>}
|
||||
@@ -2253,7 +2254,7 @@ function RightAssistantSidebarView({
|
||||
</span>
|
||||
<span>{t(`task.status.${task.status}`)}</span>
|
||||
{duration && <span title={t('sidebar.tasks.stats.taskDuration')}>
|
||||
{t('sidebar.tasks.stats.taskDuration')}: {formatDuration(duration.durationMs)}
|
||||
{t('sidebar.tasks.stats.taskDuration')}: {formatDuration(duration.durationMs, duration.incomplete)}
|
||||
{duration.incomplete && ` (${t('sidebar.tasks.stats.partial')})`}
|
||||
</span>}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user