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
|
discards unclosed intervals, and marks them incomplete; old history is not
|
||||||
backfilled by scanning messages or events.
|
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
|
Schema 59 stores timing rows and adds a covering index for conversation
|
||||||
summaries. The old historical aggregation, its caches, dedicated reader/worker,
|
summaries. The old historical aggregation, its caches, dedicated reader/worker,
|
||||||
worker build entries, and five-second statistics polling have been removed.
|
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))
|
expect(raw().prepare('PRAGMA index_info(messages_summary_idx)').all().map(row => row.name))
|
||||||
.toEqual(['conversation_id', 'sequence', 'role', 'created_at'])
|
.toEqual(['conversation_id', 'sequence', 'role', 'created_at'])
|
||||||
expect(raw().prepare('PRAGMA integrity_check').get()).toEqual({ integrity_check: 'ok' })
|
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 () => {
|
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 a').closest('button')).toHaveAttribute('aria-current', 'true')
|
||||||
expect(screen.getByText('Project task b').closest('article')).not.toHaveTextContent('任务累计用时')
|
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' }))
|
view.rerender(sidebarElement({ ...props, activeConversationId: 'b' }))
|
||||||
expect(stats.queryByText('Conversation A')).not.toBeInTheDocument()
|
expect(stats.queryByText('Conversation A')).not.toBeInTheDocument()
|
||||||
expect(stats.getAllByText('暂无统计')).toHaveLength(2)
|
expect(stats.getAllByText('暂无统计')).toHaveLength(2)
|
||||||
|
|||||||
@@ -838,7 +838,8 @@ function RightAssistantSidebarView({
|
|||||||
const currentStats = activeConversationId && conversationStats?.conversationId === activeConversationId
|
const currentStats = activeConversationId && conversationStats?.conversationId === activeConversationId
|
||||||
? conversationStats
|
? conversationStats
|
||||||
: undefined
|
: 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)
|
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')}`
|
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')
|
? currentStats?.title ?? conversationTitles.get(activeConversationId) ?? t('sidebar.tasks.stats.currentConversation')
|
||||||
: t('sidebar.tasks.stats.noConversation')}</h3>
|
: t('sidebar.tasks.stats.noConversation')}</h3>
|
||||||
<dl className="task-center__stat-grid">
|
<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>
|
<div><dt>{t('sidebar.tasks.stats.messages')}</dt><dd>{currentStats ? currentStats.messageCount.toLocaleString(locale) : t('sidebar.tasks.stats.unavailable')}</dd></div>
|
||||||
</dl>
|
</dl>
|
||||||
{currentStats?.incomplete && <p className="task-center__incomplete">{t('sidebar.tasks.stats.incomplete')}</p>}
|
{currentStats?.incomplete && <p className="task-center__incomplete">{t('sidebar.tasks.stats.incomplete')}</p>}
|
||||||
@@ -2253,7 +2254,7 @@ function RightAssistantSidebarView({
|
|||||||
</span>
|
</span>
|
||||||
<span>{t(`task.status.${task.status}`)}</span>
|
<span>{t(`task.status.${task.status}`)}</span>
|
||||||
{duration && <span title={t('sidebar.tasks.stats.taskDuration')}>
|
{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')})`}
|
{duration.incomplete && ` (${t('sidebar.tasks.stats.partial')})`}
|
||||||
</span>}
|
</span>}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user