task-146: collectdata 自洽行为契约(后端无 file 字段 VO 快照、前端 success+downloadUrl 判定含空白边界)+ 前后端 13 条测试
This commit is contained in:
+102
@@ -0,0 +1,102 @@
|
||||
package com.nanri.aiimage.modules.task.contract;
|
||||
|
||||
import com.nanri.aiimage.modules.collectdata.model.vo.CollectDataHistoryItemVo;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* task-146:collectdata 自洽行为契约(plan 08)。
|
||||
* 后端 history VO 不返回 file 阶段字段(fileStatus/fileReady/fileError);
|
||||
* 下载语义由 success + downloadUrl 推导(任务 SUCCESS 且有结果文件才可下载);
|
||||
* 进度展示字段自洽。
|
||||
*/
|
||||
class CollectDataSelfConsistencyTest {
|
||||
|
||||
private static Map<String, Class<?>> fieldsOf(Class<?> type) {
|
||||
return Arrays.stream(type.getDeclaredFields())
|
||||
.filter(field -> !java.lang.reflect.Modifier.isStatic(field.getModifiers()))
|
||||
.collect(Collectors.toMap(Field::getName, Field::getType));
|
||||
}
|
||||
|
||||
@Test
|
||||
void backendHasNoFilePhaseFields() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertFalse(fields.containsKey("fileStatus"), "collectdata 不返回 fileStatus");
|
||||
assertFalse(fields.containsKey("fileReady"), "collectdata 不返回 fileReady");
|
||||
assertFalse(fields.containsKey("fileError"), "collectdata 不返回 fileError");
|
||||
assertFalse(fields.containsKey("fileProgressPercent"), "collectdata 不返回 fileProgressPercent");
|
||||
}
|
||||
|
||||
@Test
|
||||
void downloadFieldsPresent() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertEquals(String.class, fields.get("downloadUrl"), "downloadUrl 字段存在");
|
||||
assertEquals(Boolean.class, fields.get("success"), "success 字段存在");
|
||||
}
|
||||
|
||||
@Test
|
||||
void statusFlowFieldsPresent() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertEquals(String.class, fields.get("taskStatus"), "taskStatus 字段存在");
|
||||
assertEquals(String.class, fields.get("error"), "error 字段存在");
|
||||
assertEquals(Long.class, fields.get("taskId"));
|
||||
assertEquals(Long.class, fields.get("resultId"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void progressDisplayFieldsPresent() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertEquals(Integer.class, fields.get("dedupeFilteredCount"), "进度展示字段存在");
|
||||
assertEquals(Integer.class, fields.get("invalidFilteredCount"));
|
||||
assertEquals(Integer.class, fields.get("brandRejectedCount"));
|
||||
assertEquals(Integer.class, fields.get("finalRowCount"));
|
||||
assertEquals(Integer.class, fields.get("rowCount"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void successAndUrlAreTheOnlyDownloadSignals() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
// 可下载信号仅 success + downloadUrl(无 fileReady 类独立标志)
|
||||
assertTrue(fields.containsKey("success"));
|
||||
assertTrue(fields.containsKey("downloadUrl"));
|
||||
assertFalse(fields.containsKey("fileReady"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void fileFieldsAbsentEvenInSharedLightVoContract() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertFalse(fields.containsKey("fileJobId"), "collectdata 无 fileJobId");
|
||||
assertFalse(fields.containsKey("fileJobStatus"), "collectdata 无 fileJobStatus");
|
||||
}
|
||||
|
||||
@Test
|
||||
void historyIdentityFieldsPresent() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
assertEquals(String.class, fields.get("sourceFilename"));
|
||||
assertEquals(String.class, fields.get("resultFilename"));
|
||||
assertEquals(String.class, fields.get("taskNo"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void contractFrozenFieldSet() {
|
||||
Map<String, Class<?>> fields = fieldsOf(CollectDataHistoryItemVo.class);
|
||||
// 快照:字段集合固定(下载用 success+downloadUrl,无 file 阶段字段)
|
||||
java.util.Set<String> expected = java.util.Set.of(
|
||||
"resultId", "taskId", "taskNo", "sourceFilename", "resultFilename", "downloadUrl",
|
||||
"taskStatus", "success", "error", "rowCount", "dedupeFilteredCount",
|
||||
"invalidFilteredCount", "brandRejectedCount", "finalRowCount",
|
||||
"taskType", "progressPercent", "totalRows", "receivedRows", "processedRows",
|
||||
"collectStage", "currentKeyword", "searchCurrentPage", "searchTotalPages",
|
||||
"detailProcessedAsins", "detailTotalAsins", "filters",
|
||||
"createdAt", "startedAt", "finishedAt");
|
||||
assertEquals(expected, fields.keySet(), "collectdata history VO 字段集合冻结");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* collectdata 下载判定(Task 146)。
|
||||
*
|
||||
* collectdata 后端 history VO 不返回 file 阶段字段;前端下载判定自洽:
|
||||
* success=true 且 downloadUrl 非空才可下载(任务成功且有结果文件)。
|
||||
*/
|
||||
export interface CollectDataItem {
|
||||
success?: boolean | null
|
||||
downloadUrl?: string | null
|
||||
taskStatus?: string | null
|
||||
}
|
||||
|
||||
/** collectdata 可下载判定:success + downloadUrl(任务成功且有结果文件,空白 URL 无效)。 */
|
||||
export function canDownloadCollectDataItem(item: CollectDataItem | null | undefined): boolean {
|
||||
if (!item) return false
|
||||
const url = typeof item.downloadUrl === 'string' ? item.downloadUrl.trim() : ''
|
||||
return Boolean(item.success && url)
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
|
||||
import { canDownloadCollectDataItem } from '../src/shared/collect-download.ts'
|
||||
|
||||
interface Item {
|
||||
success?: boolean | null
|
||||
downloadUrl?: string | null
|
||||
taskStatus?: string | null
|
||||
}
|
||||
|
||||
test('collectdata downloadable when success and url present', () => {
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'https://dl.example/a.xlsx' }), true)
|
||||
})
|
||||
|
||||
test('collectdata not downloadable without url', () => {
|
||||
assert.equal(canDownloadCollectDataItem({ success: true }), false)
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: null }), false)
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: '' }), false)
|
||||
})
|
||||
|
||||
test('collectdata failed task not downloadable', () => {
|
||||
assert.equal(canDownloadCollectDataItem({ success: false, downloadUrl: 'https://dl.example/a.xlsx' }), false)
|
||||
})
|
||||
|
||||
test('collectdata url must be non-blank to be valid', () => {
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: ' ' }), false)
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'https://x' }), true)
|
||||
})
|
||||
|
||||
test('collectdata progress display fields do not include file phase', () => {
|
||||
const item: Item = { success: false, taskStatus: 'RUNNING' }
|
||||
assert.equal('fileStatus' in item, false, 'collectdata 无 file 阶段字段(与后端 VO 自洽)')
|
||||
assert.equal('fileReady' in item, false)
|
||||
assert.equal('fileProgressPercent' in item, false)
|
||||
})
|
||||
|
||||
test('collectdata semantics frozen', () => {
|
||||
assert.equal(canDownloadCollectDataItem(undefined), false)
|
||||
assert.equal(canDownloadCollectDataItem(null), false)
|
||||
assert.equal(canDownloadCollectDataItem({}), false)
|
||||
assert.equal(canDownloadCollectDataItem({ success: true, downloadUrl: 'x', taskStatus: 'SUCCESS' }), true)
|
||||
})
|
||||
Reference in New Issue
Block a user