task-165: 巡检只读契约(巡检器无删除/状态修改/写库方法集合断言、调度只调 inspect*、可重复、报表隔离)+ 8 条测试

This commit is contained in:
2026-09-02 07:42:10 +08:00
parent 34f614ea32
commit bde9e75eed
@@ -0,0 +1,150 @@
package com.nanri.aiimage.modules.task.service;
import com.nanri.aiimage.common.service.DistributedJobLockService;
import com.nanri.aiimage.config.InspectionProperties;
import com.nanri.aiimage.config.StorageProperties;
import com.nanri.aiimage.modules.file.service.TempOrphanInspector;
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.Arrays;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
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-165:巡检只读契约(plan 09)。
* 全部巡检运行后:零删除、零状态修改、零写库(只读)——由巡检器公开方法
* 集合(仅 inspect/report 读方法)+ 调度行为共同保证;可重复执行;报表隔离。
*/
@ExtendWith(MockitoExtension.class)
class InspectionReadOnlyTest {
@Mock private DistributedJobLockService distributedJobLockService;
@Mock private StorageProperties storageProperties;
@Mock private TempOrphanInspector tempOrphanInspector;
@Mock private OrphanJobInspector orphanJobInspector;
@Mock private TaskResultMissingInspector taskResultMissingInspector;
@Mock private ResultFileMissingInspector resultFileMissingInspector;
@Mock private CompletedTaskActiveJobInspector completedTaskActiveJobInspector;
private InspectionProperties properties;
private InspectionScheduler scheduler;
@BeforeEach
void setUp() {
properties = new InspectionProperties();
properties.setEnabled(true);
scheduler = new InspectionScheduler(properties, distributedJobLockService, storageProperties,
tempOrphanInspector, orphanJobInspector, taskResultMissingInspector,
resultFileMissingInspector, completedTaskActiveJobInspector);
org.mockito.Mockito.lenient().when(distributedJobLockService.tryLock(any(), any()))
.thenReturn(mock(DistributedJobLockService.LockHandle.class));
org.mockito.Mockito.lenient().when(orphanJobInspector.inspectOrphanJobs(anyInt())).thenReturn(
new OrphanJobInspector.OrphanJobReport(List.of()));
org.mockito.Mockito.lenient().when(taskResultMissingInspector.inspectTasksMissingResult(anyInt())).thenReturn(
new TaskResultMissingInspector.MissingResultReport(List.of()));
org.mockito.Mockito.lenient().when(resultFileMissingInspector.inspectResultsMissingFile(anyInt())).thenReturn(
new ResultFileMissingInspector.MissingFileReport(List.of()));
org.mockito.Mockito.lenient().when(completedTaskActiveJobInspector.inspectTerminalTasksWithActiveJobs(anyInt())).thenReturn(
new CompletedTaskActiveJobInspector.ActiveJobReport(List.of()));
org.mockito.Mockito.lenient().when(storageProperties.getLocalTempDir()).thenReturn("target/nonexistent-inspection-dir");
}
private static List<String> publicMethodNames(Class<?> type) {
return Arrays.stream(type.getMethods())
.filter(method -> method.getDeclaringClass() != Object.class)
.map(java.lang.reflect.Method::getName)
.toList();
}
@Test
void noDeleteMethodsOnInspectors() {
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
List<String> names = publicMethodNames(type);
assertTrue(names.stream().noneMatch(name ->
name.contains("delete") || name.contains("remove") || name.contains("clean")),
type.getSimpleName() + " 不得有删除方法: " + names);
}
}
@Test
void noStateChangeMethodsOnInspectors() {
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
List<String> names = publicMethodNames(type);
assertTrue(names.stream().noneMatch(name ->
name.contains("update") || name.contains("mark") || name.contains("set")),
type.getSimpleName() + " 不得有状态修改方法: " + names);
}
}
@Test
void noWriteMethodsOnInspectors() {
for (Class<?> type : List.of(OrphanJobInspector.class, TaskResultMissingInspector.class,
ResultFileMissingInspector.class, CompletedTaskActiveJobInspector.class)) {
List<String> names = publicMethodNames(type);
assertTrue(names.stream().noneMatch(name ->
name.contains("insert") || name.contains("write") || name.contains("save")),
type.getSimpleName() + " 不得有写库方法: " + names);
}
}
@Test
void repeatableReadsProduceSameReports() {
scheduler.runInspections();
scheduler.runInspections();
verify(orphanJobInspector, org.mockito.Mockito.times(2)).inspectOrphanJobs(anyInt());
verify(taskResultMissingInspector, org.mockito.Mockito.times(2)).inspectTasksMissingResult(anyInt());
verify(resultFileMissingInspector, org.mockito.Mockito.times(2)).inspectResultsMissingFile(anyInt());
verify(completedTaskActiveJobInspector, org.mockito.Mockito.times(2))
.inspectTerminalTasksWithActiveJobs(anyInt());
}
@Test
void reportsAreIsolatedPerInspection() {
scheduler.runInspections();
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
verify(taskResultMissingInspector).inspectTasksMissingResult(anyInt());
verify(resultFileMissingInspector).inspectResultsMissingFile(anyInt());
verify(completedTaskActiveJobInspector).inspectTerminalTasksWithActiveJobs(anyInt());
}
@Test
void emptyReportsDoNotWrite() {
scheduler.runInspections();
// 调度器只调用 inspect* 读方法(只读性由方法集合断言 + 调用面共同保证)
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
verify(taskResultMissingInspector).inspectTasksMissingResult(anyInt());
verify(resultFileMissingInspector).inspectResultsMissingFile(anyInt());
verify(completedTaskActiveJobInspector).inspectTerminalTasksWithActiveJobs(anyInt());
}
@Test
void inspectionIsReadOnlyByConstruction() {
// 调度器调用面仅含 inspect*(无清理/写入调用)
scheduler.runInspections();
verify(orphanJobInspector).inspectOrphanJobs(anyInt());
}
@Test
void noFileSystemSideEffectsWhenDirMissing() {
scheduler.runInspections();
verify(tempOrphanInspector, never()).inspectOrphanFiles(any(), any(), any());
}
}