package com.zy.ai.service.impl; import com.zy.ai.domain.autotune.AutoTuneRoutePressureSnapshot; import com.zy.ai.domain.autotune.AutoTuneHotPathSegmentItem; import com.zy.ai.domain.autotune.AutoTuneStationRuntimeItem; import com.zy.ai.domain.autotune.AutoTuneTargetStationRoutePressureItem; import com.zy.ai.domain.autotune.AutoTuneTaskDetailItem; import com.zy.ai.domain.autotune.AutoTuneTaskSnapshot; import com.zy.asrs.domain.vo.StationTaskTraceVo; import com.zy.asrs.entity.WrkMast; import com.zy.common.model.NavigateNode; import com.zy.common.utils.NavigateUtils; import com.zy.core.enums.WrkIoType; import com.zy.core.enums.WrkStsType; import com.zy.core.trace.StationTaskTraceRegistry; import com.zy.core.utils.station.StationOutboundDecisionSupport; import com.zy.system.service.ConfigService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; import java.util.Arrays; import java.util.ArrayList; import java.util.Collections; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; class RoutePressureSnapshotServiceImplTest { private RoutePressureSnapshotServiceImpl service; private StationTaskTraceRegistry traceRegistry; private NavigateUtils navigateUtils; private StationOutboundDecisionSupport stationOutboundDecisionSupport; private ConfigService configService; @BeforeEach void setUp() { service = new RoutePressureSnapshotServiceImpl(); traceRegistry = mock(StationTaskTraceRegistry.class); navigateUtils = mock(NavigateUtils.class); stationOutboundDecisionSupport = mock(StationOutboundDecisionSupport.class); configService = mock(ConfigService.class); ReflectionTestUtils.setField(service, "stationTaskTraceRegistry", traceRegistry); ReflectionTestUtils.setField(service, "navigateUtils", navigateUtils); ReflectionTestUtils.setField(service, "stationOutboundDecisionSupport", stationOutboundDecisionSupport); ReflectionTestUtils.setField(service, "configService", configService); } @Test void buildSnapshotPrefixesCurrentStationBeforePendingTracePath() { StationTaskTraceVo trace = new StationTaskTraceVo(); trace.setTaskNo(190263); trace.setCurrentStationId(196); trace.setPendingStationIds(Arrays.asList(186, 101)); when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 196, 101, 8)), taskSnapshotWithBlockedTask(190263), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(1, snapshot.getTracePathCount()); assertEquals(0, snapshot.getEstimatedPathCount()); assertEquals(0, snapshot.getPathErrorCount()); assertEquals("trace_pending", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); } @Test void buildSnapshotMarksRunningStationTaskMissingWhenTraceAbsent() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 196, 101, 8)), emptyTaskSnapshot(), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(0, snapshot.getTracePathCount()); assertEquals(0, snapshot.getEstimatedPathCount()); assertEquals(1, snapshot.getPathErrorCount()); assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals("missing station trace for running station task", snapshot.getTaskRouteSamples().get(0).getPathError()); verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); } @Test void buildSnapshotEstimatesPathFromSourceStaNoToTargetStaNoForNonRunningTask() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) .thenReturn(navigateNodes(196, 186, 101)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(nonRunningOutboundTask(190263, 196, 101, 8)), emptyTaskSnapshot(), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(0, snapshot.getTracePathCount()); assertEquals(1, snapshot.getEstimatedPathCount()); assertEquals(0, snapshot.getPathErrorCount()); assertEquals("estimated", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); } @Test void buildSnapshotEstimatesNewOutboundTaskEvenWhenResidualTraceExists() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( trace(190263, 196, 888, 101) )); when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) .thenReturn(navigateNodes(196, 186, 101)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(nonRunningOutboundTask(190263, 196, 101, 8)), emptyTaskSnapshot(), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(0, snapshot.getTracePathCount()); assertEquals(1, snapshot.getEstimatedPathCount()); assertEquals(0, snapshot.getPathErrorCount()); assertEquals("estimated", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); } @Test void buildSnapshotMarksRunningStationTaskMissingWhenTraceHasNoUsableStationPath() { StationTaskTraceVo trace = new StationTaskTraceVo(); trace.setTaskNo(190263); trace.setPendingStationIds(Collections.emptyList()); trace.setLatestIssuedSegmentPath(Collections.emptyList()); trace.setIssuedStationIds(Collections.emptyList()); when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 196, 101, 8)), emptyTaskSnapshot(), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(0, snapshot.getTracePathCount()); assertEquals(0, snapshot.getEstimatedPathCount()); assertEquals(1, snapshot.getPathErrorCount()); assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals("missing usable station trace path for running station task", snapshot.getTaskRouteSamples().get(0).getPathError()); verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); } @Test void buildSnapshotDoesNotReuseEstimatedPathAcrossDifferentTaskNos() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); when(stationOutboundDecisionSupport.resolveOutboundPathLenFactor(any())).thenReturn(0.0d); when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190263), eq(0.0d))) .thenReturn(navigateNodes(196, 186, 101)); when(navigateUtils.calcOptimalPathByStationId(eq(196), eq(101), eq(190264), eq(0.0d))) .thenReturn(navigateNodes(196, 194, 101)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Arrays.asList( nonRunningOutboundTask(190263, 196, 101, 8), nonRunningOutboundTask(190264, 196, 101, 9) ), emptyTaskSnapshot(), runtimeItems() ); assertEquals(2, snapshot.getEstimatedPathCount()); assertEquals(190263, snapshot.getTaskRouteSamples().get(0).getWrkNo()); assertEquals(Arrays.asList(196, 186, 101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); assertEquals(190264, snapshot.getTaskRouteSamples().get(1).getWrkNo()); assertEquals(Arrays.asList(196, 194, 101), snapshot.getTaskRouteSamples().get(1).getPathStationIds()); verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190263, 0.0d); verify(navigateUtils).calcOptimalPathByStationId(196, 101, 190264, 0.0d); } @Test void buildSnapshotMarksMissingWhenSourceOrTargetStationIsMissing() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.emptyList()); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(nonRunningOutboundTask(190263, null, 101, 8)), emptyTaskSnapshot(), runtimeItems() ); assertEquals(1, snapshot.getAnalyzedTaskCount()); assertEquals(0, snapshot.getTracePathCount()); assertEquals(0, snapshot.getEstimatedPathCount()); assertEquals(1, snapshot.getPathErrorCount()); assertEquals("missing", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertTrue(snapshot.getTaskRouteSamples().get(0).getPathError().contains("missing sourceStaNo or staNo")); verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); } @Test void buildSnapshotUsesCurrentStationOnlyWhenCurrentIsTargetAndPendingIsEmpty() { StationTaskTraceVo trace = new StationTaskTraceVo(); trace.setTaskNo(190263); trace.setCurrentStationId(101); trace.setPendingStationIds(Collections.emptyList()); trace.setLatestIssuedSegmentPath(Arrays.asList(196, 186, 101)); trace.setIssuedStationIds(Arrays.asList(196, 186, 101)); when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList(trace)); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 196, 101, 8)), taskSnapshotWithBlockedTask(190263), Collections.singletonList(runtimeItem(101, 1, 1, 190263, 1)) ); assertEquals(1, snapshot.getTaskRouteSamples().size()); assertEquals("trace_pending", snapshot.getTaskRouteSamples().get(0).getPathSource()); assertEquals(Arrays.asList(101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); assertEquals(1, snapshot.getTaskRouteSamples().get(0).getPathLength()); assertEquals(1, snapshot.getTracePathCount()); assertEquals(0, snapshot.getEstimatedPathCount()); assertEquals(0, snapshot.getPathErrorCount()); assertTrue(snapshot.getHotPathSegments().isEmpty()); assertTrue(snapshot.getTargetStationRoutePressure().isEmpty()); verify(navigateUtils, never()).calcOptimalPathByStationId(any(), any(), any(), any()); } @Test void buildSnapshotKeepsSingleStationSampleOutOfPressureAggregation() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( trace(190263, 101) )); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 101, 101, 8)), taskSnapshotWithBlockedTask(190263), Collections.singletonList(runtimeItem(101, 1, 1, 190263, 1)) ); assertEquals(1, snapshot.getTaskRouteSamples().size()); assertEquals(Arrays.asList(101), snapshot.getTaskRouteSamples().get(0).getPathStationIds()); assertEquals(1, snapshot.getTaskRouteSamples().get(0).getPathLength()); assertEquals(1, snapshot.getTracePathCount()); assertTrue(snapshot.getHotPathSegments().isEmpty()); assertTrue(snapshot.getTargetStationRoutePressure().isEmpty()); } @Test void buildSnapshotAggregatesOverlappingRouteSegmentsByPassCountAndTargets() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Arrays.asList( trace(190263, 1, 2, 3, 4, 5), trace(190264, 1, 2, 3, 4, 6), trace(190265, 7, 1, 2, 3, 4) )); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Arrays.asList( outboundTask(190263, 1, 101, 1), outboundTask(190264, 1, 102, 2), outboundTask(190265, 7, 101, 3) ), emptyTaskSnapshot(), runtimeItems() ); AutoTuneHotPathSegmentItem sharedSegment = findHotPathSegment(snapshot, "1-2-3-4"); assertNotNull(sharedSegment); assertEquals(Arrays.asList(1, 2, 3, 4), sharedSegment.getStationIds()); assertEquals(3, sharedSegment.getPassTaskCount()); assertEquals(Arrays.asList(101, 102), sharedSegment.getRelatedTargetStations()); assertEquals(Arrays.asList(190263, 190264, 190265), sharedSegment.getSampleWrkNos()); } @Test void buildSnapshotUsesOnlyValidRouteSamplesForSegmentPassRatio() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( trace(190263, 1, 2, 3, 4) )); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Arrays.asList( outboundTask(190263, 1, 101, 1), outboundTask(190264, 5, 101, 2) ), emptyTaskSnapshot(), runtimeItems() ); AutoTuneHotPathSegmentItem segment = findHotPathSegment(snapshot, "1-2-3-4"); assertNotNull(segment); assertEquals(2, snapshot.getAnalyzedTaskCount()); assertEquals(1, snapshot.getPathErrorCount()); assertEquals(1, segment.getPassTaskCount()); assertEquals(100, segment.getPressureFactors().get("passRatio")); } @Test void buildSnapshotExposesRoutePressureRuleSnapshotFromSysConfig() { when(configService.getConfigValue(eq("aiAutoTuneRoutePressureMediumPercent"), eq("50"))).thenReturn("40"); when(configService.getConfigValue(eq("aiAutoTuneRoutePressureHighPercent"), eq("75"))).thenReturn("70"); when(configService.getConfigValue(eq("aiAutoTuneRoutePressurePassWeightPercent"), eq("35"))).thenReturn("60"); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.emptyList(), emptyTaskSnapshot(), runtimeItems() ); assertNotNull(snapshot.getRoutePressureRuleSnapshot()); assertEquals(4, snapshot.getRoutePressureRuleSnapshot().getSegmentWindowSize()); assertEquals(40, snapshot.getRoutePressureRuleSnapshot().getMediumPercent()); assertEquals(70, snapshot.getRoutePressureRuleSnapshot().getHighPercent()); assertEquals(60, snapshot.getRoutePressureRuleSnapshot().getPassWeightPercent()); assertEquals(25, snapshot.getRoutePressureRuleSnapshot().getOccupiedWeightPercent()); } @Test void buildSnapshotTreatsRunBlockAsWeightedFactorInsteadOfDirectHighPressure() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( trace(190263, 1, 2, 3, 4) )); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 1, 101, 1)), emptyTaskSnapshot(), Collections.singletonList(runtimeItem(2, 1, 0, 0, 1)) ); AutoTuneHotPathSegmentItem segment = findHotPathSegment(snapshot, "1-2-3-4"); assertNotNull(segment); assertEquals("low", segment.getPressureLevel()); assertEquals(38, segment.getPressureScore()); assertEquals(25, segment.getPressureFactors().get("runBlockRatio")); } @Test void buildSnapshotUsesBlockedTasksAsIncreaseCandidateWhenPressureIsNotHigh() { when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(Collections.singletonList( trace(190263, 1, 2, 101) )); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( Collections.singletonList(outboundTask(190263, 1, 101, 1)), taskSnapshotWithBlockedTask(190263), runtimeItems() ); AutoTuneTargetStationRoutePressureItem targetPressure = findTargetPressure(snapshot, 101); assertNotNull(targetPressure); assertEquals("medium", targetPressure.getPressureLevel()); assertEquals(55, targetPressure.getPressureScore()); assertEquals("increase_candidate", targetPressure.getRecommendedDirection()); assertTrue(targetPressure.getReason().contains("路径事实")); assertTrue(targetPressure.getReason().contains("blockedTaskCount=1")); } @Test void buildSnapshotMarksHighPressureWithPercentageScoreAndFactEvidence() { List activeTasks = new ArrayList<>(); List traces = new ArrayList<>(); for (int index = 0; index < 10; index++) { int wrkNo = 190300 + index; activeTasks.add(outboundTask(wrkNo, 1, 101, index)); traces.add(trace(wrkNo, 1, 2, 3, 4)); } when(traceRegistry.listPlanningActiveTraceSnapshots()).thenReturn(traces); AutoTuneRoutePressureSnapshot snapshot = service.buildSnapshot( activeTasks, emptyTaskSnapshot(), Arrays.asList( runtimeItem(1, 0, 1, 190300, 1), runtimeItem(2, 0, 1, 190301, 1), runtimeItem(3, 0, 1, 190302, 1), runtimeItem(4, 0, 1, 190303, 1) ) ); AutoTuneTargetStationRoutePressureItem targetPressure = findTargetPressure(snapshot, 101); assertNotNull(targetPressure); assertEquals("high", targetPressure.getPressureLevel()); assertEquals("decrease_candidate", targetPressure.getRecommendedDirection()); assertEquals("decrease_candidate", targetPressure.getHeuristicDirection()); assertTrue(targetPressure.getPressureScore() >= 75); assertTrue(targetPressure.getRecommendedTargets().contains("station/101/outTaskLimit")); assertTrue(targetPressure.getReason().contains("路径事实")); assertTrue(targetPressure.getReason().contains("blockedTaskCount=0")); assertTrue(targetPressure.getReason().contains("routeTaskCount=10")); assertTrue(targetPressure.getPressureFactors().containsKey("highestSegmentScore")); } private WrkMast outboundTask(Integer wrkNo, Integer sourceStaNo, Integer targetStaNo, Integer batchSeq) { return outboundTask(wrkNo, sourceStaNo, targetStaNo, batchSeq, WrkStsType.STATION_RUN.sts); } private WrkMast nonRunningOutboundTask(Integer wrkNo, Integer sourceStaNo, Integer targetStaNo, Integer batchSeq) { return outboundTask(wrkNo, sourceStaNo, targetStaNo, batchSeq, WrkStsType.NEW_OUTBOUND.sts); } private WrkMast outboundTask(Integer wrkNo, Integer sourceStaNo, Integer targetStaNo, Integer batchSeq, long wrkSts) { WrkMast task = new WrkMast(); task.setWrkNo(wrkNo); task.setIoType(WrkIoType.OUT.id); task.setWrkSts(wrkSts); task.setBatch("MANUAL_OUT_20260427181201"); task.setBatchSeq(batchSeq); task.setSourceStaNo(sourceStaNo); task.setStaNo(targetStaNo); return task; } private AutoTuneTaskSnapshot taskSnapshotWithBlockedTask(Integer wrkNo) { AutoTuneTaskDetailItem blockedTask = new AutoTuneTaskDetailItem(); blockedTask.setWrkNo(wrkNo); AutoTuneTaskSnapshot snapshot = new AutoTuneTaskSnapshot(); snapshot.setStationLimitBlockedTasks(Collections.singletonList(blockedTask)); return snapshot; } private AutoTuneTaskSnapshot emptyTaskSnapshot() { AutoTuneTaskSnapshot snapshot = new AutoTuneTaskSnapshot(); snapshot.setStationLimitBlockedTasks(Collections.emptyList()); return snapshot; } private List runtimeItems() { return Collections.emptyList(); } private AutoTuneStationRuntimeItem runtimeItem(Integer stationId, Integer autoing, Integer loading, Integer taskNo, Integer runBlock) { AutoTuneStationRuntimeItem runtimeItem = new AutoTuneStationRuntimeItem(); runtimeItem.setStationId(stationId); runtimeItem.setAutoing(autoing); runtimeItem.setLoading(loading); runtimeItem.setTaskNo(taskNo); runtimeItem.setRunBlock(runBlock); return runtimeItem; } private StationTaskTraceVo trace(Integer wrkNo, Integer... pendingStationIds) { StationTaskTraceVo trace = new StationTaskTraceVo(); trace.setTaskNo(wrkNo); trace.setPendingStationIds(Arrays.asList(pendingStationIds)); return trace; } private AutoTuneHotPathSegmentItem findHotPathSegment(AutoTuneRoutePressureSnapshot snapshot, String segmentKey) { return snapshot.getHotPathSegments().stream() .filter(segment -> segmentKey.equals(segment.getSegmentKey())) .findFirst() .orElse(null); } private AutoTuneTargetStationRoutePressureItem findTargetPressure(AutoTuneRoutePressureSnapshot snapshot, Integer targetStationId) { return snapshot.getTargetStationRoutePressure().stream() .filter(item -> targetStationId.equals(item.getTargetStationId())) .findFirst() .orElse(null); } private List navigateNodes(Integer... stationIds) { return Arrays.stream(stationIds) .map(this::navigateNode) .toList(); } private NavigateNode navigateNode(Integer stationId) { NavigateNode navigateNode = new NavigateNode(); navigateNode.setStationId(stationId); return navigateNode; } }