diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java index b643a669e..144e105db 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ArtifactUnitTest.java @@ -2,8 +2,10 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; @@ -21,7 +23,7 @@ */ public class ArtifactUnitTest extends WireMockTestServer { @Test - void testGetArtifact() { + void getArtifacts() { wireMockServer.stubFor( get(urlPathEqualTo("/lm/artifacts")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -50,10 +52,13 @@ void testGetArtifact() { final AiArtifactList artifactList = new ArtifactApi(getClient(destination)).artifactQuery("default"); + assertThat(artifactList).isNotNull(); assertThat(artifactList.getCount()).isEqualTo(1); assertThat(artifactList.getResources().size()).isEqualTo(1); - AiArtifact artifact = artifactList.getResources().get(0); + + final AiArtifact artifact = artifactList.getResources().get(0); + assertThat(artifact.getCreatedAt()).isEqualTo("2024-05-22T07:40:30Z"); assertThat(artifact.getDescription()).isEqualTo("dataset for aicore training"); assertThat(artifact.getId()).isEqualTo("744b0136-ed4b-49b1-bd10-08c236ed5ce7"); @@ -65,13 +70,13 @@ void testGetArtifact() { } @Test - void testPostArtifact() { + void postArtifact() { wireMockServer.stubFor( post(urlPathEqualTo("/lm/artifacts")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() - .withStatus(HttpStatus.SC_OK) + .withStatus(HttpStatus.SC_CREATED) .withHeader("content-type", "application/json") .withBody( """ @@ -82,18 +87,92 @@ void testPostArtifact() { } """))); - AiArtifactPostData artifactPostData = + final AiArtifactPostData artifactPostData = AiArtifactPostData.create() .name("default") .kind(AiArtifactPostData.KindEnum.DATASET) .url("ai://default/spam/data") .scenarioId("foundation-models") + .scenarioId("foundation-models") .description("dataset for aicore training"); final AiArtifactCreationResponse artifact = new ArtifactApi(getClient(destination)).artifactCreate("default", artifactPostData); + assertThat(artifact).isNotNull(); assertThat(artifact.getId()).isEqualTo("1a84bb38-4a84-4d12-a5aa-300ae7d33fb4"); assertThat(artifact.getMessage()).isEqualTo("AiArtifact acknowledged"); assertThat(artifact.getUrl()).isEqualTo("ai://default/spam/data"); + + wireMockServer.verify( + postRequestedFor(urlPathEqualTo("/lm/artifacts")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody( + equalToJson( + """ + { + "name": "default", + "kind": "dataset", + "url": "ai://default/spam/data", + "scenarioId": "foundation-models", + "description": "dataset for aicore training", + "labels": [] + } + """))); + } + + @Test + void getArtifactById() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/artifacts/777dea85-e9b1-4a7b-9bea-14769b977633")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withHeader("content-type", "application/json") + .withBody( + """ + { + "createdAt": "2024-08-23T09:13:21Z", + "description": "", + "id": "777dea85-e9b1-4a7b-9bea-14769b977633", + "kind": "other", + "modifiedAt": "2024-08-23T09:13:21Z", + "name": "test", + "scenarioId": "orchestration", + "url": "https://file-examples.com/wp-content/storage/2017/10/file-sample_150kB.pdf" + } + """))); + + final AiArtifact artifact = + new ArtifactApi(getClient(destination)) + .artifactGet("default", "777dea85-e9b1-4a7b-9bea-14769b977633"); + + assertThat(artifact).isNotNull(); + assertThat(artifact.getCreatedAt()).isEqualTo("2024-08-23T09:13:21Z"); + assertThat(artifact.getDescription()).isEqualTo(""); + assertThat(artifact.getId()).isEqualTo("777dea85-e9b1-4a7b-9bea-14769b977633"); + assertThat(artifact.getKind()).isEqualTo(AiArtifact.KindEnum.OTHER); + assertThat(artifact.getModifiedAt()).isEqualTo("2024-08-23T09:13:21Z"); + assertThat(artifact.getName()).isEqualTo("test"); + assertThat(artifact.getScenarioId()).isEqualTo("orchestration"); + assertThat(artifact.getUrl()) + .isEqualTo("https://file-examples.com/wp-content/storage/2017/10/file-sample_150kB.pdf"); + } + + @Test + void getArtifactCount() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/artifacts/$count")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody(""" + 4 + """))); + + final int count = new ArtifactApi(getClient(destination)).artifactCount("default"); + + assertThat(count).isEqualTo(4); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java index f23dde83b..67a7ab073 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ConfigurationUnitTest.java @@ -2,8 +2,10 @@ import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; @@ -13,6 +15,7 @@ import com.sap.ai.sdk.core.client.model.AiConfigurationBaseData; import com.sap.ai.sdk.core.client.model.AiConfigurationCreationResponse; import com.sap.ai.sdk.core.client.model.AiConfigurationList; +import com.sap.ai.sdk.core.client.model.AiParameterArgumentBinding; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; @@ -22,7 +25,7 @@ */ public class ConfigurationUnitTest extends WireMockTestServer { @Test - void testGetConfigurations() { + void getConfigurations() { wireMockServer.stubFor( get(urlPathEqualTo("/lm/configurations")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -59,6 +62,7 @@ void testGetConfigurations() { final AiConfigurationList configurationList = new ConfigurationApi(getClient(destination)).configurationQuery("default"); + assertThat(configurationList).isNotNull(); assertThat(configurationList.getCount()).isEqualTo(1); assertThat(configurationList.getResources().size()).isEqualTo(1); @@ -66,6 +70,7 @@ void testGetConfigurations() { assertThat(configuration.getCreatedAt()).isEqualTo("2024-04-17T15:19:45Z"); assertThat(configuration.getExecutableId()).isEqualTo("azure-openai"); assertThat(configuration.getId()).isEqualTo("7652a231-ba9b-4fcc-b473-2c355cb21b61"); + assertThat(configuration.getInputArtifactBindings().size()).isEqualTo(0); assertThat(configuration.getName()).isEqualTo("gpt-4-32k"); assertThat(configuration.getParameterBindings().get(0).getKey()).isEqualTo("modelName"); assertThat(configuration.getParameterBindings().get(0).getValue()).isEqualTo("gpt-4-32k"); @@ -75,13 +80,13 @@ void testGetConfigurations() { } @Test - void testPostConfiguration() { + void postConfiguration() { wireMockServer.stubFor( post(urlPathEqualTo("/lm/configurations")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() - .withStatus(HttpStatus.SC_OK) + .withStatus(HttpStatus.SC_CREATED) .withHeader("content-type", "application/json") .withBody( """ @@ -91,20 +96,118 @@ void testPostConfiguration() { } """))); - AiConfigurationBaseData configurationBaseData = + final AiArtifactArgumentBinding inputArtifactBindingsItem = + AiArtifactArgumentBinding.create() + .key("spam-data") + .artifactId("744b0136-ed4b-49b1-bd10-08c236ed5ce7"); + final AiConfigurationBaseData configurationBaseData = AiConfigurationBaseData.create() .name("i538344_exec_config") .executableId("aicore-nvidia") .scenarioId("foundation-models") - .addInputArtifactBindingsItem( - AiArtifactArgumentBinding.create() - .key("spam-data") - .artifactId("744b0136-ed4b-49b1-bd10-08c236ed5ce7")); + .addInputArtifactBindingsItem(inputArtifactBindingsItem); final AiConfigurationCreationResponse configuration = new ConfigurationApi(getClient(destination)) .configurationCreate("default", configurationBaseData); + assertThat(configuration).isNotNull(); assertThat(configuration.getId()).isEqualTo("f88e7581-ade7-45c6-94e9-807889b523ec"); assertThat(configuration.getMessage()).isEqualTo("Configuration created"); + + wireMockServer.verify( + postRequestedFor(urlPathEqualTo("/lm/configurations")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody( + equalToJson( + """ + { + "name": "i538344_exec_config", + "executableId": "aicore-nvidia", + "scenarioId": "foundation-models", + "parameterBindings":[], + "inputArtifactBindings": [ + { + "key": "spam-data", + "artifactId": "744b0136-ed4b-49b1-bd10-08c236ed5ce7" + } + ] + } + """))); + } + + @Test + void getConfigurationCount() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/configurations/$count")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody(""" + 3 + """))); + + final int configurationCount = + new ConfigurationApi(getClient(destination)).configurationCount("default"); + + assertThat(configurationCount).isEqualTo(3); + } + + @Test + void getConfigurationById() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/configurations/6ff6cb80-87db-45f0-b718-4e1d96e66332")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "createdAt": "2024-09-11T09:14:31Z", + "executableId": "st-spam-detection-i749902", + "id": "6ff6cb80-87db-45f0-b718-4e1d96e66332", + "inputArtifactBindings": [ + { + "artifactId": "c4792df8-da67-44fe-ad99-b5ea74bbb248", + "key": "modeluri" + } + ], + "name": "i749902_depl_conf", + "parameterBindings": [ + { + "key": "minreplicas", + "value": "1" + } + ], + "scenarioId": "scenario-spam-detection-i749902" + } + """))); + + final AiConfiguration configuration = + new ConfigurationApi(getClient(destination)) + .configurationGet("default", "6ff6cb80-87db-45f0-b718-4e1d96e66332"); + + assertThat(configuration).isNotNull(); + assertThat(configuration.getCreatedAt()).isEqualTo("2024-09-11T09:14:31Z"); + assertThat(configuration.getExecutableId()).isEqualTo("st-spam-detection-i749902"); + assertThat(configuration.getId()).isEqualTo("6ff6cb80-87db-45f0-b718-4e1d96e66332"); + assertThat(configuration.getInputArtifactBindings().size()).isEqualTo(1); + assertThat(configuration.getName()).isEqualTo("i749902_depl_conf"); + assertThat(configuration.getParameterBindings().size()).isEqualTo(1); + assertThat(configuration.getScenarioId()).isEqualTo("scenario-spam-detection-i749902"); + + AiArtifactArgumentBinding aiArtifactArgumentBinding = + configuration.getInputArtifactBindings().get(0); + assertThat(aiArtifactArgumentBinding.getArtifactId()) + .isEqualTo("c4792df8-da67-44fe-ad99-b5ea74bbb248"); + assertThat(aiArtifactArgumentBinding.getKey()).isEqualTo("modeluri"); + + AiParameterArgumentBinding aiParameterArgumentBinding = + configuration.getParameterBindings().get(0); + assertThat(aiParameterArgumentBinding.getKey()).isEqualTo("minreplicas"); + assertThat(aiParameterArgumentBinding.getValue()).isEqualTo("1"); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java index b431d2b80..f95a4e88a 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/DeploymentUnitTest.java @@ -8,6 +8,7 @@ import static com.github.tomakehurst.wiremock.client.WireMock.patch; import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; @@ -19,9 +20,11 @@ import com.sap.ai.sdk.core.client.model.AiDeploymentList; import com.sap.ai.sdk.core.client.model.AiDeploymentModificationRequest; import com.sap.ai.sdk.core.client.model.AiDeploymentModificationResponse; +import com.sap.ai.sdk.core.client.model.AiDeploymentResponseWithDetails; import com.sap.ai.sdk.core.client.model.AiDeploymentStatus; import com.sap.ai.sdk.core.client.model.AiDeploymentTargetStatus; import com.sap.ai.sdk.core.client.model.AiExecutionStatus; +import java.util.Map; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; @@ -31,7 +34,7 @@ */ public class DeploymentUnitTest extends WireMockTestServer { @Test - void testGetDeployments() { + void getDeployments() { wireMockServer.stubFor( get(urlPathEqualTo("/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -78,16 +81,26 @@ void testGetDeployments() { final AiDeploymentList deploymentList = new DeploymentApi(getClient(destination)).deploymentQuery("default"); + assertThat(deploymentList).isNotNull(); assertThat(deploymentList.getCount()).isEqualTo(1); assertThat(deploymentList.getResources().size()).isEqualTo(1); - AiDeployment deployment = deploymentList.getResources().get(0); + + final AiDeployment deployment = deploymentList.getResources().get(0); + assertThat(deployment.getConfigurationId()).isEqualTo("7652a231-ba9b-4fcc-b473-2c355cb21b61"); assertThat(deployment.getConfigurationName()).isEqualTo("gpt-4-32k"); assertThat(deployment.getCreatedAt()).isEqualTo("2024-04-17T15:19:53Z"); assertThat(deployment.getDeploymentUrl()) .isEqualTo( "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/d19b998f347341aa"); + // Response contains key "backend_details" while spec (mistakenly) defines key "backendDetails". + final var expected = Map.of("model", Map.of("name", "gpt-4-32k", "version", "latest")); + assertThat(deployment.getDetails().getResources().getCustomField("backend_details")) + .isEqualTo(expected); + assertThat( + deployment.getDetails().getScaling().getCustomFieldNames().contains("backend_details")) + .isTrue(); assertThat(deployment.getId()).isEqualTo("d19b998f347341aa"); assertThat(deployment.getLastOperation()).isEqualTo(AiDeployment.LastOperationEnum.CREATE); assertThat(deployment.getLatestRunningConfigurationId()) @@ -100,7 +113,7 @@ void testGetDeployments() { } @Test - void testPostAiDeployment() { + void postDeployment() { wireMockServer.stubFor( post(urlPathEqualTo("/lm/deployments")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -118,26 +131,38 @@ void testPostAiDeployment() { } """))); - AiDeploymentCreationRequest deploymentCreationRequest = + final AiDeploymentCreationRequest deploymentCreationRequest = AiDeploymentCreationRequest.create() .configurationId("7652a231-ba9b-4fcc-b473-2c355cb21b61"); final AiDeploymentCreationResponse deployment = new DeploymentApi(getClient(destination)) .deploymentCreate("default", deploymentCreationRequest); + assertThat(deployment).isNotNull(); assertThat(deployment.getDeploymentUrl()).isEqualTo(""); assertThat(deployment.getId()).isEqualTo("d5b764fe55b3e87c"); assertThat(deployment.getMessage()).isEqualTo("AiDeployment scheduled."); assertThat(deployment.getStatus()).isEqualTo(AiExecutionStatus.UNKNOWN_DEFAULT_OPEN_API); + + wireMockServer.verify( + postRequestedFor(urlPathEqualTo("/lm/deployments")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody( + equalToJson( + """ + { + "configurationId" : "7652a231-ba9b-4fcc-b473-2c355cb21b61" + } + """))); } @Test - void testPatchAiDeployment() { + void patchDeploymentStatus() { wireMockServer.stubFor( patch(urlPathEqualTo("/lm/deployments/d19b998f347341aa")) .willReturn( aResponse() - .withStatus(HttpStatus.SC_OK) + .withStatus(HttpStatus.SC_ACCEPTED) .withHeader("content-type", "application/json") .withBody( """ @@ -147,11 +172,12 @@ void testPatchAiDeployment() { } """))); - AiDeploymentModificationRequest configModification = + final AiDeploymentModificationRequest configModification = AiDeploymentModificationRequest.create().targetStatus(AiDeploymentTargetStatus.STOPPED); - AiDeploymentModificationResponse deployment = + final AiDeploymentModificationResponse deployment = new DeploymentApi(getClient(destination)) .deploymentModify("default", "d19b998f347341aa", configModification); + assertThat(deployment).isNotNull(); assertThat(deployment.getId()).isEqualTo("d5b764fe55b3e87c"); assertThat(deployment.getMessage()).isEqualTo("AiDeployment modification scheduled"); @@ -170,13 +196,13 @@ void testPatchAiDeployment() { } @Test - void testDeleteAiDeployment() { + void deleteDeployment() { wireMockServer.stubFor( delete(urlPathEqualTo("/lm/deployments/d5b764fe55b3e87c")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() - .withStatus(HttpStatus.SC_OK) + .withStatus(HttpStatus.SC_ACCEPTED) .withHeader("content-type", "application/json") .withBody( """ @@ -189,10 +215,138 @@ void testDeleteAiDeployment() { final AiDeploymentDeletionResponse deployment = new DeploymentApi(getClient(destination)).deploymentDelete("default", "d5b764fe55b3e87c"); + assertThat(deployment).isNotNull(); assertThat(deployment.getId()).isEqualTo("d5b764fe55b3e87c"); - // targetStatus is not in the generated client, but we can still get it from the - // cloudSdkCustomFields + // targetStatus is not in the generated client assertThat(deployment.getCustomField("targetStatus")).isEqualTo("DELETED"); } + + @Test + void getDeploymentById() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments/db1d64d9f06be467")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "configurationId": "dd80625e-ad86-426a-b1a7-1494c083428f", + "configurationName": "orchestration", + "createdAt": "2024-08-05T16:17:29Z", + "deploymentUrl": "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/db1d64d9f06be467", + "details": { + "resources": { + "backend_details": {} + }, + "scaling": { + "backend_details": {} + } + }, + "id": "db1d64d9f06be467", + "lastOperation": "CREATE", + "latestRunningConfigurationId": "dd80625e-ad86-426a-b1a7-1494c083428f", + "modifiedAt": "2024-08-26T12:43:18Z", + "scenarioId": "orchestration", + "startTime": "2024-08-05T16:18:41Z", + "status": "RUNNING", + "submissionTime": "2024-08-05T16:17:40Z", + "targetStatus": "RUNNING" + } + """))); + + final AiDeploymentResponseWithDetails deployment = + new DeploymentApi(getClient(destination)).deploymentGet("default", "db1d64d9f06be467"); + + assertThat(deployment).isNotNull(); + assertThat(deployment.getConfigurationId()).isEqualTo("dd80625e-ad86-426a-b1a7-1494c083428f"); + assertThat(deployment.getConfigurationName()).isEqualTo("orchestration"); + assertThat(deployment.getCreatedAt()).isEqualTo("2024-08-05T16:17:29Z"); + assertThat(deployment.getDeploymentUrl()) + .isEqualTo( + "https://api.ai.intprod-eu12.eu-central-1.aws.ml.hana.ondemand.com/v2/inference/deployments/db1d64d9f06be467"); + // Response contains key "backend_details" while spec (mistakenly) defines key "backendDetails". + assertThat( + deployment + .getDetails() + .getResources() + .getCustomFieldNames() + .contains("backend_details")) + .isTrue(); + assertThat( + deployment.getDetails().getScaling().getCustomFieldNames().contains("backend_details")) + .isTrue(); + assertThat(deployment.getId()).isEqualTo("db1d64d9f06be467"); + assertThat(deployment.getLastOperation()) + .isEqualTo(AiDeploymentResponseWithDetails.LastOperationEnum.CREATE); + assertThat(deployment.getLatestRunningConfigurationId()) + .isEqualTo("dd80625e-ad86-426a-b1a7-1494c083428f"); + assertThat(deployment.getModifiedAt()).isEqualTo("2024-08-26T12:43:18Z"); + assertThat(deployment.getStartTime()).isEqualTo("2024-08-05T16:18:41Z"); + assertThat(deployment.getStatus()).isEqualTo(AiDeploymentStatus.RUNNING); + assertThat(deployment.getSubmissionTime()).isEqualTo("2024-08-05T16:17:40Z"); + assertThat(deployment.getTargetStatus()) + .isEqualTo(AiDeploymentResponseWithDetails.TargetStatusEnum.RUNNING); + } + + @Test + void patchDeploymentConfiguration() { + wireMockServer.stubFor( + patch(urlPathEqualTo("/lm/deployments/d03050a2ab7055cc")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_ACCEPTED) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "id": "d03050a2ab7055cc", + "message": "Deployment modification scheduled" + } + """))); + + final AiDeploymentModificationRequest configModification = + AiDeploymentModificationRequest.create() + .configurationId("6ff6cb80-87db-45f0-b718-4e1d96e66332"); + final AiDeploymentModificationResponse deployment = + new DeploymentApi(getClient(destination)) + .deploymentModify("default", "d03050a2ab7055cc", configModification); + + assertThat(deployment).isNotNull(); + assertThat(deployment.getId()).isEqualTo("d03050a2ab7055cc"); + assertThat(deployment.getMessage()).isEqualTo("Deployment modification scheduled"); + + // verify that null fields are absent from the sent request + wireMockServer.verify( + patchRequestedFor(urlPathEqualTo("/lm/deployments/d03050a2ab7055cc")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody( + equalToJson( + """ + { + "configurationId": "6ff6cb80-87db-45f0-b718-4e1d96e66332" + } + """))); + } + + @Test + void getDeploymentCount() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/deployments/$count")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody(""" + 1 + """))); + + final int count = new DeploymentApi(getClient(destination)).deploymentCount("default"); + + assertThat(count).isEqualTo(1); + } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java index 470c0911d..68c67bcf6 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ExecutionUnitTest.java @@ -1,9 +1,14 @@ package com.sap.ai.sdk.core.client; import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; import static com.github.tomakehurst.wiremock.client.WireMock.equalTo; +import static com.github.tomakehurst.wiremock.client.WireMock.equalToJson; import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.patch; +import static com.github.tomakehurst.wiremock.client.WireMock.patchRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static com.github.tomakehurst.wiremock.client.WireMock.postRequestedFor; import static com.github.tomakehurst.wiremock.client.WireMock.urlPathEqualTo; import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; @@ -12,7 +17,11 @@ import com.sap.ai.sdk.core.client.model.AiEnactmentCreationRequest; import com.sap.ai.sdk.core.client.model.AiExecution; import com.sap.ai.sdk.core.client.model.AiExecutionCreationResponse; +import com.sap.ai.sdk.core.client.model.AiExecutionDeletionResponse; import com.sap.ai.sdk.core.client.model.AiExecutionList; +import com.sap.ai.sdk.core.client.model.AiExecutionModificationRequest; +import com.sap.ai.sdk.core.client.model.AiExecutionModificationResponse; +import com.sap.ai.sdk.core.client.model.AiExecutionResponseWithDetails; import com.sap.ai.sdk.core.client.model.AiExecutionStatus; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; @@ -23,7 +32,7 @@ */ public class ExecutionUnitTest extends WireMockTestServer { @Test - void testGetExecutions() { + void getExecutions() { wireMockServer.stubFor( get(urlPathEqualTo("/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -69,32 +78,47 @@ void testGetExecutions() { final AiExecutionList executionList = new ExecutionApi(getClient(destination)).executionQuery("default"); + assertThat(executionList).isNotNull(); assertThat(executionList.getCount()).isEqualTo(1); assertThat(executionList.getResources().size()).isEqualTo(1); - AiExecution execution = executionList.getResources().get(0); + + final AiExecution execution = executionList.getResources().get(0); + assertThat(execution.getCompletionTime()).isEqualTo("2023-08-05T14:10:16Z"); assertThat(execution.getConfigurationId()).isEqualTo("e0a9eb2e-9ea1-43bf-aff5-7660db166676"); assertThat(execution.getConfigurationName()).isEqualTo("spam-detection-execution-config-0"); assertThat(execution.getCreatedAt()).isEqualTo("2023-08-05T14:07:52Z"); assertThat(execution.getExecutableId()).isEqualTo("wt-spam-detection-i343697"); - assertThat(execution.getOutputArtifacts().get(0).getId()) - .isEqualTo("be0d728f-1cb2-4ff4-97ad-45c54ac592f6"); - assertThat(execution.getOutputArtifacts().get(0).getKind()) - .isEqualTo(AiArtifact.KindEnum.MODEL); - assertThat(execution.getOutputArtifacts().get(0).getUrl()) - .isEqualTo("ai://default/eab289226fe981da/classifier-model-output"); + assertThat(execution.getScenarioId()).isEqualTo("scenario-spam-detection-i343697"); + assertThat(execution.getStartTime()).isEqualTo("2023-08-05T14:09:21Z"); assertThat(execution.getStatus()).isEqualTo(AiExecutionStatus.COMPLETED); + assertThat(execution.getSubmissionTime()).isEqualTo("2023-08-05T14:09:21Z"); + assertThat(execution.getTargetStatus()).isEqualTo(AiExecution.TargetStatusEnum.COMPLETED); + assertThat(execution.getOutputArtifacts().size()).isEqualTo(1); + + final AiArtifact aiArtifact = execution.getOutputArtifacts().get(0); + + assertThat(aiArtifact.getCreatedAt()).isEqualTo("2023-08-05T14:10:05Z"); + assertThat(aiArtifact.getDescription()).isEqualTo(""); + assertThat(aiArtifact.getExecutionId()).isEqualTo("eab289226fe981da"); + assertThat(aiArtifact.getId()).isEqualTo("be0d728f-1cb2-4ff4-97ad-45c54ac592f6"); + assertThat(aiArtifact.getKind()).isEqualTo(AiArtifact.KindEnum.MODEL); + assertThat(aiArtifact.getModifiedAt()).isEqualTo("2023-08-05T14:10:05Z"); + assertThat(aiArtifact.getName()).isEqualTo("classifier-model-output"); + assertThat(aiArtifact.getScenarioId()).isEqualTo("scenario-spam-detection-i343697"); + assertThat(aiArtifact.getUrl()) + .isEqualTo("ai://default/eab289226fe981da/classifier-model-output"); } @Test - void testPostExecution() { + void postExecution() { wireMockServer.stubFor( post(urlPathEqualTo("/lm/executions")) .withHeader("AI-Resource-Group", equalTo("default")) .willReturn( aResponse() - .withStatus(HttpStatus.SC_OK) + .withStatus(HttpStatus.SC_CREATED) .withHeader("content-type", "application/json") .withBody( """ @@ -105,14 +129,180 @@ void testPostExecution() { } """))); - AiEnactmentCreationRequest enactmentCreationRequest = + final AiEnactmentCreationRequest enactmentCreationRequest = AiEnactmentCreationRequest.create().configurationId("e0a9eb2e-9ea1-43bf-aff5-7660db166676"); final AiExecutionCreationResponse execution = new ExecutionApi(getClient(destination)) .executionCreate("default", enactmentCreationRequest); + assertThat(execution).isNotNull(); assertThat(execution.getId()).isEqualTo("eab289226fe981da"); assertThat(execution.getMessage()).isEqualTo("AiExecution acknowledged"); assertThat(execution.getCustomField("url")).isEqualTo("ai://default/eab289226fe981da"); + + wireMockServer.verify( + postRequestedFor(urlPathEqualTo("/lm/executions")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody( + equalToJson( + """ + { + "configurationId": "e0a9eb2e-9ea1-43bf-aff5-7660db166676" + } + """))); + } + + @Test + void getExecutionById() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/executions/e529e8bd58740bc9")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "completionTime": "2024-09-09T19:10:58Z", + "configurationId": "218b651d-113d-4da0-be15-b63a644a92fb", + "configurationName": "i749902_exec_conf", + "createdAt": "2024-09-09T19:09:57Z", + "executableId": "wt-spam-detection-i749902", + "id": "e529e8bd58740bc9", + "modifiedAt": "2024-09-09T19:11:17Z", + "outputArtifacts": [ + { + "createdAt": "2024-09-09T19:10:48Z", + "description": "", + "executionId": "e529e8bd58740bc9", + "id": "c4792df8-da67-44fe-ad99-b5ea74bbb248", + "kind": "model", + "modifiedAt": "2024-09-09T19:10:48Z", + "name": "classifier-model-output", + "scenarioId": "scenario-spam-detection-i749902", + "url": "ai://default/e529e8bd58740bc9/classifier-model-output" + } + ], + "scenarioId": "scenario-spam-detection-i749902", + "startTime": "2024-09-09T19:10:11Z", + "status": "COMPLETED", + "submissionTime": "2024-09-09T19:10:11Z", + "targetStatus": "COMPLETED" + } + """))); + + final AiExecutionResponseWithDetails execution = + new ExecutionApi(getClient(destination)).executionGet("default", "e529e8bd58740bc9"); + + assertThat(execution).isNotNull(); + assertThat(execution.getCompletionTime()).isEqualTo("2024-09-09T19:10:58Z"); + assertThat(execution.getConfigurationId()).isEqualTo("218b651d-113d-4da0-be15-b63a644a92fb"); + assertThat(execution.getConfigurationName()).isEqualTo("i749902_exec_conf"); + assertThat(execution.getCreatedAt()).isEqualTo("2024-09-09T19:09:57Z"); + assertThat(execution.getExecutableId()).isEqualTo("wt-spam-detection-i749902"); + assertThat(execution.getId()).isEqualTo("e529e8bd58740bc9"); + assertThat(execution.getModifiedAt()).isEqualTo("2024-09-09T19:11:17Z"); + assertThat(execution.getScenarioId()).isEqualTo("scenario-spam-detection-i749902"); + assertThat(execution.getStartTime()).isEqualTo("2024-09-09T19:10:11Z"); + assertThat(execution.getStatus()).isEqualTo(AiExecutionStatus.COMPLETED); + assertThat(execution.getSubmissionTime()).isEqualTo("2024-09-09T19:10:11Z"); + assertThat(execution.getTargetStatus()) + .isEqualTo(AiExecutionResponseWithDetails.TargetStatusEnum.COMPLETED); + assertThat(execution.getOutputArtifacts().size()).isEqualTo(1); + + final AiArtifact aiArtifact = execution.getOutputArtifacts().get(0); + + assertThat(aiArtifact.getCreatedAt()).isEqualTo("2024-09-09T19:10:48Z"); + assertThat(aiArtifact.getDescription()).isEqualTo(""); + assertThat(aiArtifact.getExecutionId()).isEqualTo("e529e8bd58740bc9"); + assertThat(aiArtifact.getId()).isEqualTo("c4792df8-da67-44fe-ad99-b5ea74bbb248"); + assertThat(aiArtifact.getKind()).isEqualTo(AiArtifact.KindEnum.MODEL); + assertThat(aiArtifact.getModifiedAt()).isEqualTo("2024-09-09T19:10:48Z"); + assertThat(aiArtifact.getName()).isEqualTo("classifier-model-output"); + assertThat(aiArtifact.getScenarioId()).isEqualTo("scenario-spam-detection-i749902"); + assertThat(aiArtifact.getUrl()) + .isEqualTo("ai://default/e529e8bd58740bc9/classifier-model-output"); + } + + @Test + void deleteExecution() { + wireMockServer.stubFor( + delete(urlPathEqualTo("/lm/executions/e529e8bd58740bc9")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_ACCEPTED) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "id": "e529e8bd58740bc9", + "message": "Deletion scheduled", + "targetStatus": "DELETED" + } + """))); + + final AiExecutionDeletionResponse execution = + new ExecutionApi(getClient(destination)).executionDelete("default", "e529e8bd58740bc9"); + + assertThat(execution).isNotNull(); + assertThat(execution.getId()).isEqualTo("e529e8bd58740bc9"); + assertThat(execution.getMessage()).isEqualTo("Deletion scheduled"); + // targetStatus is not in the generated client. + assertThat(execution.getCustomField("targetStatus")).isEqualTo("DELETED"); + } + + @Test + void patchExecution() { + wireMockServer.stubFor( + patch(urlPathEqualTo("/lm/executions/eec3c6ea18bac6da")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_ACCEPTED) + .withHeader("content-type", "application/json") + .withBody( + """ + { + "id": "eec3c6ea18bac6da", + "message": "Execution modification scheduled" + } + """))); + + final AiExecutionModificationRequest aiExecutionModificationRequest = + AiExecutionModificationRequest.create() + .targetStatus(AiExecutionModificationRequest.TargetStatusEnum.STOPPED); + final AiExecutionModificationResponse aiExecutionModificationResponse = + new ExecutionApi(getClient(destination)) + .executionModify("default", "eec3c6ea18bac6da", aiExecutionModificationRequest); + + assertThat(aiExecutionModificationResponse).isNotNull(); + assertThat(aiExecutionModificationResponse.getId()).isEqualTo("eec3c6ea18bac6da"); + assertThat(aiExecutionModificationResponse.getMessage()) + .isEqualTo("Execution modification scheduled"); + + wireMockServer.verify( + patchRequestedFor(urlPathEqualTo("/lm/executions/eec3c6ea18bac6da")) + .withHeader("AI-Resource-Group", equalTo("default")) + .withRequestBody(equalToJson("{\"targetStatus\":\"STOPPED\"}"))); + } + + @Test + void getExecutionCount() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/executions/$count")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withStatus(HttpStatus.SC_OK) + .withHeader("content-type", "application/json") + .withBody(""" + 1 + """))); + + final int count = new ExecutionApi(getClient(destination)).executionCount("default"); + + assertThat(count).isEqualTo(1); } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java index 98ae6cc15..f7d973812 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/ScenarioUnitTest.java @@ -7,8 +7,14 @@ import static com.sap.ai.sdk.core.Core.getClient; import static org.assertj.core.api.Assertions.assertThat; +import com.sap.ai.sdk.core.client.model.AiModelBaseData; +import com.sap.ai.sdk.core.client.model.AiModelList; +import com.sap.ai.sdk.core.client.model.AiModelVersion; import com.sap.ai.sdk.core.client.model.AiScenario; +import com.sap.ai.sdk.core.client.model.AiScenarioLabel; import com.sap.ai.sdk.core.client.model.AiScenarioList; +import com.sap.ai.sdk.core.client.model.AiVersion; +import com.sap.ai.sdk.core.client.model.AiVersionList; import org.apache.hc.core5.http.HttpStatus; import org.junit.jupiter.api.Test; @@ -18,7 +24,7 @@ */ public class ScenarioUnitTest extends WireMockTestServer { @Test - void testGetScenarios() { + void getScenarios() { wireMockServer.stubFor( get(urlPathEqualTo("/lm/scenarios")) .withHeader("AI-Resource-Group", equalTo("default")) @@ -50,14 +56,149 @@ void testGetScenarios() { final AiScenarioList scenarioList = new ScenarioApi(getClient(destination)).scenarioQuery("default"); + assertThat(scenarioList).isNotNull(); assertThat(scenarioList.getCount()).isEqualTo(1); assertThat(scenarioList.getResources().size()).isEqualTo(1); - AiScenario scenario = scenarioList.getResources().get(0); + + final AiScenario scenario = scenarioList.getResources().get(0); + assertThat(scenario.getId()).isEqualTo("foundation-models"); assertThat(scenario.getName()).isEqualTo("foundation-models"); assertThat(scenario.getLabels().get(0).getKey()).isEqualTo("scenarios.ai.sap.com/llm"); assertThat(scenario.getLabels().get(0).getValue()).isEqualTo("true"); assertThat(scenario.getModifiedAt()).isEqualTo("2024-05-08T08:41:23+00:00"); } + + @Test + void getScenarioVersions() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/scenarios/foundation-models/versions")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withHeader("content-type", "application/json") + .withStatus(HttpStatus.SC_OK) + .withBody( + """ + { + "count": 1, + "resources": [ + { + "createdAt": "2024-05-08T08:41:23+00:00", + "id": "0.0.1", + "modifiedAt": "2024-05-08T08:41:23+00:00", + "scenarioId": "foundation-models" + } + ] + } + """))); + + final AiVersionList versionList = + new ScenarioApi(getClient(destination)) + .scenarioQueryVersions("default", "foundation-models"); + + assertThat(versionList).isNotNull(); + assertThat(versionList.getCount()).isEqualTo(1); + assertThat(versionList.getResources().size()).isEqualTo(1); + + final AiVersion version = versionList.getResources().get(0); + + assertThat(version.getCreatedAt()).isEqualTo("2024-05-08T08:41:23+00:00"); + assertThat(version.getId()).isEqualTo("0.0.1"); + assertThat(version.getModifiedAt()).isEqualTo("2024-05-08T08:41:23+00:00"); + assertThat(version.getScenarioId()).isEqualTo("foundation-models"); + } + + @Test + void getScenarioById() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/scenarios/foundation-models")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withHeader("content-type", "application/json") + .withStatus(HttpStatus.SC_OK) + .withBody( + """ + { + "createdAt": "2023-11-03T14:02:46+00:00", + "description": "AI Core Global Scenario for LLM Access", + "id": "foundation-models", + "labels": [ + { + "key": "scenarios.ai.sap.com/llm", + "value": "true" + } + ], + "modifiedAt": "2024-05-24T08:36:29+00:00", + "name": "foundation-models" + } + """))); + + final AiScenario scenario = + new ScenarioApi(getClient(destination)).scenarioGet("default", "foundation-models"); + + assertThat(scenario).isNotNull(); + assertThat(scenario.getCreatedAt()).isEqualTo("2023-11-03T14:02:46+00:00"); + assertThat(scenario.getDescription()).isEqualTo("AI Core Global Scenario for LLM Access"); + assertThat(scenario.getId()).isEqualTo("foundation-models"); + assertThat(scenario.getName()).isEqualTo("foundation-models"); + assertThat(scenario.getModifiedAt()).isEqualTo("2024-05-24T08:36:29+00:00"); + + AiScenarioLabel aiScenarioLabel = scenario.getLabels().get(0); + assertThat(aiScenarioLabel.getKey()).isEqualTo("scenarios.ai.sap.com/llm"); + assertThat(aiScenarioLabel.getValue()).isEqualTo("true"); + } + + @Test + void getScenarioModels() { + wireMockServer.stubFor( + get(urlPathEqualTo("/lm/scenarios/foundation-models/models")) + .withHeader("AI-Resource-Group", equalTo("default")) + .willReturn( + aResponse() + .withHeader("content-type", "application/json") + .withStatus(HttpStatus.SC_OK) + .withBody( + """ + { + "count": 1, + "resources": [ + { + "description": "Mistral mixtral-8x7b-instruct-v01 model", + "executableId": "aicore-opensource", + "model": "mistralai--mixtral-8x7b-instruct-v01", + "versions": [ + { + "deprecated": false, + "isLatest": true, + "name": "202407", + "retirementDate": "" + } + ] + } + ] + } + """))); + + final AiModelList scenarioList = + new ScenarioApi(getClient(destination)).modelsGet("foundation-models", "default"); + + assertThat(scenarioList).isNotNull(); + assertThat(scenarioList.getCount()).isEqualTo(1); + assertThat(scenarioList.getResources().size()).isEqualTo(1); + + final AiModelBaseData scenario = scenarioList.getResources().get(0); + assertThat(scenario.getDescription()).isEqualTo("Mistral mixtral-8x7b-instruct-v01 model"); + assertThat(scenario.getExecutableId()).isEqualTo("aicore-opensource"); + assertThat(scenario.getModel()).isEqualTo("mistralai--mixtral-8x7b-instruct-v01"); + + AiModelVersion aiModelVersion = scenario.getVersions().get(0); + assertThat(aiModelVersion.getName()).isEqualTo("202407"); + assertThat(aiModelVersion.isIsLatest()).isEqualTo(true); + // deprecated and retirementDate properties are not defined in spec. + assertThat(aiModelVersion.getCustomField("deprecated")).isEqualTo(false); + assertThat(aiModelVersion.getCustomField("retirementDate")).isEqualTo(""); + } } diff --git a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java index 5091331b0..816e1f024 100644 --- a/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java +++ b/core/src/test/java/com/sap/ai/sdk/core/client/WireMockTestServer.java @@ -6,6 +6,7 @@ import com.github.tomakehurst.wiremock.core.WireMockConfiguration; import com.sap.cloud.sdk.cloudplatform.connectivity.DefaultHttpDestination; import com.sap.cloud.sdk.cloudplatform.connectivity.Destination; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; /** Test server for all unit tests. */ @@ -22,4 +23,10 @@ static void setup() { wireMockServer.start(); destination = DefaultHttpDestination.builder(wireMockServer.baseUrl()).build(); } + + // Reset WireMock before each test to ensure clean state + @AfterEach + void reset() { + wireMockServer.resetAll(); + } }