From ffc3963345f0208bd07c2c1c21ed7e5bef5ac399 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 06:19:42 +0800 Subject: [PATCH] =?UTF-8?q?task-150:=20=E6=B8=85=E7=90=86=E5=89=8D?= =?UTF-8?q?=E5=BC=95=E7=94=A8=E6=A3=80=E6=9F=A5=E5=A5=91=E7=BA=A6=EF=BC=88?= =?UTF-8?q?chunk=20=E5=85=B1=E4=BA=AB/scope=20=E5=BC=95=E7=94=A8=E4=BF=9D?= =?UTF-8?q?=E7=95=99=E3=80=81=E8=87=AA=E8=BA=AB=E8=A1=8C=E5=8F=AF=E6=B8=85?= =?UTF-8?q?=E3=80=81=E5=BC=82=E5=B8=B8=E4=BF=9D=E5=AE=88=E4=BF=9D=E7=95=99?= =?UTF-8?q?=E3=80=81=E5=8F=AA=E8=AF=BB=E6=97=A0=E5=89=AF=E4=BD=9C=E7=94=A8?= =?UTF-8?q?=E3=80=81=E6=89=B9=E9=87=8F=20IN=20=E5=8F=8D=E6=9F=A5=EF=BC=89+?= =?UTF-8?q?=208=20=E6=9D=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TaskPayloadReferenceCheckTest.java | 152 ++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskPayloadReferenceCheckTest.java diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskPayloadReferenceCheckTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskPayloadReferenceCheckTest.java new file mode 100644 index 00000000..40642e49 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/TaskPayloadReferenceCheckTest.java @@ -0,0 +1,152 @@ +package com.nanri.aiimage.modules.task.service; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.nanri.aiimage.config.InstanceMetadata; +import com.nanri.aiimage.config.StorageProperties; +import com.nanri.aiimage.config.TransientStorageProperties; +import com.nanri.aiimage.modules.file.service.object.RustfsObjectStorageService; +import com.nanri.aiimage.modules.file.service.oss.OssStorageService; +import com.nanri.aiimage.modules.task.mapper.TaskChunkMapper; +import com.nanri.aiimage.modules.task.mapper.TaskScopeStateMapper; +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.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * task-150:清理前引用检查契约(plan 09)。 + * payload 被 biz_task_chunk / biz_task_scope_state 引用则不清理; + * chunk 仅剩自身一行(count=1)不算共享引用;查询异常保守保留; + * 检查只读无副作用;候选值批量反查。 + */ +@ExtendWith(MockitoExtension.class) +class TaskPayloadReferenceCheckTest { + + private static final String RUSTFS_VALUE = "\"rustfs:payload-key\""; + private static final String RUSTFS_POINTER = "rustfs:payload-key"; + + @Mock private RustfsObjectStorageService rustfsObjectStorageService; + @Mock private TaskChunkMapper taskChunkMapper; + @Mock private TaskScopeStateMapper taskScopeStateMapper; + @Mock private OssStorageService ossStorageService; + + private TransientPayloadStorageService service; + + @BeforeAll + static void initializeMybatisMetadata() { + MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), ""); + TableInfoHelper.initTableInfo(assistant, com.nanri.aiimage.modules.task.model.entity.TaskChunkEntity.class); + TableInfoHelper.initTableInfo(assistant, com.nanri.aiimage.modules.task.model.entity.TaskScopeStateEntity.class); + } + + @BeforeEach + void setUp() { + TransientStorageProperties transientProperties = new TransientStorageProperties(); + transientProperties.setEnabled(true); + StorageProperties storageProperties = new StorageProperties(); + storageProperties.setLocalTempDir("target/tmp-refcheck"); + service = new TransientPayloadStorageService( + transientProperties, + storageProperties, + rustfsObjectStorageService, + ossStorageService, + new ObjectMapper(), + new InstanceMetadata("test-instance"), + taskChunkMapper, + taskScopeStateMapper); + org.mockito.Mockito.lenient().when(taskChunkMapper.selectCount(any())).thenReturn(0L); + org.mockito.Mockito.lenient().when(taskScopeStateMapper.selectCount(any())).thenReturn(0L); + } + + @Test + void referencedByChunkSharedIsKept() { + when(taskChunkMapper.selectCount(any())).thenReturn(2L); + + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService, never()).deleteObject(any()); + } + + @Test + void unreferencedCandidateIsDeleted() { + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService).deleteObject("payload-key"); + } + + @Test + void referencedByScopeStateIsKept() { + when(taskScopeStateMapper.selectCount(any())).thenReturn(1L); + + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService, never()).deleteObject(any()); + } + + @Test + void singleChunkRowIsOwnRowNotSharedReference() { + when(taskChunkMapper.selectCount(any())).thenReturn(1L); + + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService).deleteObject("payload-key"); + } + + @Test + void checkFailureKeepsPayloadConservatively() { + when(taskChunkMapper.selectCount(any())).thenThrow(new RuntimeException("db down")); + + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService, never()).deleteObject(any()); + } + + @Test + void checkIsReadOnlyNoSideEffects() { + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(taskChunkMapper).selectCount(any()); + verify(taskScopeStateMapper).selectCount(any()); + verify(rustfsObjectStorageService).deleteObject("payload-key"); + } + + @Test + void checkFailureOnScopeAlsoKeeps() { + when(taskScopeStateMapper.selectCount(any())).thenThrow(new RuntimeException("db down")); + + service.deletePayloadIfPresent(RUSTFS_VALUE); + + verify(rustfsObjectStorageService, never()).deleteObject(any()); + } + + @Test + void batchCandidatesQueriedInOneInClause() { + service.deletePayloadIfPresent(RUSTFS_VALUE); + + @SuppressWarnings({"rawtypes", "unchecked"}) + ArgumentCaptor captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class); + verify(taskChunkMapper).selectCount(captor.capture()); + String segment = captor.getValue().getSqlSegment(); + assertTrue(segment.contains("IN"), "候选值必须批量 IN 查询: " + segment); + // 候选集 = value + pointer(json(pointer) 与 value 同值去重),IN 参数非空 + assertTrue(!captor.getValue().getParamNameValuePairs().isEmpty(), + "IN 查询必须携带候选参数"); + } +}