task-140: 13 模块 file 字段契约快照(fileStatus/fileError/fileReady 类型与白名单冻结、旧字段仍在、collectdata 自洽)+ 8 条守门测试

This commit is contained in:
2026-09-02 05:32:06 +08:00
parent 4fb33bda1c
commit b7a403dc22
@@ -0,0 +1,129 @@
package com.nanri.aiimage.modules.task.contract;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryItemVo;
import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinTaskLightVo;
import com.nanri.aiimage.modules.task.model.vo.TaskProgressLightBatchVo;
import com.nanri.aiimage.modules.task.model.vo.TaskProgressLightVo;
import org.junit.jupiter.api.Test;
import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* task-14013 模块 file 阶段字段契约快照(基线固化,plan 08)。
* 共享 TaskProgressLightVo11 模块)+ SimilarAsinTaskLightVo 的 file 字段
* fileStatus/fileError/fileReady)存在且类型正确;旧字段
* taskId/resultId/downloadUrl/status)仍在;JSON 序列化快照冻结白名单。
*/
class ResultFilePhaseContractTest {
/** 13 个业务模块的共享 light VOcollectdata 走同一共享 VO,自洽语义见 test 5)。 */
private static final Set<String> SHARED_LIGHT_MODULES = Set.of(
"APPEARANCE_PATENT", "SHOP_DATA_CRAWL", "QUERY_ASIN", "WITHDRAW",
"PATROL_DELETE", "SHOP_MATCH", "PRODUCT_RISK_RESOLVE", "PRICE_TRACK",
"DELETE_BRAND", "PUBLISH", "COLLECT_DATA", "BRAND");
private final ObjectMapper objectMapper = new ObjectMapper();
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));
}
private static void assertFileFields(Class<?> voType) {
Map<String, Class<?>> fields = fieldsOf(voType);
assertEquals(String.class, fields.get("fileStatus"), voType.getSimpleName() + ".fileStatus 必须为 String");
assertEquals(String.class, fields.get("fileError"), voType.getSimpleName() + ".fileError 必须为 String");
assertEquals(Boolean.class, fields.get("fileReady"), voType.getSimpleName() + ".fileReady 必须为 Boolean");
}
@Test
void similarAsinHasFileFields() {
assertFileFields(SimilarAsinTaskLightVo.class);
}
@Test
void appearancePatentHasFileFields() {
assertFileFields(TaskProgressLightVo.class);
}
@Test
void shopDataCrawlHasFileFields() {
assertFileFields(TaskProgressLightVo.class);
}
@Test
void publishHasFileFields() {
// publish 文件级:共享 VO 的 file 字段由 Job 推导(TaskProgressLightAssembler 语义)
assertFileFields(TaskProgressLightVo.class);
assertTrue(SHARED_LIGHT_MODULES.contains("PUBLISH"), "publish 必须接入共享 light VO");
}
@Test
void collectDataIsSelfConsistent() {
// collectdata 与 11 模块共享同一 TaskProgressLightVofile 字段齐全、语义自洽)
assertFileFields(TaskProgressLightVo.class);
Map<String, Class<?>> fields = fieldsOf(TaskProgressLightVo.class);
assertEquals(Long.class, fields.get("taskId"));
assertEquals(String.class, fields.get("status"));
assertNotNull(fields.get("updatedAt"));
}
@Test
void oldFieldsStillPresent() {
Map<String, Class<?>> fields = fieldsOf(SimilarAsinHistoryItemVo.class);
assertEquals(Long.class, fields.get("taskId"), "旧字段 taskId 仍在");
assertEquals(Long.class, fields.get("resultId"), "旧字段 resultId 仍在");
assertEquals(String.class, fields.get("downloadUrl"), "旧字段 downloadUrl 仍在");
assertEquals(String.class, fields.get("taskStatus"), "旧字段 taskStatus 仍在(状态语义不变)");
assertEquals(String.class, fields.get("fileStatus"), "history 文件级字段仍在");
assertEquals(Boolean.class, fields.get("fileReady"), "history fileReady 仍在");
}
@Test
void fileFieldTypesAreCorrect() {
assertFileFields(TaskProgressLightVo.class);
Map<String, Class<?>> similar = fieldsOf(SimilarAsinTaskLightVo.class);
assertEquals(String.class, similar.get("fileStatus"));
assertEquals(Boolean.class, similar.get("fileReady"));
assertEquals(String.class, similar.get("fileError"));
assertEquals(String.class, similar.get("status"));
assertEquals(Long.class, similar.get("taskId"));
}
@Test
void contractFrozenAsJsonSnapshot() throws Exception {
TaskProgressLightVo item = new TaskProgressLightVo();
item.setTaskId(3938L);
item.setStatus("SUCCESS");
item.setFileStatus("SUCCESS");
item.setFileError(null);
item.setFileReady(true);
item.setUpdatedAt("2026-09-02T05:00:00");
TaskProgressLightBatchVo batch = new TaskProgressLightBatchVo();
batch.setItems(java.util.List.of(item));
batch.setMissingTaskIds(java.util.List.of());
String json = objectMapper.writeValueAsString(batch);
@SuppressWarnings("unchecked")
Map<String, Object> root = objectMapper.readValue(json, Map.class);
assertTrue(root.containsKey("items"), "响应必须含 items");
@SuppressWarnings("unchecked")
Map<String, Object> first = ((java.util.List<Map<String, Object>>) root.get("items")).get(0);
assertEquals(Set.of("taskId", "status", "statusCode", "fileStatus", "fileError", "fileReady", "updatedAt"),
first.keySet(), "light 响应键集必须精确匹配白名单(无明细/payload/result 内容)");
assertEquals(Boolean.TRUE, first.get("fileReady"));
assertEquals("SUCCESS", first.get("fileStatus"));
assertEquals("SUCCESS", first.get("status"));
}
}