Junjie
8 天以前 1b93474a67aa2323d20630b1bb026713b2bad009
src/test/java/com/zy/ai/service/AutoTuneCoordinatorServiceImplTest.java
@@ -1,6 +1,7 @@
package com.zy.ai.service;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
import com.baomidou.mybatisplus.core.conditions.Wrapper;
import com.zy.ai.domain.autotune.AutoTuneApplyRequest;
import com.zy.ai.domain.autotune.AutoTuneApplyResult;
@@ -17,6 +18,7 @@
import com.zy.ai.mcp.service.SpringAiMcpToolManager;
import com.zy.ai.mcp.tool.AutoTuneMcpTools;
import com.zy.ai.service.impl.AutoTuneAgentServiceImpl;
import com.zy.ai.service.impl.AutoTuneControlModeServiceImpl;
import com.zy.ai.service.impl.AutoTuneCoordinatorServiceImpl;
import com.zy.asrs.service.WrkMastService;
import com.zy.common.utils.RedisUtil;
@@ -44,6 +46,7 @@
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
@@ -54,6 +57,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -82,6 +86,7 @@
    private AiPromptTemplateService aiPromptTemplateService;
    @Mock
    private ConfigService configService;
    private AutoTuneControlModeService autoTuneControlModeService;
    @Mock
    private WrkMastService wrkMastService;
    @Mock
@@ -93,12 +98,15 @@
    @BeforeEach
    void setUp() {
        autoTuneControlModeService = new AutoTuneControlModeServiceImpl(configService);
        tools = new AutoTuneMcpTools(
                autoTuneSnapshotService,
                autoTuneApplyService,
                aiAutoTuneJobService,
                aiAutoTuneChangeService,
                aiAutoTuneMcpCallService);
        lenient().when(configService.getConfigValue("aiAutoTuneEnabled", "N")).thenReturn("Y");
        lenient().when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("N");
    }
    @Test
@@ -121,12 +129,19 @@
        job.setSummary("applied");
        job.setSuccessCount(1);
        job.setRejectCount(0);
        AiAutoTuneChange change = new AiAutoTuneChange();
        change.setJobId(70L);
        change.setTargetType("sys_config");
        change.setTargetKey("conveyorStationTaskLimit");
        change.setRequestedValue("12");
        change.setResultStatus("success");
        AiAutoTuneChange agentChange = new AiAutoTuneChange();
        agentChange.setJobId(7L);
        agentChange.setTargetType("station");
        agentChange.setTargetId("101");
        agentChange.setTargetKey("outTaskLimit");
        agentChange.setRequestedValue("3");
        agentChange.setResultStatus("success");
        AiAutoTuneChange applyChange = new AiAutoTuneChange();
        applyChange.setJobId(70L);
        applyChange.setTargetType("sys_config");
        applyChange.setTargetKey("conveyorStationTaskLimit");
        applyChange.setRequestedValue("12");
        applyChange.setResultStatus("success");
        com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall();
        mcpCall.setAgentJobId(7L);
        mcpCall.setCallSeq(1);
@@ -136,7 +151,10 @@
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job));
        when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall));
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(change));
        List<AiAutoTuneChange> auditChanges = new ArrayList<>();
        auditChanges.add(agentChange);
        auditChanges.add(applyChange);
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(auditChanges);
        List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(99);
@@ -145,11 +163,54 @@
        assertFalse(result.get(0).containsKey("reasoningDigest"));
        assertEquals(1, result.get(0).get("mcpCallCount"));
        List<?> changes = (List<?>) result.get(0).get("changes");
        assertEquals(1, changes.size());
        assertEquals(2, changes.size());
        assertEquals(7L, ((Map<?, ?>) changes.get(0)).get("jobId"));
        assertEquals(70L, ((Map<?, ?>) changes.get(1)).get("jobId"));
        ArgumentCaptor<Wrapper<AiAutoTuneJob>> wrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(aiAutoTuneJobService).list(wrapperCaptor.capture());
        assertTrue(wrapperCaptor.getValue().getSqlSegment().contains("limit 20"));
        ArgumentCaptor<Wrapper<AiAutoTuneChange>> changeWrapperCaptor = ArgumentCaptor.forClass(Wrapper.class);
        verify(aiAutoTuneChangeService).list(changeWrapperCaptor.capture());
        Wrapper<AiAutoTuneChange> changeWrapper = changeWrapperCaptor.getValue();
        assertTrue(changeWrapper.getSqlSegment().contains("job_id IN"));
        List<Object> changeQueryParams = wrapperParamValues(changeWrapper);
        assertTrue(changeQueryParams.contains(7L));
        assertTrue(changeQueryParams.contains(70L));
    }
    @Test
    void recentJobsMarksRollbackMcpChangesAsRollback() {
        AiAutoTuneJob job = new AiAutoTuneJob();
        job.setId(8L);
        job.setTriggerType("manual");
        job.setStatus("success");
        job.setSuccessCount(1);
        job.setRejectCount(0);
        AiAutoTuneChange rollbackChange = new AiAutoTuneChange();
        rollbackChange.setJobId(80L);
        rollbackChange.setTargetType("sys_config");
        rollbackChange.setTargetKey("conveyorStationTaskLimit");
        rollbackChange.setResultStatus("success");
        com.zy.ai.entity.AiAutoTuneMcpCall mcpCall = new com.zy.ai.entity.AiAutoTuneMcpCall();
        mcpCall.setAgentJobId(8L);
        mcpCall.setCallSeq(1);
        mcpCall.setToolName("wcs_local_dispatch_revert_last_auto_tune_job");
        mcpCall.setStatus("success");
        mcpCall.setApplyJobId(80L);
        mcpCall.setSuccessCount(1);
        when(aiAutoTuneJobService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(job));
        when(aiAutoTuneMcpCallService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(mcpCall));
        when(aiAutoTuneChangeService.list(any(Wrapper.class))).thenReturn(Collections.singletonList(rollbackChange));
        List<Map<String, Object>> result = tools.getRecentAutoTuneJobs(1);
        assertEquals("rollback", result.get(0).get("writeBehavior"));
        List<?> changes = (List<?>) result.get(0).get("changes");
        assertEquals(1, changes.size());
        assertEquals("rollback", ((Map<?, ?>) changes.get(0)).get("writeBehavior"));
    }
    @Test
@@ -157,6 +218,7 @@
        AutoTuneApplyResult expected = new AutoTuneApplyResult();
        expected.setDryRun(true);
        expected.setSuccess(true);
        expected.setChanges(Collections.singletonList(applyResultChange("dry_run")));
        when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(expected);
        AutoTuneChangeCommand command = new AutoTuneChangeCommand();
@@ -203,10 +265,38 @@
    }
    @Test
    void applyToolRejectsDuplicateDryRunTargetsBeforeServiceCall() {
        List<AutoTuneChangeCommand> changes = new ArrayList<>();
        changes.add(change(" sys_config ", "ignored-first", " conveyorStationTaskLimit ", "12"));
        changes.add(change("SYS_CONFIG", "ignored-second", "conveyorStationTaskLimit", "13"));
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> tools.applyAutoTuneChanges("duplicate target", 10, "agent", true, null, changes));
        assertTrue(exception.getMessage().contains("Duplicate auto-tune change target"));
        assertTrue(exception.getMessage().contains("targetType=sys_config"));
        assertTrue(exception.getMessage().contains("targetKey=conveyorStationTaskLimit"));
        verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class));
    }
    @Test
    void applyToolRejectsRealApplyWithoutDryRunTokenInAnalysisOnlyMode() {
        AutoTuneChangeCommand command = change("sys_config", null, "conveyorStationTaskLimit", "12");
        IllegalArgumentException exception = assertThrows(IllegalArgumentException.class,
                () -> tools.applyAutoTuneChanges("direct apply", 10, "agent", false, null,
                        Collections.singletonList(command)));
        assertTrue(exception.getMessage().contains("dryRunToken is required"));
        verify(autoTuneApplyService, never()).apply(any(AutoTuneApplyRequest.class));
    }
    @Test
    void applyToolAllowsRealApplyOnlyWithMatchingDryRunToken() {
        AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
        dryRunResult.setDryRun(true);
        dryRunResult.setSuccess(true);
        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
        AutoTuneApplyResult applyResult = new AutoTuneApplyResult();
        applyResult.setDryRun(false);
        applyResult.setSuccess(true);
@@ -230,6 +320,7 @@
        AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
        dryRunResult.setDryRun(true);
        dryRunResult.setSuccess(true);
        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
        when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
        AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null,
@@ -250,6 +341,7 @@
        AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
        dryRunResult.setDryRun(true);
        dryRunResult.setSuccess(true);
        dryRunResult.setChanges(Collections.singletonList(applyResultChange("dry_run")));
        when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
        List<AutoTuneChangeCommand> changes = Collections.singletonList(
                change("sys_config", null, "conveyorStationTaskLimit", "12"));
@@ -264,8 +356,40 @@
    }
    @Test
    void applyToolDoesNotIssueDryRunTokenWhenAllChangesAreNoChange() {
        AutoTuneApplyResult dryRunResult = new AutoTuneApplyResult();
        dryRunResult.setDryRun(true);
        dryRunResult.setSuccess(true);
        dryRunResult.setSuccessCount(0);
        dryRunResult.setRejectCount(0);
        dryRunResult.setChanges(Collections.singletonList(applyResultChange("no_change")));
        when(autoTuneApplyService.apply(any(AutoTuneApplyRequest.class))).thenReturn(dryRunResult);
        AutoTuneApplyResult preview = tools.applyAutoTuneChanges("preview", 10, "agent", true, null,
                Collections.singletonList(change("sys_config", null, "conveyorStationTaskLimit", "12")));
        assertNull(preview.getDryRunToken());
        verify(autoTuneApplyService, times(1)).apply(any(AutoTuneApplyRequest.class));
    }
    @Test
    void rollbackToolDelegatesToApplyServiceRollback() {
        AutoTuneApplyResult expected = new AutoTuneApplyResult();
        when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected);
        AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result");
        assertSame(expected, result);
        verify(autoTuneApplyService).rollbackLastSuccessfulJob("bad result");
    }
    @Test
    void rollbackToolBlocksRollbackInAnalysisOnlyMode() {
        AutoTuneApplyResult expected = new AutoTuneApplyResult();
        expected.setSuccess(false);
        expected.setAnalysisOnly(true);
        expected.setNoApply(true);
        expected.setRejectCount(1);
        when(autoTuneApplyService.rollbackLastSuccessfulJob("bad result")).thenReturn(expected);
        AutoTuneApplyResult result = tools.revertLastAutoTuneJob("bad result");
@@ -563,7 +687,8 @@
        AutoTuneAgentServiceImpl service = new AutoTuneAgentServiceImpl(
                llmChatService,
                mcpToolManager,
                aiPromptTemplateService);
                aiPromptTemplateService,
                autoTuneControlModeService);
        AiPromptTemplate promptTemplate = new AiPromptTemplate();
        promptTemplate.setContent("system prompt");
        when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate);
@@ -658,6 +783,72 @@
    }
    @Test
    void agentCallsMcpForRealApplyInAnalysisOnlyMode() {
        when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y");
        AutoTuneAgentServiceImpl service = agentService();
        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
            String toolName = invocation.getArgument(0);
            if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) {
                return rejectedAnalysisOnlyResult(false);
            }
            return Collections.singletonMap("ok", true);
        });
        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
                .thenReturn(
                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
                                "{}"), 10, 5),
                        response("apply", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes",
                                "{\"dryRun\":false,\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5),
                        response("stop", null, 10, 5)
                );
        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
        assertFalse(result.getSuccess());
        assertTrue(result.getAnalysisOnly());
        assertFalse(result.getAllowApply());
        assertEquals("analysis_only", result.getExecutionMode());
        assertFalse(result.getActualApplyCalled());
        assertEquals(1, result.getRejectCount());
        assertTrue(result.getSummary().contains("仅分析模式禁止实际应用/回滚"));
        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class));
        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_apply_auto_tune_changes"), any(JSONObject.class));
    }
    @Test
    void agentCallsMcpForRollbackInAnalysisOnlyMode() {
        when(configService.getConfigValue("aiAutoTuneAnalysisOnly", "Y")).thenReturn("Y");
        AutoTuneAgentServiceImpl service = agentService();
        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
            String toolName = invocation.getArgument(0);
            if ("wcs_local_dispatch_revert_last_auto_tune_job".equals(toolName)) {
                return rejectedAnalysisOnlyResult(false);
            }
            return Collections.singletonMap("ok", true);
        });
        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
                .thenReturn(
                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
                                "{}"), 10, 5),
                        response("rollback", toolCall("call_2", "wcs_local_dispatch_revert_last_auto_tune_job",
                                "{\"reason\":\"bad result\"}"), 10, 5),
                        response("stop", null, 10, 5)
                );
        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
        assertFalse(result.getSuccess());
        assertTrue(result.getAnalysisOnly());
        assertTrue(result.getRollbackCalled());
        assertEquals(1, result.getRejectCount());
        assertTrue(result.getSummary().contains("仅分析模式禁止实际应用/回滚"));
        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_get_auto_tune_snapshot"), any(JSONObject.class));
        verify(mcpToolManager).callTool(eq("wcs_local_dispatch_revert_last_auto_tune_job"), any(JSONObject.class));
    }
    @Test
    void agentMarksSnapshotOnlyRunAsNoActualMutationEvenIfAssistantClaimsApplied() {
        AutoTuneAgentServiceImpl service = agentService();
        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
@@ -674,7 +865,44 @@
        assertFalse(result.getActualApplyCalled());
        assertFalse(result.getRollbackCalled());
        assertEquals(0, result.getSuccessCount());
        assertTrue(result.getSummary().startsWith("自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。"));
        assertTrue(result.getSummary().contains("自动调参 Agent 未调用实际应用或回滚工具,未修改运行参数。"));
    }
    @Test
    void agentDoesNotMarkNoChangeRealApplyAsActualApplyWhenSuccessCountIsPositive() {
        AutoTuneAgentServiceImpl service = agentService();
        when(mcpToolManager.buildOpenAiTools()).thenReturn(allowedOpenAiTools());
        when(mcpToolManager.callTool(any(), any(JSONObject.class))).thenAnswer(invocation -> {
            String toolName = invocation.getArgument(0);
            if ("wcs_local_dispatch_apply_auto_tune_changes".equals(toolName)) {
                LinkedHashMap<String, Object> noChange = new LinkedHashMap<>();
                noChange.put("resultStatus", "no_change");
                LinkedHashMap<String, Object> applyOutput = new LinkedHashMap<>();
                applyOutput.put("success", true);
                applyOutput.put("dryRun", false);
                applyOutput.put("successCount", 1);
                applyOutput.put("rejectCount", 0);
                applyOutput.put("changes", Collections.singletonList(noChange));
                return applyOutput;
            }
            return Collections.singletonMap("ok", true);
        });
        when(llmChatService.chatCompletionOrThrow(any(), anyDouble(), anyInt(), any()))
                .thenReturn(
                        response("snapshot", toolCall("call_1", "wcs_local_dispatch_get_auto_tune_snapshot",
                                "{}"), 10, 5),
                        response("apply no change", toolCall("call_2", "wcs_local_dispatch_apply_auto_tune_changes",
                                "{\"dryRun\":false,\"dryRunToken\":\"token-123\",\"changes\":[{\"targetType\":\"sys_config\",\"targetKey\":\"conveyorStationTaskLimit\",\"newValue\":\"12\"}]}"), 10, 5),
                        response("无变更", null, 10, 5)
                );
        AutoTuneAgentService.AutoTuneAgentResult result = service.runAutoTune("manual");
        assertTrue(result.getSuccess());
        assertFalse(result.getActualApplyCalled());
        assertEquals(1, result.getSuccessCount());
        assertEquals(0, result.getRejectCount());
    }
    @Test
@@ -820,12 +1048,17 @@
        AiPromptTemplate promptTemplate = new AiPromptTemplate();
        promptTemplate.setContent("system prompt");
        when(aiPromptTemplateService.resolvePublished("wcs_auto_tune_dispatch")).thenReturn(promptTemplate);
        return new AutoTuneAgentServiceImpl(llmChatService, mcpToolManager, aiPromptTemplateService);
        return new AutoTuneAgentServiceImpl(
                llmChatService,
                mcpToolManager,
                aiPromptTemplateService,
                autoTuneControlModeService);
    }
    private AutoTuneCoordinatorServiceImpl coordinatorService() {
        return new AutoTuneCoordinatorServiceImpl(
                configService,
                autoTuneControlModeService,
                wrkMastService,
                aiAutoTuneJobService,
                aiAutoTuneMcpCallService,
@@ -883,6 +1116,19 @@
        return result;
    }
    private AutoTuneApplyResult rejectedAnalysisOnlyResult(Boolean dryRun) {
        AutoTuneApplyResult result = new AutoTuneApplyResult();
        result.setDryRun(dryRun);
        result.setSuccess(false);
        result.setAnalysisOnly(true);
        result.setNoApply(true);
        result.setSuccessCount(0);
        result.setRejectCount(1);
        result.setSummary("仅分析模式禁止实际应用/回滚,未修改运行参数");
        result.setChanges(new ArrayList<>());
        return result;
    }
    private AutoTuneChangeCommand change(String targetType, String targetId, String targetKey, String newValue) {
        AutoTuneChangeCommand command = new AutoTuneChangeCommand();
        command.setTargetType(targetType);
@@ -892,6 +1138,19 @@
        return command;
    }
    private AiAutoTuneChange applyResultChange(String resultStatus) {
        AiAutoTuneChange change = new AiAutoTuneChange();
        change.setResultStatus(resultStatus);
        return change;
    }
    private List<Object> wrapperParamValues(Wrapper<?> wrapper) {
        if (!(wrapper instanceof AbstractWrapper<?, ?, ?> abstractWrapper)) {
            return Collections.emptyList();
        }
        return new ArrayList<>(abstractWrapper.getParamNameValuePairs().values());
    }
    private List<Object> allowedOpenAiTools() {
        List<Object> tools = new ArrayList<>();
        tools.add(openAiTool("wcs_local_dispatch_get_auto_tune_snapshot"));