task-144: 前端 live progress 合并补强(mergeHistoryItemPreservingLiveProgress 纯函数:缺失保留/有效覆盖/可下载不合并/幂等)+ 11 条测试
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
/**
|
||||
* live progress 合并(Task 144)。
|
||||
*
|
||||
* history 条目缺失 file 进度时保留实时缓存(live)进度;live 有值且有效时
|
||||
* 以 live 为准;history 已可下载(fileReady/downloadUrl 就绪)时不覆盖——
|
||||
* 结果文件既成,实时进度无意义。纯函数无副作用,入参不修改。
|
||||
*/
|
||||
export const FILE_PROGRESS_KEYS = [
|
||||
'fileProgressPercent',
|
||||
'fileProgressCurrent',
|
||||
'fileProgressTotal',
|
||||
'fileProgressMessage',
|
||||
'fileReady',
|
||||
'fileStatus',
|
||||
] as const
|
||||
|
||||
export interface DownloadableItem {
|
||||
resultId?: number | null
|
||||
fileReady?: boolean | null
|
||||
downloadUrl?: string | null
|
||||
}
|
||||
|
||||
/** 可下载判定:有结果记录且文件已就绪(fileReady 或 downloadUrl 非空)。 */
|
||||
export function canDownloadItem(item: DownloadableItem | null | undefined): boolean {
|
||||
if (!item) return false
|
||||
return Boolean(item.resultId && (item.fileReady || item.downloadUrl))
|
||||
}
|
||||
|
||||
/** 有效进度值:仅 fileProgressPercent 需要严格 >0,其余字段非空即可(0 是合法计数)。 */
|
||||
export function hasMeaningfulProgress(key: string, value: unknown): boolean {
|
||||
if (value === undefined || value === null) return false
|
||||
if (key === 'fileProgressPercent' && typeof value === 'number') {
|
||||
return value > 0
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
export function mergeHistoryItemPreservingLiveProgress<T extends Record<string, unknown>>(
|
||||
history: T,
|
||||
live: T | undefined,
|
||||
): T {
|
||||
if (!live) return history
|
||||
if (canDownloadItem(history as unknown as DownloadableItem)) return history
|
||||
const result: Record<string, unknown> = { ...history }
|
||||
for (const key of FILE_PROGRESS_KEYS) {
|
||||
const value = (live as Record<string, unknown>)[key]
|
||||
if (hasMeaningfulProgress(key, value)) {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
return result as T
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import {
|
||||
canDownloadItem,
|
||||
FILE_PROGRESS_KEYS,
|
||||
hasMeaningfulProgress,
|
||||
mergeHistoryItemPreservingLiveProgress,
|
||||
} from '../src/shared/merge-live-progress.ts'
|
||||
|
||||
interface HistoryItem {
|
||||
taskId: number
|
||||
resultId?: number
|
||||
fileReady?: boolean
|
||||
fileStatus?: string
|
||||
fileProgressPercent?: number
|
||||
fileProgressCurrent?: number
|
||||
fileProgressTotal?: number
|
||||
fileProgressMessage?: string
|
||||
downloadUrl?: string
|
||||
sourceFilename?: string
|
||||
}
|
||||
|
||||
const historyMissingProgress: HistoryItem = {
|
||||
taskId: 1,
|
||||
resultId: 100,
|
||||
fileStatus: 'RUNNING',
|
||||
sourceFilename: 'source.xlsx',
|
||||
}
|
||||
|
||||
const liveWithProgress: HistoryItem = {
|
||||
taskId: 1,
|
||||
resultId: 100,
|
||||
fileStatus: 'RUNNING',
|
||||
fileProgressPercent: 45,
|
||||
fileProgressCurrent: 9,
|
||||
fileProgressTotal: 20,
|
||||
fileProgressMessage: 'LLM 处理中',
|
||||
}
|
||||
|
||||
test('merge keeps live progress when history missing file progress', () => {
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, liveWithProgress)
|
||||
assert.equal(merged.fileProgressPercent, 45, 'history 缺失进度时保留 live 的百分比')
|
||||
assert.equal(merged.fileProgressCurrent, 9)
|
||||
assert.equal(merged.fileProgressTotal, 20)
|
||||
assert.equal(merged.fileProgressMessage, 'LLM 处理中')
|
||||
assert.equal(merged.sourceFilename, 'source.xlsx', '非进度字段保留 history')
|
||||
})
|
||||
|
||||
test('merge incoming wins when live has progress', () => {
|
||||
const history = { ...historyMissingProgress, fileProgressPercent: 10, fileProgressMessage: '旧消息' }
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(history, liveWithProgress)
|
||||
assert.equal(merged.fileProgressPercent, 45, 'live 有有效进度时以 live 为准')
|
||||
assert.equal(merged.fileProgressMessage, 'LLM 处理中')
|
||||
})
|
||||
|
||||
test('merge does not keep live when history is downloadable', () => {
|
||||
const history: HistoryItem = {
|
||||
taskId: 1,
|
||||
resultId: 100,
|
||||
fileReady: true,
|
||||
fileStatus: 'SUCCESS',
|
||||
fileProgressPercent: 100,
|
||||
downloadUrl: 'https://dl.example/a.xlsx',
|
||||
}
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(history, liveWithProgress)
|
||||
assert.equal(merged.fileProgressPercent, 100, '可下载时保留 history 的终态进度')
|
||||
assert.equal(merged.downloadUrl, 'https://dl.example/a.xlsx')
|
||||
assert.equal(merged.fileReady, true)
|
||||
})
|
||||
|
||||
test('merge without live returns history as-is', () => {
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, undefined)
|
||||
assert.equal(merged, historyMissingProgress, '无 live 时原样返回 history')
|
||||
assert.equal(merged.fileProgressPercent, undefined)
|
||||
})
|
||||
|
||||
test('merge partial fields keeps only present live values', () => {
|
||||
const live: HistoryItem = { taskId: 1, resultId: 100, fileStatus: 'RUNNING', fileProgressPercent: 30 }
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, live)
|
||||
assert.equal(merged.fileProgressPercent, 30)
|
||||
assert.equal(merged.fileProgressCurrent, undefined, 'live 缺失的字段不写入')
|
||||
assert.equal(merged.fileProgressTotal, undefined)
|
||||
assert.equal(merged.fileStatus, 'RUNNING')
|
||||
})
|
||||
|
||||
test('merge ignores meaningless progress values', () => {
|
||||
assert.equal(hasMeaningfulProgress('fileProgressPercent', 0), false, '0% 不算有效进度')
|
||||
assert.equal(hasMeaningfulProgress('fileProgressPercent', -1), false)
|
||||
assert.equal(hasMeaningfulProgress('fileProgressPercent', 5), true)
|
||||
assert.equal(hasMeaningfulProgress('fileProgressCurrent', 0), true, '0 是合法计数')
|
||||
assert.equal(hasMeaningfulProgress('fileProgressMessage', null), false)
|
||||
assert.equal(hasMeaningfulProgress('fileProgressMessage', undefined), false)
|
||||
|
||||
const live: HistoryItem = { taskId: 1, resultId: 100, fileProgressPercent: 0 }
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, live)
|
||||
assert.equal(merged.fileProgressPercent, undefined, '0% 不覆盖 history')
|
||||
})
|
||||
|
||||
test('merge preserves fileReady flag from live', () => {
|
||||
const live: HistoryItem = { taskId: 1, resultId: 100, fileReady: true, fileProgressPercent: 100 }
|
||||
const merged = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, live)
|
||||
assert.equal(merged.fileReady, true)
|
||||
assert.equal(merged.fileProgressPercent, 100)
|
||||
})
|
||||
|
||||
test('merge is idempotent', () => {
|
||||
const once = mergeHistoryItemPreservingLiveProgress(historyMissingProgress, liveWithProgress)
|
||||
const twice = mergeHistoryItemPreservingLiveProgress(once, liveWithProgress)
|
||||
assert.deepEqual(twice, once, '重复合并结果一致')
|
||||
})
|
||||
|
||||
test('file progress keys cover the live progress contract', () => {
|
||||
assert.deepEqual(
|
||||
[...FILE_PROGRESS_KEYS].sort(),
|
||||
[
|
||||
'fileProgressCurrent',
|
||||
'fileProgressMessage',
|
||||
'fileProgressPercent',
|
||||
'fileProgressTotal',
|
||||
'fileReady',
|
||||
'fileStatus',
|
||||
].sort(),
|
||||
)
|
||||
})
|
||||
|
||||
test('canDownloadItem requires result and ready state', () => {
|
||||
assert.equal(canDownloadItem({ resultId: 1, fileReady: true }), true)
|
||||
assert.equal(canDownloadItem({ resultId: 1, downloadUrl: 'https://x' }), true)
|
||||
assert.equal(canDownloadItem({ resultId: 1 }), false)
|
||||
assert.equal(canDownloadItem({ fileReady: true }), false, '无 resultId 不可下载')
|
||||
assert.equal(canDownloadItem(null), false)
|
||||
assert.equal(canDownloadItem(undefined), false)
|
||||
})
|
||||
Reference in New Issue
Block a user