#
Junjie
9 天以前 dc3f9cc91759823ce59486f19b138be4b296a0f1
src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -10,36 +10,51 @@
import com.zy.asrs.entity.BasCrnp;
import com.zy.asrs.entity.BasDualCrnp;
import com.zy.asrs.entity.BasStation;
import com.zy.asrs.entity.StationFlowCapacity;
import com.zy.asrs.entity.WrkMast;
import com.zy.asrs.service.BasCrnpService;
import com.zy.asrs.service.BasDualCrnpService;
import com.zy.asrs.service.BasStationService;
import com.zy.asrs.service.StationFlowCapacityService;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.utils.RedisUtil;
import com.zy.core.enums.RedisKeyType;
import com.zy.system.entity.Config;
import com.zy.system.service.ConfigService;
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.InOrder;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
import org.springframework.test.util.ReflectionTestUtils;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.SimpleTransactionStatus;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicLong;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@@ -62,33 +77,45 @@
    @Mock
    private BasDualCrnpService basDualCrnpService;
    @Mock
    private StationFlowCapacityService stationFlowCapacityService;
    private WrkMastService wrkMastService;
    @Mock
    private RedisUtil redisUtil;
    private RecordingTransactionManager transactionManager;
    @BeforeEach
    void setUp() {
        service = new AutoTuneApplyServiceImpl();
        transactionManager = new RecordingTransactionManager();
        ReflectionTestUtils.setField(service, "aiAutoTuneJobService", aiAutoTuneJobService);
        ReflectionTestUtils.setField(service, "aiAutoTuneChangeService", aiAutoTuneChangeService);
        ReflectionTestUtils.setField(service, "configService", configService);
        ReflectionTestUtils.setField(service, "basStationService", basStationService);
        ReflectionTestUtils.setField(service, "basCrnpService", basCrnpService);
        ReflectionTestUtils.setField(service, "basDualCrnpService", basDualCrnpService);
        ReflectionTestUtils.setField(service, "stationFlowCapacityService", stationFlowCapacityService);
        ReflectionTestUtils.setField(service, "wrkMastService", wrkMastService);
        ReflectionTestUtils.setField(service, "transactionManager", transactionManager);
        ReflectionTestUtils.setField(service, "redisUtil", redisUtil);
        AtomicLong jobId = new AtomicLong(100);
        when(aiAutoTuneJobService.save(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobSaveCall();
            AiAutoTuneJob job = invocation.getArgument(0);
            job.setId(jobId.incrementAndGet());
            return true;
        });
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenReturn(true);
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobUpdateCall();
            return true;
        });
        when(aiAutoTuneChangeService.saveBatch(any(Collection.class))).thenReturn(true);
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.emptyList());
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(0L);
        when(configService.getConfigValue(eq("aiAutoTuneIntervalMinutes"), any())).thenReturn("10");
        when(configService.saveConfigValue(any(), any())).thenReturn(true);
        when(basStationService.update(any(Wrapper.class))).thenReturn(true);
        when(basCrnpService.update(any(Wrapper.class))).thenReturn(true);
        when(basDualCrnpService.update(any(Wrapper.class))).thenReturn(true);
        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(true);
    }
    @Test
@@ -136,47 +163,130 @@
    }
    @Test
    void rejectCrnOutBatchRunningLimitRangeAndStepCases() {
    void crnOutBatchRunningLimitAllowsStepThreeAndRejectsRangeAndStepCases() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("crnOutBatchRunningLimit", "10"));
        service.apply(request(true,
                command("sys_config", null, "crnOutBatchRunningLimit", "13"),
                command("sys_config", null, "crnOutBatchRunningLimit", "14"),
                command("sys_config", null, "crnOutBatchRunningLimit", "21")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("步长不能超过 2"));
        assertEquals("dry_run", changes.get(0).getResultStatus());
        assertEquals("13", changes.get(0).getRequestedValue());
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("1~20"));
        assertTrue(changes.get(1).getRejectReason().contains("步长不能超过 3"));
        assertEquals("rejected", changes.get(2).getResultStatus());
        assertTrue(changes.get(2).getRejectReason().contains("1~20"));
    }
    @Test
    void rejectMaxInTaskRangeAndStepCases() {
    void maxInTaskAllowsStepThreeAndRejectsRangeAndStepCases() {
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 5));
        service.apply(request(true,
                command("crn", "1", "maxInTask", "7"),
                command("crn", "1", "maxInTask", "8"),
                command("crn", "1", "maxInTask", "9"),
                command("crn", "1", "maxInTask", "11")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("步长不能超过 1"));
        assertEquals("dry_run", changes.get(0).getResultStatus());
        assertEquals("8", changes.get(0).getRequestedValue());
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("0~10"));
        assertTrue(changes.get(1).getRejectReason().contains("步长不能超过 3"));
        assertEquals("rejected", changes.get(2).getResultStatus());
        assertTrue(changes.get(2).getRejectReason().contains("0~10"));
    }
    @Test
    void maxOutTaskAllowsStepThreeAndRejectsStepFour() {
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 1));
        service.apply(request(true,
                command("crn", "1", "maxOutTask", "4"),
                command("crn", "1", "maxOutTask", "5")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("dry_run", changes.get(0).getResultStatus());
        assertEquals("4", changes.get(0).getRequestedValue());
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("步长不能超过 3"));
    }
    @Test
    void dualCrnMaxOutTaskAllowsStepThreeAndRejectsStepFour() {
        when(basDualCrnpService.getById(2)).thenReturn(dualCrn(2, 1, 1));
        service.apply(request(true,
                command("dual_crn", "2", "maxOutTask", "4"),
                command("dual_crn", "2", "maxOutTask", "5")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("dry_run", changes.get(0).getResultStatus());
        assertEquals("4", changes.get(0).getRequestedValue());
        assertEquals("rejected", changes.get(1).getResultStatus());
        assertTrue(changes.get(1).getRejectReason().contains("步长不能超过 3"));
    }
    @Test
    void rejectStationOutTaskLimitAboveDirectionalBufferCapacity() {
        when(basStationService.getById(101)).thenReturn(station(101, 1));
        when(stationFlowCapacityService.getOne(any(Wrapper.class))).thenReturn(capacity(101, "OUT", 2));
        when(basStationService.getById(101)).thenReturn(station(101, 1, 2));
        service.apply(request(true, command("station", "101", "outTaskLimit", "3")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("0~2"));
    }
    @Test
    void rejectStationOutTaskLimitNullUnlimitedCurrentValue() {
        when(basStationService.getById(101)).thenReturn(station(101, null));
        service.apply(request(true, command("station", "101", "outTaskLimit", "1")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("需要人工先初始化为有限值"));
    }
    @Test
    void rejectStationOutTaskLimitNegativeUnlimitedCurrentValue() {
        when(basStationService.getById(101)).thenReturn(station(101, -1));
        service.apply(request(true, command("station", "101", "outTaskLimit", "1")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("需要人工先初始化为有限值"));
    }
    @Test
    void allowStationOutTaskLimitZeroToOneAsFiniteStep() {
        when(basStationService.getById(101)).thenReturn(station(101, 0, 1));
        AutoTuneApplyResult result = service.apply(request(true, command("station", "101", "outTaskLimit", "1")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertTrue(result.getSuccess());
        assertEquals("dry_run", changes.get(0).getResultStatus());
        assertEquals("0", changes.get(0).getOldValue());
        assertEquals("1", changes.get(0).getRequestedValue());
    }
    @Test
    void rejectStationOutTaskLimitWithoutOutBufferCapacity() {
        when(basStationService.getById(101)).thenReturn(station(101, 0));
        service.apply(request(true, command("station", "101", "outTaskLimit", "1")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("缺少 outBufferCapacity"));
    }
    @Test
@@ -197,8 +307,7 @@
    @Test
    void realMixedValidAndInvalidBatchRejectsAndDoesNotWriteTargets() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(basStationService.getById(101)).thenReturn(station(101, 1));
        when(stationFlowCapacityService.getOne(any(Wrapper.class))).thenReturn(capacity(101, "OUT", 2));
        when(basStationService.getById(101)).thenReturn(station(101, 1, 2));
        AutoTuneApplyResult result = service.apply(request(false,
                command("sys_config", null, "conveyorStationTaskLimit", "15"),
@@ -229,10 +338,84 @@
    }
    @Test
    void applyJobRecordsActiveTasksWhenCountIsPositive() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L);
        service.apply(request(true, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        AiAutoTuneJob updatedJob = updatedJob();
        assertEquals(1, updatedJob.getHasActiveTasks());
        ArgumentCaptor<Wrapper<WrkMast>> captor = ArgumentCaptor.forClass(Wrapper.class);
        verify(wrkMastService).count(captor.capture());
        String sqlSegment = captor.getValue().getSqlSegment();
        assertTrue(sqlSegment.contains("NOT IN"));
        assertTrue(sqlSegment.contains("OR"));
        assertTrue(sqlSegment.contains("wrk_sts IS NULL"));
    }
    @Test
    void applyJobRecordsNoActiveTasksWhenCountIsZero() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(0L);
        service.apply(request(true, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        AiAutoTuneJob updatedJob = updatedJob();
        assertEquals(0, updatedJob.getHasActiveTasks());
    }
    @Test
    void applyJobFallsBackToNoActiveTasksWhenCountFails() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(wrkMastService.count(any(Wrapper.class))).thenThrow(new IllegalStateException("count failed"));
        service.apply(request(true, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        AiAutoTuneJob updatedJob = updatedJob();
        assertEquals(0, updatedJob.getHasActiveTasks());
    }
    @Test
    void whitespaceTargetFieldsAreNormalizedBeforeValidationWriteAndAudit() {
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 9));
        AutoTuneApplyResult result = service.apply(request(false, command(" crn ", " 1 ", " maxOutTask ", "2")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertTrue(result.getSuccess());
        assertEquals("crn", changes.get(0).getTargetType());
        assertEquals("1", changes.get(0).getTargetId());
        assertEquals("maxOutTask", changes.get(0).getTargetKey());
        assertEquals("success", changes.get(0).getResultStatus());
        assertEquals(1, transactionManager.getCommitCount());
        verify(basCrnpService).update(any(Wrapper.class));
    }
    @Test
    void whitespaceTargetFieldsUseNormalizedCooldownScope() {
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 9));
        AiAutoTuneChange cooldownChange = new AiAutoTuneChange();
        cooldownChange.setResultStatus("success");
        cooldownChange.setCooldownExpireTime(new Date(System.currentTimeMillis() + 60_000L));
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(cooldownChange));
        AutoTuneApplyResult result = service.apply(request(true, command(" crn ", " 1 ", " maxOutTask ", "2")));
        List<AiAutoTuneChange> changes = savedChanges();
        assertFalse(result.getSuccess());
        assertEquals("crn", changes.get(0).getTargetType());
        assertEquals("1", changes.get(0).getTargetId());
        assertEquals("maxOutTask", changes.get(0).getTargetKey());
        assertEquals("rejected", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("冷却期"));
    }
    @Test
    void applyMixedBatchSuccessfully() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(basStationService.getById(101)).thenReturn(station(101, 1));
        when(stationFlowCapacityService.getOne(any(Wrapper.class))).thenReturn(capacity(101, "OUT", 2));
        when(basStationService.getById(101)).thenReturn(station(101, 1, 2));
        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 1));
        when(basDualCrnpService.getById(2)).thenReturn(dualCrn(2, 1, 1));
@@ -249,11 +432,33 @@
        assertEquals(4, result.getSuccessCount());
        assertEquals(0, result.getRejectCount());
        assertTrue(changes.stream().allMatch(change -> "success".equals(change.getResultStatus())));
        assertEquals(1, transactionManager.getCommitCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
        verify(configService).refreshSystemConfigCache();
        verify(basStationService).update(any(Wrapper.class));
        verify(basCrnpService).update(any(Wrapper.class));
        verify(basDualCrnpService).update(any(Wrapper.class));
    }
    @Test
    void applyCacheRefreshFailureDoesNotAppendFailedAuditOrJob() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        doThrow(new IllegalStateException("cache refresh failed"))
                .when(configService).refreshSystemConfigCache();
        AutoTuneApplyResult result = service.apply(request(false,
                command("sys_config", null, "conveyorStationTaskLimit", "15")
        ));
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertTrue(result.getSuccess());
        assertEquals("success", updatedJob.getStatus());
        assertEquals(1, changes.size());
        assertEquals("success", changes.get(0).getResultStatus());
        verify(aiAutoTuneChangeService, times(1)).saveBatch(any(Collection.class));
        verify(aiAutoTuneJobService, times(1)).updateById(any(AiAutoTuneJob.class));
        verify(configService).refreshSystemConfigCache();
    }
    @Test
@@ -267,6 +472,114 @@
        assertFalse(result.getSuccess());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("db write failed"));
        assertEquals(1, transactionManager.getRollbackCount());
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void auditSaveBatchFailureRollsBackTargetWriteAndReturnsFailedAudit() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(aiAutoTuneChangeService.saveBatch(any(Collection.class)))
                .thenThrow(new IllegalStateException("audit failed"))
                .thenReturn(true);
        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("audit failed"));
        assertEquals(1, transactionManager.getRollbackCount());
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
        verify(configService, never()).refreshSystemConfigCache();
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void jobUpdateFailureRollsBackTargetWriteTransaction() {
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobUpdateCall();
            return false;
        });
        IllegalStateException exception = assertThrows(IllegalStateException.class,
                () -> service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15"))));
        assertTrue(exception.getMessage().contains("更新调参任务状态失败"));
        assertEquals(2, transactionManager.getRollbackCount());
        assertEquals(0, transactionManager.getCommitCount());
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
        verify(configService, never()).refreshSystemConfigCache();
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void jobUpdateFailureRecordsFailureJobWhenRecoveryUpdateSucceeds() {
        AtomicInteger updateAttempts = new AtomicInteger();
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobUpdateCall();
            return updateAttempts.incrementAndGet() > 1;
        });
        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals(1, changes.size());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("更新调参任务状态失败"));
        assertEquals(1, transactionManager.getRollbackCount());
        assertTrue(transactionManager.getCommitCount() >= 1);
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "15");
        verify(configService, never()).refreshSystemConfigCache();
        verify(aiAutoTuneChangeService, times(2)).saveBatch(any(Collection.class));
        verify(aiAutoTuneJobService, times(2)).updateById(any(AiAutoTuneJob.class));
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void realApplyLockNotAcquiredRejectsWithoutTargetWrite() {
        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(false);
        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("锁不可用"));
        assertTrue(changes.get(0).getRejectReason().contains("Redis"));
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService, never()).getOne(any(Wrapper.class));
        verify(configService, never()).saveConfigValue(any(), any());
        verify(redisUtil).hasKey(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key);
        verify(redisUtil, never()).compareAndDelete(anyString(), anyString());
    }
    @Test
@@ -293,6 +606,198 @@
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "10");
        verify(configService).refreshSystemConfigCache();
        verify(basStationService).update(any(Wrapper.class));
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        InOrder rollbackOrder = inOrder(redisUtil, aiAutoTuneChangeService);
        rollbackOrder.verify(redisUtil).trySetStringIfAbsent(anyString(), anyString(), anyLong());
        rollbackOrder.verify(aiAutoTuneChangeService, atLeastOnce()).list(any(Wrapper.class));
    }
    @Test
    void rollbackJobRecordsActiveTasksWhenCountIsPositive() {
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(wrkMastService.count(any(Wrapper.class))).thenReturn(1L);
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        service.rollbackLastSuccessfulJob("manual rollback");
        AiAutoTuneJob updatedJob = updatedJob();
        assertEquals(1, updatedJob.getHasActiveTasks());
    }
    @Test
    void rollbackCacheRefreshFailureDoesNotAppendFailedAuditOrJob() {
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        doThrow(new IllegalStateException("cache refresh failed"))
                .when(configService).refreshSystemConfigCache();
        AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback");
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertTrue(result.getSuccess());
        assertEquals("success", updatedJob.getStatus());
        assertEquals(1, changes.size());
        assertEquals("success", changes.get(0).getResultStatus());
        verify(aiAutoTuneChangeService, times(1)).saveBatch(any(Collection.class));
        verify(aiAutoTuneJobService, times(1)).updateById(any(AiAutoTuneJob.class));
        verify(configService).refreshSystemConfigCache();
    }
    @Test
    void rollbackAuditSaveBatchFailureRollsBackTargetWriteAndReturnsFailedAudit() {
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        when(aiAutoTuneChangeService.saveBatch(any(Collection.class)))
                .thenThrow(new IllegalStateException("rollback audit failed"))
                .thenReturn(true);
        AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback");
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("rollback audit failed"));
        assertEquals(1, transactionManager.getRollbackCount());
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "10");
        verify(configService, never()).refreshSystemConfigCache();
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void rollbackJobUpdateFailureRollsBackAndDoesNotSaveRunningJobOutsideTransaction() {
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobUpdateCall();
            return false;
        });
        IllegalStateException exception = assertThrows(IllegalStateException.class,
                () -> service.rollbackLastSuccessfulJob("manual rollback"));
        assertTrue(exception.getMessage().contains("更新调参任务状态失败"));
        assertEquals(2, transactionManager.getRollbackCount());
        assertEquals(0, transactionManager.getCommitCount());
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "10");
        verify(configService, never()).refreshSystemConfigCache();
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void rollbackJobUpdateFailureRecordsFailureJobWhenRecoveryUpdateSucceeds() {
        AtomicInteger updateAttempts = new AtomicInteger();
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        when(aiAutoTuneJobService.updateById(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
            transactionManager.recordJobUpdateCall();
            return updateAttempts.incrementAndGet() > 1;
        });
        AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback");
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals(1, changes.size());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("更新调参任务状态失败"));
        assertEquals(1, transactionManager.getRollbackCount());
        assertTrue(transactionManager.getCommitCount() >= 1);
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(configService).saveConfigValue("conveyorStationTaskLimit", "10");
        verify(configService, never()).refreshSystemConfigCache();
        verify(aiAutoTuneChangeService, times(2)).saveBatch(any(Collection.class));
        verify(aiAutoTuneJobService, times(2)).updateById(any(AiAutoTuneJob.class));
        verify(redisUtil).compareAndDelete(eq(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key), anyString());
    }
    @Test
    void rollbackLockNotAcquiredReturnsFailedAuditWithoutTargetWrite() {
        when(redisUtil.trySetStringIfAbsent(anyString(), anyString(), anyLong())).thenReturn(false);
        AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback");
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertTrue(changes.get(0).getRejectReason().contains("锁不可用"));
        assertTrue(changes.get(0).getRejectReason().contains("Redis"));
        assertTrue(transactionManager.getJobSaveInsideTransactionCount() > 0);
        assertTrue(transactionManager.getJobUpdateInsideTransactionCount() > 0);
        assertEquals(0, transactionManager.getJobSaveOutsideTransactionCount());
        assertEquals(0, transactionManager.getJobUpdateOutsideTransactionCount());
        verify(aiAutoTuneChangeService, never()).list(any(Wrapper.class));
        verify(aiAutoTuneJobService, never()).getById(any());
        verify(configService, never()).saveConfigValue(any(), any());
        verify(configService, never()).refreshSystemConfigCache();
        verify(redisUtil).hasKey(RedisKeyType.AI_AUTO_TUNE_APPLY_LOCK.key);
        verify(redisUtil, never()).compareAndDelete(anyString(), anyString());
    }
    @Test
    void fullyFailedRollbackReturnsFailedStatus() {
        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
        AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
                .thenReturn(List.of(configChange))
                .thenReturn(List.of(configChange));
        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
        when(configService.saveConfigValue("conveyorStationTaskLimit", "10"))
                .thenThrow(new IllegalStateException("rollback write failed"));
        AutoTuneApplyResult result = service.rollbackLastSuccessfulJob("manual rollback");
        List<AiAutoTuneChange> changes = savedChanges();
        AiAutoTuneJob updatedJob = updatedJob();
        assertFalse(result.getSuccess());
        assertEquals("failed", updatedJob.getStatus());
        assertEquals("failed", changes.get(0).getResultStatus());
        assertEquals(1, transactionManager.getRollbackCount());
    }
    private AutoTuneApplyRequest request(boolean dryRun, AutoTuneChangeCommand... commands) {
@@ -322,9 +827,14 @@
    }
    private BasStation station(Integer stationId, Integer outTaskLimit) {
        return station(stationId, outTaskLimit, null);
    }
    private BasStation station(Integer stationId, Integer outTaskLimit, Integer outBufferCapacity) {
        BasStation station = new BasStation();
        station.setStationId(stationId);
        station.setOutTaskLimit(outTaskLimit);
        station.setOutBufferCapacity(outBufferCapacity);
        return station;
    }
@@ -342,14 +852,6 @@
        dualCrnp.setMaxOutTask(maxOutTask);
        dualCrnp.setMaxInTask(maxInTask);
        return dualCrnp;
    }
    private StationFlowCapacity capacity(Integer stationId, String directionCode, Integer bufferCapacity) {
        StationFlowCapacity capacity = new StationFlowCapacity();
        capacity.setStationId(stationId);
        capacity.setDirectionCode(directionCode);
        capacity.setBufferCapacity(bufferCapacity);
        return capacity;
    }
    private AiAutoTuneChange successChange(Long jobId,
@@ -379,7 +881,85 @@
    private List<AiAutoTuneChange> savedChanges() {
        ArgumentCaptor<Collection<AiAutoTuneChange>> captor = ArgumentCaptor.forClass(Collection.class);
        verify(aiAutoTuneChangeService).saveBatch(captor.capture());
        return new ArrayList<>(captor.getValue());
        verify(aiAutoTuneChangeService, atLeastOnce()).saveBatch(captor.capture());
        List<Collection<AiAutoTuneChange>> allValues = captor.getAllValues();
        return new ArrayList<>(allValues.get(allValues.size() - 1));
    }
    private AiAutoTuneJob updatedJob() {
        ArgumentCaptor<AiAutoTuneJob> captor = ArgumentCaptor.forClass(AiAutoTuneJob.class);
        verify(aiAutoTuneJobService, atLeastOnce()).updateById(captor.capture());
        List<AiAutoTuneJob> allValues = captor.getAllValues();
        return allValues.get(allValues.size() - 1);
    }
    private static class RecordingTransactionManager implements PlatformTransactionManager {
        private int beginCount;
        private int commitCount;
        private int rollbackCount;
        private boolean transactionActive;
        private int jobSaveInsideTransactionCount;
        private int jobSaveOutsideTransactionCount;
        private int jobUpdateInsideTransactionCount;
        private int jobUpdateOutsideTransactionCount;
        @Override
        public TransactionStatus getTransaction(TransactionDefinition definition) {
            beginCount++;
            transactionActive = true;
            return new SimpleTransactionStatus();
        }
        @Override
        public void commit(TransactionStatus status) {
            commitCount++;
            transactionActive = false;
        }
        @Override
        public void rollback(TransactionStatus status) {
            rollbackCount++;
            transactionActive = false;
        }
        public void recordJobSaveCall() {
            if (transactionActive) {
                jobSaveInsideTransactionCount++;
            } else {
                jobSaveOutsideTransactionCount++;
            }
        }
        public void recordJobUpdateCall() {
            if (transactionActive) {
                jobUpdateInsideTransactionCount++;
            } else {
                jobUpdateOutsideTransactionCount++;
            }
        }
        public int getCommitCount() {
            return commitCount;
        }
        public int getRollbackCount() {
            return rollbackCount;
        }
        public int getJobSaveInsideTransactionCount() {
            return jobSaveInsideTransactionCount;
        }
        public int getJobSaveOutsideTransactionCount() {
            return jobSaveOutsideTransactionCount;
        }
        public int getJobUpdateInsideTransactionCount() {
            return jobUpdateInsideTransactionCount;
        }
        public int getJobUpdateOutsideTransactionCount() {
            return jobUpdateOutsideTransactionCount;
        }
    }
}