From 9e58d28a9943651ab03dfc020b47c4db4bdf6337 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Wed, 2 Sep 2026 05:42:32 +0800 Subject: [PATCH] =?UTF-8?q?task-142:=20fileReady/fileStatus=20=E8=AF=AD?= =?UTF-8?q?=E4=B9=89=E5=A5=91=E7=BA=A6=EF=BC=88URL=20=E5=88=A4=E5=AE=9A?= =?UTF-8?q?=E3=80=81=E6=97=A0=20job=20SUCCESS/null=E3=80=81job=20=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E4=B8=8E=E9=94=99=E8=AF=AF=E6=98=A0=E5=B0=84=E3=80=81?= =?UTF-8?q?=E7=A9=BA=E7=99=BD=E8=BE=B9=E7=95=8C=EF=BC=89+=208=20=E6=9D=A1?= =?UTF-8?q?=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../support/FilePhaseSemanticsTest.java | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FilePhaseSemanticsTest.java 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()); + } +}