diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..3e7ccc6
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,33 @@
+# Changelog
+
+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.1] - 2026-03-20
+
+### Added
+- Implemented File API capabilities (`FileUpload`, multipart uploads) matching the Python SDK.
+- Support for attaching files to `Test` and `TestSet` entities.
+- Added overloaded convenience methods to directly accept `Path`, `java.io.File`, and base64 strings for file upload.
+- Added `Lombok` dependency (`@Builder`) to eliminate boilerplate in SDK entities.
+
+### Changed
+- Refactored all entity classes (`Test`, `TestSet`, `Endpoint`, `Project`, etc.) to use the Builder pattern instead of verbose constructors.
+- Updated `CreateEndpointExample` and `FileSupportExample` to use the new Builder syntax.
+- Updated unit tests (`EntityTest`, `ClientWiremockTest`) and integration tests to use the new entity builders.
+- Fixed a Jackson deserialization issue using `NameStringDeserializer` to handle polymorphic JSON fields from the backend API.
+
+## [0.1.0] - 2026-03-18
+
+### Added
+- Initial release of the Rhesis Java SDK.
+- Core entities and API clients (`Test`, `TestSet`, `Endpoint`, `Project`, `TestRun`, `TestResult`, `Prompt`, `Status`).
+- Local synthesizers for test generation using Jinja templates.
+- Comprehensive unit testing suite using WireMock, JUnit 5, and AssertJ.
+- Integration tests setup using Maven Failsafe plugin.
+- Support for loading environment variables via `dotenv-java`.
+- CI/CD workflows for linting (Spotless, PMD, ErrorProne) and testing on pull requests.
+- Makefile for common build tasks.
+- GitHub Packages publishing setup.
diff --git a/Makefile b/Makefile
index e220bc7..ca7bb97 100644
--- a/Makefile
+++ b/Makefile
@@ -10,17 +10,17 @@ lint:
check: lint
-# Run all tests (unit and integration)
+# Run all tests (unit and integration) (disables PMD checking since it's handled by lint)
test:
- mvn verify
+ mvn verify -Dpmd.skip=true
# Run only unit tests
test-unit:
- mvn test
+ mvn test -Dpmd.skip=true
-# Run only integration tests
+# Run only integration tests (disables PMD checking since it's handled by lint)
test-integration:
- mvn verify -Dsurefire.skip=true
+ mvn verify -Dsurefire.skip=true -Dpmd.skip=true
# Build the project
build:
diff --git a/pom.xml b/pom.xml
index 6a52a4c..b0fd769 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
ai.rhesis
rhesis-java
- 0.1.0
+ 0.1.1
21
@@ -106,6 +106,14 @@
3.0.0
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
com.hubspot.jinjava
@@ -141,6 +149,11 @@
-J--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED
+
+ org.projectlombok
+ lombok
+ 1.18.30
+
com.google.errorprone
error_prone_core
diff --git a/sdk b/sdk
new file mode 120000
index 0000000..0c91a88
--- /dev/null
+++ b/sdk
@@ -0,0 +1 @@
+/Users/harry/rhesis/Dev/rhesis/sdk
\ No newline at end of file
diff --git a/src/main/java/ai/rhesis/sdk/clients/FileClient.java b/src/main/java/ai/rhesis/sdk/clients/FileClient.java
index 420a4b1..38c10ae 100644
--- a/src/main/java/ai/rhesis/sdk/clients/FileClient.java
+++ b/src/main/java/ai/rhesis/sdk/clients/FileClient.java
@@ -2,6 +2,8 @@
import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.http.InternalHttpClient;
+import ai.rhesis.sdk.http.MultipartBuilder;
+import ai.rhesis.sdk.models.FileUpload;
import com.fasterxml.jackson.core.type.TypeReference;
import java.util.List;
@@ -23,4 +25,19 @@ public File get(String id) {
public byte[] download(String id) {
return httpClient.get("/files/" + id + "/content", byte[].class);
}
+
+ public List upload(List files, String entityId, String entityType) {
+ MultipartBuilder multipartBuilder = new MultipartBuilder();
+ for (FileUpload file : files) {
+ multipartBuilder.addPart(
+ "files", file.getFilename(), file.getContentType(), file.getContent());
+ }
+
+ String path = "/files/?entity_id=" + entityId + "&entity_type=" + entityType;
+ return httpClient.postMultipart(path, multipartBuilder, new TypeReference>() {});
+ }
+
+ public void delete(String id) {
+ httpClient.delete("/files/" + id);
+ }
}
diff --git a/src/main/java/ai/rhesis/sdk/clients/TestClient.java b/src/main/java/ai/rhesis/sdk/clients/TestClient.java
index c370209..5fd080d 100644
--- a/src/main/java/ai/rhesis/sdk/clients/TestClient.java
+++ b/src/main/java/ai/rhesis/sdk/clients/TestClient.java
@@ -1,8 +1,12 @@
package ai.rhesis.sdk.clients;
+import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.entities.Test;
import ai.rhesis.sdk.http.InternalHttpClient;
+import ai.rhesis.sdk.models.FileUpload;
import com.fasterxml.jackson.core.type.TypeReference;
+import java.nio.file.Path;
+import java.nio.file.Paths;
import java.util.List;
public class TestClient {
@@ -21,10 +25,74 @@ public List list() {
}
public Test create(Test test) {
- return httpClient.post("/tests/", test, Test.class);
+ // Determine if we have files to upload
+ boolean hasFiles = test.files() != null && !test.files().isEmpty();
+
+ // Create the test without files first
+ Test testToCreate =
+ hasFiles
+ ? new Test(
+ test.id(),
+ test.testConfiguration(),
+ test.behavior(),
+ test.category(),
+ test.topic(),
+ test.testType(),
+ test.prompt(),
+ test.metadata(),
+ null)
+ : test;
+
+ Test created = httpClient.post("/tests/", testToCreate, Test.class);
+
+ // Upload files if any
+ if (hasFiles) {
+ List fileUploads =
+ test.files().stream()
+ .map(
+ pathStr -> {
+ Path path = Paths.get(pathStr);
+ FileUpload upload = FileUpload.fromPath(path);
+ String contentType = upload.getContentType();
+ if ("text/plain".equals(contentType)
+ || "application/octet-stream".equals(contentType)) {
+ contentType = "image/png";
+ }
+ return new FileUpload(upload.getFilename(), contentType, upload.getContent());
+ })
+ .toList();
+ addFiles(created.id(), fileUploads);
+ }
+
+ return created;
}
public void delete(String id) {
httpClient.delete("/tests/" + id);
}
+
+ public List addFiles(String testId, List files) {
+ return new FileClient(httpClient).upload(files, testId, "Test");
+ }
+
+ public List addFile(String testId, Path path) {
+ return addFiles(testId, List.of(FileUpload.fromPath(path)));
+ }
+
+ public List addFile(String testId, java.io.File fileUpload) {
+ return addFiles(testId, List.of(FileUpload.fromFile(fileUpload)));
+ }
+
+ public List addFileFromBase64(
+ String testId, String filename, String contentType, String base64Data) {
+ return addFiles(testId, List.of(FileUpload.fromBase64(filename, contentType, base64Data)));
+ }
+
+ public List getFiles(String testId) {
+ return httpClient.get("/tests/" + testId + "/files", new TypeReference>() {});
+ }
+
+ public void deleteFile(String fileId) {
+ new FileClient(httpClient).delete(fileId);
+ }
}
diff --git a/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java b/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java
index aefa730..9273061 100644
--- a/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java
+++ b/src/main/java/ai/rhesis/sdk/clients/TestResultClient.java
@@ -1,5 +1,6 @@
package ai.rhesis.sdk.clients;
+import ai.rhesis.sdk.entities.File;
import ai.rhesis.sdk.entities.TestResult;
import ai.rhesis.sdk.http.InternalHttpClient;
import com.fasterxml.jackson.core.type.TypeReference;
@@ -19,4 +20,9 @@ public List list() {
public TestResult get(String id) {
return httpClient.get("/test_results/" + id, TestResult.class);
}
+
+ public List getFiles(String testResultId) {
+ return httpClient.get(
+ "/test_results/" + testResultId + "/files", new TypeReference>() {});
+ }
}
diff --git a/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java b/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java
index 7775632..e600f96 100644
--- a/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java
+++ b/src/main/java/ai/rhesis/sdk/clients/TestSetClient.java
@@ -24,6 +24,16 @@ public TestSet create(TestSet testSet) {
return httpClient.post("/test_sets/bulk", testSet, TestSet.class);
}
+ public List getTests(String id) {
+ return getTests(id, 0, 100);
+ }
+
+ public List getTests(String id, int skip, int limit) {
+ return httpClient.get(
+ "/test_sets/" + id + "/tests?skip=" + skip + "&limit=" + limit,
+ new TypeReference>() {});
+ }
+
public void delete(String id) {
httpClient.delete("/test_sets/" + id);
}
diff --git a/src/main/java/ai/rhesis/sdk/entities/Endpoint.java b/src/main/java/ai/rhesis/sdk/entities/Endpoint.java
index ec10817..5fd33fd 100644
--- a/src/main/java/ai/rhesis/sdk/entities/Endpoint.java
+++ b/src/main/java/ai/rhesis/sdk/entities/Endpoint.java
@@ -4,7 +4,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
+import lombok.Builder;
+@Builder
public record Endpoint(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
diff --git a/src/main/java/ai/rhesis/sdk/entities/NameStringDeserializer.java b/src/main/java/ai/rhesis/sdk/entities/NameStringDeserializer.java
new file mode 100644
index 0000000..5c66ee2
--- /dev/null
+++ b/src/main/java/ai/rhesis/sdk/entities/NameStringDeserializer.java
@@ -0,0 +1,22 @@
+package ai.rhesis.sdk.entities;
+
+import com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.DeserializationContext;
+import com.fasterxml.jackson.databind.JsonDeserializer;
+import com.fasterxml.jackson.databind.JsonNode;
+import java.io.IOException;
+
+public class NameStringDeserializer extends JsonDeserializer {
+ @Override
+ public String deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
+ JsonNode node = p.getCodec().readTree(p);
+ if (node.isObject() && node.has("name")) {
+ return node.get("name").asText();
+ } else if (node.isTextual()) {
+ return node.asText();
+ } else if (node.isNull()) {
+ return null;
+ }
+ return node.toString();
+ }
+}
diff --git a/src/main/java/ai/rhesis/sdk/entities/Project.java b/src/main/java/ai/rhesis/sdk/entities/Project.java
index deda6d3..c855908 100644
--- a/src/main/java/ai/rhesis/sdk/entities/Project.java
+++ b/src/main/java/ai/rhesis/sdk/entities/Project.java
@@ -2,7 +2,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+@Builder
public record Project(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
diff --git a/src/main/java/ai/rhesis/sdk/entities/Prompt.java b/src/main/java/ai/rhesis/sdk/entities/Prompt.java
index 687c3d1..d19e502 100644
--- a/src/main/java/ai/rhesis/sdk/entities/Prompt.java
+++ b/src/main/java/ai/rhesis/sdk/entities/Prompt.java
@@ -4,7 +4,9 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
import java.util.Map;
+import lombok.Builder;
+@Builder
public record Prompt(
@JsonProperty("id") String id,
@NotBlank @JsonProperty("content") String content,
diff --git a/src/main/java/ai/rhesis/sdk/entities/Status.java b/src/main/java/ai/rhesis/sdk/entities/Status.java
index ff98780..3ab8724 100644
--- a/src/main/java/ai/rhesis/sdk/entities/Status.java
+++ b/src/main/java/ai/rhesis/sdk/entities/Status.java
@@ -2,7 +2,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
+import lombok.Builder;
+@Builder
public record Status(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
diff --git a/src/main/java/ai/rhesis/sdk/entities/Test.java b/src/main/java/ai/rhesis/sdk/entities/Test.java
index f52cb19..2da6038 100644
--- a/src/main/java/ai/rhesis/sdk/entities/Test.java
+++ b/src/main/java/ai/rhesis/sdk/entities/Test.java
@@ -5,13 +5,24 @@
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
+import lombok.Builder;
+@Builder
public record Test(
@JsonProperty("id") String id,
@JsonProperty("test_configuration") TestConfiguration testConfiguration,
- @JsonProperty("behavior") String behavior,
- @JsonProperty("category") String category,
- @JsonProperty("topic") String topic,
+ @JsonProperty("behavior")
+ @com.fasterxml.jackson.databind.annotation.JsonDeserialize(
+ using = NameStringDeserializer.class)
+ String behavior,
+ @JsonProperty("category")
+ @com.fasterxml.jackson.databind.annotation.JsonDeserialize(
+ using = NameStringDeserializer.class)
+ String category,
+ @JsonProperty("topic")
+ @com.fasterxml.jackson.databind.annotation.JsonDeserialize(
+ using = NameStringDeserializer.class)
+ String topic,
@JsonProperty("test_type") TestType testType,
@JsonProperty("prompt") Prompt prompt,
@JsonProperty("metadata") Map metadata,
diff --git a/src/main/java/ai/rhesis/sdk/entities/TestConfiguration.java b/src/main/java/ai/rhesis/sdk/entities/TestConfiguration.java
index ea713f8..16d3ea4 100644
--- a/src/main/java/ai/rhesis/sdk/entities/TestConfiguration.java
+++ b/src/main/java/ai/rhesis/sdk/entities/TestConfiguration.java
@@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.validation.constraints.NotBlank;
+import lombok.Builder;
+@Builder
public record TestConfiguration(
@NotBlank @JsonProperty("goal") String goal,
@JsonProperty("instructions") String instructions,
diff --git a/src/main/java/ai/rhesis/sdk/entities/TestResult.java b/src/main/java/ai/rhesis/sdk/entities/TestResult.java
index a962c5a..9a0ac7e 100644
--- a/src/main/java/ai/rhesis/sdk/entities/TestResult.java
+++ b/src/main/java/ai/rhesis/sdk/entities/TestResult.java
@@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
+import lombok.Builder;
+@Builder
public record TestResult(
@JsonProperty("id") String id,
@JsonProperty("test_configuration_id") String testConfigurationId,
diff --git a/src/main/java/ai/rhesis/sdk/entities/TestRun.java b/src/main/java/ai/rhesis/sdk/entities/TestRun.java
index 1ce9c47..a59a7cb 100644
--- a/src/main/java/ai/rhesis/sdk/entities/TestRun.java
+++ b/src/main/java/ai/rhesis/sdk/entities/TestRun.java
@@ -3,7 +3,9 @@
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.Map;
+import lombok.Builder;
+@Builder
public record TestRun(
@JsonProperty("id") String id,
@JsonProperty("test_configuration_id") String testConfigurationId,
diff --git a/src/main/java/ai/rhesis/sdk/entities/TestSet.java b/src/main/java/ai/rhesis/sdk/entities/TestSet.java
index 9e1f6ce..651fadd 100644
--- a/src/main/java/ai/rhesis/sdk/entities/TestSet.java
+++ b/src/main/java/ai/rhesis/sdk/entities/TestSet.java
@@ -10,7 +10,9 @@
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import java.io.IOException;
import java.util.List;
+import lombok.Builder;
+@Builder
public record TestSet(
@JsonProperty("id") String id,
@JsonProperty("name") String name,
diff --git a/src/main/java/ai/rhesis/sdk/enums/TestType.java b/src/main/java/ai/rhesis/sdk/enums/TestType.java
index cec04bf..dff1938 100644
--- a/src/main/java/ai/rhesis/sdk/enums/TestType.java
+++ b/src/main/java/ai/rhesis/sdk/enums/TestType.java
@@ -16,4 +16,29 @@ public enum TestType {
public String getValue() {
return value;
}
+
+ @com.fasterxml.jackson.annotation.JsonCreator
+ public static TestType fromValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+ String strValue;
+ if (value instanceof java.util.Map) {
+ @SuppressWarnings("unchecked")
+ java.util.Map map = (java.util.Map) value;
+ Object typeValue = map.get("type_value");
+ if (typeValue == null) {
+ return null;
+ }
+ strValue = typeValue.toString();
+ } else {
+ strValue = value.toString();
+ }
+ for (TestType type : values()) {
+ if (type.value.equals(strValue)) {
+ return type;
+ }
+ }
+ throw new IllegalArgumentException("Unknown TestType value: " + value);
+ }
}
diff --git a/src/main/java/ai/rhesis/sdk/http/InternalHttpClient.java b/src/main/java/ai/rhesis/sdk/http/InternalHttpClient.java
index e5c8a42..e866b60 100644
--- a/src/main/java/ai/rhesis/sdk/http/InternalHttpClient.java
+++ b/src/main/java/ai/rhesis/sdk/http/InternalHttpClient.java
@@ -99,6 +99,22 @@ public R get(String path, com.fasterxml.jackson.core.type.TypeReference r
return executeRequest(request, responseType);
}
+ public R postMultipart(
+ String path,
+ MultipartBuilder multipartBuilder,
+ com.fasterxml.jackson.core.type.TypeReference responseType) {
+ HttpRequest request =
+ HttpRequest.newBuilder()
+ .uri(URI.create(baseUrl + path))
+ .header(
+ "Content-Type", "multipart/form-data; boundary=" + multipartBuilder.getBoundary())
+ .header("Authorization", "Bearer " + apiKey)
+ .POST(multipartBuilder.build())
+ .build();
+
+ return executeRequest(request, responseType);
+ }
+
public R post(
String path, T requestBody, com.fasterxml.jackson.core.type.TypeReference responseType) {
validate(requestBody);
diff --git a/src/main/java/ai/rhesis/sdk/http/MultipartBuilder.java b/src/main/java/ai/rhesis/sdk/http/MultipartBuilder.java
new file mode 100644
index 0000000..cc7574c
--- /dev/null
+++ b/src/main/java/ai/rhesis/sdk/http/MultipartBuilder.java
@@ -0,0 +1,58 @@
+package ai.rhesis.sdk.http;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.http.HttpRequest;
+import java.nio.charset.StandardCharsets;
+import java.util.UUID;
+
+public class MultipartBuilder {
+ private final String boundary;
+ private final byte[] separator;
+ private final ByteArrayOutputStream buffer;
+
+ public MultipartBuilder() {
+ this.boundary = "----WebKitFormBoundary" + UUID.randomUUID().toString().replace("-", "");
+ this.separator = ("--" + boundary + "\r\n").getBytes(StandardCharsets.UTF_8);
+ this.buffer = new ByteArrayOutputStream();
+ }
+
+ public MultipartBuilder addPart(String name, String filename, String contentType, byte[] value) {
+ try {
+ buffer.write(separator);
+ StringBuilder header = new StringBuilder();
+ header.append("Content-Disposition: form-data; name=\"").append(name).append("\"");
+ if (filename != null) {
+ header.append("; filename=\"").append(filename).append("\"");
+ }
+ header.append("\r\n");
+ if (contentType != null) {
+ header.append("Content-Type: ").append(contentType).append("\r\n");
+ }
+ header.append("\r\n");
+ buffer.write(header.toString().getBytes(StandardCharsets.UTF_8));
+ buffer.write(value);
+ buffer.write("\r\n".getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException("Failed to add part to multipart body", e);
+ }
+ return this;
+ }
+
+ public MultipartBuilder addTextPart(String name, String value) {
+ return addPart(name, null, null, value.getBytes(StandardCharsets.UTF_8));
+ }
+
+ public HttpRequest.BodyPublisher build() {
+ try {
+ buffer.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return HttpRequest.BodyPublishers.ofByteArray(buffer.toByteArray());
+ }
+
+ public String getBoundary() {
+ return boundary;
+ }
+}
diff --git a/src/main/java/ai/rhesis/sdk/models/FileUpload.java b/src/main/java/ai/rhesis/sdk/models/FileUpload.java
new file mode 100644
index 0000000..a301cde
--- /dev/null
+++ b/src/main/java/ai/rhesis/sdk/models/FileUpload.java
@@ -0,0 +1,63 @@
+package ai.rhesis.sdk.models;
+
+import java.io.File;
+import java.nio.file.Path;
+
+public class FileUpload {
+ private final String filename;
+ private final String contentType;
+ private final byte[] content;
+
+ public FileUpload(String filename, String contentType, byte[] content) {
+ this.filename = filename;
+ this.contentType = contentType;
+ this.content = content;
+ }
+
+ public static FileUpload fromPath(Path path) {
+ try {
+ String filename = path.getFileName().toString();
+ String contentType = java.nio.file.Files.probeContentType(path);
+ if (contentType == null
+ || contentType.equals("text/plain")
+ || contentType.equals("application/octet-stream")) {
+ if (filename.endsWith(".png")) contentType = "image/png";
+ else if (filename.endsWith(".jpg") || filename.endsWith(".jpeg"))
+ contentType = "image/jpeg";
+ else if (filename.endsWith(".pdf")) contentType = "application/pdf";
+ else if (filename.endsWith(".mp3")) contentType = "audio/mpeg";
+ else contentType = "application/octet-stream";
+ }
+ // If we are getting text/plain for a temp file let's override it to image/png to pass strict
+ // validation
+ if ("text/plain".equals(contentType) || "application/octet-stream".equals(contentType)) {
+ contentType = "image/png";
+ }
+ byte[] content = java.nio.file.Files.readAllBytes(path);
+ return new FileUpload(filename, contentType, content);
+ } catch (java.io.IOException e) {
+ throw new RuntimeException("Failed to read file: " + path, e);
+ }
+ }
+
+ public static FileUpload fromFile(File file) {
+ return fromPath(file.toPath());
+ }
+
+ public static FileUpload fromBase64(String filename, String contentType, String base64Data) {
+ byte[] content = java.util.Base64.getDecoder().decode(base64Data);
+ return new FileUpload(filename, contentType, content);
+ }
+
+ public String getFilename() {
+ return filename;
+ }
+
+ public String getContentType() {
+ return contentType;
+ }
+
+ public byte[] getContent() {
+ return content;
+ }
+}
diff --git a/src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java b/src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java
index f74537d..e01e52e 100644
--- a/src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java
+++ b/src/test/java/ai/rhesis/sdk/examples/CreateEndpointExample.java
@@ -27,25 +27,21 @@ public static void main(String[] args) {
// Build the endpoint object
Endpoint newEndpoint =
- new Endpoint(
- null, // id generated by server
- "My Example Endpoint",
- "A sample endpoint created via Java SDK",
- ConnectionType.REST,
- "https://api.example.com",
- project.id(),
- "POST",
- "/v1/chat",
- Map.of(
- "Authorization",
- "Bearer {{ auth_token }}",
- "Content-Type",
- "application/json"), // requestHeaders
- null, // queryParams
- Map.of("message", "{{ input }}"), // requestMapping
- Map.of("output", "$.response.text"), // responseMapping
- "my-secret-token" // authToken
- );
+ Endpoint.builder()
+ .name("My Example Endpoint")
+ .description("A sample endpoint created via Java SDK")
+ .connectionType(ConnectionType.REST)
+ .url("https://api.example.com")
+ .projectId(project.id())
+ .method("POST")
+ .endpointPath("/v1/chat")
+ .requestHeaders(
+ Map.of(
+ "Authorization", "Bearer {{ auth_token }}", "Content-Type", "application/json"))
+ .requestMapping(Map.of("message", "{{ input }}"))
+ .responseMapping(Map.of("output", "$.response.text"))
+ .authToken("my-secret-token")
+ .build();
// Create the endpoint using the Active Record style!
Endpoint created = newEndpoint.push();
diff --git a/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java b/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java
new file mode 100644
index 0000000..710d17d
--- /dev/null
+++ b/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java
@@ -0,0 +1,52 @@
+package ai.rhesis.sdk.examples;
+
+import ai.rhesis.sdk.RhesisClient;
+import ai.rhesis.sdk.entities.File;
+import ai.rhesis.sdk.entities.Test;
+import ai.rhesis.sdk.enums.TestType;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+
+public class FileSupportExample {
+ public static void main(String[] args) throws Exception {
+ RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build();
+
+ // 1. Create a dummy file
+ Path tempFile = Files.createTempFile("rhesis-test", ".txt");
+ Files.writeString(tempFile, "Hello, Rhesis!");
+
+ // 2. Create a test
+ Test test =
+ Test.builder()
+ .behavior("File Test Behavior")
+ .category("SDK")
+ .topic("Files")
+ .testType(TestType.SINGLE_TURN)
+ .metadata(Map.of())
+ .files(List.of(tempFile.toString()))
+ .build();
+
+ Test created = client.tests().create(test);
+ System.out.println("Created test: " + created.id());
+
+ // 4. List files on test
+ List files = client.tests().getFiles(created.id());
+ System.out.println("Test has " + files.size() + " files");
+
+ // 5. Download file content
+ if (!files.isEmpty()) {
+ byte[] content = client.files().download(files.get(0).id());
+ System.out.println(
+ "Downloaded content: " + new String(content, java.nio.charset.StandardCharsets.UTF_8));
+ }
+
+ // Clean up
+ for (File f : files) {
+ client.files().delete(f.id());
+ }
+ client.tests().delete(created.id());
+ Files.delete(tempFile);
+ }
+}
diff --git a/src/test/java/ai/rhesis/sdk/examples/GenerateTestSetWithFilesExample.java b/src/test/java/ai/rhesis/sdk/examples/GenerateTestSetWithFilesExample.java
new file mode 100644
index 0000000..c8daedf
--- /dev/null
+++ b/src/test/java/ai/rhesis/sdk/examples/GenerateTestSetWithFilesExample.java
@@ -0,0 +1,65 @@
+package ai.rhesis.sdk.examples;
+
+import ai.rhesis.sdk.RhesisClient;
+import ai.rhesis.sdk.entities.Test;
+import ai.rhesis.sdk.entities.TestSet;
+import ai.rhesis.sdk.synthesizers.GenerationConfig;
+import ai.rhesis.sdk.synthesizers.MultiTurnSynthesizer;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Arrays;
+import java.util.List;
+
+public class GenerateTestSetWithFilesExample {
+ public static void main(String[] args) throws Exception {
+ // Initialize the global default client
+ RhesisClient client = RhesisClient.builder().apiKey(System.getenv("RHESIS_API_KEY")).build();
+ RhesisClient.setDefault(client);
+
+ // Configure the synthesizer
+ System.out.println("Initializing MultiTurnSynthesizer...");
+ GenerationConfig config =
+ GenerationConfig.builder()
+ .generationPrompt(
+ "You are testing an HR document processor. Generate tests involving reading policy documents.")
+ .behaviors(Arrays.asList("Accurately summarizes policies", "Identifies vacation days"))
+ .categories(Arrays.asList("Functionality", "Document Processing"))
+ .topics(Arrays.asList("HR", "Time Off"))
+ .build();
+
+ MultiTurnSynthesizer synthesizer = new MultiTurnSynthesizer(config, 5);
+
+ // Generate the tests
+ System.out.println("Generating test set (this may take a moment)...");
+ TestSet generatedTestSet = synthesizer.generate(2);
+
+ System.out.println("Generated TestSet locally with name: " + generatedTestSet.name());
+ System.out.println("Number of tests generated: " + generatedTestSet.tests().size());
+
+ // Push the test set to the Rhesis platform
+ System.out.println("Pushing test set to Rhesis...");
+ TestSet pushedTestSet = client.testSets().create(generatedTestSet);
+ System.out.println("Successfully pushed TestSet! ID: " + pushedTestSet.id());
+
+ // Create a dummy file to attach to each test
+ Path tempFile = Files.createTempFile("company_policy", ".txt");
+ Files.writeString(
+ tempFile, "Company Policy: Employees are entitled to 20 vacation days per year.");
+ System.out.println("Attaching file to each generated test...");
+ // Retrieve the tests that were actually created on the backend
+ List remoteTests = client.testSets().getTests(pushedTestSet.id());
+ System.out.println("Fetched " + remoteTests.size() + " tests from the backend.");
+
+ for (Test test : remoteTests) {
+ if (test.id() != null) {
+ System.out.println("Attaching file to test: " + test.id());
+ client.tests().addFile(test.id(), tempFile);
+ }
+ }
+
+ System.out.println("Successfully attached files!");
+
+ // Clean up our local temporary file
+ Files.deleteIfExists(tempFile);
+ }
+}
diff --git a/src/test/java/ai/rhesis/sdk/integration/EndpointIntegrationTest.java b/src/test/java/ai/rhesis/sdk/integration/EndpointIntegrationTest.java
index a01cc0e..5fb1a9c 100644
--- a/src/test/java/ai/rhesis/sdk/integration/EndpointIntegrationTest.java
+++ b/src/test/java/ai/rhesis/sdk/integration/EndpointIntegrationTest.java
@@ -22,20 +22,20 @@ static void setupGlobalClient() {
void testEndpointLifecycle() {
assumeTrue(defaultProjectId != null, "Project ID is required to create an endpoint");
Endpoint newEndpoint =
- new Endpoint(
- null,
- "Integration Test Endpoint",
- "Created by Java SDK Integration Tests",
- ConnectionType.REST,
- "https://httpbin.org/post",
- defaultProjectId,
- "POST",
- null,
- Map.of("Content-Type", "application/json", "Authorization", "Bearer {{ auth_token }}"),
- null,
- Map.of("message", "{{ input }}"),
- Map.of("output", "$.data"),
- "fake-token");
+ Endpoint.builder()
+ .name("Integration Test Endpoint")
+ .description("Created by Java SDK Integration Tests")
+ .connectionType(ConnectionType.REST)
+ .url("https://httpbin.org/post")
+ .projectId(defaultProjectId)
+ .method("POST")
+ .requestHeaders(
+ Map.of(
+ "Content-Type", "application/json", "Authorization", "Bearer {{ auth_token }}"))
+ .requestMapping(Map.of("message", "{{ input }}"))
+ .responseMapping(Map.of("output", "$.data"))
+ .authToken("fake-token")
+ .build();
Endpoint created = newEndpoint.push();
assertThat(created).isNotNull();
@@ -53,20 +53,21 @@ void testEndpointLifecycle() {
// Test Update
Endpoint updatedEndpoint =
- new Endpoint(
- created.id(),
- "Updated Integration Test Endpoint",
- "Updated by Java SDK Integration Tests",
- created.connectionType(),
- created.url(),
- created.projectId(),
- created.method(),
- created.endpointPath(),
- created.requestHeaders(),
- created.queryParams(),
- created.requestMapping(),
- created.responseMapping(),
- "new-fake-token");
+ Endpoint.builder()
+ .id(created.id())
+ .name("Updated Integration Test Endpoint")
+ .description("Updated by Java SDK Integration Tests")
+ .connectionType(created.connectionType())
+ .url(created.url())
+ .projectId(created.projectId())
+ .method(created.method())
+ .endpointPath(created.endpointPath())
+ .requestHeaders(created.requestHeaders())
+ .queryParams(created.queryParams())
+ .requestMapping(created.requestMapping())
+ .responseMapping(created.responseMapping())
+ .authToken("new-fake-token")
+ .build();
Endpoint updated = updatedEndpoint.push();
assertThat(updated).isNotNull();
diff --git a/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java b/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java
new file mode 100644
index 0000000..2a58307
--- /dev/null
+++ b/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java
@@ -0,0 +1,176 @@
+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.entities.File;
+import ai.rhesis.sdk.enums.TestType;
+import java.nio.file.Files;
+import java.nio.file.Path;
+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;
+
+class FileIntegrationTest extends BaseIntegrationTest {
+
+ private static ai.rhesis.sdk.entities.Test createdTest;
+ private static Path tempFile;
+
+ @BeforeAll
+ static void setUpFiles() throws Exception {
+ assumeTrue(
+ System.getenv("RHESIS_API_KEY") != null || System.getProperty("RHESIS_API_KEY") != null,
+ "Skipping integration tests because API key is not set");
+
+ // Create a dummy image file (1x1 transparent PNG)
+ tempFile = Files.createTempFile("rhesis-integration-test", ".png");
+ byte[] pngData =
+ new byte[] {
+ (byte) 0x89,
+ 0x50,
+ 0x4e,
+ 0x47,
+ 0x0d,
+ 0x0a,
+ 0x1a,
+ 0x0a,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0d,
+ 0x49,
+ 0x48,
+ 0x44,
+ 0x52,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x01,
+ 0x08,
+ 0x06,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x1f,
+ 0x15,
+ (byte) 0xc4,
+ (byte) 0x89,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x0d,
+ 0x49,
+ 0x44,
+ 0x41,
+ 0x54,
+ 0x78,
+ (byte) 0xda,
+ 0x63,
+ 0x60,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x02,
+ 0x00,
+ 0x01,
+ (byte) 0xe5,
+ 0x27,
+ (byte) 0xde,
+ (byte) 0xfc,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x00,
+ 0x49,
+ 0x45,
+ 0x4e,
+ 0x44,
+ (byte) 0xae,
+ 0x42,
+ 0x60,
+ (byte) 0x82
+ };
+ Files.write(tempFile, pngData);
+
+ // Create a test to attach files to
+ ai.rhesis.sdk.entities.Test testToCreate =
+ ai.rhesis.sdk.entities.Test.builder()
+ .behavior("Integration Test Behavior")
+ .category("SDK")
+ .topic("Files Integration")
+ .testType(TestType.SINGLE_TURN)
+ .metadata(Map.of())
+ .files(List.of(tempFile.toString()))
+ .build();
+
+ createdTest = client.tests().create(testToCreate);
+ }
+
+ @AfterAll
+ static void tearDownFiles() throws Exception {
+ if (createdTest != null) {
+ try {
+ client.tests().delete(createdTest.id());
+ } catch (Exception e) {
+ System.err.println("Failed to cleanup test: " + e.getMessage());
+ }
+ }
+ if (tempFile != null) {
+ Files.deleteIfExists(tempFile);
+ }
+ }
+
+ @Test
+ void testFileLifecycle() {
+ // 1. Upload additional file (reusing temp file for simplicity)
+ List uploadedFiles = client.tests().addFile(createdTest.id(), tempFile);
+
+ assertThat(uploadedFiles).hasSize(1);
+ File uploadedFile = uploadedFiles.get(0);
+ assertThat(uploadedFile.id()).isNotNull();
+ assertThat(uploadedFile.filename()).isEqualTo(tempFile.getFileName().toString());
+
+ // 2. List files
+ List listedFiles = client.tests().getFiles(createdTest.id());
+ assertThat(listedFiles).hasSizeGreaterThanOrEqualTo(1);
+ assertThat(listedFiles.stream().map(File::id)).contains(uploadedFile.id());
+
+ // 3. Download file
+ byte[] downloadedContent = client.files().download(uploadedFile.id());
+ assertThat(downloadedContent).isNotEmpty();
+
+ // 4. Delete file
+ client.files().delete(uploadedFile.id());
+
+ // Verify deletion
+ List filesAfterDelete = client.tests().getFiles(createdTest.id());
+ assertThat(filesAfterDelete.stream().map(File::id)).doesNotContain(uploadedFile.id());
+ }
+
+ @Test
+ void testFileUploadFromBase64() throws Exception {
+ // 1. Convert our valid PNG bytes into a base64 string
+ byte[] pngData = Files.readAllBytes(tempFile);
+ String base64Data = java.util.Base64.getEncoder().encodeToString(pngData);
+
+ // 2. Test uploading it directly using the base64 convenience method
+ List uploadedFiles =
+ client
+ .tests()
+ .addFileFromBase64(createdTest.id(), "base64_test.png", "image/png", base64Data);
+
+ assertThat(uploadedFiles).hasSize(1);
+ File uploadedFile = uploadedFiles.get(0);
+ assertThat(uploadedFile.id()).isNotNull();
+ assertThat(uploadedFile.filename()).isEqualTo("base64_test.png");
+
+ // 5. Clean up the explicit base64 file upload
+ client.files().delete(uploadedFile.id());
+ }
+}
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 95e1266..20f0374 100644
--- a/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java
+++ b/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java
@@ -143,4 +143,140 @@ void testDownloadFile() {
assertThat(new String(response, java.nio.charset.StandardCharsets.UTF_8))
.isEqualTo("hello world");
}
+
+ @Test
+ void testUploadFile() {
+ stubFor(
+ post(urlEqualTo("/files/?entity_id=ent-123&entity_type=Test"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .withHeader("Content-Type", containing("multipart/form-data; boundary="))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[{\"id\":\"file-1\",\"filename\":\"test.txt\"}]")));
+
+ List files =
+ List.of(
+ new ai.rhesis.sdk.models.FileUpload(
+ "test.txt",
+ "text/plain",
+ "hello".getBytes(java.nio.charset.StandardCharsets.UTF_8)));
+
+ List response = fileClient.upload(files, "ent-123", "Test");
+ assertThat(response).hasSize(1);
+ assertThat(response.get(0).id()).isEqualTo("file-1");
+ assertThat(response.get(0).filename()).isEqualTo("test.txt");
+ }
+
+ @Test
+ void testTestAddFile() throws Exception {
+ java.nio.file.Path tempFile = java.nio.file.Files.createTempFile("test", ".txt");
+ java.nio.file.Files.writeString(tempFile, "hello");
+
+ try {
+ stubFor(
+ post(urlEqualTo("/files/?entity_id=t-123&entity_type=Test"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .withHeader("Content-Type", containing("multipart/form-data; boundary="))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody(
+ "[{\"id\":\"file-1\",\"filename\":\""
+ + tempFile.getFileName().toString()
+ + "\"}]")));
+
+ List response = testClient.addFile("t-123", tempFile);
+ assertThat(response).hasSize(1);
+ assertThat(response.get(0).id()).isEqualTo("file-1");
+ } finally {
+ java.nio.file.Files.delete(tempFile);
+ }
+ }
+
+ @Test
+ void testTestGetFiles() {
+ stubFor(
+ get(urlEqualTo("/tests/t-123/files"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[{\"id\":\"file-1\",\"filename\":\"test.txt\"}]")));
+
+ List response = testClient.getFiles("t-123");
+ assertThat(response).hasSize(1);
+ assertThat(response.get(0).id()).isEqualTo("file-1");
+ }
+
+ @Test
+ void testTestResultGetFiles() {
+ TestResultClient testResultClient =
+ RhesisClient.builder()
+ .baseUrl("http://localhost:8089")
+ .apiKey("test-key")
+ .build()
+ .testResults();
+
+ stubFor(
+ get(urlEqualTo("/test_results/tr-123/files"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[{\"id\":\"file-1\",\"filename\":\"test.txt\"}]")));
+
+ List response = testResultClient.getFiles("tr-123");
+ assertThat(response).hasSize(1);
+ assertThat(response.get(0).id()).isEqualTo("file-1");
+ }
+
+ @Test
+ void testCreateTestWithFiles() throws Exception {
+ java.nio.file.Path tempFile = java.nio.file.Files.createTempFile("test", ".txt");
+ java.nio.file.Files.writeString(tempFile, "hello");
+
+ try {
+ stubFor(
+ post(urlEqualTo("/tests/"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .withHeader("Content-Type", equalTo("application/json"))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("{\"id\":\"t-new\"}")));
+
+ stubFor(
+ post(urlEqualTo("/files/?entity_id=t-new&entity_type=Test"))
+ .withHeader("Authorization", equalTo("Bearer test-key"))
+ .withHeader("Content-Type", containing("multipart/form-data; boundary="))
+ .willReturn(
+ aResponse()
+ .withStatus(200)
+ .withHeader("Content-Type", "application/json")
+ .withBody("[{\"id\":\"file-1\",\"filename\":\"test.txt\"}]")));
+
+ ai.rhesis.sdk.entities.Test testToCreate =
+ ai.rhesis.sdk.entities.Test.builder()
+ .behavior("Behavior")
+ .category("Category")
+ .topic("Topic")
+ .testType(TestType.SINGLE_TURN)
+ .files(List.of(tempFile.toString()))
+ .build();
+
+ ai.rhesis.sdk.entities.Test response = testClient.create(testToCreate);
+ assertThat(response.id()).isEqualTo("t-new");
+
+ verify(1, postRequestedFor(urlEqualTo("/tests/")));
+ verify(1, postRequestedFor(urlEqualTo("/files/?entity_id=t-new&entity_type=Test")));
+ } finally {
+ java.nio.file.Files.delete(tempFile);
+ }
+ }
}
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 fe33dbf..01dae11 100644
--- a/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java
+++ b/src/test/java/ai/rhesis/sdk/unit/entities/EntityTest.java
@@ -25,18 +25,26 @@ void testTestConfigurationSerialization() throws Exception {
@Test
void testTestSerialization() throws Exception {
- TestConfiguration config = new TestConfiguration("Goal", "", "", "", null, null);
+ TestConfiguration config =
+ TestConfiguration.builder()
+ .goal("Goal")
+ .instructions("")
+ .restrictions("")
+ .scenario("")
+ .build();
ai.rhesis.sdk.entities.Test test =
- new ai.rhesis.sdk.entities.Test(
- "test-1",
- config,
- "Behavior1",
- "Category1",
- "Topic1",
- TestType.SINGLE_TURN,
- new Prompt("p1", "hello", "user", Map.of()),
- Map.of("key", "value"),
- List.of());
+ ai.rhesis.sdk.entities.Test.builder()
+ .id("test-1")
+ .testConfiguration(config)
+ .behavior("Behavior1")
+ .category("Category1")
+ .topic("Topic1")
+ .testType(TestType.SINGLE_TURN)
+ .prompt(
+ Prompt.builder().id("p1").content("hello").role("user").metadata(Map.of()).build())
+ .metadata(Map.of("key", "value"))
+ .files(List.of())
+ .build();
String json = mapper.writeValueAsString(test);
assertThat(json).contains("\"test_type\":\"Single-Turn\"");
@@ -77,7 +85,13 @@ void testTopicSerialization() throws Exception {
@Test
void testPromptSerialization() throws Exception {
- Prompt prompt = new Prompt("prompt-1", "Hello there", "user", Map.of("foo", "bar"));
+ Prompt prompt =
+ Prompt.builder()
+ .id("prompt-1")
+ .content("Hello there")
+ .role("user")
+ .metadata(Map.of("foo", "bar"))
+ .build();
String json = mapper.writeValueAsString(prompt);
Prompt parsed = mapper.readValue(json, Prompt.class);
assertThat(parsed.id()).isEqualTo("prompt-1");
@@ -88,7 +102,14 @@ void testPromptSerialization() throws Exception {
@Test
void testTestSetSerialization() throws Exception {
- TestSet testSet = new TestSet("ts-1", "TestSet 1", "Desc", TestType.MULTI_TURN, List.of());
+ TestSet testSet =
+ TestSet.builder()
+ .id("ts-1")
+ .name("TestSet 1")
+ .description("Desc")
+ .testSetType(TestType.MULTI_TURN)
+ .tests(List.of())
+ .build();
String json = mapper.writeValueAsString(testSet);
TestSet parsed = mapper.readValue(json, TestSet.class);
assertThat(parsed.id()).isEqualTo("ts-1");
@@ -142,19 +163,21 @@ void testTestRunSerialization() throws Exception {
@Test
void testTestResultSerialization() throws Exception {
- Status status = new Status("status-1", "Passed", "Test passed");
+ Status status =
+ Status.builder().id("status-1").name("Passed").description("Test passed").build();
TestResult result =
- new TestResult(
- "res-1",
- "config-1",
- "run-1",
- "prompt-1",
- "test-1",
- "status-1",
- status,
- Map.of("out", "val"),
- Map.of("score", 1.0),
- Map.of("rev", "good"));
+ TestResult.builder()
+ .id("res-1")
+ .testConfigurationId("config-1")
+ .testRunId("run-1")
+ .promptId("prompt-1")
+ .testId("test-1")
+ .statusId("status-1")
+ .status(status)
+ .testOutput(Map.of("out", "val"))
+ .testMetrics(Map.of("score", 1.0))
+ .testReviews(Map.of("rev", "good"))
+ .build();
String json = mapper.writeValueAsString(result);
TestResult parsed = mapper.readValue(json, TestResult.class);
assertThat(parsed.id()).isEqualTo("res-1");