diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FilePhaseSemanticsTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FilePhaseSemanticsTest.java new file mode 100644 index 00000000..cca15ec6 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FilePhaseSemanticsTest.java @@ -0,0 +1,168 @@ +package com.nanri.aiimage.modules.similarasin.service.support; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.similarasin.model.vo.SimilarAsinHistoryItemVo; +import com.nanri.aiimage.modules.task.mapper.FileTaskMapper; +import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper; +import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper; +import com.nanri.aiimage.modules.task.model.entity.FileResultEntity; +import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity; +import com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity; +import com.nanri.aiimage.modules.task.service.TaskProgressSnapshotService; +import com.nanri.aiimage.modules.task.service.TransientPayloadStorageService; +import org.apache.ibatis.builder.MapperBuilderAssistant; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * task-142:fileReady/fileStatus 语义契约(plan 08)。 + * fileReady = resultFileUrl 非空;无 job 时 fileStatus = ready?SUCCESS:null; + * 有 job 时 fileStatus = job.status;fileError = job.errorMessage。 + */ +@ExtendWith(MockitoExtension.class) +class FilePhaseSemanticsTest { + + private static final Long TASK_ID = 1212L; + private static final Long RESULT_ID = 1213L; + + @Mock private TaskScopeStateMapper taskScopeStateMapper; + @Mock private TaskChunkMapper taskChunkMapper; + @Mock private FileTaskMapper fileTaskMapper; + @Mock private TaskProgressSnapshotService taskProgressSnapshotService; + @Mock private OssStorageService ossStorageService; + @Mock private TransientPayloadStorageService transientPayloadStorageService; + @Mock private ObjectMapper objectMapper; + + private SimilarAsinHistoryAssembler assembler; + + @BeforeAll + static void initializeMybatisMetadata() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, FileResultEntity.class); + TableInfoHelper.initTableInfo(assistant, TaskScopeStateEntity.class); + } + + @BeforeEach + void setUp() { + assembler = new SimilarAsinHistoryAssembler(taskScopeStateMapper, taskChunkMapper, fileTaskMapper, + taskProgressSnapshotService, ossStorageService, transientPayloadStorageService, objectMapper); + lenient().when(taskScopeStateMapper.selectCount(any())).thenReturn(0L); + lenient().when(taskScopeStateMapper.selectList(any())).thenReturn(List.of()); + lenient().when(taskProgressSnapshotService.find(any(), any())).thenReturn(null); + lenient().when(taskChunkMapper.selectCount(any())).thenReturn(0L); + lenient().when(ossStorageService.generateFreshDownloadUrl(any())).thenReturn("https://dl.example/x.xlsx"); + } + + private SimilarAsinHistoryItemVo build(FileResultEntity row, TaskFileJobEntity job) { + FileTaskEntity task = new FileTaskEntity(); + task.setId(TASK_ID); + task.setModuleType("SIMILAR_ASIN"); + task.setStatus("RUNNING"); + return assembler.buildHistoryItems(List.of(row), Map.of(TASK_ID, task), + job == null ? Map.of() : Map.of(RESULT_ID, job)).getFirst(); + } + + private FileResultEntity row(String url) { + FileResultEntity row = new FileResultEntity(); + row.setId(RESULT_ID); + row.setTaskId(TASK_ID); + row.setSourceFilename("source.xlsx"); + row.setResultFileUrl(url); + row.setSuccess(0); + return row; + } + + private TaskFileJobEntity job(String status, String error) { + TaskFileJobEntity job = new TaskFileJobEntity(); + job.setId(901L); + job.setTaskId(TASK_ID); + job.setResultId(RESULT_ID); + job.setStatus(status); + job.setErrorMessage(error); + return job; + } + + @Test + void readyTrueWhenUrlPresent() { + SimilarAsinHistoryItemVo vo = build(row("oss://result/a.xlsx"), null); + + assertEquals(Boolean.TRUE, vo.getFileReady()); + } + + @Test + void readyFalseWhenUrlBlank() { + SimilarAsinHistoryItemVo vo = build(row(null), null); + + assertFalse(Boolean.TRUE.equals(vo.getFileReady())); + } + + @Test + void noJobWithReadyMapsToSuccessStatus() { + SimilarAsinHistoryItemVo vo = build(row("oss://result/a.xlsx"), null); + + assertEquals("SUCCESS", vo.getFileStatus(), "无 job 且 ready 时 fileStatus=SUCCESS"); + } + + @Test + void noJobWithoutReadyMapsToNullStatus() { + SimilarAsinHistoryItemVo vo = build(row(null), null); + + assertNull(vo.getFileStatus(), "无 job 且未 ready 时 fileStatus=null"); + assertNull(vo.getFileJobId()); + } + + @Test + void jobStatusMappedDirectly() { + SimilarAsinHistoryItemVo vo = build(row("oss://result/a.xlsx"), job("RUNNING", null)); + + assertEquals("RUNNING", vo.getFileStatus(), "有 job 时 fileStatus=job.status"); + assertEquals(901L, vo.getFileJobId()); + } + + @Test + void jobErrorMappedDirectly() { + SimilarAsinHistoryItemVo vo = build(row(null), job("FAILED", "组装失败: 超时")); + + assertEquals("组装失败: 超时", vo.getFileError(), "fileError=job.errorMessage"); + assertEquals("FAILED", vo.getFileStatus()); + } + + @Test + void blankWhitespaceUrlIsNotReady() { + SimilarAsinHistoryItemVo vo = build(row(" "), null); + + assertFalse(Boolean.TRUE.equals(vo.getFileReady()), "空白字符串不算 ready"); + assertNull(vo.getFileStatus()); + } + + @Test + void semanticsFrozenCombined() { + // 组合快照:URL + SUCCESS job → ready=true、status=SUCCESS、error 映射、jobId 携带 + SimilarAsinHistoryItemVo vo = build(row("oss://result/a.xlsx"), job("SUCCESS", null)); + + assertEquals(Boolean.TRUE, vo.getFileReady()); + assertEquals("SUCCESS", vo.getFileStatus()); + assertNull(vo.getFileError()); + assertEquals(901L, vo.getFileJobId()); + assertTrue(vo.getFileReady()); + } +}