From bde9e75eed989edf30dbc8bc0ff12a6f7cbff9a9 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 07:42:10 +0800 Subject: [PATCH] =?UTF-8?q?task-165:=20=E5=B7=A1=E6=A3=80=E5=8F=AA?= =?UTF-8?q?=E8=AF=BB=E5=A5=91=E7=BA=A6=EF=BC=88=E5=B7=A1=E6=A3=80=E5=99=A8?= =?UTF-8?q?=E6=97=A0=E5=88=A0=E9=99=A4/=E7=8A=B6=E6=80=81=E4=BF=AE?= =?UTF-8?q?=E6=94=B9/=E5=86=99=E5=BA=93=E6=96=B9=E6=B3=95=E9=9B=86?= =?UTF-8?q?=E5=90=88=E6=96=AD=E8=A8=80=E3=80=81=E8=B0=83=E5=BA=A6=E5=8F=AA?= =?UTF-8?q?=E8=B0=83=20inspect*=E3=80=81=E5=8F=AF=E9=87=8D=E5=A4=8D?= =?UTF-8?q?=E3=80=81=E6=8A=A5=E8=A1=A8=E9=9A=94=E7=A6=BB=EF=BC=89+=208=20?= =?UTF-8?q?=E6=9D=A1=E6=B5=8B=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../task/service/InspectionReadOnlyTest.java | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 backend-java/src/test/java/com/nanri/aiimage/modules/task/service/InspectionReadOnlyTest.java diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/InspectionReadOnlyTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/InspectionReadOnlyTest.java new file mode 100644 index 00000000..05374735 --- /dev/null +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/task/service/InspectionReadOnlyTest.java @@ -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 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 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 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 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()); + } +}