task-150: 清理前引用检查契约(chunk 共享/scope 引用保留、自身行可清、异常保守保留、只读无副作用、批量 IN 反查)+ 8 条测试

This commit is contained in:
2026-09-02 06:19:42 +08:00
parent dd569edc66
commit ffc3963345
@@ -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<LambdaQueryWrapper> captor = ArgumentCaptor.forClass(LambdaQueryWrapper.class);
verify(taskChunkMapper).selectCount(captor.capture());
String segment = captor.getValue().getSqlSegment();
assertTrue(segment.contains("IN"), "候选值必须批量 IN 查询: " + segment);
// 候选集 = value + pointerjson(pointer) 与 value 同值去重),IN 参数非空
assertTrue(!captor.getValue().getParamNameValuePairs().isEmpty(),
"IN 查询必须携带候选参数");
}
}