From 47b267ab3595a4c984a213ca78dbcb7ef1bdc3cc Mon Sep 17 00:00:00 2001 From: Tyler Ohlsen Date: Wed, 27 Dec 2023 20:26:16 +0000 Subject: [PATCH] Initial start of AD profile test in tool Signed-off-by: Tyler Ohlsen --- build.gradle | 2 + .../tools/SearchAnomalyDetectorsTool.java | 100 ++++++- .../agent/tools/utils/ToolConstants.java | 15 ++ .../org/opensearch/agent/TestHelpers.java | 62 +++++ .../SearchAnomalyDetectorsToolTests.java | 254 +++++++++++++++--- 5 files changed, 381 insertions(+), 52 deletions(-) create mode 100644 src/main/java/org/opensearch/agent/tools/utils/ToolConstants.java create mode 100644 src/test/java/org/opensearch/agent/TestHelpers.java diff --git a/build.gradle b/build.gradle index 1321d825..4be78d75 100644 --- a/build.gradle +++ b/build.gradle @@ -119,6 +119,8 @@ dependencies { implementation fileTree(dir: sqlJarDirectory, include: ["opensearch-sql-${version}.jar", "ppl-${version}.jar", "protocol-${version}.jar"]) compileOnly "org.opensearch:common-utils:${version}" compileOnly "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + compileOnly "org.opensearch:opensearch-job-scheduler-spi:${version}" + // ZipArchive dependencies used for integration tests zipArchive group: 'org.opensearch.plugin', name:'opensearch-ml-plugin', version: "${version}" diff --git a/src/main/java/org/opensearch/agent/tools/SearchAnomalyDetectorsTool.java b/src/main/java/org/opensearch/agent/tools/SearchAnomalyDetectorsTool.java index de397521..cc64bb54 100644 --- a/src/main/java/org/opensearch/agent/tools/SearchAnomalyDetectorsTool.java +++ b/src/main/java/org/opensearch/agent/tools/SearchAnomalyDetectorsTool.java @@ -6,14 +6,22 @@ package org.opensearch.agent.tools; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.opensearch.action.search.SearchRequest; import org.opensearch.action.search.SearchResponse; import org.opensearch.ad.client.AnomalyDetectionNodeClient; +import org.opensearch.ad.model.ADTask; +import org.opensearch.ad.transport.GetAnomalyDetectorRequest; +import org.opensearch.ad.transport.GetAnomalyDetectorResponse; +import org.opensearch.agent.tools.utils.ToolConstants.DetectorStateString; import org.opensearch.client.Client; +import org.opensearch.common.lucene.uid.Versions; import org.opensearch.core.action.ActionListener; import org.opensearch.index.query.BoolQueryBuilder; import org.opensearch.index.query.QueryBuilder; @@ -123,24 +131,98 @@ public void run(Map parameters, ActionListener listener) SearchRequest searchDetectorRequest = new SearchRequest().source(searchSourceBuilder); - if (running != null || disabled != null || failed != null) { - // TODO: add a listener to trigger when the first response is received, to trigger the profile API call - // to fetch the detector state, etc. - // Will need AD client to onboard the profile API first. - } - ActionListener searchDetectorListener = ActionListener.wrap(response -> { StringBuilder sb = new StringBuilder(); - SearchHit[] hits = response.getHits().getHits(); + List hits = Arrays.asList(response.getHits().getHits()); + Map hitsAsMap = hits.stream().collect(Collectors.toMap(SearchHit::getId, hit -> hit)); + + // If we need to filter by detector state, make subsequent profile API calls to each detector + if (running != null || disabled != null || failed != null) { + + // Send out individual AD client calls to fetch detector profiles, continuously adding to a + // tracked list of CompletableFutures + List> profileFutures = new ArrayList<>(); + for (SearchHit hit : hits) { + CompletableFuture profileFuture = new CompletableFuture<>(); + profileFutures.add(profileFuture); + ActionListener profileListener = ActionListener + .wrap(profileResponse -> { + profileFuture.complete(profileResponse); + }, e -> { + log.error("Failed to get anomaly detector profile.", e); + profileFuture.completeExceptionally(e); + listener.onFailure(e); + }); + + GetAnomalyDetectorRequest profileRequest = new GetAnomalyDetectorRequest( + hit.getId(), + Versions.MATCH_ANY, + true, + true, + "", + "", + false, + null + ); + adClient.getDetectorProfile(profileRequest, profileListener); + } + + // Wait for all CompletableFutures to complete, and iterate through the responses. Filter out + // detectors with unwanted detector states. + CompletableFuture> listFuture = CompletableFuture + .allOf(profileFutures.toArray(new CompletableFuture[0])) + .thenApply(v -> profileFutures.stream().map(CompletableFuture::join).collect(Collectors.toList())); + final List profileResponses = listFuture.join(); + for (GetAnomalyDetectorResponse profileResponse : profileResponses) { + if (profileResponse != null && profileResponse.getDetector() != null) { + String detectorId = profileResponse.getDetector().getId(); + + // We follow the existing logic as the frontend to determine overall detector state + // https://github.com/opensearch-project/anomaly-detection-dashboards-plugin/blob/main/server/routes/utils/adHelpers.ts#L437 + String detectorState; + ADTask realtimeTask = profileResponse.getRealtimeAdTask(); + + if (realtimeTask != null) { + String taskState = realtimeTask.getState(); + if (taskState.equalsIgnoreCase("CREATED")) { + detectorState = DetectorStateString.Initializing.name(); + } else if (taskState.equalsIgnoreCase("RUNNING")) { + detectorState = DetectorStateString.Running.name(); + } else if (taskState.equalsIgnoreCase("INIT_FAILURE") + || taskState.equalsIgnoreCase("UNEXPECTED_FAILURE") + || taskState.equalsIgnoreCase("FAILED")) { + detectorState = DetectorStateString.Failed.name(); + } else { + // Task states may fall under other values, such as "FEATURE_REQUIRED" / "STOPPED" / etc. + // We assume here that these will all fall under the disabled category + detectorState = DetectorStateString.Disabled.name(); + } + } else { + detectorState = DetectorStateString.Disabled.name(); + } + + if (running != null && !running && detectorState.equals(DetectorStateString.Running.name())) { + hitsAsMap.remove(detectorId); + } + if (disabled != null && !disabled && detectorState.equals(DetectorStateString.Disabled.name())) { + hitsAsMap.remove(detectorId); + } + if (failed != null && !failed && detectorState.equals(DetectorStateString.Failed.name())) { + hitsAsMap.remove(detectorId); + } + } + } + } + sb.append("AnomalyDetectors=["); - for (SearchHit hit : hits) { + for (SearchHit hit : hitsAsMap.values()) { sb.append("{"); sb.append("id=").append(hit.getId()).append(","); sb.append("name=").append(hit.getSourceAsMap().get("name")); sb.append("}"); } sb.append("]"); - sb.append("TotalAnomalyDetectors=").append(response.getHits().getTotalHits().value); + sb.append("TotalAnomalyDetectors=").append(hitsAsMap.values().size()); listener.onResponse((T) sb.toString()); }, e -> { log.error("Failed to search anomaly detectors.", e); diff --git a/src/main/java/org/opensearch/agent/tools/utils/ToolConstants.java b/src/main/java/org/opensearch/agent/tools/utils/ToolConstants.java new file mode 100644 index 00000000..35cde4b9 --- /dev/null +++ b/src/main/java/org/opensearch/agent/tools/utils/ToolConstants.java @@ -0,0 +1,15 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.agent.tools.utils; + +public class ToolConstants { + public static enum DetectorStateString { + Running, + Disabled, + Failed, + Initializing + } +} diff --git a/src/test/java/org/opensearch/agent/TestHelpers.java b/src/test/java/org/opensearch/agent/TestHelpers.java new file mode 100644 index 00000000..b93093c9 --- /dev/null +++ b/src/test/java/org/opensearch/agent/TestHelpers.java @@ -0,0 +1,62 @@ +/* + * Copyright OpenSearch Contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +package org.opensearch.agent; + +import static org.mockito.Mockito.when; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; + +import org.apache.lucene.search.TotalHits; +import org.mockito.Mockito; +import org.opensearch.action.search.SearchResponse; +import org.opensearch.action.search.SearchResponseSections; +import org.opensearch.ad.model.ADTask; +import org.opensearch.ad.model.AnomalyDetector; +import org.opensearch.ad.transport.GetAnomalyDetectorResponse; +import org.opensearch.common.xcontent.XContentType; +import org.opensearch.core.common.bytes.BytesReference; +import org.opensearch.core.xcontent.XContentBuilder; +import org.opensearch.search.SearchHit; +import org.opensearch.search.SearchHits; +import org.opensearch.search.aggregations.Aggregations; + +public class TestHelpers { + + public static SearchResponse generateSearchResponse(SearchHit[] hits) { + TotalHits totalHits = new TotalHits(hits.length, TotalHits.Relation.EQUAL_TO); + return new SearchResponse( + new SearchResponseSections(new SearchHits(hits, totalHits, 0), new Aggregations(new ArrayList<>()), null, false, null, null, 0), + null, + 0, + 0, + 0, + 0, + null, + null + ); + } + + public static GetAnomalyDetectorResponse generateGetAnomalyDetectorResponses(String[] detectorIds, String[] detectorStates) { + AnomalyDetector detector = Mockito.mock(AnomalyDetector.class); + when(detector.getId()).thenReturn(detectorIds[0], Arrays.copyOfRange(detectorIds, 1, detectorIds.length)); + ADTask realtimeAdTask = Mockito.mock(ADTask.class); + when(realtimeAdTask.getState()).thenReturn(detectorStates[0], Arrays.copyOfRange(detectorStates, 1, detectorStates.length)); + GetAnomalyDetectorResponse getDetectorProfileResponse = Mockito.mock(GetAnomalyDetectorResponse.class); + when(getDetectorProfileResponse.getRealtimeAdTask()).thenReturn(realtimeAdTask); + when(getDetectorProfileResponse.getDetector()).thenReturn(detector); + return getDetectorProfileResponse; + } + + public static SearchHit generateSearchDetectorHit(String detectorName, String detectorId) throws IOException { + XContentBuilder content = XContentBuilder.builder(XContentType.JSON.xContent()); + content.startObject(); + content.field("name", detectorName); + content.endObject(); + return new SearchHit(0, detectorId, null, null).sourceRef(BytesReference.bytes(content)); + } +} diff --git a/src/test/java/org/opensearch/agent/tools/SearchAnomalyDetectorsToolTests.java b/src/test/java/org/opensearch/agent/tools/SearchAnomalyDetectorsToolTests.java index 37ff02a1..7c568fd6 100644 --- a/src/test/java/org/opensearch/agent/tools/SearchAnomalyDetectorsToolTests.java +++ b/src/test/java/org/opensearch/agent/tools/SearchAnomalyDetectorsToolTests.java @@ -7,17 +7,17 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; -import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.Locale; import java.util.Map; -import org.apache.lucene.search.TotalHits; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentCaptor; @@ -26,10 +26,11 @@ import org.mockito.MockitoAnnotations; import org.opensearch.action.ActionType; import org.opensearch.action.search.SearchResponse; -import org.opensearch.action.search.SearchResponseSections; -import org.opensearch.client.AdminClient; -import org.opensearch.client.ClusterAdminClient; -import org.opensearch.client.IndicesAdminClient; +import org.opensearch.ad.transport.GetAnomalyDetectorAction; +import org.opensearch.ad.transport.GetAnomalyDetectorResponse; +import org.opensearch.ad.transport.SearchAnomalyDetectorAction; +import org.opensearch.agent.TestHelpers; +import org.opensearch.agent.tools.utils.ToolConstants.DetectorStateString; import org.opensearch.client.node.NodeClient; import org.opensearch.common.xcontent.XContentType; import org.opensearch.core.action.ActionListener; @@ -37,18 +38,10 @@ import org.opensearch.core.xcontent.XContentBuilder; import org.opensearch.ml.common.spi.tools.Tool; import org.opensearch.search.SearchHit; -import org.opensearch.search.SearchHits; -import org.opensearch.search.aggregations.Aggregations; public class SearchAnomalyDetectorsToolTests { @Mock private NodeClient nodeClient; - @Mock - private AdminClient adminClient; - @Mock - private IndicesAdminClient indicesAdminClient; - @Mock - private ClusterAdminClient clusterAdminClient; private Map nullParams; private Map emptyParams; @@ -67,22 +60,8 @@ public void setup() { @Test public void testRunWithNoDetectors() throws Exception { Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); - - SearchHit[] hits = new SearchHit[0]; - - TotalHits totalHits = new TotalHits(hits.length, TotalHits.Relation.EQUAL_TO); - - SearchResponse getDetectorsResponse = new SearchResponse( - new SearchResponseSections(new SearchHits(hits, totalHits, 0), new Aggregations(new ArrayList<>()), null, false, null, null, 0), - null, - 0, - 0, - 0, - 0, - null, - null - ); - String expectedResponseStr = String.format(Locale.getDefault(), "AnomalyDetectors=[]TotalAnomalyDetectors=%d", hits.length); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(new SearchHit[0]); + String expectedResponseStr = String.format(Locale.getDefault(), "AnomalyDetectors=[]TotalAnomalyDetectors=0"); @SuppressWarnings("unchecked") ActionListener listener = Mockito.mock(ActionListener.class); @@ -111,19 +90,7 @@ public void testRunWithSingleAnomalyDetector() throws Exception { content.endObject(); SearchHit[] hits = new SearchHit[1]; hits[0] = new SearchHit(0, detectorId, null, null).sourceRef(BytesReference.bytes(content)); - - TotalHits totalHits = new TotalHits(hits.length, TotalHits.Relation.EQUAL_TO); - - SearchResponse getDetectorsResponse = new SearchResponse( - new SearchResponseSections(new SearchHits(hits, totalHits, 0), new Aggregations(new ArrayList<>()), null, false, null, null, 0), - null, - 0, - 0, - 0, - 0, - null, - null - ); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); String expectedResponseStr = String .format("AnomalyDetectors=[{id=%s,name=%s}]TotalAnomalyDetectors=%d", detectorId, detectorName, hits.length); @@ -142,6 +109,191 @@ public void testRunWithSingleAnomalyDetector() throws Exception { assertEquals(expectedResponseStr, responseCaptor.getValue()); } + @Test + public void testRunWithRunningDetectorTrue() throws Exception { + final String detectorName = "detector-1"; + final String detectorId = "detector-1-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[1]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName, detectorId); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses(new String[] { detectorId }, new String[] { DetectorStateString.Running.name() }); + String expectedResponseStr = String + .format("AnomalyDetectors=[{id=%s,name=%s}]TotalAnomalyDetectors=%d", detectorId, detectorName, hits.length); + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("running", "true"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertEquals(expectedResponseStr, responseCaptor.getValue()); + } + + @Test + public void testRunWithRunningDetectorFalse() throws Exception { + final String detectorName = "detector-1"; + final String detectorId = "detector-1-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[1]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName, detectorId); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses(new String[] { detectorId }, new String[] { DetectorStateString.Running.name() }); + String expectedResponseStr = "AnomalyDetectors=[]TotalAnomalyDetectors=0"; + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("running", "false"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertEquals(expectedResponseStr, responseCaptor.getValue()); + } + + @Test + public void testRunWithRunningDetectorUndefined() throws Exception { + final String detectorName = "detector-1"; + final String detectorId = "detector-1-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[1]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName, detectorId); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses(new String[] { detectorId }, new String[] { DetectorStateString.Running.name() }); + String expectedResponseStr = String + .format("AnomalyDetectors=[{id=%s,name=%s}]TotalAnomalyDetectors=%d", detectorId, detectorName, hits.length); + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("foo", "bar"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertEquals(expectedResponseStr, responseCaptor.getValue()); + } + + @Test + public void testRunWithCombinedDetectorStatesTrue() throws Exception { + final String detectorName1 = "detector-1"; + final String detectorId1 = "detector-1-id"; + final String detectorName2 = "detector-2"; + final String detectorId2 = "detector-2-id"; + final String detectorName3 = "detector-3"; + final String detectorId3 = "detector-3-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[3]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName1, detectorId1); + hits[1] = TestHelpers.generateSearchDetectorHit(detectorName2, detectorId2); + hits[2] = TestHelpers.generateSearchDetectorHit(detectorName3, detectorId3); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses( + new String[] { detectorId1, detectorId2, detectorId3 }, + new String[] { DetectorStateString.Running.name(), DetectorStateString.Disabled.name(), DetectorStateString.Failed.name() } + ); + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("running", "true", "disabled", "true", "failed", "true"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertTrue(responseCaptor.getValue().contains("TotalAnomalyDetectors=3")); + } + + @Test + public void testRunWithCombinedDetectorStatesFalse() throws Exception { + final String detectorName1 = "detector-1"; + final String detectorId1 = "detector-1-id"; + final String detectorName2 = "detector-2"; + final String detectorId2 = "detector-2-id"; + final String detectorName3 = "detector-3"; + final String detectorId3 = "detector-3-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[3]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName1, detectorId1); + hits[1] = TestHelpers.generateSearchDetectorHit(detectorName2, detectorId2); + hits[2] = TestHelpers.generateSearchDetectorHit(detectorName3, detectorId3); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses( + new String[] { detectorId1, detectorId2, detectorId3 }, + new String[] { DetectorStateString.Running.name(), DetectorStateString.Disabled.name(), DetectorStateString.Failed.name() } + ); + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("running", "false", "disabled", "false", "failed", "false"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertTrue(responseCaptor.getValue().contains("TotalAnomalyDetectors=0")); + } + + @Test + public void testRunWithCombinedDetectorStatesMixed() throws Exception { + final String detectorName1 = "detector-1"; + final String detectorId1 = "detector-1-id"; + final String detectorName2 = "detector-2"; + final String detectorId2 = "detector-2-id"; + final String detectorName3 = "detector-3"; + final String detectorId3 = "detector-3-id"; + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + + // Generate mock values and responses + SearchHit[] hits = new SearchHit[3]; + hits[0] = TestHelpers.generateSearchDetectorHit(detectorName1, detectorId1); + hits[1] = TestHelpers.generateSearchDetectorHit(detectorName2, detectorId2); + hits[2] = TestHelpers.generateSearchDetectorHit(detectorName3, detectorId3); + SearchResponse getDetectorsResponse = TestHelpers.generateSearchResponse(hits); + GetAnomalyDetectorResponse getDetectorProfileResponse = TestHelpers + .generateGetAnomalyDetectorResponses( + new String[] { detectorId1, detectorId2, detectorId3 }, + new String[] { DetectorStateString.Running.name(), DetectorStateString.Disabled.name(), DetectorStateString.Failed.name() } + ); + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + mockProfileApiCalls(getDetectorsResponse, getDetectorProfileResponse); + + tool.run(Map.of("running", "true", "disabled", "false", "failed", "true"), listener); + ArgumentCaptor responseCaptor = ArgumentCaptor.forClass(String.class); + verify(listener, times(1)).onResponse(responseCaptor.capture()); + assertTrue(responseCaptor.getValue().contains("TotalAnomalyDetectors=2")); + } + + @Test + public void testParseParams() throws Exception { + Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); + Map validParams = new HashMap(); + validParams.put("detectorName", "foo"); + validParams.put("indices", "foo"); + validParams.put("highCardinality", "false"); + validParams.put("lastUpdateTime", "1234"); + validParams.put("sortOrder", "foo"); + validParams.put("size", "10"); + validParams.put("startIndex", "0"); + validParams.put("running", "false"); + validParams.put("disabled", "false"); + + @SuppressWarnings("unchecked") + ActionListener listener = Mockito.mock(ActionListener.class); + assertDoesNotThrow(() -> tool.run(validParams, listener)); + assertDoesNotThrow(() -> tool.run(Map.of("detectorNamePattern", "foo*"), listener)); + assertDoesNotThrow(() -> tool.run(Map.of("sortOrder", "AsC"), listener)); + } + @Test public void testValidate() { Tool tool = SearchAnomalyDetectorsTool.Factory.getInstance().create(Collections.emptyMap()); @@ -150,4 +302,20 @@ public void testValidate() { assertTrue(tool.validate(nonEmptyParams)); assertTrue(tool.validate(nullParams)); } + + private void mockProfileApiCalls(SearchResponse getDetectorsResponse, GetAnomalyDetectorResponse getDetectorProfileResponse) { + // Mock return from initial search call + doAnswer((invocation) -> { + ActionListener responseListener = invocation.getArgument(2); + responseListener.onResponse(getDetectorsResponse); + return null; + }).when(nodeClient).execute(any(SearchAnomalyDetectorAction.class), any(), any()); + + // Mock return from secondary detector profile call + doAnswer((invocation) -> { + ActionListener responseListener = invocation.getArgument(2); + responseListener.onResponse(getDetectorProfileResponse); + return null; + }).when(nodeClient).execute(any(GetAnomalyDetectorAction.class), any(), any()); + } }