diff --git a/CHANGELOG.md b/CHANGELOG.md index 3e7ccc6..9ead45b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,28 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.1.2] - 2026-03-30 + +### Added +- Test set execution support: `execute()`, `rescore()`, and `lastRun()` on `TestSetClient`, matching the Python SDK. +- `ExecutionMode` enum (`PARALLEL`, `SEQUENTIAL`) and `ExecutionRequest` model for controlling test run execution. +- Typed stats models for test run analytics (`TestRunStats`, `TestRunOverallSummary`, `StatusDistribution`, `ResultDistribution`, `TestRunTimelineData`, etc.). +- Typed stats models for test result analytics (`TestResultStats`, `MetricStats`, `OverallStats`, `TimelineData`, `TestRunSummary`, etc.). +- `TestRunStatsMode` and `TestResultStatsMode` enums for controlling which stat sections are returned. +- `TestRunClient.stats()` with overloads for mode, run ID filtering, and arbitrary filter params. +- `TestResultClient.stats()` with mode and 18 filter parameters matching the Python SDK. +- Test set metric management: `getMetrics()`, `addMetric()`, `removeMetric()` on `TestSetClient`. +- Test association management: `addTests()`, `removeTests()` on `TestSetClient`. +- Integration tests for test run lifecycle, execution, and stats (`TestRunIntegrationTest`). +- WireMock unit tests for all new client methods. +- Five new runnable examples: `ExecuteTestSetExample`, `TestRunWorkflowExample`, `TestSetMetricsExample`, `TestRunStatsExample`, `TestResultStatsExample`. +- Examples README with setup instructions and quick reference. + +### Changed +- `TestRun.status` changed from `Object` to `String` with `NameStringDeserializer` to handle both plain string and nested object responses from the API. +- `TestRun` entity now includes `createdAt`, `nanoId`, `testCount`, and `passRate` fields. +- Updated project README with new features, all nine examples, and link to examples README. + ## [0.1.1] - 2026-03-20 ### Added diff --git a/README.md b/README.md index 4b1e332..d10468f 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,8 @@ The Rhesis Java SDK empowers developers to programmatically access curated test - [Quick Start](#-quick-start) - [Generating Custom Test Sets](#generating-custom-test-sets-%EF%B8%8F) - [Examples](#examples-) + - [Test Execution](#test-execution) + - [Analytics & Stats](#analytics--stats) - [About Rhesis AI](#-about-rhesis-ai) - [Community](#-community-) - [Hugging Face](#-hugging-face) @@ -62,6 +64,9 @@ The Rhesis Java SDK provides programmatic access to the Rhesis testing platform: - **Access Test Sets**: Browse and load curated test sets across multiple domains and use cases - **Generate Test Scenarios**: Create custom test sets from prompts, requirements, or domain knowledge natively using local LLM models +- **Execute Test Runs**: Trigger test set runs against your endpoints in parallel or sequential mode +- **Analytics & Stats**: Typed stats for test runs and test results โ€” pass rates by metric, behavior, category, topic, and timeline trends +- **Manage Metrics & Tests**: Add/remove metrics on test sets, associate/disassociate tests, rescore previous runs - **Seamless Integration**: Integrate testing into your Java CI/CD pipeline and development workflow - **Comprehensive Coverage**: Scale your testing from dozens to thousands of scenarios - **Open Source**: MIT-licensed with full transparency and community-driven development @@ -258,23 +263,37 @@ If none of the existing test sets fit your needs, you can generate your own. You ### Examples ๐Ÿ’ก -Looking for more detailed examples? Check out our runnable examples in the [`src/test/java/ai/rhesis/sdk/examples/`](src/test/java/ai/rhesis/sdk/examples/) directory. These include: -- [Creating an Endpoint](src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java) -- [Generating a Test Set](src/test/java/ai/rhesis/sdk/examples/GenerateTestSetExample.java) +Looking for more detailed examples? Check out the full [Examples README](src/test/java/ai/rhesis/sdk/examples/README.md) and runnable examples in [`src/test/java/ai/rhesis/sdk/examples/`](src/test/java/ai/rhesis/sdk/examples/): -You can run these examples directly from the command line using Maven. Make sure your `RHESIS_API_KEY` is set in your environment (it will be automatically picked up from a `.env` file at the root of the project if one exists): +**Endpoints & Projects** +- [Creating an Endpoint](src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java) โ€” Look up a project and create a REST endpoint with request/response mappings + +**Test Generation** +- [Generating a Test Set](src/test/java/ai/rhesis/sdk/examples/GenerateTestSetExample.java) โ€” Configure a synthesizer, generate tests, and push to Rhesis +- [Generate Test Set with Files](src/test/java/ai/rhesis/sdk/examples/GenerateTestSetWithFilesExample.java) โ€” Generate tests and attach files to each one + +**File Management** +- [File Support](src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java) โ€” Create tests with file attachments, download content, and clean up + +**Test Execution** +- [Execute a Test Set](src/test/java/ai/rhesis/sdk/examples/ExecuteTestSetExample.java) โ€” Trigger runs in parallel or sequential mode, with custom metrics +- [Test Run Workflow](src/test/java/ai/rhesis/sdk/examples/TestRunWorkflowExample.java) โ€” Full lifecycle: list runs, inspect results, get last run, rescore + +**Test Set Management** +- [Test Set Metrics](src/test/java/ai/rhesis/sdk/examples/TestSetMetricsExample.java) โ€” List, add, and remove metrics; associate and disassociate tests + +**Analytics & Stats** +- [Test Run Stats](src/test/java/ai/rhesis/sdk/examples/TestRunStatsExample.java) โ€” Overall summary, status distribution, most-run test sets, timeline, filtered queries +- [Test Result Stats](src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java) โ€” Pass rates by metric, behavior, category, and topic; per-run summaries and timeline trends + +You can run any example from the command line using Maven. Make sure your `RHESIS_API_KEY` is set in your environment (it will be automatically picked up from a `.env` file at the root of the project if one exists): ```bash # Ensure the examples are compiled mvn clean test-compile -# Run the Create Endpoint example -mvn exec:java -Dexec.classpathScope=test -Dexec.mainClass="ai.rhesis.sdk.examples.CreateEndpointExample" -``` - -To run the multi-turn synthesizer example: -```bash -mvn exec:java -Dexec.classpathScope=test -Dexec.mainClass="ai.rhesis.sdk.examples.GenerateTestSetExample" +# Run any example +mvn exec:java -Dexec.classpathScope=test -Dexec.mainClass="ai.rhesis.sdk.examples.ExecuteTestSetExample" ``` ## ๐Ÿงช About Rhesis AI diff --git a/pom.xml b/pom.xml index b0fd769..e560adf 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ai.rhesis rhesis-java - 0.1.1 + 0.1.2 21 diff --git a/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java b/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java index 9273061..d6000b1 100644 --- a/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java +++ b/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java @@ -2,9 +2,12 @@ import ai.rhesis.sdk.entities.File; import ai.rhesis.sdk.entities.TestResult; +import ai.rhesis.sdk.entities.stats.TestResultStats; +import ai.rhesis.sdk.enums.TestResultStatsMode; import ai.rhesis.sdk.http.InternalHttpClient; import com.fasterxml.jackson.core.type.TypeReference; import java.util.List; +import java.util.Map; public class TestResultClient { private final InternalHttpClient httpClient; @@ -25,4 +28,84 @@ public List getFiles(String testResultId) { return httpClient.get( "/test_results/" + testResultId + "/files", new TypeReference>() {}); } + + /** + * Get aggregated test result statistics with all sections. + * + * @return typed TestResultStats with all sections populated + */ + public TestResultStats stats() { + return stats(TestResultStatsMode.ALL, null); + } + + /** + * Get aggregated test result statistics with the given mode. + * + * @param mode controls which sections the backend populates + * @return typed TestResultStats + */ + public TestResultStats stats(TestResultStatsMode mode) { + return stats(mode, null); + } + + /** + * Get aggregated test result statistics with full control over mode and filters. + * + *

Supported filter keys: + * + *

    + *
  • {@code months} โ€” number of months of historical data (default 6) + *
  • {@code test_run_id} โ€” filter by a single test run ID + *
  • {@code test_run_ids} โ€” filter by multiple test run IDs (List) + *
  • {@code test_set_ids} โ€” filter by test set IDs (List) + *
  • {@code behavior_ids} โ€” filter by behavior IDs (List) + *
  • {@code category_ids} โ€” filter by category IDs (List) + *
  • {@code topic_ids} โ€” filter by topic IDs (List) + *
  • {@code status_ids} โ€” filter by test status IDs (List) + *
  • {@code test_ids} โ€” filter by specific test IDs (List) + *
  • {@code test_type_ids} โ€” filter by test type IDs (List) + *
  • {@code user_ids} โ€” filter by test creator user IDs (List) + *
  • {@code assignee_ids} โ€” filter by assignee user IDs (List) + *
  • {@code owner_ids} โ€” filter by test owner user IDs (List) + *
  • {@code prompt_ids} โ€” filter by prompt IDs (List) + *
  • {@code priority_min} โ€” minimum priority (inclusive) + *
  • {@code priority_max} โ€” maximum priority (inclusive) + *
  • {@code tags} โ€” filter by tags (List) + *
  • {@code start_date} โ€” start date (ISO format), overrides months + *
  • {@code end_date} โ€” end date (ISO format), overrides months + *
+ * + * @param mode controls which sections the backend populates + * @param params optional filter parameters (may be null) + * @return typed TestResultStats + */ + public TestResultStats stats(TestResultStatsMode mode, Map params) { + StringBuilder path = new StringBuilder("/test_results/stats?mode="); + path.append(encode(mode.getValue())); + + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List listVal) { + for (Object item : listVal) { + path.append("&") + .append(encode(entry.getKey())) + .append("=") + .append(encode(item.toString())); + } + } else { + path.append("&") + .append(encode(entry.getKey())) + .append("=") + .append(encode(value.toString())); + } + } + } + + return httpClient.get(path.toString(), TestResultStats.class); + } + + private static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } } diff --git a/src/main/java/ai/rhesis/sdk/clients/TestRunClient.java b/src/main/java/ai/rhesis/sdk/clients/TestRunClient.java index 9528983..c231ad4 100644 --- a/src/main/java/ai/rhesis/sdk/clients/TestRunClient.java +++ b/src/main/java/ai/rhesis/sdk/clients/TestRunClient.java @@ -2,9 +2,13 @@ import ai.rhesis.sdk.entities.TestResult; import ai.rhesis.sdk.entities.TestRun; +import ai.rhesis.sdk.entities.stats.TestRunStats; +import ai.rhesis.sdk.enums.TestRunStatsMode; import ai.rhesis.sdk.http.InternalHttpClient; import com.fasterxml.jackson.core.type.TypeReference; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; public class TestRunClient { private final InternalHttpClient httpClient; @@ -28,4 +32,76 @@ public List getTestResults(String testRunId) { String filter = "?$filter=test_run_id%20eq%20'" + testRunId + "'"; return httpClient.get("/test_results/" + filter, new TypeReference>() {}); } + + /** + * Get aggregated test run statistics with all sections. + * + * @return typed TestRunStats with all sections populated + */ + public TestRunStats stats() { + return stats(TestRunStatsMode.ALL, null); + } + + /** + * Get aggregated test run statistics with the given mode. + * + * @param mode controls which sections the backend populates + * @return typed TestRunStats + */ + public TestRunStats stats(TestRunStatsMode mode) { + return stats(mode, null); + } + + /** + * Get statistics scoped to specific test run IDs. + * + * @param testRunIds list of test run IDs to filter by + * @return typed TestRunStats + */ + public TestRunStats stats(List testRunIds) { + Map params = new LinkedHashMap<>(); + if (testRunIds != null && !testRunIds.isEmpty()) { + params.put("test_run_ids", testRunIds); + } + return stats(TestRunStatsMode.ALL, params); + } + + /** + * Get aggregated test run statistics with full control over mode and filters. + * + * @param mode controls which sections the backend populates + * @param params optional filter parameters. Supported keys: "months", "top", "test_run_ids" + * (List), "user_ids" (List), "endpoint_ids" (List), "test_set_ids" (List), "status_list" + * (List), "start_date", "end_date" + * @return typed TestRunStats + */ + public TestRunStats stats(TestRunStatsMode mode, Map params) { + StringBuilder path = new StringBuilder("/test_runs/stats?mode="); + path.append(encode(mode.getValue())); + + if (params != null) { + for (Map.Entry entry : params.entrySet()) { + Object value = entry.getValue(); + if (value instanceof List listVal) { + for (Object item : listVal) { + path.append("&") + .append(encode(entry.getKey())) + .append("=") + .append(encode(item.toString())); + } + } else { + path.append("&") + .append(encode(entry.getKey())) + .append("=") + .append(encode(value.toString())); + } + } + } + + return httpClient.get(path.toString(), TestRunStats.class); + } + + private static String encode(String value) { + return java.net.URLEncoder.encode(value, java.nio.charset.StandardCharsets.UTF_8); + } } diff --git a/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java b/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java index e600f96..f64f941 100644 --- a/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java +++ b/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java @@ -1,9 +1,13 @@ package ai.rhesis.sdk.clients; +import ai.rhesis.sdk.entities.ExecutionRequest; +import ai.rhesis.sdk.entities.TestRun; import ai.rhesis.sdk.entities.TestSet; +import ai.rhesis.sdk.enums.ExecutionMode; import ai.rhesis.sdk.http.InternalHttpClient; import com.fasterxml.jackson.core.type.TypeReference; import java.util.List; +import java.util.Map; public class TestSetClient { private final InternalHttpClient httpClient; @@ -37,4 +41,188 @@ public List getTests(String id, int skip, int limit public void delete(String id) { httpClient.delete("/test_sets/" + id); } + + /** + * Execute a test set against the given endpoint. + * + * @param testSetId the test set ID + * @param endpointId the endpoint ID to execute tests against + * @return the execution submission response + */ + public Map execute(String testSetId, String endpointId) { + return execute(testSetId, endpointId, ExecutionMode.PARALLEL, null); + } + + /** + * Execute a test set against the given endpoint. + * + * @param testSetId the test set ID + * @param endpointId the endpoint ID to execute tests against + * @param mode execution mode (PARALLEL or SEQUENTIAL) + * @param metrics optional list of metrics for this execution; each map should contain at least an + * "id" key and optionally "name" and "scope" + * @return the execution submission response + */ + public Map execute( + String testSetId, String endpointId, ExecutionMode mode, List> metrics) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (endpointId == null) { + throw new IllegalArgumentException("endpointId must be provided"); + } + + ExecutionRequest body = ExecutionRequest.forExecution(mode, metrics); + String path = "/test_sets/" + testSetId + "/execute/" + endpointId; + return httpClient.post(path, body, new TypeReference>() {}); + } + + /** + * Re-score outputs from an existing test run without calling the endpoint again. + * + * @param testSetId the test set ID + * @param endpointId the endpoint the original run was executed against + * @param referenceTestRunId the test run whose outputs to re-score + * @return the execution submission response + */ + public Map rescore( + String testSetId, String endpointId, String referenceTestRunId) { + return rescore(testSetId, endpointId, referenceTestRunId, ExecutionMode.PARALLEL, null); + } + + /** + * Re-score outputs from an existing test run without calling the endpoint again. + * + * @param testSetId the test set ID + * @param endpointId the endpoint the original run was executed against + * @param referenceTestRunId the test run whose outputs to re-score + * @param mode execution mode (PARALLEL or SEQUENTIAL) + * @param metrics optional list of metrics for re-scoring + * @return the execution submission response + */ + public Map rescore( + String testSetId, + String endpointId, + String referenceTestRunId, + ExecutionMode mode, + List> metrics) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (endpointId == null) { + throw new IllegalArgumentException("endpointId must be provided"); + } + if (referenceTestRunId == null) { + throw new IllegalArgumentException("referenceTestRunId must be provided"); + } + + ExecutionRequest body = ExecutionRequest.forRescore(mode, metrics, referenceTestRunId); + String path = "/test_sets/" + testSetId + "/execute/" + endpointId; + return httpClient.post(path, body, new TypeReference>() {}); + } + + /** + * Get the most recent completed test run for this test set and endpoint. + * + * @param testSetId the test set ID + * @param endpointId the endpoint to look up the last run for + * @return the last run summary, or null if no completed run exists + */ + public TestRun lastRun(String testSetId, String endpointId) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (endpointId == null) { + throw new IllegalArgumentException("endpointId must be provided"); + } + + String path = "/test_sets/" + testSetId + "/last-run/" + endpointId; + return httpClient.get(path, TestRun.class); + } + + /** + * Get metrics associated with this test set. + * + * @param testSetId the test set ID + * @return list of metric maps + */ + public List> getMetrics(String testSetId) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + String path = "/test_sets/" + testSetId + "/metrics"; + return httpClient.get(path, new TypeReference>>() {}); + } + + /** + * Add a metric to this test set. + * + * @param testSetId the test set ID + * @param metricId the metric ID to add + * @return the updated list of metrics on this test set + */ + public List> addMetric(String testSetId, String metricId) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (metricId == null) { + throw new IllegalArgumentException("metricId must be provided"); + } + String path = "/test_sets/" + testSetId + "/metrics/" + metricId; + return httpClient.post(path, Map.of(), new TypeReference>>() {}); + } + + /** + * Remove a metric from this test set. + * + * @param testSetId the test set ID + * @param metricId the metric ID to remove + */ + public void removeMetric(String testSetId, String metricId) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (metricId == null) { + throw new IllegalArgumentException("metricId must be provided"); + } + httpClient.delete("/test_sets/" + testSetId + "/metrics/" + metricId); + } + + /** + * Associate existing tests with this test set. + * + * @param testSetId the test set ID + * @param testIds list of test IDs to associate + * @return association result + */ + public Map addTests(String testSetId, List testIds) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (testIds == null || testIds.isEmpty()) { + throw new IllegalArgumentException("testIds must be provided and non-empty"); + } + String path = "/test_sets/" + testSetId + "/associate"; + Map body = Map.of("test_ids", testIds); + return httpClient.post(path, body, new TypeReference>() {}); + } + + /** + * Remove test associations from this test set. + * + * @param testSetId the test set ID + * @param testIds list of test IDs to disassociate + * @return disassociation result + */ + public Map removeTests(String testSetId, List testIds) { + if (testSetId == null) { + throw new IllegalArgumentException("testSetId must be provided"); + } + if (testIds == null || testIds.isEmpty()) { + throw new IllegalArgumentException("testIds must be provided and non-empty"); + } + String path = "/test_sets/" + testSetId + "/disassociate"; + Map body = Map.of("test_ids", testIds); + return httpClient.post(path, body, new TypeReference>() {}); + } } diff --git a/src/main/java/ai/rhesis/sdk/entities/ExecutionRequest.java b/src/main/java/ai/rhesis/sdk/entities/ExecutionRequest.java new file mode 100644 index 0000000..ec96eb9 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/ExecutionRequest.java @@ -0,0 +1,37 @@ +package ai.rhesis.sdk.entities; + +import ai.rhesis.sdk.enums.ExecutionMode; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import lombok.Builder; + +@Builder +@JsonInclude(JsonInclude.Include.NON_NULL) +public record ExecutionRequest( + @JsonProperty("execution_options") ExecutionOptions executionOptions, + @JsonProperty("metrics") List> metrics, + @JsonProperty("reference_test_run_id") String referenceTestRunId) { + + @Builder + @JsonInclude(JsonInclude.Include.NON_NULL) + public record ExecutionOptions(@JsonProperty("execution_mode") ExecutionMode executionMode) {} + + public static ExecutionRequest forExecution( + ExecutionMode mode, List> metrics) { + return ExecutionRequest.builder() + .executionOptions(ExecutionOptions.builder().executionMode(mode).build()) + .metrics(metrics) + .build(); + } + + public static ExecutionRequest forRescore( + ExecutionMode mode, List> metrics, String referenceTestRunId) { + return ExecutionRequest.builder() + .executionOptions(ExecutionOptions.builder().executionMode(mode).build()) + .metrics(metrics) + .referenceTestRunId(referenceTestRunId) + .build(); + } +} diff --git a/src/main/java/ai/rhesis/sdk/entities/TestRun.java b/src/main/java/ai/rhesis/sdk/entities/TestRun.java index a59a7cb..93ca4d9 100644 --- a/src/main/java/ai/rhesis/sdk/entities/TestRun.java +++ b/src/main/java/ai/rhesis/sdk/entities/TestRun.java @@ -2,6 +2,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import java.util.Map; import lombok.Builder; @@ -12,10 +13,14 @@ public record TestRun( @JsonProperty("name") String name, @JsonProperty("user_id") String userId, @JsonProperty("organization_id") String organizationId, - @JsonProperty("status") Object status, // Can be String enum or Status object + @JsonProperty("status") @JsonDeserialize(using = NameStringDeserializer.class) String status, @JsonProperty("attributes") Map attributes, @JsonProperty("owner_id") String ownerId, - @JsonProperty("assignee_id") String assigneeId) + @JsonProperty("assignee_id") String assigneeId, + @JsonProperty("created_at") String createdAt, + @JsonProperty("nano_id") String nanoId, + @JsonProperty("test_count") Integer testCount, + @JsonProperty("pass_rate") Double passRate) implements BaseEntity { @JsonIgnore diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/ExecutorRunCount.java b/src/main/java/ai/rhesis/sdk/entities/stats/ExecutorRunCount.java new file mode 100644 index 0000000..dfdd943 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/ExecutorRunCount.java @@ -0,0 +1,8 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record ExecutorRunCount( + @JsonProperty("executor_name") String executorName, @JsonProperty("run_count") int runCount) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/MetricStats.java b/src/main/java/ai/rhesis/sdk/entities/stats/MetricStats.java new file mode 100644 index 0000000..4c9fbfd --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/MetricStats.java @@ -0,0 +1,11 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record MetricStats( + @JsonProperty("total") int total, + @JsonProperty("passed") int passed, + @JsonProperty("failed") int failed, + @JsonProperty("pass_rate") double passRate) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/OverallStats.java b/src/main/java/ai/rhesis/sdk/entities/stats/OverallStats.java new file mode 100644 index 0000000..e8d3176 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/OverallStats.java @@ -0,0 +1,11 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record OverallStats( + @JsonProperty("total") int total, + @JsonProperty("passed") int passed, + @JsonProperty("failed") int failed, + @JsonProperty("pass_rate") double passRate) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/ResultDistribution.java b/src/main/java/ai/rhesis/sdk/entities/stats/ResultDistribution.java new file mode 100644 index 0000000..3bfc3a7 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/ResultDistribution.java @@ -0,0 +1,12 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record ResultDistribution( + @JsonProperty("total") int total, + @JsonProperty("passed") int passed, + @JsonProperty("failed") int failed, + @JsonProperty("pending") int pending, + @JsonProperty("pass_rate") double passRate) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/StatusDistribution.java b/src/main/java/ai/rhesis/sdk/entities/stats/StatusDistribution.java new file mode 100644 index 0000000..3622f29 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/StatusDistribution.java @@ -0,0 +1,10 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record StatusDistribution( + @JsonProperty("status") String status, + @JsonProperty("count") int count, + @JsonProperty("percentage") double percentage) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStats.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStats.java new file mode 100644 index 0000000..447c5fd --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStats.java @@ -0,0 +1,17 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import java.util.Map; +import lombok.Builder; + +@Builder +public record TestResultStats( + @JsonProperty("metric_pass_rates") Map metricPassRates, + @JsonProperty("behavior_pass_rates") Map behaviorPassRates, + @JsonProperty("category_pass_rates") Map categoryPassRates, + @JsonProperty("topic_pass_rates") Map topicPassRates, + @JsonProperty("overall_pass_rates") OverallStats overallPassRates, + @JsonProperty("timeline") List timeline, + @JsonProperty("test_run_summary") List testRunSummary, + @JsonProperty("metadata") TestResultStatsMetadata metadata) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStatsMetadata.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStatsMetadata.java new file mode 100644 index 0000000..21a3e6a --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestResultStatsMetadata.java @@ -0,0 +1,21 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Builder; + +@Builder +public record TestResultStatsMetadata( + @JsonProperty("generated_at") String generatedAt, + @JsonProperty("organization_id") String organizationId, + @JsonProperty("test_run_id") String testRunId, + @JsonProperty("period") String period, + @JsonProperty("start_date") String startDate, + @JsonProperty("end_date") String endDate, + @JsonProperty("total_test_runs") int totalTestRuns, + @JsonProperty("total_test_results") int totalTestResults, + @JsonProperty("mode") String mode, + @JsonProperty("available_metrics") List availableMetrics, + @JsonProperty("available_behaviors") List availableBehaviors, + @JsonProperty("available_categories") List availableCategories, + @JsonProperty("available_topics") List availableTopics) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestRunOverallSummary.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunOverallSummary.java new file mode 100644 index 0000000..31af1e4 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunOverallSummary.java @@ -0,0 +1,12 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record TestRunOverallSummary( + @JsonProperty("total_runs") int totalRuns, + @JsonProperty("unique_test_sets") int uniqueTestSets, + @JsonProperty("unique_executors") int uniqueExecutors, + @JsonProperty("most_common_status") String mostCommonStatus, + @JsonProperty("pass_rate") double passRate) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStats.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStats.java new file mode 100644 index 0000000..cea5957 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStats.java @@ -0,0 +1,15 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Builder; + +@Builder +public record TestRunStats( + @JsonProperty("status_distribution") List statusDistribution, + @JsonProperty("result_distribution") ResultDistribution resultDistribution, + @JsonProperty("most_run_test_sets") List mostRunTestSets, + @JsonProperty("top_executors") List topExecutors, + @JsonProperty("timeline") List timeline, + @JsonProperty("overall_summary") TestRunOverallSummary overallSummary, + @JsonProperty("metadata") TestRunStatsMetadata metadata) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStatsMetadata.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStatsMetadata.java new file mode 100644 index 0000000..fc77e80 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunStatsMetadata.java @@ -0,0 +1,18 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; +import lombok.Builder; + +@Builder +public record TestRunStatsMetadata( + @JsonProperty("generated_at") String generatedAt, + @JsonProperty("organization_id") String organizationId, + @JsonProperty("period") String period, + @JsonProperty("start_date") String startDate, + @JsonProperty("end_date") String endDate, + @JsonProperty("total_test_runs") int totalTestRuns, + @JsonProperty("mode") String mode, + @JsonProperty("available_statuses") List availableStatuses, + @JsonProperty("available_test_sets") List availableTestSets, + @JsonProperty("available_executors") List availableExecutors) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestRunSummary.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunSummary.java new file mode 100644 index 0000000..40ff3d5 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunSummary.java @@ -0,0 +1,14 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import lombok.Builder; + +@Builder +public record TestRunSummary( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("created_at") String createdAt, + @JsonProperty("total_tests") int totalTests, + @JsonProperty("overall") OverallStats overall, + @JsonProperty("metrics") Map metrics) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestRunTimelineData.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunTimelineData.java new file mode 100644 index 0000000..ff91d94 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestRunTimelineData.java @@ -0,0 +1,11 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import lombok.Builder; + +@Builder +public record TestRunTimelineData( + @JsonProperty("date") String date, + @JsonProperty("total_runs") int totalRuns, + @JsonProperty("result_breakdown") Map resultBreakdown) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TestSetRunCount.java b/src/main/java/ai/rhesis/sdk/entities/stats/TestSetRunCount.java new file mode 100644 index 0000000..62353f2 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TestSetRunCount.java @@ -0,0 +1,8 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.Builder; + +@Builder +public record TestSetRunCount( + @JsonProperty("test_set_name") String testSetName, @JsonProperty("run_count") int runCount) {} diff --git a/src/main/java/ai/rhesis/sdk/entities/stats/TimelineData.java b/src/main/java/ai/rhesis/sdk/entities/stats/TimelineData.java new file mode 100644 index 0000000..758a840 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/entities/stats/TimelineData.java @@ -0,0 +1,11 @@ +package ai.rhesis.sdk.entities.stats; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Map; +import lombok.Builder; + +@Builder +public record TimelineData( + @JsonProperty("date") String date, + @JsonProperty("overall") OverallStats overall, + @JsonProperty("metrics") Map metrics) {} diff --git a/src/main/java/ai/rhesis/sdk/enums/ExecutionMode.java b/src/main/java/ai/rhesis/sdk/enums/ExecutionMode.java new file mode 100644 index 0000000..1cf9ee2 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/enums/ExecutionMode.java @@ -0,0 +1,37 @@ +package ai.rhesis.sdk.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum ExecutionMode { + PARALLEL("Parallel"), + SEQUENTIAL("Sequential"); + + private final String value; + + ExecutionMode(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static ExecutionMode fromValue(String value) { + if (value == null) { + return PARALLEL; + } + String normalized = value.strip().toLowerCase(); + for (ExecutionMode mode : values()) { + if (mode.value.toLowerCase().equals(normalized)) { + return mode; + } + } + throw new IllegalArgumentException( + "Invalid execution mode: '" + + value + + "'. Use 'parallel', 'sequential', or ExecutionMode.PARALLEL / ExecutionMode.SEQUENTIAL"); + } +} diff --git a/src/main/java/ai/rhesis/sdk/enums/TestResultStatsMode.java b/src/main/java/ai/rhesis/sdk/enums/TestResultStatsMode.java new file mode 100644 index 0000000..2536610 --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/enums/TestResultStatsMode.java @@ -0,0 +1,37 @@ +package ai.rhesis.sdk.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TestResultStatsMode { + ALL("all"), + SUMMARY("summary"), + METRICS("metrics"), + BEHAVIOR("behavior"), + CATEGORY("category"), + TOPIC("topic"), + OVERALL("overall"), + TIMELINE("timeline"), + TEST_RUNS("test_runs"); + + private final String value; + + TestResultStatsMode(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static TestResultStatsMode fromValue(String value) { + for (TestResultStatsMode mode : values()) { + if (mode.value.equalsIgnoreCase(value)) { + return mode; + } + } + throw new IllegalArgumentException("Invalid TestResultStatsMode: " + value); + } +} diff --git a/src/main/java/ai/rhesis/sdk/enums/TestRunStatsMode.java b/src/main/java/ai/rhesis/sdk/enums/TestRunStatsMode.java new file mode 100644 index 0000000..555d24b --- /dev/null +++ b/src/main/java/ai/rhesis/sdk/enums/TestRunStatsMode.java @@ -0,0 +1,35 @@ +package ai.rhesis.sdk.enums; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum TestRunStatsMode { + ALL("all"), + SUMMARY("summary"), + STATUS("status"), + RESULTS("results"), + TEST_SETS("test_sets"), + EXECUTORS("executors"), + TIMELINE("timeline"); + + private final String value; + + TestRunStatsMode(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static TestRunStatsMode fromValue(String value) { + for (TestRunStatsMode mode : values()) { + if (mode.value.equalsIgnoreCase(value)) { + return mode; + } + } + throw new IllegalArgumentException("Invalid TestRunStatsMode: " + value); + } +} diff --git a/src/test/java/ai/rhesis/sdk/examples/ExecuteTestSetExample.java b/src/test/java/ai/rhesis/sdk/examples/ExecuteTestSetExample.java new file mode 100644 index 0000000..edb29ea --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/ExecuteTestSetExample.java @@ -0,0 +1,47 @@ +package ai.rhesis.sdk.examples; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.Endpoint; +import ai.rhesis.sdk.entities.TestSet; +import ai.rhesis.sdk.enums.ExecutionMode; +import java.util.List; +import java.util.Map; + +public class ExecuteTestSetExample { + public static void main(String[] args) { + RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build(); + + // Find a test set and endpoint to execute against + List testSets = client.testSets().list(); + List endpoints = client.endpoints().list(); + + if (testSets.isEmpty() || endpoints.isEmpty()) { + System.err.println("Need at least one test set and one endpoint to run this example."); + return; + } + + TestSet testSet = testSets.get(0); + Endpoint endpoint = endpoints.get(0); + + System.out.println("Test Set: " + testSet.name() + " (" + testSet.id() + ")"); + System.out.println("Endpoint: " + endpoint.name() + " (" + endpoint.id() + ")"); + + // Execute the test set with default parallel mode + System.out.println("\nExecuting test set (parallel mode)..."); + Map result = client.testSets().execute(testSet.id(), endpoint.id()); + System.out.println("Execution submitted: " + result); + + // Execute with sequential mode + System.out.println("\nExecuting test set (sequential mode)..."); + Map seqResult = + client.testSets().execute(testSet.id(), endpoint.id(), ExecutionMode.SEQUENTIAL, null); + System.out.println("Execution submitted: " + seqResult); + + // Execute with specific metrics + System.out.println("\nExecuting test set with custom metrics..."); + List> metrics = List.of(Map.of("id", "your-metric-id", "name", "Accuracy")); + Map metricResult = + client.testSets().execute(testSet.id(), endpoint.id(), ExecutionMode.PARALLEL, metrics); + System.out.println("Execution submitted: " + metricResult); + } +} diff --git a/src/test/java/ai/rhesis/sdk/examples/README.md b/src/test/java/ai/rhesis/sdk/examples/README.md new file mode 100644 index 0000000..ac0e435 --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/README.md @@ -0,0 +1,125 @@ +# Rhesis Java SDK Examples + +Runnable examples demonstrating the core capabilities of the Rhesis Java SDK. + +## Prerequisites + +- Java 21+ +- Maven 3.8+ +- A Rhesis API key (get one at [rhesis.ai](https://rhesis.ai)) + +## Setup + +### 1. Install the SDK + +Add the dependency to your `pom.xml`: + +```xml + + ai.rhesis + rhesis-java + 0.1.1 + +``` + +Or build from source: + +```bash +git clone https://github.com/rhesis-ai/rhesis-java.git +cd rhesis-java +mvn install -DskipTests +``` + +### 2. Set your API key + +Create a `.env` file in the project root: + +``` +RHESIS_API_KEY=your-api-key-here +``` + +Or export it as an environment variable: + +```bash +export RHESIS_API_KEY=your-api-key-here +``` + +### 3. Run an example + +```bash +mvn compile test-compile exec:java \ + -Dexec.mainClass="ai.rhesis.sdk.examples.TestRunStatsExample" \ + -Dexec.classpathScope=test +``` + +Replace the class name with any example listed below. + +## Examples + +### Endpoints & Projects + +| Example | Description | +|---------|-------------| +| `CreateEndpointExample` | Look up a project by name and create a REST endpoint with request/response mappings. | + +### Test Generation + +| Example | Description | +|---------|-------------| +| `GenerateTestSetExample` | Configure a multi-turn synthesizer, generate tests, and push the test set to Rhesis. | +| `GenerateTestSetWithFilesExample` | Generate a test set, push it, then attach files to each test. | + +### File Management + +| Example | Description | +|---------|-------------| +| `FileSupportExample` | Create a test with an attached file, list files, download content, then clean up. | + +### Test Execution + +| Example | Description | +|---------|-------------| +| `ExecuteTestSetExample` | Trigger a test set run against an endpoint โ€” parallel mode, sequential mode, and with custom metrics. | +| `TestRunWorkflowExample` | Full lifecycle: list runs, inspect results, fetch stats, get last run, and rescore. | + +### Test Set Management + +| Example | Description | +|---------|-------------| +| `TestSetMetricsExample` | List, add, and remove metrics on a test set. Associate and disassociate tests. | + +### Analytics & Stats + +| Example | Description | +|---------|-------------| +| `TestRunStatsExample` | Test run analytics: overall summary, status distribution, most-run test sets, timeline, filtering by mode/months/run IDs. | +| `TestResultStatsExample` | Test result analytics: pass rates by metric, behavior, category, and topic. Timeline trends, per-run summaries, and filtered queries. | + +## Quick Reference + +```java +// Initialize the client +RhesisClient client = RhesisClient.builder() + .apiKey(System.getenv("RHESIS_API_KEY")) + .build(); + +// Execute a test set +Map result = client.testSets() + .execute(testSetId, endpointId); + +// Get test run stats +TestRunStats stats = client.testRuns().stats(); +System.out.println("Pass rate: " + stats.overallSummary().passRate() + "%"); + +// Get test result stats by behavior +TestResultStats behaviorStats = client.testResults() + .stats(TestResultStatsMode.BEHAVIOR); + +// Get last completed run +TestRun lastRun = client.testSets() + .lastRun(testSetId, endpointId); + +// Rescore a previous run +Map rescore = client.testSets() + .rescore(testSetId, endpointId, lastRun.id()); +``` diff --git a/src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java b/src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java new file mode 100644 index 0000000..9c9a5b6 --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/TestResultStatsExample.java @@ -0,0 +1,123 @@ +package ai.rhesis.sdk.examples; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.TestRun; +import ai.rhesis.sdk.entities.stats.MetricStats; +import ai.rhesis.sdk.entities.stats.TestResultStats; +import ai.rhesis.sdk.entities.stats.TestRunSummary; +import ai.rhesis.sdk.entities.stats.TimelineData; +import ai.rhesis.sdk.enums.TestResultStatsMode; +import java.util.List; +import java.util.Map; + +public class TestResultStatsExample { + public static void main(String[] args) { + RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build(); + + // --- Full test result stats --- + System.out.println("=== Test Result Stats (all) ==="); + TestResultStats stats = client.testResults().stats(); + + if (stats.overallPassRates() != null) { + System.out.println("Total results: " + stats.overallPassRates().total()); + System.out.println("Passed: " + stats.overallPassRates().passed()); + System.out.println("Failed: " + stats.overallPassRates().failed()); + System.out.println("Pass rate: " + stats.overallPassRates().passRate() + "%"); + } + + // --- Metric pass rates --- + if (stats.metricPassRates() != null) { + System.out.println("\n=== Metric Pass Rates ==="); + for (Map.Entry entry : stats.metricPassRates().entrySet()) { + MetricStats m = entry.getValue(); + System.out.printf( + " %-20s total=%d passed=%d failed=%d rate=%.1f%%%n", + entry.getKey(), m.total(), m.passed(), m.failed(), m.passRate()); + } + } + + // --- Behavior breakdown --- + System.out.println("\n=== Behavior Pass Rates ==="); + TestResultStats behaviorStats = client.testResults().stats(TestResultStatsMode.BEHAVIOR); + if (behaviorStats.behaviorPassRates() != null) { + for (Map.Entry entry : behaviorStats.behaviorPassRates().entrySet()) { + System.out.printf( + " %-25s rate=%.1f%% (%d/%d)%n", + entry.getKey(), + entry.getValue().passRate(), + entry.getValue().passed(), + entry.getValue().total()); + } + } + + // --- Category breakdown --- + System.out.println("\n=== Category Pass Rates ==="); + TestResultStats categoryStats = client.testResults().stats(TestResultStatsMode.CATEGORY); + if (categoryStats.categoryPassRates() != null) { + for (Map.Entry entry : categoryStats.categoryPassRates().entrySet()) { + System.out.printf(" %-25s rate=%.1f%%%n", entry.getKey(), entry.getValue().passRate()); + } + } + + // --- Topic breakdown --- + System.out.println("\n=== Topic Pass Rates ==="); + TestResultStats topicStats = client.testResults().stats(TestResultStatsMode.TOPIC); + if (topicStats.topicPassRates() != null) { + for (Map.Entry entry : topicStats.topicPassRates().entrySet()) { + System.out.printf(" %-25s rate=%.1f%%%n", entry.getKey(), entry.getValue().passRate()); + } + } + + // --- Timeline --- + if (stats.timeline() != null) { + System.out.println("\n=== Timeline ==="); + for (TimelineData point : stats.timeline()) { + System.out.printf( + " %s overall: %d/%d (%.1f%%)%n", + point.date(), + point.overall().passed(), + point.overall().total(), + point.overall().passRate()); + } + } + + // --- Per-run summary --- + if (stats.testRunSummary() != null) { + System.out.println("\n=== Per-Run Summary ==="); + for (TestRunSummary run : stats.testRunSummary()) { + System.out.printf( + " [%s] %s โ€” %d tests, rate=%.1f%%%n", + run.id(), run.name(), run.totalTests(), run.overall().passRate()); + } + } + + // --- Filtered by a specific test run --- + List runs = client.testRuns().list(); + if (!runs.isEmpty()) { + System.out.println("\n=== Results for run: " + runs.get(0).name() + " ==="); + TestResultStats runStats = + client + .testResults() + .stats(TestResultStatsMode.ALL, Map.of("test_run_ids", List.of(runs.get(0).id()))); + + if (runStats.overallPassRates() != null) { + System.out.println("Pass rate: " + runStats.overallPassRates().passRate() + "%"); + } + if (runStats.metricPassRates() != null) { + System.out.println("Metrics evaluated: " + runStats.metricPassRates().size()); + } + } + + // --- Metadata --- + if (stats.metadata() != null) { + System.out.println("\n=== Metadata ==="); + System.out.println("Period: " + stats.metadata().period()); + System.out.println("Total runs: " + stats.metadata().totalTestRuns()); + System.out.println("Total results: " + stats.metadata().totalTestResults()); + System.out.println("Metrics: " + stats.metadata().availableMetrics()); + System.out.println("Behaviors: " + stats.metadata().availableBehaviors()); + System.out.println("Categories: " + stats.metadata().availableCategories()); + System.out.println("Topics: " + stats.metadata().availableTopics()); + } + } +} diff --git a/src/test/java/ai/rhesis/sdk/examples/TestRunStatsExample.java b/src/test/java/ai/rhesis/sdk/examples/TestRunStatsExample.java new file mode 100644 index 0000000..258c19b --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/TestRunStatsExample.java @@ -0,0 +1,81 @@ +package ai.rhesis.sdk.examples; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.TestRun; +import ai.rhesis.sdk.entities.stats.StatusDistribution; +import ai.rhesis.sdk.entities.stats.TestRunStats; +import ai.rhesis.sdk.entities.stats.TestRunTimelineData; +import ai.rhesis.sdk.entities.stats.TestSetRunCount; +import ai.rhesis.sdk.enums.TestRunStatsMode; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +public class TestRunStatsExample { + public static void main(String[] args) { + RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build(); + + // --- Full stats (all sections) --- + System.out.println("=== Test Run Stats (all) ==="); + TestRunStats stats = client.testRuns().stats(); + + if (stats.overallSummary() != null) { + System.out.println("Total runs: " + stats.overallSummary().totalRuns()); + System.out.println("Unique test sets: " + stats.overallSummary().uniqueTestSets()); + System.out.println("Unique executors: " + stats.overallSummary().uniqueExecutors()); + System.out.println("Pass rate: " + stats.overallSummary().passRate() + "%"); + System.out.println("Most common: " + stats.overallSummary().mostCommonStatus()); + } + + // --- Status distribution --- + if (stats.statusDistribution() != null) { + System.out.println("\n=== Status Distribution ==="); + for (StatusDistribution sd : stats.statusDistribution()) { + System.out.printf(" %-12s %d (%.1f%%)%n", sd.status(), sd.count(), sd.percentage()); + } + } + + // --- Most run test sets --- + if (stats.mostRunTestSets() != null) { + System.out.println("\n=== Most Run Test Sets ==="); + for (TestSetRunCount ts : stats.mostRunTestSets()) { + System.out.printf(" %-30s %d runs%n", ts.testSetName(), ts.runCount()); + } + } + + // --- Timeline --- + if (stats.timeline() != null) { + System.out.println("\n=== Timeline ==="); + for (TestRunTimelineData point : stats.timeline()) { + System.out.printf(" %s total_runs=%d%n", point.date(), point.totalRuns()); + } + } + + // --- Summary-only mode (lighter payload) --- + System.out.println("\n=== Summary Mode ==="); + TestRunStats summary = client.testRuns().stats(TestRunStatsMode.SUMMARY); + if (summary.overallSummary() != null) { + System.out.println("Total runs: " + summary.overallSummary().totalRuns()); + } + + // --- Stats scoped to specific runs --- + List runs = client.testRuns().list(); + if (!runs.isEmpty()) { + System.out.println("\n=== Stats for run: " + runs.get(0).name() + " ==="); + TestRunStats scoped = client.testRuns().stats(List.of(runs.get(0).id())); + if (scoped.overallSummary() != null) { + System.out.println("Pass rate: " + scoped.overallSummary().passRate() + "%"); + } + } + + // --- Stats with custom filter params --- + System.out.println("\n=== Stats (last 3 months) ==="); + Map params = new LinkedHashMap<>(); + params.put("months", 3); + TestRunStats filtered = client.testRuns().stats(TestRunStatsMode.ALL, params); + if (filtered.metadata() != null) { + System.out.println("Period: " + filtered.metadata().period()); + System.out.println("Total runs: " + filtered.metadata().totalTestRuns()); + } + } +} diff --git a/src/test/java/ai/rhesis/sdk/examples/TestRunWorkflowExample.java b/src/test/java/ai/rhesis/sdk/examples/TestRunWorkflowExample.java new file mode 100644 index 0000000..f8390e9 --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/TestRunWorkflowExample.java @@ -0,0 +1,94 @@ +package ai.rhesis.sdk.examples; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.Endpoint; +import ai.rhesis.sdk.entities.TestResult; +import ai.rhesis.sdk.entities.TestRun; +import ai.rhesis.sdk.entities.TestSet; +import ai.rhesis.sdk.entities.stats.TestRunStats; +import ai.rhesis.sdk.exceptions.RhesisApiException; +import java.util.List; +import java.util.Map; + +public class TestRunWorkflowExample { + public static void main(String[] args) { + RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build(); + + // --- List and inspect test runs --- + System.out.println("=== Listing Test Runs ==="); + List runs = client.testRuns().list(); + System.out.println("Found " + runs.size() + " test runs"); + + for (TestRun run : runs.subList(0, Math.min(3, runs.size()))) { + System.out.printf(" [%s] %s - status: %s%n", run.id(), run.name(), run.status()); + } + + // --- Get test results for a run --- + if (!runs.isEmpty()) { + TestRun firstRun = runs.get(0); + System.out.println("\n=== Test Results for: " + firstRun.name() + " ==="); + + List results = client.testRuns().getTestResults(firstRun.id()); + System.out.println("Found " + results.size() + " test results"); + + for (TestResult result : results.subList(0, Math.min(3, results.size()))) { + System.out.printf( + " [%s] status: %s%n", + result.id(), result.status() != null ? result.status().name() : "unknown"); + } + } + + // --- Get test run stats --- + System.out.println("\n=== Test Run Stats ==="); + TestRunStats stats = client.testRuns().stats(); + if (stats.overallSummary() != null) { + System.out.println("Total runs: " + stats.overallSummary().totalRuns()); + System.out.println("Pass rate: " + stats.overallSummary().passRate()); + System.out.println("Unique test sets: " + stats.overallSummary().uniqueTestSets()); + } + + // Stats scoped to specific runs + if (!runs.isEmpty()) { + System.out.println("\n=== Stats for first run ==="); + TestRunStats scopedStats = client.testRuns().stats(List.of(runs.get(0).id())); + if (scopedStats.overallSummary() != null) { + System.out.println("Scoped pass rate: " + scopedStats.overallSummary().passRate()); + } + } + + // --- Last run for a test set + endpoint pair --- + List testSets = client.testSets().list(); + List endpoints = client.endpoints().list(); + + if (!testSets.isEmpty() && !endpoints.isEmpty()) { + System.out.println("\n=== Last Run ==="); + try { + TestRun lastRun = client.testSets().lastRun(testSets.get(0).id(), endpoints.get(0).id()); + System.out.printf( + "Last run: %s (status: %s, pass rate: %s)%n", + lastRun.name(), lastRun.status(), lastRun.passRate()); + } catch (RhesisApiException e) { + if (e.getStatusCode() == 404) { + System.out.println("No completed run found for this test set + endpoint pair."); + } else { + throw e; + } + } + } + + // --- Rescore an existing run --- + if (!testSets.isEmpty() && !endpoints.isEmpty() && !runs.isEmpty()) { + System.out.println("\n=== Rescore ==="); + try { + Map rescoreResult = + client + .testSets() + .rescore(testSets.get(0).id(), endpoints.get(0).id(), runs.get(0).id()); + System.out.println("Rescore submitted: " + rescoreResult); + } catch (RhesisApiException e) { + System.out.println( + "Rescore failed (expected if run doesn't belong to this pair): " + e.getMessage()); + } + } + } +} diff --git a/src/test/java/ai/rhesis/sdk/examples/TestSetMetricsExample.java b/src/test/java/ai/rhesis/sdk/examples/TestSetMetricsExample.java new file mode 100644 index 0000000..0c231b3 --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/TestSetMetricsExample.java @@ -0,0 +1,61 @@ +package ai.rhesis.sdk.examples; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.TestSet; +import java.util.List; +import java.util.Map; + +public class TestSetMetricsExample { + public static void main(String[] args) { + RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build(); + + List testSets = client.testSets().list(); + if (testSets.isEmpty()) { + System.err.println("No test sets found."); + return; + } + + TestSet testSet = testSets.get(0); + System.out.println("Test Set: " + testSet.name() + " (" + testSet.id() + ")"); + + // --- List current metrics --- + System.out.println("\n=== Current Metrics ==="); + List> metrics = client.testSets().getMetrics(testSet.id()); + if (metrics.isEmpty()) { + System.out.println("No metrics assigned yet."); + } else { + for (Map metric : metrics) { + System.out.printf(" [%s] %s%n", metric.get("id"), metric.get("name")); + } + } + + // --- Add a metric by ID --- + // Uncomment and replace with a real metric ID to test: + // + // String metricId = "your-metric-uuid"; + // System.out.println("\nAdding metric " + metricId + "..."); + // List> updated = client.testSets().addMetric(testSet.id(), metricId); + // System.out.println("Metrics after add: " + updated); + + // --- Remove a metric --- + // Uncomment and replace with a real metric ID to test: + // + // System.out.println("\nRemoving metric " + metricId + "..."); + // client.testSets().removeMetric(testSet.id(), metricId); + // System.out.println("Metric removed."); + + // --- Associate / disassociate tests --- + System.out.println("\n=== Test Association ==="); + List tests = client.testSets().getTests(testSet.id()); + System.out.println("Current test count: " + tests.size()); + + // Uncomment to associate tests by ID: + // + // List testIds = List.of("test-uuid-1", "test-uuid-2"); + // Map addResult = client.testSets().addTests(testSet.id(), testIds); + // System.out.println("Add result: " + addResult); + // + // Map removeResult = client.testSets().removeTests(testSet.id(), testIds); + // System.out.println("Remove result: " + removeResult); + } +} diff --git a/src/test/java/ai/rhesis/sdk/integration/TestRunIntegrationTest.java b/src/test/java/ai/rhesis/sdk/integration/TestRunIntegrationTest.java new file mode 100644 index 0000000..7bca5aa --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/integration/TestRunIntegrationTest.java @@ -0,0 +1,218 @@ +package ai.rhesis.sdk.integration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import ai.rhesis.sdk.RhesisClient; +import ai.rhesis.sdk.entities.Endpoint; +import ai.rhesis.sdk.entities.TestRun; +import ai.rhesis.sdk.entities.TestSet; +import ai.rhesis.sdk.entities.stats.TestResultStats; +import ai.rhesis.sdk.entities.stats.TestRunStats; +import ai.rhesis.sdk.enums.ExecutionMode; +import ai.rhesis.sdk.enums.TestResultStatsMode; +import ai.rhesis.sdk.enums.TestRunStatsMode; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestMethodOrder; + +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class TestRunIntegrationTest extends BaseIntegrationTest { + + private static String testSetId; + private static String endpointId; + + @BeforeAll + static void setupGlobalClient() { + RhesisClient.setDefault(client); + + List testSets = client.testSets().list(); + if (!testSets.isEmpty()) { + testSetId = testSets.get(0).id(); + } + + List endpoints = client.endpoints().list(); + if (!endpoints.isEmpty()) { + endpointId = endpoints.get(0).id(); + } + } + + @Test + @Order(1) + void testListTestRuns() { + List runs = client.testRuns().list(); + assertThat(runs).isNotNull(); + } + + @Test + @Order(2) + void testGetTestRun() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available to fetch"); + + TestRun run = client.testRuns().get(runs.get(0).id()); + assertThat(run).isNotNull(); + assertThat(run.id()).isEqualTo(runs.get(0).id()); + assertThat(run.name()).isNotNull(); + } + + @Test + @Order(3) + void testGetTestRunResults() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available to fetch results"); + + var results = client.testRuns().getTestResults(runs.get(0).id()); + assertThat(results).isNotNull(); + } + + @Test + @Order(4) + void testTestRunStats() { + TestRunStats stats = client.testRuns().stats(); + assertThat(stats).isNotNull(); + assertThat(stats.metadata()).isNotNull(); + } + + @Test + @Order(5) + void testTestRunStatsSummaryMode() { + TestRunStats stats = client.testRuns().stats(TestRunStatsMode.SUMMARY); + assertThat(stats).isNotNull(); + } + + @Test + @Order(6) + void testTestRunStatsStatusMode() { + TestRunStats stats = client.testRuns().stats(TestRunStatsMode.STATUS); + assertThat(stats).isNotNull(); + } + + @Test + @Order(7) + void testTestRunStatsFilteredByRunIds() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available for filtered stats"); + + TestRunStats stats = client.testRuns().stats(List.of(runs.get(0).id())); + assertThat(stats).isNotNull(); + } + + @Test + @Order(8) + void testTestRunStatsWithFilterParams() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available for filtered stats"); + + Map params = Map.of("months", 3, "test_run_ids", List.of(runs.get(0).id())); + TestRunStats stats = client.testRuns().stats(TestRunStatsMode.ALL, params); + assertThat(stats).isNotNull(); + } + + @Test + @Order(9) + void testTestResultStats() { + TestResultStats stats = client.testResults().stats(); + assertThat(stats).isNotNull(); + assertThat(stats.metadata()).isNotNull(); + } + + @Test + @Order(10) + void testTestResultStatsMetricsMode() { + TestResultStats stats = client.testResults().stats(TestResultStatsMode.METRICS); + assertThat(stats).isNotNull(); + } + + @Test + @Order(11) + void testTestResultStatsBehaviorMode() { + TestResultStats stats = client.testResults().stats(TestResultStatsMode.BEHAVIOR); + assertThat(stats).isNotNull(); + } + + @Test + @Order(12) + void testTestResultStatsCategoryMode() { + TestResultStats stats = client.testResults().stats(TestResultStatsMode.CATEGORY); + assertThat(stats).isNotNull(); + } + + @Test + @Order(13) + void testTestResultStatsOverallMode() { + TestResultStats stats = client.testResults().stats(TestResultStatsMode.OVERALL); + assertThat(stats).isNotNull(); + if (stats.overallPassRates() != null) { + assertThat(stats.overallPassRates().passRate()).isBetween(0.0, 100.0); + } + } + + @Test + @Order(14) + void testTestResultStatsWithRunIdFilter() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available for filtered result stats"); + + Map params = Map.of("test_run_ids", List.of(runs.get(0).id())); + TestResultStats stats = client.testResults().stats(TestResultStatsMode.ALL, params); + assertThat(stats).isNotNull(); + } + + @Test + @Order(20) + void testLastRun() { + assumeTrue(testSetId != null, "No test set available"); + assumeTrue(endpointId != null, "No endpoint available"); + + try { + TestRun lastRun = client.testSets().lastRun(testSetId, endpointId); + if (lastRun != null) { + assertThat(lastRun.id()).isNotNull(); + } + } catch (ai.rhesis.sdk.exceptions.RhesisApiException e) { + if (e.getStatusCode() == 404) { + System.out.println("No last run found for test set + endpoint combination (expected)"); + } else { + throw e; + } + } + } + + @Test + @Order(21) + void testExecuteTestSet() { + assumeTrue(testSetId != null, "No test set available"); + assumeTrue(endpointId != null, "No endpoint available"); + + Map result = + client.testSets().execute(testSetId, endpointId, ExecutionMode.PARALLEL, null); + assertThat(result).isNotNull(); + } + + @Test + @Order(22) + void testGetTestSetMetrics() { + assumeTrue(testSetId != null, "No test set available"); + + List> metrics = client.testSets().getMetrics(testSetId); + assertThat(metrics).isNotNull(); + } + + @Test + @Order(23) + void testTestRunStatusDeserialization() { + List runs = client.testRuns().list(); + assumeTrue(!runs.isEmpty(), "No test runs available"); + + TestRun run = client.testRuns().get(runs.get(0).id()); + if (run.status() != null) { + assertThat(run.status()).isInstanceOf(String.class); + assertThat(run.status()).isNotEmpty(); + } + } +} diff --git a/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java b/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java index 20f0374..55dba1d 100644 --- a/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java +++ b/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java @@ -9,11 +9,16 @@ import ai.rhesis.sdk.entities.TestResult; import ai.rhesis.sdk.entities.TestRun; import ai.rhesis.sdk.entities.TestSet; -import ai.rhesis.sdk.enums.RunStatus; +import ai.rhesis.sdk.entities.stats.TestResultStats; +import ai.rhesis.sdk.entities.stats.TestRunStats; +import ai.rhesis.sdk.enums.ExecutionMode; +import ai.rhesis.sdk.enums.TestResultStatsMode; +import ai.rhesis.sdk.enums.TestRunStatsMode; import ai.rhesis.sdk.enums.TestType; import com.github.tomakehurst.wiremock.WireMockServer; import com.github.tomakehurst.wiremock.client.WireMock; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -23,6 +28,7 @@ class ClientWiremockTest { private static TestClient testClient; private static TestSetClient testSetClient; private static TestRunClient testRunClient; + private static TestResultClient testResultClient; private static FileClient fileClient; @BeforeAll @@ -36,6 +42,7 @@ static void setUp() { testClient = rhesisClient.tests(); testSetClient = rhesisClient.testSets(); testRunClient = rhesisClient.testRuns(); + testResultClient = rhesisClient.testResults(); fileClient = rhesisClient.files(); } @@ -93,7 +100,7 @@ void testGetTestRun() { TestRun response = testRunClient.get("tr-1"); assertThat(response.id()).isEqualTo("tr-1"); - assertThat(response.status()).isEqualTo(RunStatus.COMPLETED.getValue()); + assertThat(response.status()).isEqualTo("Completed"); } @Test @@ -279,4 +286,289 @@ void testCreateTestWithFiles() throws Exception { java.nio.file.Files.delete(tempFile); } } + + @Test + void testExecuteTestSet() { + stubFor( + post(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"test_run_id\":\"tr-new\",\"status\":\"Progress\"}"))); + + Map response = testSetClient.execute("ts-1", "ep-1"); + assertThat(response).containsEntry("test_run_id", "tr-new"); + assertThat(response).containsEntry("status", "Progress"); + + verify( + 1, + postRequestedFor(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withRequestBody(containing("\"execution_mode\":\"Parallel\""))); + } + + @Test + void testExecuteTestSetSequential() { + stubFor( + post(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"test_run_id\":\"tr-seq\"}"))); + + Map response = + testSetClient.execute("ts-1", "ep-1", ExecutionMode.SEQUENTIAL, null); + assertThat(response).containsEntry("test_run_id", "tr-seq"); + + verify( + 1, + postRequestedFor(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withRequestBody(containing("\"execution_mode\":\"Sequential\""))); + } + + @Test + void testRescoreTestSet() { + stubFor( + post(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"test_run_id\":\"tr-rescore\"}"))); + + Map response = testSetClient.rescore("ts-1", "ep-1", "tr-original"); + assertThat(response).containsEntry("test_run_id", "tr-rescore"); + + verify( + 1, + postRequestedFor(urlEqualTo("/test_sets/ts-1/execute/ep-1")) + .withRequestBody(containing("\"reference_test_run_id\":\"tr-original\""))); + } + + @Test + void testLastRun() { + stubFor( + get(urlEqualTo("/test_sets/ts-1/last-run/ep-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"tr-last\",\"name\":\"Run 42\"," + + "\"status\":\"Completed\",\"pass_rate\":0.95}"))); + + TestRun response = testSetClient.lastRun("ts-1", "ep-1"); + assertThat(response.id()).isEqualTo("tr-last"); + assertThat(response.name()).isEqualTo("Run 42"); + assertThat(response.status()).isEqualTo("Completed"); + assertThat(response.passRate()).isEqualTo(0.95); + } + + @Test + void testGetTestRunWithNestedStatus() { + stubFor( + get(urlEqualTo("/test_runs/tr-nested")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"tr-nested\"," + + "\"status\":{\"name\":\"Completed\",\"id\":\"s-1\"}," + + "\"name\":\"Run Nested\"}"))); + + TestRun response = testRunClient.get("tr-nested"); + assertThat(response.id()).isEqualTo("tr-nested"); + assertThat(response.status()).isEqualTo("Completed"); + } + + @Test + void testTestRunStats() { + stubFor( + get(urlPathEqualTo("/test_runs/stats")) + .withQueryParam("mode", equalTo("all")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"overall_summary\":{\"total_runs\":10,\"unique_test_sets\":3," + + "\"unique_executors\":2,\"most_common_status\":\"Completed\"," + + "\"pass_rate\":0.85}," + + "\"status_distribution\":[{\"status\":\"Completed\",\"count\":8,\"percentage\":80.0}]," + + "\"metadata\":{\"mode\":\"all\",\"total_test_runs\":10}}"))); + + TestRunStats response = testRunClient.stats(); + assertThat(response.overallSummary()).isNotNull(); + assertThat(response.overallSummary().totalRuns()).isEqualTo(10); + assertThat(response.overallSummary().passRate()).isEqualTo(0.85); + assertThat(response.statusDistribution()).hasSize(1); + assertThat(response.statusDistribution().get(0).status()).isEqualTo("Completed"); + assertThat(response.metadata().totalTestRuns()).isEqualTo(10); + } + + @Test + void testTestRunStatsWithMode() { + stubFor( + get(urlPathEqualTo("/test_runs/stats")) + .withQueryParam("mode", equalTo("summary")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"overall_summary\":{\"total_runs\":5,\"unique_test_sets\":1," + + "\"unique_executors\":1,\"most_common_status\":\"Completed\"," + + "\"pass_rate\":0.9}}"))); + + TestRunStats response = testRunClient.stats(TestRunStatsMode.SUMMARY); + assertThat(response.overallSummary()).isNotNull(); + assertThat(response.overallSummary().totalRuns()).isEqualTo(5); + } + + @Test + void testTestRunStatsWithRunIds() { + stubFor( + get(urlPathEqualTo("/test_runs/stats")) + .withQueryParam("mode", equalTo("all")) + .withQueryParam("test_run_ids", equalTo("tr-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"overall_summary\":{\"total_runs\":1,\"unique_test_sets\":1," + + "\"unique_executors\":1,\"most_common_status\":\"Completed\"," + + "\"pass_rate\":1.0}}"))); + + TestRunStats response = testRunClient.stats(List.of("tr-1")); + assertThat(response.overallSummary()).isNotNull(); + assertThat(response.overallSummary().totalRuns()).isEqualTo(1); + } + + @Test + void testTestResultStats() { + stubFor( + get(urlPathEqualTo("/test_results/stats")) + .withQueryParam("mode", equalTo("all")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"overall_pass_rates\":{\"total\":100,\"passed\":85," + + "\"failed\":15,\"pass_rate\":0.85}," + + "\"metric_pass_rates\":{\"Accuracy\":{\"total\":50,\"passed\":45," + + "\"failed\":5,\"pass_rate\":0.9}}," + + "\"metadata\":{\"mode\":\"all\",\"total_test_results\":100}}"))); + + TestResultStats response = testResultClient.stats(); + assertThat(response.overallPassRates()).isNotNull(); + assertThat(response.overallPassRates().total()).isEqualTo(100); + assertThat(response.overallPassRates().passRate()).isEqualTo(0.85); + assertThat(response.metricPassRates()).containsKey("Accuracy"); + assertThat(response.metricPassRates().get("Accuracy").passRate()).isEqualTo(0.9); + assertThat(response.metadata().totalTestResults()).isEqualTo(100); + } + + @Test + void testTestResultStatsWithMode() { + stubFor( + get(urlPathEqualTo("/test_results/stats")) + .withQueryParam("mode", equalTo("behavior")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"behavior_pass_rates\":{\"Compliance\":{\"total\":30,\"passed\":28," + + "\"failed\":2,\"pass_rate\":0.93}}}"))); + + TestResultStats response = testResultClient.stats(TestResultStatsMode.BEHAVIOR); + assertThat(response.behaviorPassRates()).containsKey("Compliance"); + assertThat(response.behaviorPassRates().get("Compliance").total()).isEqualTo(30); + } + + @Test + void testTestResultStatsWithFilters() { + stubFor( + get(urlPathEqualTo("/test_results/stats")) + .withQueryParam("mode", equalTo("all")) + .withQueryParam("test_run_ids", equalTo("tr-1")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"overall_pass_rates\":{\"total\":20,\"passed\":18," + + "\"failed\":2,\"pass_rate\":0.9}}"))); + + Map params = Map.of("test_run_ids", List.of("tr-1")); + TestResultStats response = testResultClient.stats(TestResultStatsMode.ALL, params); + assertThat(response.overallPassRates()).isNotNull(); + assertThat(response.overallPassRates().total()).isEqualTo(20); + } + + @Test + void testGetTestSetMetrics() { + stubFor( + get(urlEqualTo("/test_sets/ts-1/metrics")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[{\"id\":\"m-1\",\"name\":\"Accuracy\"}]"))); + + List> response = testSetClient.getMetrics("ts-1"); + assertThat(response).hasSize(1); + assertThat(response.get(0)).containsEntry("name", "Accuracy"); + } + + @Test + void testAddTestsToTestSet() { + stubFor( + post(urlEqualTo("/test_sets/ts-1/associate")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"total_tests\":2}"))); + + Map response = testSetClient.addTests("ts-1", List.of("t-1", "t-2")); + assertThat(response).containsEntry("success", true); + + verify( + 1, + postRequestedFor(urlEqualTo("/test_sets/ts-1/associate")) + .withRequestBody(containing("\"test_ids\""))); + } + + @Test + void testRemoveTestsFromTestSet() { + stubFor( + post(urlEqualTo("/test_sets/ts-1/disassociate")) + .withHeader("Authorization", equalTo("Bearer test-key")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"removed_associations\":1}"))); + + Map response = testSetClient.removeTests("ts-1", List.of("t-1")); + assertThat(response).containsEntry("success", true); + } } diff --git a/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java b/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java index 01dae11..39cd423 100644 --- a/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java +++ b/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java @@ -145,20 +145,22 @@ void testStatusSerialization() throws Exception { @Test void testTestRunSerialization() throws Exception { TestRun testRun = - new TestRun( - "run-1", - "config-1", - "My Run", - "user-1", - "org-1", - ai.rhesis.sdk.enums.RunStatus.COMPLETED, - Map.of("k", "v"), - "owner-1", - "assignee-1"); + TestRun.builder() + .id("run-1") + .testConfigurationId("config-1") + .name("My Run") + .userId("user-1") + .organizationId("org-1") + .status("Completed") + .attributes(Map.of("k", "v")) + .ownerId("owner-1") + .assigneeId("assignee-1") + .build(); String json = mapper.writeValueAsString(testRun); TestRun parsed = mapper.readValue(json, TestRun.class); assertThat(parsed.id()).isEqualTo("run-1"); assertThat(parsed.name()).isEqualTo("My Run"); + assertThat(parsed.status()).isEqualTo("Completed"); } @Test