diff --git a/frontend-vue/src/shared/merge-live-progress.ts b/frontend-vue/src/shared/merge-live-progress.ts index de26559e..530dcd07 100644 --- a/frontend-vue/src/shared/merge-live-progress.ts +++ b/frontend-vue/src/shared/merge-live-progress.ts @@ -17,13 +17,20 @@ export const FILE_PROGRESS_KEYS = [ export interface DownloadableItem { resultId?: number | null fileReady?: boolean | null + fileStatus?: string | null downloadUrl?: string | null } -/** 可下载判定:有结果记录且文件已就绪(fileReady 或 downloadUrl 非空)。 */ +/** + * 可下载判定(Task 145):有结果记录,且满足其一—— + * fileReady=true、downloadUrl 非空、fileStatus 为终态(SUCCESS 可下载结果文件, + * FAILED 可下载错误文件或用于展示错误)。 + */ export function canDownloadItem(item: DownloadableItem | null | undefined): boolean { if (!item) return false - return Boolean(item.resultId && (item.fileReady || item.downloadUrl)) + const ready = item.fileReady || Boolean(item.downloadUrl) + const terminalStatus = item.fileStatus === 'SUCCESS' || item.fileStatus === 'FAILED' + return Boolean(item.resultId && (ready || terminalStatus)) } /** 有效进度值:仅 fileProgressPercent 需要严格 >0,其余字段非空即可(0 是合法计数)。 */ diff --git a/frontend-vue/tests/downloadability.test.ts b/frontend-vue/tests/downloadability.test.ts new file mode 100644 index 00000000..e09c055b --- /dev/null +++ b/frontend-vue/tests/downloadability.test.ts @@ -0,0 +1,60 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' + +import { canDownloadItem } from '../src/shared/merge-live-progress.ts' + +interface Item { + taskId?: number + resultId?: number + fileReady?: boolean + fileStatus?: string + downloadUrl?: string +} + +test('downloadable when fileReady is true', () => { + assert.equal(canDownloadItem({ resultId: 1, fileReady: true }), true) +}) + +test('downloadable when fileStatus is SUCCESS', () => { + assert.equal(canDownloadItem({ resultId: 1, fileStatus: 'SUCCESS' }), true) +}) + +test('downloadable when fileStatus is FAILED (error file or display)', () => { + assert.equal(canDownloadItem({ resultId: 1, fileStatus: 'FAILED' }), true) +}) + +test('not downloadable when not ready', () => { + assert.equal(canDownloadItem({ resultId: 1, fileReady: false, fileStatus: 'RUNNING' }), false) + assert.equal(canDownloadItem({ resultId: 1 }), false) +}) + +test('downloadable when downloadUrl present', () => { + assert.equal(canDownloadItem({ resultId: 1, downloadUrl: 'https://dl.example/a.xlsx' }), true) +}) + +test('not downloadable without resultId', () => { + assert.equal(canDownloadItem({ fileReady: true }), false) + assert.equal(canDownloadItem({ fileStatus: 'SUCCESS' }), false) +}) + +test('not downloadable while pending or running', () => { + assert.equal(canDownloadItem({ resultId: 1, fileStatus: 'PENDING' }), false) + assert.equal(canDownloadItem({ resultId: 1, fileStatus: 'RUNNING' }), false) + assert.equal(canDownloadItem({ resultId: 1, fileStatus: null }), false) +}) + +test('downloadability contract frozen', () => { + assert.equal(canDownloadItem(undefined), false) + assert.equal(canDownloadItem(null), false) + assert.equal(canDownloadItem({}), false) + // 终态与就绪任一满足即可下载;RUNNING/PENDING/空态均不可 + const terminalCases = ['SUCCESS', 'FAILED'] + const runningCases = ['PENDING', 'RUNNING', undefined, null] + for (const status of terminalCases) { + assert.equal(canDownloadItem({ resultId: 1, fileStatus: status as string }), true, `终态 ${status} 可下载`) + } + for (const status of runningCases) { + assert.equal(canDownloadItem({ resultId: 1, fileStatus: status as string | undefined }), false, + `非终态 ${String(status)} 不可下载`) + } +})