From 1080e8e862fcfb9492fd3eed78c34b7ba3abfe32 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 27 四月 2026 11:44:08 +0800
Subject: [PATCH] fix: strengthen auto tune apply transaction safety

---
 src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java |  102 +++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 102 insertions(+), 0 deletions(-)

diff --git a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
index cab797d..2d538bb 100644
--- a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -26,6 +26,10 @@
 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;
@@ -63,10 +67,12 @@
     private BasDualCrnpService basDualCrnpService;
     @Mock
     private StationFlowCapacityService stationFlowCapacityService;
+    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);
@@ -74,6 +80,7 @@
         ReflectionTestUtils.setField(service, "basCrnpService", basCrnpService);
         ReflectionTestUtils.setField(service, "basDualCrnpService", basDualCrnpService);
         ReflectionTestUtils.setField(service, "stationFlowCapacityService", stationFlowCapacityService);
+        ReflectionTestUtils.setField(service, "transactionManager", transactionManager);
 
         AtomicLong jobId = new AtomicLong(100);
         when(aiAutoTuneJobService.save(any(AiAutoTuneJob.class))).thenAnswer(invocation -> {
@@ -229,6 +236,41 @@
     }
 
     @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));
@@ -249,6 +291,7 @@
         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));
@@ -267,6 +310,7 @@
         assertFalse(result.getSuccess());
         assertEquals("failed", changes.get(0).getResultStatus());
         assertTrue(changes.get(0).getRejectReason().contains("db write failed"));
+        assertEquals(1, transactionManager.getRollbackCount());
     }
 
     @Test
@@ -293,6 +337,28 @@
         verify(configService).saveConfigValue("conveyorStationTaskLimit", "10");
         verify(configService).refreshSystemConfigCache();
         verify(basStationService).update(any(Wrapper.class));
+    }
+
+    @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) {
@@ -382,4 +448,40 @@
         verify(aiAutoTuneChangeService).saveBatch(captor.capture());
         return new ArrayList<>(captor.getValue());
     }
+
+    private AiAutoTuneJob updatedJob() {
+        ArgumentCaptor<AiAutoTuneJob> captor = ArgumentCaptor.forClass(AiAutoTuneJob.class);
+        verify(aiAutoTuneJobService).updateById(captor.capture());
+        return captor.getValue();
+    }
+
+    private static class RecordingTransactionManager implements PlatformTransactionManager {
+        private int beginCount;
+        private int commitCount;
+        private int rollbackCount;
+
+        @Override
+        public TransactionStatus getTransaction(TransactionDefinition definition) {
+            beginCount++;
+            return new SimpleTransactionStatus();
+        }
+
+        @Override
+        public void commit(TransactionStatus status) {
+            commitCount++;
+        }
+
+        @Override
+        public void rollback(TransactionStatus status) {
+            rollbackCount++;
+        }
+
+        public int getCommitCount() {
+            return commitCount;
+        }
+
+        public int getRollbackCount() {
+            return rollbackCount;
+        }
+    }
 }

--
Gitblit v1.9.1