From f955390da5d6fe785bfb9828b44f1603cd4ff0b8 Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期一, 27 四月 2026 11:07:25 +0800
Subject: [PATCH] fix: harden auto tune apply audit and rollback

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

diff --git a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
index d17da07..cab797d 100644
--- a/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
+++ b/src/test/java/com/zy/ai/service/AutoTuneApplyServiceImplTest.java
@@ -115,6 +115,16 @@
     }
 
     @Test
+    void rejectNonnumericNewValue() {
+        AutoTuneApplyResult result = service.apply(request(true, command("sys_config", null, "aiAutoTuneIntervalMinutes", "abc")));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertFalse(result.getSuccess());
+        assertEquals("rejected", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("蹇呴』涓烘暣鏁�"));
+    }
+
+    @Test
     void rejectOverStepConveyorLimitChange() {
         when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
 
@@ -123,6 +133,38 @@
         List<AiAutoTuneChange> changes = savedChanges();
         assertEquals("rejected", changes.get(0).getResultStatus());
         assertTrue(changes.get(0).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 5"));
+    }
+
+    @Test
+    void rejectCrnOutBatchRunningLimitRangeAndStepCases() {
+        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", "21")
+        ));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertEquals("rejected", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 2"));
+        assertEquals("rejected", changes.get(1).getResultStatus());
+        assertTrue(changes.get(1).getRejectReason().contains("1~20"));
+    }
+
+    @Test
+    void rejectMaxInTaskRangeAndStepCases() {
+        when(basCrnpService.getById(1)).thenReturn(crn(1, 1, 5));
+
+        service.apply(request(true,
+                command("crn", "1", "maxInTask", "7"),
+                command("crn", "1", "maxInTask", "11")
+        ));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertEquals("rejected", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("姝ラ暱涓嶈兘瓒呰繃 1"));
+        assertEquals("rejected", changes.get(1).getResultStatus());
+        assertTrue(changes.get(1).getRejectReason().contains("0~10"));
     }
 
     @Test
@@ -150,6 +192,40 @@
         List<AiAutoTuneChange> changes = savedChanges();
         assertEquals("rejected", changes.get(0).getResultStatus());
         assertTrue(changes.get(0).getRejectReason().contains("鍐峰嵈鏈�"));
+    }
+
+    @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));
+
+        AutoTuneApplyResult result = service.apply(request(false,
+                command("sys_config", null, "conveyorStationTaskLimit", "15"),
+                command("station", "101", "outTaskLimit", "3")
+        ));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertFalse(result.getSuccess());
+        assertEquals(2, changes.size());
+        assertTrue(changes.stream().allMatch(change -> "rejected".equals(change.getResultStatus())));
+        verify(configService, never()).saveConfigValue(any(), any());
+        verify(basStationService, never()).update(any(Wrapper.class));
+    }
+
+    @Test
+    void acceptedDryRunDoesNotWriteTargetStores() {
+        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
+
+        AutoTuneApplyResult result = service.apply(request(true, command("sys_config", null, "conveyorStationTaskLimit", "15")));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertTrue(result.getSuccess());
+        assertEquals("dry_run", changes.get(0).getResultStatus());
+        verify(configService, never()).saveConfigValue(any(), any());
+        verify(basStationService, never()).update(any(Wrapper.class));
+        verify(basCrnpService, never()).update(any(Wrapper.class));
+        verify(basDualCrnpService, never()).update(any(Wrapper.class));
     }
 
     @Test
@@ -181,14 +257,30 @@
     }
 
     @Test
-    void rollbackLastJobSuccessfully() {
-        AiAutoTuneJob latestJob = new AiAutoTuneJob();
-        latestJob.setId(10L);
-        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(latestJob));
+    void writeFailureReturnsFailedAuditWithoutThrowing() {
+        when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "10"));
+        when(configService.saveConfigValue("conveyorStationTaskLimit", "15")).thenThrow(new IllegalStateException("db write failed"));
 
+        AutoTuneApplyResult result = service.apply(request(false, command("sys_config", null, "conveyorStationTaskLimit", "15")));
+
+        List<AiAutoTuneChange> changes = savedChanges();
+        assertFalse(result.getSuccess());
+        assertEquals("failed", changes.get(0).getResultStatus());
+        assertTrue(changes.get(0).getRejectReason().contains("db write failed"));
+    }
+
+    @Test
+    void rollbackLastJobSuccessfully() {
+        AiAutoTuneJob rollbackJob = job(20L, "rollback", "success");
+        AiAutoTuneJob latestRealJob = job(10L, "manual", "success");
+        AiAutoTuneChange rollbackChange = successChange(20L, "sys_config", "", "conveyorStationTaskLimit", "15", "18");
         AiAutoTuneChange configChange = successChange(10L, "sys_config", "", "conveyorStationTaskLimit", "10", "15");
         AiAutoTuneChange stationChange = successChange(10L, "station", "101", "outTaskLimit", "1", "2");
-        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(List.of(configChange, stationChange));
+        when(aiAutoTuneChangeService.list(any(Wrapper.class)))
+                .thenReturn(List.of(rollbackChange, configChange, stationChange))
+                .thenReturn(List.of(configChange, stationChange));
+        when(aiAutoTuneJobService.getById(10L)).thenReturn(latestRealJob);
+        when(aiAutoTuneJobService.getById(20L)).thenReturn(rollbackJob);
         when(configService.getOne(any(Wrapper.class))).thenReturn(config("conveyorStationTaskLimit", "15"));
         when(basStationService.getById(101)).thenReturn(station(101, 2));
 
@@ -277,6 +369,14 @@
         return change;
     }
 
+    private AiAutoTuneJob job(Long id, String triggerType, String status) {
+        AiAutoTuneJob job = new AiAutoTuneJob();
+        job.setId(id);
+        job.setTriggerType(triggerType);
+        job.setStatus(status);
+        return job;
+    }
+
     private List<AiAutoTuneChange> savedChanges() {
         ArgumentCaptor<Collection<AiAutoTuneChange>> captor = ArgumentCaptor.forClass(Collection.class);
         verify(aiAutoTuneChangeService).saveBatch(captor.capture());

--
Gitblit v1.9.1