diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FileProgressSynthesisTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FileProgressSynthesisTest.java new file mode 100644 index 00000000..12f98975 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/similarasin/service/support/FileProgressSynthesisTest.java @@ -0,0 +1,212 @@ +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.TaskProgressSnapshotEntity; +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.time.LocalDateTime; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.when; + +/** + * task-141:similarasin file 进度合成逻辑单测(plan 08)。 + * attachFileProgress 三路径:LLM 阶段(completed/pending>0)、Python 上传阶段、 + * snapshot 百分比(上传完成无 LLM);fileReady=true 时恒 100%;边界安全。 + */ +@ExtendWith(MockitoExtension.class) +class FileProgressSynthesisTest { + + private static final Long TASK_ID = 2222L; + private static final Long RESULT_ID = 2223L; + + @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(Runnable configure, FileResultEntity row, FileTaskEntity task, + TaskFileJobEntity job) { + configure.run(); + return assembler.buildHistoryItems(List.of(row), + task == null ? Map.of() : Map.of(TASK_ID, task), + job == null ? Map.of() : Map.of(RESULT_ID, job)).getFirst(); + } + + private FileResultEntity resultRow() { + FileResultEntity row = new FileResultEntity(); + row.setId(RESULT_ID); + row.setTaskId(TASK_ID); + row.setSourceFilename("source.xlsx"); + row.setResultFileUrl(null); + row.setSuccess(0); + return row; + } + + private FileTaskEntity runningTask() { + FileTaskEntity task = new FileTaskEntity(); + task.setId(TASK_ID); + task.setModuleType("SIMILAR_ASIN"); + task.setStatus("RUNNING"); + return task; + } + + @Test + void fileReadyPathIsAlwaysOneHundredPercent() { + FileResultEntity row = resultRow(); + row.setResultFileUrl("oss://result/source-result.xlsx"); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, row, runningTask(), null); + + assertEquals(100, vo.getFileProgressPercent()); + assertEquals(1, vo.getFileProgressCurrent()); + assertEquals(1, vo.getFileProgressTotal()); + assertEquals("结果文件已生成", vo.getFileProgressMessage()); + assertEquals(Boolean.TRUE, vo.getFileReady()); + } + + @Test + void llmProgressPathWhenCompletedExists() { + when(taskScopeStateMapper.selectCount(any())).thenReturn(2L, 0L); + lenient().when(taskChunkMapper.selectCount(any())).thenReturn(1L); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNotNull(vo.getFileProgressPercent(), "LLM 阶段必须合成进度"); + assertEquals(2, vo.getFileProgressCurrent()); + } + + @Test + void pythonUploadPathWhenNoLlmStates() { + lenient().when(taskChunkMapper.selectCount(any())).thenReturn(1L); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNotNull(vo.getFileProgressPercent(), "Python 上传阶段必须合成进度"); + assertNotNull(vo.getFileProgressMessage()); + } + + @Test + void snapshotPercentPathWhenUploadComplete() { + TaskScopeStateEntity scope = new TaskScopeStateEntity(); + scope.setTaskId(TASK_ID); + scope.setCompleted(1); + when(taskScopeStateMapper.selectList(any())).thenReturn(List.of(scope)); + TaskProgressSnapshotEntity snapshot = new TaskProgressSnapshotEntity(); + snapshot.setTotalCount(10); + snapshot.setSuccessCount(5); + snapshot.setMessage("组装中"); + when(taskProgressSnapshotService.find(eq(TASK_ID), eq("SIMILAR_ASIN"))).thenReturn(snapshot); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNotNull(vo.getFileProgressPercent(), "上传完成无 LLM 时走 snapshot 百分比"); + assertEquals(5, vo.getFileProgressCurrent()); + assertEquals(10, vo.getFileProgressTotal()); + } + + @Test + void pendingOnlyStillUsesLlmPath() { + when(taskScopeStateMapper.selectCount(any())).thenReturn(0L, 2L); + lenient().when(taskChunkMapper.selectCount(any())).thenReturn(1L); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNotNull(vo.getFileProgressPercent(), "pending>0 也走 LLM 路径"); + } + + @Test + void uploadCompleteWithoutLlmOrSnapshotSetsNoProgress() { + TaskScopeStateEntity scope = new TaskScopeStateEntity(); + scope.setTaskId(TASK_ID); + scope.setCompleted(1); + when(taskScopeStateMapper.selectList(any())).thenReturn(List.of(scope)); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNull(vo.getFileProgressPercent(), "无 LLM 无 snapshot 不合成进度"); + } + + @Test + void nonRunningTaskWithoutProgressSkipsSynthesis() { + FileTaskEntity failed = runningTask(); + failed.setStatus("FAILED"); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), failed, null); + + assertNull(vo.getFileProgressPercent(), "非 RUNNING 任务不合成上传进度"); + } + + @Test + void zeroTotalSnapshotIsDivisionSafe() { + TaskScopeStateEntity scope = new TaskScopeStateEntity(); + scope.setTaskId(TASK_ID); + scope.setCompleted(1); + when(taskScopeStateMapper.selectList(any())).thenReturn(List.of(scope)); + TaskProgressSnapshotEntity snapshot = new TaskProgressSnapshotEntity(); + snapshot.setTotalCount(0); + snapshot.setSuccessCount(0); + when(taskProgressSnapshotService.find(eq(TASK_ID), eq("SIMILAR_ASIN"))).thenReturn(snapshot); + + SimilarAsinHistoryItemVo vo = build(() -> { + }, resultRow(), runningTask(), null); + + assertNull(vo.getFileProgressPercent(), "total=0 时不除零不合成"); + } +}