From 3173357baaa2a8bf2d4627c68afcef5e43657bba Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 3 Apr 2026 23:37:38 +0200 Subject: [PATCH 01/11] fix: remove deprecated X-Tenant-ID header from SDK requests Tenant identity is now derived from OAuth2 client credentials on the server side. The X-Tenant-ID header is no longer needed. Also fix flaky TelemetryReporterTest that assumed nothing runs on localhost:8080 (use localhost:1 for deterministic connection-refused). Part of #1488: Unify auth to OAuth2 Client Credentials (RFC 6749). --- src/main/java/com/getaxonflow/sdk/AxonFlow.java | 13 ------------- .../sdk/telemetry/TelemetryReporterTest.java | 4 +++- 2 files changed, 3 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 6644f55..2152bcb 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -2727,7 +2727,6 @@ public void streamExecutionStatus( .get(); addAuthHeaders(builder); - addTenantIdHeader(builder); Request httpRequest = builder.build(); @@ -2929,11 +2928,6 @@ private Request buildRequest(String method, String path, Object body) { // Add authentication headers addAuthHeaders(builder); - // Add tenant ID for policy APIs (uses clientId) - if (config.getClientId() != null && !config.getClientId().isEmpty()) { - builder.header("X-Tenant-ID", config.getClientId()); - } - // Add mode header if (config.getMode() != null) { builder.header("X-AxonFlow-Mode", config.getMode().getValue()); @@ -3139,12 +3133,6 @@ private String getEffectiveClientId() { return (clientId != null && !clientId.isEmpty()) ? clientId : "community"; } - private void addTenantIdHeader(Request.Builder builder) { - if (config.getClientId() != null && !config.getClientId().isEmpty()) { - builder.header("X-Tenant-ID", config.getClientId()); - } - } - private T parseResponse(Response response, Class type) throws IOException { handleErrorResponse(response); @@ -3739,7 +3727,6 @@ private Request buildOrchestratorRequest(String method, String path, Object body .header("Accept", "application/json"); addAuthHeaders(builder); - addTenantIdHeader(builder); RequestBody requestBody = null; if (body != null) { diff --git a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java index 0109af3..b13bc7a 100644 --- a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java +++ b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java @@ -419,9 +419,11 @@ void testPayloadDeploymentModeEnterprise(WireMockRuntimeInfo wmRuntimeInfo) thro String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) + // Use localhost:1 so detectPlatformVersion gets immediate connection-refused + // (localhost:8080 may have a running service that returns a version) TelemetryReporter.sendPing( "enterprise", - "http://localhost:8080", + "http://localhost:1", Boolean.TRUE, false, true, // hasCredentials From c986770bb1a2376fe8492879f785b18024f54aa2 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Fri, 3 Apr 2026 23:40:34 +0200 Subject: [PATCH 02/11] fix: always send Basic auth using effective clientId clientId-only configs previously sent no auth header when clientSecret was null. Now always sends Basic auth with effective clientId (defaults to "community") and empty secret for null clientSecret. Server derives tenant from the auth header. --- src/main/java/com/getaxonflow/sdk/AxonFlow.java | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 2152bcb..696f2fa 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -3103,15 +3103,11 @@ private void appendQueryParam(StringBuilder query, String name, String value) { } private void addAuthHeaders(Request.Builder builder) { - // Add auth headers only when credentials are provided - // Community/self-hosted mode works without credentials - if (!config.hasCredentials()) { - logger.debug("No credentials configured - community/self-hosted mode"); - return; - } - - // OAuth2-style: Authorization: Basic base64(clientId:clientSecret) - String credentials = config.getClientId() + ":" + config.getClientSecret(); + // Always send Basic auth with the effective clientId — server derives tenant from it. + // clientSecret defaults to empty string for community/no-secret mode. + String effectiveClientId = getEffectiveClientId(); + String secret = config.getClientSecret() != null ? config.getClientSecret() : ""; + String credentials = effectiveClientId + ":" + secret; String encoded = Base64.getEncoder().encodeToString( credentials.getBytes(StandardCharsets.UTF_8) ); From 2249270efaf123696ee6307481841595ff6fe3be Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 00:22:01 +0200 Subject: [PATCH 03/11] fix: update zero-config test for always-send-auth behavior --- .../getaxonflow/sdk/SelfHostedZeroConfigTest.java | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java b/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java index 1a4401a..44774a5 100644 --- a/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java +++ b/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java @@ -422,8 +422,8 @@ void shouldSupportFirstTimeUser(WireMockRuntimeInfo wmRuntimeInfo) { class AuthHeaderTests { @Test - @DisplayName("should not send auth headers when no credentials configured") - void shouldNotSendAuthHeadersWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { + @DisplayName("should send community Basic auth when no credentials configured") + void shouldSendCommunityAuthWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(post(urlEqualTo("/api/request")) .willReturn(aResponse() .withStatus(200) @@ -433,7 +433,7 @@ void shouldNotSendAuthHeadersWithoutCredentials(WireMockRuntimeInfo wmRuntimeInf + "\"data\": {\"answer\": \"test\"}" + "}"))); - // No credentials - community mode + // No credentials - community mode (effective clientId = "community") AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) .build()); @@ -445,12 +445,14 @@ void shouldNotSendAuthHeadersWithoutCredentials(WireMockRuntimeInfo wmRuntimeInf .build() ); - // Verify no auth headers were sent when no credentials configured + // Basic auth always sent with effective clientId ("community:") + String expectedAuth = "Basic " + java.util.Base64.getEncoder() + .encodeToString("community:".getBytes(java.nio.charset.StandardCharsets.UTF_8)); verify(postRequestedFor(urlEqualTo("/api/request")) .withoutHeader("X-License-Key") - .withoutHeader("Authorization")); + .withHeader("Authorization", equalTo(expectedAuth))); - System.out.println("✅ Auth headers not sent in community mode (no credentials)"); + System.out.println("✅ Community mode: Basic auth with default clientId"); } @Test From 97f24b54bc858054be7fa2a7431c8f732f4b5711 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 16:29:37 +0200 Subject: [PATCH 04/11] fix: add JsonProperty for materiality_classification (server canonical name) --- src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java index 70aa3ca..9daddd6 100644 --- a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java +++ b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java @@ -404,6 +404,7 @@ public static class AISystemRegistry { private int customerImpact; private int modelComplexity; private int humanReliance; + @com.fasterxml.jackson.annotation.JsonProperty("materiality_classification") private MaterialityClassification materiality; private SystemStatus status; private Instant createdAt; From a72ed799bf152cf9db937a183227113c51210d17 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 16:35:44 +0200 Subject: [PATCH 05/11] fix: rename materiality to materialityClassification to match server field name BREAKING: getMateriality() renamed to getMaterialityClassification(). --- src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java index 9daddd6..9ccd140 100644 --- a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java +++ b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java @@ -405,7 +405,7 @@ public static class AISystemRegistry { private int modelComplexity; private int humanReliance; @com.fasterxml.jackson.annotation.JsonProperty("materiality_classification") - private MaterialityClassification materiality; + private MaterialityClassification materialityClassification; private SystemStatus status; private Instant createdAt; private Instant updatedAt; @@ -433,8 +433,8 @@ public static class AISystemRegistry { public void setModelComplexity(int modelComplexity) { this.modelComplexity = modelComplexity; } public int getHumanReliance() { return humanReliance; } public void setHumanReliance(int humanReliance) { this.humanReliance = humanReliance; } - public MaterialityClassification getMateriality() { return materiality; } - public void setMateriality(MaterialityClassification materiality) { this.materiality = materiality; } + public MaterialityClassification getMaterialityClassification() { return materialityClassification; } + public void setMaterialityClassification(MaterialityClassification materialityClassification) { this.materialityClassification = materialityClassification; } public SystemStatus getStatus() { return status; } public void setStatus(SystemStatus status) { this.status = status; } public Instant getCreatedAt() { return createdAt; } From 381ba1102c7be209c268f2cdf13fc441a1bac28d Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 16:54:40 +0200 Subject: [PATCH 06/11] docs: add v6.0.0 changelog entry --- CHANGELOG.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5706f90..7dfe0f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,23 @@ All notable changes to the AxonFlow Java SDK 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). +## [Unreleased] + +### BREAKING CHANGES + +- **`X-Tenant-ID` header removed.** The SDK no longer sends `X-Tenant-ID`. The server derives tenant from OAuth2 Client Credentials (Basic auth). Requires platform v6.0.0+. +- **`getMaterialityClassification()` method renamed.** MAS FEAT `getMateriality()` renamed to `getMaterialityClassification()` to match server JSON field `materiality_classification`. + +### Added + +- **`Status` field on `PlanResponse`.** The server returns plan status (pending, executing, completed, failed, cancelled) which was previously not parsed by the SDK. + +### Fixed + +- **MCP examples missing `client_id` and `user_token`** in request body for enterprise MCP handler authentication. + +--- + ## [4.3.0] - 2026-03-24 ### Added From 1cb31c3a78ebf4a037598d71ce436dac0c12ec72 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 17:23:58 +0200 Subject: [PATCH 07/11] fix: update tests for getMaterialityClassification rename --- .../java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java | 2 +- src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java index 4e7fe17..ece00d6 100644 --- a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java @@ -80,7 +80,7 @@ void testRegisterSystem() { assertThat(result.getId()).isEqualTo("sys-123"); assertThat(result.getSystemName()).isEqualTo("Credit Scoring Model"); - assertThat(result.getMateriality()).isEqualTo(MaterialityClassification.HIGH); + assertThat(result.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/registry"))); } diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java index cb3cb03..aa61f72 100644 --- a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java @@ -348,7 +348,7 @@ void testSettersAndGetters() { assertThat(registry.getCustomerImpact()).isEqualTo(3); assertThat(registry.getModelComplexity()).isEqualTo(2); assertThat(registry.getHumanReliance()).isEqualTo(1); - assertThat(registry.getMateriality()).isEqualTo(MaterialityClassification.HIGH); + assertThat(registry.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); assertThat(registry.getStatus()).isEqualTo(SystemStatus.ACTIVE); assertThat(registry.getMetadata()).containsEntry("version", "1.0"); assertThat(registry.getCreatedAt()).isEqualTo(now); From 28c279b9035e80b01fc92ee5838b4ef97b19a3d4 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 17:31:50 +0200 Subject: [PATCH 08/11] fix: run google-java-format across entire codebase The CI format check (com.spotify.fmt:fmt-maven-plugin:check) was failing on pre-existing formatting violations. Ran the formatter on all files. --- .../java/com/getaxonflow/sdk/AxonFlow.java | 12007 ++++++++-------- .../com/getaxonflow/sdk/AxonFlowConfig.java | 739 +- .../sdk/adapters/CheckGateOptions.java | 136 +- .../sdk/adapters/CheckToolGateOptions.java | 135 +- .../sdk/adapters/LangGraphAdapter.java | 916 +- .../sdk/adapters/MCPInterceptorOptions.java | 107 +- .../sdk/adapters/MCPToolHandler.java | 20 +- .../sdk/adapters/MCPToolInterceptor.java | 169 +- .../sdk/adapters/MCPToolRequest.java | 82 +- .../sdk/adapters/StepCompletedOptions.java | 155 +- .../sdk/adapters/ToolCompletedOptions.java | 155 +- .../WorkflowApprovalRequiredError.java | 91 +- .../sdk/adapters/WorkflowBlockedError.java | 90 +- .../exceptions/AuthenticationException.java | 61 +- .../sdk/exceptions/AxonFlowException.java | 157 +- .../exceptions/ConfigurationException.java | 85 +- .../sdk/exceptions/ConnectionException.java | 115 +- .../sdk/exceptions/ConnectorException.java | 112 +- .../exceptions/PlanExecutionException.java | 112 +- .../exceptions/PolicyViolationException.java | 187 +- .../sdk/exceptions/RateLimitException.java | 132 +- .../sdk/exceptions/TimeoutException.java | 104 +- .../exceptions/VersionConflictException.java | 93 +- .../sdk/exceptions/package-info.java | 6 +- .../interceptors/AnthropicInterceptor.java | 892 +- .../sdk/interceptors/BedrockInterceptor.java | 580 +- .../interceptors/ChatCompletionRequest.java | 255 +- .../interceptors/ChatCompletionResponse.java | 294 +- .../sdk/interceptors/ChatMessage.java | 137 +- .../sdk/interceptors/GeminiInterceptor.java | 784 +- .../sdk/interceptors/OllamaInterceptor.java | 1018 +- .../sdk/interceptors/OpenAIInterceptor.java | 381 +- .../sdk/interceptors/package-info.java | 12 +- .../getaxonflow/sdk/masfeat/MASFEATTypes.java | 2373 +-- .../getaxonflow/sdk/masfeat/package-info.java | 10 +- .../com/getaxonflow/sdk/package-info.java | 15 +- .../sdk/simulation/ImpactReportInput.java | 79 +- .../sdk/simulation/ImpactReportRequest.java | 82 +- .../sdk/simulation/ImpactReportResponse.java | 175 +- .../sdk/simulation/ImpactReportResult.java | 70 +- .../sdk/simulation/PolicyConflict.java | 85 +- .../sdk/simulation/PolicyConflictRef.java | 46 +- .../simulation/PolicyConflictResponse.java | 85 +- .../simulation/SimulatePoliciesRequest.java | 143 +- .../simulation/SimulatePoliciesResponse.java | 160 +- .../sdk/simulation/SimulationDailyUsage.java | 31 +- .../sdk/telemetry/TelemetryReporter.java | 422 +- .../getaxonflow/sdk/types/AuditLogEntry.java | 466 +- .../getaxonflow/sdk/types/AuditOptions.java | 509 +- .../sdk/types/AuditQueryOptions.java | 139 +- .../getaxonflow/sdk/types/AuditResult.java | 158 +- .../sdk/types/AuditSearchRequest.java | 298 +- .../sdk/types/AuditSearchResponse.java | 180 +- .../sdk/types/AuditToolCallRequest.java | 529 +- .../sdk/types/AuditToolCallResponse.java | 129 +- .../com/getaxonflow/sdk/types/BudgetInfo.java | 188 +- .../sdk/types/CancelPlanResponse.java | 129 +- .../sdk/types/CircuitBreakerConfig.java | 145 +- .../sdk/types/CircuitBreakerConfigUpdate.java | 181 +- .../CircuitBreakerConfigUpdateResponse.java | 43 +- .../sdk/types/CircuitBreakerHistoryEntry.java | 204 +- .../types/CircuitBreakerHistoryResponse.java | 65 +- .../types/CircuitBreakerStatusResponse.java | 81 +- .../getaxonflow/sdk/types/ClientRequest.java | 450 +- .../getaxonflow/sdk/types/ClientResponse.java | 413 +- .../getaxonflow/sdk/types/CodeArtifact.java | 346 +- .../sdk/types/ConnectorHealthStatus.java | 207 +- .../getaxonflow/sdk/types/ConnectorInfo.java | 329 +- .../sdk/types/ConnectorPolicyInfo.java | 288 +- .../getaxonflow/sdk/types/ConnectorQuery.java | 217 +- .../sdk/types/ConnectorResponse.java | 271 +- .../sdk/types/DynamicPolicyInfo.java | 132 +- .../sdk/types/DynamicPolicyMatch.java | 185 +- .../getaxonflow/sdk/types/ExecutionMode.java | 109 +- .../sdk/types/ExfiltrationCheckInfo.java | 180 +- .../sdk/types/GeneratePlanOptions.java | 116 +- .../getaxonflow/sdk/types/HealthStatus.java | 296 +- .../sdk/types/MCPCheckInputRequest.java | 168 +- .../sdk/types/MCPCheckInputResponse.java | 136 +- .../sdk/types/MCPCheckOutputRequest.java | 190 +- .../sdk/types/MCPCheckOutputResponse.java | 212 +- .../sdk/types/MediaAnalysisResponse.java | 90 +- .../sdk/types/MediaAnalysisResult.java | 354 +- .../getaxonflow/sdk/types/MediaContent.java | 166 +- .../sdk/types/MediaGovernanceConfig.java | 156 +- .../sdk/types/MediaGovernanceStatus.java | 132 +- .../java/com/getaxonflow/sdk/types/Mode.java | 74 +- .../getaxonflow/sdk/types/PlanRequest.java | 396 +- .../getaxonflow/sdk/types/PlanResponse.java | 393 +- .../com/getaxonflow/sdk/types/PlanStep.java | 325 +- .../sdk/types/PlanVersionEntry.java | 210 +- .../sdk/types/PlanVersionsResponse.java | 114 +- .../sdk/types/PlatformCapability.java | 85 +- .../sdk/types/PolicyApprovalRequest.java | 306 +- .../sdk/types/PolicyApprovalResult.java | 394 +- .../com/getaxonflow/sdk/types/PolicyInfo.java | 317 +- .../sdk/types/PolicyMatchInfo.java | 165 +- .../sdk/types/PortalLoginResponse.java | 165 +- .../getaxonflow/sdk/types/RateLimitInfo.java | 142 +- .../getaxonflow/sdk/types/RequestType.java | 90 +- .../sdk/types/ResumePlanResponse.java | 308 +- .../sdk/types/RollbackPlanResponse.java | 157 +- .../sdk/types/SDKCompatibility.java | 81 +- .../com/getaxonflow/sdk/types/TokenUsage.java | 156 +- .../UpdateMediaGovernanceConfigRequest.java | 115 +- .../sdk/types/UpdatePlanRequest.java | 216 +- .../sdk/types/UpdatePlanResponse.java | 157 +- .../sdk/types/codegovernance/CodeFile.java | 188 +- .../codegovernance/CodeGovernanceMetrics.java | 224 +- .../ConfigureGitProviderRequest.java | 247 +- .../ConfigureGitProviderResponse.java | 76 +- .../types/codegovernance/CreatePRRequest.java | 389 +- .../codegovernance/CreatePRResponse.java | 157 +- .../types/codegovernance/ExportOptions.java | 70 +- .../types/codegovernance/ExportResponse.java | 82 +- .../sdk/types/codegovernance/FileAction.java | 40 +- .../types/codegovernance/GitProviderInfo.java | 61 +- .../types/codegovernance/GitProviderType.java | 40 +- .../ListGitProvidersResponse.java | 71 +- .../types/codegovernance/ListPRsOptions.java | 111 +- .../types/codegovernance/ListPRsResponse.java | 69 +- .../sdk/types/codegovernance/PRRecord.java | 317 +- .../ValidateGitProviderRequest.java | 235 +- .../ValidateGitProviderResponse.java | 83 +- .../types/codegovernance/package-info.java | 7 +- .../types/costcontrols/CostControlTypes.java | 1643 ++- .../sdk/types/costcontrols/package-info.java | 9 +- .../sdk/types/execution/ExecutionTypes.java | 1593 +- .../sdk/types/execution/package-info.java | 10 +- .../executionreplay/ExecutionReplayTypes.java | 893 +- .../types/executionreplay/package-info.java | 4 +- .../getaxonflow/sdk/types/hitl/HITLTypes.java | 839 +- .../sdk/types/hitl/package-info.java | 4 +- .../getaxonflow/sdk/types/package-info.java | 28 +- .../sdk/types/policies/PolicyTypes.java | 2894 ++-- .../sdk/types/webhook/WebhookTypes.java | 707 +- .../sdk/types/webhook/package-info.java | 4 +- .../types/workflow/PlanExecutionResponse.java | 610 +- .../workflow/PolicyEvaluationResult.java | 366 +- .../sdk/types/workflow/PolicyMatch.java | 295 +- .../sdk/types/workflow/WorkflowTypes.java | 2345 +-- .../sdk/types/workflow/package-info.java | 17 +- .../com/getaxonflow/sdk/util/CacheConfig.java | 200 +- .../sdk/util/HttpClientFactory.java | 150 +- .../getaxonflow/sdk/util/ResponseCache.java | 254 +- .../com/getaxonflow/sdk/util/RetryConfig.java | 315 +- .../getaxonflow/sdk/util/RetryExecutor.java | 248 +- .../getaxonflow/sdk/util/package-info.java | 8 +- .../com/getaxonflow/sdk/AuditReadTest.java | 1171 +- .../getaxonflow/sdk/AuditToolCallTest.java | 366 +- .../getaxonflow/sdk/AxonFlowConfigTest.java | 291 +- .../com/getaxonflow/sdk/AxonFlowTest.java | 5104 ++++--- .../getaxonflow/sdk/CircuitBreakerTest.java | 873 +- .../getaxonflow/sdk/CodeGovernanceTest.java | 624 +- .../java/com/getaxonflow/sdk/HITLTest.java | 823 +- .../getaxonflow/sdk/MediaGovernanceTest.java | 572 +- .../getaxonflow/sdk/PolicySimulationTest.java | 971 +- .../java/com/getaxonflow/sdk/PolicyTest.java | 1451 +- .../sdk/SelfHostedZeroConfigTest.java | 1001 +- .../sdk/adapters/LangGraphAdapterTest.java | 1655 ++- .../sdk/exceptions/ExceptionsTest.java | 308 +- .../integration/AxonFlowIntegrationTest.java | 646 +- .../AnthropicInterceptorTest.java | 729 +- .../interceptors/BedrockInterceptorTest.java | 954 +- .../interceptors/GeminiInterceptorTest.java | 870 +- .../interceptors/OllamaInterceptorTest.java | 1098 +- .../interceptors/OpenAIInterceptorTest.java | 714 +- .../sdk/masfeat/MASFEATClientTest.java | 920 +- .../sdk/masfeat/MASFEATTypesTest.java | 937 +- .../sdk/telemetry/TelemetryReporterTest.java | 853 +- .../sdk/types/AdditionalTypesTest.java | 1446 +- .../getaxonflow/sdk/types/AuditTypesTest.java | 229 +- .../sdk/types/ClientRequestTest.java | 173 +- .../sdk/types/ClientResponseTest.java | 209 +- .../sdk/types/CostControlTypesTest.java | 917 +- .../sdk/types/ExecutionReplayTypesTest.java | 463 +- .../sdk/types/ExecutionTypesTest.java | 903 +- .../sdk/types/MediaGovernanceTypesTest.java | 993 +- .../getaxonflow/sdk/types/MoreTypesTest.java | 3745 ++--- .../getaxonflow/sdk/types/PlanTypesTest.java | 215 +- .../sdk/types/PolicyApprovalTest.java | 254 +- .../sdk/types/RollbackPlanResponseTest.java | 160 +- .../CodeGovernanceTypesTest.java | 1846 ++- .../sdk/types/webhook/WebhookTypesTest.java | 711 +- .../types/workflow/WCPApprovalTypesTest.java | 587 +- .../workflow/WorkflowPolicyTypesTest.java | 557 +- .../getaxonflow/sdk/util/CacheConfigTest.java | 110 +- .../sdk/util/HttpClientFactoryTest.java | 102 +- .../sdk/util/ResponseCacheTest.java | 196 +- .../getaxonflow/sdk/util/RetryConfigTest.java | 196 +- .../sdk/util/RetryExecutorTest.java | 365 +- 191 files changed, 47652 insertions(+), 42607 deletions(-) diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 696f2fa..973b77d 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -15,7 +15,16 @@ */ package com.getaxonflow.sdk; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.getaxonflow.sdk.exceptions.*; +import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; +import com.getaxonflow.sdk.simulation.*; import com.getaxonflow.sdk.telemetry.TelemetryReporter; import com.getaxonflow.sdk.types.*; import com.getaxonflow.sdk.types.codegovernance.*; @@ -23,26 +32,12 @@ import com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.*; import com.getaxonflow.sdk.types.hitl.HITLTypes.*; import com.getaxonflow.sdk.types.policies.PolicyTypes.*; -import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; import com.getaxonflow.sdk.types.webhook.WebhookTypes.*; -import com.getaxonflow.sdk.simulation.*; import com.getaxonflow.sdk.util.*; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import okhttp3.*; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import java.io.BufferedReader; import java.io.Closeable; import java.io.IOException; import java.io.InputStreamReader; -import java.net.URI; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Base64; @@ -55,19 +50,24 @@ import java.util.concurrent.Executor; import java.util.concurrent.ForkJoinPool; import java.util.function.Consumer; +import okhttp3.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * Main client for interacting with the AxonFlow API. * *

The AxonFlow client provides methods for: + * *

    - *
  • Gateway Mode: Pre-check and audit for your own LLM calls
  • - *
  • Proxy Mode: Let AxonFlow handle policy and LLM routing
  • - *
  • Planning: Multi-agent planning (MAP) operations
  • - *
  • Connectors: MCP connector discovery and queries
  • + *
  • Gateway Mode: Pre-check and audit for your own LLM calls + *
  • Proxy Mode: Let AxonFlow handle policy and LLM routing + *
  • Planning: Multi-agent planning (MAP) operations + *
  • Connectors: MCP connector discovery and queries *
* *

Gateway Mode Example

+ * *
{@code
  * AxonFlow axonflow = AxonFlow.builder()
  *     .agentUrl("http://localhost:8080")
@@ -98,6 +98,7 @@
  * }
* *

Proxy Mode Example

+ * *
{@code
  * ClientResponse response = axonflow.proxyLLMCall(
  *     ClientRequest.builder()
@@ -118,6011 +119,6405 @@
  */
 public final class AxonFlow implements Closeable {
 
-    private static final Logger logger = LoggerFactory.getLogger(AxonFlow.class);
-    private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
-
-    private final AxonFlowConfig config;
-    private final OkHttpClient httpClient;
-    private final ObjectMapper objectMapper;
-    private final RetryExecutor retryExecutor;
-    private final ResponseCache cache;
-    private final Executor asyncExecutor;
-    private volatile String sessionCookie; // Session cookie for Customer Portal authentication
-    private final MASFEATNamespace masfeatNamespace;
-
-    private AxonFlow(AxonFlowConfig config) {
-        this.config = Objects.requireNonNull(config, "config cannot be null");
-        this.httpClient = HttpClientFactory.create(config);
-        this.objectMapper = createObjectMapper();
-        this.retryExecutor = new RetryExecutor(config.getRetryConfig());
-        this.cache = new ResponseCache(config.getCacheConfig());
-        this.asyncExecutor = ForkJoinPool.commonPool();
-        this.masfeatNamespace = new MASFEATNamespace();
-
-        logger.info("AxonFlow client initialized for {}", config.getEndpoint());
-
-        // Send telemetry ping (fire-and-forget).
-        boolean hasCredentials = config.getClientId() != null && !config.getClientId().isEmpty()
-                && config.getClientSecret() != null && !config.getClientSecret().isEmpty();
-        TelemetryReporter.sendPing(
-            config.getMode() != null ? config.getMode().getValue() : "production",
-            config.getEndpoint(),
-            config.getTelemetry(),
-            config.isDebug(),
-            hasCredentials
-        );
-    }
+  private static final Logger logger = LoggerFactory.getLogger(AxonFlow.class);
+  private static final MediaType JSON = MediaType.get("application/json; charset=utf-8");
+
+  private final AxonFlowConfig config;
+  private final OkHttpClient httpClient;
+  private final ObjectMapper objectMapper;
+  private final RetryExecutor retryExecutor;
+  private final ResponseCache cache;
+  private final Executor asyncExecutor;
+  private volatile String sessionCookie; // Session cookie for Customer Portal authentication
+  private final MASFEATNamespace masfeatNamespace;
+
+  private AxonFlow(AxonFlowConfig config) {
+    this.config = Objects.requireNonNull(config, "config cannot be null");
+    this.httpClient = HttpClientFactory.create(config);
+    this.objectMapper = createObjectMapper();
+    this.retryExecutor = new RetryExecutor(config.getRetryConfig());
+    this.cache = new ResponseCache(config.getCacheConfig());
+    this.asyncExecutor = ForkJoinPool.commonPool();
+    this.masfeatNamespace = new MASFEATNamespace();
+
+    logger.info("AxonFlow client initialized for {}", config.getEndpoint());
+
+    // Send telemetry ping (fire-and-forget).
+    boolean hasCredentials =
+        config.getClientId() != null
+            && !config.getClientId().isEmpty()
+            && config.getClientSecret() != null
+            && !config.getClientSecret().isEmpty();
+    TelemetryReporter.sendPing(
+        config.getMode() != null ? config.getMode().getValue() : "production",
+        config.getEndpoint(),
+        config.getTelemetry(),
+        config.isDebug(),
+        hasCredentials);
+  }
+
+  private static ObjectMapper createObjectMapper() {
+    ObjectMapper mapper = new ObjectMapper();
+    mapper.registerModule(new JavaTimeModule());
+    mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
+    mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
+    mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false);
+    return mapper;
+  }
+
+  /**
+   * Compares two semantic version strings numerically (major.minor.patch). Returns negative if a <
+   * b, zero if equal, positive if a > b.
+   */
+  private static int compareSemver(String a, String b) {
+    String[] partsA = a.split("\\.");
+    String[] partsB = b.split("\\.");
+    int length = Math.max(partsA.length, partsB.length);
+    for (int i = 0; i < length; i++) {
+      int numA = 0;
+      int numB = 0;
+      if (i < partsA.length) {
+        try {
+          String cleanA =
+              partsA[i].contains("-") ? partsA[i].substring(0, partsA[i].indexOf("-")) : partsA[i];
+          numA = Integer.parseInt(cleanA);
+        } catch (NumberFormatException ignored) {
+          // default to 0
+        }
+      }
+      if (i < partsB.length) {
+        try {
+          String cleanB =
+              partsB[i].contains("-") ? partsB[i].substring(0, partsB[i].indexOf("-")) : partsB[i];
+          numB = Integer.parseInt(cleanB);
+        } catch (NumberFormatException ignored) {
+          // default to 0
+        }
+      }
+      if (numA != numB) {
+        return Integer.compare(numA, numB);
+      }
+    }
+    return 0;
+  }
+
+  // ========================================================================
+  // Factory Methods
+  // ========================================================================
+
+  /**
+   * Creates a new builder for AxonFlow configuration.
+   *
+   * @return a new builder
+   */
+  public static AxonFlowConfig.Builder builder() {
+    return AxonFlowConfig.builder();
+  }
+
+  /**
+   * Creates an AxonFlow client with the given configuration.
+   *
+   * @param config the configuration
+   * @return a new AxonFlow client
+   */
+  public static AxonFlow create(AxonFlowConfig config) {
+    return new AxonFlow(config);
+  }
+
+  /**
+   * Creates an AxonFlow client from environment variables.
+   *
+   * @return a new AxonFlow client
+   * @see AxonFlowConfig#fromEnvironment()
+   */
+  public static AxonFlow fromEnvironment() {
+    return new AxonFlow(AxonFlowConfig.fromEnvironment());
+  }
+
+  /**
+   * Creates an AxonFlow client in sandbox mode.
+   *
+   * @param agentUrl the Agent URL
+   * @return a new AxonFlow client in sandbox mode
+   */
+  public static AxonFlow sandbox(String agentUrl) {
+    return new AxonFlow(AxonFlowConfig.builder().agentUrl(agentUrl).mode(Mode.SANDBOX).build());
+  }
+
+  // ========================================================================
+  // Health Check
+  // ========================================================================
+
+  /**
+   * Checks if the AxonFlow Agent is healthy.
+   *
+   * @return the health status
+   * @throws ConnectionException if the Agent cannot be reached
+   */
+  public HealthStatus healthCheck() {
+    HealthStatus status =
+        retryExecutor.execute(
+            () -> {
+              Request request = buildRequest("GET", "/health", null);
+              try (Response response = httpClient.newCall(request).execute()) {
+                return parseResponse(response, HealthStatus.class);
+              }
+            },
+            "healthCheck");
+
+    if (status.getSdkCompatibility() != null
+        && status.getSdkCompatibility().getMinSdkVersion() != null
+        && !"unknown".equals(AxonFlowConfig.SDK_VERSION)
+        && compareSemver(
+                AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion())
+            < 0) {
+      logger.warn(
+          "SDK version {} is below minimum supported version {}. Please upgrade.",
+          AxonFlowConfig.SDK_VERSION,
+          status.getSdkCompatibility().getMinSdkVersion());
+    }
+
+    return status;
+  }
+
+  /**
+   * Asynchronously checks if the AxonFlow Agent is healthy.
+   *
+   * @return a future containing the health status
+   */
+  public CompletableFuture healthCheckAsync() {
+    return CompletableFuture.supplyAsync(this::healthCheck, asyncExecutor);
+  }
+
+  // ========================================================================
+  // MAS FEAT Namespace Accessor
+  // ========================================================================
+
+  /**
+   * Returns the MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, Accountability,
+   * Transparency) compliance namespace.
+   *
+   * 

Enterprise Feature: Requires AxonFlow Enterprise license. + * + *

Example usage: + * + *

{@code
+   * AISystemRegistry system = client.masfeat().registerSystem(
+   *     RegisterSystemRequest.builder()
+   *         .systemId("credit-scoring-ai")
+   *         .systemName("Credit Scoring AI")
+   *         .useCase(AISystemUseCase.CREDIT_SCORING)
+   *         .ownerTeam("Risk Management")
+   *         .customerImpact(4)
+   *         .modelComplexity(3)
+   *         .humanReliance(5)
+   *         .build()
+   * );
+   * }
+ * + * @return the MAS FEAT compliance namespace + */ + public MASFEATNamespace masfeat() { + return masfeatNamespace; + } + + /** + * Checks if the AxonFlow Orchestrator is healthy. + * + * @return the health status + * @throws ConnectionException if the Orchestrator cannot be reached + */ + public HealthStatus orchestratorHealthCheck() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("GET", "/health", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + return new HealthStatus("unhealthy", null, null, null, null, null); + } + return parseResponse(response, HealthStatus.class); + } + }, + "orchestratorHealthCheck"); + } + + /** + * Asynchronously checks if the AxonFlow Orchestrator is healthy. + * + * @return a future containing the health status + */ + public CompletableFuture orchestratorHealthCheckAsync() { + return CompletableFuture.supplyAsync(this::orchestratorHealthCheck, asyncExecutor); + } + + // ======================================================================== + // Gateway Mode - Policy Pre-check and Audit + // ======================================================================== + + /** + * Pre-checks a request against policies (Gateway Mode - Step 1). + * + *

This is the first step in Gateway Mode. If approved, make your LLM call directly, then call + * {@link #auditLLMCall(AuditOptions)} to complete the flow. + * + * @param request the policy approval request + * @return the approval result with context ID for auditing + * @throws PolicyViolationException if the request is blocked by policy + * @throws AuthenticationException if authentication fails + */ + public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + // Use smart default for clientId - enables zero-config community mode + String effectiveClientId = + (request.getClientId() != null && !request.getClientId().isEmpty()) + ? request.getClientId() + : getEffectiveClientId(); - private static ObjectMapper createObjectMapper() { - ObjectMapper mapper = new ObjectMapper(); - mapper.registerModule(new JavaTimeModule()); - mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - mapper.configure(DeserializationFeature.READ_DATE_TIMESTAMPS_AS_NANOSECONDS, false); - return mapper; - } + Map ctx = request.getContext(); + PolicyApprovalRequest effectiveRequest = + PolicyApprovalRequest.builder() + .userToken(request.getUserToken()) + .query(request.getQuery()) + .dataSources(request.getDataSources()) + .context(ctx == null || ctx.isEmpty() ? null : ctx) + .clientId(effectiveClientId) + .build(); - /** - * Compares two semantic version strings numerically (major.minor.patch). - * Returns negative if a < b, zero if equal, positive if a > b. - */ - private static int compareSemver(String a, String b) { - String[] partsA = a.split("\\."); - String[] partsB = b.split("\\."); - int length = Math.max(partsA.length, partsB.length); - for (int i = 0; i < length; i++) { - int numA = 0; - int numB = 0; - if (i < partsA.length) { - try { - String cleanA = partsA[i].contains("-") ? partsA[i].substring(0, partsA[i].indexOf("-")) : partsA[i]; - numA = Integer.parseInt(cleanA); - } catch (NumberFormatException ignored) { - // default to 0 + final PolicyApprovalRequest finalRequest = effectiveRequest; + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest); + try (Response response = httpClient.newCall(httpRequest).execute()) { + PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class); + + if (!result.isApproved()) { + throw new PolicyViolationException( + result.getBlockReason(), result.getBlockingPolicyName(), result.getPolicies()); + } + + return result; + } + }, + "getPolicyApprovedContext"); + } + + /** + * Alias for {@link #getPolicyApprovedContext(PolicyApprovalRequest)}. + * + * @param request the policy approval request + * @return the approval result + */ + public PolicyApprovalResult preCheck(PolicyApprovalRequest request) { + return getPolicyApprovedContext(request); + } + + /** + * Asynchronously pre-checks a request against policies. + * + * @param request the policy approval request + * @return a future containing the approval result + */ + public CompletableFuture getPolicyApprovedContextAsync( + PolicyApprovalRequest request) { + return CompletableFuture.supplyAsync(() -> getPolicyApprovedContext(request), asyncExecutor); + } + + /** + * Audits an LLM call for compliance tracking (Gateway Mode - Step 3). + * + *

Call this after making your direct LLM call to record it for compliance and observability. + * + * @param options the audit options including context ID from pre-check + * @return the audit result + * @throws AxonFlowException if the audit fails + */ + public AuditResult auditLLMCall(AuditOptions options) { + Objects.requireNonNull(options, "options cannot be null"); + + // Use smart default for clientId - enables zero-config community mode + String effectiveClientId = + (options.getClientId() != null && !options.getClientId().isEmpty()) + ? options.getClientId() + : getEffectiveClientId(); + + // Create effective options with the smart default clientId + AuditOptions.Builder builder = + AuditOptions.builder() + .contextId(options.getContextId()) + .clientId(effectiveClientId) + .responseSummary(options.getResponseSummary()) + .provider(options.getProvider()) + .model(options.getModel()) + .tokenUsage(options.getTokenUsage()) + .metadata(options.getMetadata()) + .success(options.getSuccess()) + .errorMessage(options.getErrorMessage()); + + // Handle null latencyMs (builder takes primitive long) + if (options.getLatencyMs() != null) { + builder.latencyMs(options.getLatencyMs()); + } + + AuditOptions effectiveOptions = builder.build(); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("POST", "/api/audit/llm-call", effectiveOptions); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, AuditResult.class); + } + }, + "auditLLMCall"); + } + + /** + * Asynchronously audits an LLM call. + * + * @param options the audit options + * @return a future containing the audit result + */ + public CompletableFuture auditLLMCallAsync(AuditOptions options) { + return CompletableFuture.supplyAsync(() -> auditLLMCall(options), asyncExecutor); + } + + // ======================================================================== + // Audit Log Read Methods + // ======================================================================== + + /** + * Searches audit logs with flexible filtering options. + * + *

Example usage: + * + *

{@code
+   * AuditSearchResponse response = axonflow.searchAuditLogs(
+   *     AuditSearchRequest.builder()
+   *         .userEmail("analyst@company.com")
+   *         .startTime(Instant.now().minus(Duration.ofDays(7)))
+   *         .requestType("llm_chat")
+   *         .limit(100)
+   *         .build());
+   *
+   * for (AuditLogEntry entry : response.getEntries()) {
+   *     System.out.println(entry.getId() + ": " + entry.getQuerySummary());
+   * }
+   * }
+ * + * @param request the search request with optional filters + * @return the search response containing matching audit log entries + * @throws AxonFlowException if the search fails + */ + public AuditSearchResponse searchAuditLogs(AuditSearchRequest request) { + return retryExecutor.execute( + () -> { + AuditSearchRequest req = request != null ? request : AuditSearchRequest.builder().build(); + Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/audit/search", req); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + + // Handle both array and wrapped response formats + if (node.isArray()) { + List entries = + objectMapper.convertValue(node, new TypeReference>() {}); + return AuditSearchResponse.fromArray( + entries, + req.getLimit() != null ? req.getLimit() : 100, + req.getOffset() != null ? req.getOffset() : 0); + } + + return objectMapper.treeToValue(node, AuditSearchResponse.class); + } + }, + "searchAuditLogs"); + } + + /** + * Searches audit logs with default options (last 100 entries). + * + * @return the search response + */ + public AuditSearchResponse searchAuditLogs() { + return searchAuditLogs(null); + } + + /** + * Asynchronously searches audit logs. + * + * @param request the search request + * @return a future containing the search response + */ + public CompletableFuture searchAuditLogsAsync(AuditSearchRequest request) { + return CompletableFuture.supplyAsync(() -> searchAuditLogs(request), asyncExecutor); + } + + /** + * Gets audit logs for a specific tenant. + * + *

Example usage: + * + *

{@code
+   * AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc",
+   *     AuditQueryOptions.builder()
+   *         .limit(100)
+   *         .offset(50)
+   *         .build());
+   *
+   * System.out.println("Total entries: " + response.getTotal());
+   * System.out.println("Has more: " + response.hasMore());
+   * }
+ * + * @param tenantId the tenant ID to query + * @param options optional pagination options + * @return the search response containing audit log entries for the tenant + * @throws IllegalArgumentException if tenantId is null or empty + * @throws AxonFlowException if the query fails + */ + public AuditSearchResponse getAuditLogsByTenant(String tenantId, AuditQueryOptions options) { + if (tenantId == null || tenantId.isEmpty()) { + throw new IllegalArgumentException("tenantId is required"); + } + + return retryExecutor.execute( + () -> { + AuditQueryOptions opts = options != null ? options : AuditQueryOptions.defaults(); + String encodedTenantId = java.net.URLEncoder.encode(tenantId, "UTF-8"); + String path = + "/api/v1/audit/tenant/" + + encodedTenantId + + "?limit=" + + opts.getLimit() + + "&offset=" + + opts.getOffset(); + + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + + // Handle both array and wrapped response formats + if (node.isArray()) { + List entries = + objectMapper.convertValue(node, new TypeReference>() {}); + return AuditSearchResponse.fromArray(entries, opts.getLimit(), opts.getOffset()); + } + + return objectMapper.treeToValue(node, AuditSearchResponse.class); + } + }, + "getAuditLogsByTenant"); + } + + /** + * Gets audit logs for a specific tenant with default options. + * + * @param tenantId the tenant ID to query + * @return the search response + */ + public AuditSearchResponse getAuditLogsByTenant(String tenantId) { + return getAuditLogsByTenant(tenantId, null); + } + + /** + * Asynchronously gets audit logs for a specific tenant. + * + * @param tenantId the tenant ID to query + * @param options optional pagination options + * @return a future containing the search response + */ + public CompletableFuture getAuditLogsByTenantAsync( + String tenantId, AuditQueryOptions options) { + return CompletableFuture.supplyAsync( + () -> getAuditLogsByTenant(tenantId, options), asyncExecutor); + } + + // ======================================================================== + // Audit Tool Call + // ======================================================================== + + /** + * Audits a non-LLM tool call for compliance and observability. + * + *

Records tool invocations such as function calls, MCP operations, or API calls to the audit + * log. + * + *

Example usage: + * + *

{@code
+   * AuditToolCallResponse response = axonflow.auditToolCall(
+   *     AuditToolCallRequest.builder()
+   *         .toolName("web_search")
+   *         .toolType("function")
+   *         .input(Map.of("query", "latest news"))
+   *         .output(Map.of("results", 5))
+   *         .workflowId("wf_123")
+   *         .durationMs(450L)
+   *         .success(true)
+   *         .build());
+   * }
+ * + * @param request the audit tool call request + * @return the audit tool call response with audit ID + * @throws NullPointerException if request is null + * @throws IllegalArgumentException if tool_name is null or empty + * @throws AxonFlowException if the audit fails + */ + public AuditToolCallResponse auditToolCall(AuditToolCallRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/audit/tool-call", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, AuditToolCallResponse.class); + } + }, + "auditToolCall"); + } + + /** + * Asynchronously audits a non-LLM tool call. + * + * @param request the audit tool call request + * @return a future containing the audit tool call response + */ + public CompletableFuture auditToolCallAsync(AuditToolCallRequest request) { + return CompletableFuture.supplyAsync(() -> auditToolCall(request), asyncExecutor); + } + + // ======================================================================== + // Circuit Breaker Observability + // ======================================================================== + + /** + * Gets the current circuit breaker status, including all active (tripped) circuits. + * + *

Example usage: + * + *

{@code
+   * CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus();
+   * System.out.println("Active circuits: " + status.getCount());
+   * System.out.println("Emergency stop: " + status.isEmergencyStopActive());
+   * }
+ * + * @return the circuit breaker status + * @throws AxonFlowException if the request fails + */ + public CircuitBreakerStatusResponse getCircuitBreakerStatus() { + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/circuit-breaker/status", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), CircuitBreakerStatusResponse.class); + } + return objectMapper.treeToValue(node, CircuitBreakerStatusResponse.class); + } + }, + "getCircuitBreakerStatus"); + } + + /** + * Asynchronously gets the current circuit breaker status. + * + * @return a future containing the circuit breaker status + */ + public CompletableFuture getCircuitBreakerStatusAsync() { + return CompletableFuture.supplyAsync(this::getCircuitBreakerStatus, asyncExecutor); + } + + /** + * Gets the circuit breaker history, including past trips and resets. + * + *

Example usage: + * + *

{@code
+   * CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50);
+   * for (CircuitBreakerHistoryEntry entry : history.getHistory()) {
+   *     System.out.println(entry.getScope() + "/" + entry.getScopeId() + " - " + entry.getState());
+   * }
+   * }
+ * + * @param limit the maximum number of history entries to return + * @return the circuit breaker history + * @throws IllegalArgumentException if limit is less than 1 + * @throws AxonFlowException if the request fails + */ + public CircuitBreakerHistoryResponse getCircuitBreakerHistory(int limit) { + if (limit < 1) { + throw new IllegalArgumentException("limit must be at least 1"); + } + + return retryExecutor.execute( + () -> { + String path = "/api/v1/circuit-breaker/history?limit=" + limit; + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue( + node.get("data"), CircuitBreakerHistoryResponse.class); + } + return objectMapper.treeToValue(node, CircuitBreakerHistoryResponse.class); + } + }, + "getCircuitBreakerHistory"); + } + + /** + * Asynchronously gets the circuit breaker history. + * + * @param limit the maximum number of history entries to return + * @return a future containing the circuit breaker history + */ + public CompletableFuture getCircuitBreakerHistoryAsync(int limit) { + return CompletableFuture.supplyAsync(() -> getCircuitBreakerHistory(limit), asyncExecutor); + } + + /** + * Gets the circuit breaker configuration for a specific tenant. + * + *

Example usage: + * + *

{@code
+   * CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123");
+   * System.out.println("Error threshold: " + config.getErrorThreshold());
+   * System.out.println("Auto recovery: " + config.isEnableAutoRecovery());
+   * }
+ * + * @param tenantId the tenant ID to get configuration for + * @return the circuit breaker configuration + * @throws NullPointerException if tenantId is null + * @throws IllegalArgumentException if tenantId is empty + * @throws AxonFlowException if the request fails + */ + public CircuitBreakerConfig getCircuitBreakerConfig(String tenantId) { + Objects.requireNonNull(tenantId, "tenantId cannot be null"); + if (tenantId.isEmpty()) { + throw new IllegalArgumentException("tenantId cannot be empty"); + } + + return retryExecutor.execute( + () -> { + String path = + "/api/v1/circuit-breaker/config?tenant_id=" + + java.net.URLEncoder.encode(tenantId, java.nio.charset.StandardCharsets.UTF_8); + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfig.class); + } + return objectMapper.treeToValue(node, CircuitBreakerConfig.class); + } + }, + "getCircuitBreakerConfig"); + } + + /** + * Asynchronously gets the circuit breaker configuration for a specific tenant. + * + * @param tenantId the tenant ID to get configuration for + * @return a future containing the circuit breaker configuration + */ + public CompletableFuture getCircuitBreakerConfigAsync(String tenantId) { + return CompletableFuture.supplyAsync(() -> getCircuitBreakerConfig(tenantId), asyncExecutor); + } + + /** + * Updates the circuit breaker configuration for a tenant. + * + *

Example usage: + * + *

{@code
+   * CircuitBreakerConfig updated = axonflow.updateCircuitBreakerConfig(
+   *     CircuitBreakerConfigUpdate.builder()
+   *         .tenantId("tenant_123")
+   *         .errorThreshold(10)
+   *         .violationThreshold(5)
+   *         .enableAutoRecovery(true)
+   *         .build());
+   * }
+ * + * @param config the configuration update + * @return confirmation with tenant_id and message + * @throws NullPointerException if config is null + * @throws AxonFlowException if the request fails + */ + public CircuitBreakerConfigUpdateResponse updateCircuitBreakerConfig( + CircuitBreakerConfigUpdate config) { + Objects.requireNonNull(config, "config cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("PUT", "/api/v1/circuit-breaker/config", config); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue( + node.get("data"), CircuitBreakerConfigUpdateResponse.class); + } + return objectMapper.treeToValue(node, CircuitBreakerConfigUpdateResponse.class); + } + }, + "updateCircuitBreakerConfig"); + } + + /** + * Asynchronously updates the circuit breaker configuration for a tenant. + * + * @param config the configuration update + * @return a future containing the update confirmation + */ + public CompletableFuture updateCircuitBreakerConfigAsync( + CircuitBreakerConfigUpdate config) { + return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor); + } + + // ======================================================================== + // Policy Simulation + // ======================================================================== + + /** + * Simulates policy evaluation against a query without actually enforcing policies. + * + *

This is a dry-run mode that shows which policies would match and what actions would be + * taken, without blocking the request. + * + *

Example usage: + * + *

{@code
+   * SimulatePoliciesResponse result = axonflow.simulatePolicies(
+   *     SimulatePoliciesRequest.builder()
+   *         .query("Transfer $50,000 to external account")
+   *         .requestType("execute")
+   *         .build());
+   * System.out.println("Allowed: " + result.isAllowed());
+   * System.out.println("Applied policies: " + result.getAppliedPolicies());
+   * System.out.println("Risk score: " + result.getRiskScore());
+   * }
+ * + *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. + * + * @param request the simulation request + * @return the simulation result + * @throws NullPointerException if request is null + * @throws AxonFlowException if the request fails + */ + public SimulatePoliciesResponse simulatePolicies(SimulatePoliciesRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/policies/simulate", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), SimulatePoliciesResponse.class); + } + return objectMapper.treeToValue(node, SimulatePoliciesResponse.class); + } + }, + "simulatePolicies"); + } + + /** + * Asynchronously simulates policy evaluation against a query. + * + * @param request the simulation request + * @return a future containing the simulation result + */ + public CompletableFuture simulatePoliciesAsync( + SimulatePoliciesRequest request) { + return CompletableFuture.supplyAsync(() -> simulatePolicies(request), asyncExecutor); + } + + /** + * Generates a policy impact report by testing a set of inputs against a specific policy. + * + *

This helps you understand how a policy would affect real traffic before deploying it. + * + *

Example usage: + * + *

{@code
+   * ImpactReportResponse report = axonflow.getPolicyImpactReport(
+   *     ImpactReportRequest.builder()
+   *         .policyId("policy_block_pii")
+   *         .inputs(List.of(
+   *             ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
+   *             ImpactReportInput.builder().query("What is the weather?").build()))
+   *         .build());
+   * System.out.println("Match rate: " + report.getMatchRate());
+   * System.out.println("Block rate: " + report.getBlockRate());
+   * }
+ * + *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. + * + * @param request the impact report request + * @return the impact report + * @throws NullPointerException if request is null + * @throws AxonFlowException if the request fails + */ + public ImpactReportResponse getPolicyImpactReport(ImpactReportRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/policies/impact-report", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), ImpactReportResponse.class); + } + return objectMapper.treeToValue(node, ImpactReportResponse.class); + } + }, + "getPolicyImpactReport"); + } + + /** + * Asynchronously generates a policy impact report. + * + * @param request the impact report request + * @return a future containing the impact report + */ + public CompletableFuture getPolicyImpactReportAsync( + ImpactReportRequest request) { + return CompletableFuture.supplyAsync(() -> getPolicyImpactReport(request), asyncExecutor); + } + + /** + * Scans all active policies for conflicts. + * + *

Example usage: + * + *

{@code
+   * PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts();
+   * System.out.println("Conflicts found: " + conflicts.getConflictCount());
+   * for (PolicyConflict conflict : conflicts.getConflicts()) {
+   *     System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
+   * }
+   * }
+ * + *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. + * + * @return the conflict detection result + * @throws AxonFlowException if the request fails + */ + public PolicyConflictResponse detectPolicyConflicts() { + return detectPolicyConflicts(null); + } + + /** + * Detects conflicts between a specific policy and other active policies, or scans all policies if + * policyId is null. + * + *

Example usage: + * + *

{@code
+   * PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts("policy_block_pii");
+   * System.out.println("Conflicts found: " + conflicts.getConflictCount());
+   * for (PolicyConflict conflict : conflicts.getConflicts()) {
+   *     System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
+   * }
+   * }
+ * + *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. + * + * @param policyId the policy ID to check for conflicts, or null to scan all policies + * @return the conflict detection result + * @throws IllegalArgumentException if policyId is non-null and empty + * @throws AxonFlowException if the request fails + */ + public PolicyConflictResponse detectPolicyConflicts(String policyId) { + if (policyId != null && policyId.isEmpty()) { + throw new IllegalArgumentException("policyId cannot be empty"); + } + + return retryExecutor.execute( + () -> { + Object body; + if (policyId != null) { + body = java.util.Map.of("policy_id", policyId); + } else { + body = java.util.Map.of(); + } + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/policies/conflicts", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), PolicyConflictResponse.class); + } + return objectMapper.treeToValue(node, PolicyConflictResponse.class); + } + }, + "detectPolicyConflicts"); + } + + /** + * Asynchronously scans all active policies for conflicts. + * + * @return a future containing the conflict detection result + */ + public CompletableFuture detectPolicyConflictsAsync() { + return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(), asyncExecutor); + } + + /** + * Asynchronously detects conflicts between a specific policy and other active policies. + * + * @param policyId the policy ID to check for conflicts, or null to scan all policies + * @return a future containing the conflict detection result + */ + public CompletableFuture detectPolicyConflictsAsync(String policyId) { + return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(policyId), asyncExecutor); + } + + // ======================================================================== + // Proxy Mode - Query Execution + // ======================================================================== + + /** + * Sends a query through AxonFlow with full policy enforcement (Proxy Mode). + * + *

This is Proxy Mode - AxonFlow acts as an intermediary, making the LLM call on your behalf. + * + *

Use this when you want AxonFlow to: + * + *

    + *
  • Evaluate policies before the LLM call + *
  • Make the LLM call to the configured provider + *
  • Filter/redact sensitive data from responses + *
  • Automatically track costs and audit the interaction + *
+ * + *

For Gateway Mode (lower latency, you make the LLM call), use: + * + *

    + *
  • {@link #getPolicyApprovedContext} before your LLM call + *
  • {@link #auditLLMCall} after your LLM call + *
+ * + * @param request the client request + * @return the response from AxonFlow + * @throws PolicyViolationException if the request is blocked by policy + * @throws AuthenticationException if authentication fails + */ + public ClientResponse proxyLLMCall(ClientRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + // Auto-populate clientId from config if not set in request (matches Go/Python/TypeScript SDK + // behavior) + ClientRequest effectiveRequest = request; + if ((request.getClientId() == null || request.getClientId().isEmpty()) + && config.getClientId() != null + && !config.getClientId().isEmpty()) { + effectiveRequest = + ClientRequest.builder() + .query(request.getQuery()) + .userToken(request.getUserToken()) + .clientId(config.getClientId()) + .requestType( + request.getRequestType() != null + ? RequestType.fromValue(request.getRequestType()) + : RequestType.CHAT) + .context(request.getContext()) + .llmProvider(request.getLlmProvider()) + .model(request.getModel()) + .media(request.getMedia()) + .build(); + } + + final ClientRequest finalRequest = effectiveRequest; + + // Media requests must not be cached — binary content makes cache keys unreliable + boolean hasMedia = finalRequest.getMedia() != null && !finalRequest.getMedia().isEmpty(); + + // Check cache first (skip for media requests) + String cacheKey = + ResponseCache.generateKey( + finalRequest.getRequestType(), finalRequest.getQuery(), finalRequest.getUserToken()); + + if (!hasMedia) { + java.util.Optional cached = cache.get(cacheKey, ClientResponse.class); + if (cached.isPresent()) { + return cached.get(); + } + } + + ClientResponse response = + retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("POST", "/api/request", finalRequest); + try (Response httpResponse = httpClient.newCall(httpRequest).execute()) { + ClientResponse result = parseResponse(httpResponse, ClientResponse.class); + + if (result.isBlocked()) { + throw new PolicyViolationException( + result.getBlockReason(), + result.getBlockingPolicyName(), + result.getPolicyInfo() != null + ? result.getPolicyInfo().getPoliciesEvaluated() + : null); } + + return result; + } + }, + "proxyLLMCall"); + + // Cache successful responses (skip for media requests) + if (!hasMedia && response.isSuccess() && !response.isBlocked()) { + cache.put(cacheKey, response); + } + + return response; + } + + /** + * Asynchronously sends a query through AxonFlow with full policy enforcement (Proxy Mode). + * + * @param request the client request + * @return a future containing the response + * @see #proxyLLMCall(ClientRequest) + */ + public CompletableFuture proxyLLMCallAsync(ClientRequest request) { + return CompletableFuture.supplyAsync(() -> proxyLLMCall(request), asyncExecutor); + } + + // ======================================================================== + // Multi-Agent Planning (MAP) + // ======================================================================== + + /** + * Generates a multi-agent plan for a complex task. + * + *

This method uses the Agent API with request_type "multi-agent-plan" to generate and execute + * plans through the governance layer. + * + * @param request the plan request + * @return the generated plan + * @throws PlanExecutionException if plan generation fails + */ + public PlanResponse generatePlan(PlanRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + // Build agent request format - use HashMap to allow null-safe values + String userToken = request.getUserToken(); + if (userToken == null) { + userToken = config.getClientId() != null ? config.getClientId() : "default"; + } + String clientId = config.getClientId() != null ? config.getClientId() : "default"; + String domain = request.getDomain() != null ? request.getDomain() : "generic"; + + Map agentRequest = new java.util.HashMap<>(); + agentRequest.put("query", request.getObjective()); + agentRequest.put("user_token", userToken); + agentRequest.put("client_id", clientId); + agentRequest.put("request_type", "multi-agent-plan"); + agentRequest.put("context", Map.of("domain", domain)); + + Request httpRequest = buildRequest("POST", "/api/request", agentRequest); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parsePlanResponse(response, request.getDomain()); + } + }, + "generatePlan"); + } + + /** + * Parses the Agent API response format into PlanResponse. The Agent API returns: {success, + * plan_id, data: {steps, domain, ...}, metadata, result} + */ + @SuppressWarnings("unchecked") + private PlanResponse parsePlanResponse(Response response, String requestDomain) + throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + Map agentResponse = + objectMapper.readValue(json, new TypeReference>() {}); + + // Check for errors + Boolean success = (Boolean) agentResponse.get("success"); + if (success == null || !success) { + String error = (String) agentResponse.get("error"); + throw new PlanExecutionException(error != null ? error : "Plan generation failed"); + } + + // Extract fields from Agent API response format + String planId = (String) agentResponse.get("plan_id"); + Map data = (Map) agentResponse.get("data"); + Map metadata = (Map) agentResponse.get("metadata"); + String result = (String) agentResponse.get("result"); + + // Extract nested fields from data + List steps = Collections.emptyList(); + String domain = requestDomain != null ? requestDomain : "generic"; + Integer complexity = null; + Boolean parallel = null; + String estimatedDuration = null; + + if (data != null) { + // Parse steps if present + List> rawSteps = (List>) data.get("steps"); + if (rawSteps != null) { + steps = + rawSteps.stream() + .map(stepMap -> objectMapper.convertValue(stepMap, PlanStep.class)) + .collect(java.util.stream.Collectors.toList()); + } + domain = data.get("domain") != null ? (String) data.get("domain") : domain; + complexity = + data.get("complexity") != null ? ((Number) data.get("complexity")).intValue() : null; + parallel = (Boolean) data.get("parallel"); + estimatedDuration = (String) data.get("estimated_duration"); + } + + return new PlanResponse( + planId, steps, domain, complexity, parallel, estimatedDuration, metadata, null, result); + } + + /** + * Asynchronously generates a multi-agent plan. + * + * @param request the plan request + * @return a future containing the generated plan + */ + public CompletableFuture generatePlanAsync(PlanRequest request) { + return CompletableFuture.supplyAsync(() -> generatePlan(request), asyncExecutor); + } + + /** + * Executes a previously generated plan. + * + * @param planId the ID of the plan to execute + * @return the execution result + * @throws PlanExecutionException if execution fails + */ + public PlanResponse executePlan(String planId) { + return executePlan(planId, null); + } + + /** + * Executes a previously generated plan with an explicit user token. + * + * @param planId the ID of the plan to execute + * @param userToken the user token (JWT) for authentication; if null, defaults to clientId + * @return the execution result + * @throws PlanExecutionException if execution fails + */ + public PlanResponse executePlan(String planId, String userToken) { + Objects.requireNonNull(planId, "planId cannot be null"); + + // executePlan is a mutation — do NOT retry (retrying causes 409 "Plan has already been + // executed") + try { + // Build agent request format - like generatePlan but with request_type "execute-plan" + String token = + userToken != null + ? userToken + : (config.getClientId() != null ? config.getClientId() : "default"); + String clientId = config.getClientId() != null ? config.getClientId() : "default"; + + Map agentRequest = new java.util.HashMap<>(); + agentRequest.put("query", ""); + agentRequest.put("user_token", token); + agentRequest.put("client_id", clientId); + agentRequest.put("request_type", "execute-plan"); + agentRequest.put("context", Map.of("plan_id", planId)); + + Request httpRequest = buildRequest("POST", "/api/request", agentRequest); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseExecutePlanResponse(response, planId); + } + } catch (AxonFlowException e) { + throw e; + } catch (Exception e) { + throw new PlanExecutionException("executePlan failed: " + e.getMessage(), planId, null, e); + } + } + + /** Parses the execute plan response. */ + @SuppressWarnings("unchecked") + private PlanResponse parseExecutePlanResponse(Response response, String planId) + throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + Map agentResponse = + objectMapper.readValue(json, new TypeReference>() {}); + + // Check for errors (outer response) + Boolean success = (Boolean) agentResponse.get("success"); + + // Detect nested data.success=false (agent wraps orchestrator errors) + Object dataObj = agentResponse.get("data"); + if (dataObj instanceof Map) { + @SuppressWarnings("unchecked") + Map dataMap = (Map) dataObj; + Boolean dataSuccess = (Boolean) dataMap.get("success"); + if (dataSuccess != null && !dataSuccess) { + success = false; + String dataError = (String) dataMap.get("error"); + if (dataError != null) { + throw new PlanExecutionException(dataError); + } + } + } + + if (success == null || !success) { + String error = (String) agentResponse.get("error"); + throw new PlanExecutionException(error != null ? error : "Plan execution failed"); + } + + // Extract result - this is the completed plan output + String result = (String) agentResponse.get("result"); + + // Read status from response data (e.g., "awaiting_approval" for confirm mode) + // Precedence: data.status > metadata.status > top-level status > "completed" + String status = "completed"; + Object dataObj2 = agentResponse.get("data"); + if (dataObj2 instanceof Map) { + @SuppressWarnings("unchecked") + Map dm = (Map) dataObj2; + Object dataStatus = dm.get("status"); + if (dataStatus instanceof String && !((String) dataStatus).isEmpty()) { + status = (String) dataStatus; + } + } + if ("completed".equals(status)) { + Object metaObj = agentResponse.get("metadata"); + if (metaObj instanceof Map) { + @SuppressWarnings("unchecked") + Map metaMap = (Map) metaObj; + Object metaStatus = metaMap.get("status"); + if (metaStatus instanceof String && !((String) metaStatus).isEmpty()) { + status = (String) metaStatus; + } + } + } + if ("completed".equals(status)) { + Object topStatus = agentResponse.get("status"); + if (topStatus instanceof String && !((String) topStatus).isEmpty()) { + status = (String) topStatus; + } + } + + // Build response with execution status + return new PlanResponse( + planId, Collections.emptyList(), null, null, null, null, null, status, result); + } + + /** + * Gets the status of a plan. + * + * @param planId the plan ID + * @return the plan status + */ + public PlanResponse getPlanStatus(String planId) { + Objects.requireNonNull(planId, "planId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/plan/" + planId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, PlanResponse.class); + } + }, + "getPlanStatus"); + } + + /** + * Generates a multi-agent plan with additional options. + * + *

This overload allows specifying execution mode and other generation options beyond what is + * in the base {@link PlanRequest}. + * + * @param request the plan request + * @param options additional generation options + * @return the generated plan + * @throws PlanExecutionException if plan generation fails + */ + public PlanResponse generatePlan(PlanRequest request, GeneratePlanOptions options) { + Objects.requireNonNull(request, "request cannot be null"); + Objects.requireNonNull(options, "options cannot be null"); + + return retryExecutor.execute( + () -> { + // Build agent request format - use HashMap to allow null-safe values + String userToken = request.getUserToken(); + if (userToken == null) { + userToken = config.getClientId() != null ? config.getClientId() : "default"; + } + String clientId = config.getClientId() != null ? config.getClientId() : "default"; + String domain = request.getDomain() != null ? request.getDomain() : "generic"; + + Map context = new java.util.HashMap<>(); + context.put("domain", domain); + if (options.getExecutionMode() != null) { + context.put("execution_mode", options.getExecutionMode().getValue()); + } + + Map agentRequest = new java.util.HashMap<>(); + agentRequest.put("query", request.getObjective()); + agentRequest.put("user_token", userToken); + agentRequest.put("client_id", clientId); + agentRequest.put("request_type", "multi-agent-plan"); + agentRequest.put("context", context); + + Request httpRequest = buildRequest("POST", "/api/request", agentRequest); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parsePlanResponse(response, request.getDomain()); + } + }, + "generatePlan"); + } + + /** + * Cancels a running or pending plan. + * + * @param planId the ID of the plan to cancel + * @param reason an optional reason for the cancellation + * @return the cancellation result + */ + public CancelPlanResponse cancelPlan(String planId, String reason) { + Objects.requireNonNull(planId, "planId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new java.util.HashMap<>(); + if (reason != null) { + body.put("reason", reason); + } + + Request httpRequest = + buildRequest( + "POST", "/api/v1/plan/" + planId + "/cancel", body.isEmpty() ? null : body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, CancelPlanResponse.class); + } + }, + "cancelPlan"); + } + + /** + * Cancels a running or pending plan without specifying a reason. + * + * @param planId the ID of the plan to cancel + * @return the cancellation result + */ + public CancelPlanResponse cancelPlan(String planId) { + return cancelPlan(planId, null); + } + + /** + * Updates a plan with optimistic concurrency control. + * + *

The request must include the expected version number. If the version does not match the + * current server version, a {@link VersionConflictException} is thrown. + * + * @param planId the ID of the plan to update + * @param request the update request with version and changes + * @return the update result + * @throws VersionConflictException if the plan version has changed + */ + public UpdatePlanResponse updatePlan(String planId, UpdatePlanRequest request) { + Objects.requireNonNull(planId, "planId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + try { + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("PUT", "/api/v1/plan/" + planId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, UpdatePlanResponse.class); + } + }, + "updatePlan"); + } catch (AxonFlowException e) { + if (e.getStatusCode() == 409) { + throw new VersionConflictException(e.getMessage(), planId, request.getVersion(), null); + } + throw e; + } + } + + /** + * Gets the version history of a plan. + * + * @param planId the plan ID + * @return the version history + */ + public PlanVersionsResponse getPlanVersions(String planId) { + Objects.requireNonNull(planId, "planId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/plan/" + planId + "/versions", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, PlanVersionsResponse.class); + } + }, + "getPlanVersions"); + } + + /** + * Resumes a paused plan, optionally approving or rejecting it. + * + * @param planId the ID of the plan to resume + * @param approved whether to approve the plan to continue (true) or reject it (false) + * @return the resume result + */ + public ResumePlanResponse resumePlan(String planId, Boolean approved) { + Objects.requireNonNull(planId, "planId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new java.util.HashMap<>(); + body.put("approved", approved != null ? approved : true); + + Request httpRequest = buildRequest("POST", "/api/v1/plan/" + planId + "/resume", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ResumePlanResponse.class); + } + }, + "resumePlan"); + } + + /** + * Resumes a paused plan with approval (default). + * + *

This is equivalent to calling {@code resumePlan(planId, true)}. + * + * @param planId the ID of the plan to resume + * @return the resume result + */ + public ResumePlanResponse resumePlan(String planId) { + return resumePlan(planId, true); + } + + /** + * Rolls back a plan to a previous version. + * + * @param planId the ID of the plan to roll back + * @param targetVersion the version number to roll back to + * @return the rollback result + * @throws AxonFlowException if the rollback fails + */ + public RollbackPlanResponse rollbackPlan(String planId, int targetVersion) { + Objects.requireNonNull(planId, "planId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildRequest("POST", "/api/v1/plan/" + planId + "/rollback/" + targetVersion, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, RollbackPlanResponse.class); + } + }, + "rollbackPlan"); + } + + /** + * Asynchronously rolls back a plan to a previous version. + * + * @param planId the ID of the plan to roll back + * @param targetVersion the version number to roll back to + * @return a future containing the rollback result + */ + public CompletableFuture rollbackPlanAsync( + String planId, int targetVersion) { + return CompletableFuture.supplyAsync(() -> rollbackPlan(planId, targetVersion), asyncExecutor); + } + + // ======================================================================== + // MCP Connectors + // ======================================================================== + + /** + * Lists available MCP connectors. + * + * @return list of available connectors + */ + public List listConnectors() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/connectors", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Response is wrapped: {"connectors": [...], "total": N} + JsonNode node = parseResponseNode(response); + if (node.has("connectors")) { + return objectMapper.convertValue( + node.get("connectors"), new TypeReference>() {}); + } + return objectMapper.convertValue(node, new TypeReference>() {}); + } + }, + "listConnectors"); + } + + /** + * Asynchronously lists available MCP connectors. + * + * @return a future containing the list of connectors + */ + public CompletableFuture> listConnectorsAsync() { + return CompletableFuture.supplyAsync(this::listConnectors, asyncExecutor); + } + + /** + * Installs an MCP connector. + * + * @param connectorId the connector ID to install + * @param config the connector configuration + * @return the installed connector info + */ + public ConnectorInfo installConnector(String connectorId, Map config) { + Objects.requireNonNull(connectorId, "connectorId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = Map.of("config", config != null ? config : Map.of()); + String path = "/api/v1/connectors/" + connectorId + "/install"; + Request httpRequest = buildOrchestratorRequest("POST", path, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ConnectorInfo.class); + } + }, + "installConnector"); + } + + /** + * Uninstalls an MCP connector. + * + * @param connectorName the name of the connector to uninstall + */ + public void uninstallConnector(String connectorName) { + Objects.requireNonNull(connectorName, "connectorName cannot be null"); + + retryExecutor.execute( + () -> { + String path = "/api/v1/connectors/" + connectorName; + Request httpRequest = buildOrchestratorRequest("DELETE", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); } - if (i < partsB.length) { - try { - String cleanB = partsB[i].contains("-") ? partsB[i].substring(0, partsB[i].indexOf("-")) : partsB[i]; - numB = Integer.parseInt(cleanB); - } catch (NumberFormatException ignored) { - // default to 0 - } + return null; + } + }, + "uninstallConnector"); + } + + /** + * Gets details for a specific connector by ID. + * + * @param connectorId the connector ID + * @return the connector info + */ + public ConnectorInfo getConnector(String connectorId) { + Objects.requireNonNull(connectorId, "connectorId cannot be null"); + + return retryExecutor.execute( + () -> { + String path = "/api/v1/connectors/" + connectorId; + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ConnectorInfo.class); + } + }, + "getConnector"); + } + + /** + * Asynchronously gets details for a specific connector by ID. + * + * @param connectorId the connector ID + * @return a future containing the connector info + */ + public CompletableFuture getConnectorAsync(String connectorId) { + return CompletableFuture.supplyAsync(() -> getConnector(connectorId), asyncExecutor); + } + + /** + * Gets the health status of an installed connector. + * + * @param connectorId the connector ID + * @return the health status + */ + public ConnectorHealthStatus getConnectorHealth(String connectorId) { + Objects.requireNonNull(connectorId, "connectorId cannot be null"); + + return retryExecutor.execute( + () -> { + String path = "/api/v1/connectors/" + connectorId + "/health"; + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ConnectorHealthStatus.class); + } + }, + "getConnectorHealth"); + } + + /** + * Asynchronously gets the health status of an installed connector. + * + * @param connectorId the connector ID + * @return a future containing the health status + */ + public CompletableFuture getConnectorHealthAsync(String connectorId) { + return CompletableFuture.supplyAsync(() -> getConnectorHealth(connectorId), asyncExecutor); + } + + /** + * Queries an MCP connector. + * + *

This method sends the query to the AxonFlow Agent using the standard request format with + * request_type: "mcp-query", which is routed to the configured MCP connector. + * + * @param query the connector query + * @return the query response + * @throws ConnectorException if the query fails + */ + public ConnectorResponse queryConnector(ConnectorQuery query) { + Objects.requireNonNull(query, "query cannot be null"); + + return retryExecutor.execute( + () -> { + // Build a ClientRequest with MCP_QUERY request type + // This follows the same pattern as Go and TypeScript SDKs + Map context = new HashMap<>(); + context.put("connector", query.getConnectorId()); + if (query.getParameters() != null && !query.getParameters().isEmpty()) { + context.put("params", query.getParameters()); + } + + String clientId = config.getClientId(); + + ClientRequest clientRequest = + ClientRequest.builder() + .query(query.getOperation()) + .userToken(query.getUserToken() != null ? query.getUserToken() : clientId) + .clientId(clientId) + .requestType(RequestType.MCP_QUERY) + .context(context) + .build(); + + Request httpRequest = buildRequest("POST", "/api/request", clientRequest); + try (Response response = httpClient.newCall(httpRequest).execute()) { + ClientResponse clientResponse = parseResponse(response, ClientResponse.class); + + // Convert ClientResponse to ConnectorResponse + ConnectorResponse result = + new ConnectorResponse( + clientResponse.isSuccess(), + clientResponse.getData(), + clientResponse.getError(), + query.getConnectorId(), + query.getOperation(), + null, // processingTime not available from ClientResponse + false, // redacted - not available from this endpoint + null, // redactedFields - not available from this endpoint + null // policyInfo - not available from this endpoint + ); + + if (!result.isSuccess()) { + throw new ConnectorException( + result.getError(), query.getConnectorId(), query.getOperation()); + } + + return result; + } + }, + "queryConnector"); + } + + /** + * Asynchronously queries an MCP connector. + * + * @param query the connector query + * @return a future containing the response + */ + public CompletableFuture queryConnectorAsync(ConnectorQuery query) { + return CompletableFuture.supplyAsync(() -> queryConnector(query), asyncExecutor); + } + + /** + * Executes a query directly against the MCP connector endpoint. + * + *

This method calls the agent's /mcp/resources/query endpoint which provides: + * + *

    + *
  • Request-phase policy evaluation (SQLi blocking, PII blocking) + *
  • Response-phase policy evaluation (PII redaction) + *
  • PolicyInfo metadata in responses + *
+ * + *

Example usage: + * + *

+   * ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM customers LIMIT 10");
+   * if (response.isRedacted()) {
+   *     System.out.println("Fields redacted: " + response.getRedactedFields());
+   * }
+   * System.out.println("Policies evaluated: " + response.getPolicyInfo().getPoliciesEvaluated());
+   * 
+ * + * @param connector name of the MCP connector (e.g., "postgres") + * @param statement SQL statement or query to execute + * @return ConnectorResponse with data, redaction info, and policy_info + * @throws ConnectorException if the request is blocked by policy or fails + */ + public ConnectorResponse mcpQuery(String connector, String statement) { + return mcpQuery(connector, statement, null); + } + + /** + * Executes a query directly against the MCP connector endpoint with options. + * + * @param connector name of the MCP connector (e.g., "postgres") + * @param statement SQL statement or query to execute + * @param options optional additional options for the query + * @return ConnectorResponse with data, redaction info, and policy_info + * @throws ConnectorException if the request is blocked by policy or fails + */ + public ConnectorResponse mcpQuery( + String connector, String statement, Map options) { + Objects.requireNonNull(connector, "connector cannot be null"); + Objects.requireNonNull(statement, "statement cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("connector", connector); + body.put("statement", statement); + if (options != null && !options.isEmpty()) { + body.put("options", options); + } + + Request httpRequest = buildRequest("POST", "/mcp/resources/query", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Parse the response body + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new ConnectorException("Empty response from MCP query", connector, "mcpQuery"); + } + String responseJson = responseBody.string(); + + // Handle policy blocks (403 responses) + if (!response.isSuccessful()) { + try { + Map errorData = + objectMapper.readValue( + responseJson, + new com.fasterxml.jackson.core.type.TypeReference< + Map>() {}); + String errorMsg = + errorData.get("error") != null + ? errorData.get("error").toString() + : "MCP query failed: " + response.code(); + throw new ConnectorException(errorMsg, connector, "mcpQuery"); + } catch (JsonProcessingException e) { + throw new ConnectorException( + "MCP query failed: " + response.code(), connector, "mcpQuery"); + } + } + + return objectMapper.readValue(responseJson, ConnectorResponse.class); + } + }, + "mcpQuery"); + } + + /** + * Asynchronously executes a query against the MCP connector endpoint. + * + * @param connector name of the MCP connector + * @param statement SQL statement to execute + * @return a future containing the response + */ + public CompletableFuture mcpQueryAsync(String connector, String statement) { + return CompletableFuture.supplyAsync(() -> mcpQuery(connector, statement), asyncExecutor); + } + + /** + * Asynchronously executes a query against the MCP connector endpoint with options. + * + * @param connector name of the MCP connector + * @param statement SQL statement to execute + * @param options optional additional options + * @return a future containing the response + */ + public CompletableFuture mcpQueryAsync( + String connector, String statement, Map options) { + return CompletableFuture.supplyAsync( + () -> mcpQuery(connector, statement, options), asyncExecutor); + } + + /** + * Executes a statement against an MCP connector (alias for mcpQuery). + * + * @param connector name of the MCP connector + * @param statement SQL statement to execute + * @return ConnectorResponse with data, redaction info, and policy_info + */ + public ConnectorResponse mcpExecute(String connector, String statement) { + return mcpQuery(connector, statement); + } + + // ======================================================================== + // MCP Policy Check (Standalone) + // ======================================================================== + + /** + * Validates an MCP input statement against configured policies without executing it. + * + *

This method calls the agent's {@code /api/v1/mcp/check-input} endpoint to pre-validate a + * statement before sending it to the connector. Useful for checking SQL injection patterns, + * blocked operations, and input policy violations. + * + *

Example usage: + * + *

{@code
+   * MCPCheckInputResponse result = axonflow.mcpCheckInput("postgres", "SELECT * FROM users");
+   * if (!result.isAllowed()) {
+   *     System.out.println("Blocked: " + result.getBlockReason());
+   * }
+   * }
+ * + * @param connectorType name of the MCP connector type (e.g., "postgres") + * @param statement the statement to validate + * @return MCPCheckInputResponse with allowed status, block reason, and policy info + * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) + */ + public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement) { + return mcpCheckInput(connectorType, statement, null); + } + + /** + * Validates an MCP input statement against configured policies with options. + * + * @param connectorType name of the MCP connector type (e.g., "postgres") + * @param statement the statement to validate + * @param options optional parameters: "operation" (String), "parameters" (Map) + * @return MCPCheckInputResponse with allowed status, block reason, and policy info + * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) + */ + public MCPCheckInputResponse mcpCheckInput( + String connectorType, String statement, Map options) { + Objects.requireNonNull(connectorType, "connectorType cannot be null"); + Objects.requireNonNull(statement, "statement cannot be null"); + + return retryExecutor.execute( + () -> { + MCPCheckInputRequest request; + if (options != null) { + String operation = (String) options.getOrDefault("operation", "execute"); + @SuppressWarnings("unchecked") + Map parameters = (Map) options.get("parameters"); + request = new MCPCheckInputRequest(connectorType, statement, parameters, operation); + } else { + request = new MCPCheckInputRequest(connectorType, statement); + } + + Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-input", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new ConnectorException( + "Empty response from MCP check-input", connectorType, "mcpCheckInput"); + } + String responseJson = responseBody.string(); + + // 403 means policy blocked — the body is still a valid response + if (!response.isSuccessful() && response.code() != 403) { + try { + Map errorData = + objectMapper.readValue( + responseJson, new TypeReference>() {}); + String errorMsg = + errorData.get("error") != null + ? errorData.get("error").toString() + : "MCP check-input failed: " + response.code(); + throw new ConnectorException(errorMsg, connectorType, "mcpCheckInput"); + } catch (JsonProcessingException e) { + throw new ConnectorException( + "MCP check-input failed: " + response.code(), connectorType, "mcpCheckInput"); + } + } + + return objectMapper.readValue(responseJson, MCPCheckInputResponse.class); + } + }, + "mcpCheckInput"); + } + + /** + * Asynchronously validates an MCP input statement against configured policies. + * + * @param connectorType name of the MCP connector type + * @param statement the statement to validate + * @return a future containing the check result + */ + public CompletableFuture mcpCheckInputAsync( + String connectorType, String statement) { + return CompletableFuture.supplyAsync( + () -> mcpCheckInput(connectorType, statement), asyncExecutor); + } + + /** + * Asynchronously validates an MCP input statement against configured policies with options. + * + * @param connectorType name of the MCP connector type + * @param statement the statement to validate + * @param options optional parameters + * @return a future containing the check result + */ + public CompletableFuture mcpCheckInputAsync( + String connectorType, String statement, Map options) { + return CompletableFuture.supplyAsync( + () -> mcpCheckInput(connectorType, statement, options), asyncExecutor); + } + + /** + * Validates MCP response data against configured policies. + * + *

This method calls the agent's {@code /api/v1/mcp/check-output} endpoint to check response + * data for PII content, exfiltration limit violations, and other output policy violations. If PII + * redaction is active, {@code redactedData} contains the sanitized version. + * + *

Example usage: + * + *

{@code
+   * List> rows = List.of(
+   *     Map.of("name", "John", "ssn", "123-45-6789")
+   * );
+   * MCPCheckOutputResponse result = axonflow.mcpCheckOutput("postgres", rows);
+   * if (!result.isAllowed()) {
+   *     System.out.println("Blocked: " + result.getBlockReason());
+   * }
+   * if (result.getRedactedData() != null) {
+   *     System.out.println("Redacted: " + result.getRedactedData());
+   * }
+   * }
+ * + * @param connectorType name of the MCP connector type (e.g., "postgres") + * @param responseData the response data rows to validate + * @return MCPCheckOutputResponse with allowed status, redacted data, and policy info + * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) + */ + public MCPCheckOutputResponse mcpCheckOutput( + String connectorType, List> responseData) { + return mcpCheckOutput(connectorType, responseData, null); + } + + /** + * Validates MCP response data against configured policies with options. + * + * @param connectorType name of the MCP connector type (e.g., "postgres") + * @param responseData the response data rows to validate + * @param options optional parameters: "message" (String), "metadata" (Map), "row_count" (int) + * @return MCPCheckOutputResponse with allowed status, redacted data, and policy info + * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) + */ + public MCPCheckOutputResponse mcpCheckOutput( + String connectorType, List> responseData, Map options) { + Objects.requireNonNull(connectorType, "connectorType cannot be null"); + // responseData can be null for execute-style requests that use message instead + + return retryExecutor.execute( + () -> { + String message = options != null ? (String) options.get("message") : null; + @SuppressWarnings("unchecked") + Map metadata = + options != null ? (Map) options.get("metadata") : null; + int rowCount = options != null ? (int) options.getOrDefault("row_count", 0) : 0; + + MCPCheckOutputRequest request = + new MCPCheckOutputRequest(connectorType, responseData, message, metadata, rowCount); + + Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-output", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + ResponseBody responseBody = response.body(); + if (responseBody == null) { + throw new ConnectorException( + "Empty response from MCP check-output", connectorType, "mcpCheckOutput"); + } + String responseJson = responseBody.string(); + + // 403 means policy blocked — the body is still a valid response + if (!response.isSuccessful() && response.code() != 403) { + try { + Map errorData = + objectMapper.readValue( + responseJson, new TypeReference>() {}); + String errorMsg = + errorData.get("error") != null + ? errorData.get("error").toString() + : "MCP check-output failed: " + response.code(); + throw new ConnectorException(errorMsg, connectorType, "mcpCheckOutput"); + } catch (JsonProcessingException e) { + throw new ConnectorException( + "MCP check-output failed: " + response.code(), connectorType, "mcpCheckOutput"); + } + } + + return objectMapper.readValue(responseJson, MCPCheckOutputResponse.class); + } + }, + "mcpCheckOutput"); + } + + /** + * Asynchronously validates MCP response data against configured policies. + * + * @param connectorType name of the MCP connector type + * @param responseData the response data rows to validate + * @return a future containing the check result + */ + public CompletableFuture mcpCheckOutputAsync( + String connectorType, List> responseData) { + return CompletableFuture.supplyAsync( + () -> mcpCheckOutput(connectorType, responseData), asyncExecutor); + } + + /** + * Asynchronously validates MCP response data against configured policies with options. + * + * @param connectorType name of the MCP connector type + * @param responseData the response data rows to validate + * @param options optional parameters + * @return a future containing the check result + */ + public CompletableFuture mcpCheckOutputAsync( + String connectorType, List> responseData, Map options) { + return CompletableFuture.supplyAsync( + () -> mcpCheckOutput(connectorType, responseData, options), asyncExecutor); + } + + // ======================================================================== + // Policy CRUD - Static Policies + // ======================================================================== + + /** + * Lists static policies with optional filtering. + * + * @return list of static policies + */ + public List listStaticPolicies() { + return listStaticPolicies((ListStaticPoliciesOptions) null); + } + + /** + * Lists static policies with filtering options. + * + * @param options filtering options + * @return list of static policies + */ + public List listStaticPolicies(ListStaticPoliciesOptions options) { + return retryExecutor.execute( + () -> { + String path = buildPolicyQueryString("/api/v1/static-policies", options); + Request httpRequest = buildRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + StaticPoliciesResponse wrapper = parseResponse(response, StaticPoliciesResponse.class); + // Handle null wrapper or null policies list (Issue #40) + if (wrapper == null || wrapper.getPolicies() == null) { + return java.util.Collections.emptyList(); + } + return wrapper.getPolicies(); + } + }, + "listStaticPolicies"); + } + + /** + * Lists static policies filtered by tier and organization ID (Enterprise). + * + * @param tier the policy tier + * @param organizationId the organization ID + * @return list of static policies + */ + public List listStaticPolicies(PolicyTier tier, String organizationId) { + return listStaticPolicies( + ListStaticPoliciesOptions.builder().tier(tier).organizationId(organizationId).build()); + } + + /** + * Lists static policies filtered by tier and category. + * + * @param tier the policy tier + * @param category the policy category + * @return list of static policies + */ + public List listStaticPolicies(PolicyTier tier, PolicyCategory category) { + return listStaticPolicies( + ListStaticPoliciesOptions.builder().tier(tier).category(category).build()); + } + + /** + * Lists static policies filtered by category. + * + * @param category the policy category + * @return list of static policies + */ + public List listStaticPolicies(PolicyCategory category) { + return listStaticPolicies(ListStaticPoliciesOptions.builder().category(category).build()); + } + + /** + * Gets a specific static policy by ID. + * + * @param policyId the policy ID + * @return the static policy + */ + public StaticPolicy getStaticPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/static-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, + "getStaticPolicy"); + } + + /** + * Creates a new static policy. + * + * @param request the create request + * @return the created policy + */ + public StaticPolicy createStaticPolicy(CreateStaticPolicyRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("POST", "/api/v1/static-policies", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, + "createStaticPolicy"); + } + + /** + * Updates an existing static policy. + * + * @param policyId the policy ID + * @param request the update request + * @return the updated policy + */ + public StaticPolicy updateStaticPolicy(String policyId, UpdateStaticPolicyRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("PUT", "/api/v1/static-policies/" + policyId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, + "updateStaticPolicy"); + } + + /** + * Deletes a static policy. + * + * @param policyId the policy ID + */ + public void deleteStaticPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("DELETE", "/api/v1/static-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, + "deleteStaticPolicy"); + } + + /** + * Toggles a static policy's enabled status. + * + * @param policyId the policy ID + * @param enabled the new enabled status + * @return the updated policy + */ + public StaticPolicy toggleStaticPolicy(String policyId, boolean enabled) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = Map.of("enabled", enabled); + Request httpRequest = buildPatchRequest("/api/v1/static-policies/" + policyId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, StaticPolicy.class); + } + }, + "toggleStaticPolicy"); + } + + /** + * Gets effective static policies after inheritance and overrides. + * + * @return list of effective policies + */ + public List getEffectiveStaticPolicies() { + return getEffectiveStaticPolicies((EffectivePoliciesOptions) null); + } + + /** + * Gets effective static policies filtered by category. + * + * @param category the policy category + * @return list of effective policies + */ + public List getEffectiveStaticPolicies(PolicyCategory category) { + return getEffectiveStaticPolicies( + EffectivePoliciesOptions.builder().category(category).build()); + } + + /** + * Gets effective static policies with options. + * + * @param options filtering options + * @return list of effective policies + */ + public List getEffectiveStaticPolicies(EffectivePoliciesOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/static-policies/effective"); + if (options != null) { + String query = buildEffectivePoliciesQuery(options); + if (!query.isEmpty()) { + path.append("?").append(query); + } + } + Request httpRequest = buildRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + EffectivePoliciesResponse wrapper = + parseResponse(response, EffectivePoliciesResponse.class); + // Handle null wrapper or null policies list (Issue #40) + if (wrapper == null || wrapper.getStaticPolicies() == null) { + return java.util.Collections.emptyList(); + } + return wrapper.getStaticPolicies(); + } + }, + "getEffectiveStaticPolicies"); + } + + /** + * Tests a regex pattern against sample inputs. + * + * @param pattern the regex pattern + * @param testInputs sample inputs to test + * @return the test result + */ + public TestPatternResult testPattern(String pattern, List testInputs) { + Objects.requireNonNull(pattern, "pattern cannot be null"); + Objects.requireNonNull(testInputs, "testInputs cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = + Map.of( + "pattern", pattern, + "inputs", testInputs); + Request httpRequest = buildRequest("POST", "/api/v1/static-policies/test", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, TestPatternResult.class); + } + }, + "testPattern"); + } + + /** + * Gets version history for a static policy. + * + * @param policyId the policy ID + * @return list of policy versions + */ + public List getStaticPolicyVersions(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildRequest("GET", "/api/v1/static-policies/" + policyId + "/versions", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + Map wrapper = + parseResponse(response, new TypeReference>() {}); + @SuppressWarnings("unchecked") + List> versionsRaw = + (List>) wrapper.get("versions"); + if (versionsRaw == null) { + return new ArrayList<>(); + } + List versions = new ArrayList<>(); + for (Map v : versionsRaw) { + PolicyVersion version = objectMapper.convertValue(v, PolicyVersion.class); + versions.add(version); + } + return versions; + } + }, + "getStaticPolicyVersions"); + } + + // ======================================================================== + // Policy CRUD - Overrides (Enterprise) + // ======================================================================== + + /** + * Creates a policy override. + * + * @param policyId the policy ID + * @param request the override request + * @return the created override + */ + public PolicyOverride createPolicyOverride(String policyId, CreatePolicyOverrideRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildRequest("POST", "/api/v1/static-policies/" + policyId + "/override", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, PolicyOverride.class); + } + }, + "createPolicyOverride"); + } + + /** + * Deletes a policy override. + * + * @param policyId the policy ID + */ + public void deletePolicyOverride(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildRequest("DELETE", "/api/v1/static-policies/" + policyId + "/override", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, + "deletePolicyOverride"); + } + + /** + * Lists all active policy overrides (Enterprise). + * + * @return list of policy overrides + */ + public List listPolicyOverrides() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/static-policies/overrides", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Backend returns wrapped response: {"overrides": [...], "count": N} + Map wrapper = + parseResponse(response, new TypeReference>() {}); + @SuppressWarnings("unchecked") + List> overridesRaw = + (List>) wrapper.get("overrides"); + if (overridesRaw == null) { + return java.util.Collections.emptyList(); + } + return overridesRaw.stream() + .map(raw -> objectMapper.convertValue(raw, PolicyOverride.class)) + .collect(java.util.stream.Collectors.toList()); + } + }, + "listPolicyOverrides"); + } + + // ======================================================================== + // Policy CRUD - Dynamic Policies + // ======================================================================== + + /** + * Lists dynamic policies. + * + * @return list of dynamic policies + */ + public List listDynamicPolicies() { + return listDynamicPolicies(null); + } + + /** + * Lists dynamic policies with filtering options. + * + * @param options filtering options + * @return list of dynamic policies + */ + public List listDynamicPolicies(ListDynamicPoliciesOptions options) { + return retryExecutor.execute( + () -> { + String path = buildDynamicPolicyQueryString("/api/v1/dynamic-policies", options); + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policies": [...]} wrapper + DynamicPoliciesResponse wrapper = + parseResponse(response, DynamicPoliciesResponse.class); + // Handle null wrapper or null policies list (Issue #40) + if (wrapper == null || wrapper.getPolicies() == null) { + return java.util.Collections.emptyList(); + } + return wrapper.getPolicies(); + } + }, + "listDynamicPolicies"); + } + + /** + * Gets a specific dynamic policy by ID. + * + * @param policyId the policy ID + * @return the dynamic policy + */ + public DynamicPolicy getDynamicPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/dynamic-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); + return wrapper != null ? wrapper.getPolicy() : null; + } + }, + "getDynamicPolicy"); + } + + /** + * Creates a new dynamic policy. + * + * @param request the create request + * @return the created policy + */ + public DynamicPolicy createDynamicPolicy(CreateDynamicPolicyRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/dynamic-policies", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); + return wrapper != null ? wrapper.getPolicy() : null; + } + }, + "createDynamicPolicy"); + } + + /** + * Updates an existing dynamic policy. + * + * @param policyId the policy ID + * @param request the update request + * @return the updated policy + */ + public DynamicPolicy updateDynamicPolicy(String policyId, UpdateDynamicPolicyRequest request) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); + return wrapper != null ? wrapper.getPolicy() : null; + } + }, + "updateDynamicPolicy"); + } + + /** + * Deletes a dynamic policy. + * + * @param policyId the policy ID + */ + public void deleteDynamicPolicy(String policyId) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("DELETE", "/api/v1/dynamic-policies/" + policyId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, + "deleteDynamicPolicy"); + } + + /** + * Toggles a dynamic policy's enabled status. + * + * @param policyId the policy ID + * @param enabled the new enabled status + * @return the updated policy + */ + public DynamicPolicy toggleDynamicPolicy(String policyId, boolean enabled) { + Objects.requireNonNull(policyId, "policyId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = Map.of("enabled", enabled); + Request httpRequest = + buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); + return wrapper != null ? wrapper.getPolicy() : null; + } + }, + "toggleDynamicPolicy"); + } + + /** + * Gets effective dynamic policies after inheritance. + * + * @return list of effective policies + */ + public List getEffectiveDynamicPolicies() { + return getEffectiveDynamicPolicies(null); + } + + /** + * Gets effective dynamic policies with options. + * + * @param options filtering options + * @return list of effective policies + */ + public List getEffectiveDynamicPolicies(EffectivePoliciesOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/dynamic-policies/effective"); + if (options != null) { + String query = buildEffectivePoliciesQuery(options); + if (!query.isEmpty()) { + path.append("?").append(query); + } + } + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + // Agent proxy (Issue #886) returns {"policies": [...]} wrapper + DynamicPoliciesResponse wrapper = + parseResponse(response, DynamicPoliciesResponse.class); + // Handle null wrapper or null policies list (Issue #40) + if (wrapper == null || wrapper.getPolicies() == null) { + return java.util.Collections.emptyList(); + } + return wrapper.getPolicies(); + } + }, + "getEffectiveDynamicPolicies"); + } + + // ======================================================================== + // Unified Execution Tracking (Issue #1075 - EPIC #1074) + // ======================================================================== + + /** + * Gets the unified execution status for a given execution ID. + * + *

This method works for both MAP plans and WCP workflows, returning a consistent status format + * regardless of execution type. + * + * @param executionId the execution ID (plan ID or workflow ID) + * @return the unified execution status + */ + public com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus getExecutionStatus( + String executionId) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/unified/executions/" + executionId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus.class); + } + }, + "getExecutionStatus"); + } + + /** + * Lists unified executions with optional filtering. + * + * @param request filter options + * @return paginated list of executions + */ + public com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsResponse + listUnifiedExecutions( + com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsRequest request) { + + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/unified/executions"); + if (request != null) { + StringBuilder params = new StringBuilder(); + if (request.getExecutionType() != null) { + params.append("execution_type=").append(request.getExecutionType().getValue()); + } + if (request.getStatus() != null) { + if (params.length() > 0) params.append("&"); + params.append("status=").append(request.getStatus().getValue()); + } + if (request.getTenantId() != null) { + if (params.length() > 0) params.append("&"); + params.append("tenant_id=").append(request.getTenantId()); + } + if (request.getOrgId() != null) { + if (params.length() > 0) params.append("&"); + params.append("org_id=").append(request.getOrgId()); + } + if (request.getLimit() > 0) { + if (params.length() > 0) params.append("&"); + params.append("limit=").append(request.getLimit()); + } + if (request.getOffset() > 0) { + if (params.length() > 0) params.append("&"); + params.append("offset=").append(request.getOffset()); + } + if (params.length() > 0) { + path.append("?").append(params); + } + } + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsResponse + .class); + } + }, + "listUnifiedExecutions"); + } + + /** + * Cancels a unified execution (MAP plan or WCP workflow). + * + *

This method cancels an execution via the unified execution API, automatically propagating to + * the correct subsystem (MAP or WCP). + * + * @param executionId the execution ID (plan ID or workflow ID) + * @param reason optional reason for cancellation + */ + public void cancelExecution(String executionId, String reason) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + retryExecutor.execute( + () -> { + Map body = + reason != null ? Collections.singletonMap("reason", reason) : Collections.emptyMap(); + Request httpRequest = + buildOrchestratorRequest( + "POST", "/api/v1/unified/executions/" + executionId + "/cancel", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); } - if (numA != numB) { - return Integer.compare(numA, numB); + return null; + } + }, + "cancelExecution"); + } + + /** + * Cancels a unified execution without a reason. + * + * @param executionId the execution ID + */ + public void cancelExecution(String executionId) { + cancelExecution(executionId, null); + } + + /** + * Streams real-time execution status updates via Server-Sent Events (SSE). + * + *

Connects to the SSE streaming endpoint and invokes the callback with each {@link + * com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus} update as it arrives. The + * stream automatically closes when the execution reaches a terminal state (completed, failed, + * cancelled, aborted, or expired). + * + *

Example usage: + * + *

{@code
+   * axonflow.streamExecutionStatus("exec_123", status -> {
+   *     System.out.printf("Progress: %.0f%% - Status: %s%n",
+   *         status.getProgressPercent(), status.getStatus().getValue());
+   *     if (status.getCurrentStep() != null) {
+   *         System.out.println("  Current step: " + status.getCurrentStep().getStepName());
+   *     }
+   * });
+   * }
+ * + * @param executionId the execution ID (plan ID or workflow ID) + * @param callback consumer invoked with each ExecutionStatus update + * @throws AxonFlowException if the connection fails or an I/O error occurs + * @throws AuthenticationException if authentication fails (401/403) + */ + public void streamExecutionStatus( + String executionId, + Consumer callback) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + Objects.requireNonNull(callback, "callback cannot be null"); + + logger.debug("Streaming execution status for {}", executionId); + + HttpUrl url = + HttpUrl.parse( + config.getEndpoint() + "/api/v1/unified/executions/" + executionId + "/stream"); + if (url == null) { + throw new ConfigurationException( + "Invalid URL: " + + config.getEndpoint() + + "/api/v1/unified/executions/" + + executionId + + "/stream"); + } + + Request.Builder builder = + new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "text/event-stream") + .get(); + + addAuthHeaders(builder); + + Request httpRequest = builder.build(); + + try { + Response response = httpClient.newCall(httpRequest).execute(); + try { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("SSE response has no body", 0, null); + } + + try (BufferedReader reader = + new BufferedReader(new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { + StringBuilder eventBuffer = new StringBuilder(); + String line; + + while ((line = reader.readLine()) != null) { + if (line.isEmpty()) { + // Empty line = end of SSE event + String event = eventBuffer.toString().trim(); + eventBuffer.setLength(0); + + if (event.isEmpty()) { + continue; + } + + // Parse SSE data lines + for (String eventLine : event.split("\n")) { + if (eventLine.startsWith("data: ")) { + String jsonStr = eventLine.substring(6); + if (jsonStr.isEmpty() || "[DONE]".equals(jsonStr)) { + continue; + } + try { + com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus status = + objectMapper.readValue( + jsonStr, + com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus + .class); + callback.accept(status); + + // Check for terminal status + if (status.getStatus() != null && status.getStatus().isTerminal()) { + return; + } + } catch (JsonProcessingException e) { + logger.warn("Failed to parse SSE data: {}", jsonStr, e); + } + } + } + } else { + eventBuffer.append(line).append("\n"); } + } } - return 0; + } finally { + response.close(); + } + } catch (IOException e) { + throw new AxonFlowException("SSE stream failed: " + e.getMessage(), e); + } + } + + // ======================================================================== + // Media Governance Config + // ======================================================================== + + /** + * Gets the media governance configuration for the current tenant. + * + *

Returns per-tenant settings controlling whether media analysis is enabled and which + * analyzers are allowed. + * + * @return the media governance configuration + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceConfig getMediaGovernanceConfig() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/media-governance/config", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceConfig.class); + } + }, + "getMediaGovernanceConfig"); + } + + /** + * Asynchronously gets the media governance configuration for the current tenant. + * + * @return a future containing the media governance configuration + */ + public CompletableFuture getMediaGovernanceConfigAsync() { + return CompletableFuture.supplyAsync(this::getMediaGovernanceConfig, asyncExecutor); + } + + /** + * Updates the media governance configuration for the current tenant. + * + *

Allows enabling/disabling media analysis and controlling which analyzers are permitted. + * + * @param request the update request + * @return the updated media governance configuration + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceConfig updateMediaGovernanceConfig( + UpdateMediaGovernanceConfigRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("PUT", "/api/v1/media-governance/config", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceConfig.class); + } + }, + "updateMediaGovernanceConfig"); + } + + /** + * Asynchronously updates the media governance configuration for the current tenant. + * + * @param request the update request + * @return a future containing the updated media governance configuration + */ + public CompletableFuture updateMediaGovernanceConfigAsync( + UpdateMediaGovernanceConfigRequest request) { + return CompletableFuture.supplyAsync(() -> updateMediaGovernanceConfig(request), asyncExecutor); + } + + /** + * Gets the platform-level media governance status. + * + *

Returns whether media governance is available, default enablement, and the required license + * tier. + * + * @return the media governance status + * @throws AxonFlowException if the request fails + */ + public MediaGovernanceStatus getMediaGovernanceStatus() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildRequest("GET", "/api/v1/media-governance/status", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, MediaGovernanceStatus.class); + } + }, + "getMediaGovernanceStatus"); + } + + /** + * Asynchronously gets the platform-level media governance status. + * + * @return a future containing the media governance status + */ + public CompletableFuture getMediaGovernanceStatusAsync() { + return CompletableFuture.supplyAsync(this::getMediaGovernanceStatus, asyncExecutor); + } + + // ======================================================================== + // Configuration Access + // ======================================================================== + + /** + * Returns the current configuration. + * + * @return the configuration + */ + public AxonFlowConfig getConfig() { + return config; + } + + /** + * Returns cache statistics. + * + * @return cache stats string + */ + public String getCacheStats() { + return cache.getStats(); + } + + /** Clears the response cache. */ + public void clearCache() { + cache.clear(); + } + + // ======================================================================== + // Internal Methods + // ======================================================================== + + private Request buildRequest(String method, String path, Object body) { + HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); + if (url == null) { + throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); + } + + Request.Builder builder = + new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "application/json"); + + // Add authentication headers + addAuthHeaders(builder); + + // Add mode header + if (config.getMode() != null) { + builder.header("X-AxonFlow-Mode", config.getMode().getValue()); + } + + // Set method and body + RequestBody requestBody = null; + if (body != null) { + try { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, JSON); + } catch (JsonProcessingException e) { + throw new AxonFlowException("Failed to serialize request body", e); + } + } + + switch (method.toUpperCase()) { + case "GET": + builder.get(); + break; + case "POST": + builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "PUT": + builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "DELETE": + builder.delete(requestBody); + break; + default: + throw new IllegalArgumentException("Unsupported method: " + method); + } + + return builder.build(); + } + + private Request buildPatchRequest(String path, Object body) { + HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); + if (url == null) { + throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); + } + + Request.Builder builder = + new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "application/json"); + + addAuthHeaders(builder); + + if (config.getMode() != null) { + builder.header("X-AxonFlow-Mode", config.getMode().getValue()); } - // ======================================================================== - // Factory Methods - // ======================================================================== + RequestBody requestBody = null; + if (body != null) { + try { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, JSON); + } catch (JsonProcessingException e) { + throw new AxonFlowException("Failed to serialize request body", e); + } + } - /** - * Creates a new builder for AxonFlow configuration. - * - * @return a new builder - */ - public static AxonFlowConfig.Builder builder() { - return AxonFlowConfig.builder(); + builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); + return builder.build(); + } + + private String buildPolicyQueryString(String basePath, ListStaticPoliciesOptions options) { + if (options == null) { + return basePath; } - /** - * Creates an AxonFlow client with the given configuration. - * - * @param config the configuration - * @return a new AxonFlow client - */ - public static AxonFlow create(AxonFlowConfig config) { - return new AxonFlow(config); + StringBuilder path = new StringBuilder(basePath); + StringBuilder query = new StringBuilder(); + + if (options.getCategory() != null) { + appendQueryParam(query, "category", options.getCategory().getValue()); + } + if (options.getTier() != null) { + appendQueryParam(query, "tier", options.getTier().getValue()); + } + if (options.getOrganizationId() != null) { + appendQueryParam(query, "organization_id", options.getOrganizationId()); + } + if (options.getEnabled() != null) { + appendQueryParam(query, "enabled", options.getEnabled().toString()); + } + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getSortBy() != null) { + appendQueryParam(query, "sort_by", options.getSortBy()); + } + if (options.getSortOrder() != null) { + appendQueryParam(query, "sort_order", options.getSortOrder()); + } + if (options.getSearch() != null) { + appendQueryParam(query, "search", options.getSearch()); } - /** - * Creates an AxonFlow client from environment variables. - * - * @return a new AxonFlow client - * @see AxonFlowConfig#fromEnvironment() - */ - public static AxonFlow fromEnvironment() { - return new AxonFlow(AxonFlowConfig.fromEnvironment()); + if (query.length() > 0) { + path.append("?").append(query); } + return path.toString(); + } - /** - * Creates an AxonFlow client in sandbox mode. - * - * @param agentUrl the Agent URL - * @return a new AxonFlow client in sandbox mode - */ - public static AxonFlow sandbox(String agentUrl) { - return new AxonFlow(AxonFlowConfig.builder() - .agentUrl(agentUrl) - .mode(Mode.SANDBOX) - .build()); + private String buildDynamicPolicyQueryString( + String basePath, ListDynamicPoliciesOptions options) { + if (options == null) { + return basePath; } - // ======================================================================== - // Health Check - // ======================================================================== + StringBuilder path = new StringBuilder(basePath); + StringBuilder query = new StringBuilder(); - /** - * Checks if the AxonFlow Agent is healthy. - * - * @return the health status - * @throws ConnectionException if the Agent cannot be reached - */ - public HealthStatus healthCheck() { - HealthStatus status = retryExecutor.execute(() -> { - Request request = buildRequest("GET", "/health", null); - try (Response response = httpClient.newCall(request).execute()) { - return parseResponse(response, HealthStatus.class); - } - }, "healthCheck"); - - if (status.getSdkCompatibility() != null - && status.getSdkCompatibility().getMinSdkVersion() != null - && !"unknown".equals(AxonFlowConfig.SDK_VERSION) - && compareSemver(AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion()) < 0) { - logger.warn("SDK version {} is below minimum supported version {}. Please upgrade.", - AxonFlowConfig.SDK_VERSION, status.getSdkCompatibility().getMinSdkVersion()); - } + if (options.getType() != null) { + appendQueryParam(query, "type", options.getType()); + } + if (options.getTier() != null) { + appendQueryParam(query, "tier", options.getTier().getValue()); + } + if (options.getOrganizationId() != null) { + appendQueryParam(query, "organization_id", options.getOrganizationId()); + } + if (options.getEnabled() != null) { + appendQueryParam(query, "enabled", options.getEnabled().toString()); + } + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getSortBy() != null) { + appendQueryParam(query, "sort_by", options.getSortBy()); + } + if (options.getSortOrder() != null) { + appendQueryParam(query, "sort_order", options.getSortOrder()); + } + if (options.getSearch() != null) { + appendQueryParam(query, "search", options.getSearch()); + } - return status; + if (query.length() > 0) { + path.append("?").append(query); } + return path.toString(); + } - /** - * Asynchronously checks if the AxonFlow Agent is healthy. - * - * @return a future containing the health status - */ - public CompletableFuture healthCheckAsync() { - return CompletableFuture.supplyAsync(this::healthCheck, asyncExecutor); + private String buildEffectivePoliciesQuery(EffectivePoliciesOptions options) { + StringBuilder query = new StringBuilder(); + + if (options.getCategory() != null) { + appendQueryParam(query, "category", options.getCategory().getValue()); + } + if (options.isIncludeDisabled()) { + appendQueryParam(query, "include_disabled", "true"); + } + if (options.isIncludeOverridden()) { + appendQueryParam(query, "include_overridden", "true"); } - // ======================================================================== - // MAS FEAT Namespace Accessor - // ======================================================================== + return query.toString(); + } - /** - * Returns the MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, - * Accountability, Transparency) compliance namespace. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - *

Example usage: - *

{@code
-     * AISystemRegistry system = client.masfeat().registerSystem(
-     *     RegisterSystemRequest.builder()
-     *         .systemId("credit-scoring-ai")
-     *         .systemName("Credit Scoring AI")
-     *         .useCase(AISystemUseCase.CREDIT_SCORING)
-     *         .ownerTeam("Risk Management")
-     *         .customerImpact(4)
-     *         .modelComplexity(3)
-     *         .humanReliance(5)
-     *         .build()
-     * );
-     * }
- * - * @return the MAS FEAT compliance namespace - */ - public MASFEATNamespace masfeat() { - return masfeatNamespace; + private void appendQueryParam(StringBuilder query, String name, String value) { + if (query.length() > 0) { + query.append("&"); } + query.append(name).append("=").append(value); + } - /** - * Checks if the AxonFlow Orchestrator is healthy. - * - * @return the health status - * @throws ConnectionException if the Orchestrator cannot be reached - */ - public HealthStatus orchestratorHealthCheck() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/health", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - return new HealthStatus("unhealthy", null, null, null, null, null); - } - return parseResponse(response, HealthStatus.class); - } - }, "orchestratorHealthCheck"); + private void addAuthHeaders(Request.Builder builder) { + // Always send Basic auth with the effective clientId — server derives tenant from it. + // clientSecret defaults to empty string for community/no-secret mode. + String effectiveClientId = getEffectiveClientId(); + String secret = config.getClientSecret() != null ? config.getClientSecret() : ""; + String credentials = effectiveClientId + ":" + secret; + String encoded = + Base64.getEncoder().encodeToString(credentials.getBytes(StandardCharsets.UTF_8)); + builder.header("Authorization", "Basic " + encoded); + } + + /** + * Requires credentials for enterprise features. Get the effective clientId, using smart default + * for community mode. + * + *

Returns the configured clientId if set, otherwise returns "community" as a smart default. + * This enables zero-config usage for community/self-hosted deployments while still supporting + * enterprise deployments with explicit credentials. + * + * @return the clientId to use in requests + */ + private String getEffectiveClientId() { + String clientId = config.getClientId(); + return (clientId != null && !clientId.isEmpty()) ? clientId : "community"; + } + + private T parseResponse(Response response, Class type) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); } - /** - * Asynchronously checks if the AxonFlow Orchestrator is healthy. - * - * @return a future containing the health status - */ - public CompletableFuture orchestratorHealthCheckAsync() { - return CompletableFuture.supplyAsync(this::orchestratorHealthCheck, asyncExecutor); + String json = body.string(); + if (json.isEmpty()) { + throw new AxonFlowException("Empty response body", response.code(), null); } - // ======================================================================== - // Gateway Mode - Policy Pre-check and Audit - // ======================================================================== + try { + return objectMapper.readValue(json, type); + } catch (JsonProcessingException e) { + throw new AxonFlowException( + "Failed to parse response: " + e.getMessage(), response.code(), null, e); + } + } - /** - * Pre-checks a request against policies (Gateway Mode - Step 1). - * - *

This is the first step in Gateway Mode. If approved, make your LLM call - * directly, then call {@link #auditLLMCall(AuditOptions)} to complete the flow. - * - * @param request the policy approval request - * @return the approval result with context ID for auditing - * @throws PolicyViolationException if the request is blocked by policy - * @throws AuthenticationException if authentication fails - */ - public PolicyApprovalResult getPolicyApprovedContext(PolicyApprovalRequest request) { - Objects.requireNonNull(request, "request cannot be null"); + private T parseResponse(Response response, TypeReference typeRef) throws IOException { + handleErrorResponse(response); - // Use smart default for clientId - enables zero-config community mode - String effectiveClientId = (request.getClientId() != null && !request.getClientId().isEmpty()) - ? request.getClientId() - : getEffectiveClientId(); + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } - Map ctx = request.getContext(); - PolicyApprovalRequest effectiveRequest = PolicyApprovalRequest.builder() - .userToken(request.getUserToken()) - .query(request.getQuery()) - .dataSources(request.getDataSources()) - .context(ctx == null || ctx.isEmpty() ? null : ctx) - .clientId(effectiveClientId) - .build(); + String json = body.string(); + try { + return objectMapper.readValue(json, typeRef); + } catch (JsonProcessingException e) { + throw new AxonFlowException( + "Failed to parse response: " + e.getMessage(), response.code(), null, e); + } + } - final PolicyApprovalRequest finalRequest = effectiveRequest; - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", "/api/policy/pre-check", finalRequest); - try (Response response = httpClient.newCall(httpRequest).execute()) { - PolicyApprovalResult result = parseResponse(response, PolicyApprovalResult.class); + private JsonNode parseResponseNode(Response response) throws IOException { + handleErrorResponse(response); - if (!result.isApproved()) { - throw new PolicyViolationException( - result.getBlockReason(), - result.getBlockingPolicyName(), - result.getPolicies() - ); - } + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } - return result; - } - }, "getPolicyApprovedContext"); + String json = body.string(); + if (json.isEmpty()) { + return objectMapper.createObjectNode(); } - /** - * Alias for {@link #getPolicyApprovedContext(PolicyApprovalRequest)}. - * - * @param request the policy approval request - * @return the approval result - */ - public PolicyApprovalResult preCheck(PolicyApprovalRequest request) { - return getPolicyApprovedContext(request); + try { + return objectMapper.readTree(json); + } catch (JsonProcessingException e) { + throw new AxonFlowException( + "Failed to parse response: " + e.getMessage(), response.code(), null, e); } + } - /** - * Asynchronously pre-checks a request against policies. - * - * @param request the policy approval request - * @return a future containing the approval result - */ - public CompletableFuture getPolicyApprovedContextAsync(PolicyApprovalRequest request) { - return CompletableFuture.supplyAsync(() -> getPolicyApprovedContext(request), asyncExecutor); + private void handleErrorResponse(Response response) throws IOException { + if (response.isSuccessful()) { + return; } - /** - * Audits an LLM call for compliance tracking (Gateway Mode - Step 3). - * - *

Call this after making your direct LLM call to record it for - * compliance and observability. - * - * @param options the audit options including context ID from pre-check - * @return the audit result - * @throws AxonFlowException if the audit fails - */ - public AuditResult auditLLMCall(AuditOptions options) { - Objects.requireNonNull(options, "options cannot be null"); + int code = response.code(); + String message = response.message(); + String body = response.body() != null ? response.body().string() : ""; + + // Try to extract error message from JSON body + String errorMessage = extractErrorMessage(body, message); - // Use smart default for clientId - enables zero-config community mode - String effectiveClientId = (options.getClientId() != null && !options.getClientId().isEmpty()) - ? options.getClientId() - : getEffectiveClientId(); + switch (code) { + case 401: + throw new AuthenticationException(errorMessage); + case 402: + // Budget exceeded - treat similarly to 403 policy violation + throw new PolicyViolationException(errorMessage); + case 403: + // Check if this is a policy violation + if (body.contains("policy") || body.contains("blocked")) { + throw new PolicyViolationException(errorMessage); + } + throw new AuthenticationException(errorMessage, 403); + case 409: + throw new AxonFlowException(errorMessage, 409, "VERSION_CONFLICT"); + case 429: + throw new RateLimitException(errorMessage); + case 408: + case 504: + throw new TimeoutException(errorMessage); + default: + throw new AxonFlowException(errorMessage, code, null); + } + } + + private String extractErrorMessage(String body, String defaultMessage) { + if (body == null || body.isEmpty()) { + return defaultMessage; + } + + try { + Map errorResponse = + objectMapper.readValue(body, new TypeReference>() {}); + + if (errorResponse.containsKey("error")) { + return String.valueOf(errorResponse.get("error")); + } + if (errorResponse.containsKey("message")) { + return String.valueOf(errorResponse.get("message")); + } + if (errorResponse.containsKey("block_reason")) { + return String.valueOf(errorResponse.get("block_reason")); + } + } catch (JsonProcessingException e) { + // Body is not JSON, return as-is if short enough + if (body.length() < 200) { + return body; + } + } + + return defaultMessage; + } + + // ======================================================================== + // Portal Authentication (Enterprise) + // ======================================================================== + + /** + * Login to Customer Portal and store session cookie. Required before using Code Governance + * methods. + * + * @param orgId the organization ID + * @param password the organization password + * @return login response with session info + * @throws IOException if the request fails + * @example + *

{@code
+   * PortalLoginResponse login = axonflow.loginToPortal("test-org-001", "test123");
+   * System.out.println("Logged in as: " + login.getName());
+   *
+   * // Now you can use Code Governance methods
+   * ListGitProvidersResponse providers = axonflow.listGitProviders();
+   * }
+ */ + public PortalLoginResponse loginToPortal(String orgId, String password) throws IOException { + logger.debug("Logging in to portal: {}", orgId); + + String json = + objectMapper.writeValueAsString(java.util.Map.of("org_id", orgId, "password", password)); + RequestBody body = RequestBody.create(json, JSON); + + Request request = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/auth/login") + .post(body) + .header("Content-Type", "application/json") + .build(); - // Create effective options with the smart default clientId - AuditOptions.Builder builder = AuditOptions.builder() - .contextId(options.getContextId()) - .clientId(effectiveClientId) - .responseSummary(options.getResponseSummary()) - .provider(options.getProvider()) - .model(options.getModel()) - .tokenUsage(options.getTokenUsage()) - .metadata(options.getMetadata()) - .success(options.getSuccess()) - .errorMessage(options.getErrorMessage()); + try (Response response = httpClient.newCall(request).execute()) { + if (!response.isSuccessful()) { + throw new AuthenticationException("Login failed: " + response.body().string()); + } - // Handle null latencyMs (builder takes primitive long) - if (options.getLatencyMs() != null) { - builder.latencyMs(options.getLatencyMs()); + PortalLoginResponse loginResponse = parseResponse(response, PortalLoginResponse.class); + + // Extract session cookie from response + String cookies = response.header("Set-Cookie"); + if (cookies != null && cookies.contains("axonflow_session=")) { + int start = cookies.indexOf("axonflow_session=") + 17; + int end = cookies.indexOf(";", start); + if (end > start) { + this.sessionCookie = cookies.substring(start, end); } + } + + // Fallback to session_id in response body + if (this.sessionCookie == null && loginResponse.getSessionId() != null) { + this.sessionCookie = loginResponse.getSessionId(); + } + + logger.info("Portal login successful for {}", orgId); + return loginResponse; + } + } + + /** Logout from Customer Portal and clear session cookie. */ + public void logoutFromPortal() { + if (sessionCookie == null) { + return; + } + + try { + Request request = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/auth/logout") + .post(RequestBody.create("", JSON)) + .header("Cookie", "axonflow_session=" + sessionCookie) + .build(); + + httpClient.newCall(request).execute().close(); + } catch (Exception e) { + // Ignore logout errors + } + + sessionCookie = null; + logger.info("Portal logout successful"); + } + + /** + * Check if logged in to Customer Portal. + * + * @return true if logged in + */ + public boolean isLoggedIn() { + return sessionCookie != null; + } + + // ======================================================================== + // Code Governance - Git Provider APIs (Enterprise) + // ======================================================================== + + /** + * Validates Git provider credentials without saving them. Requires prior authentication via + * loginToPortal(). + * + * @param request the validation request with provider type and credentials + * @return validation result + * @throws IOException if the request fails + */ + public ValidateGitProviderResponse validateGitProvider(ValidateGitProviderRequest request) + throws IOException { + requirePortalLogin(); + logger.debug("Validating Git provider: {}", request.getType()); + + String json = objectMapper.writeValueAsString(request); + RequestBody body = RequestBody.create(json, JSON); + + Request.Builder builder = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/code-governance/git-providers/validate") + .post(body); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, ValidateGitProviderResponse.class); + } + } + + /** + * Configures a Git provider for code governance. + * + * @param request the configuration request with provider type and credentials + * @return configuration result + * @throws IOException if the request fails + */ + public ConfigureGitProviderResponse configureGitProvider(ConfigureGitProviderRequest request) + throws IOException { + requirePortalLogin(); + logger.debug("Configuring Git provider: {}", request.getType()); + + String json = objectMapper.writeValueAsString(request); + RequestBody body = RequestBody.create(json, JSON); + + Request.Builder builder = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/code-governance/git-providers") + .post(body); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, ConfigureGitProviderResponse.class); + } + } + + /** + * Lists configured Git providers. + * + * @return list of configured providers + * @throws IOException if the request fails + */ + public ListGitProvidersResponse listGitProviders() throws IOException { + requirePortalLogin(); + logger.debug("Listing Git providers"); + + Request.Builder builder = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/code-governance/git-providers") + .get(); - AuditOptions effectiveOptions = builder.build(); + addPortalSessionCookie(builder); - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", "/api/audit/llm-call", effectiveOptions); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, AuditResult.class); - } - }, "auditLLMCall"); - } + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, ListGitProvidersResponse.class); + } + } + + /** + * Deletes a configured Git provider. + * + * @param providerType the provider type to delete + * @throws IOException if the request fails + */ + public void deleteGitProvider(GitProviderType providerType) throws IOException { + requirePortalLogin(); + logger.debug("Deleting Git provider: {}", providerType); + + Request.Builder builder = + new Request.Builder() + .url( + config.getEndpoint() + + "/api/v1/code-governance/git-providers/" + + providerType.getValue()) + .delete(); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + handleErrorResponse(response); + } + } + + /** + * Creates a Pull Request from LLM-generated code. + * + * @param request the PR creation request with repository info and files + * @return the created PR details + * @throws IOException if the request fails + */ + public CreatePRResponse createPR(CreatePRRequest request) throws IOException { + requirePortalLogin(); + logger.debug( + "Creating PR: {} in {}/{}", request.getTitle(), request.getOwner(), request.getRepo()); + + String json = objectMapper.writeValueAsString(request); + RequestBody body = RequestBody.create(json, JSON); + + Request.Builder builder = + new Request.Builder().url(config.getEndpoint() + "/api/v1/code-governance/prs").post(body); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, CreatePRResponse.class); + } + } + + /** + * Lists PRs with optional filtering. + * + * @param options filtering options (limit, offset, state) + * @return list of PRs + * @throws IOException if the request fails + */ + public ListPRsResponse listPRs(ListPRsOptions options) throws IOException { + requirePortalLogin(); + logger.debug("Listing PRs"); + + StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/prs"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getLimit() != null) { + appendQueryParam(query, "limit", String.valueOf(options.getLimit())); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", String.valueOf(options.getOffset())); + } + if (options.getState() != null) { + appendQueryParam(query, "state", options.getState()); + } + } + + if (query.length() > 0) { + url.append("?").append(query); + } + + Request.Builder builder = new Request.Builder().url(url.toString()).get(); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, ListPRsResponse.class); + } + } + + /** + * Lists PRs with default options. + * + * @return list of PRs + * @throws IOException if the request fails + */ + public ListPRsResponse listPRs() throws IOException { + return listPRs(null); + } + + /** + * Gets a specific PR by ID. + * + * @param prId the PR record ID + * @return the PR record + * @throws IOException if the request fails + */ + public PRRecord getPR(String prId) throws IOException { + requirePortalLogin(); + logger.debug("Getting PR: {}", prId); + + Request.Builder builder = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/code-governance/prs/" + prId) + .get(); - /** - * Asynchronously audits an LLM call. - * - * @param options the audit options - * @return a future containing the audit result - */ - public CompletableFuture auditLLMCallAsync(AuditOptions options) { - return CompletableFuture.supplyAsync(() -> auditLLMCall(options), asyncExecutor); + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, PRRecord.class); + } + } + + /** + * Syncs PR status from the Git provider. + * + * @param prId the PR record ID to sync + * @return the updated PR record + * @throws IOException if the request fails + */ + public PRRecord syncPRStatus(String prId) throws IOException { + requirePortalLogin(); + logger.debug("Syncing PR status: {}", prId); + + RequestBody body = RequestBody.create("{}", JSON); + + Request.Builder builder = + new Request.Builder() + .url(config.getEndpoint() + "/api/v1/code-governance/prs/" + prId + "/sync") + .post(body); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, PRRecord.class); } + } - // ======================================================================== - // Audit Log Read Methods - // ======================================================================== + /** + * Closes a PR without merging and optionally deletes the branch. This is an enterprise feature + * for cleaning up test/demo PRs. Supports all Git providers: GitHub, GitLab, Bitbucket. + * + * @param prId the PR record ID to close + * @param deleteBranch whether to also delete the source branch + * @return the closed PR record + * @throws IOException if the request fails + */ + public PRRecord closePR(String prId, boolean deleteBranch) throws IOException { + requirePortalLogin(); + logger.debug("Closing PR: {} (deleteBranch={})", prId, deleteBranch); + + String url = config.getEndpoint() + "/api/v1/code-governance/prs/" + prId; + if (deleteBranch) { + url += "?delete_branch=true"; + } - /** - * Searches audit logs with flexible filtering options. - * - *

Example usage: - *

{@code
-     * AuditSearchResponse response = axonflow.searchAuditLogs(
-     *     AuditSearchRequest.builder()
-     *         .userEmail("analyst@company.com")
-     *         .startTime(Instant.now().minus(Duration.ofDays(7)))
-     *         .requestType("llm_chat")
-     *         .limit(100)
-     *         .build());
-     *
-     * for (AuditLogEntry entry : response.getEntries()) {
-     *     System.out.println(entry.getId() + ": " + entry.getQuerySummary());
-     * }
-     * }
- * - * @param request the search request with optional filters - * @return the search response containing matching audit log entries - * @throws AxonFlowException if the search fails - */ - public AuditSearchResponse searchAuditLogs(AuditSearchRequest request) { - return retryExecutor.execute(() -> { - AuditSearchRequest req = request != null ? request : AuditSearchRequest.builder().build(); - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/audit/search", req); - try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - - // Handle both array and wrapped response formats - if (node.isArray()) { - List entries = objectMapper.convertValue( - node, new TypeReference>() {}); - return AuditSearchResponse.fromArray(entries, - req.getLimit() != null ? req.getLimit() : 100, - req.getOffset() != null ? req.getOffset() : 0); - } + Request.Builder builder = new Request.Builder().url(url).delete(); - return objectMapper.treeToValue(node, AuditSearchResponse.class); - } - }, "searchAuditLogs"); + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, PRRecord.class); } + } + + /** + * Gets aggregated code governance metrics for the tenant. + * + * @return aggregated metrics including PR counts, file counts, and security findings + * @throws IOException if the request fails + */ + public CodeGovernanceMetrics getCodeGovernanceMetrics() throws IOException { + requirePortalLogin(); + logger.debug("Getting code governance metrics"); - /** - * Searches audit logs with default options (last 100 entries). - * - * @return the search response - */ - public AuditSearchResponse searchAuditLogs() { - return searchAuditLogs(null); + Request.Builder builder = + new Request.Builder().url(config.getEndpoint() + "/api/v1/code-governance/metrics").get(); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, CodeGovernanceMetrics.class); } + } - /** - * Asynchronously searches audit logs. - * - * @param request the search request - * @return a future containing the search response - */ - public CompletableFuture searchAuditLogsAsync(AuditSearchRequest request) { - return CompletableFuture.supplyAsync(() -> searchAuditLogs(request), asyncExecutor); + /** + * Exports code governance data in JSON format. + * + * @param options export options (format, date range, state filter) + * @return export response with PR records + * @throws IOException if the request fails + */ + public ExportResponse exportCodeGovernanceData(ExportOptions options) throws IOException { + requirePortalLogin(); + logger.debug("Exporting code governance data"); + + StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/export"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + appendQueryParam(query, "format", options.getFormat() != null ? options.getFormat() : "json"); + if (options.getStartDate() != null) { + appendQueryParam(query, "start_date", options.getStartDate().toString()); + } + if (options.getEndDate() != null) { + appendQueryParam(query, "end_date", options.getEndDate().toString()); + } + if (options.getState() != null) { + appendQueryParam(query, "state", options.getState()); + } + } else { + appendQueryParam(query, "format", "json"); + } + + if (query.length() > 0) { + url.append("?").append(query); + } + + Request.Builder builder = new Request.Builder().url(url.toString()).get(); + + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + return parseResponse(response, ExportResponse.class); + } + } + + /** + * Exports code governance data in CSV format. + * + * @param options export options (date range, state filter) + * @return CSV data as a string + * @throws IOException if the request fails + */ + public String exportCodeGovernanceDataCSV(ExportOptions options) throws IOException { + requirePortalLogin(); + logger.debug("Exporting code governance data as CSV"); + + StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/export"); + StringBuilder query = new StringBuilder(); + + appendQueryParam(query, "format", "csv"); + if (options != null) { + if (options.getStartDate() != null) { + appendQueryParam(query, "start_date", options.getStartDate().toString()); + } + if (options.getEndDate() != null) { + appendQueryParam(query, "end_date", options.getEndDate().toString()); + } + if (options.getState() != null) { + appendQueryParam(query, "state", options.getState()); + } } - /** - * Gets audit logs for a specific tenant. - * - *

Example usage: - *

{@code
-     * AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc",
-     *     AuditQueryOptions.builder()
-     *         .limit(100)
-     *         .offset(50)
-     *         .build());
-     *
-     * System.out.println("Total entries: " + response.getTotal());
-     * System.out.println("Has more: " + response.hasMore());
-     * }
- * - * @param tenantId the tenant ID to query - * @param options optional pagination options - * @return the search response containing audit log entries for the tenant - * @throws IllegalArgumentException if tenantId is null or empty - * @throws AxonFlowException if the query fails - */ - public AuditSearchResponse getAuditLogsByTenant(String tenantId, AuditQueryOptions options) { - if (tenantId == null || tenantId.isEmpty()) { - throw new IllegalArgumentException("tenantId is required"); - } + url.append("?").append(query); - return retryExecutor.execute(() -> { - AuditQueryOptions opts = options != null ? options : AuditQueryOptions.defaults(); - String encodedTenantId = java.net.URLEncoder.encode(tenantId, "UTF-8"); - String path = "/api/v1/audit/tenant/" + encodedTenantId + - "?limit=" + opts.getLimit() + "&offset=" + opts.getOffset(); + Request.Builder builder = new Request.Builder().url(url.toString()).get(); - Request httpRequest = buildOrchestratorRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); + addPortalSessionCookie(builder); + + try (Response response = httpClient.newCall(builder.build()).execute()) { + handleErrorResponse(response); + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + return body.string(); + } + } + + // ======================================================================== + // Execution Replay API + // ======================================================================== - // Handle both array and wrapped response formats - if (node.isArray()) { - List entries = objectMapper.convertValue( - node, new TypeReference>() {}); - return AuditSearchResponse.fromArray(entries, opts.getLimit(), opts.getOffset()); - } + /** Builds a request for the orchestrator API. */ + private Request buildOrchestratorRequest(String method, String path, Object body) { + HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); + if (url == null) { + throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); + } + + Request.Builder builder = + new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "application/json"); - return objectMapper.treeToValue(node, AuditSearchResponse.class); - } - }, "getAuditLogsByTenant"); + addAuthHeaders(builder); + + RequestBody requestBody = null; + if (body != null) { + try { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, JSON); + } catch (JsonProcessingException e) { + throw new AxonFlowException("Failed to serialize request body", e); + } + } + + switch (method.toUpperCase()) { + case "GET": + builder.get(); + break; + case "POST": + builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "PUT": + builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "PATCH": + builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "DELETE": + builder.delete(requestBody); + break; + default: + throw new IllegalArgumentException("Unsupported method: " + method); } - /** - * Gets audit logs for a specific tenant with default options. - * - * @param tenantId the tenant ID to query - * @return the search response - */ - public AuditSearchResponse getAuditLogsByTenant(String tenantId) { - return getAuditLogsByTenant(tenantId, null); + return builder.build(); + } + + /** Requires portal login before making code governance requests. */ + private void requirePortalLogin() { + if (sessionCookie == null) { + throw new AuthenticationException( + "Not logged in to Customer Portal. Call loginToPortal() first."); } + } - /** - * Asynchronously gets audit logs for a specific tenant. - * - * @param tenantId the tenant ID to query - * @param options optional pagination options - * @return a future containing the search response - */ - public CompletableFuture getAuditLogsByTenantAsync(String tenantId, AuditQueryOptions options) { - return CompletableFuture.supplyAsync(() -> getAuditLogsByTenant(tenantId, options), asyncExecutor); + /** Adds the session cookie header for portal authentication. */ + private void addPortalSessionCookie(Request.Builder builder) { + if (sessionCookie != null) { + builder.header("Cookie", "axonflow_session=" + sessionCookie); } + } - // ======================================================================== - // Audit Tool Call - // ======================================================================== + /** + * Builds a request for the Customer Portal API (enterprise features). Requires prior + * authentication via loginToPortal(). + */ + private Request buildPortalRequest(String method, String path, Object body) { + requirePortalLogin(); - /** - * Audits a non-LLM tool call for compliance and observability. - * - *

Records tool invocations such as function calls, MCP operations, - * or API calls to the audit log. - * - *

Example usage: - *

{@code
-     * AuditToolCallResponse response = axonflow.auditToolCall(
-     *     AuditToolCallRequest.builder()
-     *         .toolName("web_search")
-     *         .toolType("function")
-     *         .input(Map.of("query", "latest news"))
-     *         .output(Map.of("results", 5))
-     *         .workflowId("wf_123")
-     *         .durationMs(450L)
-     *         .success(true)
-     *         .build());
-     * }
- * - * @param request the audit tool call request - * @return the audit tool call response with audit ID - * @throws NullPointerException if request is null - * @throws IllegalArgumentException if tool_name is null or empty - * @throws AxonFlowException if the audit fails - */ - public AuditToolCallResponse auditToolCall(AuditToolCallRequest request) { - Objects.requireNonNull(request, "request cannot be null"); + HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); + if (url == null) { + throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); + } - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/audit/tool-call", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, AuditToolCallResponse.class); + Request.Builder builder = + new Request.Builder() + .url(url) + .header("User-Agent", config.getUserAgent()) + .header("Accept", "application/json"); + + addPortalSessionCookie(builder); + + RequestBody requestBody = null; + if (body != null) { + try { + String json = objectMapper.writeValueAsString(body); + requestBody = RequestBody.create(json, JSON); + } catch (JsonProcessingException e) { + throw new AxonFlowException("Failed to serialize request body", e); + } + } + + switch (method.toUpperCase()) { + case "GET": + builder.get(); + break; + case "POST": + builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "PUT": + builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "PATCH": + builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); + break; + case "DELETE": + builder.delete(requestBody); + break; + default: + throw new IllegalArgumentException("Unsupported method: " + method); + } + + return builder.build(); + } + + /** + * Lists workflow executions with optional filtering and pagination. + * + * @param options filtering and pagination options + * @return paginated list of execution summaries + * @example + *
{@code
+   * ListExecutionsResponse response = axonflow.listExecutions(
+   *     ListExecutionsOptions.builder()
+   *         .setStatus("completed")
+   *         .setLimit(10)
+   * );
+   * for (ExecutionSummary exec : response.getExecutions()) {
+   *     System.out.println(exec.getRequestId() + ": " + exec.getStatus());
+   * }
+   * }
+ */ + public ListExecutionsResponse listExecutions(ListExecutionsOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/executions"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); } - }, "auditToolCall"); - } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getStatus() != null) { + appendQueryParam(query, "status", options.getStatus()); + } + if (options.getWorkflowId() != null) { + appendQueryParam(query, "workflow_id", options.getWorkflowId()); + } + if (options.getStartTime() != null) { + appendQueryParam(query, "start_time", options.getStartTime()); + } + if (options.getEndTime() != null) { + appendQueryParam(query, "end_time", options.getEndTime()); + } + } - /** - * Asynchronously audits a non-LLM tool call. - * - * @param request the audit tool call request - * @return a future containing the audit tool call response - */ - public CompletableFuture auditToolCallAsync(AuditToolCallRequest request) { - return CompletableFuture.supplyAsync(() -> auditToolCall(request), asyncExecutor); - } + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ListExecutionsResponse.class); + } + }, + "listExecutions"); + } + + /** + * Lists workflow executions with default options. + * + * @return list of execution summaries + */ + public ListExecutionsResponse listExecutions() { + return listExecutions(null); + } + + /** + * Gets a complete execution record including summary and all steps. + * + * @param executionId the execution ID (request_id) + * @return full execution details with all step snapshots + * @example + *
{@code
+   * ExecutionDetail detail = axonflow.getExecution("exec-abc123");
+   * System.out.println("Status: " + detail.getSummary().getStatus());
+   * for (ExecutionSnapshot step : detail.getSteps()) {
+   *     System.out.println("Step " + step.getStepIndex() + ": " + step.getStepName());
+   * }
+   * }
+ */ + public ExecutionDetail getExecution(String executionId) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/executions/" + executionId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ExecutionDetail.class); + } + }, + "getExecution"); + } + + /** + * Gets all step snapshots for an execution. + * + * @param executionId the execution ID (request_id) + * @return list of step snapshots + */ + public List getExecutionSteps(String executionId) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/executions/" + executionId + "/steps", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, + "getExecutionSteps"); + } + + /** + * Gets a timeline view of execution events for visualization. + * + * @param executionId the execution ID (request_id) + * @return list of timeline entries + */ + public List getExecutionTimeline(String executionId) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "GET", "/api/v1/executions/" + executionId + "/timeline", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, + "getExecutionTimeline"); + } + + /** + * Exports a complete execution record for compliance or archival. + * + * @param executionId the execution ID (request_id) + * @param options export options + * @return execution data as a map + * @example + *
{@code
+   * Map export = axonflow.exportExecution("exec-abc123",
+   *     ExecutionExportOptions.builder()
+   *         .setIncludeInput(true)
+   *         .setIncludeOutput(true));
+   * // Save to file for audit
+   * }
+ */ + public Map exportExecution(String executionId, ExecutionExportOptions options) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/executions/" + executionId + "/export"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getFormat() != null) { + appendQueryParam(query, "format", options.getFormat()); + } + if (options.isIncludeInput()) { + appendQueryParam(query, "include_input", "true"); + } + if (options.isIncludeOutput()) { + appendQueryParam(query, "include_output", "true"); + } + if (options.isIncludePolicies()) { + appendQueryParam(query, "include_policies", "true"); + } + } + + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, new TypeReference>() {}); + } + }, + "exportExecution"); + } + + /** + * Exports a complete execution record with default options. + * + * @param executionId the execution ID (request_id) + * @return execution data as a map + */ + public Map exportExecution(String executionId) { + return exportExecution(executionId, null); + } + + /** + * Deletes an execution and all associated step snapshots. + * + * @param executionId the execution ID (request_id) + */ + public void deleteExecution(String executionId) { + Objects.requireNonNull(executionId, "executionId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("DELETE", "/api/v1/executions/" + executionId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, + "deleteExecution"); + } + + /** + * Asynchronously lists workflow executions. + * + * @param options filtering and pagination options + * @return a future containing the list of executions + */ + public CompletableFuture listExecutionsAsync( + ListExecutionsOptions options) { + return CompletableFuture.supplyAsync(() -> listExecutions(options), asyncExecutor); + } + + /** + * Asynchronously gets execution details. + * + * @param executionId the execution ID + * @return a future containing the execution details + */ + public CompletableFuture getExecutionAsync(String executionId) { + return CompletableFuture.supplyAsync(() -> getExecution(executionId), asyncExecutor); + } + + // ======================================== + // COST CONTROLS - BUDGETS + // ======================================== + + /** + * Creates a new budget. + * + * @param request the budget creation request + * @return the created budget + */ + public Budget createBudget(CreateBudgetRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/budgets", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, Budget.class); + } + }, + "createBudget"); + } + + /** + * Gets a budget by ID. + * + * @param budgetId the budget ID + * @return the budget + */ + public Budget getBudget(String budgetId) { + Objects.requireNonNull(budgetId, "budgetId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, Budget.class); + } + }, + "getBudget"); + } + + /** + * Lists all budgets. + * + * @param options filtering and pagination options + * @return list of budgets + */ + public BudgetsResponse listBudgets(ListBudgetsOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/budgets"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getScope() != null) { + appendQueryParam(query, "scope", options.getScope().getValue()); + } + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + } - // ======================================================================== - // Circuit Breaker Observability - // ======================================================================== + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, BudgetsResponse.class); + } + }, + "listBudgets"); + } + + /** + * Lists all budgets with default options. + * + * @return list of budgets + */ + public BudgetsResponse listBudgets() { + return listBudgets(null); + } + + /** + * Updates an existing budget. + * + * @param budgetId the budget ID + * @param request the update request + * @return the updated budget + */ + public Budget updateBudget(String budgetId, UpdateBudgetRequest request) { + Objects.requireNonNull(budgetId, "budgetId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("PUT", "/api/v1/budgets/" + budgetId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, Budget.class); + } + }, + "updateBudget"); + } + + /** + * Deletes a budget. + * + * @param budgetId the budget ID + */ + public void deleteBudget(String budgetId) { + Objects.requireNonNull(budgetId, "budgetId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("DELETE", "/api/v1/budgets/" + budgetId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful() && response.code() != 204) { + handleErrorResponse(response); + } + return null; + } + }, + "deleteBudget"); + } + + // ======================================== + // COST CONTROLS - BUDGET STATUS & ALERTS + // ======================================== + + /** + * Gets the current status of a budget. + * + * @param budgetId the budget ID + * @return the budget status + */ + public BudgetStatus getBudgetStatus(String budgetId) { + Objects.requireNonNull(budgetId, "budgetId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId + "/status", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, BudgetStatus.class); + } + }, + "getBudgetStatus"); + } + + /** + * Gets alerts for a budget. + * + * @param budgetId the budget ID + * @return the budget alerts + */ + public BudgetAlertsResponse getBudgetAlerts(String budgetId) { + Objects.requireNonNull(budgetId, "budgetId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId + "/alerts", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, BudgetAlertsResponse.class); + } + }, + "getBudgetAlerts"); + } + + /** + * Performs a pre-flight budget check. + * + * @param request the check request + * @return the budget decision + */ + public BudgetDecision checkBudget(BudgetCheckRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/budgets/check", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, BudgetDecision.class); + } + }, + "checkBudget"); + } + + // ======================================== + // COST CONTROLS - USAGE + // ======================================== + + /** + * Gets usage summary for a period. + * + * @param period the period (daily, weekly, monthly, quarterly, yearly) + * @return the usage summary + */ + public UsageSummary getUsageSummary(String period) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/usage"); + if (period != null && !period.isEmpty()) { + path.append("?period=").append(period); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, UsageSummary.class); + } + }, + "getUsageSummary"); + } + + /** + * Gets usage summary with default period. + * + * @return the usage summary + */ + public UsageSummary getUsageSummary() { + return getUsageSummary(null); + } + + /** + * Gets usage breakdown by a grouping dimension. + * + * @param groupBy the dimension to group by (provider, model, agent, team, workflow) + * @param period the period (daily, weekly, monthly, quarterly, yearly) + * @return the usage breakdown + */ + public UsageBreakdown getUsageBreakdown(String groupBy, String period) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/usage/breakdown"); + StringBuilder query = new StringBuilder(); + + if (groupBy != null && !groupBy.isEmpty()) { + appendQueryParam(query, "group_by", groupBy); + } + if (period != null && !period.isEmpty()) { + appendQueryParam(query, "period", period); + } + + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, UsageBreakdown.class); + } + }, + "getUsageBreakdown"); + } + + /** + * Lists usage records. + * + * @param options filtering and pagination options + * @return list of usage records + */ + public UsageRecordsResponse listUsageRecords(ListUsageRecordsOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/usage/records"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getLimit() != null) { + appendQueryParam(query, "limit", options.getLimit().toString()); + } + if (options.getOffset() != null) { + appendQueryParam(query, "offset", options.getOffset().toString()); + } + if (options.getProvider() != null) { + appendQueryParam(query, "provider", options.getProvider()); + } + if (options.getModel() != null) { + appendQueryParam(query, "model", options.getModel()); + } + } - /** - * Gets the current circuit breaker status, including all active (tripped) circuits. - * - *

Example usage: - *

{@code
-     * CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus();
-     * System.out.println("Active circuits: " + status.getCount());
-     * System.out.println("Emergency stop: " + status.isEmergencyStopActive());
-     * }
- * - * @return the circuit breaker status - * @throws AxonFlowException if the request fails - */ - public CircuitBreakerStatusResponse getCircuitBreakerStatus() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/circuit-breaker/status", null); + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, UsageRecordsResponse.class); + } + }, + "listUsageRecords"); + } + + /** + * Lists usage records with default options. + * + * @return list of usage records + */ + public UsageRecordsResponse listUsageRecords() { + return listUsageRecords(null); + } + + // ======================================== + // COST CONTROLS - PRICING + // ======================================== + + /** + * Gets pricing information for models. + * + * @param provider filter by provider (optional) + * @param model filter by model (optional) + * @return pricing information + */ + public PricingListResponse getPricing(String provider, String model) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/pricing"); + StringBuilder query = new StringBuilder(); + + if (provider != null && !provider.isEmpty()) { + appendQueryParam(query, "provider", provider); + } + if (model != null && !model.isEmpty()) { + appendQueryParam(query, "model", model); + } + + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + String body = response.body() != null ? response.body().string() : ""; + if (!response.isSuccessful()) { + throw new AxonFlowException("Failed to get pricing: " + body); + } + + // Handle single object or array response + if (body.trim().startsWith("{") && body.contains("\"provider\"")) { + // Single object response - wrap in list + PricingInfo singlePricing = objectMapper.readValue(body, PricingInfo.class); + PricingListResponse result = new PricingListResponse(); + result.setPricing(Collections.singletonList(singlePricing)); + return result; + } else { + return objectMapper.readValue(body, PricingListResponse.class); + } + } + }, + "getPricing"); + } + + /** + * Gets all pricing information. + * + * @return all pricing information + */ + public PricingListResponse getPricing() { + return getPricing(null, null); + } + + // ======================================== + // COST CONTROLS - ASYNC METHODS + // ======================================== + + /** + * Asynchronously creates a budget. + * + * @param request the budget creation request + * @return a future containing the created budget + */ + public CompletableFuture createBudgetAsync(CreateBudgetRequest request) { + return CompletableFuture.supplyAsync(() -> createBudget(request), asyncExecutor); + } + + /** + * Asynchronously gets a budget. + * + * @param budgetId the budget ID + * @return a future containing the budget + */ + public CompletableFuture getBudgetAsync(String budgetId) { + return CompletableFuture.supplyAsync(() -> getBudget(budgetId), asyncExecutor); + } + + /** + * Asynchronously lists budgets. + * + * @param options filtering and pagination options + * @return a future containing the list of budgets + */ + public CompletableFuture listBudgetsAsync(ListBudgetsOptions options) { + return CompletableFuture.supplyAsync(() -> listBudgets(options), asyncExecutor); + } + + /** + * Asynchronously gets budget status. + * + * @param budgetId the budget ID + * @return a future containing the budget status + */ + public CompletableFuture getBudgetStatusAsync(String budgetId) { + return CompletableFuture.supplyAsync(() -> getBudgetStatus(budgetId), asyncExecutor); + } + + /** + * Asynchronously gets usage summary. + * + * @param period the period + * @return a future containing the usage summary + */ + public CompletableFuture getUsageSummaryAsync(String period) { + return CompletableFuture.supplyAsync(() -> getUsageSummary(period), asyncExecutor); + } + + // ======================================================================== + // Workflow Control Plane + // ======================================================================== + // The Workflow Control Plane provides governance gates for external + // orchestrators like LangChain, LangGraph, and CrewAI. + // + // "LangChain runs the workflow. AxonFlow decides when it's allowed to move forward." + + /** + * Creates a new workflow for governance tracking. + * + *

Registers a new workflow with AxonFlow. Call this at the start of your external orchestrator + * workflow (LangChain, LangGraph, CrewAI, etc.). + * + * @param request workflow creation request + * @return created workflow with ID + * @throws AxonFlowException if creation fails + * @example + *

{@code
+   * CreateWorkflowResponse workflow = axonflow.createWorkflow(
+   *     CreateWorkflowRequest.builder()
+   *         .workflowName("code-review-pipeline")
+   *         .source(WorkflowSource.LANGGRAPH)
+   *         .build()
+   * );
+   * System.out.println("Workflow created: " + workflow.getWorkflowId());
+   * }
+ */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowResponse createWorkflow( + com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/workflows", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowResponse>() {}); + } + }, + "createWorkflow"); + } + + /** + * Gets the status of a workflow. + * + * @param workflowId workflow ID + * @return workflow status including steps + * @throws AxonFlowException if workflow not found + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatusResponse getWorkflow( + String workflowId) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/workflows/" + workflowId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatusResponse>() {}); + } + }, + "getWorkflow"); + } + + /** + * Checks if a workflow step is allowed to proceed (step gate). + * + *

This is the core governance method. Call this before executing each step in your workflow to + * check if the step is allowed based on policies. + * + * @param workflowId workflow ID + * @param stepId unique step identifier (you provide this) + * @param request step gate request with step details + * @return gate decision: allow, block, or require_approval + * @throws AxonFlowException if check fails + * @example + *

{@code
+   * StepGateResponse gate = axonflow.stepGate(
+   *     workflow.getWorkflowId(),
+   *     "step-1",
+   *     StepGateRequest.builder()
+   *         .stepName("Generate Code")
+   *         .stepType(StepType.LLM_CALL)
+   *         .model("gpt-4")
+   *         .provider("openai")
+   *         .build()
+   * );
+   *
+   * if (gate.isBlocked()) {
+   *     throw new RuntimeException("Step blocked: " + gate.getReason());
+   * } else if (gate.requiresApproval()) {
+   *     System.out.println("Approval needed: " + gate.getApprovalUrl());
+   * } else {
+   *     // Execute the step
+   *     executeStep();
+   * }
+   * }
+ */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateResponse stepGate( + String workflowId, + String stepId, + com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateRequest request) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + Objects.requireNonNull(stepId, "stepId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", + "/api/v1/workflows/" + workflowId + "/steps/" + stepId + "/gate", + request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateResponse>() {}); + } + }, + "stepGate"); + } + + /** + * Marks a step as completed. + * + *

Call this after successfully executing a step to record its completion. + * + * @param workflowId workflow ID + * @param stepId step ID + * @param request optional completion request with output data + */ + public void markStepCompleted( + String workflowId, + String stepId, + com.getaxonflow.sdk.types.workflow.WorkflowTypes.MarkStepCompletedRequest request) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + Objects.requireNonNull(stepId, "stepId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", + "/api/v1/workflows/" + workflowId + "/steps/" + stepId + "/complete", + request != null ? request : Collections.emptyMap()); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "markStepCompleted"); + } + + /** + * Marks a step as completed with no output data. + * + * @param workflowId workflow ID + * @param stepId step ID + */ + public void markStepCompleted(String workflowId, String stepId) { + markStepCompleted(workflowId, stepId, null); + } + + /** + * Completes a workflow successfully. + * + *

Call this when your workflow has completed all steps successfully. + * + * @param workflowId workflow ID + */ + public void completeWorkflow(String workflowId) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", "/api/v1/workflows/" + workflowId + "/complete", Collections.emptyMap()); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "completeWorkflow"); + } + + /** + * Aborts a workflow. + * + *

Call this when you need to stop a workflow due to an error or user request. + * + * @param workflowId workflow ID + * @param reason optional reason for aborting + */ + public void abortWorkflow(String workflowId, String reason) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + + retryExecutor.execute( + () -> { + Map body = + reason != null ? Collections.singletonMap("reason", reason) : Collections.emptyMap(); + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/workflows/" + workflowId + "/abort", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "abortWorkflow"); + } + + /** + * Aborts a workflow with no reason. + * + * @param workflowId workflow ID + */ + public void abortWorkflow(String workflowId) { + abortWorkflow(workflowId, null); + } + + /** + * Fails a workflow. + * + *

Call this when a workflow has encountered an unrecoverable error and should be marked as + * failed. + * + * @param workflowId workflow ID + * @param reason optional reason for failing + */ + public void failWorkflow(String workflowId, String reason) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + + retryExecutor.execute( + () -> { + Map body = + reason != null ? Collections.singletonMap("reason", reason) : Collections.emptyMap(); + Request httpRequest = + buildOrchestratorRequest("POST", "/api/v1/workflows/" + workflowId + "/fail", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "failWorkflow"); + } + + /** + * Fails a workflow with no reason. + * + * @param workflowId workflow ID + */ + public void failWorkflow(String workflowId) { + failWorkflow(workflowId, null); + } + + /** + * Asynchronously fails a workflow. + * + * @param workflowId workflow ID + * @param reason optional reason for failing + * @return a future that completes when the workflow has been failed + */ + public CompletableFuture failWorkflowAsync(String workflowId, String reason) { + return CompletableFuture.supplyAsync( + () -> { + failWorkflow(workflowId, reason); + return null; + }, + asyncExecutor); + } + + /** + * Resumes a workflow after approval. + * + *

Call this after a step has been approved to continue the workflow. + * + * @param workflowId workflow ID + */ + public void resumeWorkflow(String workflowId) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", "/api/v1/workflows/" + workflowId + "/resume", Collections.emptyMap()); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "resumeWorkflow"); + } + + /** + * Lists workflows with optional filters. + * + * @param options filter and pagination options + * @return list of workflows + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse listWorkflows( + com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsOptions options) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/workflows"); + StringBuilder query = new StringBuilder(); + + if (options != null) { + if (options.getStatus() != null) { + appendQueryParam(query, "status", options.getStatus().getValue()); + } + if (options.getSource() != null) { + appendQueryParam(query, "source", options.getSource().getValue()); + } + if (options.getLimit() > 0) { + appendQueryParam(query, "limit", String.valueOf(options.getLimit())); + } + if (options.getOffset() > 0) { + appendQueryParam(query, "offset", String.valueOf(options.getOffset())); + } + if (options.getTraceId() != null) { + appendQueryParam(query, "trace_id", options.getTraceId()); + } + } + + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse>() {}); + } + }, + "listWorkflows"); + } + + /** + * Lists all workflows with default options. + * + * @return list of workflows + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse listWorkflows() { + return listWorkflows(null); + } + + /** + * Asynchronously creates a workflow. + * + * @param request workflow creation request + * @return a future containing the created workflow + */ + public CompletableFuture + createWorkflowAsync( + com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowRequest request) { + return CompletableFuture.supplyAsync(() -> createWorkflow(request), asyncExecutor); + } + + /** + * Asynchronously checks a step gate. + * + * @param workflowId workflow ID + * @param stepId step ID + * @param request step gate request + * @return a future containing the gate decision + */ + public CompletableFuture + stepGateAsync( + String workflowId, + String stepId, + com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateRequest request) { + return CompletableFuture.supplyAsync( + () -> stepGate(workflowId, stepId, request), asyncExecutor); + } + + // ======================================================================== + // WCP Approval Methods + // ======================================================================== + + /** + * Approves a workflow step that requires human approval. + * + *

Call this when a step gate returns {@code require_approval} to approve the step and allow + * the workflow to proceed. + * + * @param workflowId workflow ID + * @param stepId step ID + * @return the approval response + * @throws AxonFlowException if the approval fails + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse approveStep( + String workflowId, String stepId) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + Objects.requireNonNull(stepId, "stepId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", + "/api/v1/workflow-control/" + workflowId + "/steps/" + stepId + "/approve", + Collections.emptyMap()); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse>() {}); + } + }, + "approveStep"); + } + + /** + * Asynchronously approves a workflow step. + * + * @param workflowId workflow ID + * @param stepId step ID + * @return a future containing the approval response + */ + public CompletableFuture + approveStepAsync(String workflowId, String stepId) { + return CompletableFuture.supplyAsync(() -> approveStep(workflowId, stepId), asyncExecutor); + } + + /** + * Rejects a workflow step that requires human approval. + * + *

Call this when a step gate returns {@code require_approval} to reject the step and prevent + * the workflow from proceeding. + * + * @param workflowId workflow ID + * @param stepId step ID + * @return the rejection response + * @throws AxonFlowException if the rejection fails + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse rejectStep( + String workflowId, String stepId) { + return rejectStep(workflowId, stepId, null); + } + + /** + * Rejects a workflow step that requires human approval, with a reason. + * + *

Call this when a step gate returns {@code require_approval} to reject the step and prevent + * the workflow from proceeding. + * + * @param workflowId workflow ID + * @param stepId step ID + * @param reason optional reason for rejection (included in request body) + * @return the rejection response + * @throws AxonFlowException if the rejection fails + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse rejectStep( + String workflowId, String stepId, String reason) { + Objects.requireNonNull(workflowId, "workflowId cannot be null"); + Objects.requireNonNull(stepId, "stepId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + if (reason != null && !reason.isEmpty()) { + body.put("reason", reason); + } + Request httpRequest = + buildOrchestratorRequest( + "POST", + "/api/v1/workflow-control/" + workflowId + "/steps/" + stepId + "/reject", + body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse>() {}); + } + }, + "rejectStep"); + } + + /** + * Asynchronously rejects a workflow step. + * + * @param workflowId workflow ID + * @param stepId step ID + * @return a future containing the rejection response + */ + public CompletableFuture + rejectStepAsync(String workflowId, String stepId) { + return rejectStepAsync(workflowId, stepId, null); + } + + /** + * Asynchronously rejects a workflow step with a reason. + * + * @param workflowId workflow ID + * @param stepId step ID + * @param reason optional reason for rejection + * @return a future containing the rejection response + */ + public CompletableFuture + rejectStepAsync(String workflowId, String stepId, String reason) { + return CompletableFuture.supplyAsync( + () -> rejectStep(workflowId, stepId, reason), asyncExecutor); + } + + /** + * Gets pending approvals with a limit. + * + * @param limit maximum number of pending approvals to return + * @return the pending approvals response + * @throws AxonFlowException if the request fails + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse + getPendingApprovals(int limit) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/workflow-control/pending-approvals"); + if (limit > 0) { + path.append("?limit=").append(limit); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse( + response, + new TypeReference< + com.getaxonflow.sdk.types.workflow.WorkflowTypes + .PendingApprovalsResponse>() {}); + } + }, + "getPendingApprovals"); + } + + /** + * Gets all pending approvals with default limit. + * + * @return the pending approvals response + * @throws AxonFlowException if the request fails + */ + public com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse + getPendingApprovals() { + return getPendingApprovals(0); + } + + /** + * Asynchronously gets pending approvals with a limit. + * + * @param limit maximum number of pending approvals to return + * @return a future containing the pending approvals response + */ + public CompletableFuture< + com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse> + getPendingApprovalsAsync(int limit) { + return CompletableFuture.supplyAsync(() -> getPendingApprovals(limit), asyncExecutor); + } + + // ======================================================================== + // Webhook Subscriptions + // ======================================================================== + + /** + * Creates a new webhook subscription. + * + * @param request the webhook creation request + * @return the created webhook subscription + * @throws AxonFlowException if creation fails + */ + public WebhookSubscription createWebhook(CreateWebhookRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/webhooks", request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, WebhookSubscription.class); + } + }, + "createWebhook"); + } + + /** + * Asynchronously creates a new webhook subscription. + * + * @param request the webhook creation request + * @return a future containing the created webhook subscription + */ + public CompletableFuture createWebhookAsync(CreateWebhookRequest request) { + return CompletableFuture.supplyAsync(() -> createWebhook(request), asyncExecutor); + } + + /** + * Gets a webhook subscription by ID. + * + * @param webhookId the webhook ID + * @return the webhook subscription + * @throws AxonFlowException if the webhook is not found + */ + public WebhookSubscription getWebhook(String webhookId) { + Objects.requireNonNull(webhookId, "webhookId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/webhooks/" + webhookId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, WebhookSubscription.class); + } + }, + "getWebhook"); + } + + /** + * Asynchronously gets a webhook subscription by ID. + * + * @param webhookId the webhook ID + * @return a future containing the webhook subscription + */ + public CompletableFuture getWebhookAsync(String webhookId) { + return CompletableFuture.supplyAsync(() -> getWebhook(webhookId), asyncExecutor); + } + + /** + * Updates an existing webhook subscription. + * + * @param webhookId the webhook ID + * @param request the update request + * @return the updated webhook subscription + * @throws AxonFlowException if the update fails + */ + public WebhookSubscription updateWebhook(String webhookId, UpdateWebhookRequest request) { + Objects.requireNonNull(webhookId, "webhookId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("PUT", "/api/v1/webhooks/" + webhookId, request); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, WebhookSubscription.class); + } + }, + "updateWebhook"); + } + + /** + * Asynchronously updates an existing webhook subscription. + * + * @param webhookId the webhook ID + * @param request the update request + * @return a future containing the updated webhook subscription + */ + public CompletableFuture updateWebhookAsync( + String webhookId, UpdateWebhookRequest request) { + return CompletableFuture.supplyAsync(() -> updateWebhook(webhookId, request), asyncExecutor); + } + + /** + * Deletes a webhook subscription. + * + * @param webhookId the webhook ID + * @throws AxonFlowException if the deletion fails + */ + public void deleteWebhook(String webhookId) { + Objects.requireNonNull(webhookId, "webhookId cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("DELETE", "/api/v1/webhooks/" + webhookId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "deleteWebhook"); + } + + /** + * Asynchronously deletes a webhook subscription. + * + * @param webhookId the webhook ID + * @return a future that completes when the webhook is deleted + */ + public CompletableFuture deleteWebhookAsync(String webhookId) { + return CompletableFuture.runAsync(() -> deleteWebhook(webhookId), asyncExecutor); + } + + /** + * Lists all webhook subscriptions. + * + * @return the list of webhook subscriptions + * @throws AxonFlowException if the request fails + */ + public ListWebhooksResponse listWebhooks() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/webhooks", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseResponse(response, ListWebhooksResponse.class); + } + }, + "listWebhooks"); + } + + /** + * Asynchronously lists all webhook subscriptions. + * + * @return a future containing the list of webhook subscriptions + */ + public CompletableFuture listWebhooksAsync() { + return CompletableFuture.supplyAsync(this::listWebhooks, asyncExecutor); + } + + // ======================================================================== + // HITL (Human-in-the-Loop) Queue + // ======================================================================== + + /** + * Lists pending HITL approval requests. + * + *

Returns approval requests from the HITL queue, optionally filtered by status and severity. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @param opts filtering and pagination options (may be null) + * @return the list response containing approval requests + * @throws AxonFlowException if the request fails + */ + public HITLQueueListResponse listHITLQueue(HITLQueueListOptions opts) { + return retryExecutor.execute( + () -> { + StringBuilder path = new StringBuilder("/api/v1/hitl/queue"); + StringBuilder query = new StringBuilder(); + + if (opts != null) { + if (opts.getStatus() != null) { + appendQueryParam(query, "status", opts.getStatus()); + } + if (opts.getSeverity() != null) { + appendQueryParam(query, "severity", opts.getSeverity()); + } + if (opts.getLimit() != null) { + appendQueryParam(query, "limit", opts.getLimit().toString()); + } + if (opts.getOffset() != null) { + appendQueryParam(query, "offset", opts.getOffset().toString()); + } + } + + if (query.length() > 0) { + path.append("?").append(query); + } + + Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + + // Server wraps response: {"success": true, "data": [...], "meta": {...}} + HITLQueueListResponse result = new HITLQueueListResponse(); + if (node.has("data") && node.get("data").isArray()) { + List items = + objectMapper.convertValue( + node.get("data"), new TypeReference>() {}); + result.setItems(items); + } + if (node.has("meta")) { + JsonNode meta = node.get("meta"); + long total = 0; + long offset = 0; + if (meta.has("total")) { + total = meta.get("total").asLong(); + result.setTotal(total); + } + if (meta.has("offset")) { + offset = meta.get("offset").asLong(); + } + // Compute hasMore from total/offset/items (consistent with Go/TS SDKs) + result.setHasMore((offset + result.getItems().size()) < total); + } + return result; + } + }, + "listHITLQueue"); + } + + /** + * Lists pending HITL approval requests with default options. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @return the list response containing approval requests + * @throws AxonFlowException if the request fails + */ + public HITLQueueListResponse listHITLQueue() { + return listHITLQueue(null); + } + + /** + * Asynchronously lists pending HITL approval requests. + * + * @param opts filtering and pagination options (may be null) + * @return a future containing the list response + */ + public CompletableFuture listHITLQueueAsync(HITLQueueListOptions opts) { + return CompletableFuture.supplyAsync(() -> listHITLQueue(opts), asyncExecutor); + } + + /** + * Gets a specific HITL approval request by ID. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @param requestId the approval request ID + * @return the approval request + * @throws AxonFlowException if the request is not found or the call fails + */ + public HITLApprovalRequest getHITLRequest(String requestId) { + Objects.requireNonNull(requestId, "requestId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", "/api/v1/hitl/queue/" + requestId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + + // Server wraps response: {"success": true, "data": {...}} + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), HITLApprovalRequest.class); + } + return objectMapper.treeToValue(node, HITLApprovalRequest.class); + } + }, + "getHITLRequest"); + } + + /** + * Asynchronously gets a specific HITL approval request by ID. + * + * @param requestId the approval request ID + * @return a future containing the approval request + */ + public CompletableFuture getHITLRequestAsync(String requestId) { + return CompletableFuture.supplyAsync(() -> getHITLRequest(requestId), asyncExecutor); + } + + /** + * Approves a HITL approval request. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @param requestId the approval request ID + * @param review the review input containing reviewer details + * @throws AxonFlowException if the approval fails + */ + public void approveHITLRequest(String requestId, HITLReviewInput review) { + Objects.requireNonNull(requestId, "requestId cannot be null"); + Objects.requireNonNull(review, "review cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", "/api/v1/hitl/queue/" + requestId + "/approve", review); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "approveHITLRequest"); + } + + /** + * Asynchronously approves a HITL approval request. + * + * @param requestId the approval request ID + * @param review the review input containing reviewer details + * @return a future that completes when the request has been approved + */ + public CompletableFuture approveHITLRequestAsync(String requestId, HITLReviewInput review) { + return CompletableFuture.runAsync(() -> approveHITLRequest(requestId, review), asyncExecutor); + } + + /** + * Rejects a HITL approval request. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @param requestId the approval request ID + * @param review the review input containing reviewer details + * @throws AxonFlowException if the rejection fails + */ + public void rejectHITLRequest(String requestId, HITLReviewInput review) { + Objects.requireNonNull(requestId, "requestId cannot be null"); + Objects.requireNonNull(review, "review cannot be null"); + + retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", "/api/v1/hitl/queue/" + requestId + "/reject", review); + try (Response response = httpClient.newCall(httpRequest).execute()) { + if (!response.isSuccessful()) { + handleErrorResponse(response); + } + return null; + } + }, + "rejectHITLRequest"); + } + + /** + * Asynchronously rejects a HITL approval request. + * + * @param requestId the approval request ID + * @param review the review input containing reviewer details + * @return a future that completes when the request has been rejected + */ + public CompletableFuture rejectHITLRequestAsync(String requestId, HITLReviewInput review) { + return CompletableFuture.runAsync(() -> rejectHITLRequest(requestId, review), asyncExecutor); + } + + /** + * Gets HITL dashboard statistics. + * + *

Returns aggregate statistics about the HITL queue including total pending requests, priority + * breakdowns, and age metrics. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + * + * @return the dashboard statistics + * @throws AxonFlowException if the request fails + */ + public HITLStats getHITLStats() { + return retryExecutor.execute( + () -> { + Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/hitl/stats", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + JsonNode node = parseResponseNode(response); + + // Server wraps response: {"success": true, "data": {...}} + if (node.has("data") && node.get("data").isObject()) { + return objectMapper.treeToValue(node.get("data"), HITLStats.class); + } + return objectMapper.treeToValue(node, HITLStats.class); + } + }, + "getHITLStats"); + } + + /** + * Asynchronously gets HITL dashboard statistics. + * + * @return a future containing the dashboard statistics + */ + public CompletableFuture getHITLStatsAsync() { + return CompletableFuture.supplyAsync(this::getHITLStats, asyncExecutor); + } + + // ======================================================================== + // MAS FEAT Namespace Inner Class + // ======================================================================== + + /** + * MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, Accountability, Transparency) + * compliance namespace. + * + *

Provides methods for AI system registry, FEAT assessments, and kill switch management for + * Singapore financial services compliance. + * + *

Enterprise Feature: Requires AxonFlow Enterprise license. + */ + public final class MASFEATNamespace { + + private static final String BASE_PATH = "/api/v1/masfeat"; + + /** + * Registers a new AI system in the MAS FEAT registry. + * + * @param request the registration request + * @return the registered system + */ + public AISystemRegistry registerSystem(RegisterSystemRequest request) { + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + // Map SDK field names to backend field names + Map body = new HashMap<>(); + body.put("system_id", request.getSystemId()); + body.put("system_name", request.getSystemName()); + if (request.getDescription() != null) { + body.put("description", request.getDescription()); + } + if (request.getUseCase() != null) { + body.put("use_case", request.getUseCase().getValue()); + } + body.put("owner_team", request.getOwnerTeam()); + if (request.getTechnicalOwner() != null) { + body.put("technical_owner", request.getTechnicalOwner()); + } + // businessOwner maps to owner_email + if (request.getBusinessOwner() != null) { + body.put("owner_email", request.getBusinessOwner()); + } + // Risk rating fields + body.put("risk_rating_impact", request.getCustomerImpact()); + body.put("risk_rating_complexity", request.getModelComplexity()); + body.put("risk_rating_reliance", request.getHumanReliance()); + if (request.getMetadata() != null) { + body.put("metadata", request.getMetadata()); + } + + Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/registry", body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), CircuitBreakerStatusResponse.class); - } - return objectMapper.treeToValue(node, CircuitBreakerStatusResponse.class); + return parseSystemResponse(response); } - }, "getCircuitBreakerStatus"); + }, + "masfeat.registerSystem"); } /** - * Asynchronously gets the current circuit breaker status. + * Activates an AI system (changes status to 'active'). * - * @return a future containing the circuit breaker status + * @param systemId the system UUID (not the systemId string) + * @return the activated system */ - public CompletableFuture getCircuitBreakerStatusAsync() { - return CompletableFuture.supplyAsync(this::getCircuitBreakerStatus, asyncExecutor); + public AISystemRegistry activateSystem(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("status", "active"); + + Request httpRequest = + buildOrchestratorRequest("PUT", BASE_PATH + "/registry/" + systemId, body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSystemResponse(response); + } + }, + "masfeat.activateSystem"); } /** - * Gets the circuit breaker history, including past trips and resets. - * - *

Example usage: - *

{@code
-     * CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50);
-     * for (CircuitBreakerHistoryEntry entry : history.getHistory()) {
-     *     System.out.println(entry.getScope() + "/" + entry.getScopeId() + " - " + entry.getState());
-     * }
-     * }
+ * Gets an AI system by its UUID. * - * @param limit the maximum number of history entries to return - * @return the circuit breaker history - * @throws IllegalArgumentException if limit is less than 1 - * @throws AxonFlowException if the request fails + * @param systemId the system UUID + * @return the system */ - public CircuitBreakerHistoryResponse getCircuitBreakerHistory(int limit) { - if (limit < 1) { - throw new IllegalArgumentException("limit must be at least 1"); - } + public AISystemRegistry getSystem(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); - return retryExecutor.execute(() -> { - String path = "/api/v1/circuit-breaker/history?limit=" + limit; - Request httpRequest = buildOrchestratorRequest("GET", path, null); + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", BASE_PATH + "/registry/" + systemId, null); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), CircuitBreakerHistoryResponse.class); - } - return objectMapper.treeToValue(node, CircuitBreakerHistoryResponse.class); + return parseSystemResponse(response); } - }, "getCircuitBreakerHistory"); + }, + "masfeat.getSystem"); } /** - * Asynchronously gets the circuit breaker history. + * Gets the registry summary statistics. * - * @param limit the maximum number of history entries to return - * @return a future containing the circuit breaker history + * @return the registry summary */ - public CompletableFuture getCircuitBreakerHistoryAsync(int limit) { - return CompletableFuture.supplyAsync(() -> getCircuitBreakerHistory(limit), asyncExecutor); + public RegistrySummary getRegistrySummary() { + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", BASE_PATH + "/registry/summary", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseSummaryResponse(response); + } + }, + "masfeat.getRegistrySummary"); } /** - * Gets the circuit breaker configuration for a specific tenant. - * - *

Example usage: - *

{@code
-     * CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123");
-     * System.out.println("Error threshold: " + config.getErrorThreshold());
-     * System.out.println("Auto recovery: " + config.isEnableAutoRecovery());
-     * }
+ * Creates a new FEAT assessment. * - * @param tenantId the tenant ID to get configuration for - * @return the circuit breaker configuration - * @throws NullPointerException if tenantId is null - * @throws IllegalArgumentException if tenantId is empty - * @throws AxonFlowException if the request fails + * @param request the assessment creation request + * @return the created assessment */ - public CircuitBreakerConfig getCircuitBreakerConfig(String tenantId) { - Objects.requireNonNull(tenantId, "tenantId cannot be null"); - if (tenantId.isEmpty()) { - throw new IllegalArgumentException("tenantId cannot be empty"); - } + public FEATAssessment createAssessment(CreateAssessmentRequest request) { + Objects.requireNonNull(request, "request cannot be null"); - return retryExecutor.execute(() -> { - String path = "/api/v1/circuit-breaker/config?tenant_id=" + java.net.URLEncoder.encode(tenantId, java.nio.charset.StandardCharsets.UTF_8); - Request httpRequest = buildOrchestratorRequest("GET", path, null); + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("system_id", request.getSystemId()); + body.put("assessment_type", request.getAssessmentType()); + if (request.getAssessors() != null) { + body.put("assessors", request.getAssessors()); + } + + Request httpRequest = + buildOrchestratorRequest("POST", BASE_PATH + "/assessments", body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfig.class); - } - return objectMapper.treeToValue(node, CircuitBreakerConfig.class); + return parseAssessmentResponse(response); } - }, "getCircuitBreakerConfig"); + }, + "masfeat.createAssessment"); } /** - * Asynchronously gets the circuit breaker configuration for a specific tenant. + * Gets a FEAT assessment by its ID. * - * @param tenantId the tenant ID to get configuration for - * @return a future containing the circuit breaker configuration + * @param assessmentId the assessment ID + * @return the assessment */ - public CompletableFuture getCircuitBreakerConfigAsync(String tenantId) { - return CompletableFuture.supplyAsync(() -> getCircuitBreakerConfig(tenantId), asyncExecutor); + public FEATAssessment getAssessment(String assessmentId) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", BASE_PATH + "/assessments/" + assessmentId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, + "masfeat.getAssessment"); } /** - * Updates the circuit breaker configuration for a tenant. - * - *

Example usage: - *

{@code
-     * CircuitBreakerConfig updated = axonflow.updateCircuitBreakerConfig(
-     *     CircuitBreakerConfigUpdate.builder()
-     *         .tenantId("tenant_123")
-     *         .errorThreshold(10)
-     *         .violationThreshold(5)
-     *         .enableAutoRecovery(true)
-     *         .build());
-     * }
+ * Updates a FEAT assessment with pillar scores and details. * - * @param config the configuration update - * @return confirmation with tenant_id and message - * @throws NullPointerException if config is null - * @throws AxonFlowException if the request fails + * @param assessmentId the assessment ID + * @param request the update request + * @return the updated assessment */ - public CircuitBreakerConfigUpdateResponse updateCircuitBreakerConfig(CircuitBreakerConfigUpdate config) { - Objects.requireNonNull(config, "config cannot be null"); + public FEATAssessment updateAssessment(String assessmentId, UpdateAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + if (request.getFairnessScore() != null) { + body.put("fairness_score", request.getFairnessScore()); + } + if (request.getEthicsScore() != null) { + body.put("ethics_score", request.getEthicsScore()); + } + if (request.getAccountabilityScore() != null) { + body.put("accountability_score", request.getAccountabilityScore()); + } + if (request.getTransparencyScore() != null) { + body.put("transparency_score", request.getTransparencyScore()); + } + if (request.getFairnessDetails() != null) { + body.put("fairness_details", request.getFairnessDetails()); + } + if (request.getEthicsDetails() != null) { + body.put("ethics_details", request.getEthicsDetails()); + } + if (request.getAccountabilityDetails() != null) { + body.put("accountability_details", request.getAccountabilityDetails()); + } + if (request.getTransparencyDetails() != null) { + body.put("transparency_details", request.getTransparencyDetails()); + } + if (request.getFindings() != null) { + body.put("findings", request.getFindings()); + } + if (request.getRecommendations() != null) { + body.put("recommendations", request.getRecommendations()); + } + if (request.getAssessors() != null) { + body.put("assessors", request.getAssessors()); + } - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/circuit-breaker/config", config); + Request httpRequest = + buildOrchestratorRequest("PUT", BASE_PATH + "/assessments/" + assessmentId, body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), CircuitBreakerConfigUpdateResponse.class); - } - return objectMapper.treeToValue(node, CircuitBreakerConfigUpdateResponse.class); + return parseAssessmentResponse(response); } - }, "updateCircuitBreakerConfig"); + }, + "masfeat.updateAssessment"); } /** - * Asynchronously updates the circuit breaker configuration for a tenant. + * Submits a FEAT assessment for review. * - * @param config the configuration update - * @return a future containing the update confirmation + * @param assessmentId the assessment ID + * @return the submitted assessment */ - public CompletableFuture updateCircuitBreakerConfigAsync(CircuitBreakerConfigUpdate config) { - return CompletableFuture.supplyAsync(() -> updateCircuitBreakerConfig(config), asyncExecutor); - } + public FEATAssessment submitAssessment(String assessmentId) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - // ======================================================================== - // Policy Simulation - // ======================================================================== + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/assessments/" + assessmentId + "/submit", null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseAssessmentResponse(response); + } + }, + "masfeat.submitAssessment"); + } /** - * Simulates policy evaluation against a query without actually enforcing policies. - * - *

This is a dry-run mode that shows which policies would match and what actions - * would be taken, without blocking the request. + * Approves a FEAT assessment. * - *

Example usage: - *

{@code
-     * SimulatePoliciesResponse result = axonflow.simulatePolicies(
-     *     SimulatePoliciesRequest.builder()
-     *         .query("Transfer $50,000 to external account")
-     *         .requestType("execute")
-     *         .build());
-     * System.out.println("Allowed: " + result.isAllowed());
-     * System.out.println("Applied policies: " + result.getAppliedPolicies());
-     * System.out.println("Risk score: " + result.getRiskScore());
-     * }
- * - *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. - * - * @param request the simulation request - * @return the simulation result - * @throws NullPointerException if request is null - * @throws AxonFlowException if the request fails + * @param assessmentId the assessment ID + * @param request the approval request + * @return the approved assessment */ - public SimulatePoliciesResponse simulatePolicies(SimulatePoliciesRequest request) { - Objects.requireNonNull(request, "request cannot be null"); + public FEATAssessment approveAssessment(String assessmentId, ApproveAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("approved_by", request.getApprovedBy()); + if (request.getComments() != null) { + body.put("comments", request.getComments()); + } - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/simulate", request); + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/assessments/" + assessmentId + "/approve", body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), SimulatePoliciesResponse.class); - } - return objectMapper.treeToValue(node, SimulatePoliciesResponse.class); + return parseAssessmentResponse(response); } - }, "simulatePolicies"); + }, + "masfeat.approveAssessment"); } /** - * Asynchronously simulates policy evaluation against a query. + * Rejects a FEAT assessment. * - * @param request the simulation request - * @return a future containing the simulation result + * @param assessmentId the assessment ID + * @param request the rejection request + * @return the rejected assessment */ - public CompletableFuture simulatePoliciesAsync(SimulatePoliciesRequest request) { - return CompletableFuture.supplyAsync(() -> simulatePolicies(request), asyncExecutor); - } + public FEATAssessment rejectAssessment(String assessmentId, RejectAssessmentRequest request) { + Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); - /** - * Generates a policy impact report by testing a set of inputs against a specific policy. - * - *

This helps you understand how a policy would affect real traffic before deploying it. - * - *

Example usage: - *

{@code
-     * ImpactReportResponse report = axonflow.getPolicyImpactReport(
-     *     ImpactReportRequest.builder()
-     *         .policyId("policy_block_pii")
-     *         .inputs(List.of(
-     *             ImpactReportInput.builder().query("My SSN is 123-45-6789").build(),
-     *             ImpactReportInput.builder().query("What is the weather?").build()))
-     *         .build());
-     * System.out.println("Match rate: " + report.getMatchRate());
-     * System.out.println("Block rate: " + report.getBlockRate());
-     * }
- * - *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. - * - * @param request the impact report request - * @return the impact report - * @throws NullPointerException if request is null - * @throws AxonFlowException if the request fails - */ - public ImpactReportResponse getPolicyImpactReport(ImpactReportRequest request) { - Objects.requireNonNull(request, "request cannot be null"); + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("rejected_by", request.getRejectedBy()); + body.put("reason", request.getReason()); - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/impact-report", request); + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/assessments/" + assessmentId + "/reject", body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), ImpactReportResponse.class); - } - return objectMapper.treeToValue(node, ImpactReportResponse.class); + return parseAssessmentResponse(response); } - }, "getPolicyImpactReport"); + }, + "masfeat.rejectAssessment"); } /** - * Asynchronously generates a policy impact report. + * Gets the kill switch configuration for an AI system. * - * @param request the impact report request - * @return a future containing the impact report + * @param systemId the system ID (string ID, not UUID) + * @return the kill switch configuration */ - public CompletableFuture getPolicyImpactReportAsync(ImpactReportRequest request) { - return CompletableFuture.supplyAsync(() -> getPolicyImpactReport(request), asyncExecutor); + public KillSwitch getKillSwitch(String systemId) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute( + () -> { + Request httpRequest = + buildOrchestratorRequest("GET", BASE_PATH + "/killswitch/" + systemId, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, + "masfeat.getKillSwitch"); } /** - * Scans all active policies for conflicts. - * - *

Example usage: - *

{@code
-     * PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts();
-     * System.out.println("Conflicts found: " + conflicts.getConflictCount());
-     * for (PolicyConflict conflict : conflicts.getConflicts()) {
-     *     System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
-     * }
-     * }
- * - *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. + * Configures the kill switch for an AI system. * - * @return the conflict detection result - * @throws AxonFlowException if the request fails + * @param systemId the system ID (string ID, not UUID) + * @param request the configuration request + * @return the configured kill switch */ - public PolicyConflictResponse detectPolicyConflicts() { - return detectPolicyConflicts(null); + public KillSwitch configureKillSwitch(String systemId, ConfigureKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + if (request.getAccuracyThreshold() != null) { + body.put("accuracy_threshold", request.getAccuracyThreshold()); + } + if (request.getBiasThreshold() != null) { + body.put("bias_threshold", request.getBiasThreshold()); + } + if (request.getErrorRateThreshold() != null) { + body.put("error_rate_threshold", request.getErrorRateThreshold()); + } + if (request.getAutoTriggerEnabled() != null) { + body.put("auto_trigger_enabled", request.getAutoTriggerEnabled()); + } + + // Note: configure uses POST, not PUT + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/killswitch/" + systemId + "/configure", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, + "masfeat.configureKillSwitch"); } /** - * Detects conflicts between a specific policy and other active policies, - * or scans all policies if policyId is null. - * - *

Example usage: - *

{@code
-     * PolicyConflictResponse conflicts = axonflow.detectPolicyConflicts("policy_block_pii");
-     * System.out.println("Conflicts found: " + conflicts.getConflictCount());
-     * for (PolicyConflict conflict : conflicts.getConflicts()) {
-     *     System.out.println(conflict.getConflictType() + ": " + conflict.getDescription());
-     * }
-     * }
+ * Triggers the kill switch for an AI system. * - *

Evaluation+ Feature: Requires AxonFlow Evaluation tier or higher. - * - * @param policyId the policy ID to check for conflicts, or null to scan all policies - * @return the conflict detection result - * @throws IllegalArgumentException if policyId is non-null and empty - * @throws AxonFlowException if the request fails + * @param systemId the system ID (string ID, not UUID) + * @param request the trigger request + * @return the triggered kill switch */ - public PolicyConflictResponse detectPolicyConflicts(String policyId) { - if (policyId != null && policyId.isEmpty()) { - throw new IllegalArgumentException("policyId cannot be empty"); - } + public KillSwitch triggerKillSwitch(String systemId, TriggerKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); - return retryExecutor.execute(() -> { - Object body; - if (policyId != null) { - body = java.util.Map.of("policy_id", policyId); - } else { - body = java.util.Map.of(); + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("reason", request.getReason()); + if (request.getTriggeredBy() != null) { + body.put("triggered_by", request.getTriggeredBy()); } - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/policies/conflicts", body); + + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/killswitch/" + systemId + "/trigger", body); try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), PolicyConflictResponse.class); - } - return objectMapper.treeToValue(node, PolicyConflictResponse.class); + return parseKillSwitchResponse(response); } - }, "detectPolicyConflicts"); + }, + "masfeat.triggerKillSwitch"); } /** - * Asynchronously scans all active policies for conflicts. + * Restores the kill switch for an AI system after remediation. * - * @return a future containing the conflict detection result + * @param systemId the system ID (string ID, not UUID) + * @param request the restore request + * @return the restored kill switch */ - public CompletableFuture detectPolicyConflictsAsync() { - return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(), asyncExecutor); + public KillSwitch restoreKillSwitch(String systemId, RestoreKillSwitchRequest request) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + Objects.requireNonNull(request, "request cannot be null"); + + return retryExecutor.execute( + () -> { + Map body = new HashMap<>(); + body.put("reason", request.getReason()); + if (request.getRestoredBy() != null) { + body.put("restored_by", request.getRestoredBy()); + } + + Request httpRequest = + buildOrchestratorRequest( + "POST", BASE_PATH + "/killswitch/" + systemId + "/restore", body); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchResponse(response); + } + }, + "masfeat.restoreKillSwitch"); } /** - * Asynchronously detects conflicts between a specific policy and other active policies. + * Gets the kill switch event history for an AI system. * - * @param policyId the policy ID to check for conflicts, or null to scan all policies - * @return a future containing the conflict detection result + * @param systemId the system ID (string ID, not UUID) + * @param limit maximum number of events to return + * @return list of kill switch events */ - public CompletableFuture detectPolicyConflictsAsync(String policyId) { - return CompletableFuture.supplyAsync(() -> detectPolicyConflicts(policyId), asyncExecutor); + public List getKillSwitchHistory(String systemId, int limit) { + Objects.requireNonNull(systemId, "systemId cannot be null"); + + return retryExecutor.execute( + () -> { + String path = BASE_PATH + "/killswitch/" + systemId + "/history"; + if (limit > 0) { + path += "?limit=" + limit; + } + + Request httpRequest = buildOrchestratorRequest("GET", path, null); + try (Response response = httpClient.newCall(httpRequest).execute()) { + return parseKillSwitchHistoryResponse(response); + } + }, + "masfeat.getKillSwitchHistory"); } // ======================================================================== - // Proxy Mode - Query Execution + // Response Parsing Helpers // ======================================================================== - /** - * Sends a query through AxonFlow with full policy enforcement (Proxy Mode). - * - *

This is Proxy Mode - AxonFlow acts as an intermediary, making the LLM call on your behalf. - * - *

Use this when you want AxonFlow to: - *

    - *
  • Evaluate policies before the LLM call
  • - *
  • Make the LLM call to the configured provider
  • - *
  • Filter/redact sensitive data from responses
  • - *
  • Automatically track costs and audit the interaction
  • - *
- * - *

For Gateway Mode (lower latency, you make the LLM call), use: - *

    - *
  • {@link #getPolicyApprovedContext} before your LLM call
  • - *
  • {@link #auditLLMCall} after your LLM call
  • - *
- * - * @param request the client request - * @return the response from AxonFlow - * @throws PolicyViolationException if the request is blocked by policy - * @throws AuthenticationException if authentication fails - */ - public ClientResponse proxyLLMCall(ClientRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - // Auto-populate clientId from config if not set in request (matches Go/Python/TypeScript SDK behavior) - ClientRequest effectiveRequest = request; - if ((request.getClientId() == null || request.getClientId().isEmpty()) - && config.getClientId() != null && !config.getClientId().isEmpty()) { - effectiveRequest = ClientRequest.builder() - .query(request.getQuery()) - .userToken(request.getUserToken()) - .clientId(config.getClientId()) - .requestType(request.getRequestType() != null - ? RequestType.fromValue(request.getRequestType()) - : RequestType.CHAT) - .context(request.getContext()) - .llmProvider(request.getLlmProvider()) - .model(request.getModel()) - .media(request.getMedia()) - .build(); + private AISystemRegistry parseSystemResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + AISystemRegistry system = new AISystemRegistry(); + system.setId(getTextOrNull(node, "id")); + system.setOrgId(getTextOrNull(node, "org_id")); + system.setSystemId(getTextOrNull(node, "system_id")); + system.setSystemName(getTextOrNull(node, "system_name")); + system.setDescription(getTextOrNull(node, "description")); + system.setOwnerTeam(getTextOrNull(node, "owner_team")); + system.setTechnicalOwner(getTextOrNull(node, "technical_owner")); + system.setBusinessOwner(getTextOrNull(node, "owner_email")); + system.setCreatedBy(getTextOrNull(node, "created_by")); + + // Handle use_case enum + String useCase = getTextOrNull(node, "use_case"); + if (useCase != null) { + try { + system.setUseCase(AISystemUseCase.fromValue(useCase)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown use case: {}", useCase); } + } + + // Handle risk ratings + system.setCustomerImpact(getIntOrZero(node, "risk_rating_impact")); + system.setModelComplexity(getIntOrZero(node, "risk_rating_complexity")); + system.setHumanReliance(getIntOrZero(node, "risk_rating_reliance")); + + // Handle materiality (may be "materiality" or "materiality_classification") + String materiality = getTextOrNull(node, "materiality"); + if (materiality == null) { + materiality = getTextOrNull(node, "materiality_classification"); + } + if (materiality != null) { + try { + system.setMateriality(MaterialityClassification.fromValue(materiality)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown materiality: {}", materiality); + } + } - final ClientRequest finalRequest = effectiveRequest; - - // Media requests must not be cached — binary content makes cache keys unreliable - boolean hasMedia = finalRequest.getMedia() != null && !finalRequest.getMedia().isEmpty(); + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { + try { + system.setStatus(SystemStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown status: {}", status); + } + } - // Check cache first (skip for media requests) - String cacheKey = ResponseCache.generateKey( - finalRequest.getRequestType(), - finalRequest.getQuery(), - finalRequest.getUserToken() - ); + // Handle timestamps + system.setCreatedAt(parseInstant(node, "created_at")); + system.setUpdatedAt(parseInstant(node, "updated_at")); - if (!hasMedia) { - java.util.Optional cached = cache.get(cacheKey, ClientResponse.class); - if (cached.isPresent()) { - return cached.get(); - } - } + // Handle metadata + if (node.has("metadata") && !node.get("metadata").isNull()) { + system.setMetadata( + objectMapper.convertValue( + node.get("metadata"), new TypeReference>() {})); + } - ClientResponse response = retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", "/api/request", finalRequest); - try (Response httpResponse = httpClient.newCall(httpRequest).execute()) { - ClientResponse result = parseResponse(httpResponse, ClientResponse.class); + return system; + } - if (result.isBlocked()) { - throw new PolicyViolationException( - result.getBlockReason(), - result.getBlockingPolicyName(), - result.getPolicyInfo() != null - ? result.getPolicyInfo().getPoliciesEvaluated() - : null - ); - } + private RegistrySummary parseSummaryResponse(Response response) throws IOException { + handleErrorResponse(response); - return result; - } - }, "proxyLLMCall"); + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } - // Cache successful responses (skip for media requests) - if (!hasMedia && response.isSuccess() && !response.isBlocked()) { - cache.put(cacheKey, response); - } + String json = body.string(); + JsonNode node = objectMapper.readTree(json); - return response; - } + RegistrySummary summary = new RegistrySummary(); + summary.setTotalSystems(getIntOrZero(node, "total_systems")); + summary.setActiveSystems(getIntOrZero(node, "active_systems")); - /** - * Asynchronously sends a query through AxonFlow with full policy enforcement (Proxy Mode). - * - * @param request the client request - * @return a future containing the response - * @see #proxyLLMCall(ClientRequest) - */ - public CompletableFuture proxyLLMCallAsync(ClientRequest request) { - return CompletableFuture.supplyAsync(() -> proxyLLMCall(request), asyncExecutor); - } + // Handle high_materiality_count (may be "high_materiality_count" or "high_materiality") + int highMateriality = getIntOrZero(node, "high_materiality_count"); + if (highMateriality == 0) { + highMateriality = getIntOrZero(node, "high_materiality"); + } + summary.setHighMaterialityCount(highMateriality); - // ======================================================================== - // Multi-Agent Planning (MAP) - // ======================================================================== + summary.setMediumMaterialityCount(getIntOrZero(node, "medium_materiality_count")); + summary.setLowMaterialityCount(getIntOrZero(node, "low_materiality_count")); - /** - * Generates a multi-agent plan for a complex task. - * - *

This method uses the Agent API with request_type "multi-agent-plan" - * to generate and execute plans through the governance layer. - * - * @param request the plan request - * @return the generated plan - * @throws PlanExecutionException if plan generation fails - */ - public PlanResponse generatePlan(PlanRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - // Build agent request format - use HashMap to allow null-safe values - String userToken = request.getUserToken(); - if (userToken == null) { - userToken = config.getClientId() != null ? config.getClientId() : "default"; - } - String clientId = config.getClientId() != null ? config.getClientId() : "default"; - String domain = request.getDomain() != null ? request.getDomain() : "generic"; + if (node.has("by_use_case") && !node.get("by_use_case").isNull()) { + summary.setByUseCase( + objectMapper.convertValue( + node.get("by_use_case"), new TypeReference>() {})); + } - Map agentRequest = new java.util.HashMap<>(); - agentRequest.put("query", request.getObjective()); - agentRequest.put("user_token", userToken); - agentRequest.put("client_id", clientId); - agentRequest.put("request_type", "multi-agent-plan"); - agentRequest.put("context", Map.of("domain", domain)); + if (node.has("by_status") && !node.get("by_status").isNull()) { + summary.setByStatus( + objectMapper.convertValue( + node.get("by_status"), new TypeReference>() {})); + } - Request httpRequest = buildRequest("POST", "/api/request", agentRequest); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parsePlanResponse(response, request.getDomain()); - } - }, "generatePlan"); + return summary; } - /** - * Parses the Agent API response format into PlanResponse. - * The Agent API returns: {success, plan_id, data: {steps, domain, ...}, metadata, result} - */ - @SuppressWarnings("unchecked") - private PlanResponse parsePlanResponse(Response response, String requestDomain) throws IOException { - handleErrorResponse(response); + private FEATAssessment parseAssessmentResponse(Response response) throws IOException { + handleErrorResponse(response); - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } - String json = body.string(); - Map agentResponse = objectMapper.readValue(json, - new TypeReference>() {}); + String json = body.string(); + JsonNode node = objectMapper.readTree(json); - // Check for errors - Boolean success = (Boolean) agentResponse.get("success"); - if (success == null || !success) { - String error = (String) agentResponse.get("error"); - throw new PlanExecutionException(error != null ? error : "Plan generation failed"); - } + FEATAssessment assessment = new FEATAssessment(); + assessment.setId(getTextOrNull(node, "id")); + assessment.setOrgId(getTextOrNull(node, "org_id")); + assessment.setSystemId(getTextOrNull(node, "system_id")); + assessment.setAssessmentType(getTextOrNull(node, "assessment_type")); + assessment.setApprovedBy(getTextOrNull(node, "approved_by")); + assessment.setCreatedBy(getTextOrNull(node, "created_by")); - // Extract fields from Agent API response format - String planId = (String) agentResponse.get("plan_id"); - Map data = (Map) agentResponse.get("data"); - Map metadata = (Map) agentResponse.get("metadata"); - String result = (String) agentResponse.get("result"); - - // Extract nested fields from data - List steps = Collections.emptyList(); - String domain = requestDomain != null ? requestDomain : "generic"; - Integer complexity = null; - Boolean parallel = null; - String estimatedDuration = null; - - if (data != null) { - // Parse steps if present - List> rawSteps = (List>) data.get("steps"); - if (rawSteps != null) { - steps = rawSteps.stream() - .map(stepMap -> objectMapper.convertValue(stepMap, PlanStep.class)) - .collect(java.util.stream.Collectors.toList()); - } - domain = data.get("domain") != null ? (String) data.get("domain") : domain; - complexity = data.get("complexity") != null ? ((Number) data.get("complexity")).intValue() : null; - parallel = (Boolean) data.get("parallel"); - estimatedDuration = (String) data.get("estimated_duration"); - } - - return new PlanResponse(planId, steps, domain, complexity, parallel, - estimatedDuration, metadata, null, result); - } - - /** - * Asynchronously generates a multi-agent plan. - * - * @param request the plan request - * @return a future containing the generated plan - */ - public CompletableFuture generatePlanAsync(PlanRequest request) { - return CompletableFuture.supplyAsync(() -> generatePlan(request), asyncExecutor); - } - - /** - * Executes a previously generated plan. - * - * @param planId the ID of the plan to execute - * @return the execution result - * @throws PlanExecutionException if execution fails - */ - public PlanResponse executePlan(String planId) { - return executePlan(planId, null); - } - - /** - * Executes a previously generated plan with an explicit user token. - * - * @param planId the ID of the plan to execute - * @param userToken the user token (JWT) for authentication; if null, defaults to clientId - * @return the execution result - * @throws PlanExecutionException if execution fails - */ - public PlanResponse executePlan(String planId, String userToken) { - Objects.requireNonNull(planId, "planId cannot be null"); - - // executePlan is a mutation — do NOT retry (retrying causes 409 "Plan has already been executed") + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { try { - // Build agent request format - like generatePlan but with request_type "execute-plan" - String token = userToken != null ? userToken : (config.getClientId() != null ? config.getClientId() : "default"); - String clientId = config.getClientId() != null ? config.getClientId() : "default"; - - Map agentRequest = new java.util.HashMap<>(); - agentRequest.put("query", ""); - agentRequest.put("user_token", token); - agentRequest.put("client_id", clientId); - agentRequest.put("request_type", "execute-plan"); - agentRequest.put("context", Map.of("plan_id", planId)); - - Request httpRequest = buildRequest("POST", "/api/request", agentRequest); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseExecutePlanResponse(response, planId); - } - } catch (AxonFlowException e) { - throw e; - } catch (Exception e) { - throw new PlanExecutionException("executePlan failed: " + e.getMessage(), planId, null, e); + assessment.setStatus(FEATAssessmentStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown assessment status: {}", status); } - } - - /** - * Parses the execute plan response. - */ - @SuppressWarnings("unchecked") - private PlanResponse parseExecutePlanResponse(Response response, String planId) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); + } + + // Handle scores + assessment.setFairnessScore(getIntegerOrNull(node, "fairness_score")); + assessment.setEthicsScore(getIntegerOrNull(node, "ethics_score")); + assessment.setAccountabilityScore(getIntegerOrNull(node, "accountability_score")); + assessment.setTransparencyScore(getIntegerOrNull(node, "transparency_score")); + + // Overall score may be int or float + if (node.has("overall_score") && !node.get("overall_score").isNull()) { + JsonNode scoreNode = node.get("overall_score"); + if (scoreNode.isNumber()) { + assessment.setOverallScore(scoreNode.asInt()); } - - String json = body.string(); - Map agentResponse = objectMapper.readValue(json, - new TypeReference>() {}); - - // Check for errors (outer response) - Boolean success = (Boolean) agentResponse.get("success"); - - // Detect nested data.success=false (agent wraps orchestrator errors) - Object dataObj = agentResponse.get("data"); - if (dataObj instanceof Map) { - @SuppressWarnings("unchecked") - Map dataMap = (Map) dataObj; - Boolean dataSuccess = (Boolean) dataMap.get("success"); - if (dataSuccess != null && !dataSuccess) { - success = false; - String dataError = (String) dataMap.get("error"); - if (dataError != null) { - throw new PlanExecutionException(dataError); - } - } + } + + // Handle timestamps + assessment.setAssessmentDate(parseInstant(node, "assessment_date")); + assessment.setValidUntil(parseInstant(node, "valid_until")); + assessment.setApprovedAt(parseInstant(node, "approved_at")); + assessment.setCreatedAt(parseInstant(node, "created_at")); + assessment.setUpdatedAt(parseInstant(node, "updated_at")); + + // Handle details + if (node.has("fairness_details") && !node.get("fairness_details").isNull()) { + assessment.setFairnessDetails( + objectMapper.convertValue( + node.get("fairness_details"), new TypeReference>() {})); + } + if (node.has("ethics_details") && !node.get("ethics_details").isNull()) { + assessment.setEthicsDetails( + objectMapper.convertValue( + node.get("ethics_details"), new TypeReference>() {})); + } + if (node.has("accountability_details") && !node.get("accountability_details").isNull()) { + assessment.setAccountabilityDetails( + objectMapper.convertValue( + node.get("accountability_details"), new TypeReference>() {})); + } + if (node.has("transparency_details") && !node.get("transparency_details").isNull()) { + assessment.setTransparencyDetails( + objectMapper.convertValue( + node.get("transparency_details"), new TypeReference>() {})); + } + + // Handle assessors + if (node.has("assessors") && node.get("assessors").isArray()) { + assessment.setAssessors( + objectMapper.convertValue(node.get("assessors"), new TypeReference>() {})); + } + + // Handle recommendations + if (node.has("recommendations") && node.get("recommendations").isArray()) { + assessment.setRecommendations( + objectMapper.convertValue( + node.get("recommendations"), new TypeReference>() {})); + } + + return assessment; + } + + private KillSwitch parseKillSwitchResponse(Response response) throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + // Handle nested response: {"kill_switch": {...}, "message": "..."} + if (node.has("kill_switch") && !node.get("kill_switch").isNull()) { + node = node.get("kill_switch"); + } + + KillSwitch ks = new KillSwitch(); + ks.setId(getTextOrNull(node, "id")); + ks.setOrgId(getTextOrNull(node, "org_id")); + ks.setSystemId(getTextOrNull(node, "system_id")); + ks.setTriggeredBy(getTextOrNull(node, "triggered_by")); + ks.setRestoredBy(getTextOrNull(node, "restored_by")); + + // Handle triggered_reason (may be "triggered_reason" or "trigger_reason") + String triggeredReason = getTextOrNull(node, "triggered_reason"); + if (triggeredReason == null) { + triggeredReason = getTextOrNull(node, "trigger_reason"); + } + ks.setTriggeredReason(triggeredReason); + + // Handle status + String status = getTextOrNull(node, "status"); + if (status != null) { + try { + ks.setStatus(KillSwitchStatus.fromValue(status)); + } catch (IllegalArgumentException e) { + logger.warn("Unknown kill switch status: {}", status); } - - if (success == null || !success) { - String error = (String) agentResponse.get("error"); - throw new PlanExecutionException(error != null ? error : "Plan execution failed"); + } + + // Handle auto_trigger + if (node.has("auto_trigger_enabled") && !node.get("auto_trigger_enabled").isNull()) { + ks.setAutoTriggerEnabled(node.get("auto_trigger_enabled").asBoolean()); + } + + // Handle thresholds + ks.setAccuracyThreshold(getDoubleOrNull(node, "accuracy_threshold")); + ks.setBiasThreshold(getDoubleOrNull(node, "bias_threshold")); + ks.setErrorRateThreshold(getDoubleOrNull(node, "error_rate_threshold")); + + // Handle timestamps + ks.setTriggeredAt(parseInstant(node, "triggered_at")); + ks.setRestoredAt(parseInstant(node, "restored_at")); + ks.setCreatedAt(parseInstant(node, "created_at")); + ks.setUpdatedAt(parseInstant(node, "updated_at")); + + return ks; + } + + private List parseKillSwitchHistoryResponse(Response response) + throws IOException { + handleErrorResponse(response); + + ResponseBody body = response.body(); + if (body == null) { + throw new AxonFlowException("Empty response body", response.code(), null); + } + + String json = body.string(); + JsonNode node = objectMapper.readTree(json); + + // Handle nested response: {"history": [...]} or direct array + JsonNode eventsNode; + if (node.has("history") && node.get("history").isArray()) { + eventsNode = node.get("history"); + } else if (node.has("events") && node.get("events").isArray()) { + eventsNode = node.get("events"); + } else if (node.isArray()) { + eventsNode = node; + } else { + return new ArrayList<>(); + } + + List events = new ArrayList<>(); + for (JsonNode eventNode : eventsNode) { + KillSwitchEvent event = new KillSwitchEvent(); + event.setId(getTextOrNull(eventNode, "id")); + event.setKillSwitchId(getTextOrNull(eventNode, "kill_switch_id")); + + // Handle event_type (may be "event_type" or "action") + String eventType = getTextOrNull(eventNode, "event_type"); + if (eventType == null) { + eventType = getTextOrNull(eventNode, "action"); } + event.setEventType(eventType); - // Extract result - this is the completed plan output - String result = (String) agentResponse.get("result"); - - // Read status from response data (e.g., "awaiting_approval" for confirm mode) - // Precedence: data.status > metadata.status > top-level status > "completed" - String status = "completed"; - Object dataObj2 = agentResponse.get("data"); - if (dataObj2 instanceof Map) { - @SuppressWarnings("unchecked") - Map dm = (Map) dataObj2; - Object dataStatus = dm.get("status"); - if (dataStatus instanceof String && !((String) dataStatus).isEmpty()) { - status = (String) dataStatus; - } - } - if ("completed".equals(status)) { - Object metaObj = agentResponse.get("metadata"); - if (metaObj instanceof Map) { - @SuppressWarnings("unchecked") - Map metaMap = (Map) metaObj; - Object metaStatus = metaMap.get("status"); - if (metaStatus instanceof String && !((String) metaStatus).isEmpty()) { - status = (String) metaStatus; - } - } - } - if ("completed".equals(status)) { - Object topStatus = agentResponse.get("status"); - if (topStatus instanceof String && !((String) topStatus).isEmpty()) { - status = (String) topStatus; - } + // Handle created_by (may be "created_by" or "performed_by") + String createdBy = getTextOrNull(eventNode, "created_by"); + if (createdBy == null) { + createdBy = getTextOrNull(eventNode, "performed_by"); } + event.setCreatedBy(createdBy); - // Build response with execution status - return new PlanResponse(planId, Collections.emptyList(), null, null, null, - null, null, status, result); - } - - /** - * Gets the status of a plan. - * - * @param planId the plan ID - * @return the plan status - */ - public PlanResponse getPlanStatus(String planId) { - Objects.requireNonNull(planId, "planId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", - "/api/v1/plan/" + planId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, PlanResponse.class); - } - }, "getPlanStatus"); - } - - /** - * Generates a multi-agent plan with additional options. - * - *

This overload allows specifying execution mode and other generation - * options beyond what is in the base {@link PlanRequest}. - * - * @param request the plan request - * @param options additional generation options - * @return the generated plan - * @throws PlanExecutionException if plan generation fails - */ - public PlanResponse generatePlan(PlanRequest request, GeneratePlanOptions options) { - Objects.requireNonNull(request, "request cannot be null"); - Objects.requireNonNull(options, "options cannot be null"); - - return retryExecutor.execute(() -> { - // Build agent request format - use HashMap to allow null-safe values - String userToken = request.getUserToken(); - if (userToken == null) { - userToken = config.getClientId() != null ? config.getClientId() : "default"; - } - String clientId = config.getClientId() != null ? config.getClientId() : "default"; - String domain = request.getDomain() != null ? request.getDomain() : "generic"; - - Map context = new java.util.HashMap<>(); - context.put("domain", domain); - if (options.getExecutionMode() != null) { - context.put("execution_mode", options.getExecutionMode().getValue()); - } - - Map agentRequest = new java.util.HashMap<>(); - agentRequest.put("query", request.getObjective()); - agentRequest.put("user_token", userToken); - agentRequest.put("client_id", clientId); - agentRequest.put("request_type", "multi-agent-plan"); - agentRequest.put("context", context); - - Request httpRequest = buildRequest("POST", "/api/request", agentRequest); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parsePlanResponse(response, request.getDomain()); - } - }, "generatePlan"); - } - - /** - * Cancels a running or pending plan. - * - * @param planId the ID of the plan to cancel - * @param reason an optional reason for the cancellation - * @return the cancellation result - */ - public CancelPlanResponse cancelPlan(String planId, String reason) { - Objects.requireNonNull(planId, "planId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new java.util.HashMap<>(); - if (reason != null) { - body.put("reason", reason); - } - - Request httpRequest = buildRequest("POST", - "/api/v1/plan/" + planId + "/cancel", body.isEmpty() ? null : body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, CancelPlanResponse.class); - } - }, "cancelPlan"); - } - - /** - * Cancels a running or pending plan without specifying a reason. - * - * @param planId the ID of the plan to cancel - * @return the cancellation result - */ - public CancelPlanResponse cancelPlan(String planId) { - return cancelPlan(planId, null); - } - - /** - * Updates a plan with optimistic concurrency control. - * - *

The request must include the expected version number. If the version - * does not match the current server version, a {@link VersionConflictException} - * is thrown. - * - * @param planId the ID of the plan to update - * @param request the update request with version and changes - * @return the update result - * @throws VersionConflictException if the plan version has changed - */ - public UpdatePlanResponse updatePlan(String planId, UpdatePlanRequest request) { - Objects.requireNonNull(planId, "planId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - try { - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("PUT", - "/api/v1/plan/" + planId, request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, UpdatePlanResponse.class); - } - }, "updatePlan"); - } catch (AxonFlowException e) { - if (e.getStatusCode() == 409) { - throw new VersionConflictException( - e.getMessage(), planId, request.getVersion(), null); - } - throw e; + // Handle created_at (may be "created_at" or "performed_at") + java.time.Instant createdAt = parseInstant(eventNode, "created_at"); + if (createdAt == null) { + createdAt = parseInstant(eventNode, "performed_at"); } - } - - /** - * Gets the version history of a plan. - * - * @param planId the plan ID - * @return the version history - */ - public PlanVersionsResponse getPlanVersions(String planId) { - Objects.requireNonNull(planId, "planId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", - "/api/v1/plan/" + planId + "/versions", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, PlanVersionsResponse.class); - } - }, "getPlanVersions"); - } - - /** - * Resumes a paused plan, optionally approving or rejecting it. - * - * @param planId the ID of the plan to resume - * @param approved whether to approve the plan to continue (true) or reject it (false) - * @return the resume result - */ - public ResumePlanResponse resumePlan(String planId, Boolean approved) { - Objects.requireNonNull(planId, "planId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new java.util.HashMap<>(); - body.put("approved", approved != null ? approved : true); - - Request httpRequest = buildRequest("POST", - "/api/v1/plan/" + planId + "/resume", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ResumePlanResponse.class); - } - }, "resumePlan"); - } - - /** - * Resumes a paused plan with approval (default). - * - *

This is equivalent to calling {@code resumePlan(planId, true)}. - * - * @param planId the ID of the plan to resume - * @return the resume result - */ - public ResumePlanResponse resumePlan(String planId) { - return resumePlan(planId, true); - } + event.setCreatedAt(createdAt); - /** - * Rolls back a plan to a previous version. - * - * @param planId the ID of the plan to roll back - * @param targetVersion the version number to roll back to - * @return the rollback result - * @throws AxonFlowException if the rollback fails - */ - public RollbackPlanResponse rollbackPlan(String planId, int targetVersion) { - Objects.requireNonNull(planId, "planId cannot be null"); + // Handle event_data + if (eventNode.has("event_data") && !eventNode.get("event_data").isNull()) { + event.setEventData( + objectMapper.convertValue( + eventNode.get("event_data"), new TypeReference>() {})); + } else { + // Build event_data from individual fields if present + Map eventData = new HashMap<>(); + String prevStatus = getTextOrNull(eventNode, "previous_status"); + String newStatus = getTextOrNull(eventNode, "new_status"); + String reason = getTextOrNull(eventNode, "reason"); + if (prevStatus != null) eventData.put("previous_status", prevStatus); + if (newStatus != null) eventData.put("new_status", newStatus); + if (reason != null) eventData.put("reason", reason); + if (!eventData.isEmpty()) { + event.setEventData(eventData); + } + } - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", - "/api/v1/plan/" + planId + "/rollback/" + targetVersion, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, RollbackPlanResponse.class); - } - }, "rollbackPlan"); - } + events.add(event); + } - /** - * Asynchronously rolls back a plan to a previous version. - * - * @param planId the ID of the plan to roll back - * @param targetVersion the version number to roll back to - * @return a future containing the rollback result - */ - public CompletableFuture rollbackPlanAsync(String planId, int targetVersion) { - return CompletableFuture.supplyAsync(() -> rollbackPlan(planId, targetVersion), asyncExecutor); + return events; } // ======================================================================== - // MCP Connectors + // JSON Helper Methods // ======================================================================== - /** - * Lists available MCP connectors. - * - * @return list of available connectors - */ - public List listConnectors() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/connectors", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Response is wrapped: {"connectors": [...], "total": N} - JsonNode node = parseResponseNode(response); - if (node.has("connectors")) { - return objectMapper.convertValue( - node.get("connectors"), - new TypeReference>() {} - ); - } - return objectMapper.convertValue(node, new TypeReference>() {}); - } - }, "listConnectors"); - } - - /** - * Asynchronously lists available MCP connectors. - * - * @return a future containing the list of connectors - */ - public CompletableFuture> listConnectorsAsync() { - return CompletableFuture.supplyAsync(this::listConnectors, asyncExecutor); - } - - /** - * Installs an MCP connector. - * - * @param connectorId the connector ID to install - * @param config the connector configuration - * @return the installed connector info - */ - public ConnectorInfo installConnector(String connectorId, Map config) { - Objects.requireNonNull(connectorId, "connectorId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = Map.of( - "config", config != null ? config : Map.of() - ); - String path = "/api/v1/connectors/" + connectorId + "/install"; - Request httpRequest = buildOrchestratorRequest("POST", path, body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ConnectorInfo.class); - } - }, "installConnector"); - } - - /** - * Uninstalls an MCP connector. - * - * @param connectorName the name of the connector to uninstall - */ - public void uninstallConnector(String connectorName) { - Objects.requireNonNull(connectorName, "connectorName cannot be null"); - - retryExecutor.execute(() -> { - String path = "/api/v1/connectors/" + connectorName; - Request httpRequest = buildOrchestratorRequest("DELETE", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "uninstallConnector"); - } - - /** - * Gets details for a specific connector by ID. - * - * @param connectorId the connector ID - * @return the connector info - */ - public ConnectorInfo getConnector(String connectorId) { - Objects.requireNonNull(connectorId, "connectorId cannot be null"); - - return retryExecutor.execute(() -> { - String path = "/api/v1/connectors/" + connectorId; - Request httpRequest = buildOrchestratorRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ConnectorInfo.class); - } - }, "getConnector"); - } - - /** - * Asynchronously gets details for a specific connector by ID. - * - * @param connectorId the connector ID - * @return a future containing the connector info - */ - public CompletableFuture getConnectorAsync(String connectorId) { - return CompletableFuture.supplyAsync(() -> getConnector(connectorId), asyncExecutor); - } - - /** - * Gets the health status of an installed connector. - * - * @param connectorId the connector ID - * @return the health status - */ - public ConnectorHealthStatus getConnectorHealth(String connectorId) { - Objects.requireNonNull(connectorId, "connectorId cannot be null"); - - return retryExecutor.execute(() -> { - String path = "/api/v1/connectors/" + connectorId + "/health"; - Request httpRequest = buildOrchestratorRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ConnectorHealthStatus.class); - } - }, "getConnectorHealth"); - } - - /** - * Asynchronously gets the health status of an installed connector. - * - * @param connectorId the connector ID - * @return a future containing the health status - */ - public CompletableFuture getConnectorHealthAsync(String connectorId) { - return CompletableFuture.supplyAsync(() -> getConnectorHealth(connectorId), asyncExecutor); - } - - /** - * Queries an MCP connector. - * - *

This method sends the query to the AxonFlow Agent using the standard - * request format with request_type: "mcp-query", which is routed to the - * configured MCP connector. - * - * @param query the connector query - * @return the query response - * @throws ConnectorException if the query fails - */ - public ConnectorResponse queryConnector(ConnectorQuery query) { - Objects.requireNonNull(query, "query cannot be null"); - - return retryExecutor.execute(() -> { - // Build a ClientRequest with MCP_QUERY request type - // This follows the same pattern as Go and TypeScript SDKs - Map context = new HashMap<>(); - context.put("connector", query.getConnectorId()); - if (query.getParameters() != null && !query.getParameters().isEmpty()) { - context.put("params", query.getParameters()); - } - - String clientId = config.getClientId(); - - ClientRequest clientRequest = ClientRequest.builder() - .query(query.getOperation()) - .userToken(query.getUserToken() != null ? query.getUserToken() : clientId) - .clientId(clientId) - .requestType(RequestType.MCP_QUERY) - .context(context) - .build(); - - Request httpRequest = buildRequest("POST", "/api/request", clientRequest); - try (Response response = httpClient.newCall(httpRequest).execute()) { - ClientResponse clientResponse = parseResponse(response, ClientResponse.class); - - // Convert ClientResponse to ConnectorResponse - ConnectorResponse result = new ConnectorResponse( - clientResponse.isSuccess(), - clientResponse.getData(), - clientResponse.getError(), - query.getConnectorId(), - query.getOperation(), - null, // processingTime not available from ClientResponse - false, // redacted - not available from this endpoint - null, // redactedFields - not available from this endpoint - null // policyInfo - not available from this endpoint - ); - - if (!result.isSuccess()) { - throw new ConnectorException( - result.getError(), - query.getConnectorId(), - query.getOperation() - ); - } - - return result; - } - }, "queryConnector"); - } - - /** - * Asynchronously queries an MCP connector. - * - * @param query the connector query - * @return a future containing the response - */ - public CompletableFuture queryConnectorAsync(ConnectorQuery query) { - return CompletableFuture.supplyAsync(() -> queryConnector(query), asyncExecutor); - } - - /** - * Executes a query directly against the MCP connector endpoint. - * - *

This method calls the agent's /mcp/resources/query endpoint which provides: - *

    - *
  • Request-phase policy evaluation (SQLi blocking, PII blocking)
  • - *
  • Response-phase policy evaluation (PII redaction)
  • - *
  • PolicyInfo metadata in responses
  • - *
- * - *

Example usage: - *

-     * ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM customers LIMIT 10");
-     * if (response.isRedacted()) {
-     *     System.out.println("Fields redacted: " + response.getRedactedFields());
-     * }
-     * System.out.println("Policies evaluated: " + response.getPolicyInfo().getPoliciesEvaluated());
-     * 
- * - * @param connector name of the MCP connector (e.g., "postgres") - * @param statement SQL statement or query to execute - * @return ConnectorResponse with data, redaction info, and policy_info - * @throws ConnectorException if the request is blocked by policy or fails - */ - public ConnectorResponse mcpQuery(String connector, String statement) { - return mcpQuery(connector, statement, null); - } - - /** - * Executes a query directly against the MCP connector endpoint with options. - * - * @param connector name of the MCP connector (e.g., "postgres") - * @param statement SQL statement or query to execute - * @param options optional additional options for the query - * @return ConnectorResponse with data, redaction info, and policy_info - * @throws ConnectorException if the request is blocked by policy or fails - */ - public ConnectorResponse mcpQuery(String connector, String statement, Map options) { - Objects.requireNonNull(connector, "connector cannot be null"); - Objects.requireNonNull(statement, "statement cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("connector", connector); - body.put("statement", statement); - if (options != null && !options.isEmpty()) { - body.put("options", options); - } - - Request httpRequest = buildRequest("POST", "/mcp/resources/query", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Parse the response body - ResponseBody responseBody = response.body(); - if (responseBody == null) { - throw new ConnectorException("Empty response from MCP query", connector, "mcpQuery"); - } - String responseJson = responseBody.string(); - - // Handle policy blocks (403 responses) - if (!response.isSuccessful()) { - try { - Map errorData = objectMapper.readValue(responseJson, - new com.fasterxml.jackson.core.type.TypeReference>() {}); - String errorMsg = errorData.get("error") != null ? - errorData.get("error").toString() : - "MCP query failed: " + response.code(); - throw new ConnectorException(errorMsg, connector, "mcpQuery"); - } catch (JsonProcessingException e) { - throw new ConnectorException("MCP query failed: " + response.code(), connector, "mcpQuery"); - } - } - - return objectMapper.readValue(responseJson, ConnectorResponse.class); - } - }, "mcpQuery"); - } - - /** - * Asynchronously executes a query against the MCP connector endpoint. - * - * @param connector name of the MCP connector - * @param statement SQL statement to execute - * @return a future containing the response - */ - public CompletableFuture mcpQueryAsync(String connector, String statement) { - return CompletableFuture.supplyAsync(() -> mcpQuery(connector, statement), asyncExecutor); + private String getTextOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asText(); + } + return null; } - /** - * Asynchronously executes a query against the MCP connector endpoint with options. - * - * @param connector name of the MCP connector - * @param statement SQL statement to execute - * @param options optional additional options - * @return a future containing the response - */ - public CompletableFuture mcpQueryAsync(String connector, String statement, Map options) { - return CompletableFuture.supplyAsync(() -> mcpQuery(connector, statement, options), asyncExecutor); + private int getIntOrZero(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asInt(); + } + return 0; } - /** - * Executes a statement against an MCP connector (alias for mcpQuery). - * - * @param connector name of the MCP connector - * @param statement SQL statement to execute - * @return ConnectorResponse with data, redaction info, and policy_info - */ - public ConnectorResponse mcpExecute(String connector, String statement) { - return mcpQuery(connector, statement); + private Integer getIntegerOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asInt(); + } + return null; } - // ======================================================================== - // MCP Policy Check (Standalone) - // ======================================================================== - - /** - * Validates an MCP input statement against configured policies without executing it. - * - *

This method calls the agent's {@code /api/v1/mcp/check-input} endpoint to pre-validate - * a statement before sending it to the connector. Useful for checking SQL injection - * patterns, blocked operations, and input policy violations.

- * - *

Example usage: - *

{@code
-     * MCPCheckInputResponse result = axonflow.mcpCheckInput("postgres", "SELECT * FROM users");
-     * if (!result.isAllowed()) {
-     *     System.out.println("Blocked: " + result.getBlockReason());
-     * }
-     * }
- * - * @param connectorType name of the MCP connector type (e.g., "postgres") - * @param statement the statement to validate - * @return MCPCheckInputResponse with allowed status, block reason, and policy info - * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) - */ - public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement) { - return mcpCheckInput(connectorType, statement, null); + private Double getDoubleOrNull(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + return node.get(field).asDouble(); + } + return null; } - /** - * Validates an MCP input statement against configured policies with options. - * - * @param connectorType name of the MCP connector type (e.g., "postgres") - * @param statement the statement to validate - * @param options optional parameters: "operation" (String), "parameters" (Map) - * @return MCPCheckInputResponse with allowed status, block reason, and policy info - * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) - */ - public MCPCheckInputResponse mcpCheckInput(String connectorType, String statement, Map options) { - Objects.requireNonNull(connectorType, "connectorType cannot be null"); - Objects.requireNonNull(statement, "statement cannot be null"); - - return retryExecutor.execute(() -> { - MCPCheckInputRequest request; - if (options != null) { - String operation = (String) options.getOrDefault("operation", "execute"); - @SuppressWarnings("unchecked") - Map parameters = (Map) options.get("parameters"); - request = new MCPCheckInputRequest(connectorType, statement, parameters, operation); - } else { - request = new MCPCheckInputRequest(connectorType, statement); - } - - Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-input", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - ResponseBody responseBody = response.body(); - if (responseBody == null) { - throw new ConnectorException("Empty response from MCP check-input", connectorType, "mcpCheckInput"); - } - String responseJson = responseBody.string(); - - // 403 means policy blocked — the body is still a valid response - if (!response.isSuccessful() && response.code() != 403) { - try { - Map errorData = objectMapper.readValue(responseJson, - new TypeReference>() {}); - String errorMsg = errorData.get("error") != null ? - errorData.get("error").toString() : - "MCP check-input failed: " + response.code(); - throw new ConnectorException(errorMsg, connectorType, "mcpCheckInput"); - } catch (JsonProcessingException e) { - throw new ConnectorException("MCP check-input failed: " + response.code(), connectorType, "mcpCheckInput"); - } - } - - return objectMapper.readValue(responseJson, MCPCheckInputResponse.class); - } - }, "mcpCheckInput"); - } - - /** - * Asynchronously validates an MCP input statement against configured policies. - * - * @param connectorType name of the MCP connector type - * @param statement the statement to validate - * @return a future containing the check result - */ - public CompletableFuture mcpCheckInputAsync(String connectorType, String statement) { - return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement), asyncExecutor); - } - - /** - * Asynchronously validates an MCP input statement against configured policies with options. - * - * @param connectorType name of the MCP connector type - * @param statement the statement to validate - * @param options optional parameters - * @return a future containing the check result - */ - public CompletableFuture mcpCheckInputAsync(String connectorType, String statement, Map options) { - return CompletableFuture.supplyAsync(() -> mcpCheckInput(connectorType, statement, options), asyncExecutor); - } - - /** - * Validates MCP response data against configured policies. - * - *

This method calls the agent's {@code /api/v1/mcp/check-output} endpoint to check - * response data for PII content, exfiltration limit violations, and other output - * policy violations. If PII redaction is active, {@code redactedData} contains the - * sanitized version.

- * - *

Example usage: - *

{@code
-     * List> rows = List.of(
-     *     Map.of("name", "John", "ssn", "123-45-6789")
-     * );
-     * MCPCheckOutputResponse result = axonflow.mcpCheckOutput("postgres", rows);
-     * if (!result.isAllowed()) {
-     *     System.out.println("Blocked: " + result.getBlockReason());
-     * }
-     * if (result.getRedactedData() != null) {
-     *     System.out.println("Redacted: " + result.getRedactedData());
-     * }
-     * }
- * - * @param connectorType name of the MCP connector type (e.g., "postgres") - * @param responseData the response data rows to validate - * @return MCPCheckOutputResponse with allowed status, redacted data, and policy info - * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) - */ - public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List> responseData) { - return mcpCheckOutput(connectorType, responseData, null); - } - - /** - * Validates MCP response data against configured policies with options. - * - * @param connectorType name of the MCP connector type (e.g., "postgres") - * @param responseData the response data rows to validate - * @param options optional parameters: "message" (String), "metadata" (Map), "row_count" (int) - * @return MCPCheckOutputResponse with allowed status, redacted data, and policy info - * @throws ConnectorException if the request fails (note: 403 is not an error, it means blocked) - */ - public MCPCheckOutputResponse mcpCheckOutput(String connectorType, List> responseData, Map options) { - Objects.requireNonNull(connectorType, "connectorType cannot be null"); - // responseData can be null for execute-style requests that use message instead - - return retryExecutor.execute(() -> { - String message = options != null ? (String) options.get("message") : null; - @SuppressWarnings("unchecked") - Map metadata = options != null ? (Map) options.get("metadata") : null; - int rowCount = options != null ? (int) options.getOrDefault("row_count", 0) : 0; - - MCPCheckOutputRequest request = new MCPCheckOutputRequest(connectorType, responseData, message, metadata, rowCount); - - Request httpRequest = buildRequest("POST", "/api/v1/mcp/check-output", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - ResponseBody responseBody = response.body(); - if (responseBody == null) { - throw new ConnectorException("Empty response from MCP check-output", connectorType, "mcpCheckOutput"); - } - String responseJson = responseBody.string(); - - // 403 means policy blocked — the body is still a valid response - if (!response.isSuccessful() && response.code() != 403) { - try { - Map errorData = objectMapper.readValue(responseJson, - new TypeReference>() {}); - String errorMsg = errorData.get("error") != null ? - errorData.get("error").toString() : - "MCP check-output failed: " + response.code(); - throw new ConnectorException(errorMsg, connectorType, "mcpCheckOutput"); - } catch (JsonProcessingException e) { - throw new ConnectorException("MCP check-output failed: " + response.code(), connectorType, "mcpCheckOutput"); - } - } - - return objectMapper.readValue(responseJson, MCPCheckOutputResponse.class); - } - }, "mcpCheckOutput"); - } - - /** - * Asynchronously validates MCP response data against configured policies. - * - * @param connectorType name of the MCP connector type - * @param responseData the response data rows to validate - * @return a future containing the check result - */ - public CompletableFuture mcpCheckOutputAsync(String connectorType, List> responseData) { - return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData), asyncExecutor); - } - - /** - * Asynchronously validates MCP response data against configured policies with options. - * - * @param connectorType name of the MCP connector type - * @param responseData the response data rows to validate - * @param options optional parameters - * @return a future containing the check result - */ - public CompletableFuture mcpCheckOutputAsync(String connectorType, List> responseData, Map options) { - return CompletableFuture.supplyAsync(() -> mcpCheckOutput(connectorType, responseData, options), asyncExecutor); - } - - // ======================================================================== - // Policy CRUD - Static Policies - // ======================================================================== - - /** - * Lists static policies with optional filtering. - * - * @return list of static policies - */ - public List listStaticPolicies() { - return listStaticPolicies((ListStaticPoliciesOptions) null); - } - - /** - * Lists static policies with filtering options. - * - * @param options filtering options - * @return list of static policies - */ - public List listStaticPolicies(ListStaticPoliciesOptions options) { - return retryExecutor.execute(() -> { - String path = buildPolicyQueryString("/api/v1/static-policies", options); - Request httpRequest = buildRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - StaticPoliciesResponse wrapper = parseResponse(response, StaticPoliciesResponse.class); - // Handle null wrapper or null policies list (Issue #40) - if (wrapper == null || wrapper.getPolicies() == null) { - return java.util.Collections.emptyList(); - } - return wrapper.getPolicies(); - } - }, "listStaticPolicies"); - } - - /** - * Lists static policies filtered by tier and organization ID (Enterprise). - * - * @param tier the policy tier - * @param organizationId the organization ID - * @return list of static policies - */ - public List listStaticPolicies(PolicyTier tier, String organizationId) { - return listStaticPolicies(ListStaticPoliciesOptions.builder() - .tier(tier) - .organizationId(organizationId) - .build()); - } - - /** - * Lists static policies filtered by tier and category. - * - * @param tier the policy tier - * @param category the policy category - * @return list of static policies - */ - public List listStaticPolicies(PolicyTier tier, PolicyCategory category) { - return listStaticPolicies(ListStaticPoliciesOptions.builder() - .tier(tier) - .category(category) - .build()); - } - - /** - * Lists static policies filtered by category. - * - * @param category the policy category - * @return list of static policies - */ - public List listStaticPolicies(PolicyCategory category) { - return listStaticPolicies(ListStaticPoliciesOptions.builder() - .category(category) - .build()); - } - - /** - * Gets a specific static policy by ID. - * - * @param policyId the policy ID - * @return the static policy - */ - public StaticPolicy getStaticPolicy(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", "/api/v1/static-policies/" + policyId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, StaticPolicy.class); - } - }, "getStaticPolicy"); - } - - /** - * Creates a new static policy. - * - * @param request the create request - * @return the created policy - */ - public StaticPolicy createStaticPolicy(CreateStaticPolicyRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", "/api/v1/static-policies", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, StaticPolicy.class); - } - }, "createStaticPolicy"); - } - - /** - * Updates an existing static policy. - * - * @param policyId the policy ID - * @param request the update request - * @return the updated policy - */ - public StaticPolicy updateStaticPolicy(String policyId, UpdateStaticPolicyRequest request) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("PUT", "/api/v1/static-policies/" + policyId, request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, StaticPolicy.class); - } - }, "updateStaticPolicy"); - } - - /** - * Deletes a static policy. - * - * @param policyId the policy ID - */ - public void deleteStaticPolicy(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildRequest("DELETE", "/api/v1/static-policies/" + policyId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "deleteStaticPolicy"); - } - - /** - * Toggles a static policy's enabled status. - * - * @param policyId the policy ID - * @param enabled the new enabled status - * @return the updated policy - */ - public StaticPolicy toggleStaticPolicy(String policyId, boolean enabled) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = Map.of("enabled", enabled); - Request httpRequest = buildPatchRequest("/api/v1/static-policies/" + policyId, body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, StaticPolicy.class); - } - }, "toggleStaticPolicy"); - } - - /** - * Gets effective static policies after inheritance and overrides. - * - * @return list of effective policies - */ - public List getEffectiveStaticPolicies() { - return getEffectiveStaticPolicies((EffectivePoliciesOptions) null); - } - - /** - * Gets effective static policies filtered by category. - * - * @param category the policy category - * @return list of effective policies - */ - public List getEffectiveStaticPolicies(PolicyCategory category) { - return getEffectiveStaticPolicies(EffectivePoliciesOptions.builder() - .category(category) - .build()); - } - - /** - * Gets effective static policies with options. - * - * @param options filtering options - * @return list of effective policies - */ - public List getEffectiveStaticPolicies(EffectivePoliciesOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/static-policies/effective"); - if (options != null) { - String query = buildEffectivePoliciesQuery(options); - if (!query.isEmpty()) { - path.append("?").append(query); - } - } - Request httpRequest = buildRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - EffectivePoliciesResponse wrapper = parseResponse(response, EffectivePoliciesResponse.class); - // Handle null wrapper or null policies list (Issue #40) - if (wrapper == null || wrapper.getStaticPolicies() == null) { - return java.util.Collections.emptyList(); - } - return wrapper.getStaticPolicies(); - } - }, "getEffectiveStaticPolicies"); - } - - /** - * Tests a regex pattern against sample inputs. - * - * @param pattern the regex pattern - * @param testInputs sample inputs to test - * @return the test result - */ - public TestPatternResult testPattern(String pattern, List testInputs) { - Objects.requireNonNull(pattern, "pattern cannot be null"); - Objects.requireNonNull(testInputs, "testInputs cannot be null"); - - return retryExecutor.execute(() -> { - Map body = Map.of( - "pattern", pattern, - "inputs", testInputs - ); - Request httpRequest = buildRequest("POST", "/api/v1/static-policies/test", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, TestPatternResult.class); - } - }, "testPattern"); - } - - /** - * Gets version history for a static policy. - * - * @param policyId the policy ID - * @return list of policy versions - */ - public List getStaticPolicyVersions(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", "/api/v1/static-policies/" + policyId + "/versions", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - Map wrapper = parseResponse(response, new TypeReference>() {}); - @SuppressWarnings("unchecked") - List> versionsRaw = (List>) wrapper.get("versions"); - if (versionsRaw == null) { - return new ArrayList<>(); - } - List versions = new ArrayList<>(); - for (Map v : versionsRaw) { - PolicyVersion version = objectMapper.convertValue(v, PolicyVersion.class); - versions.add(version); - } - return versions; - } - }, "getStaticPolicyVersions"); - } - - // ======================================================================== - // Policy CRUD - Overrides (Enterprise) - // ======================================================================== - - /** - * Creates a policy override. - * - * @param policyId the policy ID - * @param request the override request - * @return the created override - */ - public PolicyOverride createPolicyOverride(String policyId, CreatePolicyOverrideRequest request) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("POST", "/api/v1/static-policies/" + policyId + "/override", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, PolicyOverride.class); - } - }, "createPolicyOverride"); - } - - /** - * Deletes a policy override. - * - * @param policyId the policy ID - */ - public void deletePolicyOverride(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildRequest("DELETE", "/api/v1/static-policies/" + policyId + "/override", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "deletePolicyOverride"); - } - - /** - * Lists all active policy overrides (Enterprise). - * - * @return list of policy overrides - */ - public List listPolicyOverrides() { - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", "/api/v1/static-policies/overrides", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Backend returns wrapped response: {"overrides": [...], "count": N} - Map wrapper = parseResponse(response, new TypeReference>() {}); - @SuppressWarnings("unchecked") - List> overridesRaw = (List>) wrapper.get("overrides"); - if (overridesRaw == null) { - return java.util.Collections.emptyList(); - } - return overridesRaw.stream() - .map(raw -> objectMapper.convertValue(raw, PolicyOverride.class)) - .collect(java.util.stream.Collectors.toList()); - } - }, "listPolicyOverrides"); - } - - // ======================================================================== - // Policy CRUD - Dynamic Policies - // ======================================================================== - - /** - * Lists dynamic policies. - * - * @return list of dynamic policies - */ - public List listDynamicPolicies() { - return listDynamicPolicies(null); - } - - /** - * Lists dynamic policies with filtering options. - * - * @param options filtering options - * @return list of dynamic policies - */ - public List listDynamicPolicies(ListDynamicPoliciesOptions options) { - return retryExecutor.execute(() -> { - String path = buildDynamicPolicyQueryString("/api/v1/dynamic-policies", options); - Request httpRequest = buildOrchestratorRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policies": [...]} wrapper - DynamicPoliciesResponse wrapper = parseResponse(response, DynamicPoliciesResponse.class); - // Handle null wrapper or null policies list (Issue #40) - if (wrapper == null || wrapper.getPolicies() == null) { - return java.util.Collections.emptyList(); - } - return wrapper.getPolicies(); - } - }, "listDynamicPolicies"); - } - - /** - * Gets a specific dynamic policy by ID. - * - * @param policyId the policy ID - * @return the dynamic policy - */ - public DynamicPolicy getDynamicPolicy(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/dynamic-policies/" + policyId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); - return wrapper != null ? wrapper.getPolicy() : null; - } - }, "getDynamicPolicy"); - } - - /** - * Creates a new dynamic policy. - * - * @param request the create request - * @return the created policy - */ - public DynamicPolicy createDynamicPolicy(CreateDynamicPolicyRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/dynamic-policies", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); - return wrapper != null ? wrapper.getPolicy() : null; - } - }, "createDynamicPolicy"); - } - - /** - * Updates an existing dynamic policy. - * - * @param policyId the policy ID - * @param request the update request - * @return the updated policy - */ - public DynamicPolicy updateDynamicPolicy(String policyId, UpdateDynamicPolicyRequest request) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); - return wrapper != null ? wrapper.getPolicy() : null; - } - }, "updateDynamicPolicy"); - } - - /** - * Deletes a dynamic policy. - * - * @param policyId the policy ID - */ - public void deleteDynamicPolicy(String policyId) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("DELETE", "/api/v1/dynamic-policies/" + policyId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "deleteDynamicPolicy"); - } - - /** - * Toggles a dynamic policy's enabled status. - * - * @param policyId the policy ID - * @param enabled the new enabled status - * @return the updated policy - */ - public DynamicPolicy toggleDynamicPolicy(String policyId, boolean enabled) { - Objects.requireNonNull(policyId, "policyId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = Map.of("enabled", enabled); - Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/dynamic-policies/" + policyId, body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - DynamicPolicyResponse wrapper = parseResponse(response, DynamicPolicyResponse.class); - return wrapper != null ? wrapper.getPolicy() : null; - } - }, "toggleDynamicPolicy"); - } - - /** - * Gets effective dynamic policies after inheritance. - * - * @return list of effective policies - */ - public List getEffectiveDynamicPolicies() { - return getEffectiveDynamicPolicies(null); - } - - /** - * Gets effective dynamic policies with options. - * - * @param options filtering options - * @return list of effective policies - */ - public List getEffectiveDynamicPolicies(EffectivePoliciesOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/dynamic-policies/effective"); - if (options != null) { - String query = buildEffectivePoliciesQuery(options); - if (!query.isEmpty()) { - path.append("?").append(query); - } - } - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - // Agent proxy (Issue #886) returns {"policies": [...]} wrapper - DynamicPoliciesResponse wrapper = parseResponse(response, DynamicPoliciesResponse.class); - // Handle null wrapper or null policies list (Issue #40) - if (wrapper == null || wrapper.getPolicies() == null) { - return java.util.Collections.emptyList(); - } - return wrapper.getPolicies(); - } - }, "getEffectiveDynamicPolicies"); - } - - // ======================================================================== - // Unified Execution Tracking (Issue #1075 - EPIC #1074) - // ======================================================================== - - /** - * Gets the unified execution status for a given execution ID. - * - *

This method works for both MAP plans and WCP workflows, returning - * a consistent status format regardless of execution type. - * - * @param executionId the execution ID (plan ID or workflow ID) - * @return the unified execution status - */ - public com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus getExecutionStatus(String executionId) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/unified/executions/" + executionId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus.class); - } - }, "getExecutionStatus"); - } - - /** - * Lists unified executions with optional filtering. - * - * @param request filter options - * @return paginated list of executions - */ - public com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsResponse listUnifiedExecutions( - com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsRequest request) { - - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/unified/executions"); - if (request != null) { - StringBuilder params = new StringBuilder(); - if (request.getExecutionType() != null) { - params.append("execution_type=").append(request.getExecutionType().getValue()); - } - if (request.getStatus() != null) { - if (params.length() > 0) params.append("&"); - params.append("status=").append(request.getStatus().getValue()); - } - if (request.getTenantId() != null) { - if (params.length() > 0) params.append("&"); - params.append("tenant_id=").append(request.getTenantId()); - } - if (request.getOrgId() != null) { - if (params.length() > 0) params.append("&"); - params.append("org_id=").append(request.getOrgId()); - } - if (request.getLimit() > 0) { - if (params.length() > 0) params.append("&"); - params.append("limit=").append(request.getLimit()); - } - if (request.getOffset() > 0) { - if (params.length() > 0) params.append("&"); - params.append("offset=").append(request.getOffset()); - } - if (params.length() > 0) { - path.append("?").append(params); - } - } - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, com.getaxonflow.sdk.types.execution.ExecutionTypes.UnifiedListExecutionsResponse.class); - } - }, "listUnifiedExecutions"); - } - - /** - * Cancels a unified execution (MAP plan or WCP workflow). - * - *

This method cancels an execution via the unified execution API, - * automatically propagating to the correct subsystem (MAP or WCP). - * - * @param executionId the execution ID (plan ID or workflow ID) - * @param reason optional reason for cancellation - */ - public void cancelExecution(String executionId, String reason) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - retryExecutor.execute(() -> { - Map body = reason != null ? - Collections.singletonMap("reason", reason) : Collections.emptyMap(); - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/unified/executions/" + executionId + "/cancel", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "cancelExecution"); - } - - /** - * Cancels a unified execution without a reason. - * - * @param executionId the execution ID - */ - public void cancelExecution(String executionId) { - cancelExecution(executionId, null); - } - - /** - * Streams real-time execution status updates via Server-Sent Events (SSE). - * - *

Connects to the SSE streaming endpoint and invokes the callback with each - * {@link com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus} update - * as it arrives. The stream automatically closes when the execution reaches a - * terminal state (completed, failed, cancelled, aborted, or expired). - * - *

Example usage: - *

{@code
-     * axonflow.streamExecutionStatus("exec_123", status -> {
-     *     System.out.printf("Progress: %.0f%% - Status: %s%n",
-     *         status.getProgressPercent(), status.getStatus().getValue());
-     *     if (status.getCurrentStep() != null) {
-     *         System.out.println("  Current step: " + status.getCurrentStep().getStepName());
-     *     }
-     * });
-     * }
- * - * @param executionId the execution ID (plan ID or workflow ID) - * @param callback consumer invoked with each ExecutionStatus update - * @throws AxonFlowException if the connection fails or an I/O error occurs - * @throws AuthenticationException if authentication fails (401/403) - */ - public void streamExecutionStatus( - String executionId, - Consumer callback) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - Objects.requireNonNull(callback, "callback cannot be null"); - - logger.debug("Streaming execution status for {}", executionId); - - HttpUrl url = HttpUrl.parse(config.getEndpoint() + "/api/v1/unified/executions/" + executionId + "/stream"); - if (url == null) { - throw new ConfigurationException("Invalid URL: " + config.getEndpoint() - + "/api/v1/unified/executions/" + executionId + "/stream"); - } - - Request.Builder builder = new Request.Builder() - .url(url) - .header("User-Agent", config.getUserAgent()) - .header("Accept", "text/event-stream") - .get(); - - addAuthHeaders(builder); - - Request httpRequest = builder.build(); - - try { - Response response = httpClient.newCall(httpRequest).execute(); - try { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("SSE response has no body", 0, null); - } - - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(body.byteStream(), StandardCharsets.UTF_8))) { - StringBuilder eventBuffer = new StringBuilder(); - String line; - - while ((line = reader.readLine()) != null) { - if (line.isEmpty()) { - // Empty line = end of SSE event - String event = eventBuffer.toString().trim(); - eventBuffer.setLength(0); - - if (event.isEmpty()) { - continue; - } - - // Parse SSE data lines - for (String eventLine : event.split("\n")) { - if (eventLine.startsWith("data: ")) { - String jsonStr = eventLine.substring(6); - if (jsonStr.isEmpty() || "[DONE]".equals(jsonStr)) { - continue; - } - try { - com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus status = - objectMapper.readValue(jsonStr, - com.getaxonflow.sdk.types.execution.ExecutionTypes.ExecutionStatus.class); - callback.accept(status); - - // Check for terminal status - if (status.getStatus() != null && status.getStatus().isTerminal()) { - return; - } - } catch (JsonProcessingException e) { - logger.warn("Failed to parse SSE data: {}", jsonStr, e); - } - } - } - } else { - eventBuffer.append(line).append("\n"); - } - } - } - } finally { - response.close(); - } - } catch (IOException e) { - throw new AxonFlowException("SSE stream failed: " + e.getMessage(), e); - } - } - - // ======================================================================== - // Media Governance Config - // ======================================================================== - - /** - * Gets the media governance configuration for the current tenant. - * - *

Returns per-tenant settings controlling whether media analysis is - * enabled and which analyzers are allowed. - * - * @return the media governance configuration - * @throws AxonFlowException if the request fails - */ - public MediaGovernanceConfig getMediaGovernanceConfig() { - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", "/api/v1/media-governance/config", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, MediaGovernanceConfig.class); - } - }, "getMediaGovernanceConfig"); - } - - /** - * Asynchronously gets the media governance configuration for the current tenant. - * - * @return a future containing the media governance configuration - */ - public CompletableFuture getMediaGovernanceConfigAsync() { - return CompletableFuture.supplyAsync(this::getMediaGovernanceConfig, asyncExecutor); - } - - /** - * Updates the media governance configuration for the current tenant. - * - *

Allows enabling/disabling media analysis and controlling which - * analyzers are permitted. - * - * @param request the update request - * @return the updated media governance configuration - * @throws AxonFlowException if the request fails - */ - public MediaGovernanceConfig updateMediaGovernanceConfig(UpdateMediaGovernanceConfigRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("PUT", "/api/v1/media-governance/config", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, MediaGovernanceConfig.class); - } - }, "updateMediaGovernanceConfig"); - } - - /** - * Asynchronously updates the media governance configuration for the current tenant. - * - * @param request the update request - * @return a future containing the updated media governance configuration - */ - public CompletableFuture updateMediaGovernanceConfigAsync(UpdateMediaGovernanceConfigRequest request) { - return CompletableFuture.supplyAsync(() -> updateMediaGovernanceConfig(request), asyncExecutor); - } - - /** - * Gets the platform-level media governance status. - * - *

Returns whether media governance is available, default enablement, - * and the required license tier. - * - * @return the media governance status - * @throws AxonFlowException if the request fails - */ - public MediaGovernanceStatus getMediaGovernanceStatus() { - return retryExecutor.execute(() -> { - Request httpRequest = buildRequest("GET", "/api/v1/media-governance/status", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, MediaGovernanceStatus.class); - } - }, "getMediaGovernanceStatus"); - } - - /** - * Asynchronously gets the platform-level media governance status. - * - * @return a future containing the media governance status - */ - public CompletableFuture getMediaGovernanceStatusAsync() { - return CompletableFuture.supplyAsync(this::getMediaGovernanceStatus, asyncExecutor); - } - - // ======================================================================== - // Configuration Access - // ======================================================================== - - /** - * Returns the current configuration. - * - * @return the configuration - */ - public AxonFlowConfig getConfig() { - return config; - } - - /** - * Returns cache statistics. - * - * @return cache stats string - */ - public String getCacheStats() { - return cache.getStats(); - } - - /** - * Clears the response cache. - */ - public void clearCache() { - cache.clear(); - } - - // ======================================================================== - // Internal Methods - // ======================================================================== - - private Request buildRequest(String method, String path, Object body) { - HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); - if (url == null) { - throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); - } - - Request.Builder builder = new Request.Builder() - .url(url) - .header("User-Agent", config.getUserAgent()) - .header("Accept", "application/json"); - - // Add authentication headers - addAuthHeaders(builder); - - // Add mode header - if (config.getMode() != null) { - builder.header("X-AxonFlow-Mode", config.getMode().getValue()); - } - - // Set method and body - RequestBody requestBody = null; - if (body != null) { - try { - String json = objectMapper.writeValueAsString(body); - requestBody = RequestBody.create(json, JSON); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to serialize request body", e); - } - } - - switch (method.toUpperCase()) { - case "GET": - builder.get(); - break; - case "POST": - builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "PUT": - builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "DELETE": - builder.delete(requestBody); - break; - default: - throw new IllegalArgumentException("Unsupported method: " + method); - } - - return builder.build(); - } - - private Request buildPatchRequest(String path, Object body) { - HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); - if (url == null) { - throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); - } - - Request.Builder builder = new Request.Builder() - .url(url) - .header("User-Agent", config.getUserAgent()) - .header("Accept", "application/json"); - - addAuthHeaders(builder); - - if (config.getMode() != null) { - builder.header("X-AxonFlow-Mode", config.getMode().getValue()); - } - - RequestBody requestBody = null; - if (body != null) { - try { - String json = objectMapper.writeValueAsString(body); - requestBody = RequestBody.create(json, JSON); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to serialize request body", e); - } - } - - builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); - return builder.build(); - } - - private String buildPolicyQueryString(String basePath, ListStaticPoliciesOptions options) { - if (options == null) { - return basePath; - } - - StringBuilder path = new StringBuilder(basePath); - StringBuilder query = new StringBuilder(); - - if (options.getCategory() != null) { - appendQueryParam(query, "category", options.getCategory().getValue()); - } - if (options.getTier() != null) { - appendQueryParam(query, "tier", options.getTier().getValue()); - } - if (options.getOrganizationId() != null) { - appendQueryParam(query, "organization_id", options.getOrganizationId()); - } - if (options.getEnabled() != null) { - appendQueryParam(query, "enabled", options.getEnabled().toString()); - } - if (options.getLimit() != null) { - appendQueryParam(query, "limit", options.getLimit().toString()); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", options.getOffset().toString()); - } - if (options.getSortBy() != null) { - appendQueryParam(query, "sort_by", options.getSortBy()); - } - if (options.getSortOrder() != null) { - appendQueryParam(query, "sort_order", options.getSortOrder()); - } - if (options.getSearch() != null) { - appendQueryParam(query, "search", options.getSearch()); - } - - if (query.length() > 0) { - path.append("?").append(query); - } - return path.toString(); - } - - private String buildDynamicPolicyQueryString(String basePath, ListDynamicPoliciesOptions options) { - if (options == null) { - return basePath; - } - - StringBuilder path = new StringBuilder(basePath); - StringBuilder query = new StringBuilder(); - - if (options.getType() != null) { - appendQueryParam(query, "type", options.getType()); - } - if (options.getTier() != null) { - appendQueryParam(query, "tier", options.getTier().getValue()); - } - if (options.getOrganizationId() != null) { - appendQueryParam(query, "organization_id", options.getOrganizationId()); - } - if (options.getEnabled() != null) { - appendQueryParam(query, "enabled", options.getEnabled().toString()); - } - if (options.getLimit() != null) { - appendQueryParam(query, "limit", options.getLimit().toString()); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", options.getOffset().toString()); - } - if (options.getSortBy() != null) { - appendQueryParam(query, "sort_by", options.getSortBy()); - } - if (options.getSortOrder() != null) { - appendQueryParam(query, "sort_order", options.getSortOrder()); - } - if (options.getSearch() != null) { - appendQueryParam(query, "search", options.getSearch()); - } - - if (query.length() > 0) { - path.append("?").append(query); - } - return path.toString(); - } - - private String buildEffectivePoliciesQuery(EffectivePoliciesOptions options) { - StringBuilder query = new StringBuilder(); - - if (options.getCategory() != null) { - appendQueryParam(query, "category", options.getCategory().getValue()); - } - if (options.isIncludeDisabled()) { - appendQueryParam(query, "include_disabled", "true"); - } - if (options.isIncludeOverridden()) { - appendQueryParam(query, "include_overridden", "true"); - } - - return query.toString(); - } - - private void appendQueryParam(StringBuilder query, String name, String value) { - if (query.length() > 0) { - query.append("&"); - } - query.append(name).append("=").append(value); - } - - private void addAuthHeaders(Request.Builder builder) { - // Always send Basic auth with the effective clientId — server derives tenant from it. - // clientSecret defaults to empty string for community/no-secret mode. - String effectiveClientId = getEffectiveClientId(); - String secret = config.getClientSecret() != null ? config.getClientSecret() : ""; - String credentials = effectiveClientId + ":" + secret; - String encoded = Base64.getEncoder().encodeToString( - credentials.getBytes(StandardCharsets.UTF_8) - ); - builder.header("Authorization", "Basic " + encoded); - } - - /** - * Requires credentials for enterprise features. - * Get the effective clientId, using smart default for community mode. - * - *

Returns the configured clientId if set, otherwise returns "community" - * as a smart default. This enables zero-config usage for community/self-hosted - * deployments while still supporting enterprise deployments with explicit credentials. - * - * @return the clientId to use in requests - */ - private String getEffectiveClientId() { - String clientId = config.getClientId(); - return (clientId != null && !clientId.isEmpty()) ? clientId : "community"; - } - - private T parseResponse(Response response, Class type) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - if (json.isEmpty()) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - try { - return objectMapper.readValue(json, type); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to parse response: " + e.getMessage(), response.code(), null, e); - } - } - - private T parseResponse(Response response, TypeReference typeRef) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - try { - return objectMapper.readValue(json, typeRef); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to parse response: " + e.getMessage(), response.code(), null, e); - } - } - - private JsonNode parseResponseNode(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - if (json.isEmpty()) { - return objectMapper.createObjectNode(); - } - - try { - return objectMapper.readTree(json); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to parse response: " + e.getMessage(), response.code(), null, e); - } - } - - private void handleErrorResponse(Response response) throws IOException { - if (response.isSuccessful()) { - return; - } - - int code = response.code(); - String message = response.message(); - String body = response.body() != null ? response.body().string() : ""; - - // Try to extract error message from JSON body - String errorMessage = extractErrorMessage(body, message); - - switch (code) { - case 401: - throw new AuthenticationException(errorMessage); - case 402: - // Budget exceeded - treat similarly to 403 policy violation - throw new PolicyViolationException(errorMessage); - case 403: - // Check if this is a policy violation - if (body.contains("policy") || body.contains("blocked")) { - throw new PolicyViolationException(errorMessage); - } - throw new AuthenticationException(errorMessage, 403); - case 409: - throw new AxonFlowException(errorMessage, 409, "VERSION_CONFLICT"); - case 429: - throw new RateLimitException(errorMessage); - case 408: - case 504: - throw new TimeoutException(errorMessage); - default: - throw new AxonFlowException(errorMessage, code, null); - } - } - - private String extractErrorMessage(String body, String defaultMessage) { - if (body == null || body.isEmpty()) { - return defaultMessage; - } - - try { - Map errorResponse = objectMapper.readValue(body, - new TypeReference>() {}); - - if (errorResponse.containsKey("error")) { - return String.valueOf(errorResponse.get("error")); - } - if (errorResponse.containsKey("message")) { - return String.valueOf(errorResponse.get("message")); - } - if (errorResponse.containsKey("block_reason")) { - return String.valueOf(errorResponse.get("block_reason")); - } - } catch (JsonProcessingException e) { - // Body is not JSON, return as-is if short enough - if (body.length() < 200) { - return body; - } - } - - return defaultMessage; - } - - // ======================================================================== - // Portal Authentication (Enterprise) - // ======================================================================== - - /** - * Login to Customer Portal and store session cookie. - * Required before using Code Governance methods. - * - * @param orgId the organization ID - * @param password the organization password - * @return login response with session info - * @throws IOException if the request fails - * - * @example - *

{@code
-     * PortalLoginResponse login = axonflow.loginToPortal("test-org-001", "test123");
-     * System.out.println("Logged in as: " + login.getName());
-     *
-     * // Now you can use Code Governance methods
-     * ListGitProvidersResponse providers = axonflow.listGitProviders();
-     * }
- */ - public PortalLoginResponse loginToPortal(String orgId, String password) throws IOException { - logger.debug("Logging in to portal: {}", orgId); - - String json = objectMapper.writeValueAsString( - java.util.Map.of("org_id", orgId, "password", password) - ); - RequestBody body = RequestBody.create(json, JSON); - - Request request = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/auth/login") - .post(body) - .header("Content-Type", "application/json") - .build(); - - try (Response response = httpClient.newCall(request).execute()) { - if (!response.isSuccessful()) { - throw new AuthenticationException("Login failed: " + response.body().string()); - } - - PortalLoginResponse loginResponse = parseResponse(response, PortalLoginResponse.class); - - // Extract session cookie from response - String cookies = response.header("Set-Cookie"); - if (cookies != null && cookies.contains("axonflow_session=")) { - int start = cookies.indexOf("axonflow_session=") + 17; - int end = cookies.indexOf(";", start); - if (end > start) { - this.sessionCookie = cookies.substring(start, end); - } - } - - // Fallback to session_id in response body - if (this.sessionCookie == null && loginResponse.getSessionId() != null) { - this.sessionCookie = loginResponse.getSessionId(); - } - - logger.info("Portal login successful for {}", orgId); - return loginResponse; - } - } - - /** - * Logout from Customer Portal and clear session cookie. - */ - public void logoutFromPortal() { - if (sessionCookie == null) { - return; - } - - try { - Request request = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/auth/logout") - .post(RequestBody.create("", JSON)) - .header("Cookie", "axonflow_session=" + sessionCookie) - .build(); - - httpClient.newCall(request).execute().close(); - } catch (Exception e) { - // Ignore logout errors - } - - sessionCookie = null; - logger.info("Portal logout successful"); - } - - /** - * Check if logged in to Customer Portal. - * - * @return true if logged in - */ - public boolean isLoggedIn() { - return sessionCookie != null; - } - - // ======================================================================== - // Code Governance - Git Provider APIs (Enterprise) - // ======================================================================== - - /** - * Validates Git provider credentials without saving them. - * Requires prior authentication via loginToPortal(). - * - * @param request the validation request with provider type and credentials - * @return validation result - * @throws IOException if the request fails - */ - public ValidateGitProviderResponse validateGitProvider(ValidateGitProviderRequest request) throws IOException { - requirePortalLogin(); - logger.debug("Validating Git provider: {}", request.getType()); - - String json = objectMapper.writeValueAsString(request); - RequestBody body = RequestBody.create(json, JSON); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/git-providers/validate") - .post(body); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, ValidateGitProviderResponse.class); - } - } - - /** - * Configures a Git provider for code governance. - * - * @param request the configuration request with provider type and credentials - * @return configuration result - * @throws IOException if the request fails - */ - public ConfigureGitProviderResponse configureGitProvider(ConfigureGitProviderRequest request) throws IOException { - requirePortalLogin(); - logger.debug("Configuring Git provider: {}", request.getType()); - - String json = objectMapper.writeValueAsString(request); - RequestBody body = RequestBody.create(json, JSON); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/git-providers") - .post(body); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, ConfigureGitProviderResponse.class); - } - } - - /** - * Lists configured Git providers. - * - * @return list of configured providers - * @throws IOException if the request fails - */ - public ListGitProvidersResponse listGitProviders() throws IOException { - requirePortalLogin(); - logger.debug("Listing Git providers"); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/git-providers") - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, ListGitProvidersResponse.class); - } - } - - /** - * Deletes a configured Git provider. - * - * @param providerType the provider type to delete - * @throws IOException if the request fails - */ - public void deleteGitProvider(GitProviderType providerType) throws IOException { - requirePortalLogin(); - logger.debug("Deleting Git provider: {}", providerType); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/git-providers/" + providerType.getValue()) - .delete(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - handleErrorResponse(response); - } - } - - /** - * Creates a Pull Request from LLM-generated code. - * - * @param request the PR creation request with repository info and files - * @return the created PR details - * @throws IOException if the request fails - */ - public CreatePRResponse createPR(CreatePRRequest request) throws IOException { - requirePortalLogin(); - logger.debug("Creating PR: {} in {}/{}", request.getTitle(), request.getOwner(), request.getRepo()); - - String json = objectMapper.writeValueAsString(request); - RequestBody body = RequestBody.create(json, JSON); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/prs") - .post(body); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, CreatePRResponse.class); - } - } - - /** - * Lists PRs with optional filtering. - * - * @param options filtering options (limit, offset, state) - * @return list of PRs - * @throws IOException if the request fails - */ - public ListPRsResponse listPRs(ListPRsOptions options) throws IOException { - requirePortalLogin(); - logger.debug("Listing PRs"); - - StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/prs"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getLimit() != null) { - appendQueryParam(query, "limit", String.valueOf(options.getLimit())); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", String.valueOf(options.getOffset())); - } - if (options.getState() != null) { - appendQueryParam(query, "state", options.getState()); - } - } - - if (query.length() > 0) { - url.append("?").append(query); - } - - Request.Builder builder = new Request.Builder() - .url(url.toString()) - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, ListPRsResponse.class); - } - } - - /** - * Lists PRs with default options. - * - * @return list of PRs - * @throws IOException if the request fails - */ - public ListPRsResponse listPRs() throws IOException { - return listPRs(null); - } - - /** - * Gets a specific PR by ID. - * - * @param prId the PR record ID - * @return the PR record - * @throws IOException if the request fails - */ - public PRRecord getPR(String prId) throws IOException { - requirePortalLogin(); - logger.debug("Getting PR: {}", prId); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/prs/" + prId) - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, PRRecord.class); - } - } - - /** - * Syncs PR status from the Git provider. - * - * @param prId the PR record ID to sync - * @return the updated PR record - * @throws IOException if the request fails - */ - public PRRecord syncPRStatus(String prId) throws IOException { - requirePortalLogin(); - logger.debug("Syncing PR status: {}", prId); - - RequestBody body = RequestBody.create("{}", JSON); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/prs/" + prId + "/sync") - .post(body); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, PRRecord.class); - } - } - - /** - * Closes a PR without merging and optionally deletes the branch. - * This is an enterprise feature for cleaning up test/demo PRs. - * Supports all Git providers: GitHub, GitLab, Bitbucket. - * - * @param prId the PR record ID to close - * @param deleteBranch whether to also delete the source branch - * @return the closed PR record - * @throws IOException if the request fails - */ - public PRRecord closePR(String prId, boolean deleteBranch) throws IOException { - requirePortalLogin(); - logger.debug("Closing PR: {} (deleteBranch={})", prId, deleteBranch); - - String url = config.getEndpoint() + "/api/v1/code-governance/prs/" + prId; - if (deleteBranch) { - url += "?delete_branch=true"; - } - - Request.Builder builder = new Request.Builder() - .url(url) - .delete(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, PRRecord.class); - } - } - - /** - * Gets aggregated code governance metrics for the tenant. - * - * @return aggregated metrics including PR counts, file counts, and security findings - * @throws IOException if the request fails - */ - public CodeGovernanceMetrics getCodeGovernanceMetrics() throws IOException { - requirePortalLogin(); - logger.debug("Getting code governance metrics"); - - Request.Builder builder = new Request.Builder() - .url(config.getEndpoint() + "/api/v1/code-governance/metrics") - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, CodeGovernanceMetrics.class); - } - } - - /** - * Exports code governance data in JSON format. - * - * @param options export options (format, date range, state filter) - * @return export response with PR records - * @throws IOException if the request fails - */ - public ExportResponse exportCodeGovernanceData(ExportOptions options) throws IOException { - requirePortalLogin(); - logger.debug("Exporting code governance data"); - - StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/export"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - appendQueryParam(query, "format", options.getFormat() != null ? options.getFormat() : "json"); - if (options.getStartDate() != null) { - appendQueryParam(query, "start_date", options.getStartDate().toString()); - } - if (options.getEndDate() != null) { - appendQueryParam(query, "end_date", options.getEndDate().toString()); - } - if (options.getState() != null) { - appendQueryParam(query, "state", options.getState()); - } - } else { - appendQueryParam(query, "format", "json"); - } - - if (query.length() > 0) { - url.append("?").append(query); - } - - Request.Builder builder = new Request.Builder() - .url(url.toString()) - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - return parseResponse(response, ExportResponse.class); - } - } - - /** - * Exports code governance data in CSV format. - * - * @param options export options (date range, state filter) - * @return CSV data as a string - * @throws IOException if the request fails - */ - public String exportCodeGovernanceDataCSV(ExportOptions options) throws IOException { - requirePortalLogin(); - logger.debug("Exporting code governance data as CSV"); - - StringBuilder url = new StringBuilder(config.getEndpoint() + "/api/v1/code-governance/export"); - StringBuilder query = new StringBuilder(); - - appendQueryParam(query, "format", "csv"); - if (options != null) { - if (options.getStartDate() != null) { - appendQueryParam(query, "start_date", options.getStartDate().toString()); - } - if (options.getEndDate() != null) { - appendQueryParam(query, "end_date", options.getEndDate().toString()); - } - if (options.getState() != null) { - appendQueryParam(query, "state", options.getState()); - } - } - - url.append("?").append(query); - - Request.Builder builder = new Request.Builder() - .url(url.toString()) - .get(); - - addPortalSessionCookie(builder); - - try (Response response = httpClient.newCall(builder.build()).execute()) { - handleErrorResponse(response); - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - return body.string(); - } - } - - // ======================================================================== - // Execution Replay API - // ======================================================================== - - /** - * Builds a request for the orchestrator API. - */ - private Request buildOrchestratorRequest(String method, String path, Object body) { - HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); - if (url == null) { - throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); - } - - Request.Builder builder = new Request.Builder() - .url(url) - .header("User-Agent", config.getUserAgent()) - .header("Accept", "application/json"); - - addAuthHeaders(builder); - - RequestBody requestBody = null; - if (body != null) { - try { - String json = objectMapper.writeValueAsString(body); - requestBody = RequestBody.create(json, JSON); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to serialize request body", e); - } - } - - switch (method.toUpperCase()) { - case "GET": - builder.get(); - break; - case "POST": - builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "PUT": - builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "PATCH": - builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "DELETE": - builder.delete(requestBody); - break; - default: - throw new IllegalArgumentException("Unsupported method: " + method); - } - - return builder.build(); - } - - /** - * Requires portal login before making code governance requests. - */ - private void requirePortalLogin() { - if (sessionCookie == null) { - throw new AuthenticationException("Not logged in to Customer Portal. Call loginToPortal() first."); - } - } - - /** - * Adds the session cookie header for portal authentication. - */ - private void addPortalSessionCookie(Request.Builder builder) { - if (sessionCookie != null) { - builder.header("Cookie", "axonflow_session=" + sessionCookie); - } - } - - /** - * Builds a request for the Customer Portal API (enterprise features). - * Requires prior authentication via loginToPortal(). - */ - private Request buildPortalRequest(String method, String path, Object body) { - requirePortalLogin(); - - HttpUrl url = HttpUrl.parse(config.getEndpoint() + path); - if (url == null) { - throw new ConfigurationException("Invalid URL: " + config.getEndpoint() + path); - } - - Request.Builder builder = new Request.Builder() - .url(url) - .header("User-Agent", config.getUserAgent()) - .header("Accept", "application/json"); - - addPortalSessionCookie(builder); - - RequestBody requestBody = null; - if (body != null) { - try { - String json = objectMapper.writeValueAsString(body); - requestBody = RequestBody.create(json, JSON); - } catch (JsonProcessingException e) { - throw new AxonFlowException("Failed to serialize request body", e); - } - } - - switch (method.toUpperCase()) { - case "GET": - builder.get(); - break; - case "POST": - builder.post(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "PUT": - builder.put(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "PATCH": - builder.patch(requestBody != null ? requestBody : RequestBody.create("", JSON)); - break; - case "DELETE": - builder.delete(requestBody); - break; - default: - throw new IllegalArgumentException("Unsupported method: " + method); - } - - return builder.build(); - } - - /** - * Lists workflow executions with optional filtering and pagination. - * - * @param options filtering and pagination options - * @return paginated list of execution summaries - * - * @example - *
{@code
-     * ListExecutionsResponse response = axonflow.listExecutions(
-     *     ListExecutionsOptions.builder()
-     *         .setStatus("completed")
-     *         .setLimit(10)
-     * );
-     * for (ExecutionSummary exec : response.getExecutions()) {
-     *     System.out.println(exec.getRequestId() + ": " + exec.getStatus());
-     * }
-     * }
- */ - public ListExecutionsResponse listExecutions(ListExecutionsOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/executions"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getLimit() != null) { - appendQueryParam(query, "limit", options.getLimit().toString()); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", options.getOffset().toString()); - } - if (options.getStatus() != null) { - appendQueryParam(query, "status", options.getStatus()); - } - if (options.getWorkflowId() != null) { - appendQueryParam(query, "workflow_id", options.getWorkflowId()); - } - if (options.getStartTime() != null) { - appendQueryParam(query, "start_time", options.getStartTime()); - } - if (options.getEndTime() != null) { - appendQueryParam(query, "end_time", options.getEndTime()); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ListExecutionsResponse.class); - } - }, "listExecutions"); - } - - /** - * Lists workflow executions with default options. - * - * @return list of execution summaries - */ - public ListExecutionsResponse listExecutions() { - return listExecutions(null); - } - - /** - * Gets a complete execution record including summary and all steps. - * - * @param executionId the execution ID (request_id) - * @return full execution details with all step snapshots - * - * @example - *
{@code
-     * ExecutionDetail detail = axonflow.getExecution("exec-abc123");
-     * System.out.println("Status: " + detail.getSummary().getStatus());
-     * for (ExecutionSnapshot step : detail.getSteps()) {
-     *     System.out.println("Step " + step.getStepIndex() + ": " + step.getStepName());
-     * }
-     * }
- */ - public ExecutionDetail getExecution(String executionId) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", - "/api/v1/executions/" + executionId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ExecutionDetail.class); - } - }, "getExecution"); - } - - /** - * Gets all step snapshots for an execution. - * - * @param executionId the execution ID (request_id) - * @return list of step snapshots - */ - public List getExecutionSteps(String executionId) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", - "/api/v1/executions/" + executionId + "/steps", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, new TypeReference>() {}); - } - }, "getExecutionSteps"); - } - - /** - * Gets a timeline view of execution events for visualization. - * - * @param executionId the execution ID (request_id) - * @return list of timeline entries - */ - public List getExecutionTimeline(String executionId) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", - "/api/v1/executions/" + executionId + "/timeline", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, new TypeReference>() {}); - } - }, "getExecutionTimeline"); - } - - /** - * Exports a complete execution record for compliance or archival. - * - * @param executionId the execution ID (request_id) - * @param options export options - * @return execution data as a map - * - * @example - *
{@code
-     * Map export = axonflow.exportExecution("exec-abc123",
-     *     ExecutionExportOptions.builder()
-     *         .setIncludeInput(true)
-     *         .setIncludeOutput(true));
-     * // Save to file for audit
-     * }
- */ - public Map exportExecution(String executionId, ExecutionExportOptions options) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/executions/" + executionId + "/export"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getFormat() != null) { - appendQueryParam(query, "format", options.getFormat()); - } - if (options.isIncludeInput()) { - appendQueryParam(query, "include_input", "true"); - } - if (options.isIncludeOutput()) { - appendQueryParam(query, "include_output", "true"); - } - if (options.isIncludePolicies()) { - appendQueryParam(query, "include_policies", "true"); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, new TypeReference>() {}); - } - }, "exportExecution"); - } - - /** - * Exports a complete execution record with default options. - * - * @param executionId the execution ID (request_id) - * @return execution data as a map - */ - public Map exportExecution(String executionId) { - return exportExecution(executionId, null); - } - - /** - * Deletes an execution and all associated step snapshots. - * - * @param executionId the execution ID (request_id) - */ - public void deleteExecution(String executionId) { - Objects.requireNonNull(executionId, "executionId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("DELETE", - "/api/v1/executions/" + executionId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "deleteExecution"); - } - - /** - * Asynchronously lists workflow executions. - * - * @param options filtering and pagination options - * @return a future containing the list of executions - */ - public CompletableFuture listExecutionsAsync(ListExecutionsOptions options) { - return CompletableFuture.supplyAsync(() -> listExecutions(options), asyncExecutor); - } - - /** - * Asynchronously gets execution details. - * - * @param executionId the execution ID - * @return a future containing the execution details - */ - public CompletableFuture getExecutionAsync(String executionId) { - return CompletableFuture.supplyAsync(() -> getExecution(executionId), asyncExecutor); - } - - // ======================================== - // COST CONTROLS - BUDGETS - // ======================================== - - /** - * Creates a new budget. - * - * @param request the budget creation request - * @return the created budget - */ - public Budget createBudget(CreateBudgetRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/budgets", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, Budget.class); - } - }, "createBudget"); - } - - /** - * Gets a budget by ID. - * - * @param budgetId the budget ID - * @return the budget - */ - public Budget getBudget(String budgetId) { - Objects.requireNonNull(budgetId, "budgetId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, Budget.class); - } - }, "getBudget"); - } - - /** - * Lists all budgets. - * - * @param options filtering and pagination options - * @return list of budgets - */ - public BudgetsResponse listBudgets(ListBudgetsOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/budgets"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getScope() != null) { - appendQueryParam(query, "scope", options.getScope().getValue()); - } - if (options.getLimit() != null) { - appendQueryParam(query, "limit", options.getLimit().toString()); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", options.getOffset().toString()); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, BudgetsResponse.class); - } - }, "listBudgets"); - } - - /** - * Lists all budgets with default options. - * - * @return list of budgets - */ - public BudgetsResponse listBudgets() { - return listBudgets(null); - } - - /** - * Updates an existing budget. - * - * @param budgetId the budget ID - * @param request the update request - * @return the updated budget - */ - public Budget updateBudget(String budgetId, UpdateBudgetRequest request) { - Objects.requireNonNull(budgetId, "budgetId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("PUT", "/api/v1/budgets/" + budgetId, request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, Budget.class); - } - }, "updateBudget"); - } - - /** - * Deletes a budget. - * - * @param budgetId the budget ID - */ - public void deleteBudget(String budgetId) { - Objects.requireNonNull(budgetId, "budgetId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("DELETE", "/api/v1/budgets/" + budgetId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful() && response.code() != 204) { - handleErrorResponse(response); - } - return null; - } - }, "deleteBudget"); - } - - // ======================================== - // COST CONTROLS - BUDGET STATUS & ALERTS - // ======================================== - - /** - * Gets the current status of a budget. - * - * @param budgetId the budget ID - * @return the budget status - */ - public BudgetStatus getBudgetStatus(String budgetId) { - Objects.requireNonNull(budgetId, "budgetId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId + "/status", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, BudgetStatus.class); - } - }, "getBudgetStatus"); - } - - /** - * Gets alerts for a budget. - * - * @param budgetId the budget ID - * @return the budget alerts - */ - public BudgetAlertsResponse getBudgetAlerts(String budgetId) { - Objects.requireNonNull(budgetId, "budgetId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/budgets/" + budgetId + "/alerts", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, BudgetAlertsResponse.class); - } - }, "getBudgetAlerts"); - } - - /** - * Performs a pre-flight budget check. - * - * @param request the check request - * @return the budget decision - */ - public BudgetDecision checkBudget(BudgetCheckRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/budgets/check", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, BudgetDecision.class); - } - }, "checkBudget"); - } - - // ======================================== - // COST CONTROLS - USAGE - // ======================================== - - /** - * Gets usage summary for a period. - * - * @param period the period (daily, weekly, monthly, quarterly, yearly) - * @return the usage summary - */ - public UsageSummary getUsageSummary(String period) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/usage"); - if (period != null && !period.isEmpty()) { - path.append("?period=").append(period); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, UsageSummary.class); - } - }, "getUsageSummary"); - } - - /** - * Gets usage summary with default period. - * - * @return the usage summary - */ - public UsageSummary getUsageSummary() { - return getUsageSummary(null); - } - - /** - * Gets usage breakdown by a grouping dimension. - * - * @param groupBy the dimension to group by (provider, model, agent, team, workflow) - * @param period the period (daily, weekly, monthly, quarterly, yearly) - * @return the usage breakdown - */ - public UsageBreakdown getUsageBreakdown(String groupBy, String period) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/usage/breakdown"); - StringBuilder query = new StringBuilder(); - - if (groupBy != null && !groupBy.isEmpty()) { - appendQueryParam(query, "group_by", groupBy); - } - if (period != null && !period.isEmpty()) { - appendQueryParam(query, "period", period); - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, UsageBreakdown.class); - } - }, "getUsageBreakdown"); - } - - /** - * Lists usage records. - * - * @param options filtering and pagination options - * @return list of usage records - */ - public UsageRecordsResponse listUsageRecords(ListUsageRecordsOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/usage/records"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getLimit() != null) { - appendQueryParam(query, "limit", options.getLimit().toString()); - } - if (options.getOffset() != null) { - appendQueryParam(query, "offset", options.getOffset().toString()); - } - if (options.getProvider() != null) { - appendQueryParam(query, "provider", options.getProvider()); - } - if (options.getModel() != null) { - appendQueryParam(query, "model", options.getModel()); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, UsageRecordsResponse.class); - } - }, "listUsageRecords"); - } - - /** - * Lists usage records with default options. - * - * @return list of usage records - */ - public UsageRecordsResponse listUsageRecords() { - return listUsageRecords(null); - } - - // ======================================== - // COST CONTROLS - PRICING - // ======================================== - - /** - * Gets pricing information for models. - * - * @param provider filter by provider (optional) - * @param model filter by model (optional) - * @return pricing information - */ - public PricingListResponse getPricing(String provider, String model) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/pricing"); - StringBuilder query = new StringBuilder(); - - if (provider != null && !provider.isEmpty()) { - appendQueryParam(query, "provider", provider); - } - if (model != null && !model.isEmpty()) { - appendQueryParam(query, "model", model); - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - String body = response.body() != null ? response.body().string() : ""; - if (!response.isSuccessful()) { - throw new AxonFlowException("Failed to get pricing: " + body); - } - - // Handle single object or array response - if (body.trim().startsWith("{") && body.contains("\"provider\"")) { - // Single object response - wrap in list - PricingInfo singlePricing = objectMapper.readValue(body, PricingInfo.class); - PricingListResponse result = new PricingListResponse(); - result.setPricing(Collections.singletonList(singlePricing)); - return result; - } else { - return objectMapper.readValue(body, PricingListResponse.class); - } - } - }, "getPricing"); - } - - /** - * Gets all pricing information. - * - * @return all pricing information - */ - public PricingListResponse getPricing() { - return getPricing(null, null); - } - - // ======================================== - // COST CONTROLS - ASYNC METHODS - // ======================================== - - /** - * Asynchronously creates a budget. - * - * @param request the budget creation request - * @return a future containing the created budget - */ - public CompletableFuture createBudgetAsync(CreateBudgetRequest request) { - return CompletableFuture.supplyAsync(() -> createBudget(request), asyncExecutor); - } - - /** - * Asynchronously gets a budget. - * - * @param budgetId the budget ID - * @return a future containing the budget - */ - public CompletableFuture getBudgetAsync(String budgetId) { - return CompletableFuture.supplyAsync(() -> getBudget(budgetId), asyncExecutor); - } - - /** - * Asynchronously lists budgets. - * - * @param options filtering and pagination options - * @return a future containing the list of budgets - */ - public CompletableFuture listBudgetsAsync(ListBudgetsOptions options) { - return CompletableFuture.supplyAsync(() -> listBudgets(options), asyncExecutor); - } - - /** - * Asynchronously gets budget status. - * - * @param budgetId the budget ID - * @return a future containing the budget status - */ - public CompletableFuture getBudgetStatusAsync(String budgetId) { - return CompletableFuture.supplyAsync(() -> getBudgetStatus(budgetId), asyncExecutor); - } - - /** - * Asynchronously gets usage summary. - * - * @param period the period - * @return a future containing the usage summary - */ - public CompletableFuture getUsageSummaryAsync(String period) { - return CompletableFuture.supplyAsync(() -> getUsageSummary(period), asyncExecutor); - } - - // ======================================================================== - // Workflow Control Plane - // ======================================================================== - // The Workflow Control Plane provides governance gates for external - // orchestrators like LangChain, LangGraph, and CrewAI. - // - // "LangChain runs the workflow. AxonFlow decides when it's allowed to move forward." - - /** - * Creates a new workflow for governance tracking. - * - *

Registers a new workflow with AxonFlow. Call this at the start of your - * external orchestrator workflow (LangChain, LangGraph, CrewAI, etc.). - * - * @param request workflow creation request - * @return created workflow with ID - * @throws AxonFlowException if creation fails - * - * @example - *

{@code
-     * CreateWorkflowResponse workflow = axonflow.createWorkflow(
-     *     CreateWorkflowRequest.builder()
-     *         .workflowName("code-review-pipeline")
-     *         .source(WorkflowSource.LANGGRAPH)
-     *         .build()
-     * );
-     * System.out.println("Workflow created: " + workflow.getWorkflowId());
-     * }
- */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowResponse createWorkflow( - com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/workflows", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "createWorkflow"); - } - - /** - * Gets the status of a workflow. - * - * @param workflowId workflow ID - * @return workflow status including steps - * @throws AxonFlowException if workflow not found - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatusResponse getWorkflow(String workflowId) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/workflows/" + workflowId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "getWorkflow"); - } - - /** - * Checks if a workflow step is allowed to proceed (step gate). - * - *

This is the core governance method. Call this before executing each step - * in your workflow to check if the step is allowed based on policies. - * - * @param workflowId workflow ID - * @param stepId unique step identifier (you provide this) - * @param request step gate request with step details - * @return gate decision: allow, block, or require_approval - * @throws AxonFlowException if check fails - * - * @example - *

{@code
-     * StepGateResponse gate = axonflow.stepGate(
-     *     workflow.getWorkflowId(),
-     *     "step-1",
-     *     StepGateRequest.builder()
-     *         .stepName("Generate Code")
-     *         .stepType(StepType.LLM_CALL)
-     *         .model("gpt-4")
-     *         .provider("openai")
-     *         .build()
-     * );
-     *
-     * if (gate.isBlocked()) {
-     *     throw new RuntimeException("Step blocked: " + gate.getReason());
-     * } else if (gate.requiresApproval()) {
-     *     System.out.println("Approval needed: " + gate.getApprovalUrl());
-     * } else {
-     *     // Execute the step
-     *     executeStep();
-     * }
-     * }
- */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateResponse stepGate( - String workflowId, - String stepId, - com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateRequest request) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - Objects.requireNonNull(stepId, "stepId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/steps/" + stepId + "/gate", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "stepGate"); - } - - /** - * Marks a step as completed. - * - *

Call this after successfully executing a step to record its completion. - * - * @param workflowId workflow ID - * @param stepId step ID - * @param request optional completion request with output data - */ - public void markStepCompleted( - String workflowId, - String stepId, - com.getaxonflow.sdk.types.workflow.WorkflowTypes.MarkStepCompletedRequest request) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - Objects.requireNonNull(stepId, "stepId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/steps/" + stepId + "/complete", - request != null ? request : Collections.emptyMap()); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "markStepCompleted"); - } - - /** - * Marks a step as completed with no output data. - * - * @param workflowId workflow ID - * @param stepId step ID - */ - public void markStepCompleted(String workflowId, String stepId) { - markStepCompleted(workflowId, stepId, null); - } - - /** - * Completes a workflow successfully. - * - *

Call this when your workflow has completed all steps successfully. - * - * @param workflowId workflow ID - */ - public void completeWorkflow(String workflowId) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/complete", Collections.emptyMap()); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "completeWorkflow"); - } - - /** - * Aborts a workflow. - * - *

Call this when you need to stop a workflow due to an error or user request. - * - * @param workflowId workflow ID - * @param reason optional reason for aborting - */ - public void abortWorkflow(String workflowId, String reason) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - - retryExecutor.execute(() -> { - Map body = reason != null ? - Collections.singletonMap("reason", reason) : Collections.emptyMap(); - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/abort", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "abortWorkflow"); - } - - /** - * Aborts a workflow with no reason. - * - * @param workflowId workflow ID - */ - public void abortWorkflow(String workflowId) { - abortWorkflow(workflowId, null); - } - - /** - * Fails a workflow. - * - *

Call this when a workflow has encountered an unrecoverable error and should - * be marked as failed. - * - * @param workflowId workflow ID - * @param reason optional reason for failing - */ - public void failWorkflow(String workflowId, String reason) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - - retryExecutor.execute(() -> { - Map body = reason != null ? - Collections.singletonMap("reason", reason) : Collections.emptyMap(); - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/fail", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "failWorkflow"); - } - - /** - * Fails a workflow with no reason. - * - * @param workflowId workflow ID - */ - public void failWorkflow(String workflowId) { - failWorkflow(workflowId, null); - } - - /** - * Asynchronously fails a workflow. - * - * @param workflowId workflow ID - * @param reason optional reason for failing - * @return a future that completes when the workflow has been failed - */ - public CompletableFuture failWorkflowAsync(String workflowId, String reason) { - return CompletableFuture.supplyAsync(() -> { - failWorkflow(workflowId, reason); - return null; - }, asyncExecutor); - } - - /** - * Resumes a workflow after approval. - * - *

Call this after a step has been approved to continue the workflow. - * - * @param workflowId workflow ID - */ - public void resumeWorkflow(String workflowId) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflows/" + workflowId + "/resume", Collections.emptyMap()); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "resumeWorkflow"); - } - - /** - * Lists workflows with optional filters. - * - * @param options filter and pagination options - * @return list of workflows - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse listWorkflows( - com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsOptions options) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/workflows"); - StringBuilder query = new StringBuilder(); - - if (options != null) { - if (options.getStatus() != null) { - appendQueryParam(query, "status", options.getStatus().getValue()); - } - if (options.getSource() != null) { - appendQueryParam(query, "source", options.getSource().getValue()); - } - if (options.getLimit() > 0) { - appendQueryParam(query, "limit", String.valueOf(options.getLimit())); - } - if (options.getOffset() > 0) { - appendQueryParam(query, "offset", String.valueOf(options.getOffset())); - } - if (options.getTraceId() != null) { - appendQueryParam(query, "trace_id", options.getTraceId()); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "listWorkflows"); - } - - /** - * Lists all workflows with default options. - * - * @return list of workflows - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ListWorkflowsResponse listWorkflows() { - return listWorkflows(null); - } - - /** - * Asynchronously creates a workflow. - * - * @param request workflow creation request - * @return a future containing the created workflow - */ - public CompletableFuture createWorkflowAsync( - com.getaxonflow.sdk.types.workflow.WorkflowTypes.CreateWorkflowRequest request) { - return CompletableFuture.supplyAsync(() -> createWorkflow(request), asyncExecutor); - } - - /** - * Asynchronously checks a step gate. - * - * @param workflowId workflow ID - * @param stepId step ID - * @param request step gate request - * @return a future containing the gate decision - */ - public CompletableFuture stepGateAsync( - String workflowId, - String stepId, - com.getaxonflow.sdk.types.workflow.WorkflowTypes.StepGateRequest request) { - return CompletableFuture.supplyAsync(() -> stepGate(workflowId, stepId, request), asyncExecutor); - } - - // ======================================================================== - // WCP Approval Methods - // ======================================================================== - - /** - * Approves a workflow step that requires human approval. - * - *

Call this when a step gate returns {@code require_approval} to approve - * the step and allow the workflow to proceed. - * - * @param workflowId workflow ID - * @param stepId step ID - * @return the approval response - * @throws AxonFlowException if the approval fails - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse approveStep( - String workflowId, String stepId) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - Objects.requireNonNull(stepId, "stepId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflow-control/" + workflowId + "/steps/" + stepId + "/approve", - Collections.emptyMap()); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "approveStep"); - } - - /** - * Asynchronously approves a workflow step. - * - * @param workflowId workflow ID - * @param stepId step ID - * @return a future containing the approval response - */ - public CompletableFuture approveStepAsync( - String workflowId, String stepId) { - return CompletableFuture.supplyAsync(() -> approveStep(workflowId, stepId), asyncExecutor); - } - - /** - * Rejects a workflow step that requires human approval. - * - *

Call this when a step gate returns {@code require_approval} to reject - * the step and prevent the workflow from proceeding. - * - * @param workflowId workflow ID - * @param stepId step ID - * @return the rejection response - * @throws AxonFlowException if the rejection fails - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse rejectStep( - String workflowId, String stepId) { - return rejectStep(workflowId, stepId, null); - } - - /** - * Rejects a workflow step that requires human approval, with a reason. - * - *

Call this when a step gate returns {@code require_approval} to reject - * the step and prevent the workflow from proceeding. - * - * @param workflowId workflow ID - * @param stepId step ID - * @param reason optional reason for rejection (included in request body) - * @return the rejection response - * @throws AxonFlowException if the rejection fails - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse rejectStep( - String workflowId, String stepId, String reason) { - Objects.requireNonNull(workflowId, "workflowId cannot be null"); - Objects.requireNonNull(stepId, "stepId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - if (reason != null && !reason.isEmpty()) { - body.put("reason", reason); - } - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/workflow-control/" + workflowId + "/steps/" + stepId + "/reject", - body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "rejectStep"); - } - - /** - * Asynchronously rejects a workflow step. - * - * @param workflowId workflow ID - * @param stepId step ID - * @return a future containing the rejection response - */ - public CompletableFuture rejectStepAsync( - String workflowId, String stepId) { - return rejectStepAsync(workflowId, stepId, null); - } - - /** - * Asynchronously rejects a workflow step with a reason. - * - * @param workflowId workflow ID - * @param stepId step ID - * @param reason optional reason for rejection - * @return a future containing the rejection response - */ - public CompletableFuture rejectStepAsync( - String workflowId, String stepId, String reason) { - return CompletableFuture.supplyAsync(() -> rejectStep(workflowId, stepId, reason), asyncExecutor); - } - - /** - * Gets pending approvals with a limit. - * - * @param limit maximum number of pending approvals to return - * @return the pending approvals response - * @throws AxonFlowException if the request fails - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse getPendingApprovals(int limit) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/workflow-control/pending-approvals"); - if (limit > 0) { - path.append("?limit=").append(limit); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, - new TypeReference() {}); - } - }, "getPendingApprovals"); - } - - /** - * Gets all pending approvals with default limit. - * - * @return the pending approvals response - * @throws AxonFlowException if the request fails - */ - public com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse getPendingApprovals() { - return getPendingApprovals(0); - } - - /** - * Asynchronously gets pending approvals with a limit. - * - * @param limit maximum number of pending approvals to return - * @return a future containing the pending approvals response - */ - public CompletableFuture getPendingApprovalsAsync( - int limit) { - return CompletableFuture.supplyAsync(() -> getPendingApprovals(limit), asyncExecutor); - } - - // ======================================================================== - // Webhook Subscriptions - // ======================================================================== - - /** - * Creates a new webhook subscription. - * - * @param request the webhook creation request - * @return the created webhook subscription - * @throws AxonFlowException if creation fails - */ - public WebhookSubscription createWebhook(CreateWebhookRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", "/api/v1/webhooks", request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, WebhookSubscription.class); - } - }, "createWebhook"); - } - - /** - * Asynchronously creates a new webhook subscription. - * - * @param request the webhook creation request - * @return a future containing the created webhook subscription - */ - public CompletableFuture createWebhookAsync(CreateWebhookRequest request) { - return CompletableFuture.supplyAsync(() -> createWebhook(request), asyncExecutor); - } - - /** - * Gets a webhook subscription by ID. - * - * @param webhookId the webhook ID - * @return the webhook subscription - * @throws AxonFlowException if the webhook is not found - */ - public WebhookSubscription getWebhook(String webhookId) { - Objects.requireNonNull(webhookId, "webhookId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", - "/api/v1/webhooks/" + webhookId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, WebhookSubscription.class); - } - }, "getWebhook"); - } - - /** - * Asynchronously gets a webhook subscription by ID. - * - * @param webhookId the webhook ID - * @return a future containing the webhook subscription - */ - public CompletableFuture getWebhookAsync(String webhookId) { - return CompletableFuture.supplyAsync(() -> getWebhook(webhookId), asyncExecutor); - } - - /** - * Updates an existing webhook subscription. - * - * @param webhookId the webhook ID - * @param request the update request - * @return the updated webhook subscription - * @throws AxonFlowException if the update fails - */ - public WebhookSubscription updateWebhook(String webhookId, UpdateWebhookRequest request) { - Objects.requireNonNull(webhookId, "webhookId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("PUT", - "/api/v1/webhooks/" + webhookId, request); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, WebhookSubscription.class); - } - }, "updateWebhook"); - } - - /** - * Asynchronously updates an existing webhook subscription. - * - * @param webhookId the webhook ID - * @param request the update request - * @return a future containing the updated webhook subscription - */ - public CompletableFuture updateWebhookAsync(String webhookId, UpdateWebhookRequest request) { - return CompletableFuture.supplyAsync(() -> updateWebhook(webhookId, request), asyncExecutor); - } - - /** - * Deletes a webhook subscription. - * - * @param webhookId the webhook ID - * @throws AxonFlowException if the deletion fails - */ - public void deleteWebhook(String webhookId) { - Objects.requireNonNull(webhookId, "webhookId cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("DELETE", - "/api/v1/webhooks/" + webhookId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "deleteWebhook"); - } - - /** - * Asynchronously deletes a webhook subscription. - * - * @param webhookId the webhook ID - * @return a future that completes when the webhook is deleted - */ - public CompletableFuture deleteWebhookAsync(String webhookId) { - return CompletableFuture.runAsync(() -> deleteWebhook(webhookId), asyncExecutor); - } - - /** - * Lists all webhook subscriptions. - * - * @return the list of webhook subscriptions - * @throws AxonFlowException if the request fails - */ - public ListWebhooksResponse listWebhooks() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/webhooks", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseResponse(response, ListWebhooksResponse.class); - } - }, "listWebhooks"); - } - - /** - * Asynchronously lists all webhook subscriptions. - * - * @return a future containing the list of webhook subscriptions - */ - public CompletableFuture listWebhooksAsync() { - return CompletableFuture.supplyAsync(this::listWebhooks, asyncExecutor); - } - - // ======================================================================== - // HITL (Human-in-the-Loop) Queue - // ======================================================================== - - /** - * Lists pending HITL approval requests. - * - *

Returns approval requests from the HITL queue, optionally filtered - * by status and severity. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @param opts filtering and pagination options (may be null) - * @return the list response containing approval requests - * @throws AxonFlowException if the request fails - */ - public HITLQueueListResponse listHITLQueue(HITLQueueListOptions opts) { - return retryExecutor.execute(() -> { - StringBuilder path = new StringBuilder("/api/v1/hitl/queue"); - StringBuilder query = new StringBuilder(); - - if (opts != null) { - if (opts.getStatus() != null) { - appendQueryParam(query, "status", opts.getStatus()); - } - if (opts.getSeverity() != null) { - appendQueryParam(query, "severity", opts.getSeverity()); - } - if (opts.getLimit() != null) { - appendQueryParam(query, "limit", opts.getLimit().toString()); - } - if (opts.getOffset() != null) { - appendQueryParam(query, "offset", opts.getOffset().toString()); - } - } - - if (query.length() > 0) { - path.append("?").append(query); - } - - Request httpRequest = buildOrchestratorRequest("GET", path.toString(), null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - - // Server wraps response: {"success": true, "data": [...], "meta": {...}} - HITLQueueListResponse result = new HITLQueueListResponse(); - if (node.has("data") && node.get("data").isArray()) { - List items = objectMapper.convertValue( - node.get("data"), new TypeReference>() {}); - result.setItems(items); - } - if (node.has("meta")) { - JsonNode meta = node.get("meta"); - long total = 0; - long offset = 0; - if (meta.has("total")) { - total = meta.get("total").asLong(); - result.setTotal(total); - } - if (meta.has("offset")) { - offset = meta.get("offset").asLong(); - } - // Compute hasMore from total/offset/items (consistent with Go/TS SDKs) - result.setHasMore((offset + result.getItems().size()) < total); - } - return result; - } - }, "listHITLQueue"); - } - - /** - * Lists pending HITL approval requests with default options. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @return the list response containing approval requests - * @throws AxonFlowException if the request fails - */ - public HITLQueueListResponse listHITLQueue() { - return listHITLQueue(null); - } - - /** - * Asynchronously lists pending HITL approval requests. - * - * @param opts filtering and pagination options (may be null) - * @return a future containing the list response - */ - public CompletableFuture listHITLQueueAsync(HITLQueueListOptions opts) { - return CompletableFuture.supplyAsync(() -> listHITLQueue(opts), asyncExecutor); - } - - /** - * Gets a specific HITL approval request by ID. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @param requestId the approval request ID - * @return the approval request - * @throws AxonFlowException if the request is not found or the call fails - */ - public HITLApprovalRequest getHITLRequest(String requestId) { - Objects.requireNonNull(requestId, "requestId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", - "/api/v1/hitl/queue/" + requestId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - - // Server wraps response: {"success": true, "data": {...}} - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), HITLApprovalRequest.class); - } - return objectMapper.treeToValue(node, HITLApprovalRequest.class); - } - }, "getHITLRequest"); - } - - /** - * Asynchronously gets a specific HITL approval request by ID. - * - * @param requestId the approval request ID - * @return a future containing the approval request - */ - public CompletableFuture getHITLRequestAsync(String requestId) { - return CompletableFuture.supplyAsync(() -> getHITLRequest(requestId), asyncExecutor); - } - - /** - * Approves a HITL approval request. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @param requestId the approval request ID - * @param review the review input containing reviewer details - * @throws AxonFlowException if the approval fails - */ - public void approveHITLRequest(String requestId, HITLReviewInput review) { - Objects.requireNonNull(requestId, "requestId cannot be null"); - Objects.requireNonNull(review, "review cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/hitl/queue/" + requestId + "/approve", review); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "approveHITLRequest"); - } - - /** - * Asynchronously approves a HITL approval request. - * - * @param requestId the approval request ID - * @param review the review input containing reviewer details - * @return a future that completes when the request has been approved - */ - public CompletableFuture approveHITLRequestAsync(String requestId, HITLReviewInput review) { - return CompletableFuture.runAsync(() -> approveHITLRequest(requestId, review), asyncExecutor); - } - - /** - * Rejects a HITL approval request. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @param requestId the approval request ID - * @param review the review input containing reviewer details - * @throws AxonFlowException if the rejection fails - */ - public void rejectHITLRequest(String requestId, HITLReviewInput review) { - Objects.requireNonNull(requestId, "requestId cannot be null"); - Objects.requireNonNull(review, "review cannot be null"); - - retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", - "/api/v1/hitl/queue/" + requestId + "/reject", review); - try (Response response = httpClient.newCall(httpRequest).execute()) { - if (!response.isSuccessful()) { - handleErrorResponse(response); - } - return null; - } - }, "rejectHITLRequest"); - } - - /** - * Asynchronously rejects a HITL approval request. - * - * @param requestId the approval request ID - * @param review the review input containing reviewer details - * @return a future that completes when the request has been rejected - */ - public CompletableFuture rejectHITLRequestAsync(String requestId, HITLReviewInput review) { - return CompletableFuture.runAsync(() -> rejectHITLRequest(requestId, review), asyncExecutor); - } - - /** - * Gets HITL dashboard statistics. - * - *

Returns aggregate statistics about the HITL queue including - * total pending requests, priority breakdowns, and age metrics. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - * - * @return the dashboard statistics - * @throws AxonFlowException if the request fails - */ - public HITLStats getHITLStats() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", "/api/v1/hitl/stats", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - JsonNode node = parseResponseNode(response); - - // Server wraps response: {"success": true, "data": {...}} - if (node.has("data") && node.get("data").isObject()) { - return objectMapper.treeToValue(node.get("data"), HITLStats.class); - } - return objectMapper.treeToValue(node, HITLStats.class); - } - }, "getHITLStats"); - } - - /** - * Asynchronously gets HITL dashboard statistics. - * - * @return a future containing the dashboard statistics - */ - public CompletableFuture getHITLStatsAsync() { - return CompletableFuture.supplyAsync(this::getHITLStats, asyncExecutor); - } - - // ======================================================================== - // MAS FEAT Namespace Inner Class - // ======================================================================== - - /** - * MAS FEAT (Monetary Authority of Singapore - Fairness, Ethics, Accountability, - * Transparency) compliance namespace. - * - *

Provides methods for AI system registry, FEAT assessments, and kill switch - * management for Singapore financial services compliance. - * - *

Enterprise Feature: Requires AxonFlow Enterprise license. - */ - public final class MASFEATNamespace { - - private static final String BASE_PATH = "/api/v1/masfeat"; - - /** - * Registers a new AI system in the MAS FEAT registry. - * - * @param request the registration request - * @return the registered system - */ - public AISystemRegistry registerSystem(RegisterSystemRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - // Map SDK field names to backend field names - Map body = new HashMap<>(); - body.put("system_id", request.getSystemId()); - body.put("system_name", request.getSystemName()); - if (request.getDescription() != null) { - body.put("description", request.getDescription()); - } - if (request.getUseCase() != null) { - body.put("use_case", request.getUseCase().getValue()); - } - body.put("owner_team", request.getOwnerTeam()); - if (request.getTechnicalOwner() != null) { - body.put("technical_owner", request.getTechnicalOwner()); - } - // businessOwner maps to owner_email - if (request.getBusinessOwner() != null) { - body.put("owner_email", request.getBusinessOwner()); - } - // Risk rating fields - body.put("risk_rating_impact", request.getCustomerImpact()); - body.put("risk_rating_complexity", request.getModelComplexity()); - body.put("risk_rating_reliance", request.getHumanReliance()); - if (request.getMetadata() != null) { - body.put("metadata", request.getMetadata()); - } - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/registry", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseSystemResponse(response); - } - }, "masfeat.registerSystem"); - } - - /** - * Activates an AI system (changes status to 'active'). - * - * @param systemId the system UUID (not the systemId string) - * @return the activated system - */ - public AISystemRegistry activateSystem(String systemId) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("status", "active"); - - Request httpRequest = buildOrchestratorRequest("PUT", BASE_PATH + "/registry/" + systemId, body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseSystemResponse(response); - } - }, "masfeat.activateSystem"); - } - - /** - * Gets an AI system by its UUID. - * - * @param systemId the system UUID - * @return the system - */ - public AISystemRegistry getSystem(String systemId) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/registry/" + systemId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseSystemResponse(response); - } - }, "masfeat.getSystem"); - } - - /** - * Gets the registry summary statistics. - * - * @return the registry summary - */ - public RegistrySummary getRegistrySummary() { - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/registry/summary", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseSummaryResponse(response); - } - }, "masfeat.getRegistrySummary"); - } - - /** - * Creates a new FEAT assessment. - * - * @param request the assessment creation request - * @return the created assessment - */ - public FEATAssessment createAssessment(CreateAssessmentRequest request) { - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("system_id", request.getSystemId()); - body.put("assessment_type", request.getAssessmentType()); - if (request.getAssessors() != null) { - body.put("assessors", request.getAssessors()); - } - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.createAssessment"); - } - - /** - * Gets a FEAT assessment by its ID. - * - * @param assessmentId the assessment ID - * @return the assessment - */ - public FEATAssessment getAssessment(String assessmentId) { - Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/assessments/" + assessmentId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.getAssessment"); - } - - /** - * Updates a FEAT assessment with pillar scores and details. - * - * @param assessmentId the assessment ID - * @param request the update request - * @return the updated assessment - */ - public FEATAssessment updateAssessment(String assessmentId, UpdateAssessmentRequest request) { - Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - if (request.getFairnessScore() != null) { - body.put("fairness_score", request.getFairnessScore()); - } - if (request.getEthicsScore() != null) { - body.put("ethics_score", request.getEthicsScore()); - } - if (request.getAccountabilityScore() != null) { - body.put("accountability_score", request.getAccountabilityScore()); - } - if (request.getTransparencyScore() != null) { - body.put("transparency_score", request.getTransparencyScore()); - } - if (request.getFairnessDetails() != null) { - body.put("fairness_details", request.getFairnessDetails()); - } - if (request.getEthicsDetails() != null) { - body.put("ethics_details", request.getEthicsDetails()); - } - if (request.getAccountabilityDetails() != null) { - body.put("accountability_details", request.getAccountabilityDetails()); - } - if (request.getTransparencyDetails() != null) { - body.put("transparency_details", request.getTransparencyDetails()); - } - if (request.getFindings() != null) { - body.put("findings", request.getFindings()); - } - if (request.getRecommendations() != null) { - body.put("recommendations", request.getRecommendations()); - } - if (request.getAssessors() != null) { - body.put("assessors", request.getAssessors()); - } - - Request httpRequest = buildOrchestratorRequest("PUT", BASE_PATH + "/assessments/" + assessmentId, body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.updateAssessment"); - } - - /** - * Submits a FEAT assessment for review. - * - * @param assessmentId the assessment ID - * @return the submitted assessment - */ - public FEATAssessment submitAssessment(String assessmentId) { - Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/submit", null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.submitAssessment"); - } - - /** - * Approves a FEAT assessment. - * - * @param assessmentId the assessment ID - * @param request the approval request - * @return the approved assessment - */ - public FEATAssessment approveAssessment(String assessmentId, ApproveAssessmentRequest request) { - Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("approved_by", request.getApprovedBy()); - if (request.getComments() != null) { - body.put("comments", request.getComments()); - } - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/approve", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.approveAssessment"); - } - - /** - * Rejects a FEAT assessment. - * - * @param assessmentId the assessment ID - * @param request the rejection request - * @return the rejected assessment - */ - public FEATAssessment rejectAssessment(String assessmentId, RejectAssessmentRequest request) { - Objects.requireNonNull(assessmentId, "assessmentId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("rejected_by", request.getRejectedBy()); - body.put("reason", request.getReason()); - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/assessments/" + assessmentId + "/reject", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseAssessmentResponse(response); - } - }, "masfeat.rejectAssessment"); - } - - /** - * Gets the kill switch configuration for an AI system. - * - * @param systemId the system ID (string ID, not UUID) - * @return the kill switch configuration - */ - public KillSwitch getKillSwitch(String systemId) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - - return retryExecutor.execute(() -> { - Request httpRequest = buildOrchestratorRequest("GET", BASE_PATH + "/killswitch/" + systemId, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseKillSwitchResponse(response); - } - }, "masfeat.getKillSwitch"); - } - - /** - * Configures the kill switch for an AI system. - * - * @param systemId the system ID (string ID, not UUID) - * @param request the configuration request - * @return the configured kill switch - */ - public KillSwitch configureKillSwitch(String systemId, ConfigureKillSwitchRequest request) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - if (request.getAccuracyThreshold() != null) { - body.put("accuracy_threshold", request.getAccuracyThreshold()); - } - if (request.getBiasThreshold() != null) { - body.put("bias_threshold", request.getBiasThreshold()); - } - if (request.getErrorRateThreshold() != null) { - body.put("error_rate_threshold", request.getErrorRateThreshold()); - } - if (request.getAutoTriggerEnabled() != null) { - body.put("auto_trigger_enabled", request.getAutoTriggerEnabled()); - } - - // Note: configure uses POST, not PUT - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/configure", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseKillSwitchResponse(response); - } - }, "masfeat.configureKillSwitch"); - } - - /** - * Triggers the kill switch for an AI system. - * - * @param systemId the system ID (string ID, not UUID) - * @param request the trigger request - * @return the triggered kill switch - */ - public KillSwitch triggerKillSwitch(String systemId, TriggerKillSwitchRequest request) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("reason", request.getReason()); - if (request.getTriggeredBy() != null) { - body.put("triggered_by", request.getTriggeredBy()); - } - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/trigger", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseKillSwitchResponse(response); - } - }, "masfeat.triggerKillSwitch"); - } - - /** - * Restores the kill switch for an AI system after remediation. - * - * @param systemId the system ID (string ID, not UUID) - * @param request the restore request - * @return the restored kill switch - */ - public KillSwitch restoreKillSwitch(String systemId, RestoreKillSwitchRequest request) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - Objects.requireNonNull(request, "request cannot be null"); - - return retryExecutor.execute(() -> { - Map body = new HashMap<>(); - body.put("reason", request.getReason()); - if (request.getRestoredBy() != null) { - body.put("restored_by", request.getRestoredBy()); - } - - Request httpRequest = buildOrchestratorRequest("POST", BASE_PATH + "/killswitch/" + systemId + "/restore", body); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseKillSwitchResponse(response); - } - }, "masfeat.restoreKillSwitch"); - } - - /** - * Gets the kill switch event history for an AI system. - * - * @param systemId the system ID (string ID, not UUID) - * @param limit maximum number of events to return - * @return list of kill switch events - */ - public List getKillSwitchHistory(String systemId, int limit) { - Objects.requireNonNull(systemId, "systemId cannot be null"); - - return retryExecutor.execute(() -> { - String path = BASE_PATH + "/killswitch/" + systemId + "/history"; - if (limit > 0) { - path += "?limit=" + limit; - } - - Request httpRequest = buildOrchestratorRequest("GET", path, null); - try (Response response = httpClient.newCall(httpRequest).execute()) { - return parseKillSwitchHistoryResponse(response); - } - }, "masfeat.getKillSwitchHistory"); - } - - // ======================================================================== - // Response Parsing Helpers - // ======================================================================== - - private AISystemRegistry parseSystemResponse(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - JsonNode node = objectMapper.readTree(json); - - AISystemRegistry system = new AISystemRegistry(); - system.setId(getTextOrNull(node, "id")); - system.setOrgId(getTextOrNull(node, "org_id")); - system.setSystemId(getTextOrNull(node, "system_id")); - system.setSystemName(getTextOrNull(node, "system_name")); - system.setDescription(getTextOrNull(node, "description")); - system.setOwnerTeam(getTextOrNull(node, "owner_team")); - system.setTechnicalOwner(getTextOrNull(node, "technical_owner")); - system.setBusinessOwner(getTextOrNull(node, "owner_email")); - system.setCreatedBy(getTextOrNull(node, "created_by")); - - // Handle use_case enum - String useCase = getTextOrNull(node, "use_case"); - if (useCase != null) { - try { - system.setUseCase(AISystemUseCase.fromValue(useCase)); - } catch (IllegalArgumentException e) { - logger.warn("Unknown use case: {}", useCase); - } - } - - // Handle risk ratings - system.setCustomerImpact(getIntOrZero(node, "risk_rating_impact")); - system.setModelComplexity(getIntOrZero(node, "risk_rating_complexity")); - system.setHumanReliance(getIntOrZero(node, "risk_rating_reliance")); - - // Handle materiality (may be "materiality" or "materiality_classification") - String materiality = getTextOrNull(node, "materiality"); - if (materiality == null) { - materiality = getTextOrNull(node, "materiality_classification"); - } - if (materiality != null) { - try { - system.setMateriality(MaterialityClassification.fromValue(materiality)); - } catch (IllegalArgumentException e) { - logger.warn("Unknown materiality: {}", materiality); - } - } - - // Handle status - String status = getTextOrNull(node, "status"); - if (status != null) { - try { - system.setStatus(SystemStatus.fromValue(status)); - } catch (IllegalArgumentException e) { - logger.warn("Unknown status: {}", status); - } - } - - // Handle timestamps - system.setCreatedAt(parseInstant(node, "created_at")); - system.setUpdatedAt(parseInstant(node, "updated_at")); - - // Handle metadata - if (node.has("metadata") && !node.get("metadata").isNull()) { - system.setMetadata(objectMapper.convertValue(node.get("metadata"), - new TypeReference>() {})); - } - - return system; - } - - private RegistrySummary parseSummaryResponse(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - JsonNode node = objectMapper.readTree(json); - - RegistrySummary summary = new RegistrySummary(); - summary.setTotalSystems(getIntOrZero(node, "total_systems")); - summary.setActiveSystems(getIntOrZero(node, "active_systems")); - - // Handle high_materiality_count (may be "high_materiality_count" or "high_materiality") - int highMateriality = getIntOrZero(node, "high_materiality_count"); - if (highMateriality == 0) { - highMateriality = getIntOrZero(node, "high_materiality"); - } - summary.setHighMaterialityCount(highMateriality); - - summary.setMediumMaterialityCount(getIntOrZero(node, "medium_materiality_count")); - summary.setLowMaterialityCount(getIntOrZero(node, "low_materiality_count")); - - if (node.has("by_use_case") && !node.get("by_use_case").isNull()) { - summary.setByUseCase(objectMapper.convertValue(node.get("by_use_case"), - new TypeReference>() {})); - } - - if (node.has("by_status") && !node.get("by_status").isNull()) { - summary.setByStatus(objectMapper.convertValue(node.get("by_status"), - new TypeReference>() {})); - } - - return summary; - } - - private FEATAssessment parseAssessmentResponse(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - JsonNode node = objectMapper.readTree(json); - - FEATAssessment assessment = new FEATAssessment(); - assessment.setId(getTextOrNull(node, "id")); - assessment.setOrgId(getTextOrNull(node, "org_id")); - assessment.setSystemId(getTextOrNull(node, "system_id")); - assessment.setAssessmentType(getTextOrNull(node, "assessment_type")); - assessment.setApprovedBy(getTextOrNull(node, "approved_by")); - assessment.setCreatedBy(getTextOrNull(node, "created_by")); - - // Handle status - String status = getTextOrNull(node, "status"); - if (status != null) { - try { - assessment.setStatus(FEATAssessmentStatus.fromValue(status)); - } catch (IllegalArgumentException e) { - logger.warn("Unknown assessment status: {}", status); - } - } - - // Handle scores - assessment.setFairnessScore(getIntegerOrNull(node, "fairness_score")); - assessment.setEthicsScore(getIntegerOrNull(node, "ethics_score")); - assessment.setAccountabilityScore(getIntegerOrNull(node, "accountability_score")); - assessment.setTransparencyScore(getIntegerOrNull(node, "transparency_score")); - - // Overall score may be int or float - if (node.has("overall_score") && !node.get("overall_score").isNull()) { - JsonNode scoreNode = node.get("overall_score"); - if (scoreNode.isNumber()) { - assessment.setOverallScore(scoreNode.asInt()); - } - } - - // Handle timestamps - assessment.setAssessmentDate(parseInstant(node, "assessment_date")); - assessment.setValidUntil(parseInstant(node, "valid_until")); - assessment.setApprovedAt(parseInstant(node, "approved_at")); - assessment.setCreatedAt(parseInstant(node, "created_at")); - assessment.setUpdatedAt(parseInstant(node, "updated_at")); - - // Handle details - if (node.has("fairness_details") && !node.get("fairness_details").isNull()) { - assessment.setFairnessDetails(objectMapper.convertValue(node.get("fairness_details"), - new TypeReference>() {})); - } - if (node.has("ethics_details") && !node.get("ethics_details").isNull()) { - assessment.setEthicsDetails(objectMapper.convertValue(node.get("ethics_details"), - new TypeReference>() {})); - } - if (node.has("accountability_details") && !node.get("accountability_details").isNull()) { - assessment.setAccountabilityDetails(objectMapper.convertValue(node.get("accountability_details"), - new TypeReference>() {})); - } - if (node.has("transparency_details") && !node.get("transparency_details").isNull()) { - assessment.setTransparencyDetails(objectMapper.convertValue(node.get("transparency_details"), - new TypeReference>() {})); - } - - // Handle assessors - if (node.has("assessors") && node.get("assessors").isArray()) { - assessment.setAssessors(objectMapper.convertValue(node.get("assessors"), - new TypeReference>() {})); - } - - // Handle recommendations - if (node.has("recommendations") && node.get("recommendations").isArray()) { - assessment.setRecommendations(objectMapper.convertValue(node.get("recommendations"), - new TypeReference>() {})); - } - - return assessment; - } - - private KillSwitch parseKillSwitchResponse(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - JsonNode node = objectMapper.readTree(json); - - // Handle nested response: {"kill_switch": {...}, "message": "..."} - if (node.has("kill_switch") && !node.get("kill_switch").isNull()) { - node = node.get("kill_switch"); - } - - KillSwitch ks = new KillSwitch(); - ks.setId(getTextOrNull(node, "id")); - ks.setOrgId(getTextOrNull(node, "org_id")); - ks.setSystemId(getTextOrNull(node, "system_id")); - ks.setTriggeredBy(getTextOrNull(node, "triggered_by")); - ks.setRestoredBy(getTextOrNull(node, "restored_by")); - - // Handle triggered_reason (may be "triggered_reason" or "trigger_reason") - String triggeredReason = getTextOrNull(node, "triggered_reason"); - if (triggeredReason == null) { - triggeredReason = getTextOrNull(node, "trigger_reason"); - } - ks.setTriggeredReason(triggeredReason); - - // Handle status - String status = getTextOrNull(node, "status"); - if (status != null) { - try { - ks.setStatus(KillSwitchStatus.fromValue(status)); - } catch (IllegalArgumentException e) { - logger.warn("Unknown kill switch status: {}", status); - } - } - - // Handle auto_trigger - if (node.has("auto_trigger_enabled") && !node.get("auto_trigger_enabled").isNull()) { - ks.setAutoTriggerEnabled(node.get("auto_trigger_enabled").asBoolean()); - } - - // Handle thresholds - ks.setAccuracyThreshold(getDoubleOrNull(node, "accuracy_threshold")); - ks.setBiasThreshold(getDoubleOrNull(node, "bias_threshold")); - ks.setErrorRateThreshold(getDoubleOrNull(node, "error_rate_threshold")); - - // Handle timestamps - ks.setTriggeredAt(parseInstant(node, "triggered_at")); - ks.setRestoredAt(parseInstant(node, "restored_at")); - ks.setCreatedAt(parseInstant(node, "created_at")); - ks.setUpdatedAt(parseInstant(node, "updated_at")); - - return ks; - } - - private List parseKillSwitchHistoryResponse(Response response) throws IOException { - handleErrorResponse(response); - - ResponseBody body = response.body(); - if (body == null) { - throw new AxonFlowException("Empty response body", response.code(), null); - } - - String json = body.string(); - JsonNode node = objectMapper.readTree(json); - - // Handle nested response: {"history": [...]} or direct array - JsonNode eventsNode; - if (node.has("history") && node.get("history").isArray()) { - eventsNode = node.get("history"); - } else if (node.has("events") && node.get("events").isArray()) { - eventsNode = node.get("events"); - } else if (node.isArray()) { - eventsNode = node; - } else { - return new ArrayList<>(); - } - - List events = new ArrayList<>(); - for (JsonNode eventNode : eventsNode) { - KillSwitchEvent event = new KillSwitchEvent(); - event.setId(getTextOrNull(eventNode, "id")); - event.setKillSwitchId(getTextOrNull(eventNode, "kill_switch_id")); - - // Handle event_type (may be "event_type" or "action") - String eventType = getTextOrNull(eventNode, "event_type"); - if (eventType == null) { - eventType = getTextOrNull(eventNode, "action"); - } - event.setEventType(eventType); - - // Handle created_by (may be "created_by" or "performed_by") - String createdBy = getTextOrNull(eventNode, "created_by"); - if (createdBy == null) { - createdBy = getTextOrNull(eventNode, "performed_by"); - } - event.setCreatedBy(createdBy); - - // Handle created_at (may be "created_at" or "performed_at") - java.time.Instant createdAt = parseInstant(eventNode, "created_at"); - if (createdAt == null) { - createdAt = parseInstant(eventNode, "performed_at"); - } - event.setCreatedAt(createdAt); - - // Handle event_data - if (eventNode.has("event_data") && !eventNode.get("event_data").isNull()) { - event.setEventData(objectMapper.convertValue(eventNode.get("event_data"), - new TypeReference>() {})); - } else { - // Build event_data from individual fields if present - Map eventData = new HashMap<>(); - String prevStatus = getTextOrNull(eventNode, "previous_status"); - String newStatus = getTextOrNull(eventNode, "new_status"); - String reason = getTextOrNull(eventNode, "reason"); - if (prevStatus != null) eventData.put("previous_status", prevStatus); - if (newStatus != null) eventData.put("new_status", newStatus); - if (reason != null) eventData.put("reason", reason); - if (!eventData.isEmpty()) { - event.setEventData(eventData); - } - } - - events.add(event); - } - - return events; - } - - // ======================================================================== - // JSON Helper Methods - // ======================================================================== - - private String getTextOrNull(JsonNode node, String field) { - if (node.has(field) && !node.get(field).isNull()) { - return node.get(field).asText(); - } - return null; - } - - private int getIntOrZero(JsonNode node, String field) { - if (node.has(field) && !node.get(field).isNull()) { - return node.get(field).asInt(); - } - return 0; - } - - private Integer getIntegerOrNull(JsonNode node, String field) { - if (node.has(field) && !node.get(field).isNull()) { - return node.get(field).asInt(); - } - return null; - } - - private Double getDoubleOrNull(JsonNode node, String field) { - if (node.has(field) && !node.get(field).isNull()) { - return node.get(field).asDouble(); - } - return null; - } - - private java.time.Instant parseInstant(JsonNode node, String field) { - if (node.has(field) && !node.get(field).isNull()) { - String value = node.get(field).asText(); - try { - return java.time.Instant.parse(value); - } catch (Exception e) { - logger.warn("Failed to parse timestamp '{}': {}", value, e.getMessage()); - } - } - return null; + private java.time.Instant parseInstant(JsonNode node, String field) { + if (node.has(field) && !node.get(field).isNull()) { + String value = node.get(field).asText(); + try { + return java.time.Instant.parse(value); + } catch (Exception e) { + logger.warn("Failed to parse timestamp '{}': {}", value, e.getMessage()); } - } - - @Override - public void close() { - httpClient.dispatcher().executorService().shutdown(); - httpClient.connectionPool().evictAll(); - cache.clear(); - logger.info("AxonFlow client closed"); - } + } + return null; + } + } + + @Override + public void close() { + httpClient.dispatcher().executorService().shutdown(); + httpClient.connectionPool().evictAll(); + cache.clear(); + logger.info("AxonFlow client closed"); + } } diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java index bb06490..2d7033a 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlowConfig.java @@ -17,9 +17,8 @@ import com.getaxonflow.sdk.exceptions.ConfigurationException; import com.getaxonflow.sdk.types.Mode; -import com.getaxonflow.sdk.util.RetryConfig; import com.getaxonflow.sdk.util.CacheConfig; - +import com.getaxonflow.sdk.util.RetryConfig; import java.io.InputStream; import java.time.Duration; import java.util.Objects; @@ -29,6 +28,7 @@ * Configuration for the AxonFlow client. * *

Use the builder to create a configuration: + * *

{@code
  * AxonFlowConfig config = AxonFlowConfig.builder()
  *     .endpoint("http://localhost:8080")
@@ -38,423 +38,432 @@
  * }
* *

Configuration can also be loaded from environment variables: + * *

{@code
  * AxonFlowConfig config = AxonFlowConfig.fromEnvironment();
  * }
*/ public final class AxonFlowConfig { - /** SDK version string, read from Maven pom.properties at runtime. */ - public static final String SDK_VERSION = detectSdkVersion(); - - private static String detectSdkVersion() { - // Try Maven-generated pom.properties (available in packaged JAR) - try (InputStream is = AxonFlowConfig.class.getResourceAsStream( - "/META-INF/maven/com.getaxonflow/axonflow-sdk/pom.properties")) { - if (is != null) { - Properties props = new Properties(); - props.load(is); - String version = props.getProperty("version"); - if (version != null && !version.isEmpty()) { - return version; - } - } - } catch (Exception ignored) { - // Fall through to manifest check - } - // Try JAR manifest Implementation-Version - Package pkg = AxonFlowConfig.class.getPackage(); - if (pkg != null && pkg.getImplementationVersion() != null) { - return pkg.getImplementationVersion(); + /** SDK version string, read from Maven pom.properties at runtime. */ + public static final String SDK_VERSION = detectSdkVersion(); + + private static String detectSdkVersion() { + // Try Maven-generated pom.properties (available in packaged JAR) + try (InputStream is = + AxonFlowConfig.class.getResourceAsStream( + "/META-INF/maven/com.getaxonflow/axonflow-sdk/pom.properties")) { + if (is != null) { + Properties props = new Properties(); + props.load(is); + String version = props.getProperty("version"); + if (version != null && !version.isEmpty()) { + return version; } - // Fallback — "unknown" avoids hardcoded version drift - return "unknown"; + } + } catch (Exception ignored) { + // Fall through to manifest check + } + // Try JAR manifest Implementation-Version + Package pkg = AxonFlowConfig.class.getPackage(); + if (pkg != null && pkg.getImplementationVersion() != null) { + return pkg.getImplementationVersion(); + } + // Fallback — "unknown" avoids hardcoded version drift + return "unknown"; + } + + /** Default timeout for HTTP requests. */ + public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(60); + + /** Default endpoint URL. */ + public static final String DEFAULT_ENDPOINT = "http://localhost:8080"; + + private final String endpoint; + private final String clientId; + private final String clientSecret; + private final Mode mode; + private final Duration timeout; + private final boolean debug; + private final boolean insecureSkipVerify; + private final RetryConfig retryConfig; + private final CacheConfig cacheConfig; + private final String userAgent; + private final Boolean telemetry; + + private AxonFlowConfig(Builder builder) { + this.endpoint = normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT); + this.clientId = builder.clientId; + this.clientSecret = builder.clientSecret; + this.mode = builder.mode != null ? builder.mode : Mode.PRODUCTION; + this.timeout = builder.timeout != null ? builder.timeout : DEFAULT_TIMEOUT; + this.debug = builder.debug; + this.insecureSkipVerify = builder.insecureSkipVerify; + this.retryConfig = builder.retryConfig != null ? builder.retryConfig : RetryConfig.defaults(); + this.cacheConfig = builder.cacheConfig != null ? builder.cacheConfig : CacheConfig.defaults(); + this.userAgent = + builder.userAgent != null ? builder.userAgent : "axonflow-sdk-java/" + SDK_VERSION; + this.telemetry = builder.telemetry; + + validate(); + } + + private void validate() { + if (endpoint == null || endpoint.isEmpty()) { + throw new ConfigurationException("endpoint is required", "endpoint"); + } + // Credentials are optional for community/self-hosted deployments + // Enterprise features require credentials (validated at method call time) + } + + /** + * Checks if credentials are configured. + * + *

Returns true if clientId is set. clientSecret is optional for community mode but required + * for enterprise. + * + * @return true if clientId is available + */ + public boolean hasCredentials() { + return clientId != null && !clientId.isEmpty(); + } + + private String normalizeUrl(String url) { + if (url == null) return null; + return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + } + + /** + * Checks if the configured endpoint is localhost. + * + * @return true if connecting to localhost + */ + public boolean isLocalhost() { + return endpoint != null + && (endpoint.contains("localhost") + || endpoint.contains("127.0.0.1") + || endpoint.contains("[::1]")); + } + + /** + * Creates a configuration from environment variables. + * + *

Supported environment variables: + * + *

    + *
  • AXONFLOW_AGENT_URL - The endpoint URL (kept for backwards compatibility) + *
  • AXONFLOW_CLIENT_ID - The client ID + *
  • AXONFLOW_CLIENT_SECRET - The client secret + *
  • AXONFLOW_MODE - Operating mode (production/sandbox) + *
  • AXONFLOW_TIMEOUT_SECONDS - Request timeout in seconds + *
  • AXONFLOW_DEBUG - Enable debug mode (true/false) + *
+ * + * @return a new configuration based on environment variables + */ + public static AxonFlowConfig fromEnvironment() { + Builder builder = builder(); + + // Keep AXONFLOW_AGENT_URL for backwards compatibility, map to endpoint + String endpoint = System.getenv("AXONFLOW_AGENT_URL"); + if (endpoint != null && !endpoint.isEmpty()) { + builder.endpoint(endpoint); } - /** Default timeout for HTTP requests. */ - public static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(60); - - /** Default endpoint URL. */ - public static final String DEFAULT_ENDPOINT = "http://localhost:8080"; - - private final String endpoint; - private final String clientId; - private final String clientSecret; - private final Mode mode; - private final Duration timeout; - private final boolean debug; - private final boolean insecureSkipVerify; - private final RetryConfig retryConfig; - private final CacheConfig cacheConfig; - private final String userAgent; - private final Boolean telemetry; - - private AxonFlowConfig(Builder builder) { - this.endpoint = normalizeUrl(builder.endpoint != null ? builder.endpoint : DEFAULT_ENDPOINT); - this.clientId = builder.clientId; - this.clientSecret = builder.clientSecret; - this.mode = builder.mode != null ? builder.mode : Mode.PRODUCTION; - this.timeout = builder.timeout != null ? builder.timeout : DEFAULT_TIMEOUT; - this.debug = builder.debug; - this.insecureSkipVerify = builder.insecureSkipVerify; - this.retryConfig = builder.retryConfig != null ? builder.retryConfig : RetryConfig.defaults(); - this.cacheConfig = builder.cacheConfig != null ? builder.cacheConfig : CacheConfig.defaults(); - this.userAgent = builder.userAgent != null ? builder.userAgent : "axonflow-sdk-java/" + SDK_VERSION; - this.telemetry = builder.telemetry; - - validate(); + String clientId = System.getenv("AXONFLOW_CLIENT_ID"); + if (clientId != null && !clientId.isEmpty()) { + builder.clientId(clientId); } - private void validate() { - if (endpoint == null || endpoint.isEmpty()) { - throw new ConfigurationException("endpoint is required", "endpoint"); - } - // Credentials are optional for community/self-hosted deployments - // Enterprise features require credentials (validated at method call time) + String clientSecret = System.getenv("AXONFLOW_CLIENT_SECRET"); + if (clientSecret != null && !clientSecret.isEmpty()) { + builder.clientSecret(clientSecret); } - /** - * Checks if credentials are configured. - * - *

Returns true if clientId is set. - * clientSecret is optional for community mode but required for enterprise. - * - * @return true if clientId is available - */ - public boolean hasCredentials() { - return clientId != null && !clientId.isEmpty(); + String modeStr = System.getenv("AXONFLOW_MODE"); + if (modeStr != null && !modeStr.isEmpty()) { + builder.mode(Mode.fromValue(modeStr)); + } + + String timeoutStr = System.getenv("AXONFLOW_TIMEOUT_SECONDS"); + if (timeoutStr != null && !timeoutStr.isEmpty()) { + try { + builder.timeout(Duration.ofSeconds(Long.parseLong(timeoutStr))); + } catch (NumberFormatException e) { + // Ignore invalid timeout, use default + } } - private String normalizeUrl(String url) { - if (url == null) return null; - return url.endsWith("/") ? url.substring(0, url.length() - 1) : url; + String debugStr = System.getenv("AXONFLOW_DEBUG"); + if ("true".equalsIgnoreCase(debugStr)) { + builder.debug(true); } + return builder.build(); + } + + public String getEndpoint() { + return endpoint; + } + + public String getClientId() { + return clientId; + } + + public String getClientSecret() { + return clientSecret; + } + + public Mode getMode() { + return mode; + } + + public Duration getTimeout() { + return timeout; + } + + public boolean isDebug() { + return debug; + } + + public boolean isInsecureSkipVerify() { + return insecureSkipVerify; + } + + public RetryConfig getRetryConfig() { + return retryConfig; + } + + public CacheConfig getCacheConfig() { + return cacheConfig; + } + + public String getUserAgent() { + return userAgent; + } + + /** + * Returns the telemetry config override. + * + *

{@code null} means use the default behavior (ON for production, OFF for sandbox). {@code + * Boolean.TRUE} forces telemetry on, {@code Boolean.FALSE} forces it off. + * + * @return the telemetry override, or null for default behavior + */ + public Boolean getTelemetry() { + return telemetry; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AxonFlowConfig that = (AxonFlowConfig) o; + return debug == that.debug + && insecureSkipVerify == that.insecureSkipVerify + && Objects.equals(endpoint, that.endpoint) + && Objects.equals(clientId, that.clientId) + && mode == that.mode; + } + + @Override + public int hashCode() { + return Objects.hash(endpoint, clientId, mode, debug, insecureSkipVerify); + } + + @Override + public String toString() { + return "AxonFlowConfig{" + + "endpoint='" + + endpoint + + '\'' + + ", clientId='" + + clientId + + '\'' + + ", mode=" + + mode + + ", timeout=" + + timeout + + ", debug=" + + debug + + '}'; + } + + /** Builder for AxonFlowConfig. */ + public static final class Builder { + private String endpoint; + private String clientId; + private String clientSecret; + private Mode mode; + private Duration timeout; + private boolean debug; + private boolean insecureSkipVerify; + private RetryConfig retryConfig; + private CacheConfig cacheConfig; + private String userAgent; + private Boolean telemetry; + + private Builder() {} + /** - * Checks if the configured endpoint is localhost. + * Sets the AxonFlow endpoint URL. All routes now go through a single endpoint (ADR-026 Single + * Entry Point). * - * @return true if connecting to localhost + * @param endpoint the endpoint URL + * @return this builder */ - public boolean isLocalhost() { - return endpoint != null && ( - endpoint.contains("localhost") || - endpoint.contains("127.0.0.1") || - endpoint.contains("[::1]") - ); + public Builder endpoint(String endpoint) { + this.endpoint = endpoint; + return this; } /** - * Creates a configuration from environment variables. - * - *

Supported environment variables: - *

    - *
  • AXONFLOW_AGENT_URL - The endpoint URL (kept for backwards compatibility)
  • - *
  • AXONFLOW_CLIENT_ID - The client ID
  • - *
  • AXONFLOW_CLIENT_SECRET - The client secret
  • - *
  • AXONFLOW_MODE - Operating mode (production/sandbox)
  • - *
  • AXONFLOW_TIMEOUT_SECONDS - Request timeout in seconds
  • - *
  • AXONFLOW_DEBUG - Enable debug mode (true/false)
  • - *
+ * Sets the AxonFlow Agent URL. * - * @return a new configuration based on environment variables + * @deprecated Use {@link #endpoint(String)} instead. This method is kept for backwards + * compatibility. + * @param agentUrl the Agent URL + * @return this builder */ - public static AxonFlowConfig fromEnvironment() { - Builder builder = builder(); - - // Keep AXONFLOW_AGENT_URL for backwards compatibility, map to endpoint - String endpoint = System.getenv("AXONFLOW_AGENT_URL"); - if (endpoint != null && !endpoint.isEmpty()) { - builder.endpoint(endpoint); - } - - String clientId = System.getenv("AXONFLOW_CLIENT_ID"); - if (clientId != null && !clientId.isEmpty()) { - builder.clientId(clientId); - } - - String clientSecret = System.getenv("AXONFLOW_CLIENT_SECRET"); - if (clientSecret != null && !clientSecret.isEmpty()) { - builder.clientSecret(clientSecret); - } - - String modeStr = System.getenv("AXONFLOW_MODE"); - if (modeStr != null && !modeStr.isEmpty()) { - builder.mode(Mode.fromValue(modeStr)); - } - - String timeoutStr = System.getenv("AXONFLOW_TIMEOUT_SECONDS"); - if (timeoutStr != null && !timeoutStr.isEmpty()) { - try { - builder.timeout(Duration.ofSeconds(Long.parseLong(timeoutStr))); - } catch (NumberFormatException e) { - // Ignore invalid timeout, use default - } - } - - String debugStr = System.getenv("AXONFLOW_DEBUG"); - if ("true".equalsIgnoreCase(debugStr)) { - builder.debug(true); - } - - return builder.build(); + @Deprecated + public Builder agentUrl(String agentUrl) { + this.endpoint = agentUrl; + return this; } - public String getEndpoint() { - return endpoint; - } + // Note: portalUrl() and orchestratorUrl() methods were removed in v2.0.0 + // All routes now go through a single endpoint (ADR-026 Single Entry Point) - public String getClientId() { - return clientId; + /** + * Sets the client ID for authentication. + * + * @param clientId the client ID + * @return this builder + */ + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; } - public String getClientSecret() { - return clientSecret; + /** + * Sets the client secret for authentication. + * + * @param clientSecret the client secret + * @return this builder + */ + public Builder clientSecret(String clientSecret) { + this.clientSecret = clientSecret; + return this; } - public Mode getMode() { - return mode; + /** + * Sets the operating mode. + * + * @param mode the mode (PRODUCTION or SANDBOX) + * @return this builder + */ + public Builder mode(Mode mode) { + this.mode = mode; + return this; } - public Duration getTimeout() { - return timeout; + /** + * Sets the request timeout. + * + * @param timeout the timeout duration + * @return this builder + */ + public Builder timeout(Duration timeout) { + this.timeout = timeout; + return this; } - public boolean isDebug() { - return debug; + /** + * Enables debug mode for verbose logging. + * + * @param debug true to enable debug mode + * @return this builder + */ + public Builder debug(boolean debug) { + this.debug = debug; + return this; } - public boolean isInsecureSkipVerify() { - return insecureSkipVerify; + /** + * Skips SSL certificate verification. + * + *

Warning: Only use this in development/testing. + * + * @param insecureSkipVerify true to skip verification + * @return this builder + */ + public Builder insecureSkipVerify(boolean insecureSkipVerify) { + this.insecureSkipVerify = insecureSkipVerify; + return this; } - public RetryConfig getRetryConfig() { - return retryConfig; + /** + * Sets the retry configuration. + * + * @param retryConfig the retry configuration + * @return this builder + */ + public Builder retryConfig(RetryConfig retryConfig) { + this.retryConfig = retryConfig; + return this; } - public CacheConfig getCacheConfig() { - return cacheConfig; + /** + * Sets the cache configuration. + * + * @param cacheConfig the cache configuration + * @return this builder + */ + public Builder cacheConfig(CacheConfig cacheConfig) { + this.cacheConfig = cacheConfig; + return this; } - public String getUserAgent() { - return userAgent; + /** + * Sets a custom user agent string. + * + * @param userAgent the user agent string + * @return this builder + */ + public Builder userAgent(String userAgent) { + this.userAgent = userAgent; + return this; } /** - * Returns the telemetry config override. + * Sets the telemetry override. * - *

{@code null} means use the default behavior (ON for production, OFF for sandbox). + *

{@code null} (default) uses the mode-based default: ON for production, OFF for sandbox. * {@code Boolean.TRUE} forces telemetry on, {@code Boolean.FALSE} forces it off. * - * @return the telemetry override, or null for default behavior + *

Telemetry can also be disabled globally via environment variables: {@code DO_NOT_TRACK=1} + * or {@code AXONFLOW_TELEMETRY=off}. + * + * @param telemetry true to enable, false to disable, null for default behavior + * @return this builder */ - public Boolean getTelemetry() { - return telemetry; - } - - public static Builder builder() { - return new Builder(); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AxonFlowConfig that = (AxonFlowConfig) o; - return debug == that.debug && - insecureSkipVerify == that.insecureSkipVerify && - Objects.equals(endpoint, that.endpoint) && - Objects.equals(clientId, that.clientId) && - mode == that.mode; - } - - @Override - public int hashCode() { - return Objects.hash(endpoint, clientId, mode, debug, insecureSkipVerify); - } - - @Override - public String toString() { - return "AxonFlowConfig{" + - "endpoint='" + endpoint + '\'' + - ", clientId='" + clientId + '\'' + - ", mode=" + mode + - ", timeout=" + timeout + - ", debug=" + debug + - '}'; + public Builder telemetry(Boolean telemetry) { + this.telemetry = telemetry; + return this; } /** - * Builder for AxonFlowConfig. + * Builds the configuration. + * + * @return a new AxonFlowConfig instance + * @throws ConfigurationException if the configuration is invalid */ - public static final class Builder { - private String endpoint; - private String clientId; - private String clientSecret; - private Mode mode; - private Duration timeout; - private boolean debug; - private boolean insecureSkipVerify; - private RetryConfig retryConfig; - private CacheConfig cacheConfig; - private String userAgent; - private Boolean telemetry; - - private Builder() {} - - /** - * Sets the AxonFlow endpoint URL. - * All routes now go through a single endpoint (ADR-026 Single Entry Point). - * - * @param endpoint the endpoint URL - * @return this builder - */ - public Builder endpoint(String endpoint) { - this.endpoint = endpoint; - return this; - } - - /** - * Sets the AxonFlow Agent URL. - * @deprecated Use {@link #endpoint(String)} instead. This method is kept for backwards compatibility. - * - * @param agentUrl the Agent URL - * @return this builder - */ - @Deprecated - public Builder agentUrl(String agentUrl) { - this.endpoint = agentUrl; - return this; - } - - // Note: portalUrl() and orchestratorUrl() methods were removed in v2.0.0 - // All routes now go through a single endpoint (ADR-026 Single Entry Point) - - /** - * Sets the client ID for authentication. - * - * @param clientId the client ID - * @return this builder - */ - public Builder clientId(String clientId) { - this.clientId = clientId; - return this; - } - - /** - * Sets the client secret for authentication. - * - * @param clientSecret the client secret - * @return this builder - */ - public Builder clientSecret(String clientSecret) { - this.clientSecret = clientSecret; - return this; - } - - /** - * Sets the operating mode. - * - * @param mode the mode (PRODUCTION or SANDBOX) - * @return this builder - */ - public Builder mode(Mode mode) { - this.mode = mode; - return this; - } - - /** - * Sets the request timeout. - * - * @param timeout the timeout duration - * @return this builder - */ - public Builder timeout(Duration timeout) { - this.timeout = timeout; - return this; - } - - /** - * Enables debug mode for verbose logging. - * - * @param debug true to enable debug mode - * @return this builder - */ - public Builder debug(boolean debug) { - this.debug = debug; - return this; - } - - /** - * Skips SSL certificate verification. - * - *

Warning: Only use this in development/testing. - * - * @param insecureSkipVerify true to skip verification - * @return this builder - */ - public Builder insecureSkipVerify(boolean insecureSkipVerify) { - this.insecureSkipVerify = insecureSkipVerify; - return this; - } - - /** - * Sets the retry configuration. - * - * @param retryConfig the retry configuration - * @return this builder - */ - public Builder retryConfig(RetryConfig retryConfig) { - this.retryConfig = retryConfig; - return this; - } - - /** - * Sets the cache configuration. - * - * @param cacheConfig the cache configuration - * @return this builder - */ - public Builder cacheConfig(CacheConfig cacheConfig) { - this.cacheConfig = cacheConfig; - return this; - } - - /** - * Sets a custom user agent string. - * - * @param userAgent the user agent string - * @return this builder - */ - public Builder userAgent(String userAgent) { - this.userAgent = userAgent; - return this; - } - - /** - * Sets the telemetry override. - * - *

{@code null} (default) uses the mode-based default: ON for production, OFF for sandbox. - * {@code Boolean.TRUE} forces telemetry on, {@code Boolean.FALSE} forces it off. - * - *

Telemetry can also be disabled globally via environment variables: - * {@code DO_NOT_TRACK=1} or {@code AXONFLOW_TELEMETRY=off}. - * - * @param telemetry true to enable, false to disable, null for default behavior - * @return this builder - */ - public Builder telemetry(Boolean telemetry) { - this.telemetry = telemetry; - return this; - } - - /** - * Builds the configuration. - * - * @return a new AxonFlowConfig instance - * @throws ConfigurationException if the configuration is invalid - */ - public AxonFlowConfig build() { - return new AxonFlowConfig(this); - } + public AxonFlowConfig build() { + return new AxonFlowConfig(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/CheckGateOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/CheckGateOptions.java index ea3352f..e079dd5 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/CheckGateOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/CheckGateOptions.java @@ -16,89 +16,85 @@ package com.getaxonflow.sdk.adapters; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.ToolContext; - import java.util.Map; -/** - * Options for {@link LangGraphAdapter#checkGate}. - */ +/** Options for {@link LangGraphAdapter#checkGate}. */ public final class CheckGateOptions { - private final String stepId; - private final Map stepInput; - private final String model; - private final String provider; - private final ToolContext toolContext; - - private CheckGateOptions(Builder builder) { - this.stepId = builder.stepId; - this.stepInput = builder.stepInput; - this.model = builder.model; - this.provider = builder.provider; - this.toolContext = builder.toolContext; - } - - public String getStepId() { - return stepId; - } - - public Map getStepInput() { - return stepInput; + private final String stepId; + private final Map stepInput; + private final String model; + private final String provider; + private final ToolContext toolContext; + + private CheckGateOptions(Builder builder) { + this.stepId = builder.stepId; + this.stepInput = builder.stepInput; + this.model = builder.model; + this.provider = builder.provider; + this.toolContext = builder.toolContext; + } + + public String getStepId() { + return stepId; + } + + public Map getStepInput() { + return stepInput; + } + + public String getModel() { + return model; + } + + public String getProvider() { + return provider; + } + + public ToolContext getToolContext() { + return toolContext; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String stepId; + private Map stepInput; + private String model; + private String provider; + private ToolContext toolContext; + + private Builder() {} + + public Builder stepId(String stepId) { + this.stepId = stepId; + return this; } - public String getModel() { - return model; + public Builder stepInput(Map stepInput) { + this.stepInput = stepInput; + return this; } - public String getProvider() { - return provider; + public Builder model(String model) { + this.model = model; + return this; } - public ToolContext getToolContext() { - return toolContext; + public Builder provider(String provider) { + this.provider = provider; + return this; } - public static Builder builder() { - return new Builder(); + public Builder toolContext(ToolContext toolContext) { + this.toolContext = toolContext; + return this; } - public static final class Builder { - private String stepId; - private Map stepInput; - private String model; - private String provider; - private ToolContext toolContext; - - private Builder() { - } - - public Builder stepId(String stepId) { - this.stepId = stepId; - return this; - } - - public Builder stepInput(Map stepInput) { - this.stepInput = stepInput; - return this; - } - - public Builder model(String model) { - this.model = model; - return this; - } - - public Builder provider(String provider) { - this.provider = provider; - return this; - } - - public Builder toolContext(ToolContext toolContext) { - this.toolContext = toolContext; - return this; - } - - public CheckGateOptions build() { - return new CheckGateOptions(this); - } + public CheckGateOptions build() { + return new CheckGateOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/CheckToolGateOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/CheckToolGateOptions.java index 82ad9a8..d6ebfd3 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/CheckToolGateOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/CheckToolGateOptions.java @@ -17,86 +17,83 @@ import java.util.Map; -/** - * Options for {@link LangGraphAdapter#checkToolGate}. - */ +/** Options for {@link LangGraphAdapter#checkToolGate}. */ public final class CheckToolGateOptions { - private final String stepName; - private final String stepId; - private final Map toolInput; - private final String model; - private final String provider; - - private CheckToolGateOptions(Builder builder) { - this.stepName = builder.stepName; - this.stepId = builder.stepId; - this.toolInput = builder.toolInput; - this.model = builder.model; - this.provider = builder.provider; - } - - public String getStepName() { - return stepName; - } - - public String getStepId() { - return stepId; + private final String stepName; + private final String stepId; + private final Map toolInput; + private final String model; + private final String provider; + + private CheckToolGateOptions(Builder builder) { + this.stepName = builder.stepName; + this.stepId = builder.stepId; + this.toolInput = builder.toolInput; + this.model = builder.model; + this.provider = builder.provider; + } + + public String getStepName() { + return stepName; + } + + public String getStepId() { + return stepId; + } + + public Map getToolInput() { + return toolInput; + } + + public String getModel() { + return model; + } + + public String getProvider() { + return provider; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String stepName; + private String stepId; + private Map toolInput; + private String model; + private String provider; + + private Builder() {} + + public Builder stepName(String stepName) { + this.stepName = stepName; + return this; } - public Map getToolInput() { - return toolInput; + public Builder stepId(String stepId) { + this.stepId = stepId; + return this; } - public String getModel() { - return model; + public Builder toolInput(Map toolInput) { + this.toolInput = toolInput; + return this; } - public String getProvider() { - return provider; + public Builder model(String model) { + this.model = model; + return this; } - public static Builder builder() { - return new Builder(); + public Builder provider(String provider) { + this.provider = provider; + return this; } - public static final class Builder { - private String stepName; - private String stepId; - private Map toolInput; - private String model; - private String provider; - - private Builder() { - } - - public Builder stepName(String stepName) { - this.stepName = stepName; - return this; - } - - public Builder stepId(String stepId) { - this.stepId = stepId; - return this; - } - - public Builder toolInput(Map toolInput) { - this.toolInput = toolInput; - return this; - } - - public Builder model(String model) { - this.model = model; - return this; - } - - public Builder provider(String provider) { - this.provider = provider; - return this; - } - - public CheckToolGateOptions build() { - return new CheckToolGateOptions(this); - } + public CheckToolGateOptions build() { + return new CheckToolGateOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java index b974410..fa7569c 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/LangGraphAdapter.java @@ -28,7 +28,6 @@ import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowSource; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatusResponse; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStepInfo; - import java.util.Collections; import java.util.Map; import java.util.Objects; @@ -38,17 +37,17 @@ /** * Wraps LangGraph workflows with AxonFlow governance gates. * - *

This adapter provides a simple interface for integrating AxonFlow's - * Workflow Control Plane with LangGraph workflows. It handles workflow - * registration, step gate checks, per-tool governance, MCP tool interception, - * and workflow lifecycle management. + *

This adapter provides a simple interface for integrating AxonFlow's Workflow Control Plane + * with LangGraph workflows. It handles workflow registration, step gate checks, per-tool + * governance, MCP tool interception, and workflow lifecycle management. * - *

Thread safety: This class is not thread-safe. Each workflow - * execution should use its own adapter instance from a single thread. + *

Thread safety: This class is not thread-safe. Each workflow execution should + * use its own adapter instance from a single thread. * *

"LangGraph runs the workflow. AxonFlow decides when it's allowed to move forward." * *

Example usage: + * *

{@code
  * try (LangGraphAdapter adapter = LangGraphAdapter.builder(client, "code-review")
  *         .autoBlock(true)
@@ -69,498 +68,497 @@
  */
 public final class LangGraphAdapter implements AutoCloseable {
 
-    private final AxonFlow client;
-    private final String workflowName;
-    private final WorkflowSource source;
-    private final boolean autoBlock;
-    private String workflowId;
-    private int stepCounter;
-    private boolean closedNormally;
-
-    private LangGraphAdapter(Builder builder) {
-        this.client = builder.client;
-        this.workflowName = builder.workflowName;
-        this.source = builder.source;
-        this.autoBlock = builder.autoBlock;
+  private final AxonFlow client;
+  private final String workflowName;
+  private final WorkflowSource source;
+  private final boolean autoBlock;
+  private String workflowId;
+  private int stepCounter;
+  private boolean closedNormally;
+
+  private LangGraphAdapter(Builder builder) {
+    this.client = builder.client;
+    this.workflowName = builder.workflowName;
+    this.source = builder.source;
+    this.autoBlock = builder.autoBlock;
+  }
+
+  /**
+   * Creates a new builder for a LangGraphAdapter.
+   *
+   * @param client the AxonFlow client instance
+   * @param workflowName human-readable name for the workflow
+   * @return a new builder
+   */
+  public static Builder builder(AxonFlow client, String workflowName) {
+    return new Builder(client, workflowName);
+  }
+
+  // ========================================================================
+  // Workflow Lifecycle
+  // ========================================================================
+
+  /**
+   * Registers the workflow with AxonFlow.
+   *
+   * 

Call this at the start of your LangGraph workflow execution. + * + * @param metadata additional workflow metadata (may be null) + * @param traceId external trace ID for correlation (may be null) + * @return the assigned workflow ID + */ + public String startWorkflow(Map metadata, String traceId) { + CreateWorkflowRequest request = + new CreateWorkflowRequest( + workflowName, source, metadata != null ? metadata : Collections.emptyMap(), traceId); + + CreateWorkflowResponse response = client.createWorkflow(request); + this.workflowId = response.getWorkflowId(); + return this.workflowId; + } + + /** + * Registers the workflow with AxonFlow using default metadata. + * + * @return the assigned workflow ID + */ + public String startWorkflow() { + return startWorkflow(null, null); + } + + /** + * Marks the workflow as completed successfully. + * + *

Call this when your LangGraph workflow finishes all steps. + * + * @throws IllegalStateException if workflow not started + */ + public void completeWorkflow() { + requireStarted(); + client.completeWorkflow(workflowId); + closedNormally = true; + } + + /** + * Aborts the workflow. + * + * @param reason the reason for aborting (may be null) + * @throws IllegalStateException if workflow not started + */ + public void abortWorkflow(String reason) { + requireStarted(); + client.abortWorkflow(workflowId, reason); + closedNormally = true; + } + + /** + * Fails the workflow. + * + * @param reason the reason for failing (may be null) + * @throws IllegalStateException if workflow not started + */ + public void failWorkflow(String reason) { + requireStarted(); + client.failWorkflow(workflowId, reason); + closedNormally = true; + } + + // ======================================================================== + // Step Governance + // ======================================================================== + + /** + * Checks if a step is allowed to proceed. + * + *

Call this before executing each LangGraph node. + * + * @param stepName human-readable step name + * @param stepType type of step (llm_call, tool_call, connector_call, human_task) + * @return true if allowed, false if blocked (when autoBlock is false) + * @throws WorkflowBlockedError if blocked and autoBlock is true + * @throws WorkflowApprovalRequiredError if approval is required + * @throws IllegalStateException if workflow not started + */ + public boolean checkGate(String stepName, String stepType) { + return checkGate(stepName, stepType, null); + } + + /** + * Checks if a step is allowed to proceed, with options. + * + * @param stepName human-readable step name + * @param stepType type of step (llm_call, tool_call, connector_call, human_task) + * @param options additional options (may be null) + * @return true if allowed, false if blocked (when autoBlock is false) + * @throws WorkflowBlockedError if blocked and autoBlock is true + * @throws WorkflowApprovalRequiredError if approval is required + * @throws IllegalStateException if workflow not started + */ + public boolean checkGate(String stepName, String stepType, CheckGateOptions options) { + requireStarted(); + + StepType resolvedType = StepType.fromValue(stepType); + + // Generate or use provided step ID + String stepId; + if (options != null && options.getStepId() != null) { + stepId = options.getStepId(); + } else { + stepCounter++; + String safeName = stepName.toLowerCase().replace(" ", "-").replace("/", "-"); + stepId = "step-" + stepCounter + "-" + safeName; } - /** - * Creates a new builder for a LangGraphAdapter. - * - * @param client the AxonFlow client instance - * @param workflowName human-readable name for the workflow - * @return a new builder - */ - public static Builder builder(AxonFlow client, String workflowName) { - return new Builder(client, workflowName); + StepGateRequest request = + new StepGateRequest( + stepName, + resolvedType, + options != null && options.getStepInput() != null + ? options.getStepInput() + : Collections.emptyMap(), + options != null ? options.getModel() : null, + options != null ? options.getProvider() : null, + options != null ? options.getToolContext() : null); + + StepGateResponse response = client.stepGate(workflowId, stepId, request); + + if (response.getDecision() == GateDecision.BLOCK) { + if (autoBlock) { + throw new WorkflowBlockedError( + "Step '" + stepName + "' blocked: " + response.getReason(), + response.getStepId(), + response.getReason(), + response.getPolicyIds()); + } + return false; } - // ======================================================================== - // Workflow Lifecycle - // ======================================================================== - - /** - * Registers the workflow with AxonFlow. - * - *

Call this at the start of your LangGraph workflow execution. - * - * @param metadata additional workflow metadata (may be null) - * @param traceId external trace ID for correlation (may be null) - * @return the assigned workflow ID - */ - public String startWorkflow(Map metadata, String traceId) { - CreateWorkflowRequest request = new CreateWorkflowRequest( - workflowName, - source, - metadata != null ? metadata : Collections.emptyMap(), - traceId - ); - - CreateWorkflowResponse response = client.createWorkflow(request); - this.workflowId = response.getWorkflowId(); - return this.workflowId; + if (response.getDecision() == GateDecision.REQUIRE_APPROVAL) { + throw new WorkflowApprovalRequiredError( + "Step '" + stepName + "' requires approval", + response.getStepId(), + response.getApprovalUrl(), + response.getReason()); } - /** - * Registers the workflow with AxonFlow using default metadata. - * - * @return the assigned workflow ID - */ - public String startWorkflow() { - return startWorkflow(null, null); - } - - /** - * Marks the workflow as completed successfully. - * - *

Call this when your LangGraph workflow finishes all steps. - * - * @throws IllegalStateException if workflow not started - */ - public void completeWorkflow() { - requireStarted(); - client.completeWorkflow(workflowId); - closedNormally = true; - } - - /** - * Aborts the workflow. - * - * @param reason the reason for aborting (may be null) - * @throws IllegalStateException if workflow not started - */ - public void abortWorkflow(String reason) { - requireStarted(); - client.abortWorkflow(workflowId, reason); - closedNormally = true; - } - - /** - * Fails the workflow. - * - * @param reason the reason for failing (may be null) - * @throws IllegalStateException if workflow not started - */ - public void failWorkflow(String reason) { - requireStarted(); - client.failWorkflow(workflowId, reason); - closedNormally = true; + return true; + } + + /** + * Marks a step as completed. + * + *

Call this after successfully executing a LangGraph node. + * + * @param stepName the step name (used to derive step ID if not provided) + * @throws IllegalStateException if workflow not started + */ + public void stepCompleted(String stepName) { + stepCompleted(stepName, null); + } + + /** + * Marks a step as completed, with options. + * + * @param stepName the step name + * @param options additional options (may be null) + * @throws IllegalStateException if workflow not started + */ + public void stepCompleted(String stepName, StepCompletedOptions options) { + requireStarted(); + + // Generate step ID matching the current counter (same as last checkGate) + String stepId; + if (options != null && options.getStepId() != null) { + stepId = options.getStepId(); + } else { + String safeName = stepName.toLowerCase().replace(" ", "-").replace("/", "-"); + stepId = "step-" + stepCounter + "-" + safeName; } - // ======================================================================== - // Step Governance - // ======================================================================== - - /** - * Checks if a step is allowed to proceed. - * - *

Call this before executing each LangGraph node. - * - * @param stepName human-readable step name - * @param stepType type of step (llm_call, tool_call, connector_call, human_task) - * @return true if allowed, false if blocked (when autoBlock is false) - * @throws WorkflowBlockedError if blocked and autoBlock is true - * @throws WorkflowApprovalRequiredError if approval is required - * @throws IllegalStateException if workflow not started - */ - public boolean checkGate(String stepName, String stepType) { - return checkGate(stepName, stepType, null); + MarkStepCompletedRequest request = + new MarkStepCompletedRequest( + options != null ? options.getOutput() : null, + options != null ? options.getMetadata() : null, + options != null ? options.getTokensIn() : null, + options != null ? options.getTokensOut() : null, + options != null ? options.getCostUsd() : null); + + client.markStepCompleted(workflowId, stepId, request); + } + + // ======================================================================== + // Per-Tool Governance + // ======================================================================== + + /** + * Checks if a specific tool invocation is allowed. + * + *

Convenience wrapper around {@link #checkGate} that creates a {@link ToolContext} and uses + * step type {@code tool_call}. + * + * @param toolName the tool name + * @param toolType the tool type (function, mcp, api) + * @return true if allowed, false if blocked (when autoBlock is false) + * @throws WorkflowBlockedError if blocked and autoBlock is true + * @throws WorkflowApprovalRequiredError if approval is required + * @throws IllegalStateException if workflow not started + */ + public boolean checkToolGate(String toolName, String toolType) { + return checkToolGate(toolName, toolType, null); + } + + /** + * Checks if a specific tool invocation is allowed, with options. + * + * @param toolName the tool name + * @param toolType the tool type (function, mcp, api) + * @param options additional options (may be null) + * @return true if allowed, false if blocked (when autoBlock is false) + */ + public boolean checkToolGate(String toolName, String toolType, CheckToolGateOptions options) { + String stepName = + (options != null && options.getStepName() != null) + ? options.getStepName() + : "tools/" + toolName; + + ToolContext toolContext = + ToolContext.builder(toolName) + .toolType(toolType) + .toolInput(options != null ? options.getToolInput() : null) + .build(); + + CheckGateOptions gateOptions = + CheckGateOptions.builder() + .stepId(options != null ? options.getStepId() : null) + .model(options != null ? options.getModel() : null) + .provider(options != null ? options.getProvider() : null) + .toolContext(toolContext) + .build(); + + return checkGate(stepName, StepType.TOOL_CALL.getValue(), gateOptions); + } + + /** + * Marks a tool invocation as completed. + * + * @param toolName the tool name + * @throws IllegalStateException if workflow not started + */ + public void toolCompleted(String toolName) { + toolCompleted(toolName, null); + } + + /** + * Marks a tool invocation as completed, with options. + * + * @param toolName the tool name + * @param options additional options (may be null) + * @throws IllegalStateException if workflow not started + */ + public void toolCompleted(String toolName, ToolCompletedOptions options) { + String stepName = + (options != null && options.getStepName() != null) + ? options.getStepName() + : "tools/" + toolName; + + StepCompletedOptions stepOptions = null; + if (options != null) { + stepOptions = + StepCompletedOptions.builder() + .stepId(options.getStepId()) + .output(options.getOutput()) + .tokensIn(options.getTokensIn()) + .tokensOut(options.getTokensOut()) + .costUsd(options.getCostUsd()) + .build(); } - /** - * Checks if a step is allowed to proceed, with options. - * - * @param stepName human-readable step name - * @param stepType type of step (llm_call, tool_call, connector_call, human_task) - * @param options additional options (may be null) - * @return true if allowed, false if blocked (when autoBlock is false) - * @throws WorkflowBlockedError if blocked and autoBlock is true - * @throws WorkflowApprovalRequiredError if approval is required - * @throws IllegalStateException if workflow not started - */ - public boolean checkGate(String stepName, String stepType, CheckGateOptions options) { - requireStarted(); - - StepType resolvedType = StepType.fromValue(stepType); - - // Generate or use provided step ID - String stepId; - if (options != null && options.getStepId() != null) { - stepId = options.getStepId(); - } else { - stepCounter++; - String safeName = stepName.toLowerCase().replace(" ", "-").replace("/", "-"); - stepId = "step-" + stepCounter + "-" + safeName; - } - - StepGateRequest request = new StepGateRequest( - stepName, - resolvedType, - options != null && options.getStepInput() != null ? options.getStepInput() : Collections.emptyMap(), - options != null ? options.getModel() : null, - options != null ? options.getProvider() : null, - options != null ? options.getToolContext() : null - ); - - StepGateResponse response = client.stepGate(workflowId, stepId, request); - - if (response.getDecision() == GateDecision.BLOCK) { - if (autoBlock) { - throw new WorkflowBlockedError( - "Step '" + stepName + "' blocked: " + response.getReason(), - response.getStepId(), - response.getReason(), - response.getPolicyIds() - ); + stepCompleted(stepName, stepOptions); + } + + // ======================================================================== + // Approval + // ======================================================================== + + /** + * Waits for a step to be approved by polling the workflow status. + * + * @param stepId the step ID to wait for + * @param pollIntervalMs milliseconds between polls + * @param timeoutMs maximum milliseconds to wait + * @return true if approved, false if rejected + * @throws InterruptedException if the thread is interrupted while waiting + * @throws TimeoutException if approval not received within timeout + * @throws IllegalStateException if workflow not started + */ + public boolean waitForApproval(String stepId, long pollIntervalMs, long timeoutMs) + throws InterruptedException, TimeoutException { + requireStarted(); + + long deadlineNanos = + System.nanoTime() + java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(timeoutMs); + while (System.nanoTime() < deadlineNanos) { + WorkflowStatusResponse status = client.getWorkflow(workflowId); + + for (WorkflowStepInfo step : status.getSteps()) { + if (stepId.equals(step.getStepId())) { + if (step.getApprovalStatus() != null) { + if (step.getApprovalStatus() == ApprovalStatus.APPROVED) { + return true; } - return false; - } - - if (response.getDecision() == GateDecision.REQUIRE_APPROVAL) { - throw new WorkflowApprovalRequiredError( - "Step '" + stepName + "' requires approval", - response.getStepId(), - response.getApprovalUrl(), - response.getReason() - ); - } - - return true; - } - - /** - * Marks a step as completed. - * - *

Call this after successfully executing a LangGraph node. - * - * @param stepName the step name (used to derive step ID if not provided) - * @throws IllegalStateException if workflow not started - */ - public void stepCompleted(String stepName) { - stepCompleted(stepName, null); - } - - /** - * Marks a step as completed, with options. - * - * @param stepName the step name - * @param options additional options (may be null) - * @throws IllegalStateException if workflow not started - */ - public void stepCompleted(String stepName, StepCompletedOptions options) { - requireStarted(); - - // Generate step ID matching the current counter (same as last checkGate) - String stepId; - if (options != null && options.getStepId() != null) { - stepId = options.getStepId(); - } else { - String safeName = stepName.toLowerCase().replace(" ", "-").replace("/", "-"); - stepId = "step-" + stepCounter + "-" + safeName; + if (step.getApprovalStatus() == ApprovalStatus.REJECTED) { + return false; + } + } + break; } + } - MarkStepCompletedRequest request = new MarkStepCompletedRequest( - options != null ? options.getOutput() : null, - options != null ? options.getMetadata() : null, - options != null ? options.getTokensIn() : null, - options != null ? options.getTokensOut() : null, - options != null ? options.getCostUsd() : null - ); - - client.markStepCompleted(workflowId, stepId, request); + Thread.sleep(pollIntervalMs); } - // ======================================================================== - // Per-Tool Governance - // ======================================================================== - - /** - * Checks if a specific tool invocation is allowed. - * - *

Convenience wrapper around {@link #checkGate} that creates a - * {@link ToolContext} and uses step type {@code tool_call}. - * - * @param toolName the tool name - * @param toolType the tool type (function, mcp, api) - * @return true if allowed, false if blocked (when autoBlock is false) - * @throws WorkflowBlockedError if blocked and autoBlock is true - * @throws WorkflowApprovalRequiredError if approval is required - * @throws IllegalStateException if workflow not started - */ - public boolean checkToolGate(String toolName, String toolType) { - return checkToolGate(toolName, toolType, null); + throw new TimeoutException("Approval timeout after " + timeoutMs + "ms for step " + stepId); + } + + // ======================================================================== + // MCP Interceptor + // ======================================================================== + + /** + * Creates an MCP tool interceptor with default options. + * + *

The interceptor enforces AxonFlow input and output policies around every MCP tool call. + * + * @return a new MCP tool interceptor + */ + public MCPToolInterceptor mcpToolInterceptor() { + return mcpToolInterceptor(null); + } + + /** + * Creates an MCP tool interceptor with custom options. + * + * @param options interceptor options (may be null for defaults) + * @return a new MCP tool interceptor + */ + public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) { + Function connectorTypeFn; + String operation; + + if (options != null) { + connectorTypeFn = + options.getConnectorTypeFn() != null + ? options.getConnectorTypeFn() + : req -> req.getServerName() + "." + req.getName(); + operation = options.getOperation(); + } else { + connectorTypeFn = req -> req.getServerName() + "." + req.getName(); + operation = "execute"; } - /** - * Checks if a specific tool invocation is allowed, with options. - * - * @param toolName the tool name - * @param toolType the tool type (function, mcp, api) - * @param options additional options (may be null) - * @return true if allowed, false if blocked (when autoBlock is false) - */ - public boolean checkToolGate(String toolName, String toolType, CheckToolGateOptions options) { - String stepName = (options != null && options.getStepName() != null) - ? options.getStepName() - : "tools/" + toolName; - - ToolContext toolContext = ToolContext.builder(toolName) - .toolType(toolType) - .toolInput(options != null ? options.getToolInput() : null) - .build(); - - CheckGateOptions gateOptions = CheckGateOptions.builder() - .stepId(options != null ? options.getStepId() : null) - .model(options != null ? options.getModel() : null) - .provider(options != null ? options.getProvider() : null) - .toolContext(toolContext) - .build(); - - return checkGate(stepName, StepType.TOOL_CALL.getValue(), gateOptions); + return new MCPToolInterceptor(client, connectorTypeFn, operation); + } + + // ======================================================================== + // Accessors + // ======================================================================== + + /** + * Returns the workflow ID assigned after {@link #startWorkflow()}. + * + * @return the workflow ID, or null if not yet started + */ + public String getWorkflowId() { + return workflowId; + } + + /** + * Returns the current step counter value. + * + * @return the step counter + */ + int getStepCounter() { + return stepCounter; + } + + // ======================================================================== + // AutoCloseable + // ======================================================================== + + /** + * Closes the adapter. If the workflow was started but not explicitly completed, aborted, or + * failed, it will be aborted automatically. + */ + @Override + public void close() { + if (workflowId != null && !closedNormally) { + try { + abortWorkflow("Adapter closed without explicit completion"); + } catch (Exception ignored) { + // Best-effort cleanup + } } + } - /** - * Marks a tool invocation as completed. - * - * @param toolName the tool name - * @throws IllegalStateException if workflow not started - */ - public void toolCompleted(String toolName) { - toolCompleted(toolName, null); - } + // ======================================================================== + // Internals + // ======================================================================== - /** - * Marks a tool invocation as completed, with options. - * - * @param toolName the tool name - * @param options additional options (may be null) - * @throws IllegalStateException if workflow not started - */ - public void toolCompleted(String toolName, ToolCompletedOptions options) { - String stepName = (options != null && options.getStepName() != null) - ? options.getStepName() - : "tools/" + toolName; - - StepCompletedOptions stepOptions = null; - if (options != null) { - stepOptions = StepCompletedOptions.builder() - .stepId(options.getStepId()) - .output(options.getOutput()) - .tokensIn(options.getTokensIn()) - .tokensOut(options.getTokensOut()) - .costUsd(options.getCostUsd()) - .build(); - } - - stepCompleted(stepName, stepOptions); + private void requireStarted() { + if (workflowId == null) { + throw new IllegalStateException("Workflow not started. Call startWorkflow() first."); } + } - // ======================================================================== - // Approval - // ======================================================================== + // ======================================================================== + // Builder + // ======================================================================== - /** - * Waits for a step to be approved by polling the workflow status. - * - * @param stepId the step ID to wait for - * @param pollIntervalMs milliseconds between polls - * @param timeoutMs maximum milliseconds to wait - * @return true if approved, false if rejected - * @throws InterruptedException if the thread is interrupted while waiting - * @throws TimeoutException if approval not received within timeout - * @throws IllegalStateException if workflow not started - */ - public boolean waitForApproval(String stepId, long pollIntervalMs, long timeoutMs) - throws InterruptedException, TimeoutException { - requireStarted(); - - long deadlineNanos = System.nanoTime() + java.util.concurrent.TimeUnit.MILLISECONDS.toNanos(timeoutMs); - while (System.nanoTime() < deadlineNanos) { - WorkflowStatusResponse status = client.getWorkflow(workflowId); - - for (WorkflowStepInfo step : status.getSteps()) { - if (stepId.equals(step.getStepId())) { - if (step.getApprovalStatus() != null) { - if (step.getApprovalStatus() == ApprovalStatus.APPROVED) { - return true; - } - if (step.getApprovalStatus() == ApprovalStatus.REJECTED) { - return false; - } - } - break; - } - } + /** Builder for {@link LangGraphAdapter}. */ + public static final class Builder { - Thread.sleep(pollIntervalMs); - } + private final AxonFlow client; + private final String workflowName; + private WorkflowSource source = WorkflowSource.LANGGRAPH; + private boolean autoBlock = true; - throw new TimeoutException("Approval timeout after " + timeoutMs + "ms for step " + stepId); + private Builder(AxonFlow client, String workflowName) { + this.client = Objects.requireNonNull(client, "client cannot be null"); + this.workflowName = Objects.requireNonNull(workflowName, "workflowName cannot be null"); } - // ======================================================================== - // MCP Interceptor - // ======================================================================== - /** - * Creates an MCP tool interceptor with default options. - * - *

The interceptor enforces AxonFlow input and output policies around - * every MCP tool call. + * Sets the workflow source. Defaults to {@link WorkflowSource#LANGGRAPH}. * - * @return a new MCP tool interceptor + * @param source the workflow source + * @return this builder */ - public MCPToolInterceptor mcpToolInterceptor() { - return mcpToolInterceptor(null); + public Builder source(WorkflowSource source) { + this.source = Objects.requireNonNull(source, "source cannot be null"); + return this; } /** - * Creates an MCP tool interceptor with custom options. + * Sets whether to automatically throw on blocked steps. Defaults to {@code true}. * - * @param options interceptor options (may be null for defaults) - * @return a new MCP tool interceptor - */ - public MCPToolInterceptor mcpToolInterceptor(MCPInterceptorOptions options) { - Function connectorTypeFn; - String operation; - - if (options != null) { - connectorTypeFn = options.getConnectorTypeFn() != null - ? options.getConnectorTypeFn() - : req -> req.getServerName() + "." + req.getName(); - operation = options.getOperation(); - } else { - connectorTypeFn = req -> req.getServerName() + "." + req.getName(); - operation = "execute"; - } - - return new MCPToolInterceptor(client, connectorTypeFn, operation); - } - - // ======================================================================== - // Accessors - // ======================================================================== - - /** - * Returns the workflow ID assigned after {@link #startWorkflow()}. + *

When {@code true}, {@link LangGraphAdapter#checkGate} throws {@link WorkflowBlockedError} + * on block decisions. When {@code false}, it returns {@code false}. * - * @return the workflow ID, or null if not yet started + * @param autoBlock whether to auto-block + * @return this builder */ - public String getWorkflowId() { - return workflowId; + public Builder autoBlock(boolean autoBlock) { + this.autoBlock = autoBlock; + return this; } /** - * Returns the current step counter value. + * Builds the adapter. * - * @return the step counter + * @return a new LangGraphAdapter */ - int getStepCounter() { - return stepCounter; - } - - // ======================================================================== - // AutoCloseable - // ======================================================================== - - /** - * Closes the adapter. If the workflow was started but not explicitly - * completed, aborted, or failed, it will be aborted automatically. - */ - @Override - public void close() { - if (workflowId != null && !closedNormally) { - try { - abortWorkflow("Adapter closed without explicit completion"); - } catch (Exception ignored) { - // Best-effort cleanup - } - } - } - - // ======================================================================== - // Internals - // ======================================================================== - - private void requireStarted() { - if (workflowId == null) { - throw new IllegalStateException("Workflow not started. Call startWorkflow() first."); - } - } - - // ======================================================================== - // Builder - // ======================================================================== - - /** - * Builder for {@link LangGraphAdapter}. - */ - public static final class Builder { - - private final AxonFlow client; - private final String workflowName; - private WorkflowSource source = WorkflowSource.LANGGRAPH; - private boolean autoBlock = true; - - private Builder(AxonFlow client, String workflowName) { - this.client = Objects.requireNonNull(client, "client cannot be null"); - this.workflowName = Objects.requireNonNull(workflowName, "workflowName cannot be null"); - } - - /** - * Sets the workflow source. Defaults to {@link WorkflowSource#LANGGRAPH}. - * - * @param source the workflow source - * @return this builder - */ - public Builder source(WorkflowSource source) { - this.source = Objects.requireNonNull(source, "source cannot be null"); - return this; - } - - /** - * Sets whether to automatically throw on blocked steps. - * Defaults to {@code true}. - * - *

When {@code true}, {@link LangGraphAdapter#checkGate} throws - * {@link WorkflowBlockedError} on block decisions. - * When {@code false}, it returns {@code false}. - * - * @param autoBlock whether to auto-block - * @return this builder - */ - public Builder autoBlock(boolean autoBlock) { - this.autoBlock = autoBlock; - return this; - } - - /** - * Builds the adapter. - * - * @return a new LangGraphAdapter - */ - public LangGraphAdapter build() { - return new LangGraphAdapter(this); - } + public LangGraphAdapter build() { + return new LangGraphAdapter(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java index cc3f584..c419e35 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPInterceptorOptions.java @@ -20,75 +20,72 @@ /** * Options for {@link LangGraphAdapter#mcpToolInterceptor}. * - *

Controls how MCP tool requests are mapped to connector types and what - * operation type is used for policy checks. + *

Controls how MCP tool requests are mapped to connector types and what operation type is used + * for policy checks. */ public final class MCPInterceptorOptions { - private final Function connectorTypeFn; - private final String operation; + private final Function connectorTypeFn; + private final String operation; - private MCPInterceptorOptions(Builder builder) { - this.connectorTypeFn = builder.connectorTypeFn; - this.operation = builder.operation; - } + private MCPInterceptorOptions(Builder builder) { + this.connectorTypeFn = builder.connectorTypeFn; + this.operation = builder.operation; + } + + /** + * Returns the function that maps an MCP request to a connector type string. May be null, in which + * case the default "{serverName}.{toolName}" is used. + * + * @return the connector type function, or null + */ + public Function getConnectorTypeFn() { + return connectorTypeFn; + } + + /** + * Returns the operation type passed to {@code mcpCheckInput}. Defaults to "execute". + * + * @return the operation type + */ + public String getOperation() { + return operation; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private Function connectorTypeFn; + private String operation = "execute"; + + private Builder() {} /** - * Returns the function that maps an MCP request to a connector type string. - * May be null, in which case the default "{serverName}.{toolName}" is used. + * Sets a custom function to derive the connector type from an MCP request. * - * @return the connector type function, or null + * @param connectorTypeFn mapping function + * @return this builder */ - public Function getConnectorTypeFn() { - return connectorTypeFn; + public Builder connectorTypeFn(Function connectorTypeFn) { + this.connectorTypeFn = connectorTypeFn; + return this; } /** - * Returns the operation type passed to {@code mcpCheckInput}. - * Defaults to "execute". + * Sets the operation type. Defaults to "execute". Use "query" for known read-only tool calls. * - * @return the operation type + * @param operation the operation type + * @return this builder */ - public String getOperation() { - return operation; + public Builder operation(String operation) { + this.operation = operation; + return this; } - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private Function connectorTypeFn; - private String operation = "execute"; - - private Builder() { - } - - /** - * Sets a custom function to derive the connector type from an MCP request. - * - * @param connectorTypeFn mapping function - * @return this builder - */ - public Builder connectorTypeFn(Function connectorTypeFn) { - this.connectorTypeFn = connectorTypeFn; - return this; - } - - /** - * Sets the operation type. Defaults to "execute". - * Use "query" for known read-only tool calls. - * - * @param operation the operation type - * @return this builder - */ - public Builder operation(String operation) { - this.operation = operation; - return this; - } - - public MCPInterceptorOptions build() { - return new MCPInterceptorOptions(this); - } + public MCPInterceptorOptions build() { + return new MCPInterceptorOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolHandler.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolHandler.java index a519680..5850cf2 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolHandler.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolHandler.java @@ -18,18 +18,18 @@ /** * Functional interface for handling an MCP tool request. * - *

Implementations execute the actual tool call and return the result. - * Used by {@link MCPToolInterceptor} as the downstream handler. + *

Implementations execute the actual tool call and return the result. Used by {@link + * MCPToolInterceptor} as the downstream handler. */ @FunctionalInterface public interface MCPToolHandler { - /** - * Handles an MCP tool request. - * - * @param request the tool request to handle - * @return the result of the tool invocation - * @throws Exception if the tool call fails - */ - Object handle(MCPToolRequest request) throws Exception; + /** + * Handles an MCP tool request. + * + * @param request the tool request to handle + * @return the result of the tool invocation + * @throws Exception if the tool call fails + */ + Object handle(MCPToolRequest request) throws Exception; } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java index 57d53af..b79fcf3 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolInterceptor.java @@ -21,7 +21,6 @@ import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.getaxonflow.sdk.types.MCPCheckInputResponse; import com.getaxonflow.sdk.types.MCPCheckOutputResponse; - import java.util.HashMap; import java.util.Map; import java.util.function.Function; @@ -29,14 +28,14 @@ /** * Intercepts MCP tool calls with AxonFlow policy enforcement. * - *

Wraps each MCP tool invocation with pre-call input validation and - * post-call output validation. If a policy blocks the input or output, - * a {@link PolicyViolationException} is thrown. If redaction is active, - * the redacted data is returned instead of the raw result. + *

Wraps each MCP tool invocation with pre-call input validation and post-call output validation. + * If a policy blocks the input or output, a {@link PolicyViolationException} is thrown. If + * redaction is active, the redacted data is returned instead of the raw result. * *

Obtain an instance via {@link LangGraphAdapter#mcpToolInterceptor()}. * *

Example usage: + * *

{@code
  * MCPToolInterceptor interceptor = adapter.mcpToolInterceptor();
  * Object result = interceptor.intercept(request, req -> callMCPServer(req));
@@ -44,91 +43,99 @@
  */
 public final class MCPToolInterceptor {
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
-
-    private final AxonFlow client;
-    private final Function connectorTypeFn;
-    private final String operation;
-
-    MCPToolInterceptor(AxonFlow client, Function connectorTypeFn, String operation) {
-        this.client = client;
-        this.connectorTypeFn = connectorTypeFn;
-        this.operation = operation;
+  private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+
+  private final AxonFlow client;
+  private final Function connectorTypeFn;
+  private final String operation;
+
+  MCPToolInterceptor(
+      AxonFlow client, Function connectorTypeFn, String operation) {
+    this.client = client;
+    this.connectorTypeFn = connectorTypeFn;
+    this.operation = operation;
+  }
+
+  /**
+   * Intercepts an MCP tool call with policy enforcement.
+   *
+   * 

Flow: + * + *

    + *
  1. Derive connector type from the request + *
  2. Build a statement string and call {@code mcpCheckInput} + *
  3. If blocked, throw {@link PolicyViolationException} + *
  4. Execute the handler + *
  5. Call {@code mcpCheckOutput} with the result + *
  6. If blocked, throw {@link PolicyViolationException} + *
  7. If redacted data is present, return it instead of the raw result + *
+ * + * @param request the MCP tool request + * @param handler the downstream handler that executes the actual tool call + * @return the tool result, or redacted data if redaction policies are active + * @throws PolicyViolationException if input or output is blocked by policy + * @throws Exception if the handler throws + */ + public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws Exception { + String connectorType = connectorTypeFn.apply(request); + String argsStr = serializeArgs(request.getArgs()); + String statement = connectorType + "(" + argsStr + ")"; + + // Pre-check: validate input + Map inputOptions = new HashMap<>(); + inputOptions.put("operation", operation); + if (request.getArgs() != null && !request.getArgs().isEmpty()) { + inputOptions.put("parameters", request.getArgs()); } - /** - * Intercepts an MCP tool call with policy enforcement. - * - *

Flow: - *

    - *
  1. Derive connector type from the request
  2. - *
  3. Build a statement string and call {@code mcpCheckInput}
  4. - *
  5. If blocked, throw {@link PolicyViolationException}
  6. - *
  7. Execute the handler
  8. - *
  9. Call {@code mcpCheckOutput} with the result
  10. - *
  11. If blocked, throw {@link PolicyViolationException}
  12. - *
  13. If redacted data is present, return it instead of the raw result
  14. - *
- * - * @param request the MCP tool request - * @param handler the downstream handler that executes the actual tool call - * @return the tool result, or redacted data if redaction policies are active - * @throws PolicyViolationException if input or output is blocked by policy - * @throws Exception if the handler throws - */ - public Object intercept(MCPToolRequest request, MCPToolHandler handler) throws Exception { - String connectorType = connectorTypeFn.apply(request); - String argsStr = serializeArgs(request.getArgs()); - String statement = connectorType + "(" + argsStr + ")"; - - // Pre-check: validate input - Map inputOptions = new HashMap<>(); - inputOptions.put("operation", operation); - if (request.getArgs() != null && !request.getArgs().isEmpty()) { - inputOptions.put("parameters", request.getArgs()); - } + MCPCheckInputResponse preCheck = client.mcpCheckInput(connectorType, statement, inputOptions); + if (!preCheck.isAllowed()) { + String reason = + preCheck.getBlockReason() != null + ? preCheck.getBlockReason() + : "Tool call blocked by policy"; + throw new PolicyViolationException(reason); + } - MCPCheckInputResponse preCheck = client.mcpCheckInput(connectorType, statement, inputOptions); - if (!preCheck.isAllowed()) { - String reason = preCheck.getBlockReason() != null ? preCheck.getBlockReason() : "Tool call blocked by policy"; - throw new PolicyViolationException(reason); - } + // Execute the tool + Object result = handler.handle(request); - // Execute the tool - Object result = handler.handle(request); + // Post-check: validate output + String resultStr; + try { + resultStr = OBJECT_MAPPER.writeValueAsString(result); + } catch (JsonProcessingException e) { + resultStr = String.valueOf(result); + } - // Post-check: validate output - String resultStr; - try { - resultStr = OBJECT_MAPPER.writeValueAsString(result); - } catch (JsonProcessingException e) { - resultStr = String.valueOf(result); - } + Map outputOptions = new HashMap<>(); + outputOptions.put("message", resultStr); - Map outputOptions = new HashMap<>(); - outputOptions.put("message", resultStr); + MCPCheckOutputResponse outputCheck = client.mcpCheckOutput(connectorType, null, outputOptions); + if (!outputCheck.isAllowed()) { + String reason = + outputCheck.getBlockReason() != null + ? outputCheck.getBlockReason() + : "Tool result blocked by policy"; + throw new PolicyViolationException(reason); + } - MCPCheckOutputResponse outputCheck = client.mcpCheckOutput(connectorType, null, outputOptions); - if (!outputCheck.isAllowed()) { - String reason = outputCheck.getBlockReason() != null ? outputCheck.getBlockReason() : "Tool result blocked by policy"; - throw new PolicyViolationException(reason); - } + if (outputCheck.getRedactedData() != null) { + return outputCheck.getRedactedData(); + } - if (outputCheck.getRedactedData() != null) { - return outputCheck.getRedactedData(); - } + return result; + } - return result; + private static String serializeArgs(Map args) { + if (args == null || args.isEmpty()) { + return "{}"; } - - private static String serializeArgs(Map args) { - if (args == null || args.isEmpty()) { - return "{}"; - } - try { - return OBJECT_MAPPER.writeValueAsString(args); - } catch (JsonProcessingException e) { - return args.toString(); - } + try { + return OBJECT_MAPPER.writeValueAsString(args); + } catch (JsonProcessingException e) { + return args.toString(); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolRequest.java b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolRequest.java index b411ad2..aaf2d14 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/MCPToolRequest.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/MCPToolRequest.java @@ -22,52 +22,52 @@ /** * Represents an MCP tool invocation request. * - *

Used by {@link MCPToolInterceptor} to pass tool call information - * through the interceptor chain. + *

Used by {@link MCPToolInterceptor} to pass tool call information through the interceptor + * chain. */ public final class MCPToolRequest { - private final String serverName; - private final String name; - private final Map args; + private final String serverName; + private final String name; + private final Map args; - /** - * Creates a new MCPToolRequest. - * - * @param serverName the MCP server name - * @param name the tool name - * @param args the tool arguments - */ - public MCPToolRequest(String serverName, String name, Map args) { - this.serverName = Objects.requireNonNull(serverName, "serverName cannot be null"); - this.name = Objects.requireNonNull(name, "name cannot be null"); - this.args = args != null ? Collections.unmodifiableMap(args) : Collections.emptyMap(); - } + /** + * Creates a new MCPToolRequest. + * + * @param serverName the MCP server name + * @param name the tool name + * @param args the tool arguments + */ + public MCPToolRequest(String serverName, String name, Map args) { + this.serverName = Objects.requireNonNull(serverName, "serverName cannot be null"); + this.name = Objects.requireNonNull(name, "name cannot be null"); + this.args = args != null ? Collections.unmodifiableMap(args) : Collections.emptyMap(); + } - /** - * Returns the MCP server name. - * - * @return the server name - */ - public String getServerName() { - return serverName; - } + /** + * Returns the MCP server name. + * + * @return the server name + */ + public String getServerName() { + return serverName; + } - /** - * Returns the tool name. - * - * @return the tool name - */ - public String getName() { - return name; - } + /** + * Returns the tool name. + * + * @return the tool name + */ + public String getName() { + return name; + } - /** - * Returns the tool arguments. - * - * @return immutable map of arguments - */ - public Map getArgs() { - return args; - } + /** + * Returns the tool arguments. + * + * @return immutable map of arguments + */ + public Map getArgs() { + return args; + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/StepCompletedOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/StepCompletedOptions.java index 3f06133..6e8e628 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/StepCompletedOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/StepCompletedOptions.java @@ -17,98 +17,95 @@ import java.util.Map; -/** - * Options for {@link LangGraphAdapter#stepCompleted}. - */ +/** Options for {@link LangGraphAdapter#stepCompleted}. */ public final class StepCompletedOptions { - private final String stepId; - private final Map output; - private final Map metadata; - private final Integer tokensIn; - private final Integer tokensOut; - private final Double costUsd; - - private StepCompletedOptions(Builder builder) { - this.stepId = builder.stepId; - this.output = builder.output; - this.metadata = builder.metadata; - this.tokensIn = builder.tokensIn; - this.tokensOut = builder.tokensOut; - this.costUsd = builder.costUsd; - } - - public String getStepId() { - return stepId; - } - - public Map getOutput() { - return output; + private final String stepId; + private final Map output; + private final Map metadata; + private final Integer tokensIn; + private final Integer tokensOut; + private final Double costUsd; + + private StepCompletedOptions(Builder builder) { + this.stepId = builder.stepId; + this.output = builder.output; + this.metadata = builder.metadata; + this.tokensIn = builder.tokensIn; + this.tokensOut = builder.tokensOut; + this.costUsd = builder.costUsd; + } + + public String getStepId() { + return stepId; + } + + public Map getOutput() { + return output; + } + + public Map getMetadata() { + return metadata; + } + + public Integer getTokensIn() { + return tokensIn; + } + + public Integer getTokensOut() { + return tokensOut; + } + + public Double getCostUsd() { + return costUsd; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String stepId; + private Map output; + private Map metadata; + private Integer tokensIn; + private Integer tokensOut; + private Double costUsd; + + private Builder() {} + + public Builder stepId(String stepId) { + this.stepId = stepId; + return this; } - public Map getMetadata() { - return metadata; + public Builder output(Map output) { + this.output = output; + return this; } - public Integer getTokensIn() { - return tokensIn; + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; } - public Integer getTokensOut() { - return tokensOut; + public Builder tokensIn(Integer tokensIn) { + this.tokensIn = tokensIn; + return this; } - public Double getCostUsd() { - return costUsd; + public Builder tokensOut(Integer tokensOut) { + this.tokensOut = tokensOut; + return this; } - public static Builder builder() { - return new Builder(); + public Builder costUsd(Double costUsd) { + this.costUsd = costUsd; + return this; } - public static final class Builder { - private String stepId; - private Map output; - private Map metadata; - private Integer tokensIn; - private Integer tokensOut; - private Double costUsd; - - private Builder() { - } - - public Builder stepId(String stepId) { - this.stepId = stepId; - return this; - } - - public Builder output(Map output) { - this.output = output; - return this; - } - - public Builder metadata(Map metadata) { - this.metadata = metadata; - return this; - } - - public Builder tokensIn(Integer tokensIn) { - this.tokensIn = tokensIn; - return this; - } - - public Builder tokensOut(Integer tokensOut) { - this.tokensOut = tokensOut; - return this; - } - - public Builder costUsd(Double costUsd) { - this.costUsd = costUsd; - return this; - } - - public StepCompletedOptions build() { - return new StepCompletedOptions(this); - } + public StepCompletedOptions build() { + return new StepCompletedOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/ToolCompletedOptions.java b/src/main/java/com/getaxonflow/sdk/adapters/ToolCompletedOptions.java index 6eab259..d15c6fa 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/ToolCompletedOptions.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/ToolCompletedOptions.java @@ -17,98 +17,95 @@ import java.util.Map; -/** - * Options for {@link LangGraphAdapter#toolCompleted}. - */ +/** Options for {@link LangGraphAdapter#toolCompleted}. */ public final class ToolCompletedOptions { - private final String stepName; - private final String stepId; - private final Map output; - private final Integer tokensIn; - private final Integer tokensOut; - private final Double costUsd; - - private ToolCompletedOptions(Builder builder) { - this.stepName = builder.stepName; - this.stepId = builder.stepId; - this.output = builder.output; - this.tokensIn = builder.tokensIn; - this.tokensOut = builder.tokensOut; - this.costUsd = builder.costUsd; - } - - public String getStepName() { - return stepName; - } - - public String getStepId() { - return stepId; + private final String stepName; + private final String stepId; + private final Map output; + private final Integer tokensIn; + private final Integer tokensOut; + private final Double costUsd; + + private ToolCompletedOptions(Builder builder) { + this.stepName = builder.stepName; + this.stepId = builder.stepId; + this.output = builder.output; + this.tokensIn = builder.tokensIn; + this.tokensOut = builder.tokensOut; + this.costUsd = builder.costUsd; + } + + public String getStepName() { + return stepName; + } + + public String getStepId() { + return stepId; + } + + public Map getOutput() { + return output; + } + + public Integer getTokensIn() { + return tokensIn; + } + + public Integer getTokensOut() { + return tokensOut; + } + + public Double getCostUsd() { + return costUsd; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String stepName; + private String stepId; + private Map output; + private Integer tokensIn; + private Integer tokensOut; + private Double costUsd; + + private Builder() {} + + public Builder stepName(String stepName) { + this.stepName = stepName; + return this; } - public Map getOutput() { - return output; + public Builder stepId(String stepId) { + this.stepId = stepId; + return this; } - public Integer getTokensIn() { - return tokensIn; + public Builder output(Map output) { + this.output = output; + return this; } - public Integer getTokensOut() { - return tokensOut; + public Builder tokensIn(Integer tokensIn) { + this.tokensIn = tokensIn; + return this; } - public Double getCostUsd() { - return costUsd; + public Builder tokensOut(Integer tokensOut) { + this.tokensOut = tokensOut; + return this; } - public static Builder builder() { - return new Builder(); + public Builder costUsd(Double costUsd) { + this.costUsd = costUsd; + return this; } - public static final class Builder { - private String stepName; - private String stepId; - private Map output; - private Integer tokensIn; - private Integer tokensOut; - private Double costUsd; - - private Builder() { - } - - public Builder stepName(String stepName) { - this.stepName = stepName; - return this; - } - - public Builder stepId(String stepId) { - this.stepId = stepId; - return this; - } - - public Builder output(Map output) { - this.output = output; - return this; - } - - public Builder tokensIn(Integer tokensIn) { - this.tokensIn = tokensIn; - return this; - } - - public Builder tokensOut(Integer tokensOut) { - this.tokensOut = tokensOut; - return this; - } - - public Builder costUsd(Double costUsd) { - this.costUsd = costUsd; - return this; - } - - public ToolCompletedOptions build() { - return new ToolCompletedOptions(this); - } + public ToolCompletedOptions build() { + return new ToolCompletedOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/WorkflowApprovalRequiredError.java b/src/main/java/com/getaxonflow/sdk/adapters/WorkflowApprovalRequiredError.java index 0b11732..62704c9 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/WorkflowApprovalRequiredError.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/WorkflowApprovalRequiredError.java @@ -18,57 +18,58 @@ /** * Raised when a workflow step requires human approval before proceeding. * - *

This exception is thrown by {@link LangGraphAdapter#checkGate} when the - * gate decision is {@code REQUIRE_APPROVAL}. The caller should use - * {@link LangGraphAdapter#waitForApproval} to poll for approval. + *

This exception is thrown by {@link LangGraphAdapter#checkGate} when the gate decision is + * {@code REQUIRE_APPROVAL}. The caller should use {@link LangGraphAdapter#waitForApproval} to poll + * for approval. */ public class WorkflowApprovalRequiredError extends RuntimeException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String stepId; - private final String approvalUrl; - private final String reason; + private final String stepId; + private final String approvalUrl; + private final String reason; - /** - * Creates a new WorkflowApprovalRequiredError. - * - * @param message the error message - * @param stepId the step ID that requires approval - * @param approvalUrl the URL where approval can be granted - * @param reason the reason approval is required - */ - public WorkflowApprovalRequiredError(String message, String stepId, String approvalUrl, String reason) { - super(message); - this.stepId = stepId; - this.approvalUrl = approvalUrl; - this.reason = reason; - } + /** + * Creates a new WorkflowApprovalRequiredError. + * + * @param message the error message + * @param stepId the step ID that requires approval + * @param approvalUrl the URL where approval can be granted + * @param reason the reason approval is required + */ + public WorkflowApprovalRequiredError( + String message, String stepId, String approvalUrl, String reason) { + super(message); + this.stepId = stepId; + this.approvalUrl = approvalUrl; + this.reason = reason; + } - /** - * Returns the step ID that requires approval. - * - * @return the step ID - */ - public String getStepId() { - return stepId; - } + /** + * Returns the step ID that requires approval. + * + * @return the step ID + */ + public String getStepId() { + return stepId; + } - /** - * Returns the URL where approval can be granted. - * - * @return the approval URL - */ - public String getApprovalUrl() { - return approvalUrl; - } + /** + * Returns the URL where approval can be granted. + * + * @return the approval URL + */ + public String getApprovalUrl() { + return approvalUrl; + } - /** - * Returns the reason approval is required. - * - * @return the reason - */ - public String getReason() { - return reason; - } + /** + * Returns the reason approval is required. + * + * @return the reason + */ + public String getReason() { + return reason; + } } diff --git a/src/main/java/com/getaxonflow/sdk/adapters/WorkflowBlockedError.java b/src/main/java/com/getaxonflow/sdk/adapters/WorkflowBlockedError.java index 2186777..bb16523 100644 --- a/src/main/java/com/getaxonflow/sdk/adapters/WorkflowBlockedError.java +++ b/src/main/java/com/getaxonflow/sdk/adapters/WorkflowBlockedError.java @@ -21,56 +21,58 @@ /** * Raised when a workflow step is blocked by policy. * - *

This exception is thrown by {@link LangGraphAdapter#checkGate} when - * {@code autoBlock} is {@code true} and the gate decision is {@code BLOCK}. + *

This exception is thrown by {@link LangGraphAdapter#checkGate} when {@code autoBlock} is + * {@code true} and the gate decision is {@code BLOCK}. */ public class WorkflowBlockedError extends RuntimeException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String stepId; - private final String reason; - private final List policyIds; + private final String stepId; + private final String reason; + private final List policyIds; - /** - * Creates a new WorkflowBlockedError. - * - * @param message the error message - * @param stepId the step ID that was blocked - * @param reason the reason the step was blocked - * @param policyIds the policy IDs that caused the block - */ - public WorkflowBlockedError(String message, String stepId, String reason, List policyIds) { - super(message); - this.stepId = stepId; - this.reason = reason; - this.policyIds = policyIds != null ? Collections.unmodifiableList(policyIds) : Collections.emptyList(); - } + /** + * Creates a new WorkflowBlockedError. + * + * @param message the error message + * @param stepId the step ID that was blocked + * @param reason the reason the step was blocked + * @param policyIds the policy IDs that caused the block + */ + public WorkflowBlockedError( + String message, String stepId, String reason, List policyIds) { + super(message); + this.stepId = stepId; + this.reason = reason; + this.policyIds = + policyIds != null ? Collections.unmodifiableList(policyIds) : Collections.emptyList(); + } - /** - * Returns the step ID that was blocked. - * - * @return the step ID - */ - public String getStepId() { - return stepId; - } + /** + * Returns the step ID that was blocked. + * + * @return the step ID + */ + public String getStepId() { + return stepId; + } - /** - * Returns the reason the step was blocked. - * - * @return the block reason - */ - public String getReason() { - return reason; - } + /** + * Returns the reason the step was blocked. + * + * @return the block reason + */ + public String getReason() { + return reason; + } - /** - * Returns the policy IDs that caused the block. - * - * @return immutable list of policy IDs - */ - public List getPolicyIds() { - return policyIds; - } + /** + * Returns the policy IDs that caused the block. + * + * @return immutable list of policy IDs + */ + public List getPolicyIds() { + return policyIds; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/AuthenticationException.java b/src/main/java/com/getaxonflow/sdk/exceptions/AuthenticationException.java index 71e1de6..8f0592d 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/AuthenticationException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/AuthenticationException.java @@ -19,42 +19,43 @@ * Thrown when authentication with the AxonFlow API fails. * *

This typically occurs when: + * *

    - *
  • The license key is invalid or expired
  • - *
  • The client ID/secret combination is incorrect
  • - *
  • The API key has been revoked
  • + *
  • The license key is invalid or expired + *
  • The client ID/secret combination is incorrect + *
  • The API key has been revoked *
*/ public class AuthenticationException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - /** - * Creates a new AuthenticationException. - * - * @param message the error message - */ - public AuthenticationException(String message) { - super(message, 401, "AUTHENTICATION_FAILED"); - } + /** + * Creates a new AuthenticationException. + * + * @param message the error message + */ + public AuthenticationException(String message) { + super(message, 401, "AUTHENTICATION_FAILED"); + } - /** - * Creates a new AuthenticationException with a cause. - * - * @param message the error message - * @param cause the underlying cause - */ - public AuthenticationException(String message, Throwable cause) { - super(message, 401, "AUTHENTICATION_FAILED", cause); - } + /** + * Creates a new AuthenticationException with a cause. + * + * @param message the error message + * @param cause the underlying cause + */ + public AuthenticationException(String message, Throwable cause) { + super(message, 401, "AUTHENTICATION_FAILED", cause); + } - /** - * Creates a new AuthenticationException with a custom status code. - * - * @param message the error message - * @param statusCode the HTTP status code - */ - public AuthenticationException(String message, int statusCode) { - super(message, statusCode, "AUTHENTICATION_FAILED"); - } + /** + * Creates a new AuthenticationException with a custom status code. + * + * @param message the error message + * @param statusCode the HTTP status code + */ + public AuthenticationException(String message, int statusCode) { + super(message, statusCode, "AUTHENTICATION_FAILED"); + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/AxonFlowException.java b/src/main/java/com/getaxonflow/sdk/exceptions/AxonFlowException.java index f814241..c3a5a33 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/AxonFlowException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/AxonFlowException.java @@ -18,10 +18,11 @@ /** * Base exception for all AxonFlow SDK errors. * - *

All exceptions thrown by the AxonFlow SDK extend this class, allowing - * callers to catch all SDK-related errors with a single catch block. + *

All exceptions thrown by the AxonFlow SDK extend this class, allowing callers to catch all + * SDK-related errors with a single catch block. * *

Example usage: + * *

{@code
  * try {
  *     PolicyApprovalResult result = axonflow.getPolicyApprovedContext(request);
@@ -36,89 +37,89 @@
  */
 public class AxonFlowException extends RuntimeException {
 
-    private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 1L;
 
-    private final int statusCode;
-    private final String errorCode;
+  private final int statusCode;
+  private final String errorCode;
 
-    /**
-     * Creates a new AxonFlowException with a message.
-     *
-     * @param message the error message
-     */
-    public AxonFlowException(String message) {
-        super(message);
-        this.statusCode = 0;
-        this.errorCode = null;
-    }
+  /**
+   * Creates a new AxonFlowException with a message.
+   *
+   * @param message the error message
+   */
+  public AxonFlowException(String message) {
+    super(message);
+    this.statusCode = 0;
+    this.errorCode = null;
+  }
 
-    /**
-     * Creates a new AxonFlowException with a message and cause.
-     *
-     * @param message the error message
-     * @param cause   the underlying cause
-     */
-    public AxonFlowException(String message, Throwable cause) {
-        super(message, cause);
-        this.statusCode = 0;
-        this.errorCode = null;
-    }
+  /**
+   * Creates a new AxonFlowException with a message and cause.
+   *
+   * @param message the error message
+   * @param cause the underlying cause
+   */
+  public AxonFlowException(String message, Throwable cause) {
+    super(message, cause);
+    this.statusCode = 0;
+    this.errorCode = null;
+  }
 
-    /**
-     * Creates a new AxonFlowException with full details.
-     *
-     * @param message    the error message
-     * @param statusCode the HTTP status code (if applicable)
-     * @param errorCode  the error code (if applicable)
-     */
-    public AxonFlowException(String message, int statusCode, String errorCode) {
-        super(message);
-        this.statusCode = statusCode;
-        this.errorCode = errorCode;
-    }
+  /**
+   * Creates a new AxonFlowException with full details.
+   *
+   * @param message the error message
+   * @param statusCode the HTTP status code (if applicable)
+   * @param errorCode the error code (if applicable)
+   */
+  public AxonFlowException(String message, int statusCode, String errorCode) {
+    super(message);
+    this.statusCode = statusCode;
+    this.errorCode = errorCode;
+  }
 
-    /**
-     * Creates a new AxonFlowException with full details and cause.
-     *
-     * @param message    the error message
-     * @param statusCode the HTTP status code (if applicable)
-     * @param errorCode  the error code (if applicable)
-     * @param cause      the underlying cause
-     */
-    public AxonFlowException(String message, int statusCode, String errorCode, Throwable cause) {
-        super(message, cause);
-        this.statusCode = statusCode;
-        this.errorCode = errorCode;
-    }
+  /**
+   * Creates a new AxonFlowException with full details and cause.
+   *
+   * @param message the error message
+   * @param statusCode the HTTP status code (if applicable)
+   * @param errorCode the error code (if applicable)
+   * @param cause the underlying cause
+   */
+  public AxonFlowException(String message, int statusCode, String errorCode, Throwable cause) {
+    super(message, cause);
+    this.statusCode = statusCode;
+    this.errorCode = errorCode;
+  }
 
-    /**
-     * Returns the HTTP status code associated with this error.
-     *
-     * @return the HTTP status code, or 0 if not applicable
-     */
-    public int getStatusCode() {
-        return statusCode;
-    }
+  /**
+   * Returns the HTTP status code associated with this error.
+   *
+   * @return the HTTP status code, or 0 if not applicable
+   */
+  public int getStatusCode() {
+    return statusCode;
+  }
 
-    /**
-     * Returns the error code from the API.
-     *
-     * @return the error code, or null if not available
-     */
-    public String getErrorCode() {
-        return errorCode;
-    }
+  /**
+   * Returns the error code from the API.
+   *
+   * @return the error code, or null if not available
+   */
+  public String getErrorCode() {
+    return errorCode;
+  }
 
-    @Override
-    public String toString() {
-        StringBuilder sb = new StringBuilder(getClass().getSimpleName());
-        sb.append(": ").append(getMessage());
-        if (statusCode > 0) {
-            sb.append(" (status=").append(statusCode).append(")");
-        }
-        if (errorCode != null) {
-            sb.append(" [").append(errorCode).append("]");
-        }
-        return sb.toString();
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(getClass().getSimpleName());
+    sb.append(": ").append(getMessage());
+    if (statusCode > 0) {
+      sb.append(" (status=").append(statusCode).append(")");
+    }
+    if (errorCode != null) {
+      sb.append(" [").append(errorCode).append("]");
     }
+    return sb.toString();
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/ConfigurationException.java b/src/main/java/com/getaxonflow/sdk/exceptions/ConfigurationException.java
index d29294c..11fa8b1 100644
--- a/src/main/java/com/getaxonflow/sdk/exceptions/ConfigurationException.java
+++ b/src/main/java/com/getaxonflow/sdk/exceptions/ConfigurationException.java
@@ -19,56 +19,57 @@
  * Thrown when the SDK is misconfigured.
  *
  * 

This typically occurs when: + * *

    - *
  • Required configuration parameters are missing
  • - *
  • Invalid values are provided for configuration
  • - *
  • Incompatible configuration options are used together
  • + *
  • Required configuration parameters are missing + *
  • Invalid values are provided for configuration + *
  • Incompatible configuration options are used together *
*/ public class ConfigurationException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String configKey; + private final String configKey; - /** - * Creates a new ConfigurationException. - * - * @param message the error message - */ - public ConfigurationException(String message) { - super(message, 0, "CONFIGURATION_ERROR"); - this.configKey = null; - } + /** + * Creates a new ConfigurationException. + * + * @param message the error message + */ + public ConfigurationException(String message) { + super(message, 0, "CONFIGURATION_ERROR"); + this.configKey = null; + } - /** - * Creates a new ConfigurationException for a specific configuration key. - * - * @param message the error message - * @param configKey the configuration key that is invalid - */ - public ConfigurationException(String message, String configKey) { - super(message, 0, "CONFIGURATION_ERROR"); - this.configKey = configKey; - } + /** + * Creates a new ConfigurationException for a specific configuration key. + * + * @param message the error message + * @param configKey the configuration key that is invalid + */ + public ConfigurationException(String message, String configKey) { + super(message, 0, "CONFIGURATION_ERROR"); + this.configKey = configKey; + } - /** - * Creates a new ConfigurationException with cause. - * - * @param message the error message - * @param cause the underlying cause - */ - public ConfigurationException(String message, Throwable cause) { - super(message, 0, "CONFIGURATION_ERROR", cause); - this.configKey = null; - } + /** + * Creates a new ConfigurationException with cause. + * + * @param message the error message + * @param cause the underlying cause + */ + public ConfigurationException(String message, Throwable cause) { + super(message, 0, "CONFIGURATION_ERROR", cause); + this.configKey = null; + } - /** - * Returns the configuration key that caused the error. - * - * @return the config key, or null if not specific to a key - */ - public String getConfigKey() { - return configKey; - } + /** + * Returns the configuration key that caused the error. + * + * @return the config key, or null if not specific to a key + */ + public String getConfigKey() { + return configKey; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/ConnectionException.java b/src/main/java/com/getaxonflow/sdk/exceptions/ConnectionException.java index 6c33240..08d8596 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/ConnectionException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/ConnectionException.java @@ -19,72 +19,73 @@ * Thrown when a connection to the AxonFlow API fails. * *

This typically occurs when: + * *

    - *
  • The AxonFlow Agent is not running
  • - *
  • Network connectivity issues
  • - *
  • DNS resolution failures
  • - *
  • SSL/TLS handshake errors
  • + *
  • The AxonFlow Agent is not running + *
  • Network connectivity issues + *
  • DNS resolution failures + *
  • SSL/TLS handshake errors *
*/ public class ConnectionException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String host; - private final int port; + private final String host; + private final int port; - /** - * Creates a new ConnectionException. - * - * @param message the error message - */ - public ConnectionException(String message) { - super(message, 0, "CONNECTION_FAILED"); - this.host = null; - this.port = 0; - } + /** + * Creates a new ConnectionException. + * + * @param message the error message + */ + public ConnectionException(String message) { + super(message, 0, "CONNECTION_FAILED"); + this.host = null; + this.port = 0; + } - /** - * Creates a new ConnectionException with cause. - * - * @param message the error message - * @param cause the underlying cause - */ - public ConnectionException(String message, Throwable cause) { - super(message, 0, "CONNECTION_FAILED", cause); - this.host = null; - this.port = 0; - } + /** + * Creates a new ConnectionException with cause. + * + * @param message the error message + * @param cause the underlying cause + */ + public ConnectionException(String message, Throwable cause) { + super(message, 0, "CONNECTION_FAILED", cause); + this.host = null; + this.port = 0; + } - /** - * Creates a new ConnectionException with connection details. - * - * @param message the error message - * @param host the target host - * @param port the target port - * @param cause the underlying cause - */ - public ConnectionException(String message, String host, int port, Throwable cause) { - super(message, 0, "CONNECTION_FAILED", cause); - this.host = host; - this.port = port; - } + /** + * Creates a new ConnectionException with connection details. + * + * @param message the error message + * @param host the target host + * @param port the target port + * @param cause the underlying cause + */ + public ConnectionException(String message, String host, int port, Throwable cause) { + super(message, 0, "CONNECTION_FAILED", cause); + this.host = host; + this.port = port; + } - /** - * Returns the target host. - * - * @return the host, or null if not specified - */ - public String getHost() { - return host; - } + /** + * Returns the target host. + * + * @return the host, or null if not specified + */ + public String getHost() { + return host; + } - /** - * Returns the target port. - * - * @return the port, or 0 if not specified - */ - public int getPort() { - return port; - } + /** + * Returns the target port. + * + * @return the port, or 0 if not specified + */ + public int getPort() { + return port; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/ConnectorException.java b/src/main/java/com/getaxonflow/sdk/exceptions/ConnectorException.java index 8184e40..4c06870 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/ConnectorException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/ConnectorException.java @@ -15,69 +15,67 @@ */ package com.getaxonflow.sdk.exceptions; -/** - * Thrown when an MCP connector operation fails. - */ +/** Thrown when an MCP connector operation fails. */ public class ConnectorException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String connectorId; - private final String operation; + private final String connectorId; + private final String operation; - /** - * Creates a new ConnectorException. - * - * @param message the error message - */ - public ConnectorException(String message) { - super(message, 0, "CONNECTOR_ERROR"); - this.connectorId = null; - this.operation = null; - } + /** + * Creates a new ConnectorException. + * + * @param message the error message + */ + public ConnectorException(String message) { + super(message, 0, "CONNECTOR_ERROR"); + this.connectorId = null; + this.operation = null; + } - /** - * Creates a new ConnectorException with connector details. - * - * @param message the error message - * @param connectorId the connector that failed - * @param operation the operation that failed - */ - public ConnectorException(String message, String connectorId, String operation) { - super(message, 0, "CONNECTOR_ERROR"); - this.connectorId = connectorId; - this.operation = operation; - } + /** + * Creates a new ConnectorException with connector details. + * + * @param message the error message + * @param connectorId the connector that failed + * @param operation the operation that failed + */ + public ConnectorException(String message, String connectorId, String operation) { + super(message, 0, "CONNECTOR_ERROR"); + this.connectorId = connectorId; + this.operation = operation; + } - /** - * Creates a new ConnectorException with cause. - * - * @param message the error message - * @param connectorId the connector that failed - * @param operation the operation that failed - * @param cause the underlying cause - */ - public ConnectorException(String message, String connectorId, String operation, Throwable cause) { - super(message, 0, "CONNECTOR_ERROR", cause); - this.connectorId = connectorId; - this.operation = operation; - } + /** + * Creates a new ConnectorException with cause. + * + * @param message the error message + * @param connectorId the connector that failed + * @param operation the operation that failed + * @param cause the underlying cause + */ + public ConnectorException(String message, String connectorId, String operation, Throwable cause) { + super(message, 0, "CONNECTOR_ERROR", cause); + this.connectorId = connectorId; + this.operation = operation; + } - /** - * Returns the connector ID that failed. - * - * @return the connector ID - */ - public String getConnectorId() { - return connectorId; - } + /** + * Returns the connector ID that failed. + * + * @return the connector ID + */ + public String getConnectorId() { + return connectorId; + } - /** - * Returns the operation that failed. - * - * @return the operation name - */ - public String getOperation() { - return operation; - } + /** + * Returns the operation that failed. + * + * @return the operation name + */ + public String getOperation() { + return operation; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/PlanExecutionException.java b/src/main/java/com/getaxonflow/sdk/exceptions/PlanExecutionException.java index 36ed85c..fd8c8cf 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/PlanExecutionException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/PlanExecutionException.java @@ -15,69 +15,67 @@ */ package com.getaxonflow.sdk.exceptions; -/** - * Thrown when plan generation or execution fails. - */ +/** Thrown when plan generation or execution fails. */ public class PlanExecutionException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final String planId; - private final String failedStep; + private final String planId; + private final String failedStep; - /** - * Creates a new PlanExecutionException. - * - * @param message the error message - */ - public PlanExecutionException(String message) { - super(message, 0, "PLAN_EXECUTION_FAILED"); - this.planId = null; - this.failedStep = null; - } + /** + * Creates a new PlanExecutionException. + * + * @param message the error message + */ + public PlanExecutionException(String message) { + super(message, 0, "PLAN_EXECUTION_FAILED"); + this.planId = null; + this.failedStep = null; + } - /** - * Creates a new PlanExecutionException with plan details. - * - * @param message the error message - * @param planId the plan that failed - * @param failedStep the step that failed - */ - public PlanExecutionException(String message, String planId, String failedStep) { - super(message, 0, "PLAN_EXECUTION_FAILED"); - this.planId = planId; - this.failedStep = failedStep; - } + /** + * Creates a new PlanExecutionException with plan details. + * + * @param message the error message + * @param planId the plan that failed + * @param failedStep the step that failed + */ + public PlanExecutionException(String message, String planId, String failedStep) { + super(message, 0, "PLAN_EXECUTION_FAILED"); + this.planId = planId; + this.failedStep = failedStep; + } - /** - * Creates a new PlanExecutionException with cause. - * - * @param message the error message - * @param planId the plan that failed - * @param failedStep the step that failed - * @param cause the underlying cause - */ - public PlanExecutionException(String message, String planId, String failedStep, Throwable cause) { - super(message, 0, "PLAN_EXECUTION_FAILED", cause); - this.planId = planId; - this.failedStep = failedStep; - } + /** + * Creates a new PlanExecutionException with cause. + * + * @param message the error message + * @param planId the plan that failed + * @param failedStep the step that failed + * @param cause the underlying cause + */ + public PlanExecutionException(String message, String planId, String failedStep, Throwable cause) { + super(message, 0, "PLAN_EXECUTION_FAILED", cause); + this.planId = planId; + this.failedStep = failedStep; + } - /** - * Returns the plan ID that failed. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } + /** + * Returns the plan ID that failed. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } - /** - * Returns the step that failed. - * - * @return the failed step ID - */ - public String getFailedStep() { - return failedStep; - } + /** + * Returns the step that failed. + * + * @return the failed step ID + */ + public String getFailedStep() { + return failedStep; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/PolicyViolationException.java b/src/main/java/com/getaxonflow/sdk/exceptions/PolicyViolationException.java index 544d22a..f69f8d1 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/PolicyViolationException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/PolicyViolationException.java @@ -21,10 +21,11 @@ /** * Thrown when a request is blocked by a policy. * - *

This exception provides details about which policy blocked the request - * and what the violation was. + *

This exception provides details about which policy blocked the request and what the violation + * was. * *

Example usage: + * *

{@code
  * try {
  *     axonflow.proxyLLMCall(request);
@@ -37,105 +38,111 @@
  */
 public class PolicyViolationException extends AxonFlowException {
 
-    private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 1L;
 
-    private final String policyName;
-    private final String blockReason;
-    private final List policiesEvaluated;
+  private final String policyName;
+  private final String blockReason;
+  private final List policiesEvaluated;
 
-    /**
-     * Creates a new PolicyViolationException.
-     *
-     * @param blockReason the reason the request was blocked
-     */
-    public PolicyViolationException(String blockReason) {
-        super("Request blocked by policy: " + blockReason, 403, "POLICY_VIOLATION");
-        this.blockReason = blockReason;
-        this.policyName = extractPolicyName(blockReason);
-        this.policiesEvaluated = Collections.emptyList();
-    }
+  /**
+   * Creates a new PolicyViolationException.
+   *
+   * @param blockReason the reason the request was blocked
+   */
+  public PolicyViolationException(String blockReason) {
+    super("Request blocked by policy: " + blockReason, 403, "POLICY_VIOLATION");
+    this.blockReason = blockReason;
+    this.policyName = extractPolicyName(blockReason);
+    this.policiesEvaluated = Collections.emptyList();
+  }
 
-    /**
-     * Creates a new PolicyViolationException with full details.
-     *
-     * @param blockReason        the reason the request was blocked
-     * @param policyName         the name of the policy that blocked the request
-     * @param policiesEvaluated  the list of policies that were evaluated
-     */
-    public PolicyViolationException(String blockReason, String policyName, List policiesEvaluated) {
-        super("Request blocked by policy: " + (policyName != null ? policyName : blockReason), 403, "POLICY_VIOLATION");
-        this.blockReason = blockReason;
-        this.policyName = policyName != null ? policyName : extractPolicyName(blockReason);
-        this.policiesEvaluated = policiesEvaluated != null
+  /**
+   * Creates a new PolicyViolationException with full details.
+   *
+   * @param blockReason the reason the request was blocked
+   * @param policyName the name of the policy that blocked the request
+   * @param policiesEvaluated the list of policies that were evaluated
+   */
+  public PolicyViolationException(
+      String blockReason, String policyName, List policiesEvaluated) {
+    super(
+        "Request blocked by policy: " + (policyName != null ? policyName : blockReason),
+        403,
+        "POLICY_VIOLATION");
+    this.blockReason = blockReason;
+    this.policyName = policyName != null ? policyName : extractPolicyName(blockReason);
+    this.policiesEvaluated =
+        policiesEvaluated != null
             ? Collections.unmodifiableList(policiesEvaluated)
             : Collections.emptyList();
-    }
+  }
 
-    /**
-     * Returns the name of the policy that blocked the request.
-     *
-     * @return the policy name
-     */
-    public String getPolicyName() {
-        return policyName;
-    }
+  /**
+   * Returns the name of the policy that blocked the request.
+   *
+   * @return the policy name
+   */
+  public String getPolicyName() {
+    return policyName;
+  }
 
-    /**
-     * Returns the detailed reason the request was blocked.
-     *
-     * @return the block reason
-     */
-    public String getBlockReason() {
-        return blockReason;
-    }
+  /**
+   * Returns the detailed reason the request was blocked.
+   *
+   * @return the block reason
+   */
+  public String getBlockReason() {
+    return blockReason;
+  }
 
-    /**
-     * Returns the list of policies that were evaluated.
-     *
-     * @return immutable list of policy names
-     */
-    public List getPoliciesEvaluated() {
-        return policiesEvaluated;
-    }
+  /**
+   * Returns the list of policies that were evaluated.
+   *
+   * @return immutable list of policy names
+   */
+  public List getPoliciesEvaluated() {
+    return policiesEvaluated;
+  }
 
-    /**
-     * Extracts the policy name from a block reason string.
-     *
-     * 

Handles common formats: - *

    - *
  • "Request blocked by policy: policy_name"
  • - *
  • "Blocked by policy: policy_name"
  • - *
  • "[policy_name] description"
  • - *
- * - * @param blockReason the block reason string - * @return the extracted policy name - */ - private static String extractPolicyName(String blockReason) { - if (blockReason == null || blockReason.isEmpty()) { - return "unknown"; - } - - // Handle format: "Request blocked by policy: policy_name" - String prefix = "Request blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } + /** + * Extracts the policy name from a block reason string. + * + *

Handles common formats: + * + *

    + *
  • "Request blocked by policy: policy_name" + *
  • "Blocked by policy: policy_name" + *
  • "[policy_name] description" + *
+ * + * @param blockReason the block reason string + * @return the extracted policy name + */ + private static String extractPolicyName(String blockReason) { + if (blockReason == null || blockReason.isEmpty()) { + return "unknown"; + } - // Handle format: "Blocked by policy: policy_name" - prefix = "Blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } + // Handle format: "Request blocked by policy: policy_name" + String prefix = "Request blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); + } - // Handle format: "[policy_name] description" - if (blockReason.startsWith("[")) { - int endBracket = blockReason.indexOf(']'); - if (endBracket > 1) { - return blockReason.substring(1, endBracket).trim(); - } - } + // Handle format: "Blocked by policy: policy_name" + prefix = "Blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); + } - return blockReason; + // Handle format: "[policy_name] description" + if (blockReason.startsWith("[")) { + int endBracket = blockReason.indexOf(']'); + if (endBracket > 1) { + return blockReason.substring(1, endBracket).trim(); + } } + + return blockReason; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/RateLimitException.java b/src/main/java/com/getaxonflow/sdk/exceptions/RateLimitException.java index 606369a..9e0713d 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/RateLimitException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/RateLimitException.java @@ -18,81 +18,79 @@ import java.time.Duration; import java.time.Instant; -/** - * Thrown when the rate limit has been exceeded. - */ +/** Thrown when the rate limit has been exceeded. */ public class RateLimitException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final int limit; - private final int remaining; - private final Instant resetAt; + private final int limit; + private final int remaining; + private final Instant resetAt; - /** - * Creates a new RateLimitException. - * - * @param message the error message - */ - public RateLimitException(String message) { - super(message, 429, "RATE_LIMIT_EXCEEDED"); - this.limit = 0; - this.remaining = 0; - this.resetAt = null; - } + /** + * Creates a new RateLimitException. + * + * @param message the error message + */ + public RateLimitException(String message) { + super(message, 429, "RATE_LIMIT_EXCEEDED"); + this.limit = 0; + this.remaining = 0; + this.resetAt = null; + } - /** - * Creates a new RateLimitException with rate limit details. - * - * @param message the error message - * @param limit the maximum requests allowed - * @param remaining the remaining requests in the current window - * @param resetAt when the rate limit resets - */ - public RateLimitException(String message, int limit, int remaining, Instant resetAt) { - super(message, 429, "RATE_LIMIT_EXCEEDED"); - this.limit = limit; - this.remaining = remaining; - this.resetAt = resetAt; - } + /** + * Creates a new RateLimitException with rate limit details. + * + * @param message the error message + * @param limit the maximum requests allowed + * @param remaining the remaining requests in the current window + * @param resetAt when the rate limit resets + */ + public RateLimitException(String message, int limit, int remaining, Instant resetAt) { + super(message, 429, "RATE_LIMIT_EXCEEDED"); + this.limit = limit; + this.remaining = remaining; + this.resetAt = resetAt; + } - /** - * Returns the maximum number of requests allowed. - * - * @return the rate limit - */ - public int getLimit() { - return limit; - } + /** + * Returns the maximum number of requests allowed. + * + * @return the rate limit + */ + public int getLimit() { + return limit; + } - /** - * Returns the remaining requests in the current window. - * - * @return the remaining count - */ - public int getRemaining() { - return remaining; - } + /** + * Returns the remaining requests in the current window. + * + * @return the remaining count + */ + public int getRemaining() { + return remaining; + } - /** - * Returns when the rate limit resets. - * - * @return the reset time - */ - public Instant getResetAt() { - return resetAt; - } + /** + * Returns when the rate limit resets. + * + * @return the reset time + */ + public Instant getResetAt() { + return resetAt; + } - /** - * Returns the duration until the rate limit resets. - * - * @return the duration until reset, or Duration.ZERO if already reset - */ - public Duration getRetryAfter() { - if (resetAt == null) { - return Duration.ZERO; - } - Duration duration = Duration.between(Instant.now(), resetAt); - return duration.isNegative() ? Duration.ZERO : duration; + /** + * Returns the duration until the rate limit resets. + * + * @return the duration until reset, or Duration.ZERO if already reset + */ + public Duration getRetryAfter() { + if (resetAt == null) { + return Duration.ZERO; } + Duration duration = Duration.between(Instant.now(), resetAt); + return duration.isNegative() ? Duration.ZERO : duration; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/TimeoutException.java b/src/main/java/com/getaxonflow/sdk/exceptions/TimeoutException.java index 099a83d..f6ad59a 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/TimeoutException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/TimeoutException.java @@ -17,65 +17,63 @@ import java.time.Duration; -/** - * Thrown when a request times out. - */ +/** Thrown when a request times out. */ public class TimeoutException extends AxonFlowException { - private static final long serialVersionUID = 1L; + private static final long serialVersionUID = 1L; - private final Duration timeout; + private final Duration timeout; - /** - * Creates a new TimeoutException. - * - * @param message the error message - */ - public TimeoutException(String message) { - super(message, 0, "TIMEOUT"); - this.timeout = null; - } + /** + * Creates a new TimeoutException. + * + * @param message the error message + */ + public TimeoutException(String message) { + super(message, 0, "TIMEOUT"); + this.timeout = null; + } - /** - * Creates a new TimeoutException with timeout duration. - * - * @param message the error message - * @param timeout the configured timeout duration - */ - public TimeoutException(String message, Duration timeout) { - super(message, 0, "TIMEOUT"); - this.timeout = timeout; - } + /** + * Creates a new TimeoutException with timeout duration. + * + * @param message the error message + * @param timeout the configured timeout duration + */ + public TimeoutException(String message, Duration timeout) { + super(message, 0, "TIMEOUT"); + this.timeout = timeout; + } - /** - * Creates a new TimeoutException with cause. - * - * @param message the error message - * @param cause the underlying cause - */ - public TimeoutException(String message, Throwable cause) { - super(message, 0, "TIMEOUT", cause); - this.timeout = null; - } + /** + * Creates a new TimeoutException with cause. + * + * @param message the error message + * @param cause the underlying cause + */ + public TimeoutException(String message, Throwable cause) { + super(message, 0, "TIMEOUT", cause); + this.timeout = null; + } - /** - * Creates a new TimeoutException with timeout and cause. - * - * @param message the error message - * @param timeout the configured timeout duration - * @param cause the underlying cause - */ - public TimeoutException(String message, Duration timeout, Throwable cause) { - super(message, 0, "TIMEOUT", cause); - this.timeout = timeout; - } + /** + * Creates a new TimeoutException with timeout and cause. + * + * @param message the error message + * @param timeout the configured timeout duration + * @param cause the underlying cause + */ + public TimeoutException(String message, Duration timeout, Throwable cause) { + super(message, 0, "TIMEOUT", cause); + this.timeout = timeout; + } - /** - * Returns the configured timeout duration. - * - * @return the timeout duration, or null if not specified - */ - public Duration getTimeout() { - return timeout; - } + /** + * Returns the configured timeout duration. + * + * @return the timeout duration, or null if not specified + */ + public Duration getTimeout() { + return timeout; + } } diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/VersionConflictException.java b/src/main/java/com/getaxonflow/sdk/exceptions/VersionConflictException.java index 4f497c3..5c433e8 100644 --- a/src/main/java/com/getaxonflow/sdk/exceptions/VersionConflictException.java +++ b/src/main/java/com/getaxonflow/sdk/exceptions/VersionConflictException.java @@ -18,12 +18,12 @@ /** * Thrown when a plan update fails due to a version conflict (HTTP 409). * - *

This indicates that the plan was modified by another client between - * the time it was read and the time the update was attempted. The caller - * should re-read the plan, resolve any conflicts, and retry with the - * updated version number. + *

This indicates that the plan was modified by another client between the time it was read and + * the time the update was attempted. The caller should re-read the plan, resolve any conflicts, and + * retry with the updated version number. * *

Example usage: + * *

{@code
  * try {
  *     axonflow.updatePlan(planId, request);
@@ -37,51 +37,52 @@
  */
 public class VersionConflictException extends AxonFlowException {
 
-    private static final long serialVersionUID = 1L;
+  private static final long serialVersionUID = 1L;
 
-    private final String planId;
-    private final int expectedVersion;
-    private final Integer currentVersion;
+  private final String planId;
+  private final int expectedVersion;
+  private final Integer currentVersion;
 
-    /**
-     * Creates a new VersionConflictException.
-     *
-     * @param message         the error message
-     * @param planId          the plan that had the conflict
-     * @param expectedVersion the version the client expected
-     * @param currentVersion  the actual current version on the server, or null if unknown
-     */
-    public VersionConflictException(String message, String planId, int expectedVersion, Integer currentVersion) {
-        super(message, 409, "VERSION_CONFLICT");
-        this.planId = planId;
-        this.expectedVersion = expectedVersion;
-        this.currentVersion = currentVersion;
-    }
+  /**
+   * Creates a new VersionConflictException.
+   *
+   * @param message the error message
+   * @param planId the plan that had the conflict
+   * @param expectedVersion the version the client expected
+   * @param currentVersion the actual current version on the server, or null if unknown
+   */
+  public VersionConflictException(
+      String message, String planId, int expectedVersion, Integer currentVersion) {
+    super(message, 409, "VERSION_CONFLICT");
+    this.planId = planId;
+    this.expectedVersion = expectedVersion;
+    this.currentVersion = currentVersion;
+  }
 
-    /**
-     * Returns the plan ID that had the version conflict.
-     *
-     * @return the plan ID
-     */
-    public String getPlanId() {
-        return planId;
-    }
+  /**
+   * Returns the plan ID that had the version conflict.
+   *
+   * @return the plan ID
+   */
+  public String getPlanId() {
+    return planId;
+  }
 
-    /**
-     * Returns the version the client expected.
-     *
-     * @return the expected version number
-     */
-    public int getExpectedVersion() {
-        return expectedVersion;
-    }
+  /**
+   * Returns the version the client expected.
+   *
+   * @return the expected version number
+   */
+  public int getExpectedVersion() {
+    return expectedVersion;
+  }
 
-    /**
-     * Returns the actual current version on the server.
-     *
-     * @return the current version, or null if unknown
-     */
-    public Integer getCurrentVersion() {
-        return currentVersion;
-    }
+  /**
+   * Returns the actual current version on the server.
+   *
+   * @return the current version, or null if unknown
+   */
+  public Integer getCurrentVersion() {
+    return currentVersion;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/exceptions/package-info.java b/src/main/java/com/getaxonflow/sdk/exceptions/package-info.java
index 51ade60..df23c14 100644
--- a/src/main/java/com/getaxonflow/sdk/exceptions/package-info.java
+++ b/src/main/java/com/getaxonflow/sdk/exceptions/package-info.java
@@ -17,10 +17,11 @@
 /**
  * Exception types for the AxonFlow SDK.
  *
- * 

All exceptions extend {@link com.getaxonflow.sdk.exceptions.AxonFlowException}, - * allowing callers to catch all SDK errors with a single catch block. + *

All exceptions extend {@link com.getaxonflow.sdk.exceptions.AxonFlowException}, allowing + * callers to catch all SDK errors with a single catch block. * *

Exception Hierarchy

+ * *
  * AxonFlowException (base)
  * ├── AuthenticationException   - Authentication/authorization failures
@@ -35,6 +36,7 @@
  * 
* *

Usage Example

+ * *
{@code
  * try {
  *     axonflow.proxyLLMCall(request);
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptor.java b/src/main/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptor.java
index 2287430..653bc8f 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptor.java
@@ -13,7 +13,6 @@
 import com.getaxonflow.sdk.types.ClientResponse;
 import com.getaxonflow.sdk.types.RequestType;
 import com.getaxonflow.sdk.types.TokenUsage;
-
 import java.util.*;
 import java.util.concurrent.CompletableFuture;
 import java.util.function.Function;
@@ -21,10 +20,11 @@
 /**
  * Interceptor for wrapping Anthropic API calls with AxonFlow governance.
  *
- * 

This interceptor automatically applies policy checks and audit logging - * to Anthropic API calls without requiring changes to application code. + *

This interceptor automatically applies policy checks and audit logging to Anthropic API calls + * without requiring changes to application code. * *

Example Usage

+ * *
{@code
  * // Create AxonFlow client
  * AxonFlow axonflow = AxonFlow.builder()
@@ -52,460 +52,538 @@
  * @see AxonFlow
  */
 public final class AnthropicInterceptor {
-    private final AxonFlow axonflow;
-    private final String userToken;
-    private final boolean asyncAudit;
-
-    private AnthropicInterceptor(Builder builder) {
-        this.axonflow = Objects.requireNonNull(builder.axonflow, "axonflow must not be null");
-        this.userToken = builder.userToken != null ? builder.userToken : "";
-        this.asyncAudit = builder.asyncAudit;
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    /**
-     * Wraps an Anthropic message creation function with governance.
-     *
-     * @param anthropicCall the function that makes the actual Anthropic API call
-     * @return a wrapped function that applies governance before/after the call
-     */
-    public Function wrap(
-            Function anthropicCall) {
-        return request -> {
-            // Extract prompt from messages
-            String prompt = request.extractPrompt();
-
-            // Build context for policy evaluation
-            Map context = new HashMap<>();
-            context.put("provider", "anthropic");
-            context.put("model", request.getModel());
-            if (request.getTemperature() != null) {
-                context.put("temperature", request.getTemperature());
-            }
-            context.put("max_tokens", request.getMaxTokens());
-
-            // Check with AxonFlow
-            long startTime = System.currentTimeMillis();
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            // Check if request was blocked
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
-
-            // Make the actual Anthropic call
-            AnthropicResponse result = anthropicCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
-
-            // Audit the call
-            if (axonResponse.getPlanId() != null) {
-                auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-            }
-
-            return result;
-        };
-    }
-
-    /**
-     * Wraps an async Anthropic message creation function with governance.
-     *
-     * @param anthropicCall the function that makes the actual Anthropic API call
-     * @return a wrapped function that applies governance before/after the call
-     */
-    public Function> wrapAsync(
-            Function> anthropicCall) {
-        return request -> {
-            // Extract prompt from messages
-            String prompt = request.extractPrompt();
-
-            // Build context for policy evaluation
-            Map context = new HashMap<>();
-            context.put("provider", "anthropic");
-            context.put("model", request.getModel());
-            if (request.getTemperature() != null) {
-                context.put("temperature", request.getTemperature());
-            }
-            context.put("max_tokens", request.getMaxTokens());
-
-            // Check with AxonFlow (async)
-            long startTime = System.currentTimeMillis();
-
-            return axonflow.proxyLLMCallAsync(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            ).thenCompose(axonResponse -> {
+  private final AxonFlow axonflow;
+  private final String userToken;
+  private final boolean asyncAudit;
+
+  private AnthropicInterceptor(Builder builder) {
+    this.axonflow = Objects.requireNonNull(builder.axonflow, "axonflow must not be null");
+    this.userToken = builder.userToken != null ? builder.userToken : "";
+    this.asyncAudit = builder.asyncAudit;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /**
+   * Wraps an Anthropic message creation function with governance.
+   *
+   * @param anthropicCall the function that makes the actual Anthropic API call
+   * @return a wrapped function that applies governance before/after the call
+   */
+  public Function wrap(
+      Function anthropicCall) {
+    return request -> {
+      // Extract prompt from messages
+      String prompt = request.extractPrompt();
+
+      // Build context for policy evaluation
+      Map context = new HashMap<>();
+      context.put("provider", "anthropic");
+      context.put("model", request.getModel());
+      if (request.getTemperature() != null) {
+        context.put("temperature", request.getTemperature());
+      }
+      context.put("max_tokens", request.getMaxTokens());
+
+      // Check with AxonFlow
+      long startTime = System.currentTimeMillis();
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      // Check if request was blocked
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
+
+      // Make the actual Anthropic call
+      AnthropicResponse result = anthropicCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
+
+      // Audit the call
+      if (axonResponse.getPlanId() != null) {
+        auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+      }
+
+      return result;
+    };
+  }
+
+  /**
+   * Wraps an async Anthropic message creation function with governance.
+   *
+   * @param anthropicCall the function that makes the actual Anthropic API call
+   * @return a wrapped function that applies governance before/after the call
+   */
+  public Function> wrapAsync(
+      Function> anthropicCall) {
+    return request -> {
+      // Extract prompt from messages
+      String prompt = request.extractPrompt();
+
+      // Build context for policy evaluation
+      Map context = new HashMap<>();
+      context.put("provider", "anthropic");
+      context.put("model", request.getModel());
+      if (request.getTemperature() != null) {
+        context.put("temperature", request.getTemperature());
+      }
+      context.put("max_tokens", request.getMaxTokens());
+
+      // Check with AxonFlow (async)
+      long startTime = System.currentTimeMillis();
+
+      return axonflow
+          .proxyLLMCallAsync(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build())
+          .thenCompose(
+              axonResponse -> {
                 // Check if request was blocked
                 if (axonResponse.isBlocked()) {
-                    CompletableFuture failed = new CompletableFuture<>();
-                    failed.completeExceptionally(new PolicyViolationException(
-                        axonResponse.getBlockReason()
-                    ));
-                    return failed;
+                  CompletableFuture failed = new CompletableFuture<>();
+                  failed.completeExceptionally(
+                      new PolicyViolationException(axonResponse.getBlockReason()));
+                  return failed;
                 }
 
                 // Make the actual Anthropic call
-                return anthropicCall.apply(request).thenApply(result -> {
-                    long latencyMs = System.currentTimeMillis() - startTime;
-
-                    // Audit the call
-                    if (axonResponse.getPlanId() != null) {
-                        if (asyncAudit) {
-                            CompletableFuture.runAsync(() ->
-                                auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs)
-                            );
-                        } else {
-                            auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-                        }
-                    }
-
-                    return result;
-                });
-            });
-        };
-    }
-
-    private void auditCall(String contextId, AnthropicResponse result, String model, long latencyMs) {
-        try {
-            AnthropicResponse.Usage usage = result.getUsage();
-            TokenUsage tokenUsage = usage != null ?
-                TokenUsage.of(usage.getInputTokens(), usage.getOutputTokens()) :
-                TokenUsage.of(0, 0);
-
-            axonflow.auditLLMCall(AuditOptions.builder()
-                .contextId(contextId)
-                .clientId(userToken)
-                .responseSummary(result.getSummary())
-                .provider("anthropic")
-                .model(model)
-                .tokenUsage(tokenUsage)
-                .latencyMs(latencyMs)
-                .success(true)
-                .build());
-        } catch (Exception e) {
-            // Best effort - don't fail the response if audit fails
-        }
+                return anthropicCall
+                    .apply(request)
+                    .thenApply(
+                        result -> {
+                          long latencyMs = System.currentTimeMillis() - startTime;
+
+                          // Audit the call
+                          if (axonResponse.getPlanId() != null) {
+                            if (asyncAudit) {
+                              CompletableFuture.runAsync(
+                                  () ->
+                                      auditCall(
+                                          axonResponse.getPlanId(),
+                                          result,
+                                          request.getModel(),
+                                          latencyMs));
+                            } else {
+                              auditCall(
+                                  axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+                            }
+                          }
+
+                          return result;
+                        });
+              });
+    };
+  }
+
+  private void auditCall(String contextId, AnthropicResponse result, String model, long latencyMs) {
+    try {
+      AnthropicResponse.Usage usage = result.getUsage();
+      TokenUsage tokenUsage =
+          usage != null
+              ? TokenUsage.of(usage.getInputTokens(), usage.getOutputTokens())
+              : TokenUsage.of(0, 0);
+
+      axonflow.auditLLMCall(
+          AuditOptions.builder()
+              .contextId(contextId)
+              .clientId(userToken)
+              .responseSummary(result.getSummary())
+              .provider("anthropic")
+              .model(model)
+              .tokenUsage(tokenUsage)
+              .latencyMs(latencyMs)
+              .success(true)
+              .build());
+    } catch (Exception e) {
+      // Best effort - don't fail the response if audit fails
+    }
+  }
+
+  /**
+   * Creates a simple wrapper function for Anthropic messages.
+   *
+   * @param axonflow the AxonFlow client
+   * @param userToken the user token for policy evaluation
+   * @param anthropicCall the function that makes the actual Anthropic API call
+   * @return a wrapped function
+   */
+  public static Function wrapMessage(
+      AxonFlow axonflow,
+      String userToken,
+      Function anthropicCall) {
+    return builder().axonflow(axonflow).userToken(userToken).build().wrap(anthropicCall);
+  }
+
+  /** Request for Anthropic message creation. */
+  public static final class AnthropicRequest {
+    private final String model;
+    private final int maxTokens;
+    private final List messages;
+    private final String system;
+    private final Double temperature;
+    private final Double topP;
+    private final Integer topK;
+
+    private AnthropicRequest(Builder builder) {
+      this.model = Objects.requireNonNull(builder.model, "model must not be null");
+      this.maxTokens = builder.maxTokens;
+      this.messages = Collections.unmodifiableList(new ArrayList<>(builder.messages));
+      this.system = builder.system;
+      this.temperature = builder.temperature;
+      this.topP = builder.topP;
+      this.topK = builder.topK;
     }
 
-    /**
-     * Creates a simple wrapper function for Anthropic messages.
-     *
-     * @param axonflow      the AxonFlow client
-     * @param userToken     the user token for policy evaluation
-     * @param anthropicCall the function that makes the actual Anthropic API call
-     * @return a wrapped function
-     */
-    public static Function wrapMessage(
-            AxonFlow axonflow,
-            String userToken,
-            Function anthropicCall) {
-        return builder()
-            .axonflow(axonflow)
-            .userToken(userToken)
-            .build()
-            .wrap(anthropicCall);
-    }
-
-    /**
-     * Request for Anthropic message creation.
-     */
-    public static final class AnthropicRequest {
-        private final String model;
-        private final int maxTokens;
-        private final List messages;
-        private final String system;
-        private final Double temperature;
-        private final Double topP;
-        private final Integer topK;
-
-        private AnthropicRequest(Builder builder) {
-            this.model = Objects.requireNonNull(builder.model, "model must not be null");
-            this.maxTokens = builder.maxTokens;
-            this.messages = Collections.unmodifiableList(new ArrayList<>(builder.messages));
-            this.system = builder.system;
-            this.temperature = builder.temperature;
-            this.topP = builder.topP;
-            this.topK = builder.topK;
-        }
+    public static Builder builder() {
+      return new Builder();
+    }
 
-        public static Builder builder() {
-            return new Builder();
-        }
+    public String getModel() {
+      return model;
+    }
 
-        public String getModel() { return model; }
-        public int getMaxTokens() { return maxTokens; }
-        public List getMessages() { return messages; }
-        public String getSystem() { return system; }
-        public Double getTemperature() { return temperature; }
-        public Double getTopP() { return topP; }
-        public Integer getTopK() { return topK; }
-
-        /**
-         * Extracts the combined prompt from system and messages.
-         */
-        public String extractPrompt() {
-            StringBuilder sb = new StringBuilder();
-            if (system != null && !system.isEmpty()) {
-                sb.append(system);
-            }
-            for (AnthropicMessage msg : messages) {
-                for (AnthropicContentBlock block : msg.getContent()) {
-                    if ("text".equals(block.getType()) && block.getText() != null) {
-                        if (sb.length() > 0) {
-                            sb.append(" ");
-                        }
-                        sb.append(block.getText());
-                    }
-                }
-            }
-            return sb.toString();
-        }
+    public int getMaxTokens() {
+      return maxTokens;
+    }
 
-        public static final class Builder {
-            private String model;
-            private int maxTokens = 1024;
-            private final List messages = new ArrayList<>();
-            private String system;
-            private Double temperature;
-            private Double topP;
-            private Integer topK;
+    public List getMessages() {
+      return messages;
+    }
 
-            private Builder() {}
+    public String getSystem() {
+      return system;
+    }
 
-            public Builder model(String model) {
-                this.model = model;
-                return this;
-            }
+    public Double getTemperature() {
+      return temperature;
+    }
 
-            public Builder maxTokens(int maxTokens) {
-                this.maxTokens = maxTokens;
-                return this;
-            }
+    public Double getTopP() {
+      return topP;
+    }
 
-            public Builder messages(List messages) {
-                this.messages.clear();
-                if (messages != null) {
-                    this.messages.addAll(messages);
-                }
-                return this;
-            }
+    public Integer getTopK() {
+      return topK;
+    }
 
-            public Builder addMessage(AnthropicMessage message) {
-                this.messages.add(message);
-                return this;
+    /** Extracts the combined prompt from system and messages. */
+    public String extractPrompt() {
+      StringBuilder sb = new StringBuilder();
+      if (system != null && !system.isEmpty()) {
+        sb.append(system);
+      }
+      for (AnthropicMessage msg : messages) {
+        for (AnthropicContentBlock block : msg.getContent()) {
+          if ("text".equals(block.getType()) && block.getText() != null) {
+            if (sb.length() > 0) {
+              sb.append(" ");
             }
+            sb.append(block.getText());
+          }
+        }
+      }
+      return sb.toString();
+    }
 
-            public Builder addUserMessage(String text) {
-                this.messages.add(AnthropicMessage.user(text));
-                return this;
-            }
+    public static final class Builder {
+      private String model;
+      private int maxTokens = 1024;
+      private final List messages = new ArrayList<>();
+      private String system;
+      private Double temperature;
+      private Double topP;
+      private Integer topK;
+
+      private Builder() {}
+
+      public Builder model(String model) {
+        this.model = model;
+        return this;
+      }
+
+      public Builder maxTokens(int maxTokens) {
+        this.maxTokens = maxTokens;
+        return this;
+      }
+
+      public Builder messages(List messages) {
+        this.messages.clear();
+        if (messages != null) {
+          this.messages.addAll(messages);
+        }
+        return this;
+      }
+
+      public Builder addMessage(AnthropicMessage message) {
+        this.messages.add(message);
+        return this;
+      }
+
+      public Builder addUserMessage(String text) {
+        this.messages.add(AnthropicMessage.user(text));
+        return this;
+      }
+
+      public Builder addAssistantMessage(String text) {
+        this.messages.add(AnthropicMessage.assistant(text));
+        return this;
+      }
+
+      public Builder system(String system) {
+        this.system = system;
+        return this;
+      }
+
+      public Builder temperature(Double temperature) {
+        this.temperature = temperature;
+        return this;
+      }
+
+      public Builder topP(Double topP) {
+        this.topP = topP;
+        return this;
+      }
+
+      public Builder topK(Integer topK) {
+        this.topK = topK;
+        return this;
+      }
+
+      public AnthropicRequest build() {
+        return new AnthropicRequest(this);
+      }
+    }
+  }
+
+  /** Response from Anthropic message creation. */
+  public static final class AnthropicResponse {
+    private final String id;
+    private final String type;
+    private final String role;
+    private final String model;
+    private final List content;
+    private final String stopReason;
+    private final Usage usage;
+
+    private AnthropicResponse(Builder builder) {
+      this.id = builder.id;
+      this.type = builder.type;
+      this.role = builder.role;
+      this.model = builder.model;
+      this.content =
+          builder.content != null
+              ? Collections.unmodifiableList(new ArrayList<>(builder.content))
+              : Collections.emptyList();
+      this.stopReason = builder.stopReason;
+      this.usage = builder.usage;
+    }
 
-            public Builder addAssistantMessage(String text) {
-                this.messages.add(AnthropicMessage.assistant(text));
-                return this;
-            }
+    public static Builder builder() {
+      return new Builder();
+    }
 
-            public Builder system(String system) {
-                this.system = system;
-                return this;
-            }
+    public String getId() {
+      return id;
+    }
 
-            public Builder temperature(Double temperature) {
-                this.temperature = temperature;
-                return this;
-            }
+    public String getType() {
+      return type;
+    }
 
-            public Builder topP(Double topP) {
-                this.topP = topP;
-                return this;
-            }
+    public String getRole() {
+      return role;
+    }
 
-            public Builder topK(Integer topK) {
-                this.topK = topK;
-                return this;
-            }
+    public String getModel() {
+      return model;
+    }
 
-            public AnthropicRequest build() {
-                return new AnthropicRequest(this);
-            }
-        }
+    public List getContent() {
+      return content;
     }
 
-    /**
-     * Response from Anthropic message creation.
-     */
-    public static final class AnthropicResponse {
-        private final String id;
-        private final String type;
-        private final String role;
-        private final String model;
-        private final List content;
-        private final String stopReason;
-        private final Usage usage;
-
-        private AnthropicResponse(Builder builder) {
-            this.id = builder.id;
-            this.type = builder.type;
-            this.role = builder.role;
-            this.model = builder.model;
-            this.content = builder.content != null ?
-                Collections.unmodifiableList(new ArrayList<>(builder.content)) :
-                Collections.emptyList();
-            this.stopReason = builder.stopReason;
-            this.usage = builder.usage;
-        }
+    public String getStopReason() {
+      return stopReason;
+    }
 
-        public static Builder builder() {
-            return new Builder();
-        }
+    public Usage getUsage() {
+      return usage;
+    }
 
-        public String getId() { return id; }
-        public String getType() { return type; }
-        public String getRole() { return role; }
-        public String getModel() { return model; }
-        public List getContent() { return content; }
-        public String getStopReason() { return stopReason; }
-        public Usage getUsage() { return usage; }
-
-        /**
-         * Gets a summary of the response (first 100 characters of text content).
-         */
-        public String getSummary() {
-            for (AnthropicContentBlock block : content) {
-                if ("text".equals(block.getType()) && block.getText() != null) {
-                    String text = block.getText();
-                    if (text.length() > 100) {
-                        return text.substring(0, 100);
-                    }
-                    return text;
-                }
-            }
-            return "";
+    /** Gets a summary of the response (first 100 characters of text content). */
+    public String getSummary() {
+      for (AnthropicContentBlock block : content) {
+        if ("text".equals(block.getType()) && block.getText() != null) {
+          String text = block.getText();
+          if (text.length() > 100) {
+            return text.substring(0, 100);
+          }
+          return text;
         }
+      }
+      return "";
+    }
 
-        public static final class Usage {
-            private final int inputTokens;
-            private final int outputTokens;
+    public static final class Usage {
+      private final int inputTokens;
+      private final int outputTokens;
 
-            public Usage(int inputTokens, int outputTokens) {
-                this.inputTokens = inputTokens;
-                this.outputTokens = outputTokens;
-            }
+      public Usage(int inputTokens, int outputTokens) {
+        this.inputTokens = inputTokens;
+        this.outputTokens = outputTokens;
+      }
 
-            public int getInputTokens() { return inputTokens; }
-            public int getOutputTokens() { return outputTokens; }
-        }
+      public int getInputTokens() {
+        return inputTokens;
+      }
 
-        public static final class Builder {
-            private String id;
-            private String type = "message";
-            private String role = "assistant";
-            private String model;
-            private List content;
-            private String stopReason;
-            private Usage usage;
-
-            private Builder() {}
-
-            public Builder id(String id) { this.id = id; return this; }
-            public Builder type(String type) { this.type = type; return this; }
-            public Builder role(String role) { this.role = role; return this; }
-            public Builder model(String model) { this.model = model; return this; }
-            public Builder content(List content) { this.content = content; return this; }
-            public Builder stopReason(String stopReason) { this.stopReason = stopReason; return this; }
-            public Builder usage(Usage usage) { this.usage = usage; return this; }
-
-            public AnthropicResponse build() {
-                return new AnthropicResponse(this);
-            }
-        }
+      public int getOutputTokens() {
+        return outputTokens;
+      }
     }
 
-    /**
-     * Anthropic message with content blocks.
-     */
-    public static final class AnthropicMessage {
-        private final String role;
-        private final List content;
+    public static final class Builder {
+      private String id;
+      private String type = "message";
+      private String role = "assistant";
+      private String model;
+      private List content;
+      private String stopReason;
+      private Usage usage;
+
+      private Builder() {}
+
+      public Builder id(String id) {
+        this.id = id;
+        return this;
+      }
+
+      public Builder type(String type) {
+        this.type = type;
+        return this;
+      }
+
+      public Builder role(String role) {
+        this.role = role;
+        return this;
+      }
+
+      public Builder model(String model) {
+        this.model = model;
+        return this;
+      }
+
+      public Builder content(List content) {
+        this.content = content;
+        return this;
+      }
+
+      public Builder stopReason(String stopReason) {
+        this.stopReason = stopReason;
+        return this;
+      }
+
+      public Builder usage(Usage usage) {
+        this.usage = usage;
+        return this;
+      }
+
+      public AnthropicResponse build() {
+        return new AnthropicResponse(this);
+      }
+    }
+  }
 
-        private AnthropicMessage(String role, List content) {
-            this.role = Objects.requireNonNull(role);
-            this.content = Collections.unmodifiableList(new ArrayList<>(content));
-        }
+  /** Anthropic message with content blocks. */
+  public static final class AnthropicMessage {
+    private final String role;
+    private final List content;
 
-        public static AnthropicMessage of(String role, List content) {
-            return new AnthropicMessage(role, content);
-        }
+    private AnthropicMessage(String role, List content) {
+      this.role = Objects.requireNonNull(role);
+      this.content = Collections.unmodifiableList(new ArrayList<>(content));
+    }
 
-        public static AnthropicMessage user(String text) {
-            return new AnthropicMessage("user", List.of(AnthropicContentBlock.text(text)));
-        }
+    public static AnthropicMessage of(String role, List content) {
+      return new AnthropicMessage(role, content);
+    }
 
-        public static AnthropicMessage assistant(String text) {
-            return new AnthropicMessage("assistant", List.of(AnthropicContentBlock.text(text)));
-        }
+    public static AnthropicMessage user(String text) {
+      return new AnthropicMessage("user", List.of(AnthropicContentBlock.text(text)));
+    }
 
-        public String getRole() { return role; }
-        public List getContent() { return content; }
+    public static AnthropicMessage assistant(String text) {
+      return new AnthropicMessage("assistant", List.of(AnthropicContentBlock.text(text)));
     }
 
-    /**
-     * Content block in an Anthropic message.
-     */
-    public static final class AnthropicContentBlock {
-        private final String type;
-        private final String text;
+    public String getRole() {
+      return role;
+    }
 
-        private AnthropicContentBlock(String type, String text) {
-            this.type = type;
-            this.text = text;
-        }
+    public List getContent() {
+      return content;
+    }
+  }
 
-        public static AnthropicContentBlock text(String text) {
-            return new AnthropicContentBlock("text", text);
-        }
+  /** Content block in an Anthropic message. */
+  public static final class AnthropicContentBlock {
+    private final String type;
+    private final String text;
 
-        public String getType() { return type; }
-        public String getText() { return text; }
+    private AnthropicContentBlock(String type, String text) {
+      this.type = type;
+      this.text = text;
     }
 
-    public static final class Builder {
-        private AxonFlow axonflow;
-        private String userToken;
-        private boolean asyncAudit = true;
+    public static AnthropicContentBlock text(String text) {
+      return new AnthropicContentBlock("text", text);
+    }
 
-        private Builder() {}
+    public String getType() {
+      return type;
+    }
 
-        public Builder axonflow(AxonFlow axonflow) {
-            this.axonflow = axonflow;
-            return this;
-        }
+    public String getText() {
+      return text;
+    }
+  }
 
-        public Builder userToken(String userToken) {
-            this.userToken = userToken;
-            return this;
-        }
+  public static final class Builder {
+    private AxonFlow axonflow;
+    private String userToken;
+    private boolean asyncAudit = true;
 
-        public Builder asyncAudit(boolean asyncAudit) {
-            this.asyncAudit = asyncAudit;
-            return this;
-        }
+    private Builder() {}
 
-        public AnthropicInterceptor build() {
-            return new AnthropicInterceptor(this);
-        }
+    public Builder axonflow(AxonFlow axonflow) {
+      this.axonflow = axonflow;
+      return this;
+    }
+
+    public Builder userToken(String userToken) {
+      this.userToken = userToken;
+      return this;
+    }
+
+    public Builder asyncAudit(boolean asyncAudit) {
+      this.asyncAudit = asyncAudit;
+      return this;
+    }
+
+    public AnthropicInterceptor build() {
+      return new AnthropicInterceptor(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/BedrockInterceptor.java b/src/main/java/com/getaxonflow/sdk/interceptors/BedrockInterceptor.java
index 8f6a60e..db99849 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/BedrockInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/BedrockInterceptor.java
@@ -2,13 +2,11 @@
 
 import com.getaxonflow.sdk.AxonFlow;
 import com.getaxonflow.sdk.exceptions.PolicyViolationException;
+import com.getaxonflow.sdk.types.AuditOptions;
 import com.getaxonflow.sdk.types.ClientRequest;
 import com.getaxonflow.sdk.types.ClientResponse;
 import com.getaxonflow.sdk.types.RequestType;
 import com.getaxonflow.sdk.types.TokenUsage;
-import com.getaxonflow.sdk.types.AuditOptions;
-
-import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -19,10 +17,11 @@
 /**
  * Interceptor for AWS Bedrock API calls with automatic governance.
  *
- * 

Bedrock uses AWS IAM authentication (no API keys required). - * Supports multiple model providers: Anthropic Claude, Amazon Titan, Meta Llama, etc. + *

Bedrock uses AWS IAM authentication (no API keys required). Supports multiple model providers: + * Anthropic Claude, Amazon Titan, Meta Llama, etc. * *

Example Usage

+ * *
{@code
  * AxonFlow axonflow = new AxonFlow(config);
  * BedrockInterceptor interceptor = new BedrockInterceptor(axonflow, "user-123");
@@ -38,257 +37,324 @@
  */
 public class BedrockInterceptor {
 
-    private final AxonFlow axonflow;
-    private final String userToken;
-
-    // Common Bedrock model IDs
-    public static final String CLAUDE_3_OPUS = "anthropic.claude-3-opus-20240229-v1:0";
-    public static final String CLAUDE_3_SONNET = "anthropic.claude-3-sonnet-20240229-v1:0";
-    public static final String CLAUDE_3_HAIKU = "anthropic.claude-3-haiku-20240307-v1:0";
-    public static final String CLAUDE_2 = "anthropic.claude-v2:1";
-    public static final String TITAN_TEXT_EXPRESS = "amazon.titan-text-express-v1";
-    public static final String TITAN_TEXT_LITE = "amazon.titan-text-lite-v1";
-    public static final String LLAMA2_70B = "meta.llama2-70b-chat-v1";
-    public static final String LLAMA3_70B = "meta.llama3-70b-instruct-v1:0";
-
-    /**
-     * Creates a new BedrockInterceptor.
-     *
-     * @param axonflow the AxonFlow client for governance
-     * @param userToken the user token for policy evaluation
-     */
-    public BedrockInterceptor(AxonFlow axonflow, String userToken) {
-        if (axonflow == null) {
-            throw new IllegalArgumentException("axonflow cannot be null");
-        }
-        if (userToken == null || userToken.isEmpty()) {
-            throw new IllegalArgumentException("userToken cannot be null or empty");
-        }
-        this.axonflow = axonflow;
-        this.userToken = userToken;
-    }
-
-    /**
-     * Wraps a synchronous Bedrock InvokeModel call with governance.
-     *
-     * @param bedrockCall the original Bedrock call function
-     * @return a wrapped function that applies governance
-     */
-    public Function wrap(
-            Function bedrockCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            Map context = new HashMap<>();
-            context.put("provider", "bedrock");
-            context.put("model", request.getModelId());
-
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
-
-            long startTime = System.currentTimeMillis();
-            BedrockInvokeResponse result = bedrockCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
-
-            if (axonResponse.getPlanId() != null) {
-                auditCall(axonResponse.getPlanId(), result, request.getModelId(), latencyMs);
-            }
-
-            return result;
-        };
-    }
-
-    /**
-     * Wraps an asynchronous Bedrock InvokeModel call with governance.
-     */
-    public Function> wrapAsync(
-            Function> bedrockCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            Map context = new HashMap<>();
-            context.put("provider", "bedrock");
-            context.put("model", request.getModelId());
-
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                return CompletableFuture.failedFuture(
-                    new PolicyViolationException(axonResponse.getBlockReason())
-                );
-            }
-
-            long startTime = System.currentTimeMillis();
-            String planId = axonResponse.getPlanId();
-            String modelId = request.getModelId();
-
-            return bedrockCall.apply(request)
-                .thenApply(result -> {
-                    long latencyMs = System.currentTimeMillis() - startTime;
-                    if (planId != null) {
-                        auditCall(planId, result, modelId, latencyMs);
-                    }
-                    return result;
-                });
-        };
-    }
-
-    private void auditCall(String contextId, BedrockInvokeResponse response, String modelId, long latencyMs) {
-        try {
-            String summary = response != null ? response.getSummary() : "";
-
-            int promptTokens = response != null ? response.getInputTokens() : 0;
-            int completionTokens = response != null ? response.getOutputTokens() : 0;
-            TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
-
-            AuditOptions auditOptions = AuditOptions.builder()
-                .contextId(contextId)
-                .responseSummary(summary)
-                .provider("bedrock")
-                .model(modelId)
-                .tokenUsage(usage)
-                .latencyMs(latencyMs)
-                .build();
-
-            axonflow.auditLLMCall(auditOptions);
-        } catch (Exception e) {
-            // Log but don't fail
-        }
-    }
-
-    // ==================== Bedrock Request/Response Types ====================
-
-    /**
-     * Bedrock InvokeModel request.
-     */
-    public static class BedrockInvokeRequest {
-        private String modelId;
-        private String body;
-        private String contentType = "application/json";
-        private String accept = "application/json";
-
-        // Parsed body fields (for convenience)
-        private List messages;
-        private String inputText; // For Titan
-
-        public static BedrockInvokeRequest forClaude(String modelId, List messages, int maxTokens) {
-            BedrockInvokeRequest req = new BedrockInvokeRequest();
-            req.modelId = modelId;
-            req.messages = messages;
-            // Body would be built from messages in practice
-            return req;
-        }
-
-        public static BedrockInvokeRequest forTitan(String modelId, String inputText) {
-            BedrockInvokeRequest req = new BedrockInvokeRequest();
-            req.modelId = modelId;
-            req.inputText = inputText;
-            return req;
-        }
-
-        public String extractPrompt() {
-            if (messages != null && !messages.isEmpty()) {
-                return messages.stream()
-                    .map(ClaudeMessage::getContent)
-                    .collect(Collectors.joining(" "));
-            }
-            if (inputText != null) {
-                return inputText;
-            }
-            return "";
-        }
-
-        public String getModelId() { return modelId; }
-        public void setModelId(String modelId) { this.modelId = modelId; }
-        public String getBody() { return body; }
-        public void setBody(String body) { this.body = body; }
-        public String getContentType() { return contentType; }
-        public void setContentType(String contentType) { this.contentType = contentType; }
-        public String getAccept() { return accept; }
-        public void setAccept(String accept) { this.accept = accept; }
-        public List getMessages() { return messages; }
-        public void setMessages(List messages) { this.messages = messages; }
-        public String getInputText() { return inputText; }
-        public void setInputText(String inputText) { this.inputText = inputText; }
-    }
-
-    /**
-     * Claude message format for Bedrock.
-     */
-    public static class ClaudeMessage {
-        private String role;
-        private String content;
-
-        public ClaudeMessage() {}
-
-        public ClaudeMessage(String role, String content) {
-            this.role = role;
-            this.content = content;
-        }
-
-        public static ClaudeMessage user(String content) {
-            return new ClaudeMessage("user", content);
-        }
-
-        public static ClaudeMessage assistant(String content) {
-            return new ClaudeMessage("assistant", content);
-        }
-
-        public String getRole() { return role; }
-        public void setRole(String role) { this.role = role; }
-        public String getContent() { return content; }
-        public void setContent(String content) { this.content = content; }
-    }
-
-    /**
-     * Bedrock InvokeModel response.
-     */
-    public static class BedrockInvokeResponse {
-        private byte[] body;
-        private String contentType;
-
-        // Parsed response fields
-        private String responseText;
-        private int inputTokens;
-        private int outputTokens;
-
-        public String getSummary() {
-            if (responseText == null || responseText.isEmpty()) {
-                return "";
-            }
-            return responseText.length() > 100
-                ? responseText.substring(0, 100) + "..."
-                : responseText;
-        }
-
-        public byte[] getBody() { return body; }
-        public void setBody(byte[] body) { this.body = body; }
-        public String getContentType() { return contentType; }
-        public void setContentType(String contentType) { this.contentType = contentType; }
-        public String getResponseText() { return responseText; }
-        public void setResponseText(String responseText) { this.responseText = responseText; }
-        public int getInputTokens() { return inputTokens; }
-        public void setInputTokens(int inputTokens) { this.inputTokens = inputTokens; }
-        public int getOutputTokens() { return outputTokens; }
-        public void setOutputTokens(int outputTokens) { this.outputTokens = outputTokens; }
+  private final AxonFlow axonflow;
+  private final String userToken;
+
+  // Common Bedrock model IDs
+  public static final String CLAUDE_3_OPUS = "anthropic.claude-3-opus-20240229-v1:0";
+  public static final String CLAUDE_3_SONNET = "anthropic.claude-3-sonnet-20240229-v1:0";
+  public static final String CLAUDE_3_HAIKU = "anthropic.claude-3-haiku-20240307-v1:0";
+  public static final String CLAUDE_2 = "anthropic.claude-v2:1";
+  public static final String TITAN_TEXT_EXPRESS = "amazon.titan-text-express-v1";
+  public static final String TITAN_TEXT_LITE = "amazon.titan-text-lite-v1";
+  public static final String LLAMA2_70B = "meta.llama2-70b-chat-v1";
+  public static final String LLAMA3_70B = "meta.llama3-70b-instruct-v1:0";
+
+  /**
+   * Creates a new BedrockInterceptor.
+   *
+   * @param axonflow the AxonFlow client for governance
+   * @param userToken the user token for policy evaluation
+   */
+  public BedrockInterceptor(AxonFlow axonflow, String userToken) {
+    if (axonflow == null) {
+      throw new IllegalArgumentException("axonflow cannot be null");
+    }
+    if (userToken == null || userToken.isEmpty()) {
+      throw new IllegalArgumentException("userToken cannot be null or empty");
+    }
+    this.axonflow = axonflow;
+    this.userToken = userToken;
+  }
+
+  /**
+   * Wraps a synchronous Bedrock InvokeModel call with governance.
+   *
+   * @param bedrockCall the original Bedrock call function
+   * @return a wrapped function that applies governance
+   */
+  public Function wrap(
+      Function bedrockCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      Map context = new HashMap<>();
+      context.put("provider", "bedrock");
+      context.put("model", request.getModelId());
+
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
+
+      long startTime = System.currentTimeMillis();
+      BedrockInvokeResponse result = bedrockCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
+
+      if (axonResponse.getPlanId() != null) {
+        auditCall(axonResponse.getPlanId(), result, request.getModelId(), latencyMs);
+      }
+
+      return result;
+    };
+  }
+
+  /** Wraps an asynchronous Bedrock InvokeModel call with governance. */
+  public Function> wrapAsync(
+      Function> bedrockCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      Map context = new HashMap<>();
+      context.put("provider", "bedrock");
+      context.put("model", request.getModelId());
+
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        return CompletableFuture.failedFuture(
+            new PolicyViolationException(axonResponse.getBlockReason()));
+      }
+
+      long startTime = System.currentTimeMillis();
+      String planId = axonResponse.getPlanId();
+      String modelId = request.getModelId();
+
+      return bedrockCall
+          .apply(request)
+          .thenApply(
+              result -> {
+                long latencyMs = System.currentTimeMillis() - startTime;
+                if (planId != null) {
+                  auditCall(planId, result, modelId, latencyMs);
+                }
+                return result;
+              });
+    };
+  }
+
+  private void auditCall(
+      String contextId, BedrockInvokeResponse response, String modelId, long latencyMs) {
+    try {
+      String summary = response != null ? response.getSummary() : "";
+
+      int promptTokens = response != null ? response.getInputTokens() : 0;
+      int completionTokens = response != null ? response.getOutputTokens() : 0;
+      TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
+
+      AuditOptions auditOptions =
+          AuditOptions.builder()
+              .contextId(contextId)
+              .responseSummary(summary)
+              .provider("bedrock")
+              .model(modelId)
+              .tokenUsage(usage)
+              .latencyMs(latencyMs)
+              .build();
+
+      axonflow.auditLLMCall(auditOptions);
+    } catch (Exception e) {
+      // Log but don't fail
+    }
+  }
+
+  // ==================== Bedrock Request/Response Types ====================
+
+  /** Bedrock InvokeModel request. */
+  public static class BedrockInvokeRequest {
+    private String modelId;
+    private String body;
+    private String contentType = "application/json";
+    private String accept = "application/json";
+
+    // Parsed body fields (for convenience)
+    private List messages;
+    private String inputText; // For Titan
+
+    public static BedrockInvokeRequest forClaude(
+        String modelId, List messages, int maxTokens) {
+      BedrockInvokeRequest req = new BedrockInvokeRequest();
+      req.modelId = modelId;
+      req.messages = messages;
+      // Body would be built from messages in practice
+      return req;
+    }
+
+    public static BedrockInvokeRequest forTitan(String modelId, String inputText) {
+      BedrockInvokeRequest req = new BedrockInvokeRequest();
+      req.modelId = modelId;
+      req.inputText = inputText;
+      return req;
+    }
+
+    public String extractPrompt() {
+      if (messages != null && !messages.isEmpty()) {
+        return messages.stream().map(ClaudeMessage::getContent).collect(Collectors.joining(" "));
+      }
+      if (inputText != null) {
+        return inputText;
+      }
+      return "";
+    }
+
+    public String getModelId() {
+      return modelId;
+    }
+
+    public void setModelId(String modelId) {
+      this.modelId = modelId;
+    }
+
+    public String getBody() {
+      return body;
+    }
+
+    public void setBody(String body) {
+      this.body = body;
+    }
+
+    public String getContentType() {
+      return contentType;
+    }
+
+    public void setContentType(String contentType) {
+      this.contentType = contentType;
+    }
+
+    public String getAccept() {
+      return accept;
+    }
+
+    public void setAccept(String accept) {
+      this.accept = accept;
+    }
+
+    public List getMessages() {
+      return messages;
+    }
+
+    public void setMessages(List messages) {
+      this.messages = messages;
+    }
+
+    public String getInputText() {
+      return inputText;
+    }
+
+    public void setInputText(String inputText) {
+      this.inputText = inputText;
+    }
+  }
+
+  /** Claude message format for Bedrock. */
+  public static class ClaudeMessage {
+    private String role;
+    private String content;
+
+    public ClaudeMessage() {}
+
+    public ClaudeMessage(String role, String content) {
+      this.role = role;
+      this.content = content;
+    }
+
+    public static ClaudeMessage user(String content) {
+      return new ClaudeMessage("user", content);
+    }
+
+    public static ClaudeMessage assistant(String content) {
+      return new ClaudeMessage("assistant", content);
+    }
+
+    public String getRole() {
+      return role;
+    }
+
+    public void setRole(String role) {
+      this.role = role;
+    }
+
+    public String getContent() {
+      return content;
+    }
+
+    public void setContent(String content) {
+      this.content = content;
+    }
+  }
+
+  /** Bedrock InvokeModel response. */
+  public static class BedrockInvokeResponse {
+    private byte[] body;
+    private String contentType;
+
+    // Parsed response fields
+    private String responseText;
+    private int inputTokens;
+    private int outputTokens;
+
+    public String getSummary() {
+      if (responseText == null || responseText.isEmpty()) {
+        return "";
+      }
+      return responseText.length() > 100 ? responseText.substring(0, 100) + "..." : responseText;
+    }
+
+    public byte[] getBody() {
+      return body;
+    }
+
+    public void setBody(byte[] body) {
+      this.body = body;
+    }
+
+    public String getContentType() {
+      return contentType;
+    }
+
+    public void setContentType(String contentType) {
+      this.contentType = contentType;
+    }
+
+    public String getResponseText() {
+      return responseText;
+    }
+
+    public void setResponseText(String responseText) {
+      this.responseText = responseText;
+    }
+
+    public int getInputTokens() {
+      return inputTokens;
+    }
+
+    public void setInputTokens(int inputTokens) {
+      this.inputTokens = inputTokens;
+    }
+
+    public int getOutputTokens() {
+      return outputTokens;
+    }
+
+    public void setOutputTokens(int outputTokens) {
+      this.outputTokens = outputTokens;
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionRequest.java b/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionRequest.java
index 3724552..f883130 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionRequest.java
@@ -11,156 +11,155 @@
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Represents a chat completion request for OpenAI-compatible APIs.
- */
+/** Represents a chat completion request for OpenAI-compatible APIs. */
 public final class ChatCompletionRequest {
-    private final String model;
-    private final List messages;
-    private final Double temperature;
-    private final Integer maxTokens;
-    private final Double topP;
-    private final Integer n;
-    private final Boolean stream;
-    private final List stop;
-
-    private ChatCompletionRequest(Builder builder) {
-        this.model = Objects.requireNonNull(builder.model, "model must not be null");
-        this.messages = Collections.unmodifiableList(new ArrayList<>(builder.messages));
-        this.temperature = builder.temperature;
-        this.maxTokens = builder.maxTokens;
-        this.topP = builder.topP;
-        this.n = builder.n;
-        this.stream = builder.stream;
-        this.stop = builder.stop != null ? Collections.unmodifiableList(new ArrayList<>(builder.stop)) : null;
+  private final String model;
+  private final List messages;
+  private final Double temperature;
+  private final Integer maxTokens;
+  private final Double topP;
+  private final Integer n;
+  private final Boolean stream;
+  private final List stop;
+
+  private ChatCompletionRequest(Builder builder) {
+    this.model = Objects.requireNonNull(builder.model, "model must not be null");
+    this.messages = Collections.unmodifiableList(new ArrayList<>(builder.messages));
+    this.temperature = builder.temperature;
+    this.maxTokens = builder.maxTokens;
+    this.topP = builder.topP;
+    this.n = builder.n;
+    this.stream = builder.stream;
+    this.stop =
+        builder.stop != null ? Collections.unmodifiableList(new ArrayList<>(builder.stop)) : null;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getModel() {
+    return model;
+  }
+
+  public List getMessages() {
+    return messages;
+  }
+
+  public Double getTemperature() {
+    return temperature;
+  }
+
+  public Integer getMaxTokens() {
+    return maxTokens;
+  }
+
+  public Double getTopP() {
+    return topP;
+  }
+
+  public Integer getN() {
+    return n;
+  }
+
+  public Boolean getStream() {
+    return stream;
+  }
+
+  public List getStop() {
+    return stop;
+  }
+
+  /**
+   * Extracts the combined prompt from all messages.
+   *
+   * @return concatenated content of all messages
+   */
+  public String extractPrompt() {
+    StringBuilder sb = new StringBuilder();
+    for (ChatMessage msg : messages) {
+      if (msg.getContent() != null && !msg.getContent().isEmpty()) {
+        if (sb.length() > 0) {
+          sb.append(" ");
+        }
+        sb.append(msg.getContent());
+      }
     }
-
-    public static Builder builder() {
-        return new Builder();
+    return sb.toString();
+  }
+
+  public static final class Builder {
+    private String model;
+    private final List messages = new ArrayList<>();
+    private Double temperature;
+    private Integer maxTokens;
+    private Double topP;
+    private Integer n;
+    private Boolean stream;
+    private List stop;
+
+    private Builder() {}
+
+    public Builder model(String model) {
+      this.model = model;
+      return this;
     }
 
-    public String getModel() {
-        return model;
+    public Builder messages(List messages) {
+      this.messages.clear();
+      if (messages != null) {
+        this.messages.addAll(messages);
+      }
+      return this;
     }
 
-    public List getMessages() {
-        return messages;
+    public Builder addMessage(ChatMessage message) {
+      this.messages.add(message);
+      return this;
     }
 
-    public Double getTemperature() {
-        return temperature;
+    public Builder addUserMessage(String content) {
+      this.messages.add(ChatMessage.user(content));
+      return this;
     }
 
-    public Integer getMaxTokens() {
-        return maxTokens;
+    public Builder addSystemMessage(String content) {
+      this.messages.add(ChatMessage.system(content));
+      return this;
     }
 
-    public Double getTopP() {
-        return topP;
+    public Builder temperature(Double temperature) {
+      this.temperature = temperature;
+      return this;
     }
 
-    public Integer getN() {
-        return n;
+    public Builder maxTokens(Integer maxTokens) {
+      this.maxTokens = maxTokens;
+      return this;
     }
 
-    public Boolean getStream() {
-        return stream;
+    public Builder topP(Double topP) {
+      this.topP = topP;
+      return this;
     }
 
-    public List getStop() {
-        return stop;
+    public Builder n(Integer n) {
+      this.n = n;
+      return this;
     }
 
-    /**
-     * Extracts the combined prompt from all messages.
-     *
-     * @return concatenated content of all messages
-     */
-    public String extractPrompt() {
-        StringBuilder sb = new StringBuilder();
-        for (ChatMessage msg : messages) {
-            if (msg.getContent() != null && !msg.getContent().isEmpty()) {
-                if (sb.length() > 0) {
-                    sb.append(" ");
-                }
-                sb.append(msg.getContent());
-            }
-        }
-        return sb.toString();
+    public Builder stream(Boolean stream) {
+      this.stream = stream;
+      return this;
     }
 
-    public static final class Builder {
-        private String model;
-        private final List messages = new ArrayList<>();
-        private Double temperature;
-        private Integer maxTokens;
-        private Double topP;
-        private Integer n;
-        private Boolean stream;
-        private List stop;
-
-        private Builder() {}
-
-        public Builder model(String model) {
-            this.model = model;
-            return this;
-        }
-
-        public Builder messages(List messages) {
-            this.messages.clear();
-            if (messages != null) {
-                this.messages.addAll(messages);
-            }
-            return this;
-        }
-
-        public Builder addMessage(ChatMessage message) {
-            this.messages.add(message);
-            return this;
-        }
-
-        public Builder addUserMessage(String content) {
-            this.messages.add(ChatMessage.user(content));
-            return this;
-        }
-
-        public Builder addSystemMessage(String content) {
-            this.messages.add(ChatMessage.system(content));
-            return this;
-        }
-
-        public Builder temperature(Double temperature) {
-            this.temperature = temperature;
-            return this;
-        }
-
-        public Builder maxTokens(Integer maxTokens) {
-            this.maxTokens = maxTokens;
-            return this;
-        }
-
-        public Builder topP(Double topP) {
-            this.topP = topP;
-            return this;
-        }
-
-        public Builder n(Integer n) {
-            this.n = n;
-            return this;
-        }
-
-        public Builder stream(Boolean stream) {
-            this.stream = stream;
-            return this;
-        }
-
-        public Builder stop(List stop) {
-            this.stop = stop;
-            return this;
-        }
+    public Builder stop(List stop) {
+      this.stop = stop;
+      return this;
+    }
 
-        public ChatCompletionRequest build() {
-            return new ChatCompletionRequest(this);
-        }
+    public ChatCompletionRequest build() {
+      return new ChatCompletionRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionResponse.java b/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionResponse.java
index b71dccf..f8bab5c 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/ChatCompletionResponse.java
@@ -9,184 +9,178 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
-import java.util.Objects;
 
-/**
- * Represents a chat completion response from OpenAI-compatible APIs.
- */
+/** Represents a chat completion response from OpenAI-compatible APIs. */
 public final class ChatCompletionResponse {
-    private final String id;
-    private final String object;
-    private final long created;
-    private final String model;
-    private final List choices;
-    private final Usage usage;
-
-    private ChatCompletionResponse(Builder builder) {
-        this.id = builder.id;
-        this.object = builder.object;
-        this.created = builder.created;
-        this.model = builder.model;
-        this.choices = builder.choices != null ?
-            Collections.unmodifiableList(new ArrayList<>(builder.choices)) :
-            Collections.emptyList();
-        this.usage = builder.usage;
+  private final String id;
+  private final String object;
+  private final long created;
+  private final String model;
+  private final List choices;
+  private final Usage usage;
+
+  private ChatCompletionResponse(Builder builder) {
+    this.id = builder.id;
+    this.object = builder.object;
+    this.created = builder.created;
+    this.model = builder.model;
+    this.choices =
+        builder.choices != null
+            ? Collections.unmodifiableList(new ArrayList<>(builder.choices))
+            : Collections.emptyList();
+    this.usage = builder.usage;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public String getObject() {
+    return object;
+  }
+
+  public long getCreated() {
+    return created;
+  }
+
+  public String getModel() {
+    return model;
+  }
+
+  public List getChoices() {
+    return choices;
+  }
+
+  public Usage getUsage() {
+    return usage;
+  }
+
+  /**
+   * Gets the content of the first choice's message.
+   *
+   * @return the content or empty string if not available
+   */
+  public String getContent() {
+    if (choices.isEmpty()) {
+      return "";
     }
-
-    public static Builder builder() {
-        return new Builder();
+    ChatMessage msg = choices.get(0).getMessage();
+    return msg != null ? msg.getContent() : "";
+  }
+
+  /**
+   * Gets a summary of the response (first 100 characters).
+   *
+   * @return the summary
+   */
+  public String getSummary() {
+    String content = getContent();
+    if (content.length() > 100) {
+      return content.substring(0, 100);
     }
-
-    public String getId() {
-        return id;
+    return content;
+  }
+
+  /** Represents a choice in the completion response. */
+  public static final class Choice {
+    private final int index;
+    private final ChatMessage message;
+    private final String finishReason;
+
+    public Choice(int index, ChatMessage message, String finishReason) {
+      this.index = index;
+      this.message = message;
+      this.finishReason = finishReason;
     }
 
-    public String getObject() {
-        return object;
+    public int getIndex() {
+      return index;
     }
 
-    public long getCreated() {
-        return created;
+    public ChatMessage getMessage() {
+      return message;
     }
 
-    public String getModel() {
-        return model;
+    public String getFinishReason() {
+      return finishReason;
     }
-
-    public List getChoices() {
-        return choices;
+  }
+
+  /** Represents token usage information. */
+  public static final class Usage {
+    private final int promptTokens;
+    private final int completionTokens;
+    private final int totalTokens;
+
+    public Usage(int promptTokens, int completionTokens, int totalTokens) {
+      this.promptTokens = promptTokens;
+      this.completionTokens = completionTokens;
+      this.totalTokens = totalTokens;
     }
 
-    public Usage getUsage() {
-        return usage;
+    public static Usage of(int promptTokens, int completionTokens) {
+      return new Usage(promptTokens, completionTokens, promptTokens + completionTokens);
     }
 
-    /**
-     * Gets the content of the first choice's message.
-     *
-     * @return the content or empty string if not available
-     */
-    public String getContent() {
-        if (choices.isEmpty()) {
-            return "";
-        }
-        ChatMessage msg = choices.get(0).getMessage();
-        return msg != null ? msg.getContent() : "";
+    public int getPromptTokens() {
+      return promptTokens;
     }
 
-    /**
-     * Gets a summary of the response (first 100 characters).
-     *
-     * @return the summary
-     */
-    public String getSummary() {
-        String content = getContent();
-        if (content.length() > 100) {
-            return content.substring(0, 100);
-        }
-        return content;
+    public int getCompletionTokens() {
+      return completionTokens;
     }
 
-    /**
-     * Represents a choice in the completion response.
-     */
-    public static final class Choice {
-        private final int index;
-        private final ChatMessage message;
-        private final String finishReason;
-
-        public Choice(int index, ChatMessage message, String finishReason) {
-            this.index = index;
-            this.message = message;
-            this.finishReason = finishReason;
-        }
+    public int getTotalTokens() {
+      return totalTokens;
+    }
+  }
 
-        public int getIndex() {
-            return index;
-        }
+  public static final class Builder {
+    private String id;
+    private String object = "chat.completion";
+    private long created;
+    private String model;
+    private List choices;
+    private Usage usage;
 
-        public ChatMessage getMessage() {
-            return message;
-        }
+    private Builder() {}
 
-        public String getFinishReason() {
-            return finishReason;
-        }
+    public Builder id(String id) {
+      this.id = id;
+      return this;
     }
 
-    /**
-     * Represents token usage information.
-     */
-    public static final class Usage {
-        private final int promptTokens;
-        private final int completionTokens;
-        private final int totalTokens;
-
-        public Usage(int promptTokens, int completionTokens, int totalTokens) {
-            this.promptTokens = promptTokens;
-            this.completionTokens = completionTokens;
-            this.totalTokens = totalTokens;
-        }
+    public Builder object(String object) {
+      this.object = object;
+      return this;
+    }
 
-        public static Usage of(int promptTokens, int completionTokens) {
-            return new Usage(promptTokens, completionTokens, promptTokens + completionTokens);
-        }
+    public Builder created(long created) {
+      this.created = created;
+      return this;
+    }
 
-        public int getPromptTokens() {
-            return promptTokens;
-        }
+    public Builder model(String model) {
+      this.model = model;
+      return this;
+    }
 
-        public int getCompletionTokens() {
-            return completionTokens;
-        }
+    public Builder choices(List choices) {
+      this.choices = choices;
+      return this;
+    }
 
-        public int getTotalTokens() {
-            return totalTokens;
-        }
+    public Builder usage(Usage usage) {
+      this.usage = usage;
+      return this;
     }
-
-    public static final class Builder {
-        private String id;
-        private String object = "chat.completion";
-        private long created;
-        private String model;
-        private List choices;
-        private Usage usage;
-
-        private Builder() {}
-
-        public Builder id(String id) {
-            this.id = id;
-            return this;
-        }
-
-        public Builder object(String object) {
-            this.object = object;
-            return this;
-        }
-
-        public Builder created(long created) {
-            this.created = created;
-            return this;
-        }
-
-        public Builder model(String model) {
-            this.model = model;
-            return this;
-        }
-
-        public Builder choices(List choices) {
-            this.choices = choices;
-            return this;
-        }
-
-        public Builder usage(Usage usage) {
-            this.usage = usage;
-            return this;
-        }
-
-        public ChatCompletionResponse build() {
-            return new ChatCompletionResponse(this);
-        }
+
+    public ChatCompletionResponse build() {
+      return new ChatCompletionResponse(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/ChatMessage.java b/src/main/java/com/getaxonflow/sdk/interceptors/ChatMessage.java
index d0edbca..917d76f 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/ChatMessage.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/ChatMessage.java
@@ -9,83 +9,86 @@
 import java.util.Objects;
 
 /**
- * Represents a chat message for LLM calls.
- * Works with both OpenAI and Anthropic-style message formats.
+ * Represents a chat message for LLM calls. Works with both OpenAI and Anthropic-style message
+ * formats.
  */
 public final class ChatMessage {
-    private final String role;
-    private final String content;
+  private final String role;
+  private final String content;
 
-    private ChatMessage(String role, String content) {
-        this.role = Objects.requireNonNull(role, "role must not be null");
-        this.content = Objects.requireNonNull(content, "content must not be null");
-    }
+  private ChatMessage(String role, String content) {
+    this.role = Objects.requireNonNull(role, "role must not be null");
+    this.content = Objects.requireNonNull(content, "content must not be null");
+  }
 
-    /**
-     * Creates a new chat message.
-     *
-     * @param role    the role (e.g., "user", "assistant", "system")
-     * @param content the message content
-     * @return a new ChatMessage
-     */
-    public static ChatMessage of(String role, String content) {
-        return new ChatMessage(role, content);
-    }
+  /**
+   * Creates a new chat message.
+   *
+   * @param role the role (e.g., "user", "assistant", "system")
+   * @param content the message content
+   * @return a new ChatMessage
+   */
+  public static ChatMessage of(String role, String content) {
+    return new ChatMessage(role, content);
+  }
 
-    /**
-     * Creates a user message.
-     *
-     * @param content the message content
-     * @return a new ChatMessage with role "user"
-     */
-    public static ChatMessage user(String content) {
-        return new ChatMessage("user", content);
-    }
+  /**
+   * Creates a user message.
+   *
+   * @param content the message content
+   * @return a new ChatMessage with role "user"
+   */
+  public static ChatMessage user(String content) {
+    return new ChatMessage("user", content);
+  }
 
-    /**
-     * Creates an assistant message.
-     *
-     * @param content the message content
-     * @return a new ChatMessage with role "assistant"
-     */
-    public static ChatMessage assistant(String content) {
-        return new ChatMessage("assistant", content);
-    }
+  /**
+   * Creates an assistant message.
+   *
+   * @param content the message content
+   * @return a new ChatMessage with role "assistant"
+   */
+  public static ChatMessage assistant(String content) {
+    return new ChatMessage("assistant", content);
+  }
 
-    /**
-     * Creates a system message.
-     *
-     * @param content the message content
-     * @return a new ChatMessage with role "system"
-     */
-    public static ChatMessage system(String content) {
-        return new ChatMessage("system", content);
-    }
+  /**
+   * Creates a system message.
+   *
+   * @param content the message content
+   * @return a new ChatMessage with role "system"
+   */
+  public static ChatMessage system(String content) {
+    return new ChatMessage("system", content);
+  }
 
-    public String getRole() {
-        return role;
-    }
+  public String getRole() {
+    return role;
+  }
 
-    public String getContent() {
-        return content;
-    }
+  public String getContent() {
+    return content;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ChatMessage that = (ChatMessage) o;
-        return Objects.equals(role, that.role) && Objects.equals(content, that.content);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ChatMessage that = (ChatMessage) o;
+    return Objects.equals(role, that.role) && Objects.equals(content, that.content);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(role, content);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(role, content);
+  }
 
-    @Override
-    public String toString() {
-        return "ChatMessage{role='" + role + "', content='" +
-               (content.length() > 50 ? content.substring(0, 50) + "..." : content) + "'}";
-    }
+  @Override
+  public String toString() {
+    return "ChatMessage{role='"
+        + role
+        + "', content='"
+        + (content.length() > 50 ? content.substring(0, 50) + "..." : content)
+        + "'}";
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/GeminiInterceptor.java b/src/main/java/com/getaxonflow/sdk/interceptors/GeminiInterceptor.java
index ec969d9..e0ebfa7 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/GeminiInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/GeminiInterceptor.java
@@ -2,12 +2,11 @@
 
 import com.getaxonflow.sdk.AxonFlow;
 import com.getaxonflow.sdk.exceptions.PolicyViolationException;
+import com.getaxonflow.sdk.types.AuditOptions;
 import com.getaxonflow.sdk.types.ClientRequest;
 import com.getaxonflow.sdk.types.ClientResponse;
 import com.getaxonflow.sdk.types.RequestType;
 import com.getaxonflow.sdk.types.TokenUsage;
-import com.getaxonflow.sdk.types.AuditOptions;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -18,10 +17,11 @@
 /**
  * Interceptor for Google Gemini API calls with automatic governance.
  *
- * 

Wraps Gemini GenerativeModel calls with AxonFlow policy checking and audit logging. - * Works with the google-cloud-vertexai SDK or any compatible client. + *

Wraps Gemini GenerativeModel calls with AxonFlow policy checking and audit logging. Works with + * the google-cloud-vertexai SDK or any compatible client. * *

Example Usage

+ * *
{@code
  * AxonFlow axonflow = new AxonFlow(axonflowConfig);
  * GeminiInterceptor interceptor = new GeminiInterceptor(axonflow, "user-123");
@@ -37,473 +37,453 @@
  */
 public class GeminiInterceptor {
 
-    private final AxonFlow axonflow;
-    private final String userToken;
-
-    /**
-     * Creates a new GeminiInterceptor.
-     *
-     * @param axonflow the AxonFlow client for governance
-     * @param userToken the user token for policy evaluation
-     */
-    public GeminiInterceptor(AxonFlow axonflow, String userToken) {
-        if (axonflow == null) {
-            throw new IllegalArgumentException("axonflow cannot be null");
-        }
-        if (userToken == null || userToken.isEmpty()) {
-            throw new IllegalArgumentException("userToken cannot be null or empty");
-        }
-        this.axonflow = axonflow;
-        this.userToken = userToken;
-    }
-
-    /**
-     * Wraps a synchronous Gemini generateContent call with governance.
-     *
-     * @param geminiCall the original Gemini call function
-     * @return a wrapped function that applies governance
-     */
-    public Function wrap(
-            Function geminiCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            // Build context for policy evaluation
-            Map context = new HashMap<>();
-            context.put("provider", "gemini");
-            context.put("model", request.getModel());
-            if (request.getGenerationConfig() != null) {
-                context.put("temperature", request.getGenerationConfig().getTemperature());
-                context.put("maxOutputTokens", request.getGenerationConfig().getMaxOutputTokens());
-            }
-
-            // Pre-check with AxonFlow
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
-
-            // Execute the actual LLM call
-            long startTime = System.currentTimeMillis();
-            GeminiResponse result = geminiCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
-
-            // Audit the call
-            if (axonResponse.getPlanId() != null) {
-                auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-            }
-
-            return result;
-        };
-    }
-
-    /**
-     * Wraps an asynchronous Gemini generateContent call with governance.
-     *
-     * @param geminiCall the original async Gemini call function
-     * @return a wrapped function that applies governance
-     */
-    public Function> wrapAsync(
-            Function> geminiCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            Map context = new HashMap<>();
-            context.put("provider", "gemini");
-            context.put("model", request.getModel());
-
-            // Pre-check (synchronous for now)
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                return CompletableFuture.failedFuture(
-                    new PolicyViolationException(axonResponse.getBlockReason())
-                );
-            }
-
-            long startTime = System.currentTimeMillis();
-            String planId = axonResponse.getPlanId();
-            String model = request.getModel();
-
-            return geminiCall.apply(request)
-                .thenApply(result -> {
-                    long latencyMs = System.currentTimeMillis() - startTime;
-                    if (planId != null) {
-                        auditCall(planId, result, model, latencyMs);
-                    }
-                    return result;
-                });
-        };
-    }
-
-    private void auditCall(String contextId, GeminiResponse response, String model, long latencyMs) {
-        try {
-            String summary = response != null ? response.getSummary() : "";
-
-            int promptTokens = response != null ? response.getPromptTokenCount() : 0;
-            int completionTokens = response != null ? response.getCandidatesTokenCount() : 0;
-            TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
-
-            AuditOptions auditOptions = AuditOptions.builder()
-                .contextId(contextId)
-                .responseSummary(summary)
-                .provider("gemini")
-                .model(model)
-                .tokenUsage(usage)
-                .latencyMs(latencyMs)
-                .build();
-
-            axonflow.auditLLMCall(auditOptions);
-        } catch (Exception e) {
-            // Log but don't fail the request
-        }
+  private final AxonFlow axonflow;
+  private final String userToken;
+
+  /**
+   * Creates a new GeminiInterceptor.
+   *
+   * @param axonflow the AxonFlow client for governance
+   * @param userToken the user token for policy evaluation
+   */
+  public GeminiInterceptor(AxonFlow axonflow, String userToken) {
+    if (axonflow == null) {
+      throw new IllegalArgumentException("axonflow cannot be null");
     }
+    if (userToken == null || userToken.isEmpty()) {
+      throw new IllegalArgumentException("userToken cannot be null or empty");
+    }
+    this.axonflow = axonflow;
+    this.userToken = userToken;
+  }
+
+  /**
+   * Wraps a synchronous Gemini generateContent call with governance.
+   *
+   * @param geminiCall the original Gemini call function
+   * @return a wrapped function that applies governance
+   */
+  public Function wrap(
+      Function geminiCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      // Build context for policy evaluation
+      Map context = new HashMap<>();
+      context.put("provider", "gemini");
+      context.put("model", request.getModel());
+      if (request.getGenerationConfig() != null) {
+        context.put("temperature", request.getGenerationConfig().getTemperature());
+        context.put("maxOutputTokens", request.getGenerationConfig().getMaxOutputTokens());
+      }
+
+      // Pre-check with AxonFlow
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
+
+      // Execute the actual LLM call
+      long startTime = System.currentTimeMillis();
+      GeminiResponse result = geminiCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
+
+      // Audit the call
+      if (axonResponse.getPlanId() != null) {
+        auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+      }
+
+      return result;
+    };
+  }
+
+  /**
+   * Wraps an asynchronous Gemini generateContent call with governance.
+   *
+   * @param geminiCall the original async Gemini call function
+   * @return a wrapped function that applies governance
+   */
+  public Function> wrapAsync(
+      Function> geminiCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      Map context = new HashMap<>();
+      context.put("provider", "gemini");
+      context.put("model", request.getModel());
+
+      // Pre-check (synchronous for now)
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        return CompletableFuture.failedFuture(
+            new PolicyViolationException(axonResponse.getBlockReason()));
+      }
+
+      long startTime = System.currentTimeMillis();
+      String planId = axonResponse.getPlanId();
+      String model = request.getModel();
+
+      return geminiCall
+          .apply(request)
+          .thenApply(
+              result -> {
+                long latencyMs = System.currentTimeMillis() - startTime;
+                if (planId != null) {
+                  auditCall(planId, result, model, latencyMs);
+                }
+                return result;
+              });
+    };
+  }
+
+  private void auditCall(String contextId, GeminiResponse response, String model, long latencyMs) {
+    try {
+      String summary = response != null ? response.getSummary() : "";
+
+      int promptTokens = response != null ? response.getPromptTokenCount() : 0;
+      int completionTokens = response != null ? response.getCandidatesTokenCount() : 0;
+      TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
+
+      AuditOptions auditOptions =
+          AuditOptions.builder()
+              .contextId(contextId)
+              .responseSummary(summary)
+              .provider("gemini")
+              .model(model)
+              .tokenUsage(usage)
+              .latencyMs(latencyMs)
+              .build();
+
+      axonflow.auditLLMCall(auditOptions);
+    } catch (Exception e) {
+      // Log but don't fail the request
+    }
+  }
 
-    // ==================== Gemini Request/Response Types ====================
+  // ==================== Gemini Request/Response Types ====================
 
-    /**
-     * Represents a Gemini GenerateContent request.
-     */
-    public static class GeminiRequest {
-        private String model;
-        private List contents;
-        private GenerationConfig generationConfig;
+  /** Represents a Gemini GenerateContent request. */
+  public static class GeminiRequest {
+    private String model;
+    private List contents;
+    private GenerationConfig generationConfig;
 
-        public GeminiRequest() {
-            this.contents = new ArrayList<>();
-        }
+    public GeminiRequest() {
+      this.contents = new ArrayList<>();
+    }
 
-        public static GeminiRequest create(String model, String prompt) {
-            GeminiRequest request = new GeminiRequest();
-            request.model = model;
-            request.contents.add(Content.text(prompt));
-            return request;
-        }
+    public static GeminiRequest create(String model, String prompt) {
+      GeminiRequest request = new GeminiRequest();
+      request.model = model;
+      request.contents.add(Content.text(prompt));
+      return request;
+    }
 
-        public String getModel() {
-            return model;
-        }
+    public String getModel() {
+      return model;
+    }
 
-        public void setModel(String model) {
-            this.model = model;
-        }
+    public void setModel(String model) {
+      this.model = model;
+    }
 
-        public List getContents() {
-            return contents;
-        }
+    public List getContents() {
+      return contents;
+    }
 
-        public void setContents(List contents) {
-            this.contents = contents;
-        }
+    public void setContents(List contents) {
+      this.contents = contents;
+    }
 
-        public GenerationConfig getGenerationConfig() {
-            return generationConfig;
-        }
+    public GenerationConfig getGenerationConfig() {
+      return generationConfig;
+    }
 
-        public void setGenerationConfig(GenerationConfig generationConfig) {
-            this.generationConfig = generationConfig;
-        }
+    public void setGenerationConfig(GenerationConfig generationConfig) {
+      this.generationConfig = generationConfig;
+    }
 
-        /**
-         * Extracts the prompt text from all content parts.
-         */
-        public String extractPrompt() {
-            if (contents == null || contents.isEmpty()) {
-                return "";
-            }
-            StringBuilder sb = new StringBuilder();
-            for (Content content : contents) {
-                if (content.getParts() != null) {
-                    for (Part part : content.getParts()) {
-                        if (part.getText() != null) {
-                            if (sb.length() > 0) sb.append(" ");
-                            sb.append(part.getText());
-                        }
-                    }
-                }
+    /** Extracts the prompt text from all content parts. */
+    public String extractPrompt() {
+      if (contents == null || contents.isEmpty()) {
+        return "";
+      }
+      StringBuilder sb = new StringBuilder();
+      for (Content content : contents) {
+        if (content.getParts() != null) {
+          for (Part part : content.getParts()) {
+            if (part.getText() != null) {
+              if (sb.length() > 0) sb.append(" ");
+              sb.append(part.getText());
             }
-            return sb.toString();
+          }
         }
+      }
+      return sb.toString();
     }
+  }
 
-    /**
-     * Represents content in a Gemini request.
-     */
-    public static class Content {
-        private String role;
-        private List parts;
-
-        public Content() {
-            this.parts = new ArrayList<>();
-        }
-
-        public static Content text(String text) {
-            Content content = new Content();
-            content.role = "user";
-            content.parts.add(Part.text(text));
-            return content;
-        }
+  /** Represents content in a Gemini request. */
+  public static class Content {
+    private String role;
+    private List parts;
 
-        public String getRole() {
-            return role;
-        }
+    public Content() {
+      this.parts = new ArrayList<>();
+    }
 
-        public void setRole(String role) {
-            this.role = role;
-        }
+    public static Content text(String text) {
+      Content content = new Content();
+      content.role = "user";
+      content.parts.add(Part.text(text));
+      return content;
+    }
 
-        public List getParts() {
-            return parts;
-        }
+    public String getRole() {
+      return role;
+    }
 
-        public void setParts(List parts) {
-            this.parts = parts;
-        }
+    public void setRole(String role) {
+      this.role = role;
     }
 
-    /**
-     * Represents a part of content (text or inline data).
-     */
-    public static class Part {
-        private String text;
-        private InlineData inlineData;
+    public List getParts() {
+      return parts;
+    }
 
-        public static Part text(String text) {
-            Part part = new Part();
-            part.text = text;
-            return part;
-        }
+    public void setParts(List parts) {
+      this.parts = parts;
+    }
+  }
 
-        public String getText() {
-            return text;
-        }
+  /** Represents a part of content (text or inline data). */
+  public static class Part {
+    private String text;
+    private InlineData inlineData;
 
-        public void setText(String text) {
-            this.text = text;
-        }
+    public static Part text(String text) {
+      Part part = new Part();
+      part.text = text;
+      return part;
+    }
 
-        public InlineData getInlineData() {
-            return inlineData;
-        }
+    public String getText() {
+      return text;
+    }
 
-        public void setInlineData(InlineData inlineData) {
-            this.inlineData = inlineData;
-        }
+    public void setText(String text) {
+      this.text = text;
     }
 
-    /**
-     * Represents inline binary data (images, etc.).
-     */
-    public static class InlineData {
-        private String mimeType;
-        private String data;
+    public InlineData getInlineData() {
+      return inlineData;
+    }
 
-        public String getMimeType() {
-            return mimeType;
-        }
+    public void setInlineData(InlineData inlineData) {
+      this.inlineData = inlineData;
+    }
+  }
 
-        public void setMimeType(String mimeType) {
-            this.mimeType = mimeType;
-        }
+  /** Represents inline binary data (images, etc.). */
+  public static class InlineData {
+    private String mimeType;
+    private String data;
 
-        public String getData() {
-            return data;
-        }
+    public String getMimeType() {
+      return mimeType;
+    }
 
-        public void setData(String data) {
-            this.data = data;
-        }
+    public void setMimeType(String mimeType) {
+      this.mimeType = mimeType;
     }
 
-    /**
-     * Generation configuration parameters.
-     */
-    public static class GenerationConfig {
-        private Double temperature;
-        private Double topP;
-        private Integer topK;
-        private Integer maxOutputTokens;
-        private List stopSequences;
+    public String getData() {
+      return data;
+    }
 
-        public Double getTemperature() {
-            return temperature;
-        }
+    public void setData(String data) {
+      this.data = data;
+    }
+  }
+
+  /** Generation configuration parameters. */
+  public static class GenerationConfig {
+    private Double temperature;
+    private Double topP;
+    private Integer topK;
+    private Integer maxOutputTokens;
+    private List stopSequences;
+
+    public Double getTemperature() {
+      return temperature;
+    }
 
-        public void setTemperature(Double temperature) {
-            this.temperature = temperature;
-        }
+    public void setTemperature(Double temperature) {
+      this.temperature = temperature;
+    }
 
-        public Double getTopP() {
-            return topP;
-        }
+    public Double getTopP() {
+      return topP;
+    }
 
-        public void setTopP(Double topP) {
-            this.topP = topP;
-        }
+    public void setTopP(Double topP) {
+      this.topP = topP;
+    }
 
-        public Integer getTopK() {
-            return topK;
-        }
+    public Integer getTopK() {
+      return topK;
+    }
 
-        public void setTopK(Integer topK) {
-            this.topK = topK;
-        }
+    public void setTopK(Integer topK) {
+      this.topK = topK;
+    }
 
-        public Integer getMaxOutputTokens() {
-            return maxOutputTokens;
-        }
+    public Integer getMaxOutputTokens() {
+      return maxOutputTokens;
+    }
 
-        public void setMaxOutputTokens(Integer maxOutputTokens) {
-            this.maxOutputTokens = maxOutputTokens;
-        }
+    public void setMaxOutputTokens(Integer maxOutputTokens) {
+      this.maxOutputTokens = maxOutputTokens;
+    }
 
-        public List getStopSequences() {
-            return stopSequences;
-        }
+    public List getStopSequences() {
+      return stopSequences;
+    }
 
-        public void setStopSequences(List stopSequences) {
-            this.stopSequences = stopSequences;
-        }
+    public void setStopSequences(List stopSequences) {
+      this.stopSequences = stopSequences;
     }
+  }
 
-    /**
-     * Represents a Gemini GenerateContent response.
-     */
-    public static class GeminiResponse {
-        private List candidates;
-        private UsageMetadata usageMetadata;
+  /** Represents a Gemini GenerateContent response. */
+  public static class GeminiResponse {
+    private List candidates;
+    private UsageMetadata usageMetadata;
 
-        public List getCandidates() {
-            return candidates;
-        }
+    public List getCandidates() {
+      return candidates;
+    }
 
-        public void setCandidates(List candidates) {
-            this.candidates = candidates;
-        }
+    public void setCandidates(List candidates) {
+      this.candidates = candidates;
+    }
 
-        public UsageMetadata getUsageMetadata() {
-            return usageMetadata;
-        }
+    public UsageMetadata getUsageMetadata() {
+      return usageMetadata;
+    }
 
-        public void setUsageMetadata(UsageMetadata usageMetadata) {
-            this.usageMetadata = usageMetadata;
-        }
+    public void setUsageMetadata(UsageMetadata usageMetadata) {
+      this.usageMetadata = usageMetadata;
+    }
 
-        /**
-         * Gets a summary of the response (first 100 characters of first candidate).
-         */
-        public String getSummary() {
-            String text = getText();
-            if (text == null || text.isEmpty()) {
-                return "";
-            }
-            return text.length() > 100 ? text.substring(0, 100) + "..." : text;
-        }
+    /** Gets a summary of the response (first 100 characters of first candidate). */
+    public String getSummary() {
+      String text = getText();
+      if (text == null || text.isEmpty()) {
+        return "";
+      }
+      return text.length() > 100 ? text.substring(0, 100) + "..." : text;
+    }
 
-        /**
-         * Gets the text content from the first candidate.
-         */
-        public String getText() {
-            if (candidates == null || candidates.isEmpty()) {
-                return "";
-            }
-            Candidate first = candidates.get(0);
-            if (first.getContent() == null || first.getContent().getParts() == null) {
-                return "";
-            }
-            StringBuilder sb = new StringBuilder();
-            for (Part part : first.getContent().getParts()) {
-                if (part.getText() != null) {
-                    sb.append(part.getText());
-                }
-            }
-            return sb.toString();
-        }
+    /** Gets the text content from the first candidate. */
+    public String getText() {
+      if (candidates == null || candidates.isEmpty()) {
+        return "";
+      }
+      Candidate first = candidates.get(0);
+      if (first.getContent() == null || first.getContent().getParts() == null) {
+        return "";
+      }
+      StringBuilder sb = new StringBuilder();
+      for (Part part : first.getContent().getParts()) {
+        if (part.getText() != null) {
+          sb.append(part.getText());
+        }
+      }
+      return sb.toString();
+    }
 
-        public int getPromptTokenCount() {
-            return usageMetadata != null ? usageMetadata.getPromptTokenCount() : 0;
-        }
+    public int getPromptTokenCount() {
+      return usageMetadata != null ? usageMetadata.getPromptTokenCount() : 0;
+    }
 
-        public int getCandidatesTokenCount() {
-            return usageMetadata != null ? usageMetadata.getCandidatesTokenCount() : 0;
-        }
+    public int getCandidatesTokenCount() {
+      return usageMetadata != null ? usageMetadata.getCandidatesTokenCount() : 0;
+    }
 
-        public int getTotalTokenCount() {
-            return usageMetadata != null ? usageMetadata.getTotalTokenCount() : 0;
-        }
+    public int getTotalTokenCount() {
+      return usageMetadata != null ? usageMetadata.getTotalTokenCount() : 0;
     }
+  }
 
-    /**
-     * Represents a response candidate.
-     */
-    public static class Candidate {
-        private Content content;
-        private String finishReason;
+  /** Represents a response candidate. */
+  public static class Candidate {
+    private Content content;
+    private String finishReason;
 
-        public Content getContent() {
-            return content;
-        }
+    public Content getContent() {
+      return content;
+    }
 
-        public void setContent(Content content) {
-            this.content = content;
-        }
+    public void setContent(Content content) {
+      this.content = content;
+    }
 
-        public String getFinishReason() {
-            return finishReason;
-        }
+    public String getFinishReason() {
+      return finishReason;
+    }
 
-        public void setFinishReason(String finishReason) {
-            this.finishReason = finishReason;
-        }
+    public void setFinishReason(String finishReason) {
+      this.finishReason = finishReason;
     }
+  }
 
-    /**
-     * Token usage metadata.
-     */
-    public static class UsageMetadata {
-        private int promptTokenCount;
-        private int candidatesTokenCount;
-        private int totalTokenCount;
+  /** Token usage metadata. */
+  public static class UsageMetadata {
+    private int promptTokenCount;
+    private int candidatesTokenCount;
+    private int totalTokenCount;
 
-        public int getPromptTokenCount() {
-            return promptTokenCount;
-        }
+    public int getPromptTokenCount() {
+      return promptTokenCount;
+    }
 
-        public void setPromptTokenCount(int promptTokenCount) {
-            this.promptTokenCount = promptTokenCount;
-        }
+    public void setPromptTokenCount(int promptTokenCount) {
+      this.promptTokenCount = promptTokenCount;
+    }
 
-        public int getCandidatesTokenCount() {
-            return candidatesTokenCount;
-        }
+    public int getCandidatesTokenCount() {
+      return candidatesTokenCount;
+    }
 
-        public void setCandidatesTokenCount(int candidatesTokenCount) {
-            this.candidatesTokenCount = candidatesTokenCount;
-        }
+    public void setCandidatesTokenCount(int candidatesTokenCount) {
+      this.candidatesTokenCount = candidatesTokenCount;
+    }
 
-        public int getTotalTokenCount() {
-            return totalTokenCount;
-        }
+    public int getTotalTokenCount() {
+      return totalTokenCount;
+    }
 
-        public void setTotalTokenCount(int totalTokenCount) {
-            this.totalTokenCount = totalTokenCount;
-        }
+    public void setTotalTokenCount(int totalTokenCount) {
+      this.totalTokenCount = totalTokenCount;
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/OllamaInterceptor.java b/src/main/java/com/getaxonflow/sdk/interceptors/OllamaInterceptor.java
index aef39e8..0421507 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/OllamaInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/OllamaInterceptor.java
@@ -2,12 +2,11 @@
 
 import com.getaxonflow.sdk.AxonFlow;
 import com.getaxonflow.sdk.exceptions.PolicyViolationException;
+import com.getaxonflow.sdk.types.AuditOptions;
 import com.getaxonflow.sdk.types.ClientRequest;
 import com.getaxonflow.sdk.types.ClientResponse;
 import com.getaxonflow.sdk.types.RequestType;
 import com.getaxonflow.sdk.types.TokenUsage;
-import com.getaxonflow.sdk.types.AuditOptions;
-
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -19,10 +18,11 @@
 /**
  * Interceptor for Ollama API calls with automatic governance.
  *
- * 

Ollama is a local LLM server that runs on localhost:11434 by default. - * No authentication is required. + *

Ollama is a local LLM server that runs on localhost:11434 by default. No authentication is + * required. * *

Example Usage

+ * *
{@code
  * AxonFlow axonflow = new AxonFlow(config);
  * OllamaInterceptor interceptor = new OllamaInterceptor(axonflow, "user-123");
@@ -38,404 +38,616 @@
  */
 public class OllamaInterceptor {
 
-    private final AxonFlow axonflow;
-    private final String userToken;
-
-    /**
-     * Creates a new OllamaInterceptor.
-     *
-     * @param axonflow the AxonFlow client for governance
-     * @param userToken the user token for policy evaluation
-     */
-    public OllamaInterceptor(AxonFlow axonflow, String userToken) {
-        if (axonflow == null) {
-            throw new IllegalArgumentException("axonflow cannot be null");
-        }
-        if (userToken == null || userToken.isEmpty()) {
-            throw new IllegalArgumentException("userToken cannot be null or empty");
-        }
-        this.axonflow = axonflow;
-        this.userToken = userToken;
-    }
-
-    /**
-     * Wraps a synchronous Ollama chat call with governance.
-     *
-     * @param ollamaCall the original Ollama call function
-     * @return a wrapped function that applies governance
-     */
-    public Function wrapChat(
-            Function ollamaCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            Map context = new HashMap<>();
-            context.put("provider", "ollama");
-            context.put("model", request.getModel());
-
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
-
-            long startTime = System.currentTimeMillis();
-            OllamaChatResponse result = ollamaCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
-
-            if (axonResponse.getPlanId() != null) {
-                auditChatCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-            }
-
-            return result;
-        };
-    }
-
-    /**
-     * Wraps a synchronous Ollama generate call with governance.
-     *
-     * @param ollamaCall the original Ollama generate function
-     * @return a wrapped function that applies governance
-     */
-    public Function wrapGenerate(
-            Function ollamaCall) {
-
-        return request -> {
-            Map context = new HashMap<>();
-            context.put("provider", "ollama");
-            context.put("model", request.getModel());
-
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(request.getPrompt())
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
-
-            long startTime = System.currentTimeMillis();
-            OllamaGenerateResponse result = ollamaCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
-
-            if (axonResponse.getPlanId() != null) {
-                auditGenerateCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-            }
-
-            return result;
-        };
-    }
-
-    /**
-     * Wraps an asynchronous Ollama chat call with governance.
-     */
-    public Function> wrapChatAsync(
-            Function> ollamaCall) {
-
-        return request -> {
-            String prompt = request.extractPrompt();
-
-            Map context = new HashMap<>();
-            context.put("provider", "ollama");
-            context.put("model", request.getModel());
-
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
-
-            if (axonResponse.isBlocked()) {
-                return CompletableFuture.failedFuture(
-                    new PolicyViolationException(axonResponse.getBlockReason())
-                );
-            }
-
-            long startTime = System.currentTimeMillis();
-            String planId = axonResponse.getPlanId();
-            String model = request.getModel();
-
-            return ollamaCall.apply(request)
-                .thenApply(result -> {
-                    long latencyMs = System.currentTimeMillis() - startTime;
-                    if (planId != null) {
-                        auditChatCall(planId, result, model, latencyMs);
-                    }
-                    return result;
-                });
-        };
-    }
-
-    private void auditChatCall(String contextId, OllamaChatResponse response, String model, long latencyMs) {
-        try {
-            String summary = response != null && response.getMessage() != null
-                ? response.getMessage().getContent()
-                : "";
-            if (summary.length() > 100) {
-                summary = summary.substring(0, 100) + "...";
-            }
-
-            int promptTokens = response != null ? response.getPromptEvalCount() : 0;
-            int completionTokens = response != null ? response.getEvalCount() : 0;
-            TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
-
-            AuditOptions auditOptions = AuditOptions.builder()
-                .contextId(contextId)
-                .responseSummary(summary)
-                .provider("ollama")
-                .model(model)
-                .tokenUsage(usage)
-                .latencyMs(latencyMs)
-                .build();
-
-            axonflow.auditLLMCall(auditOptions);
-        } catch (Exception e) {
-            // Log but don't fail the request
-        }
-    }
-
-    private void auditGenerateCall(String contextId, OllamaGenerateResponse response, String model, long latencyMs) {
-        try {
-            String summary = response != null ? response.getResponse() : "";
-            if (summary.length() > 100) {
-                summary = summary.substring(0, 100) + "...";
-            }
-
-            int promptTokens = response != null ? response.getPromptEvalCount() : 0;
-            int completionTokens = response != null ? response.getEvalCount() : 0;
-            TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
-
-            AuditOptions auditOptions = AuditOptions.builder()
-                .contextId(contextId)
-                .responseSummary(summary)
-                .provider("ollama")
-                .model(model)
-                .tokenUsage(usage)
-                .latencyMs(latencyMs)
-                .build();
-
-            axonflow.auditLLMCall(auditOptions);
-        } catch (Exception e) {
-            // Log but don't fail
-        }
-    }
-
-    // ==================== Ollama Request/Response Types ====================
-
-    /**
-     * Ollama chat message.
-     */
-    public static class OllamaMessage {
-        private String role;
-        private String content;
-        private List images;
-
-        public OllamaMessage() {}
-
-        public OllamaMessage(String role, String content) {
-            this.role = role;
-            this.content = content;
-        }
-
-        public static OllamaMessage user(String content) {
-            return new OllamaMessage("user", content);
-        }
-
-        public static OllamaMessage assistant(String content) {
-            return new OllamaMessage("assistant", content);
-        }
-
-        public static OllamaMessage system(String content) {
-            return new OllamaMessage("system", content);
-        }
-
-        public String getRole() { return role; }
-        public void setRole(String role) { this.role = role; }
-        public String getContent() { return content; }
-        public void setContent(String content) { this.content = content; }
-        public List getImages() { return images; }
-        public void setImages(List images) { this.images = images; }
-    }
-
-    /**
-     * Ollama chat request.
-     */
-    public static class OllamaChatRequest {
-        private String model;
-        private List messages;
-        private boolean stream;
-        private String format;
-        private OllamaOptions options;
-
-        public OllamaChatRequest() {
-            this.messages = new ArrayList<>();
-        }
-
-        public static OllamaChatRequest create(String model, String userMessage) {
-            OllamaChatRequest req = new OllamaChatRequest();
-            req.model = model;
-            req.messages.add(OllamaMessage.user(userMessage));
-            return req;
-        }
-
-        public String extractPrompt() {
-            if (messages == null || messages.isEmpty()) {
-                return "";
-            }
-            return messages.stream()
-                .map(OllamaMessage::getContent)
-                .collect(Collectors.joining(" "));
-        }
-
-        public String getModel() { return model; }
-        public void setModel(String model) { this.model = model; }
-        public List getMessages() { return messages; }
-        public void setMessages(List messages) { this.messages = messages; }
-        public boolean isStream() { return stream; }
-        public void setStream(boolean stream) { this.stream = stream; }
-        public String getFormat() { return format; }
-        public void setFormat(String format) { this.format = format; }
-        public OllamaOptions getOptions() { return options; }
-        public void setOptions(OllamaOptions options) { this.options = options; }
-    }
-
-    /**
-     * Ollama generation options.
-     */
-    public static class OllamaOptions {
-        private Double temperature;
-        private Double topP;
-        private Integer topK;
-        private Integer numPredict;
-        private List stop;
-
-        public Double getTemperature() { return temperature; }
-        public void setTemperature(Double temperature) { this.temperature = temperature; }
-        public Double getTopP() { return topP; }
-        public void setTopP(Double topP) { this.topP = topP; }
-        public Integer getTopK() { return topK; }
-        public void setTopK(Integer topK) { this.topK = topK; }
-        public Integer getNumPredict() { return numPredict; }
-        public void setNumPredict(Integer numPredict) { this.numPredict = numPredict; }
-        public List getStop() { return stop; }
-        public void setStop(List stop) { this.stop = stop; }
-    }
-
-    /**
-     * Ollama chat response.
-     */
-    public static class OllamaChatResponse {
-        private String model;
-        private String createdAt;
-        private OllamaMessage message;
-        private boolean done;
-        private long totalDuration;
-        private long loadDuration;
-        private int promptEvalCount;
-        private long promptEvalDuration;
-        private int evalCount;
-        private long evalDuration;
-
-        public String getModel() { return model; }
-        public void setModel(String model) { this.model = model; }
-        public String getCreatedAt() { return createdAt; }
-        public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
-        public OllamaMessage getMessage() { return message; }
-        public void setMessage(OllamaMessage message) { this.message = message; }
-        public boolean isDone() { return done; }
-        public void setDone(boolean done) { this.done = done; }
-        public long getTotalDuration() { return totalDuration; }
-        public void setTotalDuration(long totalDuration) { this.totalDuration = totalDuration; }
-        public long getLoadDuration() { return loadDuration; }
-        public void setLoadDuration(long loadDuration) { this.loadDuration = loadDuration; }
-        public int getPromptEvalCount() { return promptEvalCount; }
-        public void setPromptEvalCount(int promptEvalCount) { this.promptEvalCount = promptEvalCount; }
-        public long getPromptEvalDuration() { return promptEvalDuration; }
-        public void setPromptEvalDuration(long promptEvalDuration) { this.promptEvalDuration = promptEvalDuration; }
-        public int getEvalCount() { return evalCount; }
-        public void setEvalCount(int evalCount) { this.evalCount = evalCount; }
-        public long getEvalDuration() { return evalDuration; }
-        public void setEvalDuration(long evalDuration) { this.evalDuration = evalDuration; }
-    }
-
-    /**
-     * Ollama generate request.
-     */
-    public static class OllamaGenerateRequest {
-        private String model;
-        private String prompt;
-        private boolean stream;
-        private String format;
-        private OllamaOptions options;
-
-        public static OllamaGenerateRequest create(String model, String prompt) {
-            OllamaGenerateRequest req = new OllamaGenerateRequest();
-            req.model = model;
-            req.prompt = prompt;
-            return req;
-        }
-
-        public String getModel() { return model; }
-        public void setModel(String model) { this.model = model; }
-        public String getPrompt() { return prompt; }
-        public void setPrompt(String prompt) { this.prompt = prompt; }
-        public boolean isStream() { return stream; }
-        public void setStream(boolean stream) { this.stream = stream; }
-        public String getFormat() { return format; }
-        public void setFormat(String format) { this.format = format; }
-        public OllamaOptions getOptions() { return options; }
-        public void setOptions(OllamaOptions options) { this.options = options; }
-    }
-
-    /**
-     * Ollama generate response.
-     */
-    public static class OllamaGenerateResponse {
-        private String model;
-        private String createdAt;
-        private String response;
-        private boolean done;
-        private long totalDuration;
-        private long loadDuration;
-        private int promptEvalCount;
-        private long promptEvalDuration;
-        private int evalCount;
-        private long evalDuration;
-
-        public String getModel() { return model; }
-        public void setModel(String model) { this.model = model; }
-        public String getCreatedAt() { return createdAt; }
-        public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
-        public String getResponse() { return response; }
-        public void setResponse(String response) { this.response = response; }
-        public boolean isDone() { return done; }
-        public void setDone(boolean done) { this.done = done; }
-        public long getTotalDuration() { return totalDuration; }
-        public void setTotalDuration(long totalDuration) { this.totalDuration = totalDuration; }
-        public long getLoadDuration() { return loadDuration; }
-        public void setLoadDuration(long loadDuration) { this.loadDuration = loadDuration; }
-        public int getPromptEvalCount() { return promptEvalCount; }
-        public void setPromptEvalCount(int promptEvalCount) { this.promptEvalCount = promptEvalCount; }
-        public long getPromptEvalDuration() { return promptEvalDuration; }
-        public void setPromptEvalDuration(long promptEvalDuration) { this.promptEvalDuration = promptEvalDuration; }
-        public int getEvalCount() { return evalCount; }
-        public void setEvalCount(int evalCount) { this.evalCount = evalCount; }
-        public long getEvalDuration() { return evalDuration; }
-        public void setEvalDuration(long evalDuration) { this.evalDuration = evalDuration; }
+  private final AxonFlow axonflow;
+  private final String userToken;
+
+  /**
+   * Creates a new OllamaInterceptor.
+   *
+   * @param axonflow the AxonFlow client for governance
+   * @param userToken the user token for policy evaluation
+   */
+  public OllamaInterceptor(AxonFlow axonflow, String userToken) {
+    if (axonflow == null) {
+      throw new IllegalArgumentException("axonflow cannot be null");
+    }
+    if (userToken == null || userToken.isEmpty()) {
+      throw new IllegalArgumentException("userToken cannot be null or empty");
+    }
+    this.axonflow = axonflow;
+    this.userToken = userToken;
+  }
+
+  /**
+   * Wraps a synchronous Ollama chat call with governance.
+   *
+   * @param ollamaCall the original Ollama call function
+   * @return a wrapped function that applies governance
+   */
+  public Function wrapChat(
+      Function ollamaCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      Map context = new HashMap<>();
+      context.put("provider", "ollama");
+      context.put("model", request.getModel());
+
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
+
+      long startTime = System.currentTimeMillis();
+      OllamaChatResponse result = ollamaCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
+
+      if (axonResponse.getPlanId() != null) {
+        auditChatCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+      }
+
+      return result;
+    };
+  }
+
+  /**
+   * Wraps a synchronous Ollama generate call with governance.
+   *
+   * @param ollamaCall the original Ollama generate function
+   * @return a wrapped function that applies governance
+   */
+  public Function wrapGenerate(
+      Function ollamaCall) {
+
+    return request -> {
+      Map context = new HashMap<>();
+      context.put("provider", "ollama");
+      context.put("model", request.getModel());
+
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(request.getPrompt())
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
+
+      long startTime = System.currentTimeMillis();
+      OllamaGenerateResponse result = ollamaCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
+
+      if (axonResponse.getPlanId() != null) {
+        auditGenerateCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+      }
+
+      return result;
+    };
+  }
+
+  /** Wraps an asynchronous Ollama chat call with governance. */
+  public Function> wrapChatAsync(
+      Function> ollamaCall) {
+
+    return request -> {
+      String prompt = request.extractPrompt();
+
+      Map context = new HashMap<>();
+      context.put("provider", "ollama");
+      context.put("model", request.getModel());
+
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
+
+      if (axonResponse.isBlocked()) {
+        return CompletableFuture.failedFuture(
+            new PolicyViolationException(axonResponse.getBlockReason()));
+      }
+
+      long startTime = System.currentTimeMillis();
+      String planId = axonResponse.getPlanId();
+      String model = request.getModel();
+
+      return ollamaCall
+          .apply(request)
+          .thenApply(
+              result -> {
+                long latencyMs = System.currentTimeMillis() - startTime;
+                if (planId != null) {
+                  auditChatCall(planId, result, model, latencyMs);
+                }
+                return result;
+              });
+    };
+  }
+
+  private void auditChatCall(
+      String contextId, OllamaChatResponse response, String model, long latencyMs) {
+    try {
+      String summary =
+          response != null && response.getMessage() != null
+              ? response.getMessage().getContent()
+              : "";
+      if (summary.length() > 100) {
+        summary = summary.substring(0, 100) + "...";
+      }
+
+      int promptTokens = response != null ? response.getPromptEvalCount() : 0;
+      int completionTokens = response != null ? response.getEvalCount() : 0;
+      TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
+
+      AuditOptions auditOptions =
+          AuditOptions.builder()
+              .contextId(contextId)
+              .responseSummary(summary)
+              .provider("ollama")
+              .model(model)
+              .tokenUsage(usage)
+              .latencyMs(latencyMs)
+              .build();
+
+      axonflow.auditLLMCall(auditOptions);
+    } catch (Exception e) {
+      // Log but don't fail the request
+    }
+  }
+
+  private void auditGenerateCall(
+      String contextId, OllamaGenerateResponse response, String model, long latencyMs) {
+    try {
+      String summary = response != null ? response.getResponse() : "";
+      if (summary.length() > 100) {
+        summary = summary.substring(0, 100) + "...";
+      }
+
+      int promptTokens = response != null ? response.getPromptEvalCount() : 0;
+      int completionTokens = response != null ? response.getEvalCount() : 0;
+      TokenUsage usage = TokenUsage.of(promptTokens, completionTokens);
+
+      AuditOptions auditOptions =
+          AuditOptions.builder()
+              .contextId(contextId)
+              .responseSummary(summary)
+              .provider("ollama")
+              .model(model)
+              .tokenUsage(usage)
+              .latencyMs(latencyMs)
+              .build();
+
+      axonflow.auditLLMCall(auditOptions);
+    } catch (Exception e) {
+      // Log but don't fail
+    }
+  }
+
+  // ==================== Ollama Request/Response Types ====================
+
+  /** Ollama chat message. */
+  public static class OllamaMessage {
+    private String role;
+    private String content;
+    private List images;
+
+    public OllamaMessage() {}
+
+    public OllamaMessage(String role, String content) {
+      this.role = role;
+      this.content = content;
+    }
+
+    public static OllamaMessage user(String content) {
+      return new OllamaMessage("user", content);
+    }
+
+    public static OllamaMessage assistant(String content) {
+      return new OllamaMessage("assistant", content);
+    }
+
+    public static OllamaMessage system(String content) {
+      return new OllamaMessage("system", content);
+    }
+
+    public String getRole() {
+      return role;
+    }
+
+    public void setRole(String role) {
+      this.role = role;
+    }
+
+    public String getContent() {
+      return content;
+    }
+
+    public void setContent(String content) {
+      this.content = content;
+    }
+
+    public List getImages() {
+      return images;
+    }
+
+    public void setImages(List images) {
+      this.images = images;
+    }
+  }
+
+  /** Ollama chat request. */
+  public static class OllamaChatRequest {
+    private String model;
+    private List messages;
+    private boolean stream;
+    private String format;
+    private OllamaOptions options;
+
+    public OllamaChatRequest() {
+      this.messages = new ArrayList<>();
+    }
+
+    public static OllamaChatRequest create(String model, String userMessage) {
+      OllamaChatRequest req = new OllamaChatRequest();
+      req.model = model;
+      req.messages.add(OllamaMessage.user(userMessage));
+      return req;
+    }
+
+    public String extractPrompt() {
+      if (messages == null || messages.isEmpty()) {
+        return "";
+      }
+      return messages.stream().map(OllamaMessage::getContent).collect(Collectors.joining(" "));
+    }
+
+    public String getModel() {
+      return model;
+    }
+
+    public void setModel(String model) {
+      this.model = model;
+    }
+
+    public List getMessages() {
+      return messages;
+    }
+
+    public void setMessages(List messages) {
+      this.messages = messages;
+    }
+
+    public boolean isStream() {
+      return stream;
+    }
+
+    public void setStream(boolean stream) {
+      this.stream = stream;
+    }
+
+    public String getFormat() {
+      return format;
+    }
+
+    public void setFormat(String format) {
+      this.format = format;
+    }
+
+    public OllamaOptions getOptions() {
+      return options;
+    }
+
+    public void setOptions(OllamaOptions options) {
+      this.options = options;
+    }
+  }
+
+  /** Ollama generation options. */
+  public static class OllamaOptions {
+    private Double temperature;
+    private Double topP;
+    private Integer topK;
+    private Integer numPredict;
+    private List stop;
+
+    public Double getTemperature() {
+      return temperature;
+    }
+
+    public void setTemperature(Double temperature) {
+      this.temperature = temperature;
+    }
+
+    public Double getTopP() {
+      return topP;
+    }
+
+    public void setTopP(Double topP) {
+      this.topP = topP;
+    }
+
+    public Integer getTopK() {
+      return topK;
+    }
+
+    public void setTopK(Integer topK) {
+      this.topK = topK;
+    }
+
+    public Integer getNumPredict() {
+      return numPredict;
+    }
+
+    public void setNumPredict(Integer numPredict) {
+      this.numPredict = numPredict;
+    }
+
+    public List getStop() {
+      return stop;
+    }
+
+    public void setStop(List stop) {
+      this.stop = stop;
+    }
+  }
+
+  /** Ollama chat response. */
+  public static class OllamaChatResponse {
+    private String model;
+    private String createdAt;
+    private OllamaMessage message;
+    private boolean done;
+    private long totalDuration;
+    private long loadDuration;
+    private int promptEvalCount;
+    private long promptEvalDuration;
+    private int evalCount;
+    private long evalDuration;
+
+    public String getModel() {
+      return model;
+    }
+
+    public void setModel(String model) {
+      this.model = model;
+    }
+
+    public String getCreatedAt() {
+      return createdAt;
+    }
+
+    public void setCreatedAt(String createdAt) {
+      this.createdAt = createdAt;
+    }
+
+    public OllamaMessage getMessage() {
+      return message;
+    }
+
+    public void setMessage(OllamaMessage message) {
+      this.message = message;
+    }
+
+    public boolean isDone() {
+      return done;
+    }
+
+    public void setDone(boolean done) {
+      this.done = done;
+    }
+
+    public long getTotalDuration() {
+      return totalDuration;
+    }
+
+    public void setTotalDuration(long totalDuration) {
+      this.totalDuration = totalDuration;
+    }
+
+    public long getLoadDuration() {
+      return loadDuration;
+    }
+
+    public void setLoadDuration(long loadDuration) {
+      this.loadDuration = loadDuration;
+    }
+
+    public int getPromptEvalCount() {
+      return promptEvalCount;
+    }
+
+    public void setPromptEvalCount(int promptEvalCount) {
+      this.promptEvalCount = promptEvalCount;
+    }
+
+    public long getPromptEvalDuration() {
+      return promptEvalDuration;
+    }
+
+    public void setPromptEvalDuration(long promptEvalDuration) {
+      this.promptEvalDuration = promptEvalDuration;
+    }
+
+    public int getEvalCount() {
+      return evalCount;
+    }
+
+    public void setEvalCount(int evalCount) {
+      this.evalCount = evalCount;
+    }
+
+    public long getEvalDuration() {
+      return evalDuration;
+    }
+
+    public void setEvalDuration(long evalDuration) {
+      this.evalDuration = evalDuration;
+    }
+  }
+
+  /** Ollama generate request. */
+  public static class OllamaGenerateRequest {
+    private String model;
+    private String prompt;
+    private boolean stream;
+    private String format;
+    private OllamaOptions options;
+
+    public static OllamaGenerateRequest create(String model, String prompt) {
+      OllamaGenerateRequest req = new OllamaGenerateRequest();
+      req.model = model;
+      req.prompt = prompt;
+      return req;
+    }
+
+    public String getModel() {
+      return model;
+    }
+
+    public void setModel(String model) {
+      this.model = model;
+    }
+
+    public String getPrompt() {
+      return prompt;
+    }
+
+    public void setPrompt(String prompt) {
+      this.prompt = prompt;
+    }
+
+    public boolean isStream() {
+      return stream;
+    }
+
+    public void setStream(boolean stream) {
+      this.stream = stream;
+    }
+
+    public String getFormat() {
+      return format;
+    }
+
+    public void setFormat(String format) {
+      this.format = format;
+    }
+
+    public OllamaOptions getOptions() {
+      return options;
+    }
+
+    public void setOptions(OllamaOptions options) {
+      this.options = options;
+    }
+  }
+
+  /** Ollama generate response. */
+  public static class OllamaGenerateResponse {
+    private String model;
+    private String createdAt;
+    private String response;
+    private boolean done;
+    private long totalDuration;
+    private long loadDuration;
+    private int promptEvalCount;
+    private long promptEvalDuration;
+    private int evalCount;
+    private long evalDuration;
+
+    public String getModel() {
+      return model;
+    }
+
+    public void setModel(String model) {
+      this.model = model;
+    }
+
+    public String getCreatedAt() {
+      return createdAt;
+    }
+
+    public void setCreatedAt(String createdAt) {
+      this.createdAt = createdAt;
+    }
+
+    public String getResponse() {
+      return response;
+    }
+
+    public void setResponse(String response) {
+      this.response = response;
+    }
+
+    public boolean isDone() {
+      return done;
+    }
+
+    public void setDone(boolean done) {
+      this.done = done;
+    }
+
+    public long getTotalDuration() {
+      return totalDuration;
+    }
+
+    public void setTotalDuration(long totalDuration) {
+      this.totalDuration = totalDuration;
+    }
+
+    public long getLoadDuration() {
+      return loadDuration;
+    }
+
+    public void setLoadDuration(long loadDuration) {
+      this.loadDuration = loadDuration;
+    }
+
+    public int getPromptEvalCount() {
+      return promptEvalCount;
+    }
+
+    public void setPromptEvalCount(int promptEvalCount) {
+      this.promptEvalCount = promptEvalCount;
+    }
+
+    public long getPromptEvalDuration() {
+      return promptEvalDuration;
+    }
+
+    public void setPromptEvalDuration(long promptEvalDuration) {
+      this.promptEvalDuration = promptEvalDuration;
+    }
+
+    public int getEvalCount() {
+      return evalCount;
+    }
+
+    public void setEvalCount(int evalCount) {
+      this.evalCount = evalCount;
+    }
+
+    public long getEvalDuration() {
+      return evalDuration;
+    }
+
+    public void setEvalDuration(long evalDuration) {
+      this.evalDuration = evalDuration;
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptor.java b/src/main/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptor.java
index c757ba0..aa8bd5b 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptor.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptor.java
@@ -13,7 +13,6 @@
 import com.getaxonflow.sdk.types.ClientResponse;
 import com.getaxonflow.sdk.types.RequestType;
 import com.getaxonflow.sdk.types.TokenUsage;
-
 import java.util.HashMap;
 import java.util.Map;
 import java.util.Objects;
@@ -23,10 +22,11 @@
 /**
  * Interceptor for wrapping OpenAI API calls with AxonFlow governance.
  *
- * 

This interceptor automatically applies policy checks and audit logging - * to OpenAI API calls without requiring changes to application code. + *

This interceptor automatically applies policy checks and audit logging to OpenAI API calls + * without requiring changes to application code. * *

Example Usage

+ * *
{@code
  * // Create AxonFlow client
  * AxonFlow axonflow = AxonFlow.builder()
@@ -56,220 +56,227 @@
  * @see ChatCompletionResponse
  */
 public final class OpenAIInterceptor {
-    private final AxonFlow axonflow;
-    private final String userToken;
-    private final boolean asyncAudit;
+  private final AxonFlow axonflow;
+  private final String userToken;
+  private final boolean asyncAudit;
 
-    private OpenAIInterceptor(Builder builder) {
-        this.axonflow = Objects.requireNonNull(builder.axonflow, "axonflow must not be null");
-        this.userToken = builder.userToken != null ? builder.userToken : "";
-        this.asyncAudit = builder.asyncAudit;
-    }
+  private OpenAIInterceptor(Builder builder) {
+    this.axonflow = Objects.requireNonNull(builder.axonflow, "axonflow must not be null");
+    this.userToken = builder.userToken != null ? builder.userToken : "";
+    this.asyncAudit = builder.asyncAudit;
+  }
 
-    public static Builder builder() {
-        return new Builder();
-    }
+  public static Builder builder() {
+    return new Builder();
+  }
 
-    /**
-     * Wraps an OpenAI chat completion function with governance.
-     *
-     * @param openaiCall the function that makes the actual OpenAI API call
-     * @return a wrapped function that applies governance before/after the call
-     */
-    public Function wrap(
-            Function openaiCall) {
-        return request -> {
-            // Extract prompt from messages
-            String prompt = request.extractPrompt();
+  /**
+   * Wraps an OpenAI chat completion function with governance.
+   *
+   * @param openaiCall the function that makes the actual OpenAI API call
+   * @return a wrapped function that applies governance before/after the call
+   */
+  public Function wrap(
+      Function openaiCall) {
+    return request -> {
+      // Extract prompt from messages
+      String prompt = request.extractPrompt();
 
-            // Build context for policy evaluation
-            Map context = new HashMap<>();
-            context.put("provider", "openai");
-            context.put("model", request.getModel());
-            if (request.getTemperature() != null) {
-                context.put("temperature", request.getTemperature());
-            }
-            if (request.getMaxTokens() != null) {
-                context.put("max_tokens", request.getMaxTokens());
-            }
+      // Build context for policy evaluation
+      Map context = new HashMap<>();
+      context.put("provider", "openai");
+      context.put("model", request.getModel());
+      if (request.getTemperature() != null) {
+        context.put("temperature", request.getTemperature());
+      }
+      if (request.getMaxTokens() != null) {
+        context.put("max_tokens", request.getMaxTokens());
+      }
 
-            // Check with AxonFlow
-            long startTime = System.currentTimeMillis();
-            ClientResponse axonResponse = axonflow.proxyLLMCall(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            );
+      // Check with AxonFlow
+      long startTime = System.currentTimeMillis();
+      ClientResponse axonResponse =
+          axonflow.proxyLLMCall(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build());
 
-            // Check if request was blocked
-            if (axonResponse.isBlocked()) {
-                throw new PolicyViolationException(axonResponse.getBlockReason());
-            }
+      // Check if request was blocked
+      if (axonResponse.isBlocked()) {
+        throw new PolicyViolationException(axonResponse.getBlockReason());
+      }
 
-            // Make the actual OpenAI call
-            ChatCompletionResponse result = openaiCall.apply(request);
-            long latencyMs = System.currentTimeMillis() - startTime;
+      // Make the actual OpenAI call
+      ChatCompletionResponse result = openaiCall.apply(request);
+      long latencyMs = System.currentTimeMillis() - startTime;
 
-            // Audit the call
-            if (axonResponse.getPlanId() != null) {
-                auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-            }
+      // Audit the call
+      if (axonResponse.getPlanId() != null) {
+        auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+      }
 
-            return result;
-        };
-    }
+      return result;
+    };
+  }
 
-    /**
-     * Wraps an async OpenAI chat completion function with governance.
-     *
-     * @param openaiCall the function that makes the actual OpenAI API call
-     * @return a wrapped function that applies governance before/after the call
-     */
-    public Function> wrapAsync(
-            Function> openaiCall) {
-        return request -> {
-            // Extract prompt from messages
-            String prompt = request.extractPrompt();
+  /**
+   * Wraps an async OpenAI chat completion function with governance.
+   *
+   * @param openaiCall the function that makes the actual OpenAI API call
+   * @return a wrapped function that applies governance before/after the call
+   */
+  public Function> wrapAsync(
+      Function> openaiCall) {
+    return request -> {
+      // Extract prompt from messages
+      String prompt = request.extractPrompt();
 
-            // Build context for policy evaluation
-            Map context = new HashMap<>();
-            context.put("provider", "openai");
-            context.put("model", request.getModel());
-            if (request.getTemperature() != null) {
-                context.put("temperature", request.getTemperature());
-            }
-            if (request.getMaxTokens() != null) {
-                context.put("max_tokens", request.getMaxTokens());
-            }
+      // Build context for policy evaluation
+      Map context = new HashMap<>();
+      context.put("provider", "openai");
+      context.put("model", request.getModel());
+      if (request.getTemperature() != null) {
+        context.put("temperature", request.getTemperature());
+      }
+      if (request.getMaxTokens() != null) {
+        context.put("max_tokens", request.getMaxTokens());
+      }
 
-            // Check with AxonFlow (async)
-            long startTime = System.currentTimeMillis();
+      // Check with AxonFlow (async)
+      long startTime = System.currentTimeMillis();
 
-            return axonflow.proxyLLMCallAsync(
-                ClientRequest.builder()
-                    .query(prompt)
-                    .userToken(userToken)
-                    .requestType(RequestType.CHAT)
-                    .context(context)
-                    .build()
-            ).thenCompose(axonResponse -> {
+      return axonflow
+          .proxyLLMCallAsync(
+              ClientRequest.builder()
+                  .query(prompt)
+                  .userToken(userToken)
+                  .requestType(RequestType.CHAT)
+                  .context(context)
+                  .build())
+          .thenCompose(
+              axonResponse -> {
                 // Check if request was blocked
                 if (axonResponse.isBlocked()) {
-                    CompletableFuture failed = new CompletableFuture<>();
-                    failed.completeExceptionally(new PolicyViolationException(
-                        axonResponse.getBlockReason()
-                    ));
-                    return failed;
+                  CompletableFuture failed = new CompletableFuture<>();
+                  failed.completeExceptionally(
+                      new PolicyViolationException(axonResponse.getBlockReason()));
+                  return failed;
                 }
 
                 // Make the actual OpenAI call
-                return openaiCall.apply(request).thenApply(result -> {
-                    long latencyMs = System.currentTimeMillis() - startTime;
+                return openaiCall
+                    .apply(request)
+                    .thenApply(
+                        result -> {
+                          long latencyMs = System.currentTimeMillis() - startTime;
 
-                    // Audit the call (async/fire-and-forget)
-                    if (axonResponse.getPlanId() != null) {
-                        if (asyncAudit) {
-                            CompletableFuture.runAsync(() ->
-                                auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs)
-                            );
-                        } else {
-                            auditCall(axonResponse.getPlanId(), result, request.getModel(), latencyMs);
-                        }
-                    }
+                          // Audit the call (async/fire-and-forget)
+                          if (axonResponse.getPlanId() != null) {
+                            if (asyncAudit) {
+                              CompletableFuture.runAsync(
+                                  () ->
+                                      auditCall(
+                                          axonResponse.getPlanId(),
+                                          result,
+                                          request.getModel(),
+                                          latencyMs));
+                            } else {
+                              auditCall(
+                                  axonResponse.getPlanId(), result, request.getModel(), latencyMs);
+                            }
+                          }
 
-                    return result;
-                });
-            });
-        };
-    }
+                          return result;
+                        });
+              });
+    };
+  }
 
-    private void auditCall(String contextId, ChatCompletionResponse result, String model, long latencyMs) {
-        try {
-            ChatCompletionResponse.Usage usage = result.getUsage();
-            TokenUsage tokenUsage = usage != null ?
-                TokenUsage.of(usage.getPromptTokens(), usage.getCompletionTokens()) :
-                TokenUsage.of(0, 0);
+  private void auditCall(
+      String contextId, ChatCompletionResponse result, String model, long latencyMs) {
+    try {
+      ChatCompletionResponse.Usage usage = result.getUsage();
+      TokenUsage tokenUsage =
+          usage != null
+              ? TokenUsage.of(usage.getPromptTokens(), usage.getCompletionTokens())
+              : TokenUsage.of(0, 0);
 
-            axonflow.auditLLMCall(AuditOptions.builder()
-                .contextId(contextId)
-                .clientId(userToken)
-                .responseSummary(result.getSummary())
-                .provider("openai")
-                .model(model)
-                .tokenUsage(tokenUsage)
-                .latencyMs(latencyMs)
-                .success(true)
-                .build());
-        } catch (Exception e) {
-            // Best effort - don't fail the response if audit fails
-        }
+      axonflow.auditLLMCall(
+          AuditOptions.builder()
+              .contextId(contextId)
+              .clientId(userToken)
+              .responseSummary(result.getSummary())
+              .provider("openai")
+              .model(model)
+              .tokenUsage(tokenUsage)
+              .latencyMs(latencyMs)
+              .success(true)
+              .build());
+    } catch (Exception e) {
+      // Best effort - don't fail the response if audit fails
     }
+  }
+
+  /**
+   * Creates a simple wrapper function for chat completions.
+   *
+   * @param axonflow the AxonFlow client
+   * @param userToken the user token for policy evaluation
+   * @param openaiCall the function that makes the actual OpenAI API call
+   * @return a wrapped function
+   */
+  public static Function wrapChatCompletion(
+      AxonFlow axonflow,
+      String userToken,
+      Function openaiCall) {
+    return builder().axonflow(axonflow).userToken(userToken).build().wrap(openaiCall);
+  }
+
+  public static final class Builder {
+    private AxonFlow axonflow;
+    private String userToken;
+    private boolean asyncAudit = true;
+
+    private Builder() {}
 
     /**
-     * Creates a simple wrapper function for chat completions.
+     * Sets the AxonFlow client for governance.
      *
-     * @param axonflow  the AxonFlow client
-     * @param userToken the user token for policy evaluation
-     * @param openaiCall the function that makes the actual OpenAI API call
-     * @return a wrapped function
+     * @param axonflow the AxonFlow client
+     * @return this builder
      */
-    public static Function wrapChatCompletion(
-            AxonFlow axonflow,
-            String userToken,
-            Function openaiCall) {
-        return builder()
-            .axonflow(axonflow)
-            .userToken(userToken)
-            .build()
-            .wrap(openaiCall);
+    public Builder axonflow(AxonFlow axonflow) {
+      this.axonflow = axonflow;
+      return this;
     }
 
-    public static final class Builder {
-        private AxonFlow axonflow;
-        private String userToken;
-        private boolean asyncAudit = true;
-
-        private Builder() {}
-
-        /**
-         * Sets the AxonFlow client for governance.
-         *
-         * @param axonflow the AxonFlow client
-         * @return this builder
-         */
-        public Builder axonflow(AxonFlow axonflow) {
-            this.axonflow = axonflow;
-            return this;
-        }
-
-        /**
-         * Sets the user token for policy evaluation.
-         *
-         * @param userToken the user token
-         * @return this builder
-         */
-        public Builder userToken(String userToken) {
-            this.userToken = userToken;
-            return this;
-        }
+    /**
+     * Sets the user token for policy evaluation.
+     *
+     * @param userToken the user token
+     * @return this builder
+     */
+    public Builder userToken(String userToken) {
+      this.userToken = userToken;
+      return this;
+    }
 
-        /**
-         * Sets whether to perform audit logging asynchronously.
-         * Default is true (fire-and-forget).
-         *
-         * @param asyncAudit true to audit asynchronously
-         * @return this builder
-         */
-        public Builder asyncAudit(boolean asyncAudit) {
-            this.asyncAudit = asyncAudit;
-            return this;
-        }
+    /**
+     * Sets whether to perform audit logging asynchronously. Default is true (fire-and-forget).
+     *
+     * @param asyncAudit true to audit asynchronously
+     * @return this builder
+     */
+    public Builder asyncAudit(boolean asyncAudit) {
+      this.asyncAudit = asyncAudit;
+      return this;
+    }
 
-        public OpenAIInterceptor build() {
-            return new OpenAIInterceptor(this);
-        }
+    public OpenAIInterceptor build() {
+      return new OpenAIInterceptor(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/interceptors/package-info.java b/src/main/java/com/getaxonflow/sdk/interceptors/package-info.java
index 9482042..0f4a239 100644
--- a/src/main/java/com/getaxonflow/sdk/interceptors/package-info.java
+++ b/src/main/java/com/getaxonflow/sdk/interceptors/package-info.java
@@ -8,17 +8,19 @@
 /**
  * LLM interceptors for transparent governance integration.
  *
- * 

This package provides interceptors for wrapping LLM API calls with - * AxonFlow governance, enabling automatic policy enforcement and audit - * logging without requiring changes to application code. + *

This package provides interceptors for wrapping LLM API calls with AxonFlow governance, + * enabling automatic policy enforcement and audit logging without requiring changes to application + * code. * *

Supported Providers

+ * *
    - *
  • {@link com.getaxonflow.sdk.interceptors.OpenAIInterceptor} - For OpenAI API calls
  • - *
  • {@link com.getaxonflow.sdk.interceptors.AnthropicInterceptor} - For Anthropic API calls
  • + *
  • {@link com.getaxonflow.sdk.interceptors.OpenAIInterceptor} - For OpenAI API calls + *
  • {@link com.getaxonflow.sdk.interceptors.AnthropicInterceptor} - For Anthropic API calls *
* *

Quick Example

+ * *
{@code
  * // Create AxonFlow client
  * AxonFlow axonflow = AxonFlow.builder()
diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java
index 9ccd140..7a0908c 100644
--- a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java
+++ b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java
@@ -7,938 +7,1695 @@
 /**
  * MAS FEAT Compliance Types for Singapore Regulatory Compliance.
  *
- * 

This class contains all types for the MAS FEAT (Monetary Authority of Singapore - - * Fairness, Ethics, Accountability, Transparency) compliance module. + *

This class contains all types for the MAS FEAT (Monetary Authority of Singapore - Fairness, + * Ethics, Accountability, Transparency) compliance module. * *

Enterprise Feature: Requires AxonFlow Enterprise license. */ public final class MASFEATTypes { - private MASFEATTypes() { - // Utility class - } + private MASFEATTypes() { + // Utility class + } - // ========================================================================= - // Enums - // ========================================================================= + // ========================================================================= + // Enums + // ========================================================================= - /** Materiality classification based on 3D risk rating. */ - public enum MaterialityClassification { - HIGH("high"), - MEDIUM("medium"), - LOW("low"); + /** Materiality classification based on 3D risk rating. */ + public enum MaterialityClassification { + HIGH("high"), + MEDIUM("medium"), + LOW("low"); - private final String value; + private final String value; - MaterialityClassification(String value) { - this.value = value; - } + MaterialityClassification(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static MaterialityClassification fromValue(String value) { - for (MaterialityClassification e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown materiality: " + value); + public static MaterialityClassification fromValue(String value) { + for (MaterialityClassification e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown materiality: " + value); } + } - /** AI system lifecycle status. */ - public enum SystemStatus { - DRAFT("draft"), - ACTIVE("active"), - SUSPENDED("suspended"), - RETIRED("retired"); + /** AI system lifecycle status. */ + public enum SystemStatus { + DRAFT("draft"), + ACTIVE("active"), + SUSPENDED("suspended"), + RETIRED("retired"); - private final String value; + private final String value; - SystemStatus(String value) { - this.value = value; - } + SystemStatus(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static SystemStatus fromValue(String value) { - for (SystemStatus e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown status: " + value); + public static SystemStatus fromValue(String value) { + for (SystemStatus e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown status: " + value); } + } - /** FEAT assessment lifecycle status. */ - public enum FEATAssessmentStatus { - PENDING("pending"), - IN_PROGRESS("in_progress"), - COMPLETED("completed"), - APPROVED("approved"), - REJECTED("rejected"); + /** FEAT assessment lifecycle status. */ + public enum FEATAssessmentStatus { + PENDING("pending"), + IN_PROGRESS("in_progress"), + COMPLETED("completed"), + APPROVED("approved"), + REJECTED("rejected"); - private final String value; + private final String value; - FEATAssessmentStatus(String value) { - this.value = value; - } + FEATAssessmentStatus(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static FEATAssessmentStatus fromValue(String value) { - for (FEATAssessmentStatus e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown assessment status: " + value); + public static FEATAssessmentStatus fromValue(String value) { + for (FEATAssessmentStatus e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown assessment status: " + value); } + } - /** Kill switch status. */ - public enum KillSwitchStatus { - ENABLED("enabled"), - DISABLED("disabled"), - TRIGGERED("triggered"); + /** Kill switch status. */ + public enum KillSwitchStatus { + ENABLED("enabled"), + DISABLED("disabled"), + TRIGGERED("triggered"); - private final String value; + private final String value; - KillSwitchStatus(String value) { - this.value = value; - } + KillSwitchStatus(String value) { + this.value = value; + } - public String getValue() { - return value; + public String getValue() { + return value; + } + + public static KillSwitchStatus fromValue(String value) { + for (KillSwitchStatus e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown kill switch status: " + value); + } + } + + /** AI system use case categories. */ + public enum AISystemUseCase { + CREDIT_SCORING("credit_scoring"), + ROBO_ADVISORY("robo_advisory"), + INSURANCE_UNDERWRITING("insurance_underwriting"), + TRADING_ALGORITHM("trading_algorithm"), + AML_CFT("aml_cft"), + CUSTOMER_SERVICE("customer_service"), + FRAUD_DETECTION("fraud_detection"), + OTHER("other"); + + private final String value; + + AISystemUseCase(String value) { + this.value = value; + } - public static KillSwitchStatus fromValue(String value) { - for (KillSwitchStatus e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown kill switch status: " + value); + public String getValue() { + return value; + } + + public static AISystemUseCase fromValue(String value) { + for (AISystemUseCase e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown use case: " + value); } + } - /** AI system use case categories. */ - public enum AISystemUseCase { - CREDIT_SCORING("credit_scoring"), - ROBO_ADVISORY("robo_advisory"), - INSURANCE_UNDERWRITING("insurance_underwriting"), - TRADING_ALGORITHM("trading_algorithm"), - AML_CFT("aml_cft"), - CUSTOMER_SERVICE("customer_service"), - FRAUD_DETECTION("fraud_detection"), - OTHER("other"); + /** FEAT framework pillars. */ + public enum FEATPillar { + FAIRNESS("fairness"), + ETHICS("ethics"), + ACCOUNTABILITY("accountability"), + TRANSPARENCY("transparency"); - private final String value; + private final String value; - AISystemUseCase(String value) { - this.value = value; - } + FEATPillar(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static AISystemUseCase fromValue(String value) { - for (AISystemUseCase e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown use case: " + value); + public static FEATPillar fromValue(String value) { + for (FEATPillar e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown pillar: " + value); } + } - /** FEAT framework pillars. */ - public enum FEATPillar { - FAIRNESS("fairness"), - ETHICS("ethics"), - ACCOUNTABILITY("accountability"), - TRANSPARENCY("transparency"); + /** FEAT assessment finding severity. */ + public enum FindingSeverity { + CRITICAL("critical"), + MAJOR("major"), + MINOR("minor"), + OBSERVATION("observation"); - private final String value; + private final String value; - FEATPillar(String value) { - this.value = value; - } + FindingSeverity(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static FEATPillar fromValue(String value) { - for (FEATPillar e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown pillar: " + value); + public static FindingSeverity fromValue(String value) { + for (FindingSeverity e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown finding severity: " + value); } + } - /** FEAT assessment finding severity. */ - public enum FindingSeverity { - CRITICAL("critical"), - MAJOR("major"), - MINOR("minor"), - OBSERVATION("observation"); + /** FEAT assessment finding status. */ + public enum FindingStatus { + OPEN("open"), + RESOLVED("resolved"), + ACCEPTED("accepted"); - private final String value; + private final String value; - FindingSeverity(String value) { - this.value = value; - } + FindingStatus(String value) { + this.value = value; + } - public String getValue() { - return value; - } + public String getValue() { + return value; + } - public static FindingSeverity fromValue(String value) { - for (FindingSeverity e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown finding severity: " + value); + public static FindingStatus fromValue(String value) { + for (FindingStatus e : values()) { + if (e.value.equals(value)) { + return e; } + } + throw new IllegalArgumentException("Unknown finding status: " + value); + } + } + + // ========================================================================= + // Finding Type + // ========================================================================= + + /** A FEAT assessment finding. */ + public static class Finding { + private String id; + private FEATPillar pillar; + private FindingSeverity severity; + private String category; + private String description; + private FindingStatus status; + private String remediation; + private Instant dueDate; + + public Finding() {} + + private Finding(Builder builder) { + this.id = builder.id; + this.pillar = builder.pillar; + this.severity = builder.severity; + this.category = builder.category; + this.description = builder.description; + this.status = builder.status; + this.remediation = builder.remediation; + this.dueDate = builder.dueDate; } - /** FEAT assessment finding status. */ - public enum FindingStatus { - OPEN("open"), - RESOLVED("resolved"), - ACCEPTED("accepted"); + public static Builder builder() { + return new Builder(); + } - private final String value; + public String getId() { + return id; + } - FindingStatus(String value) { - this.value = value; - } + public void setId(String id) { + this.id = id; + } - public String getValue() { - return value; - } + public FEATPillar getPillar() { + return pillar; + } - public static FindingStatus fromValue(String value) { - for (FindingStatus e : values()) { - if (e.value.equals(value)) { - return e; - } - } - throw new IllegalArgumentException("Unknown finding status: " + value); - } + public void setPillar(FEATPillar pillar) { + this.pillar = pillar; } - // ========================================================================= - // Finding Type - // ========================================================================= - - /** A FEAT assessment finding. */ - public static class Finding { - private String id; - private FEATPillar pillar; - private FindingSeverity severity; - private String category; - private String description; - private FindingStatus status; - private String remediation; - private Instant dueDate; - - public Finding() {} - - private Finding(Builder builder) { - this.id = builder.id; - this.pillar = builder.pillar; - this.severity = builder.severity; - this.category = builder.category; - this.description = builder.description; - this.status = builder.status; - this.remediation = builder.remediation; - this.dueDate = builder.dueDate; - } + public FindingSeverity getSeverity() { + return severity; + } - public static Builder builder() { - return new Builder(); - } + public void setSeverity(FindingSeverity severity) { + this.severity = severity; + } - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public FEATPillar getPillar() { return pillar; } - public void setPillar(FEATPillar pillar) { this.pillar = pillar; } - public FindingSeverity getSeverity() { return severity; } - public void setSeverity(FindingSeverity severity) { this.severity = severity; } - public String getCategory() { return category; } - public void setCategory(String category) { this.category = category; } - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - public FindingStatus getStatus() { return status; } - public void setStatus(FindingStatus status) { this.status = status; } - public String getRemediation() { return remediation; } - public void setRemediation(String remediation) { this.remediation = remediation; } - public Instant getDueDate() { return dueDate; } - public void setDueDate(Instant dueDate) { this.dueDate = dueDate; } - - public static class Builder { - private String id; - private FEATPillar pillar; - private FindingSeverity severity; - private String category; - private String description; - private FindingStatus status; - private String remediation; - private Instant dueDate; - - public Builder id(String id) { this.id = id; return this; } - public Builder pillar(FEATPillar pillar) { this.pillar = pillar; return this; } - public Builder severity(FindingSeverity severity) { this.severity = severity; return this; } - public Builder category(String category) { this.category = category; return this; } - public Builder description(String description) { this.description = description; return this; } - public Builder status(FindingStatus status) { this.status = status; return this; } - public Builder remediation(String remediation) { this.remediation = remediation; return this; } - public Builder dueDate(Instant dueDate) { this.dueDate = dueDate; return this; } - - public Finding build() { - return new Finding(this); - } - } + public String getCategory() { + return category; } - // ========================================================================= - // AI System Registry Types - // ========================================================================= - - /** Request to register an AI system. */ - public static class RegisterSystemRequest { - private final String systemId; - private final String systemName; - private final AISystemUseCase useCase; - private final String ownerTeam; - private final int customerImpact; - private final int modelComplexity; - private final int humanReliance; - private String description; - private String technicalOwner; - private String businessOwner; - private Map metadata; - - private RegisterSystemRequest(Builder builder) { - this.systemId = builder.systemId; - this.systemName = builder.systemName; - this.useCase = builder.useCase; - this.ownerTeam = builder.ownerTeam; - this.customerImpact = builder.customerImpact; - this.modelComplexity = builder.modelComplexity; - this.humanReliance = builder.humanReliance; - this.description = builder.description; - this.technicalOwner = builder.technicalOwner; - this.businessOwner = builder.businessOwner; - this.metadata = builder.metadata; - } + public void setCategory(String category) { + this.category = category; + } - public static Builder builder() { - return new Builder(); - } + public String getDescription() { + return description; + } - public String getSystemId() { return systemId; } - public String getSystemName() { return systemName; } - public AISystemUseCase getUseCase() { return useCase; } - public String getOwnerTeam() { return ownerTeam; } - public int getCustomerImpact() { return customerImpact; } - public int getModelComplexity() { return modelComplexity; } - public int getHumanReliance() { return humanReliance; } - public String getDescription() { return description; } - public String getTechnicalOwner() { return technicalOwner; } - public String getBusinessOwner() { return businessOwner; } - public Map getMetadata() { return metadata; } - - public static class Builder { - private String systemId; - private String systemName; - private AISystemUseCase useCase; - private String ownerTeam; - private int customerImpact; - private int modelComplexity; - private int humanReliance; - private String description; - private String technicalOwner; - private String businessOwner; - private Map metadata; - - public Builder systemId(String systemId) { this.systemId = systemId; return this; } - public Builder systemName(String systemName) { this.systemName = systemName; return this; } - public Builder useCase(AISystemUseCase useCase) { this.useCase = useCase; return this; } - public Builder ownerTeam(String ownerTeam) { this.ownerTeam = ownerTeam; return this; } - public Builder customerImpact(int customerImpact) { this.customerImpact = customerImpact; return this; } - public Builder modelComplexity(int modelComplexity) { this.modelComplexity = modelComplexity; return this; } - public Builder humanReliance(int humanReliance) { this.humanReliance = humanReliance; return this; } - public Builder description(String description) { this.description = description; return this; } - public Builder technicalOwner(String technicalOwner) { this.technicalOwner = technicalOwner; return this; } - public Builder businessOwner(String businessOwner) { this.businessOwner = businessOwner; return this; } - public Builder metadata(Map metadata) { this.metadata = metadata; return this; } - - public RegisterSystemRequest build() { - return new RegisterSystemRequest(this); - } - } + public void setDescription(String description) { + this.description = description; } - /** AI system registry entry. */ - public static class AISystemRegistry { - private String id; - private String orgId; - private String systemId; - private String systemName; - private AISystemUseCase useCase; - private String ownerTeam; - private int customerImpact; - private int modelComplexity; - private int humanReliance; - @com.fasterxml.jackson.annotation.JsonProperty("materiality_classification") - private MaterialityClassification materialityClassification; - private SystemStatus status; - private Instant createdAt; - private Instant updatedAt; - private String description; - private String technicalOwner; - private String businessOwner; - private Map metadata; - private String createdBy; - - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getOrgId() { return orgId; } - public void setOrgId(String orgId) { this.orgId = orgId; } - public String getSystemId() { return systemId; } - public void setSystemId(String systemId) { this.systemId = systemId; } - public String getSystemName() { return systemName; } - public void setSystemName(String systemName) { this.systemName = systemName; } - public AISystemUseCase getUseCase() { return useCase; } - public void setUseCase(AISystemUseCase useCase) { this.useCase = useCase; } - public String getOwnerTeam() { return ownerTeam; } - public void setOwnerTeam(String ownerTeam) { this.ownerTeam = ownerTeam; } - public int getCustomerImpact() { return customerImpact; } - public void setCustomerImpact(int customerImpact) { this.customerImpact = customerImpact; } - public int getModelComplexity() { return modelComplexity; } - public void setModelComplexity(int modelComplexity) { this.modelComplexity = modelComplexity; } - public int getHumanReliance() { return humanReliance; } - public void setHumanReliance(int humanReliance) { this.humanReliance = humanReliance; } - public MaterialityClassification getMaterialityClassification() { return materialityClassification; } - public void setMaterialityClassification(MaterialityClassification materialityClassification) { this.materialityClassification = materialityClassification; } - public SystemStatus getStatus() { return status; } - public void setStatus(SystemStatus status) { this.status = status; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - public String getTechnicalOwner() { return technicalOwner; } - public void setTechnicalOwner(String technicalOwner) { this.technicalOwner = technicalOwner; } - public String getBusinessOwner() { return businessOwner; } - public void setBusinessOwner(String businessOwner) { this.businessOwner = businessOwner; } - public Map getMetadata() { return metadata; } - public void setMetadata(Map metadata) { this.metadata = metadata; } - public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } - } - - /** Registry summary statistics. */ - public static class RegistrySummary { - private int totalSystems; - private int activeSystems; - private int highMaterialityCount; - private int mediumMaterialityCount; - private int lowMaterialityCount; - private Map byUseCase; - private Map byStatus; - - public int getTotalSystems() { return totalSystems; } - public void setTotalSystems(int totalSystems) { this.totalSystems = totalSystems; } - public int getActiveSystems() { return activeSystems; } - public void setActiveSystems(int activeSystems) { this.activeSystems = activeSystems; } - public int getHighMaterialityCount() { return highMaterialityCount; } - public void setHighMaterialityCount(int highMaterialityCount) { this.highMaterialityCount = highMaterialityCount; } - public int getMediumMaterialityCount() { return mediumMaterialityCount; } - public void setMediumMaterialityCount(int mediumMaterialityCount) { this.mediumMaterialityCount = mediumMaterialityCount; } - public int getLowMaterialityCount() { return lowMaterialityCount; } - public void setLowMaterialityCount(int lowMaterialityCount) { this.lowMaterialityCount = lowMaterialityCount; } - public Map getByUseCase() { return byUseCase; } - public void setByUseCase(Map byUseCase) { this.byUseCase = byUseCase; } - public Map getByStatus() { return byStatus; } - public void setByStatus(Map byStatus) { this.byStatus = byStatus; } - } - - // ========================================================================= - // FEAT Assessment Types - // ========================================================================= - - /** Request to create a FEAT assessment. */ - public static class CreateAssessmentRequest { - private final String systemId; - private String assessmentType = "initial"; - private List assessors; - - private CreateAssessmentRequest(Builder builder) { - this.systemId = builder.systemId; - this.assessmentType = builder.assessmentType; - this.assessors = builder.assessors; - } + public FindingStatus getStatus() { + return status; + } - public static Builder builder() { - return new Builder(); - } + public void setStatus(FindingStatus status) { + this.status = status; + } - public String getSystemId() { return systemId; } - public String getAssessmentType() { return assessmentType; } - public List getAssessors() { return assessors; } + public String getRemediation() { + return remediation; + } - public static class Builder { - private String systemId; - private String assessmentType = "initial"; - private List assessors; + public void setRemediation(String remediation) { + this.remediation = remediation; + } - public Builder systemId(String systemId) { this.systemId = systemId; return this; } - public Builder assessmentType(String assessmentType) { this.assessmentType = assessmentType; return this; } - public Builder assessors(List assessors) { this.assessors = assessors; return this; } + public Instant getDueDate() { + return dueDate; + } - public CreateAssessmentRequest build() { - return new CreateAssessmentRequest(this); - } - } + public void setDueDate(Instant dueDate) { + this.dueDate = dueDate; } - /** Request to update a FEAT assessment. */ - public static class UpdateAssessmentRequest { - private Integer fairnessScore; - private Integer ethicsScore; - private Integer accountabilityScore; - private Integer transparencyScore; - private Map fairnessDetails; - private Map ethicsDetails; - private Map accountabilityDetails; - private Map transparencyDetails; - private List findings; - private List recommendations; - private List assessors; - - private UpdateAssessmentRequest(Builder builder) { - this.fairnessScore = builder.fairnessScore; - this.ethicsScore = builder.ethicsScore; - this.accountabilityScore = builder.accountabilityScore; - this.transparencyScore = builder.transparencyScore; - this.fairnessDetails = builder.fairnessDetails; - this.ethicsDetails = builder.ethicsDetails; - this.accountabilityDetails = builder.accountabilityDetails; - this.transparencyDetails = builder.transparencyDetails; - this.findings = builder.findings; - this.recommendations = builder.recommendations; - this.assessors = builder.assessors; - } + public static class Builder { + private String id; + private FEATPillar pillar; + private FindingSeverity severity; + private String category; + private String description; + private FindingStatus status; + private String remediation; + private Instant dueDate; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder pillar(FEATPillar pillar) { + this.pillar = pillar; + return this; + } + + public Builder severity(FindingSeverity severity) { + this.severity = severity; + return this; + } + + public Builder category(String category) { + this.category = category; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder status(FindingStatus status) { + this.status = status; + return this; + } + + public Builder remediation(String remediation) { + this.remediation = remediation; + return this; + } + + public Builder dueDate(Instant dueDate) { + this.dueDate = dueDate; + return this; + } + + public Finding build() { + return new Finding(this); + } + } + } + + // ========================================================================= + // AI System Registry Types + // ========================================================================= + + /** Request to register an AI system. */ + public static class RegisterSystemRequest { + private final String systemId; + private final String systemName; + private final AISystemUseCase useCase; + private final String ownerTeam; + private final int customerImpact; + private final int modelComplexity; + private final int humanReliance; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + + private RegisterSystemRequest(Builder builder) { + this.systemId = builder.systemId; + this.systemName = builder.systemName; + this.useCase = builder.useCase; + this.ownerTeam = builder.ownerTeam; + this.customerImpact = builder.customerImpact; + this.modelComplexity = builder.modelComplexity; + this.humanReliance = builder.humanReliance; + this.description = builder.description; + this.technicalOwner = builder.technicalOwner; + this.businessOwner = builder.businessOwner; + this.metadata = builder.metadata; + } - public static Builder builder() { - return new Builder(); - } + public static Builder builder() { + return new Builder(); + } - public Integer getFairnessScore() { return fairnessScore; } - public Integer getEthicsScore() { return ethicsScore; } - public Integer getAccountabilityScore() { return accountabilityScore; } - public Integer getTransparencyScore() { return transparencyScore; } - public Map getFairnessDetails() { return fairnessDetails; } - public Map getEthicsDetails() { return ethicsDetails; } - public Map getAccountabilityDetails() { return accountabilityDetails; } - public Map getTransparencyDetails() { return transparencyDetails; } - public List getFindings() { return findings; } - public List getRecommendations() { return recommendations; } - public List getAssessors() { return assessors; } - - public static class Builder { - private Integer fairnessScore; - private Integer ethicsScore; - private Integer accountabilityScore; - private Integer transparencyScore; - private Map fairnessDetails; - private Map ethicsDetails; - private Map accountabilityDetails; - private Map transparencyDetails; - private List findings; - private List recommendations; - private List assessors; - - public Builder fairnessScore(int score) { this.fairnessScore = score; return this; } - public Builder ethicsScore(int score) { this.ethicsScore = score; return this; } - public Builder accountabilityScore(int score) { this.accountabilityScore = score; return this; } - public Builder transparencyScore(int score) { this.transparencyScore = score; return this; } - public Builder fairnessDetails(Map details) { this.fairnessDetails = details; return this; } - public Builder ethicsDetails(Map details) { this.ethicsDetails = details; return this; } - public Builder accountabilityDetails(Map details) { this.accountabilityDetails = details; return this; } - public Builder transparencyDetails(Map details) { this.transparencyDetails = details; return this; } - public Builder findings(List findings) { this.findings = findings; return this; } - public Builder recommendations(List recommendations) { this.recommendations = recommendations; return this; } - public Builder assessors(List assessors) { this.assessors = assessors; return this; } - - public UpdateAssessmentRequest build() { - return new UpdateAssessmentRequest(this); - } - } + public String getSystemId() { + return systemId; } - /** FEAT assessment record. */ - public static class FEATAssessment { - private String id; - private String orgId; - private String systemId; - private String assessmentType; - private FEATAssessmentStatus status; - private Instant assessmentDate; - private Instant validUntil; - private Integer fairnessScore; - private Integer ethicsScore; - private Integer accountabilityScore; - private Integer transparencyScore; - private Integer overallScore; - private Map fairnessDetails; - private Map ethicsDetails; - private Map accountabilityDetails; - private Map transparencyDetails; - private List findings; - private List recommendations; - private List assessors; - private String approvedBy; - private Instant approvedAt; - private Instant createdAt; - private Instant updatedAt; - private String createdBy; - - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getOrgId() { return orgId; } - public void setOrgId(String orgId) { this.orgId = orgId; } - public String getSystemId() { return systemId; } - public void setSystemId(String systemId) { this.systemId = systemId; } - public String getAssessmentType() { return assessmentType; } - public void setAssessmentType(String assessmentType) { this.assessmentType = assessmentType; } - public FEATAssessmentStatus getStatus() { return status; } - public void setStatus(FEATAssessmentStatus status) { this.status = status; } - public Instant getAssessmentDate() { return assessmentDate; } - public void setAssessmentDate(Instant assessmentDate) { this.assessmentDate = assessmentDate; } - public Instant getValidUntil() { return validUntil; } - public void setValidUntil(Instant validUntil) { this.validUntil = validUntil; } - public Integer getFairnessScore() { return fairnessScore; } - public void setFairnessScore(Integer fairnessScore) { this.fairnessScore = fairnessScore; } - public Integer getEthicsScore() { return ethicsScore; } - public void setEthicsScore(Integer ethicsScore) { this.ethicsScore = ethicsScore; } - public Integer getAccountabilityScore() { return accountabilityScore; } - public void setAccountabilityScore(Integer accountabilityScore) { this.accountabilityScore = accountabilityScore; } - public Integer getTransparencyScore() { return transparencyScore; } - public void setTransparencyScore(Integer transparencyScore) { this.transparencyScore = transparencyScore; } - public Integer getOverallScore() { return overallScore; } - public void setOverallScore(Integer overallScore) { this.overallScore = overallScore; } - public Map getFairnessDetails() { return fairnessDetails; } - public void setFairnessDetails(Map fairnessDetails) { this.fairnessDetails = fairnessDetails; } - public Map getEthicsDetails() { return ethicsDetails; } - public void setEthicsDetails(Map ethicsDetails) { this.ethicsDetails = ethicsDetails; } - public Map getAccountabilityDetails() { return accountabilityDetails; } - public void setAccountabilityDetails(Map accountabilityDetails) { this.accountabilityDetails = accountabilityDetails; } - public Map getTransparencyDetails() { return transparencyDetails; } - public void setTransparencyDetails(Map transparencyDetails) { this.transparencyDetails = transparencyDetails; } - public List getFindings() { return findings; } - public void setFindings(List findings) { this.findings = findings; } - public List getRecommendations() { return recommendations; } - public void setRecommendations(List recommendations) { this.recommendations = recommendations; } - public List getAssessors() { return assessors; } - public void setAssessors(List assessors) { this.assessors = assessors; } - public String getApprovedBy() { return approvedBy; } - public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; } - public Instant getApprovedAt() { return approvedAt; } - public void setApprovedAt(Instant approvedAt) { this.approvedAt = approvedAt; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } - public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } - } - - /** Request to approve an assessment. */ - public static class ApproveAssessmentRequest { - private final String approvedBy; - private String comments; - - private ApproveAssessmentRequest(Builder builder) { - this.approvedBy = builder.approvedBy; - this.comments = builder.comments; - } + public String getSystemName() { + return systemName; + } - public static Builder builder() { - return new Builder(); - } + public AISystemUseCase getUseCase() { + return useCase; + } - public String getApprovedBy() { return approvedBy; } - public String getComments() { return comments; } + public String getOwnerTeam() { + return ownerTeam; + } - public static class Builder { - private String approvedBy; - private String comments; + public int getCustomerImpact() { + return customerImpact; + } - public Builder approvedBy(String approvedBy) { this.approvedBy = approvedBy; return this; } - public Builder comments(String comments) { this.comments = comments; return this; } + public int getModelComplexity() { + return modelComplexity; + } - public ApproveAssessmentRequest build() { - return new ApproveAssessmentRequest(this); - } - } + public int getHumanReliance() { + return humanReliance; } - /** Request to reject an assessment. */ - public static class RejectAssessmentRequest { - private final String rejectedBy; - private final String reason; + public String getDescription() { + return description; + } - private RejectAssessmentRequest(Builder builder) { - this.rejectedBy = builder.rejectedBy; - this.reason = builder.reason; - } + public String getTechnicalOwner() { + return technicalOwner; + } - public static Builder builder() { - return new Builder(); - } + public String getBusinessOwner() { + return businessOwner; + } - public String getRejectedBy() { return rejectedBy; } - public String getReason() { return reason; } + public Map getMetadata() { + return metadata; + } - public static class Builder { - private String rejectedBy; - private String reason; + public static class Builder { + private String systemId; + private String systemName; + private AISystemUseCase useCase; + private String ownerTeam; + private int customerImpact; + private int modelComplexity; + private int humanReliance; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + + public Builder systemId(String systemId) { + this.systemId = systemId; + return this; + } + + public Builder systemName(String systemName) { + this.systemName = systemName; + return this; + } + + public Builder useCase(AISystemUseCase useCase) { + this.useCase = useCase; + return this; + } + + public Builder ownerTeam(String ownerTeam) { + this.ownerTeam = ownerTeam; + return this; + } + + public Builder customerImpact(int customerImpact) { + this.customerImpact = customerImpact; + return this; + } + + public Builder modelComplexity(int modelComplexity) { + this.modelComplexity = modelComplexity; + return this; + } + + public Builder humanReliance(int humanReliance) { + this.humanReliance = humanReliance; + return this; + } + + public Builder description(String description) { + this.description = description; + return this; + } + + public Builder technicalOwner(String technicalOwner) { + this.technicalOwner = technicalOwner; + return this; + } + + public Builder businessOwner(String businessOwner) { + this.businessOwner = businessOwner; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public RegisterSystemRequest build() { + return new RegisterSystemRequest(this); + } + } + } + + /** AI system registry entry. */ + public static class AISystemRegistry { + private String id; + private String orgId; + private String systemId; + private String systemName; + private AISystemUseCase useCase; + private String ownerTeam; + private int customerImpact; + private int modelComplexity; + private int humanReliance; + + @com.fasterxml.jackson.annotation.JsonProperty("materiality_classification") + private MaterialityClassification materialityClassification; + + private SystemStatus status; + private Instant createdAt; + private Instant updatedAt; + private String description; + private String technicalOwner; + private String businessOwner; + private Map metadata; + private String createdBy; + + public String getId() { + return id; + } - public Builder rejectedBy(String rejectedBy) { this.rejectedBy = rejectedBy; return this; } - public Builder reason(String reason) { this.reason = reason; return this; } + public void setId(String id) { + this.id = id; + } - public RejectAssessmentRequest build() { - return new RejectAssessmentRequest(this); - } - } + public String getOrgId() { + return orgId; } - // ========================================================================= - // Kill Switch Types - // ========================================================================= - - /** Kill switch configuration. */ - public static class KillSwitch { - private String id; - private String orgId; - private String systemId; - private KillSwitchStatus status; - private boolean autoTriggerEnabled; - private Double accuracyThreshold; - private Double biasThreshold; - private Double errorRateThreshold; - private Instant triggeredAt; - private String triggeredBy; - private String triggeredReason; - private Instant restoredAt; - private String restoredBy; - private Instant createdAt; - private Instant updatedAt; - - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getOrgId() { return orgId; } - public void setOrgId(String orgId) { this.orgId = orgId; } - public String getSystemId() { return systemId; } - public void setSystemId(String systemId) { this.systemId = systemId; } - public KillSwitchStatus getStatus() { return status; } - public void setStatus(KillSwitchStatus status) { this.status = status; } - public boolean isAutoTriggerEnabled() { return autoTriggerEnabled; } - public void setAutoTriggerEnabled(boolean autoTriggerEnabled) { this.autoTriggerEnabled = autoTriggerEnabled; } - public Double getAccuracyThreshold() { return accuracyThreshold; } - public void setAccuracyThreshold(Double accuracyThreshold) { this.accuracyThreshold = accuracyThreshold; } - public Double getBiasThreshold() { return biasThreshold; } - public void setBiasThreshold(Double biasThreshold) { this.biasThreshold = biasThreshold; } - public Double getErrorRateThreshold() { return errorRateThreshold; } - public void setErrorRateThreshold(Double errorRateThreshold) { this.errorRateThreshold = errorRateThreshold; } - public Instant getTriggeredAt() { return triggeredAt; } - public void setTriggeredAt(Instant triggeredAt) { this.triggeredAt = triggeredAt; } - public String getTriggeredBy() { return triggeredBy; } - public void setTriggeredBy(String triggeredBy) { this.triggeredBy = triggeredBy; } - public String getTriggeredReason() { return triggeredReason; } - public void setTriggeredReason(String triggeredReason) { this.triggeredReason = triggeredReason; } - public Instant getRestoredAt() { return restoredAt; } - public void setRestoredAt(Instant restoredAt) { this.restoredAt = restoredAt; } - public String getRestoredBy() { return restoredBy; } - public void setRestoredBy(String restoredBy) { this.restoredBy = restoredBy; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } - } - - /** Request to configure a kill switch. */ - public static class ConfigureKillSwitchRequest { - private Double accuracyThreshold; - private Double biasThreshold; - private Double errorRateThreshold; - private Boolean autoTriggerEnabled; - - private ConfigureKillSwitchRequest(Builder builder) { - this.accuracyThreshold = builder.accuracyThreshold; - this.biasThreshold = builder.biasThreshold; - this.errorRateThreshold = builder.errorRateThreshold; - this.autoTriggerEnabled = builder.autoTriggerEnabled; - } + public void setOrgId(String orgId) { + this.orgId = orgId; + } - public static Builder builder() { - return new Builder(); - } + public String getSystemId() { + return systemId; + } - public Double getAccuracyThreshold() { return accuracyThreshold; } - public Double getBiasThreshold() { return biasThreshold; } - public Double getErrorRateThreshold() { return errorRateThreshold; } - public Boolean getAutoTriggerEnabled() { return autoTriggerEnabled; } - - public static class Builder { - private Double accuracyThreshold; - private Double biasThreshold; - private Double errorRateThreshold; - private Boolean autoTriggerEnabled; - - public Builder accuracyThreshold(double threshold) { this.accuracyThreshold = threshold; return this; } - public Builder biasThreshold(double threshold) { this.biasThreshold = threshold; return this; } - public Builder errorRateThreshold(double threshold) { this.errorRateThreshold = threshold; return this; } - public Builder autoTriggerEnabled(boolean enabled) { this.autoTriggerEnabled = enabled; return this; } - - public ConfigureKillSwitchRequest build() { - return new ConfigureKillSwitchRequest(this); - } - } + public void setSystemId(String systemId) { + this.systemId = systemId; } - /** Request to check kill switch metrics. */ - public static class CheckKillSwitchRequest { - private final double accuracy; - private Double biasScore; - private Double errorRate; + public String getSystemName() { + return systemName; + } - private CheckKillSwitchRequest(Builder builder) { - this.accuracy = builder.accuracy; - this.biasScore = builder.biasScore; - this.errorRate = builder.errorRate; - } + public void setSystemName(String systemName) { + this.systemName = systemName; + } - public static Builder builder() { - return new Builder(); - } + public AISystemUseCase getUseCase() { + return useCase; + } - public double getAccuracy() { return accuracy; } - public Double getBiasScore() { return biasScore; } - public Double getErrorRate() { return errorRate; } + public void setUseCase(AISystemUseCase useCase) { + this.useCase = useCase; + } - public static class Builder { - private double accuracy; - private Double biasScore; - private Double errorRate; + public String getOwnerTeam() { + return ownerTeam; + } - public Builder accuracy(double accuracy) { this.accuracy = accuracy; return this; } - public Builder biasScore(double biasScore) { this.biasScore = biasScore; return this; } - public Builder errorRate(double errorRate) { this.errorRate = errorRate; return this; } + public void setOwnerTeam(String ownerTeam) { + this.ownerTeam = ownerTeam; + } - public CheckKillSwitchRequest build() { - return new CheckKillSwitchRequest(this); - } - } + public int getCustomerImpact() { + return customerImpact; } - /** Request to trigger a kill switch. */ - public static class TriggerKillSwitchRequest { - private final String reason; - private String triggeredBy; + public void setCustomerImpact(int customerImpact) { + this.customerImpact = customerImpact; + } - private TriggerKillSwitchRequest(Builder builder) { - this.reason = builder.reason; - this.triggeredBy = builder.triggeredBy; - } + public int getModelComplexity() { + return modelComplexity; + } - public static Builder builder() { - return new Builder(); - } + public void setModelComplexity(int modelComplexity) { + this.modelComplexity = modelComplexity; + } - public String getReason() { return reason; } - public String getTriggeredBy() { return triggeredBy; } + public int getHumanReliance() { + return humanReliance; + } - public static class Builder { - private String reason; - private String triggeredBy; + public void setHumanReliance(int humanReliance) { + this.humanReliance = humanReliance; + } - public Builder reason(String reason) { this.reason = reason; return this; } - public Builder triggeredBy(String triggeredBy) { this.triggeredBy = triggeredBy; return this; } + public MaterialityClassification getMaterialityClassification() { + return materialityClassification; + } - public TriggerKillSwitchRequest build() { - return new TriggerKillSwitchRequest(this); - } - } + public void setMaterialityClassification(MaterialityClassification materialityClassification) { + this.materialityClassification = materialityClassification; } - /** Request to restore a kill switch. */ - public static class RestoreKillSwitchRequest { - private final String reason; - private String restoredBy; + public SystemStatus getStatus() { + return status; + } - private RestoreKillSwitchRequest(Builder builder) { - this.reason = builder.reason; - this.restoredBy = builder.restoredBy; - } + public void setStatus(SystemStatus status) { + this.status = status; + } - public static Builder builder() { - return new Builder(); - } + public Instant getCreatedAt() { + return createdAt; + } - public String getReason() { return reason; } - public String getRestoredBy() { return restoredBy; } + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } - public static class Builder { - private String reason; - private String restoredBy; + public Instant getUpdatedAt() { + return updatedAt; + } - public Builder reason(String reason) { this.reason = reason; return this; } - public Builder restoredBy(String restoredBy) { this.restoredBy = restoredBy; return this; } + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } - public RestoreKillSwitchRequest build() { - return new RestoreKillSwitchRequest(this); - } - } + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getTechnicalOwner() { + return technicalOwner; + } + + public void setTechnicalOwner(String technicalOwner) { + this.technicalOwner = technicalOwner; + } + + public String getBusinessOwner() { + return businessOwner; + } + + public void setBusinessOwner(String businessOwner) { + this.businessOwner = businessOwner; + } + + public Map getMetadata() { + return metadata; + } + + public void setMetadata(Map metadata) { + this.metadata = metadata; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + /** Registry summary statistics. */ + public static class RegistrySummary { + private int totalSystems; + private int activeSystems; + private int highMaterialityCount; + private int mediumMaterialityCount; + private int lowMaterialityCount; + private Map byUseCase; + private Map byStatus; + + public int getTotalSystems() { + return totalSystems; + } + + public void setTotalSystems(int totalSystems) { + this.totalSystems = totalSystems; + } + + public int getActiveSystems() { + return activeSystems; + } + + public void setActiveSystems(int activeSystems) { + this.activeSystems = activeSystems; + } + + public int getHighMaterialityCount() { + return highMaterialityCount; + } + + public void setHighMaterialityCount(int highMaterialityCount) { + this.highMaterialityCount = highMaterialityCount; + } + + public int getMediumMaterialityCount() { + return mediumMaterialityCount; + } + + public void setMediumMaterialityCount(int mediumMaterialityCount) { + this.mediumMaterialityCount = mediumMaterialityCount; + } + + public int getLowMaterialityCount() { + return lowMaterialityCount; + } + + public void setLowMaterialityCount(int lowMaterialityCount) { + this.lowMaterialityCount = lowMaterialityCount; + } + + public Map getByUseCase() { + return byUseCase; + } + + public void setByUseCase(Map byUseCase) { + this.byUseCase = byUseCase; + } + + public Map getByStatus() { + return byStatus; + } + + public void setByStatus(Map byStatus) { + this.byStatus = byStatus; + } + } + + // ========================================================================= + // FEAT Assessment Types + // ========================================================================= + + /** Request to create a FEAT assessment. */ + public static class CreateAssessmentRequest { + private final String systemId; + private String assessmentType = "initial"; + private List assessors; + + private CreateAssessmentRequest(Builder builder) { + this.systemId = builder.systemId; + this.assessmentType = builder.assessmentType; + this.assessors = builder.assessors; + } + + public static Builder builder() { + return new Builder(); + } + + public String getSystemId() { + return systemId; + } + + public String getAssessmentType() { + return assessmentType; + } + + public List getAssessors() { + return assessors; + } + + public static class Builder { + private String systemId; + private String assessmentType = "initial"; + private List assessors; + + public Builder systemId(String systemId) { + this.systemId = systemId; + return this; + } + + public Builder assessmentType(String assessmentType) { + this.assessmentType = assessmentType; + return this; + } + + public Builder assessors(List assessors) { + this.assessors = assessors; + return this; + } + + public CreateAssessmentRequest build() { + return new CreateAssessmentRequest(this); + } + } + } + + /** Request to update a FEAT assessment. */ + public static class UpdateAssessmentRequest { + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + + private UpdateAssessmentRequest(Builder builder) { + this.fairnessScore = builder.fairnessScore; + this.ethicsScore = builder.ethicsScore; + this.accountabilityScore = builder.accountabilityScore; + this.transparencyScore = builder.transparencyScore; + this.fairnessDetails = builder.fairnessDetails; + this.ethicsDetails = builder.ethicsDetails; + this.accountabilityDetails = builder.accountabilityDetails; + this.transparencyDetails = builder.transparencyDetails; + this.findings = builder.findings; + this.recommendations = builder.recommendations; + this.assessors = builder.assessors; + } + + public static Builder builder() { + return new Builder(); + } + + public Integer getFairnessScore() { + return fairnessScore; + } + + public Integer getEthicsScore() { + return ethicsScore; + } + + public Integer getAccountabilityScore() { + return accountabilityScore; + } + + public Integer getTransparencyScore() { + return transparencyScore; + } + + public Map getFairnessDetails() { + return fairnessDetails; + } + + public Map getEthicsDetails() { + return ethicsDetails; + } + + public Map getAccountabilityDetails() { + return accountabilityDetails; + } + + public Map getTransparencyDetails() { + return transparencyDetails; + } + + public List getFindings() { + return findings; + } + + public List getRecommendations() { + return recommendations; + } + + public List getAssessors() { + return assessors; + } + + public static class Builder { + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + + public Builder fairnessScore(int score) { + this.fairnessScore = score; + return this; + } + + public Builder ethicsScore(int score) { + this.ethicsScore = score; + return this; + } + + public Builder accountabilityScore(int score) { + this.accountabilityScore = score; + return this; + } + + public Builder transparencyScore(int score) { + this.transparencyScore = score; + return this; + } + + public Builder fairnessDetails(Map details) { + this.fairnessDetails = details; + return this; + } + + public Builder ethicsDetails(Map details) { + this.ethicsDetails = details; + return this; + } + + public Builder accountabilityDetails(Map details) { + this.accountabilityDetails = details; + return this; + } + + public Builder transparencyDetails(Map details) { + this.transparencyDetails = details; + return this; + } + + public Builder findings(List findings) { + this.findings = findings; + return this; + } + + public Builder recommendations(List recommendations) { + this.recommendations = recommendations; + return this; + } + + public Builder assessors(List assessors) { + this.assessors = assessors; + return this; + } + + public UpdateAssessmentRequest build() { + return new UpdateAssessmentRequest(this); + } + } + } + + /** FEAT assessment record. */ + public static class FEATAssessment { + private String id; + private String orgId; + private String systemId; + private String assessmentType; + private FEATAssessmentStatus status; + private Instant assessmentDate; + private Instant validUntil; + private Integer fairnessScore; + private Integer ethicsScore; + private Integer accountabilityScore; + private Integer transparencyScore; + private Integer overallScore; + private Map fairnessDetails; + private Map ethicsDetails; + private Map accountabilityDetails; + private Map transparencyDetails; + private List findings; + private List recommendations; + private List assessors; + private String approvedBy; + private Instant approvedAt; + private Instant createdAt; + private Instant updatedAt; + private String createdBy; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getSystemId() { + return systemId; + } + + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + public String getAssessmentType() { + return assessmentType; + } + + public void setAssessmentType(String assessmentType) { + this.assessmentType = assessmentType; + } + + public FEATAssessmentStatus getStatus() { + return status; + } + + public void setStatus(FEATAssessmentStatus status) { + this.status = status; + } + + public Instant getAssessmentDate() { + return assessmentDate; + } + + public void setAssessmentDate(Instant assessmentDate) { + this.assessmentDate = assessmentDate; + } + + public Instant getValidUntil() { + return validUntil; + } + + public void setValidUntil(Instant validUntil) { + this.validUntil = validUntil; + } + + public Integer getFairnessScore() { + return fairnessScore; + } + + public void setFairnessScore(Integer fairnessScore) { + this.fairnessScore = fairnessScore; + } + + public Integer getEthicsScore() { + return ethicsScore; + } + + public void setEthicsScore(Integer ethicsScore) { + this.ethicsScore = ethicsScore; + } + + public Integer getAccountabilityScore() { + return accountabilityScore; + } + + public void setAccountabilityScore(Integer accountabilityScore) { + this.accountabilityScore = accountabilityScore; + } + + public Integer getTransparencyScore() { + return transparencyScore; + } + + public void setTransparencyScore(Integer transparencyScore) { + this.transparencyScore = transparencyScore; + } + + public Integer getOverallScore() { + return overallScore; + } + + public void setOverallScore(Integer overallScore) { + this.overallScore = overallScore; + } + + public Map getFairnessDetails() { + return fairnessDetails; + } + + public void setFairnessDetails(Map fairnessDetails) { + this.fairnessDetails = fairnessDetails; + } + + public Map getEthicsDetails() { + return ethicsDetails; + } + + public void setEthicsDetails(Map ethicsDetails) { + this.ethicsDetails = ethicsDetails; + } + + public Map getAccountabilityDetails() { + return accountabilityDetails; + } + + public void setAccountabilityDetails(Map accountabilityDetails) { + this.accountabilityDetails = accountabilityDetails; + } + + public Map getTransparencyDetails() { + return transparencyDetails; + } + + public void setTransparencyDetails(Map transparencyDetails) { + this.transparencyDetails = transparencyDetails; + } + + public List getFindings() { + return findings; + } + + public void setFindings(List findings) { + this.findings = findings; + } + + public List getRecommendations() { + return recommendations; + } + + public void setRecommendations(List recommendations) { + this.recommendations = recommendations; + } + + public List getAssessors() { + return assessors; + } + + public void setAssessors(List assessors) { + this.assessors = assessors; + } + + public String getApprovedBy() { + return approvedBy; + } + + public void setApprovedBy(String approvedBy) { + this.approvedBy = approvedBy; + } + + public Instant getApprovedAt() { + return approvedAt; + } + + public void setApprovedAt(Instant approvedAt) { + this.approvedAt = approvedAt; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + } + + /** Request to approve an assessment. */ + public static class ApproveAssessmentRequest { + private final String approvedBy; + private String comments; + + private ApproveAssessmentRequest(Builder builder) { + this.approvedBy = builder.approvedBy; + this.comments = builder.comments; + } + + public static Builder builder() { + return new Builder(); + } + + public String getApprovedBy() { + return approvedBy; + } + + public String getComments() { + return comments; + } + + public static class Builder { + private String approvedBy; + private String comments; + + public Builder approvedBy(String approvedBy) { + this.approvedBy = approvedBy; + return this; + } + + public Builder comments(String comments) { + this.comments = comments; + return this; + } + + public ApproveAssessmentRequest build() { + return new ApproveAssessmentRequest(this); + } + } + } + + /** Request to reject an assessment. */ + public static class RejectAssessmentRequest { + private final String rejectedBy; + private final String reason; + + private RejectAssessmentRequest(Builder builder) { + this.rejectedBy = builder.rejectedBy; + this.reason = builder.reason; + } + + public static Builder builder() { + return new Builder(); + } + + public String getRejectedBy() { + return rejectedBy; + } + + public String getReason() { + return reason; + } + + public static class Builder { + private String rejectedBy; + private String reason; + + public Builder rejectedBy(String rejectedBy) { + this.rejectedBy = rejectedBy; + return this; + } + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public RejectAssessmentRequest build() { + return new RejectAssessmentRequest(this); + } + } + } + + // ========================================================================= + // Kill Switch Types + // ========================================================================= + + /** Kill switch configuration. */ + public static class KillSwitch { + private String id; + private String orgId; + private String systemId; + private KillSwitchStatus status; + private boolean autoTriggerEnabled; + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Instant triggeredAt; + private String triggeredBy; + private String triggeredReason; + private Instant restoredAt; + private String restoredBy; + private Instant createdAt; + private Instant updatedAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getSystemId() { + return systemId; + } + + public void setSystemId(String systemId) { + this.systemId = systemId; + } + + public KillSwitchStatus getStatus() { + return status; + } + + public void setStatus(KillSwitchStatus status) { + this.status = status; + } + + public boolean isAutoTriggerEnabled() { + return autoTriggerEnabled; + } + + public void setAutoTriggerEnabled(boolean autoTriggerEnabled) { + this.autoTriggerEnabled = autoTriggerEnabled; + } + + public Double getAccuracyThreshold() { + return accuracyThreshold; + } + + public void setAccuracyThreshold(Double accuracyThreshold) { + this.accuracyThreshold = accuracyThreshold; + } + + public Double getBiasThreshold() { + return biasThreshold; + } + + public void setBiasThreshold(Double biasThreshold) { + this.biasThreshold = biasThreshold; + } + + public Double getErrorRateThreshold() { + return errorRateThreshold; + } + + public void setErrorRateThreshold(Double errorRateThreshold) { + this.errorRateThreshold = errorRateThreshold; + } + + public Instant getTriggeredAt() { + return triggeredAt; + } + + public void setTriggeredAt(Instant triggeredAt) { + this.triggeredAt = triggeredAt; + } + + public String getTriggeredBy() { + return triggeredBy; + } + + public void setTriggeredBy(String triggeredBy) { + this.triggeredBy = triggeredBy; + } + + public String getTriggeredReason() { + return triggeredReason; + } + + public void setTriggeredReason(String triggeredReason) { + this.triggeredReason = triggeredReason; + } + + public Instant getRestoredAt() { + return restoredAt; + } + + public void setRestoredAt(Instant restoredAt) { + this.restoredAt = restoredAt; + } + + public String getRestoredBy() { + return restoredBy; + } + + public void setRestoredBy(String restoredBy) { + this.restoredBy = restoredBy; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + } + + /** Request to configure a kill switch. */ + public static class ConfigureKillSwitchRequest { + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Boolean autoTriggerEnabled; + + private ConfigureKillSwitchRequest(Builder builder) { + this.accuracyThreshold = builder.accuracyThreshold; + this.biasThreshold = builder.biasThreshold; + this.errorRateThreshold = builder.errorRateThreshold; + this.autoTriggerEnabled = builder.autoTriggerEnabled; + } + + public static Builder builder() { + return new Builder(); + } + + public Double getAccuracyThreshold() { + return accuracyThreshold; + } + + public Double getBiasThreshold() { + return biasThreshold; + } + + public Double getErrorRateThreshold() { + return errorRateThreshold; + } + + public Boolean getAutoTriggerEnabled() { + return autoTriggerEnabled; + } + + public static class Builder { + private Double accuracyThreshold; + private Double biasThreshold; + private Double errorRateThreshold; + private Boolean autoTriggerEnabled; + + public Builder accuracyThreshold(double threshold) { + this.accuracyThreshold = threshold; + return this; + } + + public Builder biasThreshold(double threshold) { + this.biasThreshold = threshold; + return this; + } + + public Builder errorRateThreshold(double threshold) { + this.errorRateThreshold = threshold; + return this; + } + + public Builder autoTriggerEnabled(boolean enabled) { + this.autoTriggerEnabled = enabled; + return this; + } + + public ConfigureKillSwitchRequest build() { + return new ConfigureKillSwitchRequest(this); + } + } + } + + /** Request to check kill switch metrics. */ + public static class CheckKillSwitchRequest { + private final double accuracy; + private Double biasScore; + private Double errorRate; + + private CheckKillSwitchRequest(Builder builder) { + this.accuracy = builder.accuracy; + this.biasScore = builder.biasScore; + this.errorRate = builder.errorRate; + } + + public static Builder builder() { + return new Builder(); + } + + public double getAccuracy() { + return accuracy; + } + + public Double getBiasScore() { + return biasScore; + } + + public Double getErrorRate() { + return errorRate; + } + + public static class Builder { + private double accuracy; + private Double biasScore; + private Double errorRate; + + public Builder accuracy(double accuracy) { + this.accuracy = accuracy; + return this; + } + + public Builder biasScore(double biasScore) { + this.biasScore = biasScore; + return this; + } + + public Builder errorRate(double errorRate) { + this.errorRate = errorRate; + return this; + } + + public CheckKillSwitchRequest build() { + return new CheckKillSwitchRequest(this); + } + } + } + + /** Request to trigger a kill switch. */ + public static class TriggerKillSwitchRequest { + private final String reason; + private String triggeredBy; + + private TriggerKillSwitchRequest(Builder builder) { + this.reason = builder.reason; + this.triggeredBy = builder.triggeredBy; + } + + public static Builder builder() { + return new Builder(); + } + + public String getReason() { + return reason; + } + + public String getTriggeredBy() { + return triggeredBy; + } + + public static class Builder { + private String reason; + private String triggeredBy; + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public Builder triggeredBy(String triggeredBy) { + this.triggeredBy = triggeredBy; + return this; + } + + public TriggerKillSwitchRequest build() { + return new TriggerKillSwitchRequest(this); + } + } + } + + /** Request to restore a kill switch. */ + public static class RestoreKillSwitchRequest { + private final String reason; + private String restoredBy; + + private RestoreKillSwitchRequest(Builder builder) { + this.reason = builder.reason; + this.restoredBy = builder.restoredBy; + } + + public static Builder builder() { + return new Builder(); + } + + public String getReason() { + return reason; + } + + public String getRestoredBy() { + return restoredBy; + } + + public static class Builder { + private String reason; + private String restoredBy; + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + + public Builder restoredBy(String restoredBy) { + this.restoredBy = restoredBy; + return this; + } + + public RestoreKillSwitchRequest build() { + return new RestoreKillSwitchRequest(this); + } + } + } + + /** Kill switch event record. */ + public static class KillSwitchEvent { + private String id; + private String killSwitchId; + private String eventType; + private Map eventData; + private String createdBy; + private Instant createdAt; + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getKillSwitchId() { + return killSwitchId; + } + + public void setKillSwitchId(String killSwitchId) { + this.killSwitchId = killSwitchId; + } + + public String getEventType() { + return eventType; + } + + public void setEventType(String eventType) { + this.eventType = eventType; + } + + public Map getEventData() { + return eventData; + } + + public void setEventData(Map eventData) { + this.eventData = eventData; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedAt() { + return createdAt; } - /** Kill switch event record. */ - public static class KillSwitchEvent { - private String id; - private String killSwitchId; - private String eventType; - private Map eventData; - private String createdBy; - private Instant createdAt; - - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getKillSwitchId() { return killSwitchId; } - public void setKillSwitchId(String killSwitchId) { this.killSwitchId = killSwitchId; } - public String getEventType() { return eventType; } - public void setEventType(String eventType) { this.eventType = eventType; } - public Map getEventData() { return eventData; } - public void setEventData(Map eventData) { this.eventData = eventData; } - public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java b/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java index d844c6f..2ba97ca 100644 --- a/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/masfeat/package-info.java @@ -7,14 +7,16 @@ *

Enterprise Feature: Requires AxonFlow Enterprise license. * *

Features

+ * *
    - *
  • AI System Registry with 3-dimensional materiality classification
  • - *
  • FEAT Assessment lifecycle management
  • - *
  • Kill Switch for emergency model shutdown
  • - *
  • 7-year audit retention
  • + *
  • AI System Registry with 3-dimensional materiality classification + *
  • FEAT Assessment lifecycle management + *
  • Kill Switch for emergency model shutdown + *
  • 7-year audit retention *
* *

Example

+ * *
{@code
  * AxonFlowClient client = AxonFlowClient.builder()
  *     .apiKey("your-api-key")
diff --git a/src/main/java/com/getaxonflow/sdk/package-info.java b/src/main/java/com/getaxonflow/sdk/package-info.java
index 06709fd..8b98bf8 100644
--- a/src/main/java/com/getaxonflow/sdk/package-info.java
+++ b/src/main/java/com/getaxonflow/sdk/package-info.java
@@ -17,11 +17,11 @@
 /**
  * AxonFlow Java SDK - AI Governance Platform for Enterprise LLM Applications.
  *
- * 

This SDK provides a Java client for interacting with the AxonFlow API, - * enabling AI governance, policy enforcement, and compliance tracking for - * LLM applications. + *

This SDK provides a Java client for interacting with the AxonFlow API, enabling AI governance, + * policy enforcement, and compliance tracking for LLM applications. * *

Quick Start

+ * *
{@code
  * // Create a client
  * AxonFlow axonflow = AxonFlow.create(AxonFlowConfig.builder()
@@ -49,11 +49,12 @@
  * }
* *

Key Classes

+ * *
    - *
  • {@link com.getaxonflow.sdk.AxonFlow} - Main client class
  • - *
  • {@link com.getaxonflow.sdk.AxonFlowConfig} - Configuration builder
  • - *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalRequest} - Gateway Mode pre-check request
  • - *
  • {@link com.getaxonflow.sdk.types.ClientRequest} - Proxy Mode query request
  • + *
  • {@link com.getaxonflow.sdk.AxonFlow} - Main client class + *
  • {@link com.getaxonflow.sdk.AxonFlowConfig} - Configuration builder + *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalRequest} - Gateway Mode pre-check request + *
  • {@link com.getaxonflow.sdk.types.ClientRequest} - Proxy Mode query request *
* * @see com.getaxonflow.sdk.AxonFlow diff --git a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportInput.java b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportInput.java index ad6ef03..aae049e 100644 --- a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportInput.java +++ b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportInput.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Map; import java.util.Objects; @@ -26,6 +25,7 @@ * A single input to test against a policy in an impact report. * *

Use the {@link Builder} to construct instances: + * *

{@code
  * ImpactReportInput input = ImpactReportInput.builder()
  *     .query("Transfer funds to external account")
@@ -37,43 +37,60 @@
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ImpactReportInput {
 
-    @JsonProperty("query")
-    private final String query;
+  @JsonProperty("query")
+  private final String query;
 
-    @JsonProperty("request_type")
-    private final String requestType;
+  @JsonProperty("request_type")
+  private final String requestType;
 
-    @JsonProperty("context")
-    private final Map context;
+  @JsonProperty("context")
+  private final Map context;
 
-    private ImpactReportInput(Builder builder) {
-        this.query = Objects.requireNonNull(builder.query, "query cannot be null");
-        this.requestType = builder.requestType;
-        this.context = builder.context;
-    }
+  private ImpactReportInput(Builder builder) {
+    this.query = Objects.requireNonNull(builder.query, "query cannot be null");
+    this.requestType = builder.requestType;
+    this.context = builder.context;
+  }
 
-    public static Builder builder() {
-        return new Builder();
-    }
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getQuery() {
+    return query;
+  }
+
+  public String getRequestType() {
+    return requestType;
+  }
 
-    public String getQuery() { return query; }
-    public String getRequestType() { return requestType; }
-    public Map getContext() { return context; }
+  public Map getContext() {
+    return context;
+  }
 
-    /**
-     * Builder for {@link ImpactReportInput}.
-     */
-    public static final class Builder {
-        private String query;
-        private String requestType;
-        private Map context;
+  /** Builder for {@link ImpactReportInput}. */
+  public static final class Builder {
+    private String query;
+    private String requestType;
+    private Map context;
 
-        public Builder query(String query) { this.query = query; return this; }
-        public Builder requestType(String requestType) { this.requestType = requestType; return this; }
-        public Builder context(Map context) { this.context = context; return this; }
+    public Builder query(String query) {
+      this.query = query;
+      return this;
+    }
+
+    public Builder requestType(String requestType) {
+      this.requestType = requestType;
+      return this;
+    }
+
+    public Builder context(Map context) {
+      this.context = context;
+      return this;
+    }
 
-        public ImpactReportInput build() {
-            return new ImpactReportInput(this);
-        }
+    public ImpactReportInput build() {
+      return new ImpactReportInput(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportRequest.java b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportRequest.java
index 7d2d900..d7b2b1f 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportRequest.java
@@ -17,7 +17,6 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 import java.util.Objects;
 
@@ -25,6 +24,7 @@
  * Request to generate a policy impact report.
  *
  * 

Use the {@link Builder} to construct instances: + * *

{@code
  * ImpactReportRequest request = ImpactReportRequest.builder()
  *     .policyId("policy_block_pii")
@@ -38,49 +38,59 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class ImpactReportRequest {
 
-    @JsonProperty("policy_id")
-    private final String policyId;
+  @JsonProperty("policy_id")
+  private final String policyId;
 
-    @JsonProperty("inputs")
-    private final List inputs;
+  @JsonProperty("inputs")
+  private final List inputs;
 
-    private ImpactReportRequest(Builder builder) {
-        this.policyId = Objects.requireNonNull(builder.policyId, "policyId cannot be null");
-        if (this.policyId.isEmpty()) {
-            throw new IllegalArgumentException("policyId cannot be empty");
-        }
-        this.inputs = Objects.requireNonNull(builder.inputs, "inputs cannot be null");
-        if (this.inputs.isEmpty()) {
-            throw new IllegalArgumentException("inputs cannot be empty");
-        }
+  private ImpactReportRequest(Builder builder) {
+    this.policyId = Objects.requireNonNull(builder.policyId, "policyId cannot be null");
+    if (this.policyId.isEmpty()) {
+      throw new IllegalArgumentException("policyId cannot be empty");
+    }
+    this.inputs = Objects.requireNonNull(builder.inputs, "inputs cannot be null");
+    if (this.inputs.isEmpty()) {
+      throw new IllegalArgumentException("inputs cannot be empty");
     }
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getPolicyId() {
+    return policyId;
+  }
 
-    public static Builder builder() {
-        return new Builder();
+  public List getInputs() {
+    return inputs;
+  }
+
+  /** Builder for {@link ImpactReportRequest}. */
+  public static final class Builder {
+    private String policyId;
+    private List inputs;
+
+    public Builder policyId(String policyId) {
+      this.policyId = policyId;
+      return this;
     }
 
-    public String getPolicyId() { return policyId; }
-    public List getInputs() { return inputs; }
+    public Builder inputs(List inputs) {
+      this.inputs = inputs;
+      return this;
+    }
 
     /**
-     * Builder for {@link ImpactReportRequest}.
+     * Builds the ImpactReportRequest.
+     *
+     * @return the request
+     * @throws NullPointerException if policyId or inputs is null
+     * @throws IllegalArgumentException if policyId is empty or inputs is empty
      */
-    public static final class Builder {
-        private String policyId;
-        private List inputs;
-
-        public Builder policyId(String policyId) { this.policyId = policyId; return this; }
-        public Builder inputs(List inputs) { this.inputs = inputs; return this; }
-
-        /**
-         * Builds the ImpactReportRequest.
-         *
-         * @return the request
-         * @throws NullPointerException if policyId or inputs is null
-         * @throws IllegalArgumentException if policyId is empty or inputs is empty
-         */
-        public ImpactReportRequest build() {
-            return new ImpactReportRequest(this);
-        }
+    public ImpactReportRequest build() {
+      return new ImpactReportRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResponse.java b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResponse.java
index a578367..a8ecbd1 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResponse.java
@@ -17,82 +17,111 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 
-/**
- * Response from the policy impact report endpoint.
- */
+/** Response from the policy impact report endpoint. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ImpactReportResponse {
 
-    @JsonProperty("policy_id")
-    private final String policyId;
-
-    @JsonProperty("policy_name")
-    private final String policyName;
-
-    @JsonProperty("total_inputs")
-    private final int totalInputs;
-
-    @JsonProperty("matched")
-    private final int matched;
-
-    @JsonProperty("blocked")
-    private final int blocked;
-
-    @JsonProperty("match_rate")
-    private final double matchRate;
-
-    @JsonProperty("block_rate")
-    private final double blockRate;
-
-    @JsonProperty("results")
-    private final List results;
-
-    @JsonProperty("processing_time_ms")
-    private final long processingTimeMs;
-
-    @JsonProperty("generated_at")
-    private final String generatedAt;
-
-    @JsonProperty("tier")
-    private final String tier;
-
-    public ImpactReportResponse(
-            @JsonProperty("policy_id") String policyId,
-            @JsonProperty("policy_name") String policyName,
-            @JsonProperty("total_inputs") int totalInputs,
-            @JsonProperty("matched") int matched,
-            @JsonProperty("blocked") int blocked,
-            @JsonProperty("match_rate") double matchRate,
-            @JsonProperty("block_rate") double blockRate,
-            @JsonProperty("results") List results,
-            @JsonProperty("processing_time_ms") long processingTimeMs,
-            @JsonProperty("generated_at") String generatedAt,
-            @JsonProperty("tier") String tier) {
-        this.policyId = policyId;
-        this.policyName = policyName;
-        this.totalInputs = totalInputs;
-        this.matched = matched;
-        this.blocked = blocked;
-        this.matchRate = matchRate;
-        this.blockRate = blockRate;
-        this.results = results != null ? List.copyOf(results) : List.of();
-        this.processingTimeMs = processingTimeMs;
-        this.generatedAt = generatedAt;
-        this.tier = tier;
-    }
-
-    public String getPolicyId() { return policyId; }
-    public String getPolicyName() { return policyName; }
-    public int getTotalInputs() { return totalInputs; }
-    public int getMatched() { return matched; }
-    public int getBlocked() { return blocked; }
-    public double getMatchRate() { return matchRate; }
-    public double getBlockRate() { return blockRate; }
-    public List getResults() { return results; }
-    public long getProcessingTimeMs() { return processingTimeMs; }
-    public String getGeneratedAt() { return generatedAt; }
-    public String getTier() { return tier; }
+  @JsonProperty("policy_id")
+  private final String policyId;
+
+  @JsonProperty("policy_name")
+  private final String policyName;
+
+  @JsonProperty("total_inputs")
+  private final int totalInputs;
+
+  @JsonProperty("matched")
+  private final int matched;
+
+  @JsonProperty("blocked")
+  private final int blocked;
+
+  @JsonProperty("match_rate")
+  private final double matchRate;
+
+  @JsonProperty("block_rate")
+  private final double blockRate;
+
+  @JsonProperty("results")
+  private final List results;
+
+  @JsonProperty("processing_time_ms")
+  private final long processingTimeMs;
+
+  @JsonProperty("generated_at")
+  private final String generatedAt;
+
+  @JsonProperty("tier")
+  private final String tier;
+
+  public ImpactReportResponse(
+      @JsonProperty("policy_id") String policyId,
+      @JsonProperty("policy_name") String policyName,
+      @JsonProperty("total_inputs") int totalInputs,
+      @JsonProperty("matched") int matched,
+      @JsonProperty("blocked") int blocked,
+      @JsonProperty("match_rate") double matchRate,
+      @JsonProperty("block_rate") double blockRate,
+      @JsonProperty("results") List results,
+      @JsonProperty("processing_time_ms") long processingTimeMs,
+      @JsonProperty("generated_at") String generatedAt,
+      @JsonProperty("tier") String tier) {
+    this.policyId = policyId;
+    this.policyName = policyName;
+    this.totalInputs = totalInputs;
+    this.matched = matched;
+    this.blocked = blocked;
+    this.matchRate = matchRate;
+    this.blockRate = blockRate;
+    this.results = results != null ? List.copyOf(results) : List.of();
+    this.processingTimeMs = processingTimeMs;
+    this.generatedAt = generatedAt;
+    this.tier = tier;
+  }
+
+  public String getPolicyId() {
+    return policyId;
+  }
+
+  public String getPolicyName() {
+    return policyName;
+  }
+
+  public int getTotalInputs() {
+    return totalInputs;
+  }
+
+  public int getMatched() {
+    return matched;
+  }
+
+  public int getBlocked() {
+    return blocked;
+  }
+
+  public double getMatchRate() {
+    return matchRate;
+  }
+
+  public double getBlockRate() {
+    return blockRate;
+  }
+
+  public List getResults() {
+    return results;
+  }
+
+  public long getProcessingTimeMs() {
+    return processingTimeMs;
+  }
+
+  public String getGeneratedAt() {
+    return generatedAt;
+  }
+
+  public String getTier() {
+    return tier;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResult.java b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResult.java
index 8bfb67d..b4dbe96 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResult.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/ImpactReportResult.java
@@ -17,40 +17,48 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 
-/**
- * Result for a single input in an impact report.
- */
+/** Result for a single input in an impact report. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ImpactReportResult {
 
-    @JsonProperty("input_index")
-    private final int inputIndex;
-
-    @JsonProperty("matched")
-    private final boolean matched;
-
-    @JsonProperty("blocked")
-    private final boolean blocked;
-
-    @JsonProperty("actions")
-    private final List actions;
-
-    public ImpactReportResult(
-            @JsonProperty("input_index") int inputIndex,
-            @JsonProperty("matched") boolean matched,
-            @JsonProperty("blocked") boolean blocked,
-            @JsonProperty("actions") List actions) {
-        this.inputIndex = inputIndex;
-        this.matched = matched;
-        this.blocked = blocked;
-        this.actions = actions != null ? List.copyOf(actions) : List.of();
-    }
-
-    public int getInputIndex() { return inputIndex; }
-    public boolean isMatched() { return matched; }
-    public boolean isBlocked() { return blocked; }
-    public List getActions() { return actions; }
+  @JsonProperty("input_index")
+  private final int inputIndex;
+
+  @JsonProperty("matched")
+  private final boolean matched;
+
+  @JsonProperty("blocked")
+  private final boolean blocked;
+
+  @JsonProperty("actions")
+  private final List actions;
+
+  public ImpactReportResult(
+      @JsonProperty("input_index") int inputIndex,
+      @JsonProperty("matched") boolean matched,
+      @JsonProperty("blocked") boolean blocked,
+      @JsonProperty("actions") List actions) {
+    this.inputIndex = inputIndex;
+    this.matched = matched;
+    this.blocked = blocked;
+    this.actions = actions != null ? List.copyOf(actions) : List.of();
+  }
+
+  public int getInputIndex() {
+    return inputIndex;
+  }
+
+  public boolean isMatched() {
+    return matched;
+  }
+
+  public boolean isBlocked() {
+    return blocked;
+  }
+
+  public List getActions() {
+    return actions;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflict.java b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflict.java
index 4e28de5..cbdc811 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflict.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflict.java
@@ -18,49 +18,64 @@
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-/**
- * A detected conflict between policies.
- */
+/** A detected conflict between policies. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class PolicyConflict {
 
-    @JsonProperty("policy_a")
-    private final PolicyConflictRef policyA;
+  @JsonProperty("policy_a")
+  private final PolicyConflictRef policyA;
+
+  @JsonProperty("policy_b")
+  private final PolicyConflictRef policyB;
+
+  @JsonProperty("conflict_type")
+  private final String conflictType;
+
+  @JsonProperty("description")
+  private final String description;
+
+  @JsonProperty("severity")
+  private final String severity;
+
+  @JsonProperty("overlapping_field")
+  private final String overlappingField;
 
-    @JsonProperty("policy_b")
-    private final PolicyConflictRef policyB;
+  public PolicyConflict(
+      @JsonProperty("policy_a") PolicyConflictRef policyA,
+      @JsonProperty("policy_b") PolicyConflictRef policyB,
+      @JsonProperty("conflict_type") String conflictType,
+      @JsonProperty("description") String description,
+      @JsonProperty("severity") String severity,
+      @JsonProperty("overlapping_field") String overlappingField) {
+    this.policyA = policyA;
+    this.policyB = policyB;
+    this.conflictType = conflictType;
+    this.description = description;
+    this.severity = severity;
+    this.overlappingField = overlappingField;
+  }
 
-    @JsonProperty("conflict_type")
-    private final String conflictType;
+  public PolicyConflictRef getPolicyA() {
+    return policyA;
+  }
 
-    @JsonProperty("description")
-    private final String description;
+  public PolicyConflictRef getPolicyB() {
+    return policyB;
+  }
 
-    @JsonProperty("severity")
-    private final String severity;
+  public String getConflictType() {
+    return conflictType;
+  }
 
-    @JsonProperty("overlapping_field")
-    private final String overlappingField;
+  public String getDescription() {
+    return description;
+  }
 
-    public PolicyConflict(
-            @JsonProperty("policy_a") PolicyConflictRef policyA,
-            @JsonProperty("policy_b") PolicyConflictRef policyB,
-            @JsonProperty("conflict_type") String conflictType,
-            @JsonProperty("description") String description,
-            @JsonProperty("severity") String severity,
-            @JsonProperty("overlapping_field") String overlappingField) {
-        this.policyA = policyA;
-        this.policyB = policyB;
-        this.conflictType = conflictType;
-        this.description = description;
-        this.severity = severity;
-        this.overlappingField = overlappingField;
-    }
+  public String getSeverity() {
+    return severity;
+  }
 
-    public PolicyConflictRef getPolicyA() { return policyA; }
-    public PolicyConflictRef getPolicyB() { return policyB; }
-    public String getConflictType() { return conflictType; }
-    public String getDescription() { return description; }
-    public String getSeverity() { return severity; }
-    public String getOverlappingField() { return overlappingField; }
+  public String getOverlappingField() {
+    return overlappingField;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictRef.java b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictRef.java
index 39c8179..5b3f93a 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictRef.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictRef.java
@@ -18,31 +18,37 @@
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-/**
- * Reference to a policy involved in a conflict.
- */
+/** Reference to a policy involved in a conflict. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class PolicyConflictRef {
 
-    @JsonProperty("id")
-    private final String id;
+  @JsonProperty("id")
+  private final String id;
+
+  @JsonProperty("name")
+  private final String name;
+
+  @JsonProperty("type")
+  private final String type;
 
-    @JsonProperty("name")
-    private final String name;
+  public PolicyConflictRef(
+      @JsonProperty("id") String id,
+      @JsonProperty("name") String name,
+      @JsonProperty("type") String type) {
+    this.id = id;
+    this.name = name;
+    this.type = type;
+  }
 
-    @JsonProperty("type")
-    private final String type;
+  public String getId() {
+    return id;
+  }
 
-    public PolicyConflictRef(
-            @JsonProperty("id") String id,
-            @JsonProperty("name") String name,
-            @JsonProperty("type") String type) {
-        this.id = id;
-        this.name = name;
-        this.type = type;
-    }
+  public String getName() {
+    return name;
+  }
 
-    public String getId() { return id; }
-    public String getName() { return name; }
-    public String getType() { return type; }
+  public String getType() {
+    return type;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictResponse.java b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictResponse.java
index 7e700b9..5fa9aa2 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/PolicyConflictResponse.java
@@ -17,46 +17,57 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 
-/**
- * Response from the policy conflict detection endpoint.
- */
+/** Response from the policy conflict detection endpoint. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class PolicyConflictResponse {
 
-    @JsonProperty("conflicts")
-    private final List conflicts;
-
-    @JsonProperty("total_policies")
-    private final int totalPolicies;
-
-    @JsonProperty("conflict_count")
-    private final int conflictCount;
-
-    @JsonProperty("checked_at")
-    private final String checkedAt;
-
-    @JsonProperty("tier")
-    private final String tier;
-
-    public PolicyConflictResponse(
-            @JsonProperty("conflicts") List conflicts,
-            @JsonProperty("total_policies") int totalPolicies,
-            @JsonProperty("conflict_count") int conflictCount,
-            @JsonProperty("checked_at") String checkedAt,
-            @JsonProperty("tier") String tier) {
-        this.conflicts = conflicts != null ? List.copyOf(conflicts) : List.of();
-        this.totalPolicies = totalPolicies;
-        this.conflictCount = conflictCount;
-        this.checkedAt = checkedAt;
-        this.tier = tier;
-    }
-
-    public List getConflicts() { return conflicts; }
-    public int getTotalPolicies() { return totalPolicies; }
-    public int getConflictCount() { return conflictCount; }
-    public String getCheckedAt() { return checkedAt; }
-    public String getTier() { return tier; }
+  @JsonProperty("conflicts")
+  private final List conflicts;
+
+  @JsonProperty("total_policies")
+  private final int totalPolicies;
+
+  @JsonProperty("conflict_count")
+  private final int conflictCount;
+
+  @JsonProperty("checked_at")
+  private final String checkedAt;
+
+  @JsonProperty("tier")
+  private final String tier;
+
+  public PolicyConflictResponse(
+      @JsonProperty("conflicts") List conflicts,
+      @JsonProperty("total_policies") int totalPolicies,
+      @JsonProperty("conflict_count") int conflictCount,
+      @JsonProperty("checked_at") String checkedAt,
+      @JsonProperty("tier") String tier) {
+    this.conflicts = conflicts != null ? List.copyOf(conflicts) : List.of();
+    this.totalPolicies = totalPolicies;
+    this.conflictCount = conflictCount;
+    this.checkedAt = checkedAt;
+    this.tier = tier;
+  }
+
+  public List getConflicts() {
+    return conflicts;
+  }
+
+  public int getTotalPolicies() {
+    return totalPolicies;
+  }
+
+  public int getConflictCount() {
+    return conflictCount;
+  }
+
+  public String getCheckedAt() {
+    return checkedAt;
+  }
+
+  public String getTier() {
+    return tier;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesRequest.java b/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesRequest.java
index 8f0389f..05b3d4c 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesRequest.java
@@ -17,7 +17,6 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Map;
 import java.util.Objects;
 
@@ -25,6 +24,7 @@
  * Request to simulate policy evaluation against a query.
  *
  * 

Use the {@link Builder} to construct instances: + * *

{@code
  * SimulatePoliciesRequest request = SimulatePoliciesRequest.builder()
  *     .query("Transfer $50,000 to external account")
@@ -35,72 +35,103 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class SimulatePoliciesRequest {
 
-    @JsonProperty("query")
-    private final String query;
+  @JsonProperty("query")
+  private final String query;
 
-    @JsonProperty("request_type")
-    private final String requestType;
+  @JsonProperty("request_type")
+  private final String requestType;
 
-    @JsonProperty("user")
-    private final Map user;
+  @JsonProperty("user")
+  private final Map user;
 
-    @JsonProperty("client")
-    private final Map client;
+  @JsonProperty("client")
+  private final Map client;
 
-    @JsonProperty("context")
-    private final Map context;
+  @JsonProperty("context")
+  private final Map context;
 
-    private SimulatePoliciesRequest(Builder builder) {
-        this.query = Objects.requireNonNull(builder.query, "query cannot be null");
-        if (this.query.isEmpty()) {
-            throw new IllegalArgumentException("query cannot be empty");
-        }
-        this.requestType = builder.requestType;
-        this.user = builder.user;
-        this.client = builder.client;
-        this.context = builder.context;
+  private SimulatePoliciesRequest(Builder builder) {
+    this.query = Objects.requireNonNull(builder.query, "query cannot be null");
+    if (this.query.isEmpty()) {
+      throw new IllegalArgumentException("query cannot be empty");
     }
+    this.requestType = builder.requestType;
+    this.user = builder.user;
+    this.client = builder.client;
+    this.context = builder.context;
+  }
 
-    /**
-     * Creates a new builder for SimulatePoliciesRequest.
-     *
-     * @return a new builder
-     */
-    public static Builder builder() {
-        return new Builder();
+  /**
+   * Creates a new builder for SimulatePoliciesRequest.
+   *
+   * @return a new builder
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getQuery() {
+    return query;
+  }
+
+  public String getRequestType() {
+    return requestType;
+  }
+
+  public Map getUser() {
+    return user;
+  }
+
+  public Map getClient() {
+    return client;
+  }
+
+  public Map getContext() {
+    return context;
+  }
+
+  /** Builder for {@link SimulatePoliciesRequest}. */
+  public static final class Builder {
+    private String query;
+    private String requestType;
+    private Map user;
+    private Map client;
+    private Map context;
+
+    public Builder query(String query) {
+      this.query = query;
+      return this;
+    }
+
+    public Builder requestType(String requestType) {
+      this.requestType = requestType;
+      return this;
+    }
+
+    public Builder user(Map user) {
+      this.user = user;
+      return this;
     }
 
-    public String getQuery() { return query; }
-    public String getRequestType() { return requestType; }
-    public Map getUser() { return user; }
-    public Map getClient() { return client; }
-    public Map getContext() { return context; }
+    public Builder client(Map client) {
+      this.client = client;
+      return this;
+    }
+
+    public Builder context(Map context) {
+      this.context = context;
+      return this;
+    }
 
     /**
-     * Builder for {@link SimulatePoliciesRequest}.
+     * Builds the SimulatePoliciesRequest.
+     *
+     * @return the request
+     * @throws NullPointerException if query is null
+     * @throws IllegalArgumentException if query is empty
      */
-    public static final class Builder {
-        private String query;
-        private String requestType;
-        private Map user;
-        private Map client;
-        private Map context;
-
-        public Builder query(String query) { this.query = query; return this; }
-        public Builder requestType(String requestType) { this.requestType = requestType; return this; }
-        public Builder user(Map user) { this.user = user; return this; }
-        public Builder client(Map client) { this.client = client; return this; }
-        public Builder context(Map context) { this.context = context; return this; }
-
-        /**
-         * Builds the SimulatePoliciesRequest.
-         *
-         * @return the request
-         * @throws NullPointerException if query is null
-         * @throws IllegalArgumentException if query is empty
-         */
-        public SimulatePoliciesRequest build() {
-            return new SimulatePoliciesRequest(this);
-        }
+    public SimulatePoliciesRequest build() {
+      return new SimulatePoliciesRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesResponse.java b/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesResponse.java
index f529b16..f942f1b 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/SimulatePoliciesResponse.java
@@ -17,76 +17,102 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 
-/**
- * Response from policy simulation.
- */
+/** Response from policy simulation. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class SimulatePoliciesResponse {
 
-    @JsonProperty("allowed")
-    private final boolean allowed;
-
-    @JsonProperty("applied_policies")
-    private final List appliedPolicies;
-
-    @JsonProperty("risk_score")
-    private final double riskScore;
-
-    @JsonProperty("required_actions")
-    private final List requiredActions;
-
-    @JsonProperty("processing_time_ms")
-    private final long processingTimeMs;
-
-    @JsonProperty("total_policies")
-    private final int totalPolicies;
-
-    @JsonProperty("dry_run")
-    private final boolean dryRun;
-
-    @JsonProperty("simulated_at")
-    private final String simulatedAt;
-
-    @JsonProperty("tier")
-    private final String tier;
-
-    @JsonProperty("daily_usage")
-    private final SimulationDailyUsage dailyUsage;
-
-    public SimulatePoliciesResponse(
-            @JsonProperty("allowed") boolean allowed,
-            @JsonProperty("applied_policies") List appliedPolicies,
-            @JsonProperty("risk_score") double riskScore,
-            @JsonProperty("required_actions") List requiredActions,
-            @JsonProperty("processing_time_ms") long processingTimeMs,
-            @JsonProperty("total_policies") int totalPolicies,
-            @JsonProperty("dry_run") boolean dryRun,
-            @JsonProperty("simulated_at") String simulatedAt,
-            @JsonProperty("tier") String tier,
-            @JsonProperty("daily_usage") SimulationDailyUsage dailyUsage) {
-        this.allowed = allowed;
-        this.appliedPolicies = appliedPolicies != null ? List.copyOf(appliedPolicies) : List.of();
-        this.riskScore = riskScore;
-        this.requiredActions = requiredActions != null ? List.copyOf(requiredActions) : List.of();
-        this.processingTimeMs = processingTimeMs;
-        this.totalPolicies = totalPolicies;
-        this.dryRun = dryRun;
-        this.simulatedAt = simulatedAt;
-        this.tier = tier;
-        this.dailyUsage = dailyUsage;
-    }
-
-    public boolean isAllowed() { return allowed; }
-    public List getAppliedPolicies() { return appliedPolicies; }
-    public double getRiskScore() { return riskScore; }
-    public List getRequiredActions() { return requiredActions; }
-    public long getProcessingTimeMs() { return processingTimeMs; }
-    public int getTotalPolicies() { return totalPolicies; }
-    public boolean isDryRun() { return dryRun; }
-    public String getSimulatedAt() { return simulatedAt; }
-    public String getTier() { return tier; }
-    public SimulationDailyUsage getDailyUsage() { return dailyUsage; }
+  @JsonProperty("allowed")
+  private final boolean allowed;
+
+  @JsonProperty("applied_policies")
+  private final List appliedPolicies;
+
+  @JsonProperty("risk_score")
+  private final double riskScore;
+
+  @JsonProperty("required_actions")
+  private final List requiredActions;
+
+  @JsonProperty("processing_time_ms")
+  private final long processingTimeMs;
+
+  @JsonProperty("total_policies")
+  private final int totalPolicies;
+
+  @JsonProperty("dry_run")
+  private final boolean dryRun;
+
+  @JsonProperty("simulated_at")
+  private final String simulatedAt;
+
+  @JsonProperty("tier")
+  private final String tier;
+
+  @JsonProperty("daily_usage")
+  private final SimulationDailyUsage dailyUsage;
+
+  public SimulatePoliciesResponse(
+      @JsonProperty("allowed") boolean allowed,
+      @JsonProperty("applied_policies") List appliedPolicies,
+      @JsonProperty("risk_score") double riskScore,
+      @JsonProperty("required_actions") List requiredActions,
+      @JsonProperty("processing_time_ms") long processingTimeMs,
+      @JsonProperty("total_policies") int totalPolicies,
+      @JsonProperty("dry_run") boolean dryRun,
+      @JsonProperty("simulated_at") String simulatedAt,
+      @JsonProperty("tier") String tier,
+      @JsonProperty("daily_usage") SimulationDailyUsage dailyUsage) {
+    this.allowed = allowed;
+    this.appliedPolicies = appliedPolicies != null ? List.copyOf(appliedPolicies) : List.of();
+    this.riskScore = riskScore;
+    this.requiredActions = requiredActions != null ? List.copyOf(requiredActions) : List.of();
+    this.processingTimeMs = processingTimeMs;
+    this.totalPolicies = totalPolicies;
+    this.dryRun = dryRun;
+    this.simulatedAt = simulatedAt;
+    this.tier = tier;
+    this.dailyUsage = dailyUsage;
+  }
+
+  public boolean isAllowed() {
+    return allowed;
+  }
+
+  public List getAppliedPolicies() {
+    return appliedPolicies;
+  }
+
+  public double getRiskScore() {
+    return riskScore;
+  }
+
+  public List getRequiredActions() {
+    return requiredActions;
+  }
+
+  public long getProcessingTimeMs() {
+    return processingTimeMs;
+  }
+
+  public int getTotalPolicies() {
+    return totalPolicies;
+  }
+
+  public boolean isDryRun() {
+    return dryRun;
+  }
+
+  public String getSimulatedAt() {
+    return simulatedAt;
+  }
+
+  public String getTier() {
+    return tier;
+  }
+
+  public SimulationDailyUsage getDailyUsage() {
+    return dailyUsage;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/simulation/SimulationDailyUsage.java b/src/main/java/com/getaxonflow/sdk/simulation/SimulationDailyUsage.java
index e156813..46dd966 100644
--- a/src/main/java/com/getaxonflow/sdk/simulation/SimulationDailyUsage.java
+++ b/src/main/java/com/getaxonflow/sdk/simulation/SimulationDailyUsage.java
@@ -18,25 +18,26 @@
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
 
-/**
- * Daily usage counters for policy simulation.
- */
+/** Daily usage counters for policy simulation. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class SimulationDailyUsage {
 
-    @JsonProperty("used")
-    private final int used;
+  @JsonProperty("used")
+  private final int used;
+
+  @JsonProperty("limit")
+  private final int limit;
 
-    @JsonProperty("limit")
-    private final int limit;
+  public SimulationDailyUsage(@JsonProperty("used") int used, @JsonProperty("limit") int limit) {
+    this.used = used;
+    this.limit = limit;
+  }
 
-    public SimulationDailyUsage(
-            @JsonProperty("used") int used,
-            @JsonProperty("limit") int limit) {
-        this.used = used;
-        this.limit = limit;
-    }
+  public int getUsed() {
+    return used;
+  }
 
-    public int getUsed() { return used; }
-    public int getLimit() { return limit; }
+  public int getLimit() {
+    return limit;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java
index baf8968..80b459f 100644
--- a/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java
+++ b/src/main/java/com/getaxonflow/sdk/telemetry/TelemetryReporter.java
@@ -15,11 +15,14 @@
  */
 package com.getaxonflow.sdk.telemetry;
 
-import com.getaxonflow.sdk.AxonFlowConfig;
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.node.ArrayNode;
 import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.getaxonflow.sdk.AxonFlowConfig;
+import java.util.UUID;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.TimeUnit;
 import okhttp3.MediaType;
 import okhttp3.OkHttpClient;
 import okhttp3.Request;
@@ -28,250 +31,257 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.UUID;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.TimeUnit;
-
 /**
- * Fire-and-forget telemetry reporter that sends anonymous usage pings
- * to the AxonFlow checkpoint endpoint.
+ * Fire-and-forget telemetry reporter that sends anonymous usage pings to the AxonFlow checkpoint
+ * endpoint.
  *
- * 

Telemetry is completely anonymous and contains no user data, only - * SDK version, runtime environment, and deployment mode information. + *

Telemetry is completely anonymous and contains no user data, only SDK version, runtime + * environment, and deployment mode information. * *

Telemetry can be disabled via: + * *

    - *
  • Setting environment variable {@code DO_NOT_TRACK=1}
  • - *
  • Setting environment variable {@code AXONFLOW_TELEMETRY=off}
  • - *
  • Setting {@code telemetry(false)} on the config builder
  • + *
  • Setting environment variable {@code DO_NOT_TRACK=1} + *
  • Setting environment variable {@code AXONFLOW_TELEMETRY=off} + *
  • Setting {@code telemetry(false)} on the config builder *
* *

By default, telemetry is OFF in sandbox mode and ON in production mode. */ public class TelemetryReporter { - private static final Logger logger = LoggerFactory.getLogger(TelemetryReporter.class); + private static final Logger logger = LoggerFactory.getLogger(TelemetryReporter.class); - static final String DEFAULT_ENDPOINT = "https://checkpoint.getaxonflow.com/v1/ping"; - private static final int TIMEOUT_SECONDS = 3; - private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); + static final String DEFAULT_ENDPOINT = "https://checkpoint.getaxonflow.com/v1/ping"; + private static final int TIMEOUT_SECONDS = 3; + private static final MediaType JSON = MediaType.get("application/json; charset=utf-8"); - /** - * Sends an anonymous telemetry ping asynchronously (fire-and-forget). - * - * @param mode the deployment mode (e.g. "production", "sandbox") - * @param sdkEndpoint the configured SDK endpoint, used to detect platform version via /health - * @param telemetryEnabled config override for telemetry (null = use default based on mode) - * @param debug whether debug logging is enabled - */ - public static void sendPing(String mode, String sdkEndpoint, Boolean telemetryEnabled, boolean debug, - boolean hasCredentials) { - sendPing(mode, sdkEndpoint, telemetryEnabled, debug, hasCredentials, - System.getenv("DO_NOT_TRACK"), - System.getenv("AXONFLOW_TELEMETRY"), - System.getenv("AXONFLOW_CHECKPOINT_URL")); - } + /** + * Sends an anonymous telemetry ping asynchronously (fire-and-forget). + * + * @param mode the deployment mode (e.g. "production", "sandbox") + * @param sdkEndpoint the configured SDK endpoint, used to detect platform version via /health + * @param telemetryEnabled config override for telemetry (null = use default based on mode) + * @param debug whether debug logging is enabled + */ + public static void sendPing( + String mode, + String sdkEndpoint, + Boolean telemetryEnabled, + boolean debug, + boolean hasCredentials) { + sendPing( + mode, + sdkEndpoint, + telemetryEnabled, + debug, + hasCredentials, + System.getenv("DO_NOT_TRACK"), + System.getenv("AXONFLOW_TELEMETRY"), + System.getenv("AXONFLOW_CHECKPOINT_URL")); + } - /** - * Package-private overload for testability, accepting env var values as parameters. - */ - static void sendPing(String mode, String sdkEndpoint, Boolean telemetryEnabled, boolean debug, - boolean hasCredentials, - String doNotTrack, String axonflowTelemetry, String checkpointUrl) { - if (!isEnabled(mode, telemetryEnabled, hasCredentials, doNotTrack, axonflowTelemetry)) { - if (debug) { - logger.debug("Telemetry is disabled, skipping ping"); - } - return; - } + /** Package-private overload for testability, accepting env var values as parameters. */ + static void sendPing( + String mode, + String sdkEndpoint, + Boolean telemetryEnabled, + boolean debug, + boolean hasCredentials, + String doNotTrack, + String axonflowTelemetry, + String checkpointUrl) { + if (!isEnabled(mode, telemetryEnabled, hasCredentials, doNotTrack, axonflowTelemetry)) { + if (debug) { + logger.debug("Telemetry is disabled, skipping ping"); + } + return; + } - // Suppress telemetry for localhost endpoints unless explicitly enabled. - if (!Boolean.TRUE.equals(telemetryEnabled) && isLocalhostEndpoint(sdkEndpoint)) { - if (debug) { - logger.debug("Telemetry suppressed for localhost endpoint"); - } - return; - } + // Suppress telemetry for localhost endpoints unless explicitly enabled. + if (!Boolean.TRUE.equals(telemetryEnabled) && isLocalhostEndpoint(sdkEndpoint)) { + if (debug) { + logger.debug("Telemetry suppressed for localhost endpoint"); + } + return; + } - logger.info("AxonFlow: anonymous telemetry enabled. Opt out: AXONFLOW_TELEMETRY=off | https://docs.getaxonflow.com/docs/telemetry"); + logger.info( + "AxonFlow: anonymous telemetry enabled. Opt out: AXONFLOW_TELEMETRY=off | https://docs.getaxonflow.com/docs/telemetry"); - String endpoint = (checkpointUrl != null && !checkpointUrl.isEmpty()) - ? checkpointUrl - : DEFAULT_ENDPOINT; + String endpoint = + (checkpointUrl != null && !checkpointUrl.isEmpty()) ? checkpointUrl : DEFAULT_ENDPOINT; - final String finalSdkEndpoint = sdkEndpoint; - CompletableFuture.runAsync(() -> { - try { - String platformVersion = detectPlatformVersion(finalSdkEndpoint); - String payload = buildPayload(mode, platformVersion); + final String finalSdkEndpoint = sdkEndpoint; + CompletableFuture.runAsync( + () -> { + try { + String platformVersion = detectPlatformVersion(finalSdkEndpoint); + String payload = buildPayload(mode, platformVersion); - OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) - .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) - .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) - .build(); + OkHttpClient client = + new OkHttpClient.Builder() + .connectTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .readTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .writeTimeout(TIMEOUT_SECONDS, TimeUnit.SECONDS) + .build(); - RequestBody body = RequestBody.create(payload, JSON); - Request request = new Request.Builder() - .url(endpoint) - .post(body) - .build(); + RequestBody body = RequestBody.create(payload, JSON); + Request request = new Request.Builder().url(endpoint).post(body).build(); - try (Response response = client.newCall(request).execute()) { - if (debug) { - logger.debug("Telemetry ping sent, status={}", response.code()); - } - } - } catch (Exception e) { - // Silent failure - telemetry must never disrupt SDK operation - if (debug) { - logger.debug("Telemetry ping failed (silent): {}", e.getMessage()); - } + try (Response response = client.newCall(request).execute()) { + if (debug) { + logger.debug("Telemetry ping sent, status={}", response.code()); + } } + } catch (Exception e) { + // Silent failure - telemetry must never disrupt SDK operation + if (debug) { + logger.debug("Telemetry ping failed (silent): {}", e.getMessage()); + } + } }); - } + } - /** - * Determines whether telemetry is enabled based on environment and config. - * - *

Priority order: - *

    - *
  1. {@code DO_NOT_TRACK=1} environment variable disables telemetry
  2. - *
  3. {@code AXONFLOW_TELEMETRY=off} environment variable disables telemetry
  4. - *
  5. Config override ({@code Boolean.TRUE} or {@code Boolean.FALSE}) takes precedence
  6. - *
  7. Default: ON for all modes except sandbox
  8. - *
- * - * @param mode the deployment mode - * @param configOverride explicit config override (null = use default) - * @param hasCredentials whether the client has credentials (kept for API compat, no longer used in default logic) - * @return true if telemetry should be sent - */ - static boolean isEnabled(String mode, Boolean configOverride, boolean hasCredentials) { - return isEnabled(mode, configOverride, hasCredentials, - System.getenv("DO_NOT_TRACK"), System.getenv("AXONFLOW_TELEMETRY")); - } + /** + * Determines whether telemetry is enabled based on environment and config. + * + *

Priority order: + * + *

    + *
  1. {@code DO_NOT_TRACK=1} environment variable disables telemetry + *
  2. {@code AXONFLOW_TELEMETRY=off} environment variable disables telemetry + *
  3. Config override ({@code Boolean.TRUE} or {@code Boolean.FALSE}) takes precedence + *
  4. Default: ON for all modes except sandbox + *
+ * + * @param mode the deployment mode + * @param configOverride explicit config override (null = use default) + * @param hasCredentials whether the client has credentials (kept for API compat, no longer used + * in default logic) + * @return true if telemetry should be sent + */ + static boolean isEnabled(String mode, Boolean configOverride, boolean hasCredentials) { + return isEnabled( + mode, + configOverride, + hasCredentials, + System.getenv("DO_NOT_TRACK"), + System.getenv("AXONFLOW_TELEMETRY")); + } - /** - * Package-private for testing. Accepts env var values as parameters. - */ - static boolean isEnabled(String mode, Boolean configOverride, boolean hasCredentials, - String doNotTrack, String axonflowTelemetry) { - if (doNotTrack != null && "1".equals(doNotTrack.trim())) { - return false; - } - if (axonflowTelemetry != null && "off".equalsIgnoreCase(axonflowTelemetry.trim())) { - return false; - } - if (configOverride != null) { - return configOverride; - } - // Default: ON everywhere except sandbox mode. - return !"sandbox".equals(mode); + /** Package-private for testing. Accepts env var values as parameters. */ + static boolean isEnabled( + String mode, + Boolean configOverride, + boolean hasCredentials, + String doNotTrack, + String axonflowTelemetry) { + if (doNotTrack != null && "1".equals(doNotTrack.trim())) { + return false; + } + if (axonflowTelemetry != null && "off".equalsIgnoreCase(axonflowTelemetry.trim())) { + return false; + } + if (configOverride != null) { + return configOverride; } + // Default: ON everywhere except sandbox mode. + return !"sandbox".equals(mode); + } - /** - * Builds the JSON payload for the telemetry ping. - */ - static String buildPayload(String mode, String platformVersion) { - try { - ObjectMapper mapper = new ObjectMapper(); - ObjectNode root = mapper.createObjectNode(); - root.put("sdk", "java"); - root.put("sdk_version", AxonFlowConfig.SDK_VERSION); - if (platformVersion != null) { - root.put("platform_version", platformVersion); - } else { - root.putNull("platform_version"); - } - root.put("os", normalizeOS(System.getProperty("os.name"))); - root.put("arch", normalizeArch(System.getProperty("os.arch"))); - root.put("runtime_version", System.getProperty("java.version")); - root.put("deployment_mode", mode); + /** Builds the JSON payload for the telemetry ping. */ + static String buildPayload(String mode, String platformVersion) { + try { + ObjectMapper mapper = new ObjectMapper(); + ObjectNode root = mapper.createObjectNode(); + root.put("sdk", "java"); + root.put("sdk_version", AxonFlowConfig.SDK_VERSION); + if (platformVersion != null) { + root.put("platform_version", platformVersion); + } else { + root.putNull("platform_version"); + } + root.put("os", normalizeOS(System.getProperty("os.name"))); + root.put("arch", normalizeArch(System.getProperty("os.arch"))); + root.put("runtime_version", System.getProperty("java.version")); + root.put("deployment_mode", mode); - ArrayNode features = mapper.createArrayNode(); - root.set("features", features); + ArrayNode features = mapper.createArrayNode(); + root.set("features", features); - root.put("instance_id", UUID.randomUUID().toString()); + root.put("instance_id", UUID.randomUUID().toString()); - return mapper.writeValueAsString(root); - } catch (Exception e) { - // Fallback minimal payload - return "{\"sdk\":\"java\",\"sdk_version\":\"" + AxonFlowConfig.SDK_VERSION + "\"}"; - } + return mapper.writeValueAsString(root); + } catch (Exception e) { + // Fallback minimal payload + return "{\"sdk\":\"java\",\"sdk_version\":\"" + AxonFlowConfig.SDK_VERSION + "\"}"; } + } - /** - * Detect platform version by calling the agent's /health endpoint. - * Returns null on any failure. - */ - static String detectPlatformVersion(String sdkEndpoint) { - if (sdkEndpoint == null || sdkEndpoint.isEmpty()) { - return null; - } - try { - OkHttpClient client = new OkHttpClient.Builder() - .connectTimeout(2, TimeUnit.SECONDS) - .readTimeout(2, TimeUnit.SECONDS) - .build(); + /** + * Detect platform version by calling the agent's /health endpoint. Returns null on any failure. + */ + static String detectPlatformVersion(String sdkEndpoint) { + if (sdkEndpoint == null || sdkEndpoint.isEmpty()) { + return null; + } + try { + OkHttpClient client = + new OkHttpClient.Builder() + .connectTimeout(2, TimeUnit.SECONDS) + .readTimeout(2, TimeUnit.SECONDS) + .build(); - Request request = new Request.Builder() - .url(sdkEndpoint + "/health") - .get() - .build(); + Request request = new Request.Builder().url(sdkEndpoint + "/health").get().build(); - try (Response response = client.newCall(request).execute()) { - if (response.isSuccessful() && response.body() != null) { - ObjectMapper mapper = new ObjectMapper(); - JsonNode root = mapper.readTree(response.body().string()); - JsonNode versionNode = root.get("version"); - if (versionNode != null && !versionNode.isNull() && !versionNode.asText().isEmpty()) { - return versionNode.asText(); - } - } - } - } catch (Exception ignored) { - // Silent failure + try (Response response = client.newCall(request).execute()) { + if (response.isSuccessful() && response.body() != null) { + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(response.body().string()); + JsonNode versionNode = root.get("version"); + if (versionNode != null && !versionNode.isNull() && !versionNode.asText().isEmpty()) { + return versionNode.asText(); + } } - return null; + } + } catch (Exception ignored) { + // Silent failure } + return null; + } - /** - * Normalize OS name to lowercase short form consistent across SDKs. - * e.g. "Mac OS X" -> "darwin", "Windows 10" -> "windows", "Linux" -> "linux" - */ - static String normalizeOS(String osName) { - if (osName == null) return "unknown"; - String lower = osName.toLowerCase(); - if (lower.contains("mac") || lower.contains("darwin")) return "darwin"; - if (lower.contains("win")) return "windows"; - if (lower.contains("linux")) return "linux"; - return lower; - } + /** + * Normalize OS name to lowercase short form consistent across SDKs. e.g. "Mac OS X" -> "darwin", + * "Windows 10" -> "windows", "Linux" -> "linux" + */ + static String normalizeOS(String osName) { + if (osName == null) return "unknown"; + String lower = osName.toLowerCase(); + if (lower.contains("mac") || lower.contains("darwin")) return "darwin"; + if (lower.contains("win")) return "windows"; + if (lower.contains("linux")) return "linux"; + return lower; + } - /** - * Normalize arch name consistent across SDKs. - * e.g. "aarch64" -> "arm64", "x86_64" -> "x64" - */ - static String normalizeArch(String arch) { - if (arch == null) return "unknown"; - if ("aarch64".equals(arch)) return "arm64"; - if ("x86_64".equals(arch) || "amd64".equals(arch)) return "x64"; - return arch; - } + /** Normalize arch name consistent across SDKs. e.g. "aarch64" -> "arm64", "x86_64" -> "x64" */ + static String normalizeArch(String arch) { + if (arch == null) return "unknown"; + if ("aarch64".equals(arch)) return "arm64"; + if ("x86_64".equals(arch) || "amd64".equals(arch)) return "x64"; + return arch; + } - /** - * Check whether the endpoint is a localhost address. - */ - static boolean isLocalhostEndpoint(String endpoint) { - if (endpoint == null || endpoint.isEmpty()) { - return false; - } - String lower = endpoint.toLowerCase(); - return lower.contains("localhost") || lower.contains("127.0.0.1") || lower.contains("[::1]"); + /** Check whether the endpoint is a localhost address. */ + static boolean isLocalhostEndpoint(String endpoint) { + if (endpoint == null || endpoint.isEmpty()) { + return false; } + String lower = endpoint.toLowerCase(); + return lower.contains("localhost") || lower.contains("127.0.0.1") || lower.contains("[::1]"); + } - private TelemetryReporter() { - // Utility class - } + private TelemetryReporter() { + // Utility class + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java b/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java index 1d6dc2f..6e60e3f 100644 --- a/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java +++ b/src/main/java/com/getaxonflow/sdk/types/AuditLogEntry.java @@ -17,234 +17,258 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -/** - * A single audit log entry representing an audited request or event. - */ +/** A single audit log entry representing an audited request or event. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class AuditLogEntry { - @JsonProperty("id") - private final String id; - - @JsonProperty("request_id") - private final String requestId; - - @JsonProperty("timestamp") - private final Instant timestamp; - - @JsonProperty("user_email") - private final String userEmail; - - @JsonProperty("client_id") - private final String clientId; - - @JsonProperty("tenant_id") - private final String tenantId; - - @JsonProperty("request_type") - private final String requestType; - - @JsonProperty("query_summary") - private final String querySummary; - - @JsonProperty("success") - private final boolean success; - - @JsonProperty("blocked") - private final boolean blocked; - - @JsonProperty("risk_score") - private final double riskScore; - - @JsonProperty("provider") - private final String provider; - - @JsonProperty("model") - private final String model; - - @JsonProperty("tokens_used") - private final int tokensUsed; - - @JsonProperty("latency_ms") - private final int latencyMs; - - @JsonProperty("policy_violations") - private final List policyViolations; - - @JsonProperty("metadata") - private final Map metadata; - - public AuditLogEntry( - @JsonProperty("id") String id, - @JsonProperty("request_id") String requestId, - @JsonProperty("timestamp") Instant timestamp, - @JsonProperty("user_email") String userEmail, - @JsonProperty("client_id") String clientId, - @JsonProperty("tenant_id") String tenantId, - @JsonProperty("request_type") String requestType, - @JsonProperty("query_summary") String querySummary, - @JsonProperty("success") Boolean success, - @JsonProperty("blocked") Boolean blocked, - @JsonProperty("risk_score") Double riskScore, - @JsonProperty("provider") String provider, - @JsonProperty("model") String model, - @JsonProperty("tokens_used") Integer tokensUsed, - @JsonProperty("latency_ms") Integer latencyMs, - @JsonProperty("policy_violations") List policyViolations, - @JsonProperty("metadata") Map metadata) { - this.id = id != null ? id : ""; - this.requestId = requestId != null ? requestId : ""; - this.timestamp = timestamp != null ? timestamp : Instant.now(); - this.userEmail = userEmail != null ? userEmail : ""; - this.clientId = clientId != null ? clientId : ""; - this.tenantId = tenantId != null ? tenantId : ""; - this.requestType = requestType != null ? requestType : ""; - this.querySummary = querySummary != null ? querySummary : ""; - this.success = success != null ? success : true; - this.blocked = blocked != null ? blocked : false; - this.riskScore = riskScore != null ? riskScore : 0.0; - this.provider = provider != null ? provider : ""; - this.model = model != null ? model : ""; - this.tokensUsed = tokensUsed != null ? tokensUsed : 0; - this.latencyMs = latencyMs != null ? latencyMs : 0; - this.policyViolations = policyViolations != null ? policyViolations : Collections.emptyList(); - this.metadata = metadata != null ? metadata : Collections.emptyMap(); - } - - /** Returns the unique audit log ID. */ - public String getId() { - return id; - } - - /** Returns the correlation ID for the original request. */ - public String getRequestId() { - return requestId; - } - - /** Returns when the event occurred. */ - public Instant getTimestamp() { - return timestamp; - } - - /** Returns the email of the user who made the request. */ - public String getUserEmail() { - return userEmail; - } - - /** Returns the client/application that made the request. */ - public String getClientId() { - return clientId; - } - - /** Returns the tenant identifier. */ - public String getTenantId() { - return tenantId; - } - - /** Returns the type of request (e.g., "llm_chat", "sql", "mcp-query"). */ - public String getRequestType() { - return requestType; - } - - /** Returns a summary of the query/request. */ - public String getQuerySummary() { - return querySummary; - } - - /** Returns whether the request succeeded. */ - public boolean isSuccess() { - return success; - } - - /** Returns whether the request was blocked by policy. */ - public boolean isBlocked() { - return blocked; - } - - /** Returns the calculated risk score (0.0-1.0). */ - public double getRiskScore() { - return riskScore; - } - - /** Returns the LLM provider used (if applicable). */ - public String getProvider() { - return provider; - } - - /** Returns the model used (if applicable). */ - public String getModel() { - return model; - } - - /** Returns the total tokens consumed. */ - public int getTokensUsed() { - return tokensUsed; - } - - /** Returns the request latency in milliseconds. */ - public int getLatencyMs() { - return latencyMs; - } - - /** Returns the list of violated policy IDs (if any). */ - public List getPolicyViolations() { - return policyViolations; - } - - /** Returns additional metadata. */ - public Map getMetadata() { - return metadata; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AuditLogEntry that = (AuditLogEntry) o; - return success == that.success && - blocked == that.blocked && - Double.compare(that.riskScore, riskScore) == 0 && - tokensUsed == that.tokensUsed && - latencyMs == that.latencyMs && - Objects.equals(id, that.id) && - Objects.equals(requestId, that.requestId) && - Objects.equals(timestamp, that.timestamp) && - Objects.equals(userEmail, that.userEmail) && - Objects.equals(clientId, that.clientId) && - Objects.equals(tenantId, that.tenantId) && - Objects.equals(requestType, that.requestType) && - Objects.equals(querySummary, that.querySummary) && - Objects.equals(provider, that.provider) && - Objects.equals(model, that.model) && - Objects.equals(policyViolations, that.policyViolations) && - Objects.equals(metadata, that.metadata); - } - - @Override - public int hashCode() { - return Objects.hash(id, requestId, timestamp, userEmail, clientId, tenantId, requestType, - querySummary, success, blocked, riskScore, provider, model, tokensUsed, latencyMs, - policyViolations, metadata); - } - - @Override - public String toString() { - return "AuditLogEntry{" + - "id='" + id + '\'' + - ", requestId='" + requestId + '\'' + - ", timestamp=" + timestamp + - ", userEmail='" + userEmail + '\'' + - ", requestType='" + requestType + '\'' + - ", success=" + success + - ", blocked=" + blocked + - ", riskScore=" + riskScore + - '}'; - } + @JsonProperty("id") + private final String id; + + @JsonProperty("request_id") + private final String requestId; + + @JsonProperty("timestamp") + private final Instant timestamp; + + @JsonProperty("user_email") + private final String userEmail; + + @JsonProperty("client_id") + private final String clientId; + + @JsonProperty("tenant_id") + private final String tenantId; + + @JsonProperty("request_type") + private final String requestType; + + @JsonProperty("query_summary") + private final String querySummary; + + @JsonProperty("success") + private final boolean success; + + @JsonProperty("blocked") + private final boolean blocked; + + @JsonProperty("risk_score") + private final double riskScore; + + @JsonProperty("provider") + private final String provider; + + @JsonProperty("model") + private final String model; + + @JsonProperty("tokens_used") + private final int tokensUsed; + + @JsonProperty("latency_ms") + private final int latencyMs; + + @JsonProperty("policy_violations") + private final List policyViolations; + + @JsonProperty("metadata") + private final Map metadata; + + public AuditLogEntry( + @JsonProperty("id") String id, + @JsonProperty("request_id") String requestId, + @JsonProperty("timestamp") Instant timestamp, + @JsonProperty("user_email") String userEmail, + @JsonProperty("client_id") String clientId, + @JsonProperty("tenant_id") String tenantId, + @JsonProperty("request_type") String requestType, + @JsonProperty("query_summary") String querySummary, + @JsonProperty("success") Boolean success, + @JsonProperty("blocked") Boolean blocked, + @JsonProperty("risk_score") Double riskScore, + @JsonProperty("provider") String provider, + @JsonProperty("model") String model, + @JsonProperty("tokens_used") Integer tokensUsed, + @JsonProperty("latency_ms") Integer latencyMs, + @JsonProperty("policy_violations") List policyViolations, + @JsonProperty("metadata") Map metadata) { + this.id = id != null ? id : ""; + this.requestId = requestId != null ? requestId : ""; + this.timestamp = timestamp != null ? timestamp : Instant.now(); + this.userEmail = userEmail != null ? userEmail : ""; + this.clientId = clientId != null ? clientId : ""; + this.tenantId = tenantId != null ? tenantId : ""; + this.requestType = requestType != null ? requestType : ""; + this.querySummary = querySummary != null ? querySummary : ""; + this.success = success != null ? success : true; + this.blocked = blocked != null ? blocked : false; + this.riskScore = riskScore != null ? riskScore : 0.0; + this.provider = provider != null ? provider : ""; + this.model = model != null ? model : ""; + this.tokensUsed = tokensUsed != null ? tokensUsed : 0; + this.latencyMs = latencyMs != null ? latencyMs : 0; + this.policyViolations = policyViolations != null ? policyViolations : Collections.emptyList(); + this.metadata = metadata != null ? metadata : Collections.emptyMap(); + } + + /** Returns the unique audit log ID. */ + public String getId() { + return id; + } + + /** Returns the correlation ID for the original request. */ + public String getRequestId() { + return requestId; + } + + /** Returns when the event occurred. */ + public Instant getTimestamp() { + return timestamp; + } + + /** Returns the email of the user who made the request. */ + public String getUserEmail() { + return userEmail; + } + + /** Returns the client/application that made the request. */ + public String getClientId() { + return clientId; + } + + /** Returns the tenant identifier. */ + public String getTenantId() { + return tenantId; + } + + /** Returns the type of request (e.g., "llm_chat", "sql", "mcp-query"). */ + public String getRequestType() { + return requestType; + } + + /** Returns a summary of the query/request. */ + public String getQuerySummary() { + return querySummary; + } + + /** Returns whether the request succeeded. */ + public boolean isSuccess() { + return success; + } + + /** Returns whether the request was blocked by policy. */ + public boolean isBlocked() { + return blocked; + } + + /** Returns the calculated risk score (0.0-1.0). */ + public double getRiskScore() { + return riskScore; + } + + /** Returns the LLM provider used (if applicable). */ + public String getProvider() { + return provider; + } + + /** Returns the model used (if applicable). */ + public String getModel() { + return model; + } + + /** Returns the total tokens consumed. */ + public int getTokensUsed() { + return tokensUsed; + } + + /** Returns the request latency in milliseconds. */ + public int getLatencyMs() { + return latencyMs; + } + + /** Returns the list of violated policy IDs (if any). */ + public List getPolicyViolations() { + return policyViolations; + } + + /** Returns additional metadata. */ + public Map getMetadata() { + return metadata; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + AuditLogEntry that = (AuditLogEntry) o; + return success == that.success + && blocked == that.blocked + && Double.compare(that.riskScore, riskScore) == 0 + && tokensUsed == that.tokensUsed + && latencyMs == that.latencyMs + && Objects.equals(id, that.id) + && Objects.equals(requestId, that.requestId) + && Objects.equals(timestamp, that.timestamp) + && Objects.equals(userEmail, that.userEmail) + && Objects.equals(clientId, that.clientId) + && Objects.equals(tenantId, that.tenantId) + && Objects.equals(requestType, that.requestType) + && Objects.equals(querySummary, that.querySummary) + && Objects.equals(provider, that.provider) + && Objects.equals(model, that.model) + && Objects.equals(policyViolations, that.policyViolations) + && Objects.equals(metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash( + id, + requestId, + timestamp, + userEmail, + clientId, + tenantId, + requestType, + querySummary, + success, + blocked, + riskScore, + provider, + model, + tokensUsed, + latencyMs, + policyViolations, + metadata); + } + + @Override + public String toString() { + return "AuditLogEntry{" + + "id='" + + id + + '\'' + + ", requestId='" + + requestId + + '\'' + + ", timestamp=" + + timestamp + + ", userEmail='" + + userEmail + + '\'' + + ", requestType='" + + requestType + + '\'' + + ", success=" + + success + + ", blocked=" + + blocked + + ", riskScore=" + + riskScore + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditOptions.java b/src/main/java/com/getaxonflow/sdk/types/AuditOptions.java index c0660e4..4432409 100644 --- a/src/main/java/com/getaxonflow/sdk/types/AuditOptions.java +++ b/src/main/java/com/getaxonflow/sdk/types/AuditOptions.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -26,10 +25,11 @@ /** * Options for auditing an LLM call in Gateway Mode. * - *

This is the third step of the Gateway Mode pattern, used to log - * the LLM call for compliance and observability. + *

This is the third step of the Gateway Mode pattern, used to log the LLM call for compliance + * and observability. * *

Example usage: + * *

{@code
  * AuditOptions options = AuditOptions.builder()
  *     .contextId(policyResult.getContextId())
@@ -46,284 +46,303 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class AuditOptions {
 
-    @JsonProperty("context_id")
-    private final String contextId;
+  @JsonProperty("context_id")
+  private final String contextId;
 
-    @JsonProperty("client_id")
-    private final String clientId;
+  @JsonProperty("client_id")
+  private final String clientId;
 
-    @JsonProperty("response_summary")
-    private final String responseSummary;
+  @JsonProperty("response_summary")
+  private final String responseSummary;
 
-    @JsonProperty("provider")
-    private final String provider;
+  @JsonProperty("provider")
+  private final String provider;
 
-    @JsonProperty("model")
-    private final String model;
+  @JsonProperty("model")
+  private final String model;
 
-    @JsonProperty("token_usage")
-    private final TokenUsage tokenUsage;
+  @JsonProperty("token_usage")
+  private final TokenUsage tokenUsage;
 
-    @JsonProperty("latency_ms")
-    private final Long latencyMs;
+  @JsonProperty("latency_ms")
+  private final Long latencyMs;
 
-    @JsonProperty("metadata")
-    private final Map metadata;
+  @JsonProperty("metadata")
+  private final Map metadata;
 
-    @JsonProperty("success")
-    private final Boolean success;
+  @JsonProperty("success")
+  private final Boolean success;
 
-    @JsonProperty("error_message")
-    private final String errorMessage;
+  @JsonProperty("error_message")
+  private final String errorMessage;
 
-    private AuditOptions(Builder builder) {
-        this.contextId = Objects.requireNonNull(builder.contextId, "contextId cannot be null");
-        this.clientId = builder.clientId; // Optional - SDK will use smart default if null
-        this.responseSummary = builder.responseSummary;
-        this.provider = builder.provider;
-        this.model = builder.model;
-        this.tokenUsage = builder.tokenUsage;
-        this.latencyMs = builder.latencyMs;
-        this.metadata = builder.metadata != null
+  private AuditOptions(Builder builder) {
+    this.contextId = Objects.requireNonNull(builder.contextId, "contextId cannot be null");
+    this.clientId = builder.clientId; // Optional - SDK will use smart default if null
+    this.responseSummary = builder.responseSummary;
+    this.provider = builder.provider;
+    this.model = builder.model;
+    this.tokenUsage = builder.tokenUsage;
+    this.latencyMs = builder.latencyMs;
+    this.metadata =
+        builder.metadata != null
             ? Collections.unmodifiableMap(new HashMap<>(builder.metadata))
             : null;
-        this.success = builder.success;
-        this.errorMessage = builder.errorMessage;
-    }
-
-    public String getContextId() {
-        return contextId;
-    }
-
-    public String getClientId() {
-        return clientId;
-    }
-
-    public String getResponseSummary() {
-        return responseSummary;
-    }
+    this.success = builder.success;
+    this.errorMessage = builder.errorMessage;
+  }
+
+  public String getContextId() {
+    return contextId;
+  }
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getResponseSummary() {
+    return responseSummary;
+  }
+
+  public String getProvider() {
+    return provider;
+  }
+
+  public String getModel() {
+    return model;
+  }
+
+  public TokenUsage getTokenUsage() {
+    return tokenUsage;
+  }
+
+  public Long getLatencyMs() {
+    return latencyMs;
+  }
+
+  public Map getMetadata() {
+    return metadata;
+  }
+
+  public Boolean getSuccess() {
+    return success;
+  }
+
+  public String getErrorMessage() {
+    return errorMessage;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditOptions that = (AuditOptions) o;
+    return Objects.equals(contextId, that.contextId)
+        && Objects.equals(clientId, that.clientId)
+        && Objects.equals(responseSummary, that.responseSummary)
+        && Objects.equals(provider, that.provider)
+        && Objects.equals(model, that.model)
+        && Objects.equals(tokenUsage, that.tokenUsage)
+        && Objects.equals(latencyMs, that.latencyMs)
+        && Objects.equals(metadata, that.metadata)
+        && Objects.equals(success, that.success)
+        && Objects.equals(errorMessage, that.errorMessage);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        contextId,
+        clientId,
+        responseSummary,
+        provider,
+        model,
+        tokenUsage,
+        latencyMs,
+        metadata,
+        success,
+        errorMessage);
+  }
+
+  @Override
+  public String toString() {
+    return "AuditOptions{"
+        + "contextId='"
+        + contextId
+        + '\''
+        + ", clientId='"
+        + clientId
+        + '\''
+        + ", provider='"
+        + provider
+        + '\''
+        + ", model='"
+        + model
+        + '\''
+        + ", tokenUsage="
+        + tokenUsage
+        + ", latencyMs="
+        + latencyMs
+        + ", success="
+        + success
+        + '}';
+  }
+
+  /** Builder for AuditOptions. */
+  public static final class Builder {
+    private String contextId;
+    private String clientId;
+    private String responseSummary;
+    private String provider;
+    private String model;
+    private TokenUsage tokenUsage;
+    private Long latencyMs;
+    private Map metadata;
+    private Boolean success = true;
+    private String errorMessage;
+
+    private Builder() {}
 
-    public String getProvider() {
-        return provider;
+    /**
+     * Sets the context ID from the policy pre-check.
+     *
+     * @param contextId the context identifier from PolicyApprovalResult
+     * @return this builder
+     */
+    public Builder contextId(String contextId) {
+      this.contextId = contextId;
+      return this;
     }
 
-    public String getModel() {
-        return model;
+    /**
+     * Sets the client identifier.
+     *
+     * @param clientId the client identifier
+     * @return this builder
+     */
+    public Builder clientId(String clientId) {
+      this.clientId = clientId;
+      return this;
     }
 
-    public TokenUsage getTokenUsage() {
-        return tokenUsage;
+    /**
+     * Sets a summary of the LLM response.
+     *
+     * 

This should be a brief description or the actual response text. Sensitive information + * should be redacted. + * + * @param responseSummary the response summary + * @return this builder + */ + public Builder responseSummary(String responseSummary) { + this.responseSummary = responseSummary; + return this; } - public Long getLatencyMs() { - return latencyMs; + /** + * Sets the LLM provider name. + * + * @param provider the provider (e.g., "openai", "anthropic", "bedrock") + * @return this builder + */ + public Builder provider(String provider) { + this.provider = provider; + return this; } - public Map getMetadata() { - return metadata; + /** + * Sets the model used for the LLM call. + * + * @param model the model identifier (e.g., "gpt-4", "claude-3-opus") + * @return this builder + */ + public Builder model(String model) { + this.model = model; + return this; } - public Boolean getSuccess() { - return success; + /** + * Sets the token usage statistics. + * + * @param tokenUsage the token usage from the LLM response + * @return this builder + */ + public Builder tokenUsage(TokenUsage tokenUsage) { + this.tokenUsage = tokenUsage; + return this; } - public String getErrorMessage() { - return errorMessage; + /** + * Sets the latency of the LLM call in milliseconds. + * + * @param latencyMs the latency in milliseconds + * @return this builder + */ + public Builder latencyMs(long latencyMs) { + this.latencyMs = latencyMs; + return this; } - public static Builder builder() { - return new Builder(); + /** + * Sets additional metadata for the audit record. + * + * @param metadata key-value pairs of additional information + * @return this builder + */ + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - AuditOptions that = (AuditOptions) o; - return Objects.equals(contextId, that.contextId) && - Objects.equals(clientId, that.clientId) && - Objects.equals(responseSummary, that.responseSummary) && - Objects.equals(provider, that.provider) && - Objects.equals(model, that.model) && - Objects.equals(tokenUsage, that.tokenUsage) && - Objects.equals(latencyMs, that.latencyMs) && - Objects.equals(metadata, that.metadata) && - Objects.equals(success, that.success) && - Objects.equals(errorMessage, that.errorMessage); + /** + * Adds a single metadata entry. + * + * @param key the metadata key + * @param value the metadata value + * @return this builder + */ + public Builder addMetadata(String key, Object value) { + if (this.metadata == null) { + this.metadata = new HashMap<>(); + } + this.metadata.put(key, value); + return this; } - @Override - public int hashCode() { - return Objects.hash(contextId, clientId, responseSummary, provider, model, tokenUsage, - latencyMs, metadata, success, errorMessage); + /** + * Sets whether the LLM call was successful. + * + * @param success true if successful, false if failed + * @return this builder + */ + public Builder success(boolean success) { + this.success = success; + return this; } - @Override - public String toString() { - return "AuditOptions{" + - "contextId='" + contextId + '\'' + - ", clientId='" + clientId + '\'' + - ", provider='" + provider + '\'' + - ", model='" + model + '\'' + - ", tokenUsage=" + tokenUsage + - ", latencyMs=" + latencyMs + - ", success=" + success + - '}'; + /** + * Sets the error message if the LLM call failed. + * + * @param errorMessage the error message + * @return this builder + */ + public Builder errorMessage(String errorMessage) { + this.errorMessage = errorMessage; + return this; } /** - * Builder for AuditOptions. + * Builds the AuditOptions. + * + * @return a new AuditOptions instance + * @throws NullPointerException if contextId is null */ - public static final class Builder { - private String contextId; - private String clientId; - private String responseSummary; - private String provider; - private String model; - private TokenUsage tokenUsage; - private Long latencyMs; - private Map metadata; - private Boolean success = true; - private String errorMessage; - - private Builder() {} - - /** - * Sets the context ID from the policy pre-check. - * - * @param contextId the context identifier from PolicyApprovalResult - * @return this builder - */ - public Builder contextId(String contextId) { - this.contextId = contextId; - return this; - } - - /** - * Sets the client identifier. - * - * @param clientId the client identifier - * @return this builder - */ - public Builder clientId(String clientId) { - this.clientId = clientId; - return this; - } - - /** - * Sets a summary of the LLM response. - * - *

This should be a brief description or the actual response text. - * Sensitive information should be redacted. - * - * @param responseSummary the response summary - * @return this builder - */ - public Builder responseSummary(String responseSummary) { - this.responseSummary = responseSummary; - return this; - } - - /** - * Sets the LLM provider name. - * - * @param provider the provider (e.g., "openai", "anthropic", "bedrock") - * @return this builder - */ - public Builder provider(String provider) { - this.provider = provider; - return this; - } - - /** - * Sets the model used for the LLM call. - * - * @param model the model identifier (e.g., "gpt-4", "claude-3-opus") - * @return this builder - */ - public Builder model(String model) { - this.model = model; - return this; - } - - /** - * Sets the token usage statistics. - * - * @param tokenUsage the token usage from the LLM response - * @return this builder - */ - public Builder tokenUsage(TokenUsage tokenUsage) { - this.tokenUsage = tokenUsage; - return this; - } - - /** - * Sets the latency of the LLM call in milliseconds. - * - * @param latencyMs the latency in milliseconds - * @return this builder - */ - public Builder latencyMs(long latencyMs) { - this.latencyMs = latencyMs; - return this; - } - - /** - * Sets additional metadata for the audit record. - * - * @param metadata key-value pairs of additional information - * @return this builder - */ - public Builder metadata(Map metadata) { - this.metadata = metadata; - return this; - } - - /** - * Adds a single metadata entry. - * - * @param key the metadata key - * @param value the metadata value - * @return this builder - */ - public Builder addMetadata(String key, Object value) { - if (this.metadata == null) { - this.metadata = new HashMap<>(); - } - this.metadata.put(key, value); - return this; - } - - /** - * Sets whether the LLM call was successful. - * - * @param success true if successful, false if failed - * @return this builder - */ - public Builder success(boolean success) { - this.success = success; - return this; - } - - /** - * Sets the error message if the LLM call failed. - * - * @param errorMessage the error message - * @return this builder - */ - public Builder errorMessage(String errorMessage) { - this.errorMessage = errorMessage; - return this; - } - - /** - * Builds the AuditOptions. - * - * @return a new AuditOptions instance - * @throws NullPointerException if contextId is null - */ - public AuditOptions build() { - return new AuditOptions(this); - } + public AuditOptions build() { + return new AuditOptions(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditQueryOptions.java b/src/main/java/com/getaxonflow/sdk/types/AuditQueryOptions.java index 29e17e8..c50c2e0 100644 --- a/src/main/java/com/getaxonflow/sdk/types/AuditQueryOptions.java +++ b/src/main/java/com/getaxonflow/sdk/types/AuditQueryOptions.java @@ -21,6 +21,7 @@ * Options for querying audit logs by tenant. * *

Example usage: + * *

{@code
  * AuditQueryOptions options = AuditQueryOptions.builder()
  *     .limit(100)
@@ -32,84 +33,72 @@
  */
 public final class AuditQueryOptions {
 
-    private final int limit;
-    private final int offset;
-
-    private AuditQueryOptions(Builder builder) {
-        this.limit = Math.min(builder.limit != null ? builder.limit : 50, 1000);
-        this.offset = builder.offset != null ? builder.offset : 0;
-    }
-
-    /**
-     * Returns the maximum number of results to return.
-     */
-    public int getLimit() {
-        return limit;
-    }
-
-    /**
-     * Returns the pagination offset.
-     */
-    public int getOffset() {
-        return offset;
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    /**
-     * Creates default options with limit=50, offset=0.
-     */
-    public static AuditQueryOptions defaults() {
-        return builder().build();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditQueryOptions that = (AuditQueryOptions) o;
-        return limit == that.limit && offset == that.offset;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(limit, offset);
+  private final int limit;
+  private final int offset;
+
+  private AuditQueryOptions(Builder builder) {
+    this.limit = Math.min(builder.limit != null ? builder.limit : 50, 1000);
+    this.offset = builder.offset != null ? builder.offset : 0;
+  }
+
+  /** Returns the maximum number of results to return. */
+  public int getLimit() {
+    return limit;
+  }
+
+  /** Returns the pagination offset. */
+  public int getOffset() {
+    return offset;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  /** Creates default options with limit=50, offset=0. */
+  public static AuditQueryOptions defaults() {
+    return builder().build();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditQueryOptions that = (AuditQueryOptions) o;
+    return limit == that.limit && offset == that.offset;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(limit, offset);
+  }
+
+  @Override
+  public String toString() {
+    return "AuditQueryOptions{limit=" + limit + ", offset=" + offset + '}';
+  }
+
+  /** Builder for AuditQueryOptions. */
+  public static final class Builder {
+    private Integer limit;
+    private Integer offset;
+
+    private Builder() {}
+
+    /** Maximum results to return (default: 50, max: 1000). */
+    public Builder limit(int limit) {
+      this.limit = limit;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "AuditQueryOptions{limit=" + limit + ", offset=" + offset + '}';
+    /** Pagination offset (default: 0). */
+    public Builder offset(int offset) {
+      this.offset = offset;
+      return this;
     }
 
-    /**
-     * Builder for AuditQueryOptions.
-     */
-    public static final class Builder {
-        private Integer limit;
-        private Integer offset;
-
-        private Builder() {}
-
-        /**
-         * Maximum results to return (default: 50, max: 1000).
-         */
-        public Builder limit(int limit) {
-            this.limit = limit;
-            return this;
-        }
-
-        /**
-         * Pagination offset (default: 0).
-         */
-        public Builder offset(int offset) {
-            this.offset = offset;
-            return this;
-        }
-
-        public AuditQueryOptions build() {
-            return new AuditQueryOptions(this);
-        }
+    public AuditQueryOptions build() {
+      return new AuditQueryOptions(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditResult.java b/src/main/java/com/getaxonflow/sdk/types/AuditResult.java
index 5072daf..21052e2 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditResult.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditResult.java
@@ -17,97 +17,101 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Result of an audit call in Gateway Mode.
- */
+/** Result of an audit call in Gateway Mode. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class AuditResult {
 
-    @JsonProperty("success")
-    private final boolean success;
+  @JsonProperty("success")
+  private final boolean success;
 
-    @JsonProperty("audit_id")
-    private final String auditId;
+  @JsonProperty("audit_id")
+  private final String auditId;
 
-    @JsonProperty("message")
-    private final String message;
+  @JsonProperty("message")
+  private final String message;
 
-    @JsonProperty("error")
-    private final String error;
+  @JsonProperty("error")
+  private final String error;
 
-    public AuditResult(
-            @JsonProperty("success") boolean success,
-            @JsonProperty("audit_id") String auditId,
-            @JsonProperty("message") String message,
-            @JsonProperty("error") String error) {
-        this.success = success;
-        this.auditId = auditId;
-        this.message = message;
-        this.error = error;
-    }
+  public AuditResult(
+      @JsonProperty("success") boolean success,
+      @JsonProperty("audit_id") String auditId,
+      @JsonProperty("message") String message,
+      @JsonProperty("error") String error) {
+    this.success = success;
+    this.auditId = auditId;
+    this.message = message;
+    this.error = error;
+  }
 
-    /**
-     * Returns whether the audit was recorded successfully.
-     *
-     * @return true if successful
-     */
-    public boolean isSuccess() {
-        return success;
-    }
+  /**
+   * Returns whether the audit was recorded successfully.
+   *
+   * @return true if successful
+   */
+  public boolean isSuccess() {
+    return success;
+  }
 
-    /**
-     * Returns the unique identifier for this audit record.
-     *
-     * @return the audit ID
-     */
-    public String getAuditId() {
-        return auditId;
-    }
+  /**
+   * Returns the unique identifier for this audit record.
+   *
+   * @return the audit ID
+   */
+  public String getAuditId() {
+    return auditId;
+  }
 
-    /**
-     * Returns any message from the audit operation.
-     *
-     * @return the message, may be null
-     */
-    public String getMessage() {
-        return message;
-    }
+  /**
+   * Returns any message from the audit operation.
+   *
+   * @return the message, may be null
+   */
+  public String getMessage() {
+    return message;
+  }
 
-    /**
-     * Returns the error message if the audit failed.
-     *
-     * @return the error message, or null if successful
-     */
-    public String getError() {
-        return error;
-    }
+  /**
+   * Returns the error message if the audit failed.
+   *
+   * @return the error message, or null if successful
+   */
+  public String getError() {
+    return error;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditResult that = (AuditResult) o;
-        return success == that.success &&
-               Objects.equals(auditId, that.auditId) &&
-               Objects.equals(message, that.message) &&
-               Objects.equals(error, that.error);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditResult that = (AuditResult) o;
+    return success == that.success
+        && Objects.equals(auditId, that.auditId)
+        && Objects.equals(message, that.message)
+        && Objects.equals(error, that.error);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(success, auditId, message, error);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(success, auditId, message, error);
+  }
 
-    @Override
-    public String toString() {
-        return "AuditResult{" +
-               "success=" + success +
-               ", auditId='" + auditId + '\'' +
-               ", message='" + message + '\'' +
-               ", error='" + error + '\'' +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "AuditResult{"
+        + "success="
+        + success
+        + ", auditId='"
+        + auditId
+        + '\''
+        + ", message='"
+        + message
+        + '\''
+        + ", error='"
+        + error
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditSearchRequest.java b/src/main/java/com/getaxonflow/sdk/types/AuditSearchRequest.java
index 3f47a22..a74ed11 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditSearchRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditSearchRequest.java
@@ -17,7 +17,6 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.time.Instant;
 import java.util.Objects;
 
@@ -27,6 +26,7 @@
  * 

All fields are optional - omit to search all logs with default limit. * *

Example usage: + * *

{@code
  * AuditSearchRequest request = AuditSearchRequest.builder()
  *     .userEmail("analyst@company.com")
@@ -40,171 +40,163 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class AuditSearchRequest {
 
-    @JsonProperty("user_email")
-    private final String userEmail;
-
-    @JsonProperty("client_id")
-    private final String clientId;
-
-    @JsonProperty("start_time")
-    private final String startTime;
-
-    @JsonProperty("end_time")
-    private final String endTime;
-
-    @JsonProperty("request_type")
-    private final String requestType;
-
-    @JsonProperty("limit")
-    private final Integer limit;
-
-    @JsonProperty("offset")
-    private final Integer offset;
-
-    private AuditSearchRequest(Builder builder) {
-        this.userEmail = builder.userEmail;
-        this.clientId = builder.clientId;
-        this.startTime = builder.startTime != null ? builder.startTime.toString() : null;
-        this.endTime = builder.endTime != null ? builder.endTime.toString() : null;
-        this.requestType = builder.requestType;
-        this.limit = builder.limit != null ? Math.min(builder.limit, 1000) : 100;
-        this.offset = builder.offset;
-    }
-
-    public String getUserEmail() {
-        return userEmail;
-    }
-
-    public String getClientId() {
-        return clientId;
-    }
-
-    public String getStartTime() {
-        return startTime;
-    }
-
-    public String getEndTime() {
-        return endTime;
-    }
-
-    public String getRequestType() {
-        return requestType;
+  @JsonProperty("user_email")
+  private final String userEmail;
+
+  @JsonProperty("client_id")
+  private final String clientId;
+
+  @JsonProperty("start_time")
+  private final String startTime;
+
+  @JsonProperty("end_time")
+  private final String endTime;
+
+  @JsonProperty("request_type")
+  private final String requestType;
+
+  @JsonProperty("limit")
+  private final Integer limit;
+
+  @JsonProperty("offset")
+  private final Integer offset;
+
+  private AuditSearchRequest(Builder builder) {
+    this.userEmail = builder.userEmail;
+    this.clientId = builder.clientId;
+    this.startTime = builder.startTime != null ? builder.startTime.toString() : null;
+    this.endTime = builder.endTime != null ? builder.endTime.toString() : null;
+    this.requestType = builder.requestType;
+    this.limit = builder.limit != null ? Math.min(builder.limit, 1000) : 100;
+    this.offset = builder.offset;
+  }
+
+  public String getUserEmail() {
+    return userEmail;
+  }
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getStartTime() {
+    return startTime;
+  }
+
+  public String getEndTime() {
+    return endTime;
+  }
+
+  public String getRequestType() {
+    return requestType;
+  }
+
+  public Integer getLimit() {
+    return limit;
+  }
+
+  public Integer getOffset() {
+    return offset;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditSearchRequest that = (AuditSearchRequest) o;
+    return Objects.equals(userEmail, that.userEmail)
+        && Objects.equals(clientId, that.clientId)
+        && Objects.equals(startTime, that.startTime)
+        && Objects.equals(endTime, that.endTime)
+        && Objects.equals(requestType, that.requestType)
+        && Objects.equals(limit, that.limit)
+        && Objects.equals(offset, that.offset);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(userEmail, clientId, startTime, endTime, requestType, limit, offset);
+  }
+
+  @Override
+  public String toString() {
+    return "AuditSearchRequest{"
+        + "userEmail='"
+        + userEmail
+        + '\''
+        + ", clientId='"
+        + clientId
+        + '\''
+        + ", requestType='"
+        + requestType
+        + '\''
+        + ", limit="
+        + limit
+        + ", offset="
+        + offset
+        + '}';
+  }
+
+  /** Builder for AuditSearchRequest. */
+  public static final class Builder {
+    private String userEmail;
+    private String clientId;
+    private Instant startTime;
+    private Instant endTime;
+    private String requestType;
+    private Integer limit;
+    private Integer offset;
+
+    private Builder() {}
+
+    /** Filter by user email. */
+    public Builder userEmail(String userEmail) {
+      this.userEmail = userEmail;
+      return this;
     }
 
-    public Integer getLimit() {
-        return limit;
+    /** Filter by client/application ID. */
+    public Builder clientId(String clientId) {
+      this.clientId = clientId;
+      return this;
     }
 
-    public Integer getOffset() {
-        return offset;
+    /** Start of time range to search. */
+    public Builder startTime(Instant startTime) {
+      this.startTime = startTime;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    /** End of time range to search. */
+    public Builder endTime(Instant endTime) {
+      this.endTime = endTime;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditSearchRequest that = (AuditSearchRequest) o;
-        return Objects.equals(userEmail, that.userEmail) &&
-               Objects.equals(clientId, that.clientId) &&
-               Objects.equals(startTime, that.startTime) &&
-               Objects.equals(endTime, that.endTime) &&
-               Objects.equals(requestType, that.requestType) &&
-               Objects.equals(limit, that.limit) &&
-               Objects.equals(offset, that.offset);
+    /** Filter by request type (e.g., "llm_chat", "policy_check"). */
+    public Builder requestType(String requestType) {
+      this.requestType = requestType;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(userEmail, clientId, startTime, endTime, requestType, limit, offset);
+    /** Maximum results to return (default: 100, max: 1000). */
+    public Builder limit(int limit) {
+      this.limit = limit;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "AuditSearchRequest{" +
-               "userEmail='" + userEmail + '\'' +
-               ", clientId='" + clientId + '\'' +
-               ", requestType='" + requestType + '\'' +
-               ", limit=" + limit +
-               ", offset=" + offset +
-               '}';
+    /** Pagination offset (default: 0). */
+    public Builder offset(int offset) {
+      this.offset = offset;
+      return this;
     }
 
-    /**
-     * Builder for AuditSearchRequest.
-     */
-    public static final class Builder {
-        private String userEmail;
-        private String clientId;
-        private Instant startTime;
-        private Instant endTime;
-        private String requestType;
-        private Integer limit;
-        private Integer offset;
-
-        private Builder() {}
-
-        /**
-         * Filter by user email.
-         */
-        public Builder userEmail(String userEmail) {
-            this.userEmail = userEmail;
-            return this;
-        }
-
-        /**
-         * Filter by client/application ID.
-         */
-        public Builder clientId(String clientId) {
-            this.clientId = clientId;
-            return this;
-        }
-
-        /**
-         * Start of time range to search.
-         */
-        public Builder startTime(Instant startTime) {
-            this.startTime = startTime;
-            return this;
-        }
-
-        /**
-         * End of time range to search.
-         */
-        public Builder endTime(Instant endTime) {
-            this.endTime = endTime;
-            return this;
-        }
-
-        /**
-         * Filter by request type (e.g., "llm_chat", "policy_check").
-         */
-        public Builder requestType(String requestType) {
-            this.requestType = requestType;
-            return this;
-        }
-
-        /**
-         * Maximum results to return (default: 100, max: 1000).
-         */
-        public Builder limit(int limit) {
-            this.limit = limit;
-            return this;
-        }
-
-        /**
-         * Pagination offset (default: 0).
-         */
-        public Builder offset(int offset) {
-            this.offset = offset;
-            return this;
-        }
-
-        public AuditSearchRequest build() {
-            return new AuditSearchRequest(this);
-        }
+    public AuditSearchRequest build() {
+      return new AuditSearchRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditSearchResponse.java b/src/main/java/com/getaxonflow/sdk/types/AuditSearchResponse.java
index 52b2d46..2d53815 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditSearchResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditSearchResponse.java
@@ -17,102 +17,100 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Response from an audit search operation.
- */
+/** Response from an audit search operation. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class AuditSearchResponse {
 
-    @JsonProperty("entries")
-    private final List entries;
-
-    @JsonProperty("total")
-    private final int total;
-
-    @JsonProperty("limit")
-    private final int limit;
-
-    @JsonProperty("offset")
-    private final int offset;
-
-    public AuditSearchResponse(
-            @JsonProperty("entries") List entries,
-            @JsonProperty("total") Integer total,
-            @JsonProperty("limit") Integer limit,
-            @JsonProperty("offset") Integer offset) {
-        this.entries = entries != null ? entries : Collections.emptyList();
-        this.total = total != null ? total : this.entries.size();
-        this.limit = limit != null ? limit : 100;
-        this.offset = offset != null ? offset : 0;
-    }
-
-    /**
-     * Creates a response with the given entries and metadata.
-     */
-    public static AuditSearchResponse of(List entries, int total, int limit, int offset) {
-        return new AuditSearchResponse(entries, total, limit, offset);
-    }
-
-    /**
-     * Creates a response from an array (direct API response format).
-     */
-    public static AuditSearchResponse fromArray(List entries, int limit, int offset) {
-        return new AuditSearchResponse(entries, entries.size(), limit, offset);
-    }
-
-    /** Returns the audit log entries matching the search. */
-    public List getEntries() {
-        return entries;
-    }
-
-    /** Returns the total number of matching entries (for pagination). */
-    public int getTotal() {
-        return total;
-    }
-
-    /** Returns the limit that was applied. */
-    public int getLimit() {
-        return limit;
-    }
-
-    /** Returns the offset that was applied. */
-    public int getOffset() {
-        return offset;
-    }
-
-    /** Returns true if there are more results available. */
-    public boolean hasMore() {
-        return offset + entries.size() < total;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditSearchResponse that = (AuditSearchResponse) o;
-        return total == that.total &&
-               limit == that.limit &&
-               offset == that.offset &&
-               Objects.equals(entries, that.entries);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(entries, total, limit, offset);
-    }
-
-    @Override
-    public String toString() {
-        return "AuditSearchResponse{" +
-               "entriesCount=" + entries.size() +
-               ", total=" + total +
-               ", limit=" + limit +
-               ", offset=" + offset +
-               '}';
-    }
+  @JsonProperty("entries")
+  private final List entries;
+
+  @JsonProperty("total")
+  private final int total;
+
+  @JsonProperty("limit")
+  private final int limit;
+
+  @JsonProperty("offset")
+  private final int offset;
+
+  public AuditSearchResponse(
+      @JsonProperty("entries") List entries,
+      @JsonProperty("total") Integer total,
+      @JsonProperty("limit") Integer limit,
+      @JsonProperty("offset") Integer offset) {
+    this.entries = entries != null ? entries : Collections.emptyList();
+    this.total = total != null ? total : this.entries.size();
+    this.limit = limit != null ? limit : 100;
+    this.offset = offset != null ? offset : 0;
+  }
+
+  /** Creates a response with the given entries and metadata. */
+  public static AuditSearchResponse of(
+      List entries, int total, int limit, int offset) {
+    return new AuditSearchResponse(entries, total, limit, offset);
+  }
+
+  /** Creates a response from an array (direct API response format). */
+  public static AuditSearchResponse fromArray(List entries, int limit, int offset) {
+    return new AuditSearchResponse(entries, entries.size(), limit, offset);
+  }
+
+  /** Returns the audit log entries matching the search. */
+  public List getEntries() {
+    return entries;
+  }
+
+  /** Returns the total number of matching entries (for pagination). */
+  public int getTotal() {
+    return total;
+  }
+
+  /** Returns the limit that was applied. */
+  public int getLimit() {
+    return limit;
+  }
+
+  /** Returns the offset that was applied. */
+  public int getOffset() {
+    return offset;
+  }
+
+  /** Returns true if there are more results available. */
+  public boolean hasMore() {
+    return offset + entries.size() < total;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditSearchResponse that = (AuditSearchResponse) o;
+    return total == that.total
+        && limit == that.limit
+        && offset == that.offset
+        && Objects.equals(entries, that.entries);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(entries, total, limit, offset);
+  }
+
+  @Override
+  public String toString() {
+    return "AuditSearchResponse{"
+        + "entriesCount="
+        + entries.size()
+        + ", total="
+        + total
+        + ", limit="
+        + limit
+        + ", offset="
+        + offset
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditToolCallRequest.java b/src/main/java/com/getaxonflow/sdk/types/AuditToolCallRequest.java
index b43c807..d802d55 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditToolCallRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditToolCallRequest.java
@@ -17,7 +17,6 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -28,10 +27,11 @@
 /**
  * Request to audit a non-LLM tool call.
  *
- * 

Records tool invocations (function calls, MCP operations, API calls) - * for compliance and observability. + *

Records tool invocations (function calls, MCP operations, API calls) for compliance and + * observability. * *

Example usage: + * *

{@code
  * AuditToolCallRequest request = AuditToolCallRequest.builder()
  *     .toolName("web_search")
@@ -49,295 +49,314 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class AuditToolCallRequest {
 
-    @JsonProperty("tool_name")
-    private final String toolName;
+  @JsonProperty("tool_name")
+  private final String toolName;
 
-    @JsonProperty("tool_type")
-    private final String toolType;
+  @JsonProperty("tool_type")
+  private final String toolType;
 
-    @JsonProperty("input")
-    private final Map input;
+  @JsonProperty("input")
+  private final Map input;
 
-    @JsonProperty("output")
-    private final Map output;
+  @JsonProperty("output")
+  private final Map output;
 
-    @JsonProperty("workflow_id")
-    private final String workflowId;
+  @JsonProperty("workflow_id")
+  private final String workflowId;
 
-    @JsonProperty("step_id")
-    private final String stepId;
+  @JsonProperty("step_id")
+  private final String stepId;
 
-    @JsonProperty("user_id")
-    private final String userId;
+  @JsonProperty("user_id")
+  private final String userId;
 
-    @JsonProperty("duration_ms")
-    private final Long durationMs;
+  @JsonProperty("duration_ms")
+  private final Long durationMs;
 
-    @JsonProperty("policies_applied")
-    private final List policiesApplied;
+  @JsonProperty("policies_applied")
+  private final List policiesApplied;
 
-    @JsonProperty("success")
-    private final Boolean success;
+  @JsonProperty("success")
+  private final Boolean success;
 
-    @JsonProperty("error_message")
-    private final String errorMessage;
+  @JsonProperty("error_message")
+  private final String errorMessage;
 
-    private AuditToolCallRequest(Builder builder) {
-        this.toolName = Objects.requireNonNull(builder.toolName, "toolName cannot be null");
-        if (builder.toolName.isEmpty()) {
-            throw new IllegalArgumentException("toolName cannot be empty");
-        }
-        this.toolType = builder.toolType;
-        this.input = builder.input != null
-            ? Collections.unmodifiableMap(new HashMap<>(builder.input))
-            : null;
-        this.output = builder.output != null
-            ? Collections.unmodifiableMap(new HashMap<>(builder.output))
-            : null;
-        this.workflowId = builder.workflowId;
-        this.stepId = builder.stepId;
-        this.userId = builder.userId;
-        this.durationMs = builder.durationMs;
-        this.policiesApplied = builder.policiesApplied != null
+  private AuditToolCallRequest(Builder builder) {
+    this.toolName = Objects.requireNonNull(builder.toolName, "toolName cannot be null");
+    if (builder.toolName.isEmpty()) {
+      throw new IllegalArgumentException("toolName cannot be empty");
+    }
+    this.toolType = builder.toolType;
+    this.input =
+        builder.input != null ? Collections.unmodifiableMap(new HashMap<>(builder.input)) : null;
+    this.output =
+        builder.output != null ? Collections.unmodifiableMap(new HashMap<>(builder.output)) : null;
+    this.workflowId = builder.workflowId;
+    this.stepId = builder.stepId;
+    this.userId = builder.userId;
+    this.durationMs = builder.durationMs;
+    this.policiesApplied =
+        builder.policiesApplied != null
             ? Collections.unmodifiableList(new ArrayList<>(builder.policiesApplied))
             : null;
-        this.success = builder.success;
-        this.errorMessage = builder.errorMessage;
-    }
-
-    public String getToolName() {
-        return toolName;
-    }
-
-    public String getToolType() {
-        return toolType;
-    }
-
-    public Map getInput() {
-        return input;
-    }
-
-    public Map getOutput() {
-        return output;
-    }
+    this.success = builder.success;
+    this.errorMessage = builder.errorMessage;
+  }
+
+  public String getToolName() {
+    return toolName;
+  }
+
+  public String getToolType() {
+    return toolType;
+  }
+
+  public Map getInput() {
+    return input;
+  }
+
+  public Map getOutput() {
+    return output;
+  }
+
+  public String getWorkflowId() {
+    return workflowId;
+  }
+
+  public String getStepId() {
+    return stepId;
+  }
+
+  public String getUserId() {
+    return userId;
+  }
+
+  public Long getDurationMs() {
+    return durationMs;
+  }
+
+  public List getPoliciesApplied() {
+    return policiesApplied;
+  }
+
+  public Boolean getSuccess() {
+    return success;
+  }
+
+  public String getErrorMessage() {
+    return errorMessage;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditToolCallRequest that = (AuditToolCallRequest) o;
+    return Objects.equals(toolName, that.toolName)
+        && Objects.equals(toolType, that.toolType)
+        && Objects.equals(input, that.input)
+        && Objects.equals(output, that.output)
+        && Objects.equals(workflowId, that.workflowId)
+        && Objects.equals(stepId, that.stepId)
+        && Objects.equals(userId, that.userId)
+        && Objects.equals(durationMs, that.durationMs)
+        && Objects.equals(policiesApplied, that.policiesApplied)
+        && Objects.equals(success, that.success)
+        && Objects.equals(errorMessage, that.errorMessage);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        toolName,
+        toolType,
+        input,
+        output,
+        workflowId,
+        stepId,
+        userId,
+        durationMs,
+        policiesApplied,
+        success,
+        errorMessage);
+  }
+
+  @Override
+  public String toString() {
+    return "AuditToolCallRequest{"
+        + "toolName='"
+        + toolName
+        + '\''
+        + ", toolType='"
+        + toolType
+        + '\''
+        + ", workflowId='"
+        + workflowId
+        + '\''
+        + ", stepId='"
+        + stepId
+        + '\''
+        + ", userId='"
+        + userId
+        + '\''
+        + ", durationMs="
+        + durationMs
+        + ", success="
+        + success
+        + '}';
+  }
+
+  /** Builder for AuditToolCallRequest. */
+  public static final class Builder {
+    private String toolName;
+    private String toolType;
+    private Map input;
+    private Map output;
+    private String workflowId;
+    private String stepId;
+    private String userId;
+    private Long durationMs;
+    private List policiesApplied;
+    private Boolean success;
+    private String errorMessage;
+
+    private Builder() {}
 
-    public String getWorkflowId() {
-        return workflowId;
+    /**
+     * Sets the name of the tool that was called (required).
+     *
+     * @param toolName the tool name
+     * @return this builder
+     */
+    public Builder toolName(String toolName) {
+      this.toolName = toolName;
+      return this;
     }
 
-    public String getStepId() {
-        return stepId;
+    /**
+     * Sets the type of tool call.
+     *
+     * @param toolType the tool type (e.g., "function", "mcp", "api")
+     * @return this builder
+     */
+    public Builder toolType(String toolType) {
+      this.toolType = toolType;
+      return this;
     }
 
-    public String getUserId() {
-        return userId;
+    /**
+     * Sets the input parameters passed to the tool.
+     *
+     * @param input the input map
+     * @return this builder
+     */
+    public Builder input(Map input) {
+      this.input = input;
+      return this;
     }
 
-    public Long getDurationMs() {
-        return durationMs;
+    /**
+     * Sets the output returned by the tool.
+     *
+     * @param output the output map
+     * @return this builder
+     */
+    public Builder output(Map output) {
+      this.output = output;
+      return this;
     }
 
-    public List getPoliciesApplied() {
-        return policiesApplied;
+    /**
+     * Sets the workflow ID this tool call belongs to.
+     *
+     * @param workflowId the workflow identifier
+     * @return this builder
+     */
+    public Builder workflowId(String workflowId) {
+      this.workflowId = workflowId;
+      return this;
     }
 
-    public Boolean getSuccess() {
-        return success;
+    /**
+     * Sets the step ID within the workflow.
+     *
+     * @param stepId the step identifier
+     * @return this builder
+     */
+    public Builder stepId(String stepId) {
+      this.stepId = stepId;
+      return this;
     }
 
-    public String getErrorMessage() {
-        return errorMessage;
+    /**
+     * Sets the user who initiated the tool call.
+     *
+     * @param userId the user identifier
+     * @return this builder
+     */
+    public Builder userId(String userId) {
+      this.userId = userId;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    /**
+     * Sets the duration of the tool call in milliseconds.
+     *
+     * @param durationMs the duration in milliseconds
+     * @return this builder
+     */
+    public Builder durationMs(long durationMs) {
+      this.durationMs = durationMs;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditToolCallRequest that = (AuditToolCallRequest) o;
-        return Objects.equals(toolName, that.toolName) &&
-               Objects.equals(toolType, that.toolType) &&
-               Objects.equals(input, that.input) &&
-               Objects.equals(output, that.output) &&
-               Objects.equals(workflowId, that.workflowId) &&
-               Objects.equals(stepId, that.stepId) &&
-               Objects.equals(userId, that.userId) &&
-               Objects.equals(durationMs, that.durationMs) &&
-               Objects.equals(policiesApplied, that.policiesApplied) &&
-               Objects.equals(success, that.success) &&
-               Objects.equals(errorMessage, that.errorMessage);
+    /**
+     * Sets the list of policies that were applied to this tool call.
+     *
+     * @param policiesApplied the policy names
+     * @return this builder
+     */
+    public Builder policiesApplied(List policiesApplied) {
+      this.policiesApplied = policiesApplied;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(toolName, toolType, input, output, workflowId, stepId, userId,
-                           durationMs, policiesApplied, success, errorMessage);
+    /**
+     * Sets whether the tool call was successful.
+     *
+     * @param success true if successful, false if failed
+     * @return this builder
+     */
+    public Builder success(boolean success) {
+      this.success = success;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "AuditToolCallRequest{" +
-               "toolName='" + toolName + '\'' +
-               ", toolType='" + toolType + '\'' +
-               ", workflowId='" + workflowId + '\'' +
-               ", stepId='" + stepId + '\'' +
-               ", userId='" + userId + '\'' +
-               ", durationMs=" + durationMs +
-               ", success=" + success +
-               '}';
+    /**
+     * Sets the error message if the tool call failed.
+     *
+     * @param errorMessage the error message
+     * @return this builder
+     */
+    public Builder errorMessage(String errorMessage) {
+      this.errorMessage = errorMessage;
+      return this;
     }
 
     /**
-     * Builder for AuditToolCallRequest.
+     * Builds the AuditToolCallRequest.
+     *
+     * @return a new AuditToolCallRequest instance
+     * @throws NullPointerException if toolName is null
+     * @throws IllegalArgumentException if toolName is empty
      */
-    public static final class Builder {
-        private String toolName;
-        private String toolType;
-        private Map input;
-        private Map output;
-        private String workflowId;
-        private String stepId;
-        private String userId;
-        private Long durationMs;
-        private List policiesApplied;
-        private Boolean success;
-        private String errorMessage;
-
-        private Builder() {}
-
-        /**
-         * Sets the name of the tool that was called (required).
-         *
-         * @param toolName the tool name
-         * @return this builder
-         */
-        public Builder toolName(String toolName) {
-            this.toolName = toolName;
-            return this;
-        }
-
-        /**
-         * Sets the type of tool call.
-         *
-         * @param toolType the tool type (e.g., "function", "mcp", "api")
-         * @return this builder
-         */
-        public Builder toolType(String toolType) {
-            this.toolType = toolType;
-            return this;
-        }
-
-        /**
-         * Sets the input parameters passed to the tool.
-         *
-         * @param input the input map
-         * @return this builder
-         */
-        public Builder input(Map input) {
-            this.input = input;
-            return this;
-        }
-
-        /**
-         * Sets the output returned by the tool.
-         *
-         * @param output the output map
-         * @return this builder
-         */
-        public Builder output(Map output) {
-            this.output = output;
-            return this;
-        }
-
-        /**
-         * Sets the workflow ID this tool call belongs to.
-         *
-         * @param workflowId the workflow identifier
-         * @return this builder
-         */
-        public Builder workflowId(String workflowId) {
-            this.workflowId = workflowId;
-            return this;
-        }
-
-        /**
-         * Sets the step ID within the workflow.
-         *
-         * @param stepId the step identifier
-         * @return this builder
-         */
-        public Builder stepId(String stepId) {
-            this.stepId = stepId;
-            return this;
-        }
-
-        /**
-         * Sets the user who initiated the tool call.
-         *
-         * @param userId the user identifier
-         * @return this builder
-         */
-        public Builder userId(String userId) {
-            this.userId = userId;
-            return this;
-        }
-
-        /**
-         * Sets the duration of the tool call in milliseconds.
-         *
-         * @param durationMs the duration in milliseconds
-         * @return this builder
-         */
-        public Builder durationMs(long durationMs) {
-            this.durationMs = durationMs;
-            return this;
-        }
-
-        /**
-         * Sets the list of policies that were applied to this tool call.
-         *
-         * @param policiesApplied the policy names
-         * @return this builder
-         */
-        public Builder policiesApplied(List policiesApplied) {
-            this.policiesApplied = policiesApplied;
-            return this;
-        }
-
-        /**
-         * Sets whether the tool call was successful.
-         *
-         * @param success true if successful, false if failed
-         * @return this builder
-         */
-        public Builder success(boolean success) {
-            this.success = success;
-            return this;
-        }
-
-        /**
-         * Sets the error message if the tool call failed.
-         *
-         * @param errorMessage the error message
-         * @return this builder
-         */
-        public Builder errorMessage(String errorMessage) {
-            this.errorMessage = errorMessage;
-            return this;
-        }
-
-        /**
-         * Builds the AuditToolCallRequest.
-         *
-         * @return a new AuditToolCallRequest instance
-         * @throws NullPointerException if toolName is null
-         * @throws IllegalArgumentException if toolName is empty
-         */
-        public AuditToolCallRequest build() {
-            return new AuditToolCallRequest(this);
-        }
+    public AuditToolCallRequest build() {
+      return new AuditToolCallRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/AuditToolCallResponse.java b/src/main/java/com/getaxonflow/sdk/types/AuditToolCallResponse.java
index 63b4e8f..19a69e0 100644
--- a/src/main/java/com/getaxonflow/sdk/types/AuditToolCallResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/AuditToolCallResponse.java
@@ -17,81 +17,84 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Response from auditing a tool call.
- */
+/** Response from auditing a tool call. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class AuditToolCallResponse {
 
-    @JsonProperty("audit_id")
-    private final String auditId;
+  @JsonProperty("audit_id")
+  private final String auditId;
 
-    @JsonProperty("status")
-    private final String status;
+  @JsonProperty("status")
+  private final String status;
 
-    @JsonProperty("timestamp")
-    private final String timestamp;
+  @JsonProperty("timestamp")
+  private final String timestamp;
 
-    public AuditToolCallResponse(
-            @JsonProperty("audit_id") String auditId,
-            @JsonProperty("status") String status,
-            @JsonProperty("timestamp") String timestamp) {
-        this.auditId = auditId;
-        this.status = status;
-        this.timestamp = timestamp;
-    }
+  public AuditToolCallResponse(
+      @JsonProperty("audit_id") String auditId,
+      @JsonProperty("status") String status,
+      @JsonProperty("timestamp") String timestamp) {
+    this.auditId = auditId;
+    this.status = status;
+    this.timestamp = timestamp;
+  }
 
-    /**
-     * Returns the unique identifier for this audit record.
-     *
-     * @return the audit ID
-     */
-    public String getAuditId() {
-        return auditId;
-    }
+  /**
+   * Returns the unique identifier for this audit record.
+   *
+   * @return the audit ID
+   */
+  public String getAuditId() {
+    return auditId;
+  }
 
-    /**
-     * Returns the status of the audit operation.
-     *
-     * @return the status
-     */
-    public String getStatus() {
-        return status;
-    }
+  /**
+   * Returns the status of the audit operation.
+   *
+   * @return the status
+   */
+  public String getStatus() {
+    return status;
+  }
 
-    /**
-     * Returns the timestamp when the audit was recorded.
-     *
-     * @return the timestamp as an ISO 8601 string
-     */
-    public String getTimestamp() {
-        return timestamp;
-    }
+  /**
+   * Returns the timestamp when the audit was recorded.
+   *
+   * @return the timestamp as an ISO 8601 string
+   */
+  public String getTimestamp() {
+    return timestamp;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        AuditToolCallResponse that = (AuditToolCallResponse) o;
-        return Objects.equals(auditId, that.auditId) &&
-               Objects.equals(status, that.status) &&
-               Objects.equals(timestamp, that.timestamp);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    AuditToolCallResponse that = (AuditToolCallResponse) o;
+    return Objects.equals(auditId, that.auditId)
+        && Objects.equals(status, that.status)
+        && Objects.equals(timestamp, that.timestamp);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(auditId, status, timestamp);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(auditId, status, timestamp);
+  }
 
-    @Override
-    public String toString() {
-        return "AuditToolCallResponse{" +
-               "auditId='" + auditId + '\'' +
-               ", status='" + status + '\'' +
-               ", timestamp='" + timestamp + '\'' +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "AuditToolCallResponse{"
+        + "auditId='"
+        + auditId
+        + '\''
+        + ", status='"
+        + status
+        + '\''
+        + ", timestamp='"
+        + timestamp
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/BudgetInfo.java b/src/main/java/com/getaxonflow/sdk/types/BudgetInfo.java
index d035184..e8e51f5 100644
--- a/src/main/java/com/getaxonflow/sdk/types/BudgetInfo.java
+++ b/src/main/java/com/getaxonflow/sdk/types/BudgetInfo.java
@@ -17,93 +17,121 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
 /**
  * Budget enforcement status information (Issue #1082).
  *
- * Returned when a budget check is performed, showing current usage
- * relative to budget limits.
+ * 

Returned when a budget check is performed, showing current usage relative to budget limits. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class BudgetInfo { - @JsonProperty("budget_id") - private final String budgetId; - - @JsonProperty("budget_name") - private final String budgetName; - - @JsonProperty("used_usd") - private final double usedUsd; - - @JsonProperty("limit_usd") - private final double limitUsd; - - @JsonProperty("percentage") - private final double percentage; - - @JsonProperty("exceeded") - private final boolean exceeded; - - @JsonProperty("action") - private final String action; - - public BudgetInfo( - @JsonProperty("budget_id") String budgetId, - @JsonProperty("budget_name") String budgetName, - @JsonProperty("used_usd") double usedUsd, - @JsonProperty("limit_usd") double limitUsd, - @JsonProperty("percentage") double percentage, - @JsonProperty("exceeded") boolean exceeded, - @JsonProperty("action") String action) { - this.budgetId = budgetId; - this.budgetName = budgetName; - this.usedUsd = usedUsd; - this.limitUsd = limitUsd; - this.percentage = percentage; - this.exceeded = exceeded; - this.action = action; - } - - public String getBudgetId() { return budgetId; } - public String getBudgetName() { return budgetName; } - public double getUsedUsd() { return usedUsd; } - public double getLimitUsd() { return limitUsd; } - public double getPercentage() { return percentage; } - public boolean isExceeded() { return exceeded; } - public String getAction() { return action; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - BudgetInfo that = (BudgetInfo) o; - return Double.compare(that.usedUsd, usedUsd) == 0 && - Double.compare(that.limitUsd, limitUsd) == 0 && - Double.compare(that.percentage, percentage) == 0 && - exceeded == that.exceeded && - Objects.equals(budgetId, that.budgetId) && - Objects.equals(budgetName, that.budgetName) && - Objects.equals(action, that.action); - } - - @Override - public int hashCode() { - return Objects.hash(budgetId, budgetName, usedUsd, limitUsd, percentage, exceeded, action); - } - - @Override - public String toString() { - return "BudgetInfo{" + - "budgetId='" + budgetId + '\'' + - ", budgetName='" + budgetName + '\'' + - ", usedUsd=" + usedUsd + - ", limitUsd=" + limitUsd + - ", percentage=" + percentage + - ", exceeded=" + exceeded + - ", action='" + action + '\'' + - '}'; - } + @JsonProperty("budget_id") + private final String budgetId; + + @JsonProperty("budget_name") + private final String budgetName; + + @JsonProperty("used_usd") + private final double usedUsd; + + @JsonProperty("limit_usd") + private final double limitUsd; + + @JsonProperty("percentage") + private final double percentage; + + @JsonProperty("exceeded") + private final boolean exceeded; + + @JsonProperty("action") + private final String action; + + public BudgetInfo( + @JsonProperty("budget_id") String budgetId, + @JsonProperty("budget_name") String budgetName, + @JsonProperty("used_usd") double usedUsd, + @JsonProperty("limit_usd") double limitUsd, + @JsonProperty("percentage") double percentage, + @JsonProperty("exceeded") boolean exceeded, + @JsonProperty("action") String action) { + this.budgetId = budgetId; + this.budgetName = budgetName; + this.usedUsd = usedUsd; + this.limitUsd = limitUsd; + this.percentage = percentage; + this.exceeded = exceeded; + this.action = action; + } + + public String getBudgetId() { + return budgetId; + } + + public String getBudgetName() { + return budgetName; + } + + public double getUsedUsd() { + return usedUsd; + } + + public double getLimitUsd() { + return limitUsd; + } + + public double getPercentage() { + return percentage; + } + + public boolean isExceeded() { + return exceeded; + } + + public String getAction() { + return action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + BudgetInfo that = (BudgetInfo) o; + return Double.compare(that.usedUsd, usedUsd) == 0 + && Double.compare(that.limitUsd, limitUsd) == 0 + && Double.compare(that.percentage, percentage) == 0 + && exceeded == that.exceeded + && Objects.equals(budgetId, that.budgetId) + && Objects.equals(budgetName, that.budgetName) + && Objects.equals(action, that.action); + } + + @Override + public int hashCode() { + return Objects.hash(budgetId, budgetName, usedUsd, limitUsd, percentage, exceeded, action); + } + + @Override + public String toString() { + return "BudgetInfo{" + + "budgetId='" + + budgetId + + '\'' + + ", budgetName='" + + budgetName + + '\'' + + ", usedUsd=" + + usedUsd + + ", limitUsd=" + + limitUsd + + ", percentage=" + + percentage + + ", exceeded=" + + exceeded + + ", action='" + + action + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CancelPlanResponse.java b/src/main/java/com/getaxonflow/sdk/types/CancelPlanResponse.java index b76f25b..eb01fab 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CancelPlanResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/CancelPlanResponse.java @@ -17,81 +17,84 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Response from cancelling a multi-agent plan. - */ +/** Response from cancelling a multi-agent plan. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CancelPlanResponse { - @JsonProperty("plan_id") - private final String planId; + @JsonProperty("plan_id") + private final String planId; - @JsonProperty("status") - private final String status; + @JsonProperty("status") + private final String status; - @JsonProperty("message") - private final String message; + @JsonProperty("message") + private final String message; - public CancelPlanResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("status") String status, - @JsonProperty("message") String message) { - this.planId = planId; - this.status = status; - this.message = message; - } + public CancelPlanResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("status") String status, + @JsonProperty("message") String message) { + this.planId = planId; + this.status = status; + this.message = message; + } - /** - * Returns the ID of the cancelled plan. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } + /** + * Returns the ID of the cancelled plan. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } - /** - * Returns the status after cancellation. - * - * @return the status (e.g., "cancelled") - */ - public String getStatus() { - return status; - } + /** + * Returns the status after cancellation. + * + * @return the status (e.g., "cancelled") + */ + public String getStatus() { + return status; + } - /** - * Returns a human-readable message about the cancellation. - * - * @return the cancellation message - */ - public String getMessage() { - return message; - } + /** + * Returns a human-readable message about the cancellation. + * + * @return the cancellation message + */ + public String getMessage() { + return message; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CancelPlanResponse that = (CancelPlanResponse) o; - return Objects.equals(planId, that.planId) && - Objects.equals(status, that.status) && - Objects.equals(message, that.message); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CancelPlanResponse that = (CancelPlanResponse) o; + return Objects.equals(planId, that.planId) + && Objects.equals(status, that.status) + && Objects.equals(message, that.message); + } - @Override - public int hashCode() { - return Objects.hash(planId, status, message); - } + @Override + public int hashCode() { + return Objects.hash(planId, status, message); + } - @Override - public String toString() { - return "CancelPlanResponse{" + - "planId='" + planId + '\'' + - ", status='" + status + '\'' + - ", message='" + message + '\'' + - '}'; - } + @Override + public String toString() { + return "CancelPlanResponse{" + + "planId='" + + planId + + '\'' + + ", status='" + + status + + '\'' + + ", message='" + + message + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfig.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfig.java index 3061724..6d3bf5d 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfig.java +++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfig.java @@ -17,70 +17,93 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Map; -/** - * Circuit breaker configuration for a tenant. - */ +/** Circuit breaker configuration for a tenant. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CircuitBreakerConfig { - @JsonProperty("source") - private final String source; - - @JsonProperty("error_threshold") - private final int errorThreshold; - - @JsonProperty("violation_threshold") - private final int violationThreshold; - - @JsonProperty("window_seconds") - private final int windowSeconds; - - @JsonProperty("default_timeout_seconds") - private final int defaultTimeoutSeconds; - - @JsonProperty("max_timeout_seconds") - private final int maxTimeoutSeconds; - - @JsonProperty("enable_auto_recovery") - private final boolean enableAutoRecovery; - - @JsonProperty("tenant_id") - private final String tenantId; - - @JsonProperty("overrides") - private final Map overrides; - - public CircuitBreakerConfig( - @JsonProperty("source") String source, - @JsonProperty("error_threshold") int errorThreshold, - @JsonProperty("violation_threshold") int violationThreshold, - @JsonProperty("window_seconds") int windowSeconds, - @JsonProperty("default_timeout_seconds") int defaultTimeoutSeconds, - @JsonProperty("max_timeout_seconds") int maxTimeoutSeconds, - @JsonProperty("enable_auto_recovery") boolean enableAutoRecovery, - @JsonProperty("tenant_id") String tenantId, - @JsonProperty("overrides") Map overrides) { - this.source = source; - this.errorThreshold = errorThreshold; - this.violationThreshold = violationThreshold; - this.windowSeconds = windowSeconds; - this.defaultTimeoutSeconds = defaultTimeoutSeconds; - this.maxTimeoutSeconds = maxTimeoutSeconds; - this.enableAutoRecovery = enableAutoRecovery; - this.tenantId = tenantId; - this.overrides = overrides != null ? Map.copyOf(overrides) : null; - } - - public String getSource() { return source; } - public int getErrorThreshold() { return errorThreshold; } - public int getViolationThreshold() { return violationThreshold; } - public int getWindowSeconds() { return windowSeconds; } - public int getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; } - public int getMaxTimeoutSeconds() { return maxTimeoutSeconds; } - public boolean isEnableAutoRecovery() { return enableAutoRecovery; } - public String getTenantId() { return tenantId; } - public Map getOverrides() { return overrides; } + @JsonProperty("source") + private final String source; + + @JsonProperty("error_threshold") + private final int errorThreshold; + + @JsonProperty("violation_threshold") + private final int violationThreshold; + + @JsonProperty("window_seconds") + private final int windowSeconds; + + @JsonProperty("default_timeout_seconds") + private final int defaultTimeoutSeconds; + + @JsonProperty("max_timeout_seconds") + private final int maxTimeoutSeconds; + + @JsonProperty("enable_auto_recovery") + private final boolean enableAutoRecovery; + + @JsonProperty("tenant_id") + private final String tenantId; + + @JsonProperty("overrides") + private final Map overrides; + + public CircuitBreakerConfig( + @JsonProperty("source") String source, + @JsonProperty("error_threshold") int errorThreshold, + @JsonProperty("violation_threshold") int violationThreshold, + @JsonProperty("window_seconds") int windowSeconds, + @JsonProperty("default_timeout_seconds") int defaultTimeoutSeconds, + @JsonProperty("max_timeout_seconds") int maxTimeoutSeconds, + @JsonProperty("enable_auto_recovery") boolean enableAutoRecovery, + @JsonProperty("tenant_id") String tenantId, + @JsonProperty("overrides") Map overrides) { + this.source = source; + this.errorThreshold = errorThreshold; + this.violationThreshold = violationThreshold; + this.windowSeconds = windowSeconds; + this.defaultTimeoutSeconds = defaultTimeoutSeconds; + this.maxTimeoutSeconds = maxTimeoutSeconds; + this.enableAutoRecovery = enableAutoRecovery; + this.tenantId = tenantId; + this.overrides = overrides != null ? Map.copyOf(overrides) : null; + } + + public String getSource() { + return source; + } + + public int getErrorThreshold() { + return errorThreshold; + } + + public int getViolationThreshold() { + return violationThreshold; + } + + public int getWindowSeconds() { + return windowSeconds; + } + + public int getDefaultTimeoutSeconds() { + return defaultTimeoutSeconds; + } + + public int getMaxTimeoutSeconds() { + return maxTimeoutSeconds; + } + + public boolean isEnableAutoRecovery() { + return enableAutoRecovery; + } + + public String getTenantId() { + return tenantId; + } + + public Map getOverrides() { + return overrides; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdate.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdate.java index f1cc53c..9fdb026 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdate.java +++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdate.java @@ -17,13 +17,13 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Request to update circuit breaker configuration for a tenant. * *

Use the {@link Builder} to construct instances: + * *

{@code
  * CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder()
  *     .tenantId("tenant_123")
@@ -35,86 +35,131 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class CircuitBreakerConfigUpdate {
 
-    @JsonProperty("tenant_id")
-    private final String tenantId;
+  @JsonProperty("tenant_id")
+  private final String tenantId;
 
-    @JsonProperty("error_threshold")
-    private final Integer errorThreshold;
+  @JsonProperty("error_threshold")
+  private final Integer errorThreshold;
 
-    @JsonProperty("violation_threshold")
-    private final Integer violationThreshold;
+  @JsonProperty("violation_threshold")
+  private final Integer violationThreshold;
 
-    @JsonProperty("window_seconds")
-    private final Integer windowSeconds;
+  @JsonProperty("window_seconds")
+  private final Integer windowSeconds;
 
-    @JsonProperty("default_timeout_seconds")
-    private final Integer defaultTimeoutSeconds;
+  @JsonProperty("default_timeout_seconds")
+  private final Integer defaultTimeoutSeconds;
 
-    @JsonProperty("max_timeout_seconds")
-    private final Integer maxTimeoutSeconds;
+  @JsonProperty("max_timeout_seconds")
+  private final Integer maxTimeoutSeconds;
 
-    @JsonProperty("enable_auto_recovery")
-    private final Boolean enableAutoRecovery;
+  @JsonProperty("enable_auto_recovery")
+  private final Boolean enableAutoRecovery;
 
-    private CircuitBreakerConfigUpdate(Builder builder) {
-        this.tenantId = Objects.requireNonNull(builder.tenantId, "tenantId cannot be null");
-        if (this.tenantId.isEmpty()) {
-            throw new IllegalArgumentException("tenantId cannot be empty");
-        }
-        this.errorThreshold = builder.errorThreshold;
-        this.violationThreshold = builder.violationThreshold;
-        this.windowSeconds = builder.windowSeconds;
-        this.defaultTimeoutSeconds = builder.defaultTimeoutSeconds;
-        this.maxTimeoutSeconds = builder.maxTimeoutSeconds;
-        this.enableAutoRecovery = builder.enableAutoRecovery;
+  private CircuitBreakerConfigUpdate(Builder builder) {
+    this.tenantId = Objects.requireNonNull(builder.tenantId, "tenantId cannot be null");
+    if (this.tenantId.isEmpty()) {
+      throw new IllegalArgumentException("tenantId cannot be empty");
+    }
+    this.errorThreshold = builder.errorThreshold;
+    this.violationThreshold = builder.violationThreshold;
+    this.windowSeconds = builder.windowSeconds;
+    this.defaultTimeoutSeconds = builder.defaultTimeoutSeconds;
+    this.maxTimeoutSeconds = builder.maxTimeoutSeconds;
+    this.enableAutoRecovery = builder.enableAutoRecovery;
+  }
+
+  /**
+   * Creates a new builder for CircuitBreakerConfigUpdate.
+   *
+   * @return a new builder
+   */
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public Integer getErrorThreshold() {
+    return errorThreshold;
+  }
+
+  public Integer getViolationThreshold() {
+    return violationThreshold;
+  }
+
+  public Integer getWindowSeconds() {
+    return windowSeconds;
+  }
+
+  public Integer getDefaultTimeoutSeconds() {
+    return defaultTimeoutSeconds;
+  }
+
+  public Integer getMaxTimeoutSeconds() {
+    return maxTimeoutSeconds;
+  }
+
+  public Boolean getEnableAutoRecovery() {
+    return enableAutoRecovery;
+  }
+
+  /** Builder for {@link CircuitBreakerConfigUpdate}. */
+  public static final class Builder {
+    private String tenantId;
+    private Integer errorThreshold;
+    private Integer violationThreshold;
+    private Integer windowSeconds;
+    private Integer defaultTimeoutSeconds;
+    private Integer maxTimeoutSeconds;
+    private Boolean enableAutoRecovery;
+
+    public Builder tenantId(String tenantId) {
+      this.tenantId = tenantId;
+      return this;
     }
 
-    /**
-     * Creates a new builder for CircuitBreakerConfigUpdate.
-     *
-     * @return a new builder
-     */
-    public static Builder builder() {
-        return new Builder();
+    public Builder errorThreshold(int errorThreshold) {
+      this.errorThreshold = errorThreshold;
+      return this;
+    }
+
+    public Builder violationThreshold(int violationThreshold) {
+      this.violationThreshold = violationThreshold;
+      return this;
     }
 
-    public String getTenantId() { return tenantId; }
-    public Integer getErrorThreshold() { return errorThreshold; }
-    public Integer getViolationThreshold() { return violationThreshold; }
-    public Integer getWindowSeconds() { return windowSeconds; }
-    public Integer getDefaultTimeoutSeconds() { return defaultTimeoutSeconds; }
-    public Integer getMaxTimeoutSeconds() { return maxTimeoutSeconds; }
-    public Boolean getEnableAutoRecovery() { return enableAutoRecovery; }
+    public Builder windowSeconds(int windowSeconds) {
+      this.windowSeconds = windowSeconds;
+      return this;
+    }
+
+    public Builder defaultTimeoutSeconds(int defaultTimeoutSeconds) {
+      this.defaultTimeoutSeconds = defaultTimeoutSeconds;
+      return this;
+    }
+
+    public Builder maxTimeoutSeconds(int maxTimeoutSeconds) {
+      this.maxTimeoutSeconds = maxTimeoutSeconds;
+      return this;
+    }
+
+    public Builder enableAutoRecovery(boolean enableAutoRecovery) {
+      this.enableAutoRecovery = enableAutoRecovery;
+      return this;
+    }
 
     /**
-     * Builder for {@link CircuitBreakerConfigUpdate}.
+     * Builds the CircuitBreakerConfigUpdate.
+     *
+     * @return the config update
+     * @throws NullPointerException if tenantId is null
+     * @throws IllegalArgumentException if tenantId is empty
      */
-    public static final class Builder {
-        private String tenantId;
-        private Integer errorThreshold;
-        private Integer violationThreshold;
-        private Integer windowSeconds;
-        private Integer defaultTimeoutSeconds;
-        private Integer maxTimeoutSeconds;
-        private Boolean enableAutoRecovery;
-
-        public Builder tenantId(String tenantId) { this.tenantId = tenantId; return this; }
-        public Builder errorThreshold(int errorThreshold) { this.errorThreshold = errorThreshold; return this; }
-        public Builder violationThreshold(int violationThreshold) { this.violationThreshold = violationThreshold; return this; }
-        public Builder windowSeconds(int windowSeconds) { this.windowSeconds = windowSeconds; return this; }
-        public Builder defaultTimeoutSeconds(int defaultTimeoutSeconds) { this.defaultTimeoutSeconds = defaultTimeoutSeconds; return this; }
-        public Builder maxTimeoutSeconds(int maxTimeoutSeconds) { this.maxTimeoutSeconds = maxTimeoutSeconds; return this; }
-        public Builder enableAutoRecovery(boolean enableAutoRecovery) { this.enableAutoRecovery = enableAutoRecovery; return this; }
-
-        /**
-         * Builds the CircuitBreakerConfigUpdate.
-         *
-         * @return the config update
-         * @throws NullPointerException if tenantId is null
-         * @throws IllegalArgumentException if tenantId is empty
-         */
-        public CircuitBreakerConfigUpdate build() {
-            return new CircuitBreakerConfigUpdate(this);
-        }
+    public CircuitBreakerConfigUpdate build() {
+      return new CircuitBreakerConfigUpdate(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdateResponse.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdateResponse.java
index 7fc80ee..dfccf47 100644
--- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdateResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerConfigUpdateResponse.java
@@ -6,30 +6,37 @@
 /**
  * Response from updating circuit breaker configuration.
  *
- * 

The backend returns a confirmation with tenant_id and message, - * not the full config object. + *

The backend returns a confirmation with tenant_id and message, not the full config object. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CircuitBreakerConfigUpdateResponse { - @JsonProperty("tenant_id") - private final String tenantId; + @JsonProperty("tenant_id") + private final String tenantId; - @JsonProperty("message") - private final String message; + @JsonProperty("message") + private final String message; - public CircuitBreakerConfigUpdateResponse( - @JsonProperty("tenant_id") String tenantId, - @JsonProperty("message") String message) { - this.tenantId = tenantId; - this.message = message; - } + public CircuitBreakerConfigUpdateResponse( + @JsonProperty("tenant_id") String tenantId, @JsonProperty("message") String message) { + this.tenantId = tenantId; + this.message = message; + } - public String getTenantId() { return tenantId; } - public String getMessage() { return message; } + public String getTenantId() { + return tenantId; + } - @Override - public String toString() { - return "CircuitBreakerConfigUpdateResponse{tenantId='" + tenantId + "', message='" + message + "'}"; - } + public String getMessage() { + return message; + } + + @Override + public String toString() { + return "CircuitBreakerConfigUpdateResponse{tenantId='" + + tenantId + + "', message='" + + message + + "'}"; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryEntry.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryEntry.java index 79bb7fb..3e857ac 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryEntry.java +++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryEntry.java @@ -18,91 +18,127 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; -/** - * A single entry in circuit breaker history. - */ +/** A single entry in circuit breaker history. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CircuitBreakerHistoryEntry { - @JsonProperty("id") - private final String id; - - @JsonProperty("org_id") - private final String orgId; - - @JsonProperty("scope") - private final String scope; - - @JsonProperty("scope_id") - private final String scopeId; - - @JsonProperty("state") - private final String state; - - @JsonProperty("trip_reason") - private final String tripReason; - - @JsonProperty("tripped_by") - private final String trippedBy; - - @JsonProperty("tripped_at") - private final String trippedAt; - - @JsonProperty("expires_at") - private final String expiresAt; - - @JsonProperty("reset_by") - private final String resetBy; - - @JsonProperty("reset_at") - private final String resetAt; - - @JsonProperty("error_count") - private final int errorCount; - - @JsonProperty("violation_count") - private final int violationCount; - - public CircuitBreakerHistoryEntry( - @JsonProperty("id") String id, - @JsonProperty("org_id") String orgId, - @JsonProperty("scope") String scope, - @JsonProperty("scope_id") String scopeId, - @JsonProperty("state") String state, - @JsonProperty("trip_reason") String tripReason, - @JsonProperty("tripped_by") String trippedBy, - @JsonProperty("tripped_at") String trippedAt, - @JsonProperty("expires_at") String expiresAt, - @JsonProperty("reset_by") String resetBy, - @JsonProperty("reset_at") String resetAt, - @JsonProperty("error_count") int errorCount, - @JsonProperty("violation_count") int violationCount) { - this.id = id; - this.orgId = orgId; - this.scope = scope; - this.scopeId = scopeId; - this.state = state; - this.tripReason = tripReason; - this.trippedBy = trippedBy; - this.trippedAt = trippedAt; - this.expiresAt = expiresAt; - this.resetBy = resetBy; - this.resetAt = resetAt; - this.errorCount = errorCount; - this.violationCount = violationCount; - } - - public String getId() { return id; } - public String getOrgId() { return orgId; } - public String getScope() { return scope; } - public String getScopeId() { return scopeId; } - public String getState() { return state; } - public String getTripReason() { return tripReason; } - public String getTrippedBy() { return trippedBy; } - public String getTrippedAt() { return trippedAt; } - public String getExpiresAt() { return expiresAt; } - public String getResetBy() { return resetBy; } - public String getResetAt() { return resetAt; } - public int getErrorCount() { return errorCount; } - public int getViolationCount() { return violationCount; } + @JsonProperty("id") + private final String id; + + @JsonProperty("org_id") + private final String orgId; + + @JsonProperty("scope") + private final String scope; + + @JsonProperty("scope_id") + private final String scopeId; + + @JsonProperty("state") + private final String state; + + @JsonProperty("trip_reason") + private final String tripReason; + + @JsonProperty("tripped_by") + private final String trippedBy; + + @JsonProperty("tripped_at") + private final String trippedAt; + + @JsonProperty("expires_at") + private final String expiresAt; + + @JsonProperty("reset_by") + private final String resetBy; + + @JsonProperty("reset_at") + private final String resetAt; + + @JsonProperty("error_count") + private final int errorCount; + + @JsonProperty("violation_count") + private final int violationCount; + + public CircuitBreakerHistoryEntry( + @JsonProperty("id") String id, + @JsonProperty("org_id") String orgId, + @JsonProperty("scope") String scope, + @JsonProperty("scope_id") String scopeId, + @JsonProperty("state") String state, + @JsonProperty("trip_reason") String tripReason, + @JsonProperty("tripped_by") String trippedBy, + @JsonProperty("tripped_at") String trippedAt, + @JsonProperty("expires_at") String expiresAt, + @JsonProperty("reset_by") String resetBy, + @JsonProperty("reset_at") String resetAt, + @JsonProperty("error_count") int errorCount, + @JsonProperty("violation_count") int violationCount) { + this.id = id; + this.orgId = orgId; + this.scope = scope; + this.scopeId = scopeId; + this.state = state; + this.tripReason = tripReason; + this.trippedBy = trippedBy; + this.trippedAt = trippedAt; + this.expiresAt = expiresAt; + this.resetBy = resetBy; + this.resetAt = resetAt; + this.errorCount = errorCount; + this.violationCount = violationCount; + } + + public String getId() { + return id; + } + + public String getOrgId() { + return orgId; + } + + public String getScope() { + return scope; + } + + public String getScopeId() { + return scopeId; + } + + public String getState() { + return state; + } + + public String getTripReason() { + return tripReason; + } + + public String getTrippedBy() { + return trippedBy; + } + + public String getTrippedAt() { + return trippedAt; + } + + public String getExpiresAt() { + return expiresAt; + } + + public String getResetBy() { + return resetBy; + } + + public String getResetAt() { + return resetAt; + } + + public int getErrorCount() { + return errorCount; + } + + public int getViolationCount() { + return violationCount; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryResponse.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryResponse.java index 1cc6011..559b98b 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerHistoryResponse.java @@ -17,43 +17,40 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; -/** - * Response from the circuit breaker history endpoint. - */ +/** Response from the circuit breaker history endpoint. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CircuitBreakerHistoryResponse { - @JsonProperty("history") - private final List history; - - @JsonProperty("count") - private final int count; - - public CircuitBreakerHistoryResponse( - @JsonProperty("history") List history, - @JsonProperty("count") int count) { - this.history = history != null ? List.copyOf(history) : List.of(); - this.count = count; - } - - /** - * Returns the list of circuit breaker history entries. - * - * @return the history entries - */ - public List getHistory() { - return history; - } - - /** - * Returns the total number of history entries. - * - * @return the count - */ - public int getCount() { - return count; - } + @JsonProperty("history") + private final List history; + + @JsonProperty("count") + private final int count; + + public CircuitBreakerHistoryResponse( + @JsonProperty("history") List history, + @JsonProperty("count") int count) { + this.history = history != null ? List.copyOf(history) : List.of(); + this.count = count; + } + + /** + * Returns the list of circuit breaker history entries. + * + * @return the history entries + */ + public List getHistory() { + return history; + } + + /** + * Returns the total number of history entries. + * + * @return the count + */ + public int getCount() { + return count; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerStatusResponse.java b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerStatusResponse.java index 8373a58..422e7a7 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerStatusResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/CircuitBreakerStatusResponse.java @@ -17,58 +17,55 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; import java.util.Map; -/** - * Response from the circuit breaker status endpoint. - */ +/** Response from the circuit breaker status endpoint. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CircuitBreakerStatusResponse { - @JsonProperty("active_circuits") - private final List> activeCircuits; + @JsonProperty("active_circuits") + private final List> activeCircuits; - @JsonProperty("count") - private final int count; + @JsonProperty("count") + private final int count; - @JsonProperty("emergency_stop_active") - private final boolean emergencyStopActive; + @JsonProperty("emergency_stop_active") + private final boolean emergencyStopActive; - public CircuitBreakerStatusResponse( - @JsonProperty("active_circuits") List> activeCircuits, - @JsonProperty("count") int count, - @JsonProperty("emergency_stop_active") boolean emergencyStopActive) { - this.activeCircuits = activeCircuits != null ? List.copyOf(activeCircuits) : List.of(); - this.count = count; - this.emergencyStopActive = emergencyStopActive; - } + public CircuitBreakerStatusResponse( + @JsonProperty("active_circuits") List> activeCircuits, + @JsonProperty("count") int count, + @JsonProperty("emergency_stop_active") boolean emergencyStopActive) { + this.activeCircuits = activeCircuits != null ? List.copyOf(activeCircuits) : List.of(); + this.count = count; + this.emergencyStopActive = emergencyStopActive; + } - /** - * Returns the list of currently active (tripped) circuits. - * - * @return the active circuits - */ - public List> getActiveCircuits() { - return activeCircuits; - } + /** + * Returns the list of currently active (tripped) circuits. + * + * @return the active circuits + */ + public List> getActiveCircuits() { + return activeCircuits; + } - /** - * Returns the number of active circuits. - * - * @return the count - */ - public int getCount() { - return count; - } + /** + * Returns the number of active circuits. + * + * @return the count + */ + public int getCount() { + return count; + } - /** - * Returns whether the emergency stop is currently active. - * - * @return true if emergency stop is active - */ - public boolean isEmergencyStopActive() { - return emergencyStopActive; - } + /** + * Returns whether the emergency stop is currently active. + * + * @return true if emergency stop is active + */ + public boolean isEmergencyStopActive() { + return emergencyStopActive; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ClientRequest.java b/src/main/java/com/getaxonflow/sdk/types/ClientRequest.java index 691deae..8f913b0 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ClientRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/ClientRequest.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -28,10 +27,11 @@ /** * Represents a request to the AxonFlow Agent for policy evaluation. * - *

This is the primary request type for Proxy Mode operations where AxonFlow - * handles both policy enforcement and LLM routing. + *

This is the primary request type for Proxy Mode operations where AxonFlow handles both policy + * enforcement and LLM routing. * *

Example usage: + * *

{@code
  * ClientRequest request = ClientRequest.builder()
  *     .query("What is the weather today?")
@@ -43,238 +43,258 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class ClientRequest {
 
-    @JsonProperty("query")
-    private final String query;
-
-    @JsonProperty("user_token")
-    private final String userToken;
-
-    @JsonProperty("client_id")
-    private final String clientId;
-
-    @JsonProperty("request_type")
-    private final String requestType;
-
-    @JsonProperty("context")
-    private final Map context;
-
-    @JsonProperty("llm_provider")
-    private final String llmProvider;
-
-    @JsonProperty("model")
-    private final String model;
-
-    @JsonProperty("media")
-    private final List media;
+  @JsonProperty("query")
+  private final String query;
+
+  @JsonProperty("user_token")
+  private final String userToken;
+
+  @JsonProperty("client_id")
+  private final String clientId;
+
+  @JsonProperty("request_type")
+  private final String requestType;
+
+  @JsonProperty("context")
+  private final Map context;
+
+  @JsonProperty("llm_provider")
+  private final String llmProvider;
+
+  @JsonProperty("model")
+  private final String model;
+
+  @JsonProperty("media")
+  private final List media;
+
+  private ClientRequest(Builder builder) {
+    this.query = Objects.requireNonNull(builder.query, "query cannot be null");
+    // Default to "anonymous" if userToken is null or empty (community mode)
+    this.userToken =
+        (builder.userToken == null || builder.userToken.isEmpty())
+            ? "anonymous"
+            : builder.userToken;
+    this.clientId = builder.clientId;
+    this.requestType =
+        builder.requestType != null ? builder.requestType.getValue() : RequestType.CHAT.getValue();
+    this.context =
+        builder.context != null
+            ? Collections.unmodifiableMap(new HashMap<>(builder.context))
+            : null;
+    this.llmProvider = builder.llmProvider;
+    this.model = builder.model;
+    this.media =
+        builder.media != null ? Collections.unmodifiableList(new ArrayList<>(builder.media)) : null;
+  }
+
+  public String getQuery() {
+    return query;
+  }
+
+  public String getUserToken() {
+    return userToken;
+  }
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public String getRequestType() {
+    return requestType;
+  }
+
+  public Map getContext() {
+    return context;
+  }
+
+  public String getLlmProvider() {
+    return llmProvider;
+  }
+
+  public String getModel() {
+    return model;
+  }
+
+  public List getMedia() {
+    return media;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ClientRequest that = (ClientRequest) o;
+    return Objects.equals(query, that.query)
+        && Objects.equals(userToken, that.userToken)
+        && Objects.equals(clientId, that.clientId)
+        && Objects.equals(requestType, that.requestType)
+        && Objects.equals(context, that.context)
+        && Objects.equals(llmProvider, that.llmProvider)
+        && Objects.equals(model, that.model)
+        && Objects.equals(media, that.media);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(
+        query, userToken, clientId, requestType, context, llmProvider, model, media);
+  }
+
+  @Override
+  public String toString() {
+    return "ClientRequest{"
+        + "query='"
+        + query
+        + '\''
+        + ", userToken='"
+        + userToken
+        + '\''
+        + ", clientId='"
+        + clientId
+        + '\''
+        + ", requestType='"
+        + requestType
+        + '\''
+        + ", llmProvider='"
+        + llmProvider
+        + '\''
+        + ", model='"
+        + model
+        + '\''
+        + ", media="
+        + media
+        + '}';
+  }
+
+  /** Builder for creating ClientRequest instances. */
+  public static final class Builder {
+    private String query;
+    private String userToken;
+    private String clientId;
+    private RequestType requestType = RequestType.CHAT;
+    private Map context;
+    private String llmProvider;
+    private String model;
+    private List media;
+
+    private Builder() {}
 
-    private ClientRequest(Builder builder) {
-        this.query = Objects.requireNonNull(builder.query, "query cannot be null");
-        // Default to "anonymous" if userToken is null or empty (community mode)
-        this.userToken = (builder.userToken == null || builder.userToken.isEmpty()) ? "anonymous" : builder.userToken;
-        this.clientId = builder.clientId;
-        this.requestType = builder.requestType != null ? builder.requestType.getValue() : RequestType.CHAT.getValue();
-        this.context = builder.context != null ? Collections.unmodifiableMap(new HashMap<>(builder.context)) : null;
-        this.llmProvider = builder.llmProvider;
-        this.model = builder.model;
-        this.media = builder.media != null ? Collections.unmodifiableList(new ArrayList<>(builder.media)) : null;
-    }
-
-    public String getQuery() {
-        return query;
-    }
-
-    public String getUserToken() {
-        return userToken;
-    }
-
-    public String getClientId() {
-        return clientId;
-    }
-
-    public String getRequestType() {
-        return requestType;
+    /**
+     * Sets the query text to be processed.
+     *
+     * @param query the query or prompt text
+     * @return this builder
+     */
+    public Builder query(String query) {
+      this.query = query;
+      return this;
     }
 
-    public Map getContext() {
-        return context;
+    /**
+     * Sets the user token for identifying the requesting user. If null or empty, defaults to
+     * "anonymous" for audit purposes.
+     *
+     * @param userToken the user identifier token
+     * @return this builder
+     */
+    public Builder userToken(String userToken) {
+      this.userToken = userToken;
+      return this;
     }
 
-    public String getLlmProvider() {
-        return llmProvider;
+    /**
+     * Sets the client ID for multi-tenant scenarios.
+     *
+     * @param clientId the client identifier
+     * @return this builder
+     */
+    public Builder clientId(String clientId) {
+      this.clientId = clientId;
+      return this;
     }
 
-    public String getModel() {
-        return model;
+    /**
+     * Sets the type of request.
+     *
+     * @param requestType the request type
+     * @return this builder
+     */
+    public Builder requestType(RequestType requestType) {
+      this.requestType = requestType;
+      return this;
     }
 
-    public List getMedia() {
-        return media;
+    /**
+     * Sets additional context for policy evaluation.
+     *
+     * @param context key-value pairs of contextual information
+     * @return this builder
+     */
+    public Builder context(Map context) {
+      this.context = context;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    /**
+     * Adds a single context entry.
+     *
+     * @param key the context key
+     * @param value the context value
+     * @return this builder
+     */
+    public Builder addContext(String key, Object value) {
+      if (this.context == null) {
+        this.context = new HashMap<>();
+      }
+      this.context.put(key, value);
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ClientRequest that = (ClientRequest) o;
-        return Objects.equals(query, that.query) &&
-               Objects.equals(userToken, that.userToken) &&
-               Objects.equals(clientId, that.clientId) &&
-               Objects.equals(requestType, that.requestType) &&
-               Objects.equals(context, that.context) &&
-               Objects.equals(llmProvider, that.llmProvider) &&
-               Objects.equals(model, that.model) &&
-               Objects.equals(media, that.media);
+    /**
+     * Sets the LLM provider to use (for Proxy Mode).
+     *
+     * @param llmProvider the provider name (e.g., "openai", "anthropic")
+     * @return this builder
+     */
+    public Builder llmProvider(String llmProvider) {
+      this.llmProvider = llmProvider;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(query, userToken, clientId, requestType, context, llmProvider, model, media);
+    /**
+     * Sets the model to use (for Proxy Mode).
+     *
+     * @param model the model identifier (e.g., "gpt-4", "claude-3-opus")
+     * @return this builder
+     */
+    public Builder model(String model) {
+      this.model = model;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "ClientRequest{" +
-               "query='" + query + '\'' +
-               ", userToken='" + userToken + '\'' +
-               ", clientId='" + clientId + '\'' +
-               ", requestType='" + requestType + '\'' +
-               ", llmProvider='" + llmProvider + '\'' +
-               ", model='" + model + '\'' +
-               ", media=" + media +
-               '}';
+    /**
+     * Sets optional media content (images) for multimodal governance.
+     *
+     * @param media list of media content items
+     * @return this builder
+     */
+    public Builder media(List media) {
+      this.media = media;
+      return this;
     }
 
     /**
-     * Builder for creating ClientRequest instances.
+     * Builds the ClientRequest instance.
+     *
+     * @return a new ClientRequest
+     * @throws NullPointerException if query is null
      */
-    public static final class Builder {
-        private String query;
-        private String userToken;
-        private String clientId;
-        private RequestType requestType = RequestType.CHAT;
-        private Map context;
-        private String llmProvider;
-        private String model;
-        private List media;
-
-        private Builder() {}
-
-        /**
-         * Sets the query text to be processed.
-         *
-         * @param query the query or prompt text
-         * @return this builder
-         */
-        public Builder query(String query) {
-            this.query = query;
-            return this;
-        }
-
-        /**
-         * Sets the user token for identifying the requesting user.
-         * If null or empty, defaults to "anonymous" for audit purposes.
-         *
-         * @param userToken the user identifier token
-         * @return this builder
-         */
-        public Builder userToken(String userToken) {
-            this.userToken = userToken;
-            return this;
-        }
-
-        /**
-         * Sets the client ID for multi-tenant scenarios.
-         *
-         * @param clientId the client identifier
-         * @return this builder
-         */
-        public Builder clientId(String clientId) {
-            this.clientId = clientId;
-            return this;
-        }
-
-        /**
-         * Sets the type of request.
-         *
-         * @param requestType the request type
-         * @return this builder
-         */
-        public Builder requestType(RequestType requestType) {
-            this.requestType = requestType;
-            return this;
-        }
-
-        /**
-         * Sets additional context for policy evaluation.
-         *
-         * @param context key-value pairs of contextual information
-         * @return this builder
-         */
-        public Builder context(Map context) {
-            this.context = context;
-            return this;
-        }
-
-        /**
-         * Adds a single context entry.
-         *
-         * @param key the context key
-         * @param value the context value
-         * @return this builder
-         */
-        public Builder addContext(String key, Object value) {
-            if (this.context == null) {
-                this.context = new HashMap<>();
-            }
-            this.context.put(key, value);
-            return this;
-        }
-
-        /**
-         * Sets the LLM provider to use (for Proxy Mode).
-         *
-         * @param llmProvider the provider name (e.g., "openai", "anthropic")
-         * @return this builder
-         */
-        public Builder llmProvider(String llmProvider) {
-            this.llmProvider = llmProvider;
-            return this;
-        }
-
-        /**
-         * Sets the model to use (for Proxy Mode).
-         *
-         * @param model the model identifier (e.g., "gpt-4", "claude-3-opus")
-         * @return this builder
-         */
-        public Builder model(String model) {
-            this.model = model;
-            return this;
-        }
-
-        /**
-         * Sets optional media content (images) for multimodal governance.
-         *
-         * @param media list of media content items
-         * @return this builder
-         */
-        public Builder media(List media) {
-            this.media = media;
-            return this;
-        }
-
-        /**
-         * Builds the ClientRequest instance.
-         *
-         * @return a new ClientRequest
-         * @throws NullPointerException if query is null
-         */
-        public ClientRequest build() {
-            return new ClientRequest(this);
-        }
+    public ClientRequest build() {
+      return new ClientRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/ClientResponse.java b/src/main/java/com/getaxonflow/sdk/types/ClientResponse.java
index 1d5690e..f1f3f17 100644
--- a/src/main/java/com/getaxonflow/sdk/types/ClientResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/ClientResponse.java
@@ -17,228 +17,247 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
 /**
  * Represents a response from the AxonFlow Agent.
  *
  * 

This is the primary response type for Proxy Mode operations. It contains: + * *

    - *
  • Success indicator and data payload
  • - *
  • Blocking status and reason (if policy violation occurred)
  • - *
  • Policy information including evaluated policies and processing time
  • + *
  • Success indicator and data payload + *
  • Blocking status and reason (if policy violation occurred) + *
  • Policy information including evaluated policies and processing time *
*/ @JsonIgnoreProperties(ignoreUnknown = true) public final class ClientResponse { - @JsonProperty("success") - private final boolean success; - - @JsonProperty("data") - private final Object data; - - @JsonProperty("result") - private final String result; - - @JsonProperty("plan_id") - private final String planId; - - @JsonProperty("blocked") - private final boolean blocked; - - @JsonProperty("block_reason") - private final String blockReason; - - @JsonProperty("policy_info") - private final PolicyInfo policyInfo; - - @JsonProperty("error") - private final String error; - - @JsonProperty("budget_info") - private final BudgetInfo budgetInfo; - - @JsonProperty("media_analysis") - private final MediaAnalysisResponse mediaAnalysis; - - public ClientResponse( - @JsonProperty("success") boolean success, - @JsonProperty("data") Object data, - @JsonProperty("result") String result, - @JsonProperty("plan_id") String planId, - @JsonProperty("blocked") boolean blocked, - @JsonProperty("block_reason") String blockReason, - @JsonProperty("policy_info") PolicyInfo policyInfo, - @JsonProperty("error") String error, - @JsonProperty("budget_info") BudgetInfo budgetInfo, - @JsonProperty("media_analysis") MediaAnalysisResponse mediaAnalysis) { - this.success = success; - this.data = data; - this.result = result; - this.planId = planId; - this.blocked = blocked; - this.blockReason = blockReason; - this.policyInfo = policyInfo; - this.error = error; - this.budgetInfo = budgetInfo; - this.mediaAnalysis = mediaAnalysis; - } + @JsonProperty("success") + private final boolean success; - /** - * Returns whether the request was successful. - * - * @return true if successful, false otherwise - */ - public boolean isSuccess() { - return success; - } + @JsonProperty("data") + private final Object data; - /** - * Returns the data payload from the response. - * - * @return the response data, may be null - */ - public Object getData() { - return data; - } + @JsonProperty("result") + private final String result; - /** - * Returns the result string (used for planning responses). - * - * @return the result text, may be null - */ - public String getResult() { - return result; - } + @JsonProperty("plan_id") + private final String planId; - /** - * Returns the plan ID (for planning operations). - * - * @return the plan identifier, may be null - */ - public String getPlanId() { - return planId; - } + @JsonProperty("blocked") + private final boolean blocked; - /** - * Returns whether the request was blocked by policy. - * - * @return true if blocked, false otherwise - */ - public boolean isBlocked() { - return blocked; - } + @JsonProperty("block_reason") + private final String blockReason; - /** - * Returns the reason the request was blocked. - * - * @return the block reason, may be null if not blocked - */ - public String getBlockReason() { - return blockReason; - } + @JsonProperty("policy_info") + private final PolicyInfo policyInfo; - /** - * Extracts the policy name from the block reason. - * - *

Block reasons typically follow the format: "Request blocked by policy: policy_name" - * - * @return the extracted policy name, or the full block reason if extraction fails - */ - public String getBlockingPolicyName() { - if (blockReason == null || blockReason.isEmpty()) { - return null; - } - // Handle format: "Request blocked by policy: policy_name" - String prefix = "Request blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } - // Handle format: "Blocked by policy: policy_name" - prefix = "Blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } - // Handle format with brackets: "[policy_name] description" - if (blockReason.startsWith("[")) { - int endBracket = blockReason.indexOf(']'); - if (endBracket > 1) { - return blockReason.substring(1, endBracket).trim(); - } - } - return blockReason; - } + @JsonProperty("error") + private final String error; - /** - * Returns information about the policies evaluated. - * - * @return the policy info, may be null - */ - public PolicyInfo getPolicyInfo() { - return policyInfo; - } + @JsonProperty("budget_info") + private final BudgetInfo budgetInfo; - /** - * Returns the error message if the request failed. - * - * @return the error message, may be null - */ - public String getError() { - return error; - } + @JsonProperty("media_analysis") + private final MediaAnalysisResponse mediaAnalysis; - /** - * Returns budget enforcement status information. - * - * @return the budget info, may be null if no budget check was performed - */ - public BudgetInfo getBudgetInfo() { - return budgetInfo; - } + public ClientResponse( + @JsonProperty("success") boolean success, + @JsonProperty("data") Object data, + @JsonProperty("result") String result, + @JsonProperty("plan_id") String planId, + @JsonProperty("blocked") boolean blocked, + @JsonProperty("block_reason") String blockReason, + @JsonProperty("policy_info") PolicyInfo policyInfo, + @JsonProperty("error") String error, + @JsonProperty("budget_info") BudgetInfo budgetInfo, + @JsonProperty("media_analysis") MediaAnalysisResponse mediaAnalysis) { + this.success = success; + this.data = data; + this.result = result; + this.planId = planId; + this.blocked = blocked; + this.blockReason = blockReason; + this.policyInfo = policyInfo; + this.error = error; + this.budgetInfo = budgetInfo; + this.mediaAnalysis = mediaAnalysis; + } - /** - * Returns media analysis results if media was submitted. - * - * @return the media analysis response, may be null - */ - public MediaAnalysisResponse getMediaAnalysis() { - return mediaAnalysis; - } + /** + * Returns whether the request was successful. + * + * @return true if successful, false otherwise + */ + public boolean isSuccess() { + return success; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ClientResponse that = (ClientResponse) o; - return success == that.success && - blocked == that.blocked && - Objects.equals(data, that.data) && - Objects.equals(result, that.result) && - Objects.equals(planId, that.planId) && - Objects.equals(blockReason, that.blockReason) && - Objects.equals(policyInfo, that.policyInfo) && - Objects.equals(error, that.error) && - Objects.equals(budgetInfo, that.budgetInfo) && - Objects.equals(mediaAnalysis, that.mediaAnalysis); - } + /** + * Returns the data payload from the response. + * + * @return the response data, may be null + */ + public Object getData() { + return data; + } - @Override - public int hashCode() { - return Objects.hash(success, data, result, planId, blocked, blockReason, policyInfo, error, budgetInfo, mediaAnalysis); - } + /** + * Returns the result string (used for planning responses). + * + * @return the result text, may be null + */ + public String getResult() { + return result; + } + + /** + * Returns the plan ID (for planning operations). + * + * @return the plan identifier, may be null + */ + public String getPlanId() { + return planId; + } + + /** + * Returns whether the request was blocked by policy. + * + * @return true if blocked, false otherwise + */ + public boolean isBlocked() { + return blocked; + } + + /** + * Returns the reason the request was blocked. + * + * @return the block reason, may be null if not blocked + */ + public String getBlockReason() { + return blockReason; + } - @Override - public String toString() { - return "ClientResponse{" + - "success=" + success + - ", blocked=" + blocked + - ", blockReason='" + blockReason + '\'' + - ", policyInfo=" + policyInfo + - ", error='" + error + '\'' + - ", budgetInfo=" + budgetInfo + - ", mediaAnalysis=" + mediaAnalysis + - '}'; + /** + * Extracts the policy name from the block reason. + * + *

Block reasons typically follow the format: "Request blocked by policy: policy_name" + * + * @return the extracted policy name, or the full block reason if extraction fails + */ + public String getBlockingPolicyName() { + if (blockReason == null || blockReason.isEmpty()) { + return null; + } + // Handle format: "Request blocked by policy: policy_name" + String prefix = "Request blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); + } + // Handle format: "Blocked by policy: policy_name" + prefix = "Blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); + } + // Handle format with brackets: "[policy_name] description" + if (blockReason.startsWith("[")) { + int endBracket = blockReason.indexOf(']'); + if (endBracket > 1) { + return blockReason.substring(1, endBracket).trim(); + } } + return blockReason; + } + + /** + * Returns information about the policies evaluated. + * + * @return the policy info, may be null + */ + public PolicyInfo getPolicyInfo() { + return policyInfo; + } + + /** + * Returns the error message if the request failed. + * + * @return the error message, may be null + */ + public String getError() { + return error; + } + + /** + * Returns budget enforcement status information. + * + * @return the budget info, may be null if no budget check was performed + */ + public BudgetInfo getBudgetInfo() { + return budgetInfo; + } + + /** + * Returns media analysis results if media was submitted. + * + * @return the media analysis response, may be null + */ + public MediaAnalysisResponse getMediaAnalysis() { + return mediaAnalysis; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ClientResponse that = (ClientResponse) o; + return success == that.success + && blocked == that.blocked + && Objects.equals(data, that.data) + && Objects.equals(result, that.result) + && Objects.equals(planId, that.planId) + && Objects.equals(blockReason, that.blockReason) + && Objects.equals(policyInfo, that.policyInfo) + && Objects.equals(error, that.error) + && Objects.equals(budgetInfo, that.budgetInfo) + && Objects.equals(mediaAnalysis, that.mediaAnalysis); + } + + @Override + public int hashCode() { + return Objects.hash( + success, + data, + result, + planId, + blocked, + blockReason, + policyInfo, + error, + budgetInfo, + mediaAnalysis); + } + + @Override + public String toString() { + return "ClientResponse{" + + "success=" + + success + + ", blocked=" + + blocked + + ", blockReason='" + + blockReason + + '\'' + + ", policyInfo=" + + policyInfo + + ", error='" + + error + + '\'' + + ", budgetInfo=" + + budgetInfo + + ", mediaAnalysis=" + + mediaAnalysis + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/CodeArtifact.java b/src/main/java/com/getaxonflow/sdk/types/CodeArtifact.java index 1ed300d..1b43aac 100644 --- a/src/main/java/com/getaxonflow/sdk/types/CodeArtifact.java +++ b/src/main/java/com/getaxonflow/sdk/types/CodeArtifact.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; @@ -25,170 +24,191 @@ /** * Represents metadata for LLM-generated code detection. * - *

When an LLM generates code in its response, AxonFlow automatically detects - * and analyzes it. This metadata is included in PolicyInfo for audit and compliance. + *

When an LLM generates code in its response, AxonFlow automatically detects and analyzes it. + * This metadata is included in PolicyInfo for audit and compliance. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class CodeArtifact { - @JsonProperty("is_code_output") - private final boolean isCodeOutput; - - @JsonProperty("language") - private final String language; - - @JsonProperty("code_type") - private final String codeType; - - @JsonProperty("size_bytes") - private final int sizeBytes; - - @JsonProperty("line_count") - private final int lineCount; - - @JsonProperty("secrets_detected") - private final int secretsDetected; - - @JsonProperty("unsafe_patterns") - private final int unsafePatterns; - - @JsonProperty("policies_checked") - private final List policiesChecked; - - /** - * Creates a new CodeArtifact instance. - * - * @param isCodeOutput whether the response contains code - * @param language detected programming language - * @param codeType code category (function, class, script, etc.) - * @param sizeBytes size of detected code in bytes - * @param lineCount number of lines of code - * @param secretsDetected count of potential secrets found - * @param unsafePatterns count of unsafe code patterns - * @param policiesChecked list of code governance policies evaluated - */ - public CodeArtifact( - @JsonProperty("is_code_output") boolean isCodeOutput, - @JsonProperty("language") String language, - @JsonProperty("code_type") String codeType, - @JsonProperty("size_bytes") int sizeBytes, - @JsonProperty("line_count") int lineCount, - @JsonProperty("secrets_detected") int secretsDetected, - @JsonProperty("unsafe_patterns") int unsafePatterns, - @JsonProperty("policies_checked") List policiesChecked) { - this.isCodeOutput = isCodeOutput; - this.language = language != null ? language : ""; - this.codeType = codeType != null ? codeType : ""; - this.sizeBytes = sizeBytes; - this.lineCount = lineCount; - this.secretsDetected = secretsDetected; - this.unsafePatterns = unsafePatterns; - this.policiesChecked = policiesChecked != null ? Collections.unmodifiableList(policiesChecked) : Collections.emptyList(); - } - - /** - * Returns whether the response contains code. - * - * @return true if code was detected, false otherwise - */ - public boolean isCodeOutput() { - return isCodeOutput; - } - - /** - * Returns the detected programming language. - * - * @return the programming language (e.g., "python", "javascript", "go") - */ - public String getLanguage() { - return language; - } - - /** - * Returns the code category. - * - * @return the code type (e.g., "function", "class", "script", "config", "snippet") - */ - public String getCodeType() { - return codeType; - } - - /** - * Returns the size of detected code in bytes. - * - * @return code size in bytes - */ - public int getSizeBytes() { - return sizeBytes; - } - - /** - * Returns the number of lines of code. - * - * @return line count - */ - public int getLineCount() { - return lineCount; - } - - /** - * Returns the count of potential secrets found. - * - * @return number of secrets detected - */ - public int getSecretsDetected() { - return secretsDetected; - } - - /** - * Returns the count of unsafe code patterns. - * - * @return number of unsafe patterns detected - */ - public int getUnsafePatterns() { - return unsafePatterns; - } - - /** - * Returns the list of code governance policies that were evaluated. - * - * @return immutable list of policy names - */ - public List getPoliciesChecked() { - return policiesChecked; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CodeArtifact that = (CodeArtifact) o; - return isCodeOutput == that.isCodeOutput && - sizeBytes == that.sizeBytes && - lineCount == that.lineCount && - secretsDetected == that.secretsDetected && - unsafePatterns == that.unsafePatterns && - Objects.equals(language, that.language) && - Objects.equals(codeType, that.codeType) && - Objects.equals(policiesChecked, that.policiesChecked); - } - - @Override - public int hashCode() { - return Objects.hash(isCodeOutput, language, codeType, sizeBytes, lineCount, secretsDetected, unsafePatterns, policiesChecked); - } - - @Override - public String toString() { - return "CodeArtifact{" + - "isCodeOutput=" + isCodeOutput + - ", language='" + language + '\'' + - ", codeType='" + codeType + '\'' + - ", sizeBytes=" + sizeBytes + - ", lineCount=" + lineCount + - ", secretsDetected=" + secretsDetected + - ", unsafePatterns=" + unsafePatterns + - ", policiesChecked=" + policiesChecked + - '}'; - } + @JsonProperty("is_code_output") + private final boolean isCodeOutput; + + @JsonProperty("language") + private final String language; + + @JsonProperty("code_type") + private final String codeType; + + @JsonProperty("size_bytes") + private final int sizeBytes; + + @JsonProperty("line_count") + private final int lineCount; + + @JsonProperty("secrets_detected") + private final int secretsDetected; + + @JsonProperty("unsafe_patterns") + private final int unsafePatterns; + + @JsonProperty("policies_checked") + private final List policiesChecked; + + /** + * Creates a new CodeArtifact instance. + * + * @param isCodeOutput whether the response contains code + * @param language detected programming language + * @param codeType code category (function, class, script, etc.) + * @param sizeBytes size of detected code in bytes + * @param lineCount number of lines of code + * @param secretsDetected count of potential secrets found + * @param unsafePatterns count of unsafe code patterns + * @param policiesChecked list of code governance policies evaluated + */ + public CodeArtifact( + @JsonProperty("is_code_output") boolean isCodeOutput, + @JsonProperty("language") String language, + @JsonProperty("code_type") String codeType, + @JsonProperty("size_bytes") int sizeBytes, + @JsonProperty("line_count") int lineCount, + @JsonProperty("secrets_detected") int secretsDetected, + @JsonProperty("unsafe_patterns") int unsafePatterns, + @JsonProperty("policies_checked") List policiesChecked) { + this.isCodeOutput = isCodeOutput; + this.language = language != null ? language : ""; + this.codeType = codeType != null ? codeType : ""; + this.sizeBytes = sizeBytes; + this.lineCount = lineCount; + this.secretsDetected = secretsDetected; + this.unsafePatterns = unsafePatterns; + this.policiesChecked = + policiesChecked != null + ? Collections.unmodifiableList(policiesChecked) + : Collections.emptyList(); + } + + /** + * Returns whether the response contains code. + * + * @return true if code was detected, false otherwise + */ + public boolean isCodeOutput() { + return isCodeOutput; + } + + /** + * Returns the detected programming language. + * + * @return the programming language (e.g., "python", "javascript", "go") + */ + public String getLanguage() { + return language; + } + + /** + * Returns the code category. + * + * @return the code type (e.g., "function", "class", "script", "config", "snippet") + */ + public String getCodeType() { + return codeType; + } + + /** + * Returns the size of detected code in bytes. + * + * @return code size in bytes + */ + public int getSizeBytes() { + return sizeBytes; + } + + /** + * Returns the number of lines of code. + * + * @return line count + */ + public int getLineCount() { + return lineCount; + } + + /** + * Returns the count of potential secrets found. + * + * @return number of secrets detected + */ + public int getSecretsDetected() { + return secretsDetected; + } + + /** + * Returns the count of unsafe code patterns. + * + * @return number of unsafe patterns detected + */ + public int getUnsafePatterns() { + return unsafePatterns; + } + + /** + * Returns the list of code governance policies that were evaluated. + * + * @return immutable list of policy names + */ + public List getPoliciesChecked() { + return policiesChecked; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CodeArtifact that = (CodeArtifact) o; + return isCodeOutput == that.isCodeOutput + && sizeBytes == that.sizeBytes + && lineCount == that.lineCount + && secretsDetected == that.secretsDetected + && unsafePatterns == that.unsafePatterns + && Objects.equals(language, that.language) + && Objects.equals(codeType, that.codeType) + && Objects.equals(policiesChecked, that.policiesChecked); + } + + @Override + public int hashCode() { + return Objects.hash( + isCodeOutput, + language, + codeType, + sizeBytes, + lineCount, + secretsDetected, + unsafePatterns, + policiesChecked); + } + + @Override + public String toString() { + return "CodeArtifact{" + + "isCodeOutput=" + + isCodeOutput + + ", language='" + + language + + '\'' + + ", codeType='" + + codeType + + '\'' + + ", sizeBytes=" + + sizeBytes + + ", lineCount=" + + lineCount + + ", secretsDetected=" + + secretsDetected + + ", unsafePatterns=" + + unsafePatterns + + ", policiesChecked=" + + policiesChecked + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorHealthStatus.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorHealthStatus.java index 2fbc78d..acc76da 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorHealthStatus.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorHealthStatus.java @@ -17,113 +17,116 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.Map; import java.util.Objects; -/** - * Health status of an installed MCP connector. - */ +/** Health status of an installed MCP connector. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ConnectorHealthStatus { - @JsonProperty("healthy") - private final Boolean healthy; - - @JsonProperty("latency") - private final Long latency; - - @JsonProperty("details") - private final Map details; - - @JsonProperty("timestamp") - private final String timestamp; - - @JsonProperty("error") - private final String error; - - public ConnectorHealthStatus( - @JsonProperty("healthy") Boolean healthy, - @JsonProperty("latency") Long latency, - @JsonProperty("details") Map details, - @JsonProperty("timestamp") String timestamp, - @JsonProperty("error") String error) { - this.healthy = healthy != null ? healthy : false; - this.latency = latency != null ? latency : 0L; - this.details = details != null ? Collections.unmodifiableMap(details) : Collections.emptyMap(); - this.timestamp = timestamp != null ? timestamp : ""; - this.error = error; - } - - /** - * Returns whether the connector is healthy. - * - * @return true if healthy - */ - public Boolean isHealthy() { - return healthy; - } - - /** - * Returns the connection latency in nanoseconds. - * - * @return latency in nanoseconds - */ - public Long getLatency() { - return latency; - } - - /** - * Returns additional health check details. - * - * @return immutable map of health details - */ - public Map getDetails() { - return details; - } - - /** - * Returns the timestamp of the health check. - * - * @return ISO 8601 timestamp string - */ - public String getTimestamp() { - return timestamp; - } - - /** - * Returns the error message if unhealthy. - * - * @return error message or null if healthy - */ - public String getError() { - return error; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConnectorHealthStatus that = (ConnectorHealthStatus) o; - return Objects.equals(healthy, that.healthy) && - Objects.equals(latency, that.latency) && - Objects.equals(timestamp, that.timestamp) && - Objects.equals(error, that.error); - } - - @Override - public int hashCode() { - return Objects.hash(healthy, latency, timestamp, error); - } - - @Override - public String toString() { - return "ConnectorHealthStatus{" + - "healthy=" + healthy + - ", latency=" + latency + - ", timestamp='" + timestamp + '\'' + - ", error='" + error + '\'' + - '}'; - } + @JsonProperty("healthy") + private final Boolean healthy; + + @JsonProperty("latency") + private final Long latency; + + @JsonProperty("details") + private final Map details; + + @JsonProperty("timestamp") + private final String timestamp; + + @JsonProperty("error") + private final String error; + + public ConnectorHealthStatus( + @JsonProperty("healthy") Boolean healthy, + @JsonProperty("latency") Long latency, + @JsonProperty("details") Map details, + @JsonProperty("timestamp") String timestamp, + @JsonProperty("error") String error) { + this.healthy = healthy != null ? healthy : false; + this.latency = latency != null ? latency : 0L; + this.details = details != null ? Collections.unmodifiableMap(details) : Collections.emptyMap(); + this.timestamp = timestamp != null ? timestamp : ""; + this.error = error; + } + + /** + * Returns whether the connector is healthy. + * + * @return true if healthy + */ + public Boolean isHealthy() { + return healthy; + } + + /** + * Returns the connection latency in nanoseconds. + * + * @return latency in nanoseconds + */ + public Long getLatency() { + return latency; + } + + /** + * Returns additional health check details. + * + * @return immutable map of health details + */ + public Map getDetails() { + return details; + } + + /** + * Returns the timestamp of the health check. + * + * @return ISO 8601 timestamp string + */ + public String getTimestamp() { + return timestamp; + } + + /** + * Returns the error message if unhealthy. + * + * @return error message or null if healthy + */ + public String getError() { + return error; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorHealthStatus that = (ConnectorHealthStatus) o; + return Objects.equals(healthy, that.healthy) + && Objects.equals(latency, that.latency) + && Objects.equals(timestamp, that.timestamp) + && Objects.equals(error, that.error); + } + + @Override + public int hashCode() { + return Objects.hash(healthy, latency, timestamp, error); + } + + @Override + public String toString() { + return "ConnectorHealthStatus{" + + "healthy=" + + healthy + + ", latency=" + + latency + + ", timestamp='" + + timestamp + + '\'' + + ", error='" + + error + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorInfo.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorInfo.java index 68e7994..e7214be 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorInfo.java @@ -17,172 +17,181 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -/** - * Information about an available MCP (Model Context Protocol) connector. - */ +/** Information about an available MCP (Model Context Protocol) connector. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ConnectorInfo { - @JsonProperty("id") - private final String id; - - @JsonProperty("name") - private final String name; - - @JsonProperty("description") - private final String description; - - @JsonProperty("type") - private final String type; - - @JsonProperty("version") - private final String version; - - @JsonProperty("capabilities") - private final List capabilities; - - @JsonProperty("config_schema") - private final Map configSchema; - - @JsonProperty("installed") - private final Boolean installed; - - @JsonProperty("enabled") - private final Boolean enabled; - - public ConnectorInfo( - @JsonProperty("id") String id, - @JsonProperty("name") String name, - @JsonProperty("description") String description, - @JsonProperty("type") String type, - @JsonProperty("version") String version, - @JsonProperty("capabilities") List capabilities, - @JsonProperty("config_schema") Map configSchema, - @JsonProperty("installed") Boolean installed, - @JsonProperty("enabled") Boolean enabled) { - this.id = id; - this.name = name; - this.description = description; - this.type = type; - this.version = version; - this.capabilities = capabilities != null ? Collections.unmodifiableList(capabilities) : Collections.emptyList(); - this.configSchema = configSchema != null ? Collections.unmodifiableMap(configSchema) : Collections.emptyMap(); - this.installed = installed; - this.enabled = enabled; - } - - /** - * Returns the unique identifier for this connector. - * - * @return the connector ID - */ - public String getId() { - return id; - } - - /** - * Returns the display name of this connector. - * - * @return the connector name - */ - public String getName() { - return name; - } - - /** - * Returns a description of what this connector does. - * - * @return the connector description - */ - public String getDescription() { - return description; - } - - /** - * Returns the type of this connector. - * - * @return the connector type (e.g., "database", "api", "file") - */ - public String getType() { - return type; - } - - /** - * Returns the version of this connector. - * - * @return the version string - */ - public String getVersion() { - return version; - } - - /** - * Returns the capabilities this connector provides. - * - * @return immutable list of capability identifiers - */ - public List getCapabilities() { - return capabilities; - } - - /** - * Returns the configuration schema for this connector. - * - * @return immutable map of configuration options - */ - public Map getConfigSchema() { - return configSchema; - } - - /** - * Returns whether this connector is installed. - * - * @return true if installed - */ - public Boolean isInstalled() { - return installed; - } - - /** - * Returns whether this connector is enabled. - * - * @return true if enabled - */ - public Boolean isEnabled() { - return enabled; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConnectorInfo that = (ConnectorInfo) o; - return Objects.equals(id, that.id) && - Objects.equals(name, that.name) && - Objects.equals(type, that.type) && - Objects.equals(version, that.version); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, type, version); - } - - @Override - public String toString() { - return "ConnectorInfo{" + - "id='" + id + '\'' + - ", name='" + name + '\'' + - ", type='" + type + '\'' + - ", version='" + version + '\'' + - ", installed=" + installed + - ", enabled=" + enabled + - '}'; - } + @JsonProperty("id") + private final String id; + + @JsonProperty("name") + private final String name; + + @JsonProperty("description") + private final String description; + + @JsonProperty("type") + private final String type; + + @JsonProperty("version") + private final String version; + + @JsonProperty("capabilities") + private final List capabilities; + + @JsonProperty("config_schema") + private final Map configSchema; + + @JsonProperty("installed") + private final Boolean installed; + + @JsonProperty("enabled") + private final Boolean enabled; + + public ConnectorInfo( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("description") String description, + @JsonProperty("type") String type, + @JsonProperty("version") String version, + @JsonProperty("capabilities") List capabilities, + @JsonProperty("config_schema") Map configSchema, + @JsonProperty("installed") Boolean installed, + @JsonProperty("enabled") Boolean enabled) { + this.id = id; + this.name = name; + this.description = description; + this.type = type; + this.version = version; + this.capabilities = + capabilities != null ? Collections.unmodifiableList(capabilities) : Collections.emptyList(); + this.configSchema = + configSchema != null ? Collections.unmodifiableMap(configSchema) : Collections.emptyMap(); + this.installed = installed; + this.enabled = enabled; + } + + /** + * Returns the unique identifier for this connector. + * + * @return the connector ID + */ + public String getId() { + return id; + } + + /** + * Returns the display name of this connector. + * + * @return the connector name + */ + public String getName() { + return name; + } + + /** + * Returns a description of what this connector does. + * + * @return the connector description + */ + public String getDescription() { + return description; + } + + /** + * Returns the type of this connector. + * + * @return the connector type (e.g., "database", "api", "file") + */ + public String getType() { + return type; + } + + /** + * Returns the version of this connector. + * + * @return the version string + */ + public String getVersion() { + return version; + } + + /** + * Returns the capabilities this connector provides. + * + * @return immutable list of capability identifiers + */ + public List getCapabilities() { + return capabilities; + } + + /** + * Returns the configuration schema for this connector. + * + * @return immutable map of configuration options + */ + public Map getConfigSchema() { + return configSchema; + } + + /** + * Returns whether this connector is installed. + * + * @return true if installed + */ + public Boolean isInstalled() { + return installed; + } + + /** + * Returns whether this connector is enabled. + * + * @return true if enabled + */ + public Boolean isEnabled() { + return enabled; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorInfo that = (ConnectorInfo) o; + return Objects.equals(id, that.id) + && Objects.equals(name, that.name) + && Objects.equals(type, that.type) + && Objects.equals(version, that.version); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, type, version); + } + + @Override + public String toString() { + return "ConnectorInfo{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", type='" + + type + + '\'' + + ", version='" + + version + + '\'' + + ", installed=" + + installed + + ", enabled=" + + enabled + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java index 37cd9b3..3c18cd5 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorPolicyInfo.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; @@ -25,141 +24,162 @@ /** * Policy evaluation information included in MCP responses. * - * Provides transparency into policy enforcement decisions for - * request blocking and response redaction. + *

Provides transparency into policy enforcement decisions for request blocking and response + * redaction. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ConnectorPolicyInfo { - @JsonProperty("policies_evaluated") - private final int policiesEvaluated; - - @JsonProperty("blocked") - private final boolean blocked; - - @JsonProperty("block_reason") - private final String blockReason; - - @JsonProperty("redactions_applied") - private final int redactionsApplied; - - @JsonProperty("processing_time_ms") - private final long processingTimeMs; - - @JsonProperty("matched_policies") - private final List matchedPolicies; - - @JsonProperty("exfiltration_check") - private final ExfiltrationCheckInfo exfiltrationCheck; - - @JsonProperty("dynamic_policy_info") - private final DynamicPolicyInfo dynamicPolicyInfo; - - public ConnectorPolicyInfo( - @JsonProperty("policies_evaluated") int policiesEvaluated, - @JsonProperty("blocked") boolean blocked, - @JsonProperty("block_reason") String blockReason, - @JsonProperty("redactions_applied") int redactionsApplied, - @JsonProperty("processing_time_ms") long processingTimeMs, - @JsonProperty("matched_policies") List matchedPolicies, - @JsonProperty("exfiltration_check") ExfiltrationCheckInfo exfiltrationCheck, - @JsonProperty("dynamic_policy_info") DynamicPolicyInfo dynamicPolicyInfo) { - this.policiesEvaluated = policiesEvaluated; - this.blocked = blocked; - this.blockReason = blockReason; - this.redactionsApplied = redactionsApplied; - this.processingTimeMs = processingTimeMs; - this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList(); - this.exfiltrationCheck = exfiltrationCheck; - this.dynamicPolicyInfo = dynamicPolicyInfo; - } - - /** - * Backward-compatible constructor without exfiltration and dynamic policy fields. - */ - public ConnectorPolicyInfo( - int policiesEvaluated, - boolean blocked, - String blockReason, - int redactionsApplied, - long processingTimeMs, - List matchedPolicies) { - this(policiesEvaluated, blocked, blockReason, redactionsApplied, - processingTimeMs, matchedPolicies, null, null); - } - - public int getPoliciesEvaluated() { - return policiesEvaluated; - } - - public boolean isBlocked() { - return blocked; - } - - public String getBlockReason() { - return blockReason; - } - - public int getRedactionsApplied() { - return redactionsApplied; - } - - public long getProcessingTimeMs() { - return processingTimeMs; - } - - public List getMatchedPolicies() { - return matchedPolicies; - } - - /** - * Returns exfiltration check information (Issue #966). - * May be null if exfiltration checking is disabled. - */ - public ExfiltrationCheckInfo getExfiltrationCheck() { - return exfiltrationCheck; - } - - /** - * Returns dynamic policy evaluation information (Issue #968). - * May be null if dynamic policies are disabled. - */ - public DynamicPolicyInfo getDynamicPolicyInfo() { - return dynamicPolicyInfo; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConnectorPolicyInfo that = (ConnectorPolicyInfo) o; - return policiesEvaluated == that.policiesEvaluated && - blocked == that.blocked && - redactionsApplied == that.redactionsApplied && - processingTimeMs == that.processingTimeMs && - Objects.equals(blockReason, that.blockReason) && - Objects.equals(matchedPolicies, that.matchedPolicies) && - Objects.equals(exfiltrationCheck, that.exfiltrationCheck) && - Objects.equals(dynamicPolicyInfo, that.dynamicPolicyInfo); - } - - @Override - public int hashCode() { - return Objects.hash(policiesEvaluated, blocked, blockReason, redactionsApplied, - processingTimeMs, matchedPolicies, exfiltrationCheck, dynamicPolicyInfo); - } - - @Override - public String toString() { - return "ConnectorPolicyInfo{" + - "policiesEvaluated=" + policiesEvaluated + - ", blocked=" + blocked + - ", blockReason='" + blockReason + '\'' + - ", redactionsApplied=" + redactionsApplied + - ", processingTimeMs=" + processingTimeMs + - ", matchedPolicies=" + matchedPolicies + - ", exfiltrationCheck=" + exfiltrationCheck + - ", dynamicPolicyInfo=" + dynamicPolicyInfo + - '}'; - } + @JsonProperty("policies_evaluated") + private final int policiesEvaluated; + + @JsonProperty("blocked") + private final boolean blocked; + + @JsonProperty("block_reason") + private final String blockReason; + + @JsonProperty("redactions_applied") + private final int redactionsApplied; + + @JsonProperty("processing_time_ms") + private final long processingTimeMs; + + @JsonProperty("matched_policies") + private final List matchedPolicies; + + @JsonProperty("exfiltration_check") + private final ExfiltrationCheckInfo exfiltrationCheck; + + @JsonProperty("dynamic_policy_info") + private final DynamicPolicyInfo dynamicPolicyInfo; + + public ConnectorPolicyInfo( + @JsonProperty("policies_evaluated") int policiesEvaluated, + @JsonProperty("blocked") boolean blocked, + @JsonProperty("block_reason") String blockReason, + @JsonProperty("redactions_applied") int redactionsApplied, + @JsonProperty("processing_time_ms") long processingTimeMs, + @JsonProperty("matched_policies") List matchedPolicies, + @JsonProperty("exfiltration_check") ExfiltrationCheckInfo exfiltrationCheck, + @JsonProperty("dynamic_policy_info") DynamicPolicyInfo dynamicPolicyInfo) { + this.policiesEvaluated = policiesEvaluated; + this.blocked = blocked; + this.blockReason = blockReason; + this.redactionsApplied = redactionsApplied; + this.processingTimeMs = processingTimeMs; + this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList(); + this.exfiltrationCheck = exfiltrationCheck; + this.dynamicPolicyInfo = dynamicPolicyInfo; + } + + /** Backward-compatible constructor without exfiltration and dynamic policy fields. */ + public ConnectorPolicyInfo( + int policiesEvaluated, + boolean blocked, + String blockReason, + int redactionsApplied, + long processingTimeMs, + List matchedPolicies) { + this( + policiesEvaluated, + blocked, + blockReason, + redactionsApplied, + processingTimeMs, + matchedPolicies, + null, + null); + } + + public int getPoliciesEvaluated() { + return policiesEvaluated; + } + + public boolean isBlocked() { + return blocked; + } + + public String getBlockReason() { + return blockReason; + } + + public int getRedactionsApplied() { + return redactionsApplied; + } + + public long getProcessingTimeMs() { + return processingTimeMs; + } + + public List getMatchedPolicies() { + return matchedPolicies; + } + + /** + * Returns exfiltration check information (Issue #966). May be null if exfiltration checking is + * disabled. + */ + public ExfiltrationCheckInfo getExfiltrationCheck() { + return exfiltrationCheck; + } + + /** + * Returns dynamic policy evaluation information (Issue #968). May be null if dynamic policies are + * disabled. + */ + public DynamicPolicyInfo getDynamicPolicyInfo() { + return dynamicPolicyInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorPolicyInfo that = (ConnectorPolicyInfo) o; + return policiesEvaluated == that.policiesEvaluated + && blocked == that.blocked + && redactionsApplied == that.redactionsApplied + && processingTimeMs == that.processingTimeMs + && Objects.equals(blockReason, that.blockReason) + && Objects.equals(matchedPolicies, that.matchedPolicies) + && Objects.equals(exfiltrationCheck, that.exfiltrationCheck) + && Objects.equals(dynamicPolicyInfo, that.dynamicPolicyInfo); + } + + @Override + public int hashCode() { + return Objects.hash( + policiesEvaluated, + blocked, + blockReason, + redactionsApplied, + processingTimeMs, + matchedPolicies, + exfiltrationCheck, + dynamicPolicyInfo); + } + + @Override + public String toString() { + return "ConnectorPolicyInfo{" + + "policiesEvaluated=" + + policiesEvaluated + + ", blocked=" + + blocked + + ", blockReason='" + + blockReason + + '\'' + + ", redactionsApplied=" + + redactionsApplied + + ", processingTimeMs=" + + processingTimeMs + + ", matchedPolicies=" + + matchedPolicies + + ", exfiltrationCheck=" + + exfiltrationCheck + + ", dynamicPolicyInfo=" + + dynamicPolicyInfo + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorQuery.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorQuery.java index 842382b..540d867 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorQuery.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorQuery.java @@ -17,138 +17,143 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.Objects; -/** - * Request for querying an MCP connector. - */ +/** Request for querying an MCP connector. */ @JsonInclude(JsonInclude.Include.NON_NULL) public final class ConnectorQuery { - @JsonProperty("connector_id") - private final String connectorId; + @JsonProperty("connector_id") + private final String connectorId; - @JsonProperty("operation") - private final String operation; + @JsonProperty("operation") + private final String operation; - @JsonProperty("parameters") - private final Map parameters; + @JsonProperty("parameters") + private final Map parameters; - @JsonProperty("user_token") - private final String userToken; + @JsonProperty("user_token") + private final String userToken; - @JsonProperty("timeout_ms") - private final Integer timeoutMs; + @JsonProperty("timeout_ms") + private final Integer timeoutMs; - private ConnectorQuery(Builder builder) { - this.connectorId = Objects.requireNonNull(builder.connectorId, "connectorId cannot be null"); - this.operation = Objects.requireNonNull(builder.operation, "operation cannot be null"); - this.parameters = builder.parameters != null + private ConnectorQuery(Builder builder) { + this.connectorId = Objects.requireNonNull(builder.connectorId, "connectorId cannot be null"); + this.operation = Objects.requireNonNull(builder.operation, "operation cannot be null"); + this.parameters = + builder.parameters != null ? Collections.unmodifiableMap(new HashMap<>(builder.parameters)) : Collections.emptyMap(); - this.userToken = builder.userToken; - this.timeoutMs = builder.timeoutMs; - } - - public String getConnectorId() { - return connectorId; - } - - public String getOperation() { - return operation; - } - - public Map getParameters() { - return parameters; - } - - public String getUserToken() { - return userToken; + this.userToken = builder.userToken; + this.timeoutMs = builder.timeoutMs; + } + + public String getConnectorId() { + return connectorId; + } + + public String getOperation() { + return operation; + } + + public Map getParameters() { + return parameters; + } + + public String getUserToken() { + return userToken; + } + + public Integer getTimeoutMs() { + return timeoutMs; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorQuery that = (ConnectorQuery) o; + return Objects.equals(connectorId, that.connectorId) + && Objects.equals(operation, that.operation) + && Objects.equals(parameters, that.parameters) + && Objects.equals(userToken, that.userToken) + && Objects.equals(timeoutMs, that.timeoutMs); + } + + @Override + public int hashCode() { + return Objects.hash(connectorId, operation, parameters, userToken, timeoutMs); + } + + @Override + public String toString() { + return "ConnectorQuery{" + + "connectorId='" + + connectorId + + '\'' + + ", operation='" + + operation + + '\'' + + ", userToken='" + + userToken + + '\'' + + ", timeoutMs=" + + timeoutMs + + '}'; + } + + public static final class Builder { + private String connectorId; + private String operation; + private Map parameters; + private String userToken; + private Integer timeoutMs; + + private Builder() {} + + public Builder connectorId(String connectorId) { + this.connectorId = connectorId; + return this; } - public Integer getTimeoutMs() { - return timeoutMs; + public Builder operation(String operation) { + this.operation = operation; + return this; } - public static Builder builder() { - return new Builder(); + public Builder parameters(Map parameters) { + this.parameters = parameters; + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConnectorQuery that = (ConnectorQuery) o; - return Objects.equals(connectorId, that.connectorId) && - Objects.equals(operation, that.operation) && - Objects.equals(parameters, that.parameters) && - Objects.equals(userToken, that.userToken) && - Objects.equals(timeoutMs, that.timeoutMs); + public Builder addParameter(String key, Object value) { + if (this.parameters == null) { + this.parameters = new HashMap<>(); + } + this.parameters.put(key, value); + return this; } - @Override - public int hashCode() { - return Objects.hash(connectorId, operation, parameters, userToken, timeoutMs); + public Builder userToken(String userToken) { + this.userToken = userToken; + return this; } - @Override - public String toString() { - return "ConnectorQuery{" + - "connectorId='" + connectorId + '\'' + - ", operation='" + operation + '\'' + - ", userToken='" + userToken + '\'' + - ", timeoutMs=" + timeoutMs + - '}'; + public Builder timeoutMs(int timeoutMs) { + this.timeoutMs = timeoutMs; + return this; } - public static final class Builder { - private String connectorId; - private String operation; - private Map parameters; - private String userToken; - private Integer timeoutMs; - - private Builder() {} - - public Builder connectorId(String connectorId) { - this.connectorId = connectorId; - return this; - } - - public Builder operation(String operation) { - this.operation = operation; - return this; - } - - public Builder parameters(Map parameters) { - this.parameters = parameters; - return this; - } - - public Builder addParameter(String key, Object value) { - if (this.parameters == null) { - this.parameters = new HashMap<>(); - } - this.parameters.put(key, value); - return this; - } - - public Builder userToken(String userToken) { - this.userToken = userToken; - return this; - } - - public Builder timeoutMs(int timeoutMs) { - this.timeoutMs = timeoutMs; - return this; - } - - public ConnectorQuery build() { - return new ConnectorQuery(this); - } + public ConnectorQuery build() { + return new ConnectorQuery(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ConnectorResponse.java b/src/main/java/com/getaxonflow/sdk/types/ConnectorResponse.java index 40a263a..704431c 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ConnectorResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/ConnectorResponse.java @@ -17,144 +17,149 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; -/** - * Response from an MCP connector query. - */ +/** Response from an MCP connector query. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ConnectorResponse { - @JsonProperty("success") - private final boolean success; - - @JsonProperty("data") - private final Object data; - - @JsonProperty("error") - private final String error; - - @JsonProperty("connector_id") - private final String connectorId; - - @JsonProperty("operation") - private final String operation; - - @JsonProperty("processing_time") - private final String processingTime; - - @JsonProperty("redacted") - private final boolean redacted; - - @JsonProperty("redacted_fields") - private final List redactedFields; - - @JsonProperty("policy_info") - private final ConnectorPolicyInfo policyInfo; - - public ConnectorResponse( - @JsonProperty("success") boolean success, - @JsonProperty("data") Object data, - @JsonProperty("error") String error, - @JsonProperty("connector_id") String connectorId, - @JsonProperty("operation") String operation, - @JsonProperty("processing_time") String processingTime, - @JsonProperty("redacted") boolean redacted, - @JsonProperty("redacted_fields") List redactedFields, - @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { - this.success = success; - this.data = data; - this.error = error; - this.connectorId = connectorId; - this.operation = operation; - this.processingTime = processingTime; - this.redacted = redacted; - this.redactedFields = redactedFields != null ? redactedFields : Collections.emptyList(); - this.policyInfo = policyInfo; - } - - /** - * Backward-compatible constructor without policy fields. - * Creates a ConnectorResponse with default values for redacted (false), - * redactedFields (empty list), and policyInfo (null). - */ - public ConnectorResponse( - boolean success, - Object data, - String error, - String connectorId, - String operation, - String processingTime) { - this(success, data, error, connectorId, operation, processingTime, false, null, null); - } - - public boolean isSuccess() { - return success; - } - - public Object getData() { - return data; - } - - public String getError() { - return error; - } - - public String getConnectorId() { - return connectorId; - } - - public String getOperation() { - return operation; - } - - public String getProcessingTime() { - return processingTime; - } - - public boolean isRedacted() { - return redacted; - } - - public List getRedactedFields() { - return redactedFields; - } - - public ConnectorPolicyInfo getPolicyInfo() { - return policyInfo; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ConnectorResponse that = (ConnectorResponse) o; - return success == that.success && - redacted == that.redacted && - Objects.equals(data, that.data) && - Objects.equals(error, that.error) && - Objects.equals(connectorId, that.connectorId) && - Objects.equals(operation, that.operation) && - Objects.equals(redactedFields, that.redactedFields) && - Objects.equals(policyInfo, that.policyInfo); - } - - @Override - public int hashCode() { - return Objects.hash(success, data, error, connectorId, operation, redacted, redactedFields, policyInfo); - } - - @Override - public String toString() { - return "ConnectorResponse{" + - "success=" + success + - ", connectorId='" + connectorId + '\'' + - ", operation='" + operation + '\'' + - ", redacted=" + redacted + - ", error='" + error + '\'' + - '}'; - } + @JsonProperty("success") + private final boolean success; + + @JsonProperty("data") + private final Object data; + + @JsonProperty("error") + private final String error; + + @JsonProperty("connector_id") + private final String connectorId; + + @JsonProperty("operation") + private final String operation; + + @JsonProperty("processing_time") + private final String processingTime; + + @JsonProperty("redacted") + private final boolean redacted; + + @JsonProperty("redacted_fields") + private final List redactedFields; + + @JsonProperty("policy_info") + private final ConnectorPolicyInfo policyInfo; + + public ConnectorResponse( + @JsonProperty("success") boolean success, + @JsonProperty("data") Object data, + @JsonProperty("error") String error, + @JsonProperty("connector_id") String connectorId, + @JsonProperty("operation") String operation, + @JsonProperty("processing_time") String processingTime, + @JsonProperty("redacted") boolean redacted, + @JsonProperty("redacted_fields") List redactedFields, + @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { + this.success = success; + this.data = data; + this.error = error; + this.connectorId = connectorId; + this.operation = operation; + this.processingTime = processingTime; + this.redacted = redacted; + this.redactedFields = redactedFields != null ? redactedFields : Collections.emptyList(); + this.policyInfo = policyInfo; + } + + /** + * Backward-compatible constructor without policy fields. Creates a ConnectorResponse with default + * values for redacted (false), redactedFields (empty list), and policyInfo (null). + */ + public ConnectorResponse( + boolean success, + Object data, + String error, + String connectorId, + String operation, + String processingTime) { + this(success, data, error, connectorId, operation, processingTime, false, null, null); + } + + public boolean isSuccess() { + return success; + } + + public Object getData() { + return data; + } + + public String getError() { + return error; + } + + public String getConnectorId() { + return connectorId; + } + + public String getOperation() { + return operation; + } + + public String getProcessingTime() { + return processingTime; + } + + public boolean isRedacted() { + return redacted; + } + + public List getRedactedFields() { + return redactedFields; + } + + public ConnectorPolicyInfo getPolicyInfo() { + return policyInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ConnectorResponse that = (ConnectorResponse) o; + return success == that.success + && redacted == that.redacted + && Objects.equals(data, that.data) + && Objects.equals(error, that.error) + && Objects.equals(connectorId, that.connectorId) + && Objects.equals(operation, that.operation) + && Objects.equals(redactedFields, that.redactedFields) + && Objects.equals(policyInfo, that.policyInfo); + } + + @Override + public int hashCode() { + return Objects.hash( + success, data, error, connectorId, operation, redacted, redactedFields, policyInfo); + } + + @Override + public String toString() { + return "ConnectorResponse{" + + "success=" + + success + + ", connectorId='" + + connectorId + + '\'' + + ", operation='" + + operation + + '\'' + + ", redacted=" + + redacted + + ", error='" + + error + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java index 200091b..3707b09 100644 --- a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyInfo.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; @@ -25,86 +24,83 @@ /** * Information about dynamic policy evaluation (Issue #968). * - *

Dynamic policies are evaluated by the Orchestrator and can include - * rate limiting, budget controls, time-based access, and role-based access.

+ *

Dynamic policies are evaluated by the Orchestrator and can include rate limiting, budget + * controls, time-based access, and role-based access. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class DynamicPolicyInfo { - @JsonProperty("policies_evaluated") - private final int policiesEvaluated; + @JsonProperty("policies_evaluated") + private final int policiesEvaluated; - @JsonProperty("matched_policies") - private final List matchedPolicies; + @JsonProperty("matched_policies") + private final List matchedPolicies; - @JsonProperty("orchestrator_reachable") - private final boolean orchestratorReachable; + @JsonProperty("orchestrator_reachable") + private final boolean orchestratorReachable; - @JsonProperty("processing_time_ms") - private final long processingTimeMs; + @JsonProperty("processing_time_ms") + private final long processingTimeMs; - public DynamicPolicyInfo( - @JsonProperty("policies_evaluated") int policiesEvaluated, - @JsonProperty("matched_policies") List matchedPolicies, - @JsonProperty("orchestrator_reachable") boolean orchestratorReachable, - @JsonProperty("processing_time_ms") long processingTimeMs) { - this.policiesEvaluated = policiesEvaluated; - this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList(); - this.orchestratorReachable = orchestratorReachable; - this.processingTimeMs = processingTimeMs; - } + public DynamicPolicyInfo( + @JsonProperty("policies_evaluated") int policiesEvaluated, + @JsonProperty("matched_policies") List matchedPolicies, + @JsonProperty("orchestrator_reachable") boolean orchestratorReachable, + @JsonProperty("processing_time_ms") long processingTimeMs) { + this.policiesEvaluated = policiesEvaluated; + this.matchedPolicies = matchedPolicies != null ? matchedPolicies : Collections.emptyList(); + this.orchestratorReachable = orchestratorReachable; + this.processingTimeMs = processingTimeMs; + } - /** - * Returns the number of dynamic policies checked. - */ - public int getPoliciesEvaluated() { - return policiesEvaluated; - } + /** Returns the number of dynamic policies checked. */ + public int getPoliciesEvaluated() { + return policiesEvaluated; + } - /** - * Returns details about policies that matched. - */ - public List getMatchedPolicies() { - return matchedPolicies; - } + /** Returns details about policies that matched. */ + public List getMatchedPolicies() { + return matchedPolicies; + } - /** - * Returns whether the Orchestrator was reachable. - */ - public boolean isOrchestratorReachable() { - return orchestratorReachable; - } + /** Returns whether the Orchestrator was reachable. */ + public boolean isOrchestratorReachable() { + return orchestratorReachable; + } - /** - * Returns the time taken for dynamic policy evaluation in milliseconds. - */ - public long getProcessingTimeMs() { - return processingTimeMs; - } + /** Returns the time taken for dynamic policy evaluation in milliseconds. */ + public long getProcessingTimeMs() { + return processingTimeMs; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DynamicPolicyInfo that = (DynamicPolicyInfo) o; - return policiesEvaluated == that.policiesEvaluated && - orchestratorReachable == that.orchestratorReachable && - processingTimeMs == that.processingTimeMs && - Objects.equals(matchedPolicies, that.matchedPolicies); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DynamicPolicyInfo that = (DynamicPolicyInfo) o; + return policiesEvaluated == that.policiesEvaluated + && orchestratorReachable == that.orchestratorReachable + && processingTimeMs == that.processingTimeMs + && Objects.equals(matchedPolicies, that.matchedPolicies); + } - @Override - public int hashCode() { - return Objects.hash(policiesEvaluated, matchedPolicies, orchestratorReachable, processingTimeMs); - } + @Override + public int hashCode() { + return Objects.hash( + policiesEvaluated, matchedPolicies, orchestratorReachable, processingTimeMs); + } - @Override - public String toString() { - return "DynamicPolicyInfo{" + - "policiesEvaluated=" + policiesEvaluated + - ", matchedPolicies=" + matchedPolicies + - ", orchestratorReachable=" + orchestratorReachable + - ", processingTimeMs=" + processingTimeMs + - '}'; - } + @Override + public String toString() { + return "DynamicPolicyInfo{" + + "policiesEvaluated=" + + policiesEvaluated + + ", matchedPolicies=" + + matchedPolicies + + ", orchestratorReachable=" + + orchestratorReachable + + ", processingTimeMs=" + + processingTimeMs + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java index 565dccf..7a14adc 100644 --- a/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java +++ b/src/main/java/com/getaxonflow/sdk/types/DynamicPolicyMatch.java @@ -17,106 +17,105 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Details about a matched dynamic policy. * - *

Provides information about which dynamic policy matched during - * Orchestrator evaluation, including the policy type and action taken.

+ *

Provides information about which dynamic policy matched during Orchestrator evaluation, + * including the policy type and action taken. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class DynamicPolicyMatch { - @JsonProperty("policy_id") - private final String policyId; - - @JsonProperty("policy_name") - private final String policyName; - - @JsonProperty("policy_type") - private final String policyType; - - @JsonProperty("action") - private final String action; - - @JsonProperty("reason") - private final String reason; - - public DynamicPolicyMatch( - @JsonProperty("policy_id") String policyId, - @JsonProperty("policy_name") String policyName, - @JsonProperty("policy_type") String policyType, - @JsonProperty("action") String action, - @JsonProperty("reason") String reason) { - this.policyId = policyId; - this.policyName = policyName; - this.policyType = policyType; - this.action = action; - this.reason = reason; - } - - /** - * Returns the unique identifier of the policy. - */ - public String getPolicyId() { - return policyId; - } - - /** - * Returns the human-readable name of the policy. - */ - public String getPolicyName() { - return policyName; - } - - /** - * Returns the type of policy (rate-limit, budget, time-access, role-access, mcp, connector). - */ - public String getPolicyType() { - return policyType; - } - - /** - * Returns the action taken (allow, block, log, etc.). - */ - public String getAction() { - return action; - } - - /** - * Returns the context for the policy match. - */ - public String getReason() { - return reason; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DynamicPolicyMatch that = (DynamicPolicyMatch) o; - return Objects.equals(policyId, that.policyId) && - Objects.equals(policyName, that.policyName) && - Objects.equals(policyType, that.policyType) && - Objects.equals(action, that.action) && - Objects.equals(reason, that.reason); - } - - @Override - public int hashCode() { - return Objects.hash(policyId, policyName, policyType, action, reason); - } - - @Override - public String toString() { - return "DynamicPolicyMatch{" + - "policyId='" + policyId + '\'' + - ", policyName='" + policyName + '\'' + - ", policyType='" + policyType + '\'' + - ", action='" + action + '\'' + - ", reason='" + reason + '\'' + - '}'; - } + @JsonProperty("policy_id") + private final String policyId; + + @JsonProperty("policy_name") + private final String policyName; + + @JsonProperty("policy_type") + private final String policyType; + + @JsonProperty("action") + private final String action; + + @JsonProperty("reason") + private final String reason; + + public DynamicPolicyMatch( + @JsonProperty("policy_id") String policyId, + @JsonProperty("policy_name") String policyName, + @JsonProperty("policy_type") String policyType, + @JsonProperty("action") String action, + @JsonProperty("reason") String reason) { + this.policyId = policyId; + this.policyName = policyName; + this.policyType = policyType; + this.action = action; + this.reason = reason; + } + + /** Returns the unique identifier of the policy. */ + public String getPolicyId() { + return policyId; + } + + /** Returns the human-readable name of the policy. */ + public String getPolicyName() { + return policyName; + } + + /** Returns the type of policy (rate-limit, budget, time-access, role-access, mcp, connector). */ + public String getPolicyType() { + return policyType; + } + + /** Returns the action taken (allow, block, log, etc.). */ + public String getAction() { + return action; + } + + /** Returns the context for the policy match. */ + public String getReason() { + return reason; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + DynamicPolicyMatch that = (DynamicPolicyMatch) o; + return Objects.equals(policyId, that.policyId) + && Objects.equals(policyName, that.policyName) + && Objects.equals(policyType, that.policyType) + && Objects.equals(action, that.action) + && Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(policyId, policyName, policyType, action, reason); + } + + @Override + public String toString() { + return "DynamicPolicyMatch{" + + "policyId='" + + policyId + + '\'' + + ", policyName='" + + policyName + + '\'' + + ", policyType='" + + policyType + + '\'' + + ", action='" + + action + + '\'' + + ", reason='" + + reason + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ExecutionMode.java b/src/main/java/com/getaxonflow/sdk/types/ExecutionMode.java index 9b395df..6303217 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ExecutionMode.java +++ b/src/main/java/com/getaxonflow/sdk/types/ExecutionMode.java @@ -21,79 +21,68 @@ * Execution mode for multi-agent plan execution. * *

Controls how plan steps are scheduled and executed: + * *

    - *
  • {@link #AUTO} - Let the engine determine optimal execution order
  • - *
  • {@link #SEQUENTIAL} - Execute steps strictly in order
  • - *
  • {@link #PARALLEL} - Execute independent steps concurrently
  • - *
  • {@link #BALANCED} - Balance between parallelism and resource usage
  • - *
  • {@link #CONFIRM} - Pause before each step for user confirmation
  • - *
  • {@link #STEP} - Execute one step at a time with manual advancement
  • + *
  • {@link #AUTO} - Let the engine determine optimal execution order + *
  • {@link #SEQUENTIAL} - Execute steps strictly in order + *
  • {@link #PARALLEL} - Execute independent steps concurrently + *
  • {@link #BALANCED} - Balance between parallelism and resource usage + *
  • {@link #CONFIRM} - Pause before each step for user confirmation + *
  • {@link #STEP} - Execute one step at a time with manual advancement *
*/ public enum ExecutionMode { - /** - * Let the engine determine optimal execution order. - */ - AUTO("auto"), + /** Let the engine determine optimal execution order. */ + AUTO("auto"), - /** - * Execute steps strictly in order. - */ - SEQUENTIAL("sequential"), + /** Execute steps strictly in order. */ + SEQUENTIAL("sequential"), - /** - * Execute independent steps concurrently. - */ - PARALLEL("parallel"), + /** Execute independent steps concurrently. */ + PARALLEL("parallel"), - /** - * Balance between parallelism and resource usage. - */ - BALANCED("balanced"), + /** Balance between parallelism and resource usage. */ + BALANCED("balanced"), - /** - * Pause before each step for user confirmation. - */ - CONFIRM("confirm"), + /** Pause before each step for user confirmation. */ + CONFIRM("confirm"), - /** - * Execute one step at a time with manual advancement. - */ - STEP("step"); + /** Execute one step at a time with manual advancement. */ + STEP("step"); - private final String value; + private final String value; - ExecutionMode(String value) { - this.value = value; - } + ExecutionMode(String value) { + this.value = value; + } - /** - * Returns the string value used in API requests. - * - * @return the execution mode value as a string - */ - @JsonValue - public String getValue() { - return value; - } + /** + * Returns the string value used in API requests. + * + * @return the execution mode value as a string + */ + @JsonValue + public String getValue() { + return value; + } - /** - * Parses a string value to an ExecutionMode enum. - * - * @param value the string value to parse - * @return the corresponding ExecutionMode - * @throws IllegalArgumentException if the value is not recognized - */ - public static ExecutionMode fromValue(String value) { - if (value == null) { - throw new IllegalArgumentException("Execution mode cannot be null"); - } - for (ExecutionMode mode : values()) { - if (mode.value.equalsIgnoreCase(value)) { - return mode; - } - } - throw new IllegalArgumentException("Unknown execution mode: " + value); + /** + * Parses a string value to an ExecutionMode enum. + * + * @param value the string value to parse + * @return the corresponding ExecutionMode + * @throws IllegalArgumentException if the value is not recognized + */ + public static ExecutionMode fromValue(String value) { + if (value == null) { + throw new IllegalArgumentException("Execution mode cannot be null"); + } + for (ExecutionMode mode : values()) { + if (mode.value.equalsIgnoreCase(value)) { + return mode; + } } + throw new IllegalArgumentException("Unknown execution mode: " + value); + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java b/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java index 54d3f2b..b5ed013 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/ExfiltrationCheckInfo.java @@ -17,106 +17,100 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Information about exfiltration limit checks (Issue #966). * - *

Helps prevent large-scale data extraction via MCP queries by enforcing - * row count and data volume limits on responses.

+ *

Helps prevent large-scale data extraction via MCP queries by enforcing row count and data + * volume limits on responses. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ExfiltrationCheckInfo { - @JsonProperty("rows_returned") - private final long rowsReturned; - - @JsonProperty("row_limit") - private final int rowLimit; - - @JsonProperty("bytes_returned") - private final long bytesReturned; - - @JsonProperty("byte_limit") - private final long byteLimit; - - @JsonProperty("within_limits") - private final boolean withinLimits; - - public ExfiltrationCheckInfo( - @JsonProperty("rows_returned") long rowsReturned, - @JsonProperty("row_limit") int rowLimit, - @JsonProperty("bytes_returned") long bytesReturned, - @JsonProperty("byte_limit") long byteLimit, - @JsonProperty("within_limits") boolean withinLimits) { - this.rowsReturned = rowsReturned; - this.rowLimit = rowLimit; - this.bytesReturned = bytesReturned; - this.byteLimit = byteLimit; - this.withinLimits = withinLimits; - } - - /** - * Returns the number of rows in the response. - */ - public long getRowsReturned() { - return rowsReturned; - } - - /** - * Returns the configured maximum rows per query. - */ - public int getRowLimit() { - return rowLimit; - } - - /** - * Returns the size of the response data in bytes. - */ - public long getBytesReturned() { - return bytesReturned; - } - - /** - * Returns the configured maximum bytes per response. - */ - public long getByteLimit() { - return byteLimit; - } - - /** - * Returns whether the response is within configured limits. - */ - public boolean isWithinLimits() { - return withinLimits; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ExfiltrationCheckInfo that = (ExfiltrationCheckInfo) o; - return rowsReturned == that.rowsReturned && - rowLimit == that.rowLimit && - bytesReturned == that.bytesReturned && - byteLimit == that.byteLimit && - withinLimits == that.withinLimits; - } - - @Override - public int hashCode() { - return Objects.hash(rowsReturned, rowLimit, bytesReturned, byteLimit, withinLimits); - } - - @Override - public String toString() { - return "ExfiltrationCheckInfo{" + - "rowsReturned=" + rowsReturned + - ", rowLimit=" + rowLimit + - ", bytesReturned=" + bytesReturned + - ", byteLimit=" + byteLimit + - ", withinLimits=" + withinLimits + - '}'; - } + @JsonProperty("rows_returned") + private final long rowsReturned; + + @JsonProperty("row_limit") + private final int rowLimit; + + @JsonProperty("bytes_returned") + private final long bytesReturned; + + @JsonProperty("byte_limit") + private final long byteLimit; + + @JsonProperty("within_limits") + private final boolean withinLimits; + + public ExfiltrationCheckInfo( + @JsonProperty("rows_returned") long rowsReturned, + @JsonProperty("row_limit") int rowLimit, + @JsonProperty("bytes_returned") long bytesReturned, + @JsonProperty("byte_limit") long byteLimit, + @JsonProperty("within_limits") boolean withinLimits) { + this.rowsReturned = rowsReturned; + this.rowLimit = rowLimit; + this.bytesReturned = bytesReturned; + this.byteLimit = byteLimit; + this.withinLimits = withinLimits; + } + + /** Returns the number of rows in the response. */ + public long getRowsReturned() { + return rowsReturned; + } + + /** Returns the configured maximum rows per query. */ + public int getRowLimit() { + return rowLimit; + } + + /** Returns the size of the response data in bytes. */ + public long getBytesReturned() { + return bytesReturned; + } + + /** Returns the configured maximum bytes per response. */ + public long getByteLimit() { + return byteLimit; + } + + /** Returns whether the response is within configured limits. */ + public boolean isWithinLimits() { + return withinLimits; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExfiltrationCheckInfo that = (ExfiltrationCheckInfo) o; + return rowsReturned == that.rowsReturned + && rowLimit == that.rowLimit + && bytesReturned == that.bytesReturned + && byteLimit == that.byteLimit + && withinLimits == that.withinLimits; + } + + @Override + public int hashCode() { + return Objects.hash(rowsReturned, rowLimit, bytesReturned, byteLimit, withinLimits); + } + + @Override + public String toString() { + return "ExfiltrationCheckInfo{" + + "rowsReturned=" + + rowsReturned + + ", rowLimit=" + + rowLimit + + ", bytesReturned=" + + bytesReturned + + ", byteLimit=" + + byteLimit + + ", withinLimits=" + + withinLimits + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/GeneratePlanOptions.java b/src/main/java/com/getaxonflow/sdk/types/GeneratePlanOptions.java index 1ad117a..fd4f1e3 100644 --- a/src/main/java/com/getaxonflow/sdk/types/GeneratePlanOptions.java +++ b/src/main/java/com/getaxonflow/sdk/types/GeneratePlanOptions.java @@ -17,16 +17,16 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Options for generating a multi-agent plan. * - *

Provides additional configuration beyond what is in {@link PlanRequest}, - * such as execution mode control. + *

Provides additional configuration beyond what is in {@link PlanRequest}, such as execution + * mode control. * *

Example usage: + * *

{@code
  * GeneratePlanOptions options = GeneratePlanOptions.builder()
  *     .executionMode(ExecutionMode.PARALLEL)
@@ -38,72 +38,68 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class GeneratePlanOptions {
 
-    @JsonProperty("execution_mode")
-    private final ExecutionMode executionMode;
+  @JsonProperty("execution_mode")
+  private final ExecutionMode executionMode;
 
-    private GeneratePlanOptions(Builder builder) {
-        this.executionMode = builder.executionMode;
-    }
+  private GeneratePlanOptions(Builder builder) {
+    this.executionMode = builder.executionMode;
+  }
 
-    /**
-     * Returns the execution mode for the plan.
-     *
-     * @return the execution mode, or null if not specified
-     */
-    public ExecutionMode getExecutionMode() {
-        return executionMode;
-    }
+  /**
+   * Returns the execution mode for the plan.
+   *
+   * @return the execution mode, or null if not specified
+   */
+  public ExecutionMode getExecutionMode() {
+    return executionMode;
+  }
 
-    public static Builder builder() {
-        return new Builder();
-    }
+  public static Builder builder() {
+    return new Builder();
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        GeneratePlanOptions that = (GeneratePlanOptions) o;
-        return executionMode == that.executionMode;
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    GeneratePlanOptions that = (GeneratePlanOptions) o;
+    return executionMode == that.executionMode;
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(executionMode);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(executionMode);
+  }
 
-    @Override
-    public String toString() {
-        return "GeneratePlanOptions{" +
-               "executionMode=" + executionMode +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "GeneratePlanOptions{" + "executionMode=" + executionMode + '}';
+  }
 
-    /**
-     * Builder for GeneratePlanOptions.
-     */
-    public static final class Builder {
-        private ExecutionMode executionMode;
+  /** Builder for GeneratePlanOptions. */
+  public static final class Builder {
+    private ExecutionMode executionMode;
 
-        private Builder() {}
+    private Builder() {}
 
-        /**
-         * Sets the execution mode for plan generation.
-         *
-         * @param executionMode the execution mode
-         * @return this builder
-         */
-        public Builder executionMode(ExecutionMode executionMode) {
-            this.executionMode = executionMode;
-            return this;
-        }
+    /**
+     * Sets the execution mode for plan generation.
+     *
+     * @param executionMode the execution mode
+     * @return this builder
+     */
+    public Builder executionMode(ExecutionMode executionMode) {
+      this.executionMode = executionMode;
+      return this;
+    }
 
-        /**
-         * Builds the GeneratePlanOptions.
-         *
-         * @return a new GeneratePlanOptions instance
-         */
-        public GeneratePlanOptions build() {
-            return new GeneratePlanOptions(this);
-        }
+    /**
+     * Builds the GeneratePlanOptions.
+     *
+     * @return a new GeneratePlanOptions instance
+     */
+    public GeneratePlanOptions build() {
+      return new GeneratePlanOptions(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/HealthStatus.java b/src/main/java/com/getaxonflow/sdk/types/HealthStatus.java
index 052297a..88be53d 100644
--- a/src/main/java/com/getaxonflow/sdk/types/HealthStatus.java
+++ b/src/main/java/com/getaxonflow/sdk/types/HealthStatus.java
@@ -17,157 +17,163 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
 
-/**
- * Health status of the AxonFlow Agent.
- */
+/** Health status of the AxonFlow Agent. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class HealthStatus {
 
-    @JsonProperty("status")
-    private final String status;
-
-    @JsonProperty("version")
-    private final String version;
-
-    @JsonProperty("uptime")
-    private final String uptime;
-
-    @JsonProperty("components")
-    private final Map components;
-
-    @JsonProperty("capabilities")
-    private final List capabilities;
-
-    @JsonProperty("sdk_compatibility")
-    private final SDKCompatibility sdkCompatibility;
-
-    /**
-     * Backward-compatible constructor without capabilities and sdkCompatibility.
-     */
-    public HealthStatus(String status, String version, String uptime, Map components) {
-        this(status, version, uptime, components, null, null);
-    }
-
-    public HealthStatus(
-            @JsonProperty("status") String status,
-            @JsonProperty("version") String version,
-            @JsonProperty("uptime") String uptime,
-            @JsonProperty("components") Map components,
-            @JsonProperty("capabilities") List capabilities,
-            @JsonProperty("sdk_compatibility") SDKCompatibility sdkCompatibility) {
-        this.status = status;
-        this.version = version;
-        this.uptime = uptime;
-        this.components = components != null ? Collections.unmodifiableMap(components) : Collections.emptyMap();
-        this.capabilities = capabilities != null ? Collections.unmodifiableList(capabilities) : Collections.emptyList();
-        this.sdkCompatibility = sdkCompatibility;
-    }
-
-    /**
-     * Returns the overall health status.
-     *
-     * @return the status (e.g., "healthy", "degraded", "unhealthy")
-     */
-    public String getStatus() {
-        return status;
-    }
-
-    /**
-     * Returns the AxonFlow Agent version.
-     *
-     * @return the version string
-     */
-    public String getVersion() {
-        return version;
-    }
-
-    /**
-     * Returns how long the Agent has been running.
-     *
-     * @return the uptime string
-     */
-    public String getUptime() {
-        return uptime;
-    }
-
-    /**
-     * Returns the health status of individual components.
-     *
-     * @return immutable map of component statuses
-     */
-    public Map getComponents() {
-        return components;
-    }
-
-    /**
-     * Returns the list of capabilities advertised by the platform.
-     *
-     * @return immutable list of capabilities (never null)
-     */
-    public List getCapabilities() {
-        return capabilities;
-    }
-
-    /**
-     * Returns SDK compatibility information from the platform.
-     *
-     * @return the SDK compatibility info, or null if not provided
-     */
-    public SDKCompatibility getSdkCompatibility() {
-        return sdkCompatibility;
-    }
-
-    /**
-     * Checks if the Agent is healthy.
-     *
-     * @return true if status is "healthy"
-     */
-    public boolean isHealthy() {
-        return "healthy".equalsIgnoreCase(status) || "ok".equalsIgnoreCase(status);
-    }
-
-    /**
-     * Checks if the platform advertises a given capability by name.
-     *
-     * @param name the capability name to check
-     * @return true if the capability is present
-     */
-    public boolean hasCapability(String name) {
-        if (name == null) return false;
-        return capabilities.stream().anyMatch(c -> name.equals(c.getName()));
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        HealthStatus that = (HealthStatus) o;
-        return Objects.equals(status, that.status) &&
-               Objects.equals(version, that.version) &&
-               Objects.equals(uptime, that.uptime) &&
-               Objects.equals(capabilities, that.capabilities) &&
-               Objects.equals(sdkCompatibility, that.sdkCompatibility);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(status, version, uptime, capabilities, sdkCompatibility);
-    }
-
-    @Override
-    public String toString() {
-        return "HealthStatus{" +
-               "status='" + status + '\'' +
-               ", version='" + version + '\'' +
-               ", uptime='" + uptime + '\'' +
-               ", capabilities=" + capabilities +
-               ", sdkCompatibility=" + sdkCompatibility +
-               '}';
-    }
+  @JsonProperty("status")
+  private final String status;
+
+  @JsonProperty("version")
+  private final String version;
+
+  @JsonProperty("uptime")
+  private final String uptime;
+
+  @JsonProperty("components")
+  private final Map components;
+
+  @JsonProperty("capabilities")
+  private final List capabilities;
+
+  @JsonProperty("sdk_compatibility")
+  private final SDKCompatibility sdkCompatibility;
+
+  /** Backward-compatible constructor without capabilities and sdkCompatibility. */
+  public HealthStatus(
+      String status, String version, String uptime, Map components) {
+    this(status, version, uptime, components, null, null);
+  }
+
+  public HealthStatus(
+      @JsonProperty("status") String status,
+      @JsonProperty("version") String version,
+      @JsonProperty("uptime") String uptime,
+      @JsonProperty("components") Map components,
+      @JsonProperty("capabilities") List capabilities,
+      @JsonProperty("sdk_compatibility") SDKCompatibility sdkCompatibility) {
+    this.status = status;
+    this.version = version;
+    this.uptime = uptime;
+    this.components =
+        components != null ? Collections.unmodifiableMap(components) : Collections.emptyMap();
+    this.capabilities =
+        capabilities != null ? Collections.unmodifiableList(capabilities) : Collections.emptyList();
+    this.sdkCompatibility = sdkCompatibility;
+  }
+
+  /**
+   * Returns the overall health status.
+   *
+   * @return the status (e.g., "healthy", "degraded", "unhealthy")
+   */
+  public String getStatus() {
+    return status;
+  }
+
+  /**
+   * Returns the AxonFlow Agent version.
+   *
+   * @return the version string
+   */
+  public String getVersion() {
+    return version;
+  }
+
+  /**
+   * Returns how long the Agent has been running.
+   *
+   * @return the uptime string
+   */
+  public String getUptime() {
+    return uptime;
+  }
+
+  /**
+   * Returns the health status of individual components.
+   *
+   * @return immutable map of component statuses
+   */
+  public Map getComponents() {
+    return components;
+  }
+
+  /**
+   * Returns the list of capabilities advertised by the platform.
+   *
+   * @return immutable list of capabilities (never null)
+   */
+  public List getCapabilities() {
+    return capabilities;
+  }
+
+  /**
+   * Returns SDK compatibility information from the platform.
+   *
+   * @return the SDK compatibility info, or null if not provided
+   */
+  public SDKCompatibility getSdkCompatibility() {
+    return sdkCompatibility;
+  }
+
+  /**
+   * Checks if the Agent is healthy.
+   *
+   * @return true if status is "healthy"
+   */
+  public boolean isHealthy() {
+    return "healthy".equalsIgnoreCase(status) || "ok".equalsIgnoreCase(status);
+  }
+
+  /**
+   * Checks if the platform advertises a given capability by name.
+   *
+   * @param name the capability name to check
+   * @return true if the capability is present
+   */
+  public boolean hasCapability(String name) {
+    if (name == null) return false;
+    return capabilities.stream().anyMatch(c -> name.equals(c.getName()));
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    HealthStatus that = (HealthStatus) o;
+    return Objects.equals(status, that.status)
+        && Objects.equals(version, that.version)
+        && Objects.equals(uptime, that.uptime)
+        && Objects.equals(capabilities, that.capabilities)
+        && Objects.equals(sdkCompatibility, that.sdkCompatibility);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(status, version, uptime, capabilities, sdkCompatibility);
+  }
+
+  @Override
+  public String toString() {
+    return "HealthStatus{"
+        + "status='"
+        + status
+        + '\''
+        + ", version='"
+        + version
+        + '\''
+        + ", uptime='"
+        + uptime
+        + '\''
+        + ", capabilities="
+        + capabilities
+        + ", sdkCompatibility="
+        + sdkCompatibility
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
index 2511d0e..c82ca5f 100644
--- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputRequest.java
@@ -17,96 +17,100 @@
 
 import com.fasterxml.jackson.annotation.JsonInclude;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Map;
 import java.util.Objects;
 
 /**
  * Request to validate an MCP input against configured policies without executing it.
  *
- * 

Used with the {@code POST /api/v1/mcp/check-input} endpoint to pre-validate - * a statement before sending it to the connector.

+ *

Used with the {@code POST /api/v1/mcp/check-input} endpoint to pre-validate a statement before + * sending it to the connector. */ @JsonInclude(JsonInclude.Include.NON_NULL) public final class MCPCheckInputRequest { - @JsonProperty("connector_type") - private final String connectorType; - - @JsonProperty("statement") - private final String statement; - - @JsonProperty("parameters") - private final Map parameters; - - @JsonProperty("operation") - private final String operation; - - /** - * Creates a request with connector type and statement only. - * Operation defaults to "execute". - * - * @param connectorType the MCP connector type (e.g., "postgres") - * @param statement the statement to validate - */ - public MCPCheckInputRequest(String connectorType, String statement) { - this(connectorType, statement, null, "execute"); - } - - /** - * Creates a request with all fields. - * - * @param connectorType the MCP connector type (e.g., "postgres") - * @param statement the statement to validate - * @param parameters optional query parameters - * @param operation the operation type (e.g., "query", "execute") - */ - public MCPCheckInputRequest(String connectorType, String statement, - Map parameters, String operation) { - this.connectorType = connectorType; - this.statement = statement; - this.parameters = parameters; - this.operation = operation; - } - - public String getConnectorType() { - return connectorType; - } - - public String getStatement() { - return statement; - } - - public Map getParameters() { - return parameters; - } - - public String getOperation() { - return operation; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MCPCheckInputRequest that = (MCPCheckInputRequest) o; - return Objects.equals(connectorType, that.connectorType) && - Objects.equals(statement, that.statement) && - Objects.equals(parameters, that.parameters) && - Objects.equals(operation, that.operation); - } - - @Override - public int hashCode() { - return Objects.hash(connectorType, statement, parameters, operation); - } - - @Override - public String toString() { - return "MCPCheckInputRequest{" + - "connectorType='" + connectorType + '\'' + - ", statement='" + statement + '\'' + - ", operation='" + operation + '\'' + - '}'; - } + @JsonProperty("connector_type") + private final String connectorType; + + @JsonProperty("statement") + private final String statement; + + @JsonProperty("parameters") + private final Map parameters; + + @JsonProperty("operation") + private final String operation; + + /** + * Creates a request with connector type and statement only. Operation defaults to "execute". + * + * @param connectorType the MCP connector type (e.g., "postgres") + * @param statement the statement to validate + */ + public MCPCheckInputRequest(String connectorType, String statement) { + this(connectorType, statement, null, "execute"); + } + + /** + * Creates a request with all fields. + * + * @param connectorType the MCP connector type (e.g., "postgres") + * @param statement the statement to validate + * @param parameters optional query parameters + * @param operation the operation type (e.g., "query", "execute") + */ + public MCPCheckInputRequest( + String connectorType, String statement, Map parameters, String operation) { + this.connectorType = connectorType; + this.statement = statement; + this.parameters = parameters; + this.operation = operation; + } + + public String getConnectorType() { + return connectorType; + } + + public String getStatement() { + return statement; + } + + public Map getParameters() { + return parameters; + } + + public String getOperation() { + return operation; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MCPCheckInputRequest that = (MCPCheckInputRequest) o; + return Objects.equals(connectorType, that.connectorType) + && Objects.equals(statement, that.statement) + && Objects.equals(parameters, that.parameters) + && Objects.equals(operation, that.operation); + } + + @Override + public int hashCode() { + return Objects.hash(connectorType, statement, parameters, operation); + } + + @Override + public String toString() { + return "MCPCheckInputRequest{" + + "connectorType='" + + connectorType + + '\'' + + ", statement='" + + statement + + '\'' + + ", operation='" + + operation + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputResponse.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputResponse.java index c1454fe..ee50c5c 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckInputResponse.java @@ -18,94 +18,90 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Response from the MCP input policy check endpoint. * - *

Indicates whether the input statement is allowed by configured policies. - * A 403 HTTP response still returns a valid response body with {@code allowed=false} - * and details in {@code blockReason} and {@code policyInfo}.

+ *

Indicates whether the input statement is allowed by configured policies. A 403 HTTP response + * still returns a valid response body with {@code allowed=false} and details in {@code blockReason} + * and {@code policyInfo}. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class MCPCheckInputResponse { - @JsonProperty("allowed") - private final boolean allowed; + @JsonProperty("allowed") + private final boolean allowed; - @JsonProperty("block_reason") - private final String blockReason; + @JsonProperty("block_reason") + private final String blockReason; - @JsonProperty("policies_evaluated") - private final int policiesEvaluated; + @JsonProperty("policies_evaluated") + private final int policiesEvaluated; - @JsonProperty("policy_info") - private final ConnectorPolicyInfo policyInfo; + @JsonProperty("policy_info") + private final ConnectorPolicyInfo policyInfo; - @JsonCreator - public MCPCheckInputResponse( - @JsonProperty("allowed") boolean allowed, - @JsonProperty("block_reason") String blockReason, - @JsonProperty("policies_evaluated") int policiesEvaluated, - @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { - this.allowed = allowed; - this.blockReason = blockReason; - this.policiesEvaluated = policiesEvaluated; - this.policyInfo = policyInfo; - } + @JsonCreator + public MCPCheckInputResponse( + @JsonProperty("allowed") boolean allowed, + @JsonProperty("block_reason") String blockReason, + @JsonProperty("policies_evaluated") int policiesEvaluated, + @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { + this.allowed = allowed; + this.blockReason = blockReason; + this.policiesEvaluated = policiesEvaluated; + this.policyInfo = policyInfo; + } - /** - * Returns whether the input is allowed by policies. - */ - public boolean isAllowed() { - return allowed; - } + /** Returns whether the input is allowed by policies. */ + public boolean isAllowed() { + return allowed; + } - /** - * Returns the reason the input was blocked, or null if allowed. - */ - public String getBlockReason() { - return blockReason; - } + /** Returns the reason the input was blocked, or null if allowed. */ + public String getBlockReason() { + return blockReason; + } - /** - * Returns the number of policies evaluated. - */ - public int getPoliciesEvaluated() { - return policiesEvaluated; - } + /** Returns the number of policies evaluated. */ + public int getPoliciesEvaluated() { + return policiesEvaluated; + } - /** - * Returns detailed policy evaluation information. - */ - public ConnectorPolicyInfo getPolicyInfo() { - return policyInfo; - } + /** Returns detailed policy evaluation information. */ + public ConnectorPolicyInfo getPolicyInfo() { + return policyInfo; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MCPCheckInputResponse that = (MCPCheckInputResponse) o; - return allowed == that.allowed && - policiesEvaluated == that.policiesEvaluated && - Objects.equals(blockReason, that.blockReason) && - Objects.equals(policyInfo, that.policyInfo); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MCPCheckInputResponse that = (MCPCheckInputResponse) o; + return allowed == that.allowed + && policiesEvaluated == that.policiesEvaluated + && Objects.equals(blockReason, that.blockReason) + && Objects.equals(policyInfo, that.policyInfo); + } - @Override - public int hashCode() { - return Objects.hash(allowed, blockReason, policiesEvaluated, policyInfo); - } + @Override + public int hashCode() { + return Objects.hash(allowed, blockReason, policiesEvaluated, policyInfo); + } - @Override - public String toString() { - return "MCPCheckInputResponse{" + - "allowed=" + allowed + - ", blockReason='" + blockReason + '\'' + - ", policiesEvaluated=" + policiesEvaluated + - ", policyInfo=" + policyInfo + - '}'; - } + @Override + public String toString() { + return "MCPCheckInputResponse{" + + "allowed=" + + allowed + + ", blockReason='" + + blockReason + + '\'' + + ", policiesEvaluated=" + + policiesEvaluated + + ", policyInfo=" + + policyInfo + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java index 3b12ac0..160985e 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputRequest.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; import java.util.Map; import java.util.Objects; @@ -25,98 +24,107 @@ /** * Request to validate MCP response data against configured policies. * - *

Used with the {@code POST /api/v1/mcp/check-output} endpoint to check - * response data for PII, exfiltration limits, and other policy violations.

+ *

Used with the {@code POST /api/v1/mcp/check-output} endpoint to check response data for PII, + * exfiltration limits, and other policy violations. */ @JsonInclude(JsonInclude.Include.NON_NULL) public final class MCPCheckOutputRequest { - @JsonProperty("connector_type") - private final String connectorType; - - @JsonProperty("response_data") - private final List> responseData; - - @JsonProperty("message") - private final String message; - - @JsonProperty("metadata") - private final Map metadata; - - @JsonProperty("row_count") - private final int rowCount; - - /** - * Creates a request with connector type and response data only. - * - * @param connectorType the MCP connector type (e.g., "postgres") - * @param responseData the response data rows to validate - */ - public MCPCheckOutputRequest(String connectorType, List> responseData) { - this(connectorType, responseData, null, null, 0); - } - - /** - * Creates a request with all fields. - * - * @param connectorType the MCP connector type (e.g., "postgres") - * @param responseData the response data rows to validate - * @param message optional message context - * @param metadata optional metadata - * @param rowCount the number of rows in the response - */ - public MCPCheckOutputRequest(String connectorType, List> responseData, - String message, Map metadata, int rowCount) { - this.connectorType = connectorType; - this.responseData = responseData; - this.message = message; - this.metadata = metadata; - this.rowCount = rowCount; - } - - public String getConnectorType() { - return connectorType; - } - - public List> getResponseData() { - return responseData; - } - - public String getMessage() { - return message; - } - - public Map getMetadata() { - return metadata; - } - - public int getRowCount() { - return rowCount; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MCPCheckOutputRequest that = (MCPCheckOutputRequest) o; - return rowCount == that.rowCount && - Objects.equals(connectorType, that.connectorType) && - Objects.equals(responseData, that.responseData) && - Objects.equals(message, that.message) && - Objects.equals(metadata, that.metadata); - } - - @Override - public int hashCode() { - return Objects.hash(connectorType, responseData, message, metadata, rowCount); - } - - @Override - public String toString() { - return "MCPCheckOutputRequest{" + - "connectorType='" + connectorType + '\'' + - ", rowCount=" + rowCount + - ", message='" + message + '\'' + - '}'; - } + @JsonProperty("connector_type") + private final String connectorType; + + @JsonProperty("response_data") + private final List> responseData; + + @JsonProperty("message") + private final String message; + + @JsonProperty("metadata") + private final Map metadata; + + @JsonProperty("row_count") + private final int rowCount; + + /** + * Creates a request with connector type and response data only. + * + * @param connectorType the MCP connector type (e.g., "postgres") + * @param responseData the response data rows to validate + */ + public MCPCheckOutputRequest(String connectorType, List> responseData) { + this(connectorType, responseData, null, null, 0); + } + + /** + * Creates a request with all fields. + * + * @param connectorType the MCP connector type (e.g., "postgres") + * @param responseData the response data rows to validate + * @param message optional message context + * @param metadata optional metadata + * @param rowCount the number of rows in the response + */ + public MCPCheckOutputRequest( + String connectorType, + List> responseData, + String message, + Map metadata, + int rowCount) { + this.connectorType = connectorType; + this.responseData = responseData; + this.message = message; + this.metadata = metadata; + this.rowCount = rowCount; + } + + public String getConnectorType() { + return connectorType; + } + + public List> getResponseData() { + return responseData; + } + + public String getMessage() { + return message; + } + + public Map getMetadata() { + return metadata; + } + + public int getRowCount() { + return rowCount; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MCPCheckOutputRequest that = (MCPCheckOutputRequest) o; + return rowCount == that.rowCount + && Objects.equals(connectorType, that.connectorType) + && Objects.equals(responseData, that.responseData) + && Objects.equals(message, that.message) + && Objects.equals(metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash(connectorType, responseData, message, metadata, rowCount); + } + + @Override + public String toString() { + return "MCPCheckOutputRequest{" + + "connectorType='" + + connectorType + + '\'' + + ", rowCount=" + + rowCount + + ", message='" + + message + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputResponse.java b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputResponse.java index 1d4a5a7..1fa8a4b 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/MCPCheckOutputResponse.java @@ -18,123 +18,115 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Response from the MCP output policy check endpoint. * - *

Indicates whether the output data passes configured policies. May include - * redacted data if PII redaction policies are active, and exfiltration check - * information if data volume limits are configured.

+ *

Indicates whether the output data passes configured policies. May include redacted data if PII + * redaction policies are active, and exfiltration check information if data volume limits are + * configured. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class MCPCheckOutputResponse { - @JsonProperty("allowed") - private final boolean allowed; - - @JsonProperty("block_reason") - private final String blockReason; - - @JsonProperty("redacted_data") - private final Object redactedData; - - @JsonProperty("policies_evaluated") - private final int policiesEvaluated; - - @JsonProperty("exfiltration_info") - private final ExfiltrationCheckInfo exfiltrationInfo; - - @JsonProperty("policy_info") - private final ConnectorPolicyInfo policyInfo; - - @JsonCreator - public MCPCheckOutputResponse( - @JsonProperty("allowed") boolean allowed, - @JsonProperty("block_reason") String blockReason, - @JsonProperty("redacted_data") Object redactedData, - @JsonProperty("policies_evaluated") int policiesEvaluated, - @JsonProperty("exfiltration_info") ExfiltrationCheckInfo exfiltrationInfo, - @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { - this.allowed = allowed; - this.blockReason = blockReason; - this.redactedData = redactedData; - this.policiesEvaluated = policiesEvaluated; - this.exfiltrationInfo = exfiltrationInfo; - this.policyInfo = policyInfo; - } - - /** - * Returns whether the output data is allowed by policies. - */ - public boolean isAllowed() { - return allowed; - } - - /** - * Returns the reason the output was blocked, or null if allowed. - */ - public String getBlockReason() { - return blockReason; - } - - /** - * Returns the redacted version of the data, or null if no redaction was applied. - */ - public Object getRedactedData() { - return redactedData; - } - - /** - * Returns the number of policies evaluated. - */ - public int getPoliciesEvaluated() { - return policiesEvaluated; - } - - /** - * Returns exfiltration check information. - * May be null if exfiltration checking is disabled. - */ - public ExfiltrationCheckInfo getExfiltrationInfo() { - return exfiltrationInfo; - } - - /** - * Returns detailed policy evaluation information. - */ - public ConnectorPolicyInfo getPolicyInfo() { - return policyInfo; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MCPCheckOutputResponse that = (MCPCheckOutputResponse) o; - return allowed == that.allowed && - policiesEvaluated == that.policiesEvaluated && - Objects.equals(blockReason, that.blockReason) && - Objects.equals(redactedData, that.redactedData) && - Objects.equals(exfiltrationInfo, that.exfiltrationInfo) && - Objects.equals(policyInfo, that.policyInfo); - } - - @Override - public int hashCode() { - return Objects.hash(allowed, blockReason, redactedData, policiesEvaluated, - exfiltrationInfo, policyInfo); - } - - @Override - public String toString() { - return "MCPCheckOutputResponse{" + - "allowed=" + allowed + - ", blockReason='" + blockReason + '\'' + - ", policiesEvaluated=" + policiesEvaluated + - ", exfiltrationInfo=" + exfiltrationInfo + - ", policyInfo=" + policyInfo + - '}'; - } + @JsonProperty("allowed") + private final boolean allowed; + + @JsonProperty("block_reason") + private final String blockReason; + + @JsonProperty("redacted_data") + private final Object redactedData; + + @JsonProperty("policies_evaluated") + private final int policiesEvaluated; + + @JsonProperty("exfiltration_info") + private final ExfiltrationCheckInfo exfiltrationInfo; + + @JsonProperty("policy_info") + private final ConnectorPolicyInfo policyInfo; + + @JsonCreator + public MCPCheckOutputResponse( + @JsonProperty("allowed") boolean allowed, + @JsonProperty("block_reason") String blockReason, + @JsonProperty("redacted_data") Object redactedData, + @JsonProperty("policies_evaluated") int policiesEvaluated, + @JsonProperty("exfiltration_info") ExfiltrationCheckInfo exfiltrationInfo, + @JsonProperty("policy_info") ConnectorPolicyInfo policyInfo) { + this.allowed = allowed; + this.blockReason = blockReason; + this.redactedData = redactedData; + this.policiesEvaluated = policiesEvaluated; + this.exfiltrationInfo = exfiltrationInfo; + this.policyInfo = policyInfo; + } + + /** Returns whether the output data is allowed by policies. */ + public boolean isAllowed() { + return allowed; + } + + /** Returns the reason the output was blocked, or null if allowed. */ + public String getBlockReason() { + return blockReason; + } + + /** Returns the redacted version of the data, or null if no redaction was applied. */ + public Object getRedactedData() { + return redactedData; + } + + /** Returns the number of policies evaluated. */ + public int getPoliciesEvaluated() { + return policiesEvaluated; + } + + /** Returns exfiltration check information. May be null if exfiltration checking is disabled. */ + public ExfiltrationCheckInfo getExfiltrationInfo() { + return exfiltrationInfo; + } + + /** Returns detailed policy evaluation information. */ + public ConnectorPolicyInfo getPolicyInfo() { + return policyInfo; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MCPCheckOutputResponse that = (MCPCheckOutputResponse) o; + return allowed == that.allowed + && policiesEvaluated == that.policiesEvaluated + && Objects.equals(blockReason, that.blockReason) + && Objects.equals(redactedData, that.redactedData) + && Objects.equals(exfiltrationInfo, that.exfiltrationInfo) + && Objects.equals(policyInfo, that.policyInfo); + } + + @Override + public int hashCode() { + return Objects.hash( + allowed, blockReason, redactedData, policiesEvaluated, exfiltrationInfo, policyInfo); + } + + @Override + public String toString() { + return "MCPCheckOutputResponse{" + + "allowed=" + + allowed + + ", blockReason='" + + blockReason + + '\'' + + ", policiesEvaluated=" + + policiesEvaluated + + ", exfiltrationInfo=" + + exfiltrationInfo + + ", policyInfo=" + + policyInfo + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResponse.java b/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResponse.java index 651337e..b0af9eb 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResponse.java @@ -17,58 +17,68 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; -/** - * Aggregated media analysis results in the response. - */ +/** Aggregated media analysis results in the response. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class MediaAnalysisResponse { - @JsonProperty("results") - private final List results; + @JsonProperty("results") + private final List results; + + @JsonProperty("total_cost_usd") + private final double totalCostUsd; + + @JsonProperty("analysis_time_ms") + private final long analysisTimeMs; - @JsonProperty("total_cost_usd") - private final double totalCostUsd; + public MediaAnalysisResponse( + @JsonProperty("results") List results, + @JsonProperty("total_cost_usd") double totalCostUsd, + @JsonProperty("analysis_time_ms") long analysisTimeMs) { + this.results = + results != null ? Collections.unmodifiableList(results) : Collections.emptyList(); + this.totalCostUsd = totalCostUsd; + this.analysisTimeMs = analysisTimeMs; + } - @JsonProperty("analysis_time_ms") - private final long analysisTimeMs; + public List getResults() { + return results; + } - public MediaAnalysisResponse( - @JsonProperty("results") List results, - @JsonProperty("total_cost_usd") double totalCostUsd, - @JsonProperty("analysis_time_ms") long analysisTimeMs) { - this.results = results != null ? Collections.unmodifiableList(results) : Collections.emptyList(); - this.totalCostUsd = totalCostUsd; - this.analysisTimeMs = analysisTimeMs; - } + public double getTotalCostUsd() { + return totalCostUsd; + } - public List getResults() { return results; } - public double getTotalCostUsd() { return totalCostUsd; } - public long getAnalysisTimeMs() { return analysisTimeMs; } + public long getAnalysisTimeMs() { + return analysisTimeMs; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MediaAnalysisResponse that = (MediaAnalysisResponse) o; - return Double.compare(totalCostUsd, that.totalCostUsd) == 0 && - analysisTimeMs == that.analysisTimeMs && - Objects.equals(results, that.results); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaAnalysisResponse that = (MediaAnalysisResponse) o; + return Double.compare(totalCostUsd, that.totalCostUsd) == 0 + && analysisTimeMs == that.analysisTimeMs + && Objects.equals(results, that.results); + } - @Override - public int hashCode() { - return Objects.hash(results, totalCostUsd, analysisTimeMs); - } + @Override + public int hashCode() { + return Objects.hash(results, totalCostUsd, analysisTimeMs); + } - @Override - public String toString() { - return "MediaAnalysisResponse{results=" + (results != null ? results.size() : 0) + - ", totalCostUsd=" + totalCostUsd + - ", analysisTimeMs=" + analysisTimeMs + '}'; - } + @Override + public String toString() { + return "MediaAnalysisResponse{results=" + + (results != null ? results.size() : 0) + + ", totalCostUsd=" + + totalCostUsd + + ", analysisTimeMs=" + + analysisTimeMs + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResult.java b/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResult.java index a086e51..5b5f5af 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResult.java +++ b/src/main/java/com/getaxonflow/sdk/types/MediaAnalysisResult.java @@ -17,155 +17,221 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; -/** - * Analysis results for a single media item. - */ +/** Analysis results for a single media item. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class MediaAnalysisResult { - @JsonProperty("media_index") - private final int mediaIndex; - - @JsonProperty("sha256_hash") - private final String sha256Hash; - - @JsonProperty("has_faces") - private final boolean hasFaces; - - @JsonProperty("face_count") - private final int faceCount; - - @JsonProperty("has_biometric_data") - private final boolean hasBiometricData; - - @JsonProperty("nsfw_score") - private final double nsfwScore; - - @JsonProperty("violence_score") - private final double violenceScore; - - @JsonProperty("content_safe") - private final boolean contentSafe; - - @JsonProperty("document_type") - private final String documentType; - - @JsonProperty("is_sensitive_document") - private final boolean isSensitiveDocument; - - @JsonProperty("has_pii") - private final boolean hasPII; - - @JsonProperty("pii_types") - private final List piiTypes; - - @JsonProperty("has_extracted_text") - private final boolean hasExtractedText; - - @JsonProperty("extracted_text_length") - private final int extractedTextLength; - - @JsonProperty("estimated_cost_usd") - private final double estimatedCostUsd; - - @JsonProperty("warnings") - private final List warnings; - - public MediaAnalysisResult( - @JsonProperty("media_index") int mediaIndex, - @JsonProperty("sha256_hash") String sha256Hash, - @JsonProperty("has_faces") boolean hasFaces, - @JsonProperty("face_count") int faceCount, - @JsonProperty("has_biometric_data") boolean hasBiometricData, - @JsonProperty("nsfw_score") double nsfwScore, - @JsonProperty("violence_score") double violenceScore, - @JsonProperty("content_safe") boolean contentSafe, - @JsonProperty("document_type") String documentType, - @JsonProperty("is_sensitive_document") boolean isSensitiveDocument, - @JsonProperty("has_pii") boolean hasPII, - @JsonProperty("pii_types") List piiTypes, - @JsonProperty("has_extracted_text") boolean hasExtractedText, - @JsonProperty("extracted_text_length") int extractedTextLength, - @JsonProperty("estimated_cost_usd") double estimatedCostUsd, - @JsonProperty("warnings") List warnings) { - this.mediaIndex = mediaIndex; - this.sha256Hash = sha256Hash; - this.hasFaces = hasFaces; - this.faceCount = faceCount; - this.hasBiometricData = hasBiometricData; - this.nsfwScore = nsfwScore; - this.violenceScore = violenceScore; - this.contentSafe = contentSafe; - this.documentType = documentType; - this.isSensitiveDocument = isSensitiveDocument; - this.hasPII = hasPII; - this.piiTypes = piiTypes != null ? Collections.unmodifiableList(piiTypes) : Collections.emptyList(); - this.hasExtractedText = hasExtractedText; - this.extractedTextLength = extractedTextLength; - this.estimatedCostUsd = estimatedCostUsd; - this.warnings = warnings != null ? Collections.unmodifiableList(warnings) : Collections.emptyList(); - } - - public int getMediaIndex() { return mediaIndex; } - public String getSha256Hash() { return sha256Hash; } - public boolean isHasFaces() { return hasFaces; } - public int getFaceCount() { return faceCount; } - public boolean isHasBiometricData() { return hasBiometricData; } - public double getNsfwScore() { return nsfwScore; } - public double getViolenceScore() { return violenceScore; } - public boolean isContentSafe() { return contentSafe; } - public String getDocumentType() { return documentType; } - public boolean isSensitiveDocument() { return isSensitiveDocument; } - public boolean isHasPII() { return hasPII; } - public List getPiiTypes() { return piiTypes; } - public boolean isHasExtractedText() { return hasExtractedText; } - public int getExtractedTextLength() { return extractedTextLength; } - public double getEstimatedCostUsd() { return estimatedCostUsd; } - public List getWarnings() { return warnings; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MediaAnalysisResult that = (MediaAnalysisResult) o; - return mediaIndex == that.mediaIndex && - hasFaces == that.hasFaces && - faceCount == that.faceCount && - hasBiometricData == that.hasBiometricData && - Double.compare(nsfwScore, that.nsfwScore) == 0 && - Double.compare(violenceScore, that.violenceScore) == 0 && - contentSafe == that.contentSafe && - isSensitiveDocument == that.isSensitiveDocument && - hasPII == that.hasPII && - hasExtractedText == that.hasExtractedText && - extractedTextLength == that.extractedTextLength && - Double.compare(estimatedCostUsd, that.estimatedCostUsd) == 0 && - Objects.equals(sha256Hash, that.sha256Hash) && - Objects.equals(documentType, that.documentType) && - Objects.equals(piiTypes, that.piiTypes) && - Objects.equals(warnings, that.warnings); - } - - @Override - public int hashCode() { - return Objects.hash(mediaIndex, sha256Hash, hasFaces, faceCount, - hasBiometricData, nsfwScore, violenceScore, contentSafe, - documentType, isSensitiveDocument, hasPII, piiTypes, - hasExtractedText, extractedTextLength, estimatedCostUsd, warnings); - } - - @Override - public String toString() { - return "MediaAnalysisResult{mediaIndex=" + mediaIndex + - ", contentSafe=" + contentSafe + - ", hasPII=" + hasPII + - ", hasFaces=" + hasFaces + - ", hasExtractedText=" + hasExtractedText + - ", extractedTextLength=" + extractedTextLength + '}'; - } + @JsonProperty("media_index") + private final int mediaIndex; + + @JsonProperty("sha256_hash") + private final String sha256Hash; + + @JsonProperty("has_faces") + private final boolean hasFaces; + + @JsonProperty("face_count") + private final int faceCount; + + @JsonProperty("has_biometric_data") + private final boolean hasBiometricData; + + @JsonProperty("nsfw_score") + private final double nsfwScore; + + @JsonProperty("violence_score") + private final double violenceScore; + + @JsonProperty("content_safe") + private final boolean contentSafe; + + @JsonProperty("document_type") + private final String documentType; + + @JsonProperty("is_sensitive_document") + private final boolean isSensitiveDocument; + + @JsonProperty("has_pii") + private final boolean hasPII; + + @JsonProperty("pii_types") + private final List piiTypes; + + @JsonProperty("has_extracted_text") + private final boolean hasExtractedText; + + @JsonProperty("extracted_text_length") + private final int extractedTextLength; + + @JsonProperty("estimated_cost_usd") + private final double estimatedCostUsd; + + @JsonProperty("warnings") + private final List warnings; + + public MediaAnalysisResult( + @JsonProperty("media_index") int mediaIndex, + @JsonProperty("sha256_hash") String sha256Hash, + @JsonProperty("has_faces") boolean hasFaces, + @JsonProperty("face_count") int faceCount, + @JsonProperty("has_biometric_data") boolean hasBiometricData, + @JsonProperty("nsfw_score") double nsfwScore, + @JsonProperty("violence_score") double violenceScore, + @JsonProperty("content_safe") boolean contentSafe, + @JsonProperty("document_type") String documentType, + @JsonProperty("is_sensitive_document") boolean isSensitiveDocument, + @JsonProperty("has_pii") boolean hasPII, + @JsonProperty("pii_types") List piiTypes, + @JsonProperty("has_extracted_text") boolean hasExtractedText, + @JsonProperty("extracted_text_length") int extractedTextLength, + @JsonProperty("estimated_cost_usd") double estimatedCostUsd, + @JsonProperty("warnings") List warnings) { + this.mediaIndex = mediaIndex; + this.sha256Hash = sha256Hash; + this.hasFaces = hasFaces; + this.faceCount = faceCount; + this.hasBiometricData = hasBiometricData; + this.nsfwScore = nsfwScore; + this.violenceScore = violenceScore; + this.contentSafe = contentSafe; + this.documentType = documentType; + this.isSensitiveDocument = isSensitiveDocument; + this.hasPII = hasPII; + this.piiTypes = + piiTypes != null ? Collections.unmodifiableList(piiTypes) : Collections.emptyList(); + this.hasExtractedText = hasExtractedText; + this.extractedTextLength = extractedTextLength; + this.estimatedCostUsd = estimatedCostUsd; + this.warnings = + warnings != null ? Collections.unmodifiableList(warnings) : Collections.emptyList(); + } + + public int getMediaIndex() { + return mediaIndex; + } + + public String getSha256Hash() { + return sha256Hash; + } + + public boolean isHasFaces() { + return hasFaces; + } + + public int getFaceCount() { + return faceCount; + } + + public boolean isHasBiometricData() { + return hasBiometricData; + } + + public double getNsfwScore() { + return nsfwScore; + } + + public double getViolenceScore() { + return violenceScore; + } + + public boolean isContentSafe() { + return contentSafe; + } + + public String getDocumentType() { + return documentType; + } + + public boolean isSensitiveDocument() { + return isSensitiveDocument; + } + + public boolean isHasPII() { + return hasPII; + } + + public List getPiiTypes() { + return piiTypes; + } + + public boolean isHasExtractedText() { + return hasExtractedText; + } + + public int getExtractedTextLength() { + return extractedTextLength; + } + + public double getEstimatedCostUsd() { + return estimatedCostUsd; + } + + public List getWarnings() { + return warnings; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaAnalysisResult that = (MediaAnalysisResult) o; + return mediaIndex == that.mediaIndex + && hasFaces == that.hasFaces + && faceCount == that.faceCount + && hasBiometricData == that.hasBiometricData + && Double.compare(nsfwScore, that.nsfwScore) == 0 + && Double.compare(violenceScore, that.violenceScore) == 0 + && contentSafe == that.contentSafe + && isSensitiveDocument == that.isSensitiveDocument + && hasPII == that.hasPII + && hasExtractedText == that.hasExtractedText + && extractedTextLength == that.extractedTextLength + && Double.compare(estimatedCostUsd, that.estimatedCostUsd) == 0 + && Objects.equals(sha256Hash, that.sha256Hash) + && Objects.equals(documentType, that.documentType) + && Objects.equals(piiTypes, that.piiTypes) + && Objects.equals(warnings, that.warnings); + } + + @Override + public int hashCode() { + return Objects.hash( + mediaIndex, + sha256Hash, + hasFaces, + faceCount, + hasBiometricData, + nsfwScore, + violenceScore, + contentSafe, + documentType, + isSensitiveDocument, + hasPII, + piiTypes, + hasExtractedText, + extractedTextLength, + estimatedCostUsd, + warnings); + } + + @Override + public String toString() { + return "MediaAnalysisResult{mediaIndex=" + + mediaIndex + + ", contentSafe=" + + contentSafe + + ", hasPII=" + + hasPII + + ", hasFaces=" + + hasFaces + + ", hasExtractedText=" + + hasExtractedText + + ", extractedTextLength=" + + extractedTextLength + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaContent.java b/src/main/java/com/getaxonflow/sdk/types/MediaContent.java index 9c2aacc..96863ac 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MediaContent.java +++ b/src/main/java/com/getaxonflow/sdk/types/MediaContent.java @@ -18,16 +18,16 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Media content (image) to include with a request for governance analysis. * - *

Supported formats: JPEG, PNG, GIF, WebP. Images can be provided as - * base64-encoded data or referenced by URL. + *

Supported formats: JPEG, PNG, GIF, WebP. Images can be provided as base64-encoded data or + * referenced by URL. * *

Example usage: + * *

{@code
  * MediaContent image = MediaContent.builder()
  *     .source("base64")
@@ -40,78 +40,108 @@
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class MediaContent {
 
-    @JsonProperty("source")
-    private final String source;
-
-    @JsonProperty("base64_data")
-    private final String base64Data;
-
-    @JsonProperty("url")
-    private final String url;
-
-    @JsonProperty("mime_type")
-    private final String mimeType;
-
-    private MediaContent(Builder builder) {
-        this.source = Objects.requireNonNull(builder.source, "source cannot be null");
-        this.base64Data = builder.base64Data;
-        this.url = builder.url;
-        this.mimeType = Objects.requireNonNull(builder.mimeType, "mimeType cannot be null");
+  @JsonProperty("source")
+  private final String source;
+
+  @JsonProperty("base64_data")
+  private final String base64Data;
+
+  @JsonProperty("url")
+  private final String url;
+
+  @JsonProperty("mime_type")
+  private final String mimeType;
+
+  private MediaContent(Builder builder) {
+    this.source = Objects.requireNonNull(builder.source, "source cannot be null");
+    this.base64Data = builder.base64Data;
+    this.url = builder.url;
+    this.mimeType = Objects.requireNonNull(builder.mimeType, "mimeType cannot be null");
+  }
+
+  // Jackson deserialization constructor
+  public MediaContent(
+      @JsonProperty("source") String source,
+      @JsonProperty("base64_data") String base64Data,
+      @JsonProperty("url") String url,
+      @JsonProperty("mime_type") String mimeType) {
+    this.source = source;
+    this.base64Data = base64Data;
+    this.url = url;
+    this.mimeType = mimeType;
+  }
+
+  public String getSource() {
+    return source;
+  }
+
+  public String getBase64Data() {
+    return base64Data;
+  }
+
+  public String getUrl() {
+    return url;
+  }
+
+  public String getMimeType() {
+    return mimeType;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    MediaContent that = (MediaContent) o;
+    return Objects.equals(source, that.source)
+        && Objects.equals(base64Data, that.base64Data)
+        && Objects.equals(url, that.url)
+        && Objects.equals(mimeType, that.mimeType);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(source, base64Data, url, mimeType);
+  }
+
+  @Override
+  public String toString() {
+    return "MediaContent{source='" + source + "', mimeType='" + mimeType + "'}";
+  }
+
+  public static final class Builder {
+    private String source;
+    private String base64Data;
+    private String url;
+    private String mimeType;
+
+    private Builder() {}
+
+    public Builder source(String source) {
+      this.source = source;
+      return this;
     }
 
-    // Jackson deserialization constructor
-    public MediaContent(
-            @JsonProperty("source") String source,
-            @JsonProperty("base64_data") String base64Data,
-            @JsonProperty("url") String url,
-            @JsonProperty("mime_type") String mimeType) {
-        this.source = source;
-        this.base64Data = base64Data;
-        this.url = url;
-        this.mimeType = mimeType;
+    public Builder base64Data(String base64Data) {
+      this.base64Data = base64Data;
+      return this;
     }
 
-    public String getSource() { return source; }
-    public String getBase64Data() { return base64Data; }
-    public String getUrl() { return url; }
-    public String getMimeType() { return mimeType; }
-
-    public static Builder builder() { return new Builder(); }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        MediaContent that = (MediaContent) o;
-        return Objects.equals(source, that.source) &&
-               Objects.equals(base64Data, that.base64Data) &&
-               Objects.equals(url, that.url) &&
-               Objects.equals(mimeType, that.mimeType);
+    public Builder url(String url) {
+      this.url = url;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(source, base64Data, url, mimeType);
+    public Builder mimeType(String mimeType) {
+      this.mimeType = mimeType;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "MediaContent{source='" + source + "', mimeType='" + mimeType + "'}";
-    }
-
-    public static final class Builder {
-        private String source;
-        private String base64Data;
-        private String url;
-        private String mimeType;
-
-        private Builder() {}
-
-        public Builder source(String source) { this.source = source; return this; }
-        public Builder base64Data(String base64Data) { this.base64Data = base64Data; return this; }
-        public Builder url(String url) { this.url = url; return this; }
-        public Builder mimeType(String mimeType) { this.mimeType = mimeType; return this; }
-
-        public MediaContent build() { return new MediaContent(this); }
+    public MediaContent build() {
+      return new MediaContent(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java
index 1d32ce4..2a61f72 100644
--- a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java
+++ b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceConfig.java
@@ -17,76 +17,108 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.List;
 import java.util.Objects;
 
 /**
  * Per-tenant media governance configuration.
  *
- * 

Controls whether media analysis is enabled for a tenant and which - * analyzers are allowed. Returned by the media governance config API. + *

Controls whether media analysis is enabled for a tenant and which analyzers are allowed. + * Returned by the media governance config API. */ @JsonIgnoreProperties(ignoreUnknown = true) public class MediaGovernanceConfig { - @JsonProperty("tenant_id") - private String tenantId; - - @JsonProperty("enabled") - private boolean enabled; - - @JsonProperty("allowed_analyzers") - private List allowedAnalyzers; - - @JsonProperty("updated_at") - private String updatedAt; - - @JsonProperty("updated_by") - private String updatedBy; - - public MediaGovernanceConfig() {} - - public String getTenantId() { return tenantId; } - public void setTenantId(String tenantId) { this.tenantId = tenantId; } - - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - - public List getAllowedAnalyzers() { return allowedAnalyzers; } - public void setAllowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; } - - public String getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } - - public String getUpdatedBy() { return updatedBy; } - public void setUpdatedBy(String updatedBy) { this.updatedBy = updatedBy; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MediaGovernanceConfig that = (MediaGovernanceConfig) o; - return enabled == that.enabled && - Objects.equals(tenantId, that.tenantId) && - Objects.equals(allowedAnalyzers, that.allowedAnalyzers) && - Objects.equals(updatedAt, that.updatedAt) && - Objects.equals(updatedBy, that.updatedBy); - } - - @Override - public int hashCode() { - return Objects.hash(tenantId, enabled, allowedAnalyzers, updatedAt, updatedBy); - } - - @Override - public String toString() { - return "MediaGovernanceConfig{" + - "tenantId='" + tenantId + '\'' + - ", enabled=" + enabled + - ", allowedAnalyzers=" + allowedAnalyzers + - ", updatedAt='" + updatedAt + '\'' + - ", updatedBy='" + updatedBy + '\'' + - '}'; - } + @JsonProperty("tenant_id") + private String tenantId; + + @JsonProperty("enabled") + private boolean enabled; + + @JsonProperty("allowed_analyzers") + private List allowedAnalyzers; + + @JsonProperty("updated_at") + private String updatedAt; + + @JsonProperty("updated_by") + private String updatedBy; + + public MediaGovernanceConfig() {} + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public List getAllowedAnalyzers() { + return allowedAnalyzers; + } + + public void setAllowedAnalyzers(List allowedAnalyzers) { + this.allowedAnalyzers = allowedAnalyzers; + } + + public String getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + + public String getUpdatedBy() { + return updatedBy; + } + + public void setUpdatedBy(String updatedBy) { + this.updatedBy = updatedBy; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaGovernanceConfig that = (MediaGovernanceConfig) o; + return enabled == that.enabled + && Objects.equals(tenantId, that.tenantId) + && Objects.equals(allowedAnalyzers, that.allowedAnalyzers) + && Objects.equals(updatedAt, that.updatedAt) + && Objects.equals(updatedBy, that.updatedBy); + } + + @Override + public int hashCode() { + return Objects.hash(tenantId, enabled, allowedAnalyzers, updatedAt, updatedBy); + } + + @Override + public String toString() { + return "MediaGovernanceConfig{" + + "tenantId='" + + tenantId + + '\'' + + ", enabled=" + + enabled + + ", allowedAnalyzers=" + + allowedAnalyzers + + ", updatedAt='" + + updatedAt + + '\'' + + ", updatedBy='" + + updatedBy + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java index de227d9..3bdd646 100644 --- a/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java +++ b/src/main/java/com/getaxonflow/sdk/types/MediaGovernanceStatus.java @@ -17,67 +17,91 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Platform-level media governance status. * - *

Indicates whether media governance is available, the default enablement - * state, and the license tier required. + *

Indicates whether media governance is available, the default enablement state, and the license + * tier required. */ @JsonIgnoreProperties(ignoreUnknown = true) public class MediaGovernanceStatus { - @JsonProperty("available") - private boolean available; - - @JsonProperty("enabled_by_default") - private boolean enabledByDefault; - - @JsonProperty("per_tenant_control") - private boolean perTenantControl; - - @JsonProperty("tier") - private String tier; - - public MediaGovernanceStatus() {} - - public boolean isAvailable() { return available; } - public void setAvailable(boolean available) { this.available = available; } - - public boolean isEnabledByDefault() { return enabledByDefault; } - public void setEnabledByDefault(boolean enabledByDefault) { this.enabledByDefault = enabledByDefault; } - - public boolean isPerTenantControl() { return perTenantControl; } - public void setPerTenantControl(boolean perTenantControl) { this.perTenantControl = perTenantControl; } - - public String getTier() { return tier; } - public void setTier(String tier) { this.tier = tier; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - MediaGovernanceStatus that = (MediaGovernanceStatus) o; - return available == that.available && - enabledByDefault == that.enabledByDefault && - perTenantControl == that.perTenantControl && - Objects.equals(tier, that.tier); - } - - @Override - public int hashCode() { - return Objects.hash(available, enabledByDefault, perTenantControl, tier); - } - - @Override - public String toString() { - return "MediaGovernanceStatus{" + - "available=" + available + - ", enabledByDefault=" + enabledByDefault + - ", perTenantControl=" + perTenantControl + - ", tier='" + tier + '\'' + - '}'; - } + @JsonProperty("available") + private boolean available; + + @JsonProperty("enabled_by_default") + private boolean enabledByDefault; + + @JsonProperty("per_tenant_control") + private boolean perTenantControl; + + @JsonProperty("tier") + private String tier; + + public MediaGovernanceStatus() {} + + public boolean isAvailable() { + return available; + } + + public void setAvailable(boolean available) { + this.available = available; + } + + public boolean isEnabledByDefault() { + return enabledByDefault; + } + + public void setEnabledByDefault(boolean enabledByDefault) { + this.enabledByDefault = enabledByDefault; + } + + public boolean isPerTenantControl() { + return perTenantControl; + } + + public void setPerTenantControl(boolean perTenantControl) { + this.perTenantControl = perTenantControl; + } + + public String getTier() { + return tier; + } + + public void setTier(String tier) { + this.tier = tier; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MediaGovernanceStatus that = (MediaGovernanceStatus) o; + return available == that.available + && enabledByDefault == that.enabledByDefault + && perTenantControl == that.perTenantControl + && Objects.equals(tier, that.tier); + } + + @Override + public int hashCode() { + return Objects.hash(available, enabledByDefault, perTenantControl, tier); + } + + @Override + public String toString() { + return "MediaGovernanceStatus{" + + "available=" + + available + + ", enabledByDefault=" + + enabledByDefault + + ", perTenantControl=" + + perTenantControl + + ", tier='" + + tier + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/Mode.java b/src/main/java/com/getaxonflow/sdk/types/Mode.java index 9cfb96b..704f423 100644 --- a/src/main/java/com/getaxonflow/sdk/types/Mode.java +++ b/src/main/java/com/getaxonflow/sdk/types/Mode.java @@ -19,53 +19,49 @@ * Operating mode for the AxonFlow client. * *

The mode determines the behavior of certain operations: + * *

    - *
  • {@link #PRODUCTION} - Standard production mode with full governance
  • - *
  • {@link #SANDBOX} - Testing mode with relaxed policies for development
  • + *
  • {@link #PRODUCTION} - Standard production mode with full governance + *
  • {@link #SANDBOX} - Testing mode with relaxed policies for development *
*/ public enum Mode { - /** - * Production mode with full policy enforcement. - */ - PRODUCTION("production"), + /** Production mode with full policy enforcement. */ + PRODUCTION("production"), - /** - * Sandbox mode for testing and development. - * Policies may be relaxed or simulated. - */ - SANDBOX("sandbox"); + /** Sandbox mode for testing and development. Policies may be relaxed or simulated. */ + SANDBOX("sandbox"); - private final String value; + private final String value; - Mode(String value) { - this.value = value; - } + Mode(String value) { + this.value = value; + } - /** - * Returns the string value used in API requests. - * - * @return the mode value as a string - */ - public String getValue() { - return value; - } + /** + * Returns the string value used in API requests. + * + * @return the mode value as a string + */ + public String getValue() { + return value; + } - /** - * Parses a string value to a Mode enum. - * - * @param value the string value to parse - * @return the corresponding Mode, or PRODUCTION if not recognized - */ - public static Mode fromValue(String value) { - if (value == null) { - return PRODUCTION; - } - for (Mode mode : values()) { - if (mode.value.equalsIgnoreCase(value)) { - return mode; - } - } - return PRODUCTION; + /** + * Parses a string value to a Mode enum. + * + * @param value the string value to parse + * @return the corresponding Mode, or PRODUCTION if not recognized + */ + public static Mode fromValue(String value) { + if (value == null) { + return PRODUCTION; + } + for (Mode mode : values()) { + if (mode.value.equalsIgnoreCase(value)) { + return mode; + } } + return PRODUCTION; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlanRequest.java b/src/main/java/com/getaxonflow/sdk/types/PlanRequest.java index 142047e..0721d7e 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlanRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlanRequest.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.HashMap; import java.util.Map; @@ -26,11 +25,11 @@ /** * Request for generating a multi-agent plan (MAP). * - *

Multi-Agent Planning allows you to describe a complex task and have - * AxonFlow generate an execution plan with multiple steps that can be - * executed by different agents. + *

Multi-Agent Planning allows you to describe a complex task and have AxonFlow generate an + * execution plan with multiple steps that can be executed by different agents. * *

Example usage: + * *

{@code
  * PlanRequest request = PlanRequest.builder()
  *     .objective("Research and summarize the latest AI governance regulations")
@@ -44,225 +43,234 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class PlanRequest {
 
-    @JsonProperty("objective")
-    private final String objective;
+  @JsonProperty("objective")
+  private final String objective;
 
-    @JsonProperty("domain")
-    private final String domain;
+  @JsonProperty("domain")
+  private final String domain;
 
-    @JsonProperty("user_token")
-    private final String userToken;
+  @JsonProperty("user_token")
+  private final String userToken;
 
-    @JsonProperty("context")
-    private final Map context;
+  @JsonProperty("context")
+  private final Map context;
 
-    @JsonProperty("constraints")
-    private final Map constraints;
+  @JsonProperty("constraints")
+  private final Map constraints;
 
-    @JsonProperty("max_steps")
-    private final Integer maxSteps;
+  @JsonProperty("max_steps")
+  private final Integer maxSteps;
 
-    @JsonProperty("parallel")
-    private final Boolean parallel;
+  @JsonProperty("parallel")
+  private final Boolean parallel;
 
-    private PlanRequest(Builder builder) {
-        this.objective = Objects.requireNonNull(builder.objective, "objective cannot be null");
-        this.domain = builder.domain != null ? builder.domain : "generic";
-        this.userToken = builder.userToken;
-        this.context = builder.context != null
+  private PlanRequest(Builder builder) {
+    this.objective = Objects.requireNonNull(builder.objective, "objective cannot be null");
+    this.domain = builder.domain != null ? builder.domain : "generic";
+    this.userToken = builder.userToken;
+    this.context =
+        builder.context != null
             ? Collections.unmodifiableMap(new HashMap<>(builder.context))
             : null;
-        this.constraints = builder.constraints != null
+    this.constraints =
+        builder.constraints != null
             ? Collections.unmodifiableMap(new HashMap<>(builder.constraints))
             : null;
-        this.maxSteps = builder.maxSteps;
-        this.parallel = builder.parallel;
-    }
-
-    public String getObjective() {
-        return objective;
-    }
-
-    public String getDomain() {
-        return domain;
-    }
+    this.maxSteps = builder.maxSteps;
+    this.parallel = builder.parallel;
+  }
+
+  public String getObjective() {
+    return objective;
+  }
+
+  public String getDomain() {
+    return domain;
+  }
+
+  public String getUserToken() {
+    return userToken;
+  }
+
+  public Map getContext() {
+    return context;
+  }
+
+  public Map getConstraints() {
+    return constraints;
+  }
+
+  public Integer getMaxSteps() {
+    return maxSteps;
+  }
+
+  public Boolean getParallel() {
+    return parallel;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    PlanRequest that = (PlanRequest) o;
+    return Objects.equals(objective, that.objective)
+        && Objects.equals(domain, that.domain)
+        && Objects.equals(userToken, that.userToken)
+        && Objects.equals(context, that.context)
+        && Objects.equals(constraints, that.constraints)
+        && Objects.equals(maxSteps, that.maxSteps)
+        && Objects.equals(parallel, that.parallel);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(objective, domain, userToken, context, constraints, maxSteps, parallel);
+  }
+
+  @Override
+  public String toString() {
+    return "PlanRequest{"
+        + "objective='"
+        + objective
+        + '\''
+        + ", domain='"
+        + domain
+        + '\''
+        + ", userToken='"
+        + userToken
+        + '\''
+        + ", maxSteps="
+        + maxSteps
+        + ", parallel="
+        + parallel
+        + '}';
+  }
+
+  /** Builder for PlanRequest. */
+  public static final class Builder {
+    private String objective;
+    private String domain = "generic";
+    private String userToken;
+    private Map context;
+    private Map constraints;
+    private Integer maxSteps;
+    private Boolean parallel;
+
+    private Builder() {}
 
-    public String getUserToken() {
-        return userToken;
-    }
-
-    public Map getContext() {
-        return context;
+    /**
+     * Sets the objective or task description for the plan.
+     *
+     * @param objective a description of what the plan should accomplish
+     * @return this builder
+     */
+    public Builder objective(String objective) {
+      this.objective = objective;
+      return this;
     }
 
-    public Map getConstraints() {
-        return constraints;
+    /**
+     * Sets the domain for specialized planning.
+     *
+     * 

Common domains include: + * + *

    + *
  • generic - General purpose planning + *
  • travel - Travel and booking workflows + *
  • healthcare - Healthcare data processing + *
  • finance - Financial analysis workflows + *
+ * + * @param domain the domain identifier + * @return this builder + */ + public Builder domain(String domain) { + this.domain = domain; + return this; } - public Integer getMaxSteps() { - return maxSteps; + /** + * Sets the user token for identifying the requesting user. + * + * @param userToken the user identifier + * @return this builder + */ + public Builder userToken(String userToken) { + this.userToken = userToken; + return this; } - public Boolean getParallel() { - return parallel; + /** + * Sets additional context for plan generation. + * + * @param context key-value pairs of contextual information + * @return this builder + */ + public Builder context(Map context) { + this.context = context; + return this; } - public static Builder builder() { - return new Builder(); + /** + * Adds a single context entry. + * + * @param key the context key + * @param value the context value + * @return this builder + */ + public Builder addContext(String key, Object value) { + if (this.context == null) { + this.context = new HashMap<>(); + } + this.context.put(key, value); + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanRequest that = (PlanRequest) o; - return Objects.equals(objective, that.objective) && - Objects.equals(domain, that.domain) && - Objects.equals(userToken, that.userToken) && - Objects.equals(context, that.context) && - Objects.equals(constraints, that.constraints) && - Objects.equals(maxSteps, that.maxSteps) && - Objects.equals(parallel, that.parallel); + /** + * Sets constraints for plan generation. + * + * @param constraints key-value pairs of constraints + * @return this builder + */ + public Builder constraints(Map constraints) { + this.constraints = constraints; + return this; } - @Override - public int hashCode() { - return Objects.hash(objective, domain, userToken, context, constraints, maxSteps, parallel); + /** + * Sets the maximum number of steps in the plan. + * + * @param maxSteps the maximum step count + * @return this builder + */ + public Builder maxSteps(int maxSteps) { + this.maxSteps = maxSteps; + return this; } - @Override - public String toString() { - return "PlanRequest{" + - "objective='" + objective + '\'' + - ", domain='" + domain + '\'' + - ", userToken='" + userToken + '\'' + - ", maxSteps=" + maxSteps + - ", parallel=" + parallel + - '}'; + /** + * Sets whether parallel execution is allowed. + * + * @param parallel true to allow parallel step execution + * @return this builder + */ + public Builder parallel(boolean parallel) { + this.parallel = parallel; + return this; } /** - * Builder for PlanRequest. + * Builds the PlanRequest. + * + * @return a new PlanRequest instance + * @throws NullPointerException if objective is null */ - public static final class Builder { - private String objective; - private String domain = "generic"; - private String userToken; - private Map context; - private Map constraints; - private Integer maxSteps; - private Boolean parallel; - - private Builder() {} - - /** - * Sets the objective or task description for the plan. - * - * @param objective a description of what the plan should accomplish - * @return this builder - */ - public Builder objective(String objective) { - this.objective = objective; - return this; - } - - /** - * Sets the domain for specialized planning. - * - *

Common domains include: - *

    - *
  • generic - General purpose planning
  • - *
  • travel - Travel and booking workflows
  • - *
  • healthcare - Healthcare data processing
  • - *
  • finance - Financial analysis workflows
  • - *
- * - * @param domain the domain identifier - * @return this builder - */ - public Builder domain(String domain) { - this.domain = domain; - return this; - } - - /** - * Sets the user token for identifying the requesting user. - * - * @param userToken the user identifier - * @return this builder - */ - public Builder userToken(String userToken) { - this.userToken = userToken; - return this; - } - - /** - * Sets additional context for plan generation. - * - * @param context key-value pairs of contextual information - * @return this builder - */ - public Builder context(Map context) { - this.context = context; - return this; - } - - /** - * Adds a single context entry. - * - * @param key the context key - * @param value the context value - * @return this builder - */ - public Builder addContext(String key, Object value) { - if (this.context == null) { - this.context = new HashMap<>(); - } - this.context.put(key, value); - return this; - } - - /** - * Sets constraints for plan generation. - * - * @param constraints key-value pairs of constraints - * @return this builder - */ - public Builder constraints(Map constraints) { - this.constraints = constraints; - return this; - } - - /** - * Sets the maximum number of steps in the plan. - * - * @param maxSteps the maximum step count - * @return this builder - */ - public Builder maxSteps(int maxSteps) { - this.maxSteps = maxSteps; - return this; - } - - /** - * Sets whether parallel execution is allowed. - * - * @param parallel true to allow parallel step execution - * @return this builder - */ - public Builder parallel(boolean parallel) { - this.parallel = parallel; - return this; - } - - /** - * Builds the PlanRequest. - * - * @return a new PlanRequest instance - * @throws NullPointerException if objective is null - */ - public PlanRequest build() { - return new PlanRequest(this); - } + public PlanRequest build() { + return new PlanRequest(this); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlanResponse.java b/src/main/java/com/getaxonflow/sdk/types/PlanResponse.java index 6ecdc0d..3051b23 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlanResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlanResponse.java @@ -17,205 +17,212 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -/** - * Response containing a generated multi-agent plan. - */ +/** Response containing a generated multi-agent plan. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlanResponse { - @JsonProperty("plan_id") - private final String planId; - - @JsonProperty("steps") - private final List steps; - - @JsonProperty("domain") - private final String domain; - - @JsonProperty("complexity") - private final Integer complexity; - - @JsonProperty("parallel") - private final Boolean parallel; - - @JsonProperty("estimated_duration") - private final String estimatedDuration; - - @JsonProperty("metadata") - private final Map metadata; - - @JsonProperty("status") - private final String status; - - @JsonProperty("result") - private final String result; - - public PlanResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("steps") List steps, - @JsonProperty("domain") String domain, - @JsonProperty("complexity") Integer complexity, - @JsonProperty("parallel") Boolean parallel, - @JsonProperty("estimated_duration") String estimatedDuration, - @JsonProperty("metadata") Map metadata, - @JsonProperty("status") String status, - @JsonProperty("result") String result) { - this.planId = planId; - this.steps = steps != null ? Collections.unmodifiableList(steps) : Collections.emptyList(); - this.domain = domain; - this.complexity = complexity; - this.parallel = parallel; - this.estimatedDuration = estimatedDuration; - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.status = status; - this.result = result; - } - - /** - * Returns the unique identifier for this plan. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } - - /** - * Returns the steps in this plan. - * - * @return immutable list of plan steps - */ - public List getSteps() { - return steps; - } - - /** - * Returns the number of steps in this plan. - * - * @return the step count - */ - public int getStepCount() { - return steps.size(); - } - - /** - * Returns the domain this plan was generated for. - * - * @return the domain identifier - */ - public String getDomain() { - return domain; - } - - /** - * Returns the complexity score of this plan (1-10). - * - * @return the complexity score - */ - public Integer getComplexity() { - return complexity; - } - - /** - * Returns whether this plan supports parallel execution. - * - * @return true if parallel execution is supported - */ - public Boolean isParallel() { - return parallel; - } - - /** - * Returns the estimated total duration for plan execution. - * - * @return the estimated duration string - */ - public String getEstimatedDuration() { - return estimatedDuration; - } - - /** - * Returns additional metadata about the plan. - * - * @return immutable map of metadata - */ - public Map getMetadata() { - return metadata; - } - - /** - * Returns the execution status of the plan. - * - * @return the status (e.g., "pending", "in_progress", "completed", "failed") - */ - public String getStatus() { - return status; - } - - /** - * Returns the result of plan execution. - * - * @return the execution result, or null if not yet executed - */ - public String getResult() { - return result; - } - - /** - * Checks if the plan execution is complete. - * - * @return true if status is "completed" - */ - public boolean isCompleted() { - return "completed".equalsIgnoreCase(status); - } - - /** - * Checks if the plan execution failed. - * - * @return true if status is "failed" - */ - public boolean isFailed() { - return "failed".equalsIgnoreCase(status); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanResponse that = (PlanResponse) o; - return Objects.equals(planId, that.planId) && - Objects.equals(steps, that.steps) && - Objects.equals(domain, that.domain) && - Objects.equals(complexity, that.complexity) && - Objects.equals(parallel, that.parallel) && - Objects.equals(estimatedDuration, that.estimatedDuration) && - Objects.equals(metadata, that.metadata) && - Objects.equals(status, that.status) && - Objects.equals(result, that.result); - } - - @Override - public int hashCode() { - return Objects.hash(planId, steps, domain, complexity, parallel, - estimatedDuration, metadata, status, result); - } - - @Override - public String toString() { - return "PlanResponse{" + - "planId='" + planId + '\'' + - ", stepCount=" + steps.size() + - ", domain='" + domain + '\'' + - ", complexity=" + complexity + - ", parallel=" + parallel + - ", status='" + status + '\'' + - '}'; - } + @JsonProperty("plan_id") + private final String planId; + + @JsonProperty("steps") + private final List steps; + + @JsonProperty("domain") + private final String domain; + + @JsonProperty("complexity") + private final Integer complexity; + + @JsonProperty("parallel") + private final Boolean parallel; + + @JsonProperty("estimated_duration") + private final String estimatedDuration; + + @JsonProperty("metadata") + private final Map metadata; + + @JsonProperty("status") + private final String status; + + @JsonProperty("result") + private final String result; + + public PlanResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("steps") List steps, + @JsonProperty("domain") String domain, + @JsonProperty("complexity") Integer complexity, + @JsonProperty("parallel") Boolean parallel, + @JsonProperty("estimated_duration") String estimatedDuration, + @JsonProperty("metadata") Map metadata, + @JsonProperty("status") String status, + @JsonProperty("result") String result) { + this.planId = planId; + this.steps = steps != null ? Collections.unmodifiableList(steps) : Collections.emptyList(); + this.domain = domain; + this.complexity = complexity; + this.parallel = parallel; + this.estimatedDuration = estimatedDuration; + this.metadata = + metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.status = status; + this.result = result; + } + + /** + * Returns the unique identifier for this plan. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } + + /** + * Returns the steps in this plan. + * + * @return immutable list of plan steps + */ + public List getSteps() { + return steps; + } + + /** + * Returns the number of steps in this plan. + * + * @return the step count + */ + public int getStepCount() { + return steps.size(); + } + + /** + * Returns the domain this plan was generated for. + * + * @return the domain identifier + */ + public String getDomain() { + return domain; + } + + /** + * Returns the complexity score of this plan (1-10). + * + * @return the complexity score + */ + public Integer getComplexity() { + return complexity; + } + + /** + * Returns whether this plan supports parallel execution. + * + * @return true if parallel execution is supported + */ + public Boolean isParallel() { + return parallel; + } + + /** + * Returns the estimated total duration for plan execution. + * + * @return the estimated duration string + */ + public String getEstimatedDuration() { + return estimatedDuration; + } + + /** + * Returns additional metadata about the plan. + * + * @return immutable map of metadata + */ + public Map getMetadata() { + return metadata; + } + + /** + * Returns the execution status of the plan. + * + * @return the status (e.g., "pending", "in_progress", "completed", "failed") + */ + public String getStatus() { + return status; + } + + /** + * Returns the result of plan execution. + * + * @return the execution result, or null if not yet executed + */ + public String getResult() { + return result; + } + + /** + * Checks if the plan execution is complete. + * + * @return true if status is "completed" + */ + public boolean isCompleted() { + return "completed".equalsIgnoreCase(status); + } + + /** + * Checks if the plan execution failed. + * + * @return true if status is "failed" + */ + public boolean isFailed() { + return "failed".equalsIgnoreCase(status); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlanResponse that = (PlanResponse) o; + return Objects.equals(planId, that.planId) + && Objects.equals(steps, that.steps) + && Objects.equals(domain, that.domain) + && Objects.equals(complexity, that.complexity) + && Objects.equals(parallel, that.parallel) + && Objects.equals(estimatedDuration, that.estimatedDuration) + && Objects.equals(metadata, that.metadata) + && Objects.equals(status, that.status) + && Objects.equals(result, that.result); + } + + @Override + public int hashCode() { + return Objects.hash( + planId, steps, domain, complexity, parallel, estimatedDuration, metadata, status, result); + } + + @Override + public String toString() { + return "PlanResponse{" + + "planId='" + + planId + + '\'' + + ", stepCount=" + + steps.size() + + ", domain='" + + domain + + '\'' + + ", complexity=" + + complexity + + ", parallel=" + + parallel + + ", status='" + + status + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlanStep.java b/src/main/java/com/getaxonflow/sdk/types/PlanStep.java index e388be3..cd1da3c 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlanStep.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlanStep.java @@ -17,170 +17,179 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Objects; -/** - * Represents a single step in a multi-agent plan. - */ +/** Represents a single step in a multi-agent plan. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlanStep { - @JsonProperty("id") - private final String id; - - @JsonProperty("name") - private final String name; - - @JsonProperty("type") - private final String type; - - @JsonProperty("description") - private final String description; - - @JsonProperty("depends_on") - private final List dependsOn; - - @JsonProperty("agent") - private final String agent; - - @JsonProperty("parameters") - private final Map parameters; - - @JsonProperty("estimated_time") - private final String estimatedTime; - - public PlanStep( - @JsonProperty("id") String id, - @JsonProperty("name") String name, - @JsonProperty("type") String type, - @JsonProperty("description") String description, - @JsonProperty("depends_on") List dependsOn, - @JsonProperty("agent") String agent, - @JsonProperty("parameters") Map parameters, - @JsonProperty("estimated_time") String estimatedTime) { - this.id = id; - this.name = name; - this.type = type; - this.description = description; - this.dependsOn = dependsOn != null ? Collections.unmodifiableList(dependsOn) : Collections.emptyList(); - this.agent = agent; - this.parameters = parameters != null ? Collections.unmodifiableMap(parameters) : Collections.emptyMap(); - this.estimatedTime = estimatedTime; - } - - /** - * Returns the unique identifier for this step. - * - * @return the step ID - */ - public String getId() { - return id; - } - - /** - * Returns the human-readable name of this step. - * - * @return the step name - */ - public String getName() { - return name; - } - - /** - * Returns the type of this step. - * - *

Common types include: - *

    - *
  • llm-call - LLM inference
  • - *
  • api-call - External API call
  • - *
  • connector-call - MCP connector query
  • - *
  • conditional - Conditional logic
  • - *
  • function-call - Custom function execution
  • - *
- * - * @return the step type - */ - public String getType() { - return type; - } - - /** - * Returns a description of what this step does. - * - * @return the step description - */ - public String getDescription() { - return description; - } - - /** - * Returns the IDs of steps that must complete before this step. - * - * @return immutable list of dependency step IDs - */ - public List getDependsOn() { - return dependsOn; - } - - /** - * Returns the agent responsible for executing this step. - * - * @return the agent identifier - */ - public String getAgent() { - return agent; - } - - /** - * Returns the parameters for this step. - * - * @return immutable map of parameters - */ - public Map getParameters() { - return parameters; - } - - /** - * Returns the estimated execution time for this step. - * - * @return the estimated time string (e.g., "2s", "500ms") - */ - public String getEstimatedTime() { - return estimatedTime; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanStep planStep = (PlanStep) o; - return Objects.equals(id, planStep.id) && - Objects.equals(name, planStep.name) && - Objects.equals(type, planStep.type) && - Objects.equals(description, planStep.description) && - Objects.equals(dependsOn, planStep.dependsOn) && - Objects.equals(agent, planStep.agent) && - Objects.equals(parameters, planStep.parameters) && - Objects.equals(estimatedTime, planStep.estimatedTime); - } - - @Override - public int hashCode() { - return Objects.hash(id, name, type, description, dependsOn, agent, parameters, estimatedTime); - } - - @Override - public String toString() { - return "PlanStep{" + - "id='" + id + '\'' + - ", name='" + name + '\'' + - ", type='" + type + '\'' + - ", dependsOn=" + dependsOn + - ", agent='" + agent + '\'' + - '}'; - } + @JsonProperty("id") + private final String id; + + @JsonProperty("name") + private final String name; + + @JsonProperty("type") + private final String type; + + @JsonProperty("description") + private final String description; + + @JsonProperty("depends_on") + private final List dependsOn; + + @JsonProperty("agent") + private final String agent; + + @JsonProperty("parameters") + private final Map parameters; + + @JsonProperty("estimated_time") + private final String estimatedTime; + + public PlanStep( + @JsonProperty("id") String id, + @JsonProperty("name") String name, + @JsonProperty("type") String type, + @JsonProperty("description") String description, + @JsonProperty("depends_on") List dependsOn, + @JsonProperty("agent") String agent, + @JsonProperty("parameters") Map parameters, + @JsonProperty("estimated_time") String estimatedTime) { + this.id = id; + this.name = name; + this.type = type; + this.description = description; + this.dependsOn = + dependsOn != null ? Collections.unmodifiableList(dependsOn) : Collections.emptyList(); + this.agent = agent; + this.parameters = + parameters != null ? Collections.unmodifiableMap(parameters) : Collections.emptyMap(); + this.estimatedTime = estimatedTime; + } + + /** + * Returns the unique identifier for this step. + * + * @return the step ID + */ + public String getId() { + return id; + } + + /** + * Returns the human-readable name of this step. + * + * @return the step name + */ + public String getName() { + return name; + } + + /** + * Returns the type of this step. + * + *

Common types include: + * + *

    + *
  • llm-call - LLM inference + *
  • api-call - External API call + *
  • connector-call - MCP connector query + *
  • conditional - Conditional logic + *
  • function-call - Custom function execution + *
+ * + * @return the step type + */ + public String getType() { + return type; + } + + /** + * Returns a description of what this step does. + * + * @return the step description + */ + public String getDescription() { + return description; + } + + /** + * Returns the IDs of steps that must complete before this step. + * + * @return immutable list of dependency step IDs + */ + public List getDependsOn() { + return dependsOn; + } + + /** + * Returns the agent responsible for executing this step. + * + * @return the agent identifier + */ + public String getAgent() { + return agent; + } + + /** + * Returns the parameters for this step. + * + * @return immutable map of parameters + */ + public Map getParameters() { + return parameters; + } + + /** + * Returns the estimated execution time for this step. + * + * @return the estimated time string (e.g., "2s", "500ms") + */ + public String getEstimatedTime() { + return estimatedTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlanStep planStep = (PlanStep) o; + return Objects.equals(id, planStep.id) + && Objects.equals(name, planStep.name) + && Objects.equals(type, planStep.type) + && Objects.equals(description, planStep.description) + && Objects.equals(dependsOn, planStep.dependsOn) + && Objects.equals(agent, planStep.agent) + && Objects.equals(parameters, planStep.parameters) + && Objects.equals(estimatedTime, planStep.estimatedTime); + } + + @Override + public int hashCode() { + return Objects.hash(id, name, type, description, dependsOn, agent, parameters, estimatedTime); + } + + @Override + public String toString() { + return "PlanStep{" + + "id='" + + id + + '\'' + + ", name='" + + name + + '\'' + + ", type='" + + type + + '\'' + + ", dependsOn=" + + dependsOn + + ", agent='" + + agent + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlanVersionEntry.java b/src/main/java/com/getaxonflow/sdk/types/PlanVersionEntry.java index b550892..bc39979 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlanVersionEntry.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlanVersionEntry.java @@ -17,112 +17,116 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Represents a single version entry in a plan's version history. - */ +/** Represents a single version entry in a plan's version history. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlanVersionEntry { - @JsonProperty("version") - private final int version; - - @JsonProperty("changed_at") - private final String changedAt; - - @JsonProperty("changed_by") - private final String changedBy; - - @JsonProperty("change_type") - private final String changeType; - - @JsonProperty("change_summary") - private final String changeSummary; - - public PlanVersionEntry( - @JsonProperty("version") int version, - @JsonProperty("changed_at") String changedAt, - @JsonProperty("changed_by") String changedBy, - @JsonProperty("change_type") String changeType, - @JsonProperty("change_summary") String changeSummary) { - this.version = version; - this.changedAt = changedAt; - this.changedBy = changedBy; - this.changeType = changeType; - this.changeSummary = changeSummary; - } - - /** - * Returns the version number. - * - * @return the version number - */ - public int getVersion() { - return version; - } - - /** - * Returns when this version was created. - * - * @return ISO 8601 timestamp string - */ - public String getChangedAt() { - return changedAt; - } - - /** - * Returns who made this change. - * - * @return the user or system identifier - */ - public String getChangedBy() { - return changedBy; - } - - /** - * Returns the type of change (e.g., "created", "updated", "cancelled"). - * - * @return the change type - */ - public String getChangeType() { - return changeType; - } - - /** - * Returns a human-readable summary of the change. - * - * @return the change summary - */ - public String getChangeSummary() { - return changeSummary; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanVersionEntry that = (PlanVersionEntry) o; - return version == that.version && - Objects.equals(changedAt, that.changedAt) && - Objects.equals(changedBy, that.changedBy) && - Objects.equals(changeType, that.changeType) && - Objects.equals(changeSummary, that.changeSummary); - } - - @Override - public int hashCode() { - return Objects.hash(version, changedAt, changedBy, changeType, changeSummary); - } - - @Override - public String toString() { - return "PlanVersionEntry{" + - "version=" + version + - ", changedAt='" + changedAt + '\'' + - ", changedBy='" + changedBy + '\'' + - ", changeType='" + changeType + '\'' + - '}'; - } + @JsonProperty("version") + private final int version; + + @JsonProperty("changed_at") + private final String changedAt; + + @JsonProperty("changed_by") + private final String changedBy; + + @JsonProperty("change_type") + private final String changeType; + + @JsonProperty("change_summary") + private final String changeSummary; + + public PlanVersionEntry( + @JsonProperty("version") int version, + @JsonProperty("changed_at") String changedAt, + @JsonProperty("changed_by") String changedBy, + @JsonProperty("change_type") String changeType, + @JsonProperty("change_summary") String changeSummary) { + this.version = version; + this.changedAt = changedAt; + this.changedBy = changedBy; + this.changeType = changeType; + this.changeSummary = changeSummary; + } + + /** + * Returns the version number. + * + * @return the version number + */ + public int getVersion() { + return version; + } + + /** + * Returns when this version was created. + * + * @return ISO 8601 timestamp string + */ + public String getChangedAt() { + return changedAt; + } + + /** + * Returns who made this change. + * + * @return the user or system identifier + */ + public String getChangedBy() { + return changedBy; + } + + /** + * Returns the type of change (e.g., "created", "updated", "cancelled"). + * + * @return the change type + */ + public String getChangeType() { + return changeType; + } + + /** + * Returns a human-readable summary of the change. + * + * @return the change summary + */ + public String getChangeSummary() { + return changeSummary; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlanVersionEntry that = (PlanVersionEntry) o; + return version == that.version + && Objects.equals(changedAt, that.changedAt) + && Objects.equals(changedBy, that.changedBy) + && Objects.equals(changeType, that.changeType) + && Objects.equals(changeSummary, that.changeSummary); + } + + @Override + public int hashCode() { + return Objects.hash(version, changedAt, changedBy, changeType, changeSummary); + } + + @Override + public String toString() { + return "PlanVersionEntry{" + + "version=" + + version + + ", changedAt='" + + changedAt + + '\'' + + ", changedBy='" + + changedBy + + '\'' + + ", changeType='" + + changeType + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlanVersionsResponse.java b/src/main/java/com/getaxonflow/sdk/types/PlanVersionsResponse.java index 753c21c..5e360da 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlanVersionsResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlanVersionsResponse.java @@ -17,76 +17,76 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; -/** - * Response containing the version history of a multi-agent plan. - */ +/** Response containing the version history of a multi-agent plan. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlanVersionsResponse { - @JsonProperty("plan_id") - private final String planId; + @JsonProperty("plan_id") + private final String planId; - @JsonProperty("versions") - private final List versions; + @JsonProperty("versions") + private final List versions; - public PlanVersionsResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("versions") List versions) { - this.planId = planId; - this.versions = versions != null ? Collections.unmodifiableList(versions) : Collections.emptyList(); - } + public PlanVersionsResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("versions") List versions) { + this.planId = planId; + this.versions = + versions != null ? Collections.unmodifiableList(versions) : Collections.emptyList(); + } - /** - * Returns the plan ID. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } + /** + * Returns the plan ID. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } - /** - * Returns the version history entries. - * - * @return immutable list of version entries - */ - public List getVersions() { - return versions; - } + /** + * Returns the version history entries. + * + * @return immutable list of version entries + */ + public List getVersions() { + return versions; + } - /** - * Returns the number of versions. - * - * @return the version count - */ - public int getVersionCount() { - return versions.size(); - } + /** + * Returns the number of versions. + * + * @return the version count + */ + public int getVersionCount() { + return versions.size(); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanVersionsResponse that = (PlanVersionsResponse) o; - return Objects.equals(planId, that.planId) && - Objects.equals(versions, that.versions); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlanVersionsResponse that = (PlanVersionsResponse) o; + return Objects.equals(planId, that.planId) && Objects.equals(versions, that.versions); + } - @Override - public int hashCode() { - return Objects.hash(planId, versions); - } + @Override + public int hashCode() { + return Objects.hash(planId, versions); + } - @Override - public String toString() { - return "PlanVersionsResponse{" + - "planId='" + planId + '\'' + - ", versionCount=" + versions.size() + - '}'; - } + @Override + public String toString() { + return "PlanVersionsResponse{" + + "planId='" + + planId + + '\'' + + ", versionCount=" + + versions.size() + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PlatformCapability.java b/src/main/java/com/getaxonflow/sdk/types/PlatformCapability.java index bad352d..7cab490 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PlatformCapability.java +++ b/src/main/java/com/getaxonflow/sdk/types/PlatformCapability.java @@ -17,52 +17,65 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Represents a capability advertised by the AxonFlow platform. - */ +/** Represents a capability advertised by the AxonFlow platform. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlatformCapability { - @JsonProperty("name") - private final String name; + @JsonProperty("name") + private final String name; + + @JsonProperty("since") + private final String since; + + @JsonProperty("description") + private final String description; - @JsonProperty("since") - private final String since; + public PlatformCapability( + @JsonProperty("name") String name, + @JsonProperty("since") String since, + @JsonProperty("description") String description) { + this.name = name; + this.since = since; + this.description = description; + } - @JsonProperty("description") - private final String description; + public String getName() { + return name; + } - public PlatformCapability( - @JsonProperty("name") String name, - @JsonProperty("since") String since, - @JsonProperty("description") String description) { - this.name = name; - this.since = since; - this.description = description; - } + public String getSince() { + return since; + } - public String getName() { return name; } - public String getSince() { return since; } - public String getDescription() { return description; } + public String getDescription() { + return description; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlatformCapability that = (PlatformCapability) o; - return Objects.equals(name, that.name) && Objects.equals(since, that.since) && Objects.equals(description, that.description); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlatformCapability that = (PlatformCapability) o; + return Objects.equals(name, that.name) + && Objects.equals(since, that.since) + && Objects.equals(description, that.description); + } - @Override - public int hashCode() { - return Objects.hash(name, since, description); - } + @Override + public int hashCode() { + return Objects.hash(name, since, description); + } - @Override - public String toString() { - return "PlatformCapability{name='" + name + "', since='" + since + "', description='" + description + "'}"; - } + @Override + public String toString() { + return "PlatformCapability{name='" + + name + + "', since='" + + since + + "', description='" + + description + + "'}"; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalRequest.java b/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalRequest.java index 080dedd..5d885b6 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalRequest.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.HashMap; import java.util.List; @@ -28,13 +27,15 @@ * Request for policy pre-check in Gateway Mode. * *

This is the first step of the Gateway Mode pattern: + * *

    - *
  1. Pre-check: Get policy approval using this request
  2. - *
  3. Direct LLM call: Make your own call to the LLM provider
  4. - *
  5. Audit: Log the LLM call for compliance tracking
  6. + *
  7. Pre-check: Get policy approval using this request + *
  8. Direct LLM call: Make your own call to the LLM provider + *
  9. Audit: Log the LLM call for compliance tracking *
* *

Example usage: + * *

{@code
  * PolicyApprovalRequest request = PolicyApprovalRequest.builder()
  *     .userToken("user-123")
@@ -46,174 +47,181 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class PolicyApprovalRequest {
 
-    @JsonProperty("user_token")
-    private final String userToken;
+  @JsonProperty("user_token")
+  private final String userToken;
 
-    @JsonProperty("query")
-    private final String query;
+  @JsonProperty("query")
+  private final String query;
 
-    @JsonProperty("data_sources")
-    private final List dataSources;
+  @JsonProperty("data_sources")
+  private final List dataSources;
 
-    @JsonProperty("context")
-    private final Map context;
+  @JsonProperty("context")
+  private final Map context;
 
-    @JsonProperty("client_id")
-    private final String clientId;
+  @JsonProperty("client_id")
+  private final String clientId;
 
-    private PolicyApprovalRequest(Builder builder) {
-        this.userToken = Objects.requireNonNull(builder.userToken, "userToken cannot be null");
-        this.query = Objects.requireNonNull(builder.query, "query cannot be null");
-        this.dataSources = builder.dataSources != null
+  private PolicyApprovalRequest(Builder builder) {
+    this.userToken = Objects.requireNonNull(builder.userToken, "userToken cannot be null");
+    this.query = Objects.requireNonNull(builder.query, "query cannot be null");
+    this.dataSources =
+        builder.dataSources != null
             ? Collections.unmodifiableList(builder.dataSources)
             : Collections.emptyList();
-        this.context = builder.context != null
+    this.context =
+        builder.context != null
             ? Collections.unmodifiableMap(new HashMap<>(builder.context))
             : Collections.emptyMap();
-        this.clientId = builder.clientId;
-    }
-
-    public String getUserToken() {
-        return userToken;
-    }
+    this.clientId = builder.clientId;
+  }
+
+  public String getUserToken() {
+    return userToken;
+  }
+
+  public String getQuery() {
+    return query;
+  }
+
+  public List getDataSources() {
+    return dataSources;
+  }
+
+  public Map getContext() {
+    return context;
+  }
+
+  public String getClientId() {
+    return clientId;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    PolicyApprovalRequest that = (PolicyApprovalRequest) o;
+    return Objects.equals(userToken, that.userToken)
+        && Objects.equals(query, that.query)
+        && Objects.equals(dataSources, that.dataSources)
+        && Objects.equals(context, that.context)
+        && Objects.equals(clientId, that.clientId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(userToken, query, dataSources, context, clientId);
+  }
+
+  @Override
+  public String toString() {
+    return "PolicyApprovalRequest{"
+        + "userToken='"
+        + userToken
+        + '\''
+        + ", query='"
+        + query
+        + '\''
+        + ", dataSources="
+        + dataSources
+        + ", clientId='"
+        + clientId
+        + '\''
+        + '}';
+  }
+
+  /** Builder for PolicyApprovalRequest. */
+  public static final class Builder {
+    private String userToken;
+    private String query;
+    private List dataSources;
+    private Map context;
+    private String clientId;
+
+    private Builder() {}
 
-    public String getQuery() {
-        return query;
-    }
-
-    public List getDataSources() {
-        return dataSources;
-    }
-
-    public Map getContext() {
-        return context;
+    /**
+     * Sets the user token identifying the requesting user.
+     *
+     * @param userToken the user identifier
+     * @return this builder
+     */
+    public Builder userToken(String userToken) {
+      this.userToken = userToken;
+      return this;
     }
 
-    public String getClientId() {
-        return clientId;
+    /**
+     * Sets the query or prompt to be evaluated.
+     *
+     * @param query the query text
+     * @return this builder
+     */
+    public Builder query(String query) {
+      this.query = query;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    /**
+     * Sets the data sources that will be accessed.
+     *
+     * @param dataSources list of data source identifiers
+     * @return this builder
+     */
+    public Builder dataSources(List dataSources) {
+      this.dataSources = dataSources;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        PolicyApprovalRequest that = (PolicyApprovalRequest) o;
-        return Objects.equals(userToken, that.userToken) &&
-               Objects.equals(query, that.query) &&
-               Objects.equals(dataSources, that.dataSources) &&
-               Objects.equals(context, that.context) &&
-               Objects.equals(clientId, that.clientId);
+    /**
+     * Sets additional context for policy evaluation.
+     *
+     * @param context key-value pairs of contextual information
+     * @return this builder
+     */
+    public Builder context(Map context) {
+      this.context = context;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(userToken, query, dataSources, context, clientId);
+    /**
+     * Adds a single context entry.
+     *
+     * @param key the context key
+     * @param value the context value
+     * @return this builder
+     */
+    public Builder addContext(String key, Object value) {
+      if (this.context == null) {
+        this.context = new HashMap<>();
+      }
+      this.context.put(key, value);
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "PolicyApprovalRequest{" +
-               "userToken='" + userToken + '\'' +
-               ", query='" + query + '\'' +
-               ", dataSources=" + dataSources +
-               ", clientId='" + clientId + '\'' +
-               '}';
+    /**
+     * Sets the client ID for multi-tenant scenarios.
+     *
+     * @param clientId the client identifier
+     * @return this builder
+     */
+    public Builder clientId(String clientId) {
+      this.clientId = clientId;
+      return this;
     }
 
     /**
-     * Builder for PolicyApprovalRequest.
+     * Builds the PolicyApprovalRequest.
+     *
+     * @return a new PolicyApprovalRequest instance
+     * @throws NullPointerException if required fields are null
      */
-    public static final class Builder {
-        private String userToken;
-        private String query;
-        private List dataSources;
-        private Map context;
-        private String clientId;
-
-        private Builder() {}
-
-        /**
-         * Sets the user token identifying the requesting user.
-         *
-         * @param userToken the user identifier
-         * @return this builder
-         */
-        public Builder userToken(String userToken) {
-            this.userToken = userToken;
-            return this;
-        }
-
-        /**
-         * Sets the query or prompt to be evaluated.
-         *
-         * @param query the query text
-         * @return this builder
-         */
-        public Builder query(String query) {
-            this.query = query;
-            return this;
-        }
-
-        /**
-         * Sets the data sources that will be accessed.
-         *
-         * @param dataSources list of data source identifiers
-         * @return this builder
-         */
-        public Builder dataSources(List dataSources) {
-            this.dataSources = dataSources;
-            return this;
-        }
-
-        /**
-         * Sets additional context for policy evaluation.
-         *
-         * @param context key-value pairs of contextual information
-         * @return this builder
-         */
-        public Builder context(Map context) {
-            this.context = context;
-            return this;
-        }
-
-        /**
-         * Adds a single context entry.
-         *
-         * @param key   the context key
-         * @param value the context value
-         * @return this builder
-         */
-        public Builder addContext(String key, Object value) {
-            if (this.context == null) {
-                this.context = new HashMap<>();
-            }
-            this.context.put(key, value);
-            return this;
-        }
-
-        /**
-         * Sets the client ID for multi-tenant scenarios.
-         *
-         * @param clientId the client identifier
-         * @return this builder
-         */
-        public Builder clientId(String clientId) {
-            this.clientId = clientId;
-            return this;
-        }
-
-        /**
-         * Builds the PolicyApprovalRequest.
-         *
-         * @return a new PolicyApprovalRequest instance
-         * @throws NullPointerException if required fields are null
-         */
-        public PolicyApprovalRequest build() {
-            return new PolicyApprovalRequest(this);
-        }
+    public PolicyApprovalRequest build() {
+      return new PolicyApprovalRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalResult.java b/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalResult.java
index 879a667..4c23665 100644
--- a/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalResult.java
+++ b/src/main/java/com/getaxonflow/sdk/types/PolicyApprovalResult.java
@@ -17,7 +17,6 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.time.Instant;
 import java.util.Collections;
 import java.util.List;
@@ -27,10 +26,11 @@
 /**
  * Result of a policy pre-check in Gateway Mode.
  *
- * 

This response indicates whether the request is approved to proceed to the LLM call. - * If approved, the {@code contextId} must be used in the subsequent audit call. + *

This response indicates whether the request is approved to proceed to the LLM call. If + * approved, the {@code contextId} must be used in the subsequent audit call. * *

Example usage: + * *

{@code
  * PolicyApprovalResult result = axonflow.getPolicyApprovedContext(request);
  * if (result.isApproved()) {
@@ -50,212 +50,232 @@
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class PolicyApprovalResult {
 
-    @JsonProperty("context_id")
-    private final String contextId;
+  @JsonProperty("context_id")
+  private final String contextId;
 
-    @JsonProperty("approved")
-    private final boolean approved;
+  @JsonProperty("approved")
+  private final boolean approved;
 
-    @JsonProperty("requires_redaction")
-    private final boolean requiresRedaction;
+  @JsonProperty("requires_redaction")
+  private final boolean requiresRedaction;
 
-    @JsonProperty("approved_data")
-    private final Map approvedData;
+  @JsonProperty("approved_data")
+  private final Map approvedData;
 
-    @JsonProperty("policies")
-    private final List policies;
+  @JsonProperty("policies")
+  private final List policies;
 
-    @JsonProperty("expires_at")
-    private final Instant expiresAt;
+  @JsonProperty("expires_at")
+  private final Instant expiresAt;
 
-    @JsonProperty("block_reason")
-    private final String blockReason;
+  @JsonProperty("block_reason")
+  private final String blockReason;
 
-    @JsonProperty("rate_limit_info")
-    private final RateLimitInfo rateLimitInfo;
+  @JsonProperty("rate_limit_info")
+  private final RateLimitInfo rateLimitInfo;
 
-    @JsonProperty("processing_time")
-    private final String processingTime;
+  @JsonProperty("processing_time")
+  private final String processingTime;
 
-    public PolicyApprovalResult(
-            @JsonProperty("context_id") String contextId,
-            @JsonProperty("approved") boolean approved,
-            @JsonProperty("requires_redaction") boolean requiresRedaction,
-            @JsonProperty("approved_data") Map approvedData,
-            @JsonProperty("policies") List policies,
-            @JsonProperty("expires_at") Instant expiresAt,
-            @JsonProperty("block_reason") String blockReason,
-            @JsonProperty("rate_limit_info") RateLimitInfo rateLimitInfo,
-            @JsonProperty("processing_time") String processingTime) {
-        this.contextId = contextId;
-        this.approved = approved;
-        this.requiresRedaction = requiresRedaction;
-        this.approvedData = approvedData != null ? Collections.unmodifiableMap(approvedData) : Collections.emptyMap();
-        this.policies = policies != null ? Collections.unmodifiableList(policies) : Collections.emptyList();
-        this.expiresAt = expiresAt;
-        this.blockReason = blockReason;
-        this.rateLimitInfo = rateLimitInfo;
-        this.processingTime = processingTime;
-    }
+  public PolicyApprovalResult(
+      @JsonProperty("context_id") String contextId,
+      @JsonProperty("approved") boolean approved,
+      @JsonProperty("requires_redaction") boolean requiresRedaction,
+      @JsonProperty("approved_data") Map approvedData,
+      @JsonProperty("policies") List policies,
+      @JsonProperty("expires_at") Instant expiresAt,
+      @JsonProperty("block_reason") String blockReason,
+      @JsonProperty("rate_limit_info") RateLimitInfo rateLimitInfo,
+      @JsonProperty("processing_time") String processingTime) {
+    this.contextId = contextId;
+    this.approved = approved;
+    this.requiresRedaction = requiresRedaction;
+    this.approvedData =
+        approvedData != null ? Collections.unmodifiableMap(approvedData) : Collections.emptyMap();
+    this.policies =
+        policies != null ? Collections.unmodifiableList(policies) : Collections.emptyList();
+    this.expiresAt = expiresAt;
+    this.blockReason = blockReason;
+    this.rateLimitInfo = rateLimitInfo;
+    this.processingTime = processingTime;
+  }
 
-    /**
-     * Returns the context ID for correlating with the audit call.
-     *
-     * 

This ID must be passed to {@code auditLLMCall()} after making the LLM call. - * - * @return the context identifier - */ - public String getContextId() { - return contextId; - } + /** + * Returns the context ID for correlating with the audit call. + * + *

This ID must be passed to {@code auditLLMCall()} after making the LLM call. + * + * @return the context identifier + */ + public String getContextId() { + return contextId; + } - /** - * Returns whether the request is approved to proceed. - * - * @return true if approved, false if blocked by policy - */ - public boolean isApproved() { - return approved; - } + /** + * Returns whether the request is approved to proceed. + * + * @return true if approved, false if blocked by policy + */ + public boolean isApproved() { + return approved; + } - /** - * Returns whether the response requires redaction. - * - *

When true, PII was detected with redact action and the response - * should be processed for redaction before being shown to users. - * - * @return true if redaction is required - */ - public boolean isRequiresRedaction() { - return requiresRedaction; - } + /** + * Returns whether the response requires redaction. + * + *

When true, PII was detected with redact action and the response should be processed for + * redaction before being shown to users. + * + * @return true if redaction is required + */ + public boolean isRequiresRedaction() { + return requiresRedaction; + } - /** - * Returns data that has been approved/filtered by policies. - * - *

This may contain redacted or filtered versions of sensitive data - * that is safe to send to the LLM. - * - * @return immutable map of approved data - */ - public Map getApprovedData() { - return approvedData; - } + /** + * Returns data that has been approved/filtered by policies. + * + *

This may contain redacted or filtered versions of sensitive data that is safe to send to the + * LLM. + * + * @return immutable map of approved data + */ + public Map getApprovedData() { + return approvedData; + } - /** - * Returns the list of policies that were evaluated. - * - * @return immutable list of policy names - */ - public List getPolicies() { - return policies; - } + /** + * Returns the list of policies that were evaluated. + * + * @return immutable list of policy names + */ + public List getPolicies() { + return policies; + } - /** - * Returns when this approval expires. - * - *

The audit call must be made before this time, typically within 5 minutes. - * - * @return the expiration timestamp - */ - public Instant getExpiresAt() { - return expiresAt; - } + /** + * Returns when this approval expires. + * + *

The audit call must be made before this time, typically within 5 minutes. + * + * @return the expiration timestamp + */ + public Instant getExpiresAt() { + return expiresAt; + } - /** - * Checks if this approval has expired. - * - * @return true if the approval has expired - */ - public boolean isExpired() { - return expiresAt != null && Instant.now().isAfter(expiresAt); - } + /** + * Checks if this approval has expired. + * + * @return true if the approval has expired + */ + public boolean isExpired() { + return expiresAt != null && Instant.now().isAfter(expiresAt); + } - /** - * Returns the reason the request was blocked, if not approved. - * - * @return the block reason, or null if approved - */ - public String getBlockReason() { - return blockReason; - } + /** + * Returns the reason the request was blocked, if not approved. + * + * @return the block reason, or null if approved + */ + public String getBlockReason() { + return blockReason; + } - /** - * Extracts the policy name from the block reason. - * - * @return the extracted policy name, or the full block reason - */ - public String getBlockingPolicyName() { - if (blockReason == null || blockReason.isEmpty()) { - return null; - } - String prefix = "Request blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } - prefix = "Blocked by policy: "; - if (blockReason.startsWith(prefix)) { - return blockReason.substring(prefix.length()).trim(); - } - if (blockReason.startsWith("[")) { - int endBracket = blockReason.indexOf(']'); - if (endBracket > 1) { - return blockReason.substring(1, endBracket).trim(); - } - } - return blockReason; + /** + * Extracts the policy name from the block reason. + * + * @return the extracted policy name, or the full block reason + */ + public String getBlockingPolicyName() { + if (blockReason == null || blockReason.isEmpty()) { + return null; } - - /** - * Returns rate limit information, if available. - * - * @return the rate limit info, or null - */ - public RateLimitInfo getRateLimitInfo() { - return rateLimitInfo; + String prefix = "Request blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); } - - /** - * Returns the processing time for the policy evaluation. - * - * @return the processing time string (e.g., "5.23ms") - */ - public String getProcessingTime() { - return processingTime; + prefix = "Blocked by policy: "; + if (blockReason.startsWith(prefix)) { + return blockReason.substring(prefix.length()).trim(); } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PolicyApprovalResult that = (PolicyApprovalResult) o; - return approved == that.approved && - requiresRedaction == that.requiresRedaction && - Objects.equals(contextId, that.contextId) && - Objects.equals(approvedData, that.approvedData) && - Objects.equals(policies, that.policies) && - Objects.equals(expiresAt, that.expiresAt) && - Objects.equals(blockReason, that.blockReason) && - Objects.equals(rateLimitInfo, that.rateLimitInfo) && - Objects.equals(processingTime, that.processingTime); + if (blockReason.startsWith("[")) { + int endBracket = blockReason.indexOf(']'); + if (endBracket > 1) { + return blockReason.substring(1, endBracket).trim(); + } } + return blockReason; + } - @Override - public int hashCode() { - return Objects.hash(contextId, approved, requiresRedaction, approvedData, policies, expiresAt, - blockReason, rateLimitInfo, processingTime); - } + /** + * Returns rate limit information, if available. + * + * @return the rate limit info, or null + */ + public RateLimitInfo getRateLimitInfo() { + return rateLimitInfo; + } - @Override - public String toString() { - return "PolicyApprovalResult{" + - "contextId='" + contextId + '\'' + - ", approved=" + approved + - ", requiresRedaction=" + requiresRedaction + - ", policies=" + policies + - ", expiresAt=" + expiresAt + - ", blockReason='" + blockReason + '\'' + - ", processingTime='" + processingTime + '\'' + - '}'; - } + /** + * Returns the processing time for the policy evaluation. + * + * @return the processing time string (e.g., "5.23ms") + */ + public String getProcessingTime() { + return processingTime; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyApprovalResult that = (PolicyApprovalResult) o; + return approved == that.approved + && requiresRedaction == that.requiresRedaction + && Objects.equals(contextId, that.contextId) + && Objects.equals(approvedData, that.approvedData) + && Objects.equals(policies, that.policies) + && Objects.equals(expiresAt, that.expiresAt) + && Objects.equals(blockReason, that.blockReason) + && Objects.equals(rateLimitInfo, that.rateLimitInfo) + && Objects.equals(processingTime, that.processingTime); + } + + @Override + public int hashCode() { + return Objects.hash( + contextId, + approved, + requiresRedaction, + approvedData, + policies, + expiresAt, + blockReason, + rateLimitInfo, + processingTime); + } + + @Override + public String toString() { + return "PolicyApprovalResult{" + + "contextId='" + + contextId + + '\'' + + ", approved=" + + approved + + ", requiresRedaction=" + + requiresRedaction + + ", policies=" + + policies + + ", expiresAt=" + + expiresAt + + ", blockReason='" + + blockReason + + '\'' + + ", processingTime='" + + processingTime + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PolicyInfo.java b/src/main/java/com/getaxonflow/sdk/types/PolicyInfo.java index 45567d1..8848cd9 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PolicyInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/PolicyInfo.java @@ -17,167 +17,178 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Duration; import java.util.Collections; import java.util.List; import java.util.Objects; -/** - * Contains information about policies evaluated during a request. - */ +/** Contains information about policies evaluated during a request. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PolicyInfo { - @JsonProperty("policies_evaluated") - private final List policiesEvaluated; - - @JsonProperty("static_checks") - private final List staticChecks; - - @JsonProperty("processing_time") - private final String processingTime; - - @JsonProperty("tenant_id") - private final String tenantId; - - @JsonProperty("risk_score") - private final Double riskScore; - - @JsonProperty("code_artifact") - private final CodeArtifact codeArtifact; - - public PolicyInfo( - @JsonProperty("policies_evaluated") List policiesEvaluated, - @JsonProperty("static_checks") List staticChecks, - @JsonProperty("processing_time") String processingTime, - @JsonProperty("tenant_id") String tenantId, - @JsonProperty("risk_score") Double riskScore, - @JsonProperty("code_artifact") CodeArtifact codeArtifact) { - this.policiesEvaluated = policiesEvaluated != null ? Collections.unmodifiableList(policiesEvaluated) : Collections.emptyList(); - this.staticChecks = staticChecks != null ? Collections.unmodifiableList(staticChecks) : Collections.emptyList(); - this.processingTime = processingTime; - this.tenantId = tenantId; - this.riskScore = riskScore; - this.codeArtifact = codeArtifact; - } - - /** - * Returns the list of policies that were evaluated. - * - * @return immutable list of policy names - */ - public List getPoliciesEvaluated() { - return policiesEvaluated; - } - - /** - * Returns the list of static checks that were performed. - * - * @return immutable list of static check names - */ - public List getStaticChecks() { - return staticChecks; + @JsonProperty("policies_evaluated") + private final List policiesEvaluated; + + @JsonProperty("static_checks") + private final List staticChecks; + + @JsonProperty("processing_time") + private final String processingTime; + + @JsonProperty("tenant_id") + private final String tenantId; + + @JsonProperty("risk_score") + private final Double riskScore; + + @JsonProperty("code_artifact") + private final CodeArtifact codeArtifact; + + public PolicyInfo( + @JsonProperty("policies_evaluated") List policiesEvaluated, + @JsonProperty("static_checks") List staticChecks, + @JsonProperty("processing_time") String processingTime, + @JsonProperty("tenant_id") String tenantId, + @JsonProperty("risk_score") Double riskScore, + @JsonProperty("code_artifact") CodeArtifact codeArtifact) { + this.policiesEvaluated = + policiesEvaluated != null + ? Collections.unmodifiableList(policiesEvaluated) + : Collections.emptyList(); + this.staticChecks = + staticChecks != null ? Collections.unmodifiableList(staticChecks) : Collections.emptyList(); + this.processingTime = processingTime; + this.tenantId = tenantId; + this.riskScore = riskScore; + this.codeArtifact = codeArtifact; + } + + /** + * Returns the list of policies that were evaluated. + * + * @return immutable list of policy names + */ + public List getPoliciesEvaluated() { + return policiesEvaluated; + } + + /** + * Returns the list of static checks that were performed. + * + * @return immutable list of static check names + */ + public List getStaticChecks() { + return staticChecks; + } + + /** + * Returns the raw processing time string (e.g., "17.48ms"). + * + * @return processing time as a string + */ + public String getProcessingTime() { + return processingTime; + } + + /** + * Parses and returns the processing time as a Duration. + * + * @return the processing time as a Duration, or Duration.ZERO if parsing fails + */ + public Duration getProcessingDuration() { + if (processingTime == null || processingTime.isEmpty()) { + return Duration.ZERO; } - - /** - * Returns the raw processing time string (e.g., "17.48ms"). - * - * @return processing time as a string - */ - public String getProcessingTime() { - return processingTime; - } - - /** - * Parses and returns the processing time as a Duration. - * - * @return the processing time as a Duration, or Duration.ZERO if parsing fails - */ - public Duration getProcessingDuration() { - if (processingTime == null || processingTime.isEmpty()) { - return Duration.ZERO; - } - try { - String normalized = processingTime.trim().toLowerCase(); - if (normalized.endsWith("ms")) { - double millis = Double.parseDouble(normalized.substring(0, normalized.length() - 2)); - return Duration.ofNanos((long) (millis * 1_000_000)); - } else if (normalized.endsWith("s")) { - double seconds = Double.parseDouble(normalized.substring(0, normalized.length() - 1)); - return Duration.ofNanos((long) (seconds * 1_000_000_000)); - } else if (normalized.endsWith("us") || normalized.endsWith("µs")) { - String numPart = normalized.endsWith("µs") - ? normalized.substring(0, normalized.length() - 2) - : normalized.substring(0, normalized.length() - 2); - double micros = Double.parseDouble(numPart); - return Duration.ofNanos((long) (micros * 1_000)); - } else if (normalized.endsWith("ns")) { - long nanos = Long.parseLong(normalized.substring(0, normalized.length() - 2)); - return Duration.ofNanos(nanos); - } - // Try parsing as milliseconds if no unit - double millis = Double.parseDouble(normalized); - return Duration.ofNanos((long) (millis * 1_000_000)); - } catch (NumberFormatException e) { - return Duration.ZERO; - } - } - - /** - * Returns the tenant ID associated with this request. - * - * @return the tenant identifier - */ - public String getTenantId() { - return tenantId; - } - - /** - * Returns the calculated risk score for this request. - * - * @return the risk score, or null if not calculated - */ - public Double getRiskScore() { - return riskScore; - } - - /** - * Returns the code artifact metadata if code was detected in the response. - * - * @return the code artifact, or null if no code was detected - */ - public CodeArtifact getCodeArtifact() { - return codeArtifact; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PolicyInfo that = (PolicyInfo) o; - return Objects.equals(policiesEvaluated, that.policiesEvaluated) && - Objects.equals(staticChecks, that.staticChecks) && - Objects.equals(processingTime, that.processingTime) && - Objects.equals(tenantId, that.tenantId) && - Objects.equals(riskScore, that.riskScore) && - Objects.equals(codeArtifact, that.codeArtifact); - } - - @Override - public int hashCode() { - return Objects.hash(policiesEvaluated, staticChecks, processingTime, tenantId, riskScore, codeArtifact); - } - - @Override - public String toString() { - return "PolicyInfo{" + - "policiesEvaluated=" + policiesEvaluated + - ", staticChecks=" + staticChecks + - ", processingTime='" + processingTime + '\'' + - ", tenantId='" + tenantId + '\'' + - ", riskScore=" + riskScore + - ", codeArtifact=" + codeArtifact + - '}'; + try { + String normalized = processingTime.trim().toLowerCase(); + if (normalized.endsWith("ms")) { + double millis = Double.parseDouble(normalized.substring(0, normalized.length() - 2)); + return Duration.ofNanos((long) (millis * 1_000_000)); + } else if (normalized.endsWith("s")) { + double seconds = Double.parseDouble(normalized.substring(0, normalized.length() - 1)); + return Duration.ofNanos((long) (seconds * 1_000_000_000)); + } else if (normalized.endsWith("us") || normalized.endsWith("µs")) { + String numPart = + normalized.endsWith("µs") + ? normalized.substring(0, normalized.length() - 2) + : normalized.substring(0, normalized.length() - 2); + double micros = Double.parseDouble(numPart); + return Duration.ofNanos((long) (micros * 1_000)); + } else if (normalized.endsWith("ns")) { + long nanos = Long.parseLong(normalized.substring(0, normalized.length() - 2)); + return Duration.ofNanos(nanos); + } + // Try parsing as milliseconds if no unit + double millis = Double.parseDouble(normalized); + return Duration.ofNanos((long) (millis * 1_000_000)); + } catch (NumberFormatException e) { + return Duration.ZERO; } + } + + /** + * Returns the tenant ID associated with this request. + * + * @return the tenant identifier + */ + public String getTenantId() { + return tenantId; + } + + /** + * Returns the calculated risk score for this request. + * + * @return the risk score, or null if not calculated + */ + public Double getRiskScore() { + return riskScore; + } + + /** + * Returns the code artifact metadata if code was detected in the response. + * + * @return the code artifact, or null if no code was detected + */ + public CodeArtifact getCodeArtifact() { + return codeArtifact; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyInfo that = (PolicyInfo) o; + return Objects.equals(policiesEvaluated, that.policiesEvaluated) + && Objects.equals(staticChecks, that.staticChecks) + && Objects.equals(processingTime, that.processingTime) + && Objects.equals(tenantId, that.tenantId) + && Objects.equals(riskScore, that.riskScore) + && Objects.equals(codeArtifact, that.codeArtifact); + } + + @Override + public int hashCode() { + return Objects.hash( + policiesEvaluated, staticChecks, processingTime, tenantId, riskScore, codeArtifact); + } + + @Override + public String toString() { + return "PolicyInfo{" + + "policiesEvaluated=" + + policiesEvaluated + + ", staticChecks=" + + staticChecks + + ", processingTime='" + + processingTime + + '\'' + + ", tenantId='" + + tenantId + + '\'' + + ", riskScore=" + + riskScore + + ", codeArtifact=" + + codeArtifact + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PolicyMatchInfo.java b/src/main/java/com/getaxonflow/sdk/types/PolicyMatchInfo.java index fce417e..4ea5a72 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PolicyMatchInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/PolicyMatchInfo.java @@ -17,88 +17,95 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Information about a policy match during evaluation. - */ +/** Information about a policy match during evaluation. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PolicyMatchInfo { - @JsonProperty("policy_id") - private final String policyId; - - @JsonProperty("policy_name") - private final String policyName; - - @JsonProperty("category") - private final String category; - - @JsonProperty("severity") - private final String severity; - - @JsonProperty("action") - private final String action; - - public PolicyMatchInfo( - @JsonProperty("policy_id") String policyId, - @JsonProperty("policy_name") String policyName, - @JsonProperty("category") String category, - @JsonProperty("severity") String severity, - @JsonProperty("action") String action) { - this.policyId = policyId; - this.policyName = policyName; - this.category = category; - this.severity = severity; - this.action = action; - } - - public String getPolicyId() { - return policyId; - } - - public String getPolicyName() { - return policyName; - } - - public String getCategory() { - return category; - } - - public String getSeverity() { - return severity; - } - - public String getAction() { - return action; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PolicyMatchInfo that = (PolicyMatchInfo) o; - return Objects.equals(policyId, that.policyId) && - Objects.equals(policyName, that.policyName) && - Objects.equals(category, that.category) && - Objects.equals(severity, that.severity) && - Objects.equals(action, that.action); - } - - @Override - public int hashCode() { - return Objects.hash(policyId, policyName, category, severity, action); - } - - @Override - public String toString() { - return "PolicyMatchInfo{" + - "policyId='" + policyId + '\'' + - ", policyName='" + policyName + '\'' + - ", category='" + category + '\'' + - ", severity='" + severity + '\'' + - ", action='" + action + '\'' + - '}'; - } + @JsonProperty("policy_id") + private final String policyId; + + @JsonProperty("policy_name") + private final String policyName; + + @JsonProperty("category") + private final String category; + + @JsonProperty("severity") + private final String severity; + + @JsonProperty("action") + private final String action; + + public PolicyMatchInfo( + @JsonProperty("policy_id") String policyId, + @JsonProperty("policy_name") String policyName, + @JsonProperty("category") String category, + @JsonProperty("severity") String severity, + @JsonProperty("action") String action) { + this.policyId = policyId; + this.policyName = policyName; + this.category = category; + this.severity = severity; + this.action = action; + } + + public String getPolicyId() { + return policyId; + } + + public String getPolicyName() { + return policyName; + } + + public String getCategory() { + return category; + } + + public String getSeverity() { + return severity; + } + + public String getAction() { + return action; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyMatchInfo that = (PolicyMatchInfo) o; + return Objects.equals(policyId, that.policyId) + && Objects.equals(policyName, that.policyName) + && Objects.equals(category, that.category) + && Objects.equals(severity, that.severity) + && Objects.equals(action, that.action); + } + + @Override + public int hashCode() { + return Objects.hash(policyId, policyName, category, severity, action); + } + + @Override + public String toString() { + return "PolicyMatchInfo{" + + "policyId='" + + policyId + + '\'' + + ", policyName='" + + policyName + + '\'' + + ", category='" + + category + + '\'' + + ", severity='" + + severity + + '\'' + + ", action='" + + action + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/PortalLoginResponse.java b/src/main/java/com/getaxonflow/sdk/types/PortalLoginResponse.java index 48d05e4..86a5e85 100644 --- a/src/main/java/com/getaxonflow/sdk/types/PortalLoginResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/PortalLoginResponse.java @@ -17,88 +17,95 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Response from Customer Portal login. - */ +/** Response from Customer Portal login. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PortalLoginResponse { - @JsonProperty("session_id") - private final String sessionId; - - @JsonProperty("org_id") - private final String orgId; - - @JsonProperty("email") - private final String email; - - @JsonProperty("name") - private final String name; - - @JsonProperty("expires_at") - private final String expiresAt; - - public PortalLoginResponse( - @JsonProperty("session_id") String sessionId, - @JsonProperty("org_id") String orgId, - @JsonProperty("email") String email, - @JsonProperty("name") String name, - @JsonProperty("expires_at") String expiresAt) { - this.sessionId = sessionId; - this.orgId = orgId; - this.email = email; - this.name = name; - this.expiresAt = expiresAt; - } - - public String getSessionId() { - return sessionId; - } - - public String getOrgId() { - return orgId; - } - - public String getEmail() { - return email; - } - - public String getName() { - return name; - } - - public String getExpiresAt() { - return expiresAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PortalLoginResponse that = (PortalLoginResponse) o; - return Objects.equals(sessionId, that.sessionId) && - Objects.equals(orgId, that.orgId) && - Objects.equals(email, that.email) && - Objects.equals(name, that.name) && - Objects.equals(expiresAt, that.expiresAt); - } - - @Override - public int hashCode() { - return Objects.hash(sessionId, orgId, email, name, expiresAt); - } - - @Override - public String toString() { - return "PortalLoginResponse{" + - "sessionId='" + sessionId + '\'' + - ", orgId='" + orgId + '\'' + - ", email='" + email + '\'' + - ", name='" + name + '\'' + - ", expiresAt='" + expiresAt + '\'' + - '}'; - } + @JsonProperty("session_id") + private final String sessionId; + + @JsonProperty("org_id") + private final String orgId; + + @JsonProperty("email") + private final String email; + + @JsonProperty("name") + private final String name; + + @JsonProperty("expires_at") + private final String expiresAt; + + public PortalLoginResponse( + @JsonProperty("session_id") String sessionId, + @JsonProperty("org_id") String orgId, + @JsonProperty("email") String email, + @JsonProperty("name") String name, + @JsonProperty("expires_at") String expiresAt) { + this.sessionId = sessionId; + this.orgId = orgId; + this.email = email; + this.name = name; + this.expiresAt = expiresAt; + } + + public String getSessionId() { + return sessionId; + } + + public String getOrgId() { + return orgId; + } + + public String getEmail() { + return email; + } + + public String getName() { + return name; + } + + public String getExpiresAt() { + return expiresAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PortalLoginResponse that = (PortalLoginResponse) o; + return Objects.equals(sessionId, that.sessionId) + && Objects.equals(orgId, that.orgId) + && Objects.equals(email, that.email) + && Objects.equals(name, that.name) + && Objects.equals(expiresAt, that.expiresAt); + } + + @Override + public int hashCode() { + return Objects.hash(sessionId, orgId, email, name, expiresAt); + } + + @Override + public String toString() { + return "PortalLoginResponse{" + + "sessionId='" + + sessionId + + '\'' + + ", orgId='" + + orgId + + '\'' + + ", email='" + + email + + '\'' + + ", name='" + + name + + '\'' + + ", expiresAt='" + + expiresAt + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/RateLimitInfo.java b/src/main/java/com/getaxonflow/sdk/types/RateLimitInfo.java index 08eeb08..9fd33a6 100644 --- a/src/main/java/com/getaxonflow/sdk/types/RateLimitInfo.java +++ b/src/main/java/com/getaxonflow/sdk/types/RateLimitInfo.java @@ -17,91 +17,91 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; import java.util.Objects; -/** - * Contains rate limiting information from AxonFlow responses. - */ +/** Contains rate limiting information from AxonFlow responses. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class RateLimitInfo { - @JsonProperty("limit") - private final int limit; + @JsonProperty("limit") + private final int limit; - @JsonProperty("remaining") - private final int remaining; + @JsonProperty("remaining") + private final int remaining; - @JsonProperty("reset_at") - private final Instant resetAt; + @JsonProperty("reset_at") + private final Instant resetAt; - public RateLimitInfo( - @JsonProperty("limit") int limit, - @JsonProperty("remaining") int remaining, - @JsonProperty("reset_at") Instant resetAt) { - this.limit = limit; - this.remaining = remaining; - this.resetAt = resetAt; - } + public RateLimitInfo( + @JsonProperty("limit") int limit, + @JsonProperty("remaining") int remaining, + @JsonProperty("reset_at") Instant resetAt) { + this.limit = limit; + this.remaining = remaining; + this.resetAt = resetAt; + } - /** - * Returns the maximum number of requests allowed in the current window. - * - * @return the rate limit - */ - public int getLimit() { - return limit; - } + /** + * Returns the maximum number of requests allowed in the current window. + * + * @return the rate limit + */ + public int getLimit() { + return limit; + } - /** - * Returns the number of requests remaining in the current window. - * - * @return remaining requests - */ - public int getRemaining() { - return remaining; - } + /** + * Returns the number of requests remaining in the current window. + * + * @return remaining requests + */ + public int getRemaining() { + return remaining; + } - /** - * Returns when the rate limit window resets. - * - * @return the reset timestamp - */ - public Instant getResetAt() { - return resetAt; - } + /** + * Returns when the rate limit window resets. + * + * @return the reset timestamp + */ + public Instant getResetAt() { + return resetAt; + } - /** - * Checks if the rate limit has been exceeded. - * - * @return true if no requests remain - */ - public boolean isExceeded() { - return remaining <= 0; - } + /** + * Checks if the rate limit has been exceeded. + * + * @return true if no requests remain + */ + public boolean isExceeded() { + return remaining <= 0; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RateLimitInfo that = (RateLimitInfo) o; - return limit == that.limit && - remaining == that.remaining && - Objects.equals(resetAt, that.resetAt); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RateLimitInfo that = (RateLimitInfo) o; + return limit == that.limit + && remaining == that.remaining + && Objects.equals(resetAt, that.resetAt); + } - @Override - public int hashCode() { - return Objects.hash(limit, remaining, resetAt); - } + @Override + public int hashCode() { + return Objects.hash(limit, remaining, resetAt); + } - @Override - public String toString() { - return "RateLimitInfo{" + - "limit=" + limit + - ", remaining=" + remaining + - ", resetAt=" + resetAt + - '}'; - } + @Override + public String toString() { + return "RateLimitInfo{" + + "limit=" + + limit + + ", remaining=" + + remaining + + ", resetAt=" + + resetAt + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/RequestType.java b/src/main/java/com/getaxonflow/sdk/types/RequestType.java index 18220c4..31655bb 100644 --- a/src/main/java/com/getaxonflow/sdk/types/RequestType.java +++ b/src/main/java/com/getaxonflow/sdk/types/RequestType.java @@ -17,57 +17,47 @@ import com.fasterxml.jackson.annotation.JsonValue; -/** - * Types of requests that can be processed by AxonFlow. - */ +/** Types of requests that can be processed by AxonFlow. */ public enum RequestType { - /** - * Standard chat/conversation request. - */ - CHAT("chat"), - - /** - * SQL query request. - */ - SQL("sql"), - - /** - * MCP (Model Context Protocol) connector query. - */ - MCP_QUERY("mcp-query"), - - /** - * Multi-agent planning request. - */ - MULTI_AGENT_PLAN("multi-agent-plan"); - - private final String value; - - RequestType(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; + /** Standard chat/conversation request. */ + CHAT("chat"), + + /** SQL query request. */ + SQL("sql"), + + /** MCP (Model Context Protocol) connector query. */ + MCP_QUERY("mcp-query"), + + /** Multi-agent planning request. */ + MULTI_AGENT_PLAN("multi-agent-plan"); + + private final String value; + + RequestType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + /** + * Parses a string value to a RequestType enum. + * + * @param value the string value to parse + * @return the corresponding RequestType + * @throws IllegalArgumentException if the value is not recognized + */ + public static RequestType fromValue(String value) { + if (value == null) { + throw new IllegalArgumentException("Request type cannot be null"); } - - /** - * Parses a string value to a RequestType enum. - * - * @param value the string value to parse - * @return the corresponding RequestType - * @throws IllegalArgumentException if the value is not recognized - */ - public static RequestType fromValue(String value) { - if (value == null) { - throw new IllegalArgumentException("Request type cannot be null"); - } - for (RequestType type : values()) { - if (type.value.equalsIgnoreCase(value)) { - return type; - } - } - throw new IllegalArgumentException("Unknown request type: " + value); + for (RequestType type : values()) { + if (type.value.equalsIgnoreCase(value)) { + return type; + } } + throw new IllegalArgumentException("Unknown request type: " + value); + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/ResumePlanResponse.java b/src/main/java/com/getaxonflow/sdk/types/ResumePlanResponse.java index 44d1b4c..0223bea 100644 --- a/src/main/java/com/getaxonflow/sdk/types/ResumePlanResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/ResumePlanResponse.java @@ -17,159 +17,167 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Response from resuming a paused multi-agent plan. - */ +/** Response from resuming a paused multi-agent plan. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class ResumePlanResponse { - @JsonProperty("plan_id") - private final String planId; - - @JsonProperty("workflow_id") - private final String workflowId; - - @JsonProperty("status") - private final String status; - - @JsonProperty("approved") - private final Boolean approved; - - @JsonProperty("message") - private final String message; - - @JsonProperty("next_step") - private final Integer nextStep; - - @JsonProperty("next_step_name") - private final String nextStepName; - - @JsonProperty("total_steps") - private final Integer totalSteps; - - public ResumePlanResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("status") String status, - @JsonProperty("approved") Boolean approved, - @JsonProperty("message") String message, - @JsonProperty("next_step") Integer nextStep, - @JsonProperty("next_step_name") String nextStepName, - @JsonProperty("total_steps") Integer totalSteps) { - this.planId = planId; - this.workflowId = workflowId; - this.status = status; - this.approved = approved; - this.message = message; - this.nextStep = nextStep; - this.nextStepName = nextStepName; - this.totalSteps = totalSteps; - } - - /** - * Returns the ID of the resumed plan. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } - - /** - * Returns the status after resuming. - * - * @return the status (e.g., "in_progress", "rejected") - */ - public String getStatus() { - return status; - } - - /** - * Returns the WCP workflow ID. - * - * @return the workflow ID, or null - */ - public String getWorkflowId() { - return workflowId; - } - - /** - * Returns whether the plan was approved to continue. - * - * @return true if the plan was approved, false if not approved or not applicable - */ - public boolean isApproved() { - return Boolean.TRUE.equals(approved); - } - - /** - * Returns a human-readable message about the resume action. - * - * @return the resume message, or null - */ - public String getMessage() { - return message; - } - - /** - * Returns the next step index to be executed. - * - * @return the next step index, or null if completed - */ - public Integer getNextStep() { - return nextStep; - } - - /** - * Returns the name of the next step. - * - * @return the next step name, or null if completed - */ - public String getNextStepName() { - return nextStepName; - } - - /** - * Returns the total number of steps. - * - * @return total steps, or null - */ - public Integer getTotalSteps() { - return totalSteps; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ResumePlanResponse that = (ResumePlanResponse) o; - return Objects.equals(approved, that.approved) && - Objects.equals(planId, that.planId) && - Objects.equals(workflowId, that.workflowId) && - Objects.equals(status, that.status) && - Objects.equals(message, that.message) && - Objects.equals(nextStep, that.nextStep) && - Objects.equals(nextStepName, that.nextStepName) && - Objects.equals(totalSteps, that.totalSteps); - } - - @Override - public int hashCode() { - return Objects.hash(planId, workflowId, status, approved, message, nextStep, nextStepName, totalSteps); - } - - @Override - public String toString() { - return "ResumePlanResponse{" + - "planId='" + planId + '\'' + - ", workflowId='" + workflowId + '\'' + - ", status='" + status + '\'' + - ", approved=" + approved + - ", message='" + message + '\'' + - ", nextStep=" + nextStep + - '}'; - } + @JsonProperty("plan_id") + private final String planId; + + @JsonProperty("workflow_id") + private final String workflowId; + + @JsonProperty("status") + private final String status; + + @JsonProperty("approved") + private final Boolean approved; + + @JsonProperty("message") + private final String message; + + @JsonProperty("next_step") + private final Integer nextStep; + + @JsonProperty("next_step_name") + private final String nextStepName; + + @JsonProperty("total_steps") + private final Integer totalSteps; + + public ResumePlanResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("status") String status, + @JsonProperty("approved") Boolean approved, + @JsonProperty("message") String message, + @JsonProperty("next_step") Integer nextStep, + @JsonProperty("next_step_name") String nextStepName, + @JsonProperty("total_steps") Integer totalSteps) { + this.planId = planId; + this.workflowId = workflowId; + this.status = status; + this.approved = approved; + this.message = message; + this.nextStep = nextStep; + this.nextStepName = nextStepName; + this.totalSteps = totalSteps; + } + + /** + * Returns the ID of the resumed plan. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } + + /** + * Returns the status after resuming. + * + * @return the status (e.g., "in_progress", "rejected") + */ + public String getStatus() { + return status; + } + + /** + * Returns the WCP workflow ID. + * + * @return the workflow ID, or null + */ + public String getWorkflowId() { + return workflowId; + } + + /** + * Returns whether the plan was approved to continue. + * + * @return true if the plan was approved, false if not approved or not applicable + */ + public boolean isApproved() { + return Boolean.TRUE.equals(approved); + } + + /** + * Returns a human-readable message about the resume action. + * + * @return the resume message, or null + */ + public String getMessage() { + return message; + } + + /** + * Returns the next step index to be executed. + * + * @return the next step index, or null if completed + */ + public Integer getNextStep() { + return nextStep; + } + + /** + * Returns the name of the next step. + * + * @return the next step name, or null if completed + */ + public String getNextStepName() { + return nextStepName; + } + + /** + * Returns the total number of steps. + * + * @return total steps, or null + */ + public Integer getTotalSteps() { + return totalSteps; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ResumePlanResponse that = (ResumePlanResponse) o; + return Objects.equals(approved, that.approved) + && Objects.equals(planId, that.planId) + && Objects.equals(workflowId, that.workflowId) + && Objects.equals(status, that.status) + && Objects.equals(message, that.message) + && Objects.equals(nextStep, that.nextStep) + && Objects.equals(nextStepName, that.nextStepName) + && Objects.equals(totalSteps, that.totalSteps); + } + + @Override + public int hashCode() { + return Objects.hash( + planId, workflowId, status, approved, message, nextStep, nextStepName, totalSteps); + } + + @Override + public String toString() { + return "ResumePlanResponse{" + + "planId='" + + planId + + '\'' + + ", workflowId='" + + workflowId + + '\'' + + ", status='" + + status + + '\'' + + ", approved=" + + approved + + ", message='" + + message + + '\'' + + ", nextStep=" + + nextStep + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/RollbackPlanResponse.java b/src/main/java/com/getaxonflow/sdk/types/RollbackPlanResponse.java index a7998f9..b978f62 100644 --- a/src/main/java/com/getaxonflow/sdk/types/RollbackPlanResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/RollbackPlanResponse.java @@ -17,97 +17,100 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * Response from rolling back a multi-agent plan to a previous version. - */ +/** Response from rolling back a multi-agent plan to a previous version. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class RollbackPlanResponse { - @JsonProperty("plan_id") - private final String planId; + @JsonProperty("plan_id") + private final String planId; - @JsonProperty("version") - private final int version; + @JsonProperty("version") + private final int version; - @JsonProperty("previous_version") - private final int previousVersion; + @JsonProperty("previous_version") + private final int previousVersion; - @JsonProperty("status") - private final String status; + @JsonProperty("status") + private final String status; - public RollbackPlanResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("version") int version, - @JsonProperty("previous_version") int previousVersion, - @JsonProperty("status") String status) { - this.planId = planId; - this.version = version; - this.previousVersion = previousVersion; - this.status = status; - } + public RollbackPlanResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("version") int version, + @JsonProperty("previous_version") int previousVersion, + @JsonProperty("status") String status) { + this.planId = planId; + this.version = version; + this.previousVersion = previousVersion; + this.status = status; + } - /** - * Returns the ID of the rolled-back plan. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } + /** + * Returns the ID of the rolled-back plan. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } - /** - * Returns the new version number after rollback. - * - * @return the version number - */ - public int getVersion() { - return version; - } + /** + * Returns the new version number after rollback. + * + * @return the version number + */ + public int getVersion() { + return version; + } - /** - * Returns the version that was rolled back from. - * - * @return the previous version number - */ - public int getPreviousVersion() { - return previousVersion; - } + /** + * Returns the version that was rolled back from. + * + * @return the previous version number + */ + public int getPreviousVersion() { + return previousVersion; + } - /** - * Returns the status of the plan after rollback. - * - * @return the status (e.g., "rolled_back") - */ - public String getStatus() { - return status; - } + /** + * Returns the status of the plan after rollback. + * + * @return the status (e.g., "rolled_back") + */ + public String getStatus() { + return status; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RollbackPlanResponse that = (RollbackPlanResponse) o; - return version == that.version && - previousVersion == that.previousVersion && - Objects.equals(planId, that.planId) && - Objects.equals(status, that.status); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RollbackPlanResponse that = (RollbackPlanResponse) o; + return version == that.version + && previousVersion == that.previousVersion + && Objects.equals(planId, that.planId) + && Objects.equals(status, that.status); + } - @Override - public int hashCode() { - return Objects.hash(planId, version, previousVersion, status); - } + @Override + public int hashCode() { + return Objects.hash(planId, version, previousVersion, status); + } - @Override - public String toString() { - return "RollbackPlanResponse{" + - "planId='" + planId + '\'' + - ", version=" + version + - ", previousVersion=" + previousVersion + - ", status='" + status + '\'' + - '}'; - } + @Override + public String toString() { + return "RollbackPlanResponse{" + + "planId='" + + planId + + '\'' + + ", version=" + + version + + ", previousVersion=" + + previousVersion + + ", status='" + + status + + '\'' + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/SDKCompatibility.java b/src/main/java/com/getaxonflow/sdk/types/SDKCompatibility.java index a901bac..22cd27a 100644 --- a/src/main/java/com/getaxonflow/sdk/types/SDKCompatibility.java +++ b/src/main/java/com/getaxonflow/sdk/types/SDKCompatibility.java @@ -17,46 +17,53 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; -/** - * SDK compatibility information returned by the AxonFlow platform health endpoint. - */ +/** SDK compatibility information returned by the AxonFlow platform health endpoint. */ @JsonIgnoreProperties(ignoreUnknown = true) public final class SDKCompatibility { - @JsonProperty("min_sdk_version") - private final String minSdkVersion; - - @JsonProperty("recommended_sdk_version") - private final String recommendedSdkVersion; - - public SDKCompatibility( - @JsonProperty("min_sdk_version") String minSdkVersion, - @JsonProperty("recommended_sdk_version") String recommendedSdkVersion) { - this.minSdkVersion = minSdkVersion; - this.recommendedSdkVersion = recommendedSdkVersion; - } - - public String getMinSdkVersion() { return minSdkVersion; } - public String getRecommendedSdkVersion() { return recommendedSdkVersion; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - SDKCompatibility that = (SDKCompatibility) o; - return Objects.equals(minSdkVersion, that.minSdkVersion) && Objects.equals(recommendedSdkVersion, that.recommendedSdkVersion); - } - - @Override - public int hashCode() { - return Objects.hash(minSdkVersion, recommendedSdkVersion); - } - - @Override - public String toString() { - return "SDKCompatibility{minSdkVersion='" + minSdkVersion + "', recommendedSdkVersion='" + recommendedSdkVersion + "'}"; - } + @JsonProperty("min_sdk_version") + private final String minSdkVersion; + + @JsonProperty("recommended_sdk_version") + private final String recommendedSdkVersion; + + public SDKCompatibility( + @JsonProperty("min_sdk_version") String minSdkVersion, + @JsonProperty("recommended_sdk_version") String recommendedSdkVersion) { + this.minSdkVersion = minSdkVersion; + this.recommendedSdkVersion = recommendedSdkVersion; + } + + public String getMinSdkVersion() { + return minSdkVersion; + } + + public String getRecommendedSdkVersion() { + return recommendedSdkVersion; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + SDKCompatibility that = (SDKCompatibility) o; + return Objects.equals(minSdkVersion, that.minSdkVersion) + && Objects.equals(recommendedSdkVersion, that.recommendedSdkVersion); + } + + @Override + public int hashCode() { + return Objects.hash(minSdkVersion, recommendedSdkVersion); + } + + @Override + public String toString() { + return "SDKCompatibility{minSdkVersion='" + + minSdkVersion + + "', recommendedSdkVersion='" + + recommendedSdkVersion + + "'}"; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/TokenUsage.java b/src/main/java/com/getaxonflow/sdk/types/TokenUsage.java index 19611f2..77a4c9d 100644 --- a/src/main/java/com/getaxonflow/sdk/types/TokenUsage.java +++ b/src/main/java/com/getaxonflow/sdk/types/TokenUsage.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** @@ -28,90 +27,93 @@ @JsonIgnoreProperties(ignoreUnknown = true) public final class TokenUsage { - @JsonProperty("prompt_tokens") - private final int promptTokens; + @JsonProperty("prompt_tokens") + private final int promptTokens; - @JsonProperty("completion_tokens") - private final int completionTokens; + @JsonProperty("completion_tokens") + private final int completionTokens; - @JsonProperty("total_tokens") - private final int totalTokens; + @JsonProperty("total_tokens") + private final int totalTokens; - /** - * Creates a new TokenUsage instance. - * - * @param promptTokens tokens used in the prompt/input - * @param completionTokens tokens used in the completion/output - * @param totalTokens total tokens used (prompt + completion) - */ - public TokenUsage( - @JsonProperty("prompt_tokens") int promptTokens, - @JsonProperty("completion_tokens") int completionTokens, - @JsonProperty("total_tokens") int totalTokens) { - this.promptTokens = promptTokens; - this.completionTokens = completionTokens; - this.totalTokens = totalTokens; - } + /** + * Creates a new TokenUsage instance. + * + * @param promptTokens tokens used in the prompt/input + * @param completionTokens tokens used in the completion/output + * @param totalTokens total tokens used (prompt + completion) + */ + public TokenUsage( + @JsonProperty("prompt_tokens") int promptTokens, + @JsonProperty("completion_tokens") int completionTokens, + @JsonProperty("total_tokens") int totalTokens) { + this.promptTokens = promptTokens; + this.completionTokens = completionTokens; + this.totalTokens = totalTokens; + } - /** - * Creates a TokenUsage with auto-calculated total. - * - * @param promptTokens tokens used in the prompt/input - * @param completionTokens tokens used in the completion/output - * @return a new TokenUsage instance - */ - public static TokenUsage of(int promptTokens, int completionTokens) { - return new TokenUsage(promptTokens, completionTokens, promptTokens + completionTokens); - } + /** + * Creates a TokenUsage with auto-calculated total. + * + * @param promptTokens tokens used in the prompt/input + * @param completionTokens tokens used in the completion/output + * @return a new TokenUsage instance + */ + public static TokenUsage of(int promptTokens, int completionTokens) { + return new TokenUsage(promptTokens, completionTokens, promptTokens + completionTokens); + } - /** - * Returns the number of tokens used in the prompt. - * - * @return prompt token count - */ - public int getPromptTokens() { - return promptTokens; - } + /** + * Returns the number of tokens used in the prompt. + * + * @return prompt token count + */ + public int getPromptTokens() { + return promptTokens; + } - /** - * Returns the number of tokens used in the completion. - * - * @return completion token count - */ - public int getCompletionTokens() { - return completionTokens; - } + /** + * Returns the number of tokens used in the completion. + * + * @return completion token count + */ + public int getCompletionTokens() { + return completionTokens; + } - /** - * Returns the total number of tokens used. - * - * @return total token count - */ - public int getTotalTokens() { - return totalTokens; - } + /** + * Returns the total number of tokens used. + * + * @return total token count + */ + public int getTotalTokens() { + return totalTokens; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - TokenUsage that = (TokenUsage) o; - return promptTokens == that.promptTokens && - completionTokens == that.completionTokens && - totalTokens == that.totalTokens; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + TokenUsage that = (TokenUsage) o; + return promptTokens == that.promptTokens + && completionTokens == that.completionTokens + && totalTokens == that.totalTokens; + } - @Override - public int hashCode() { - return Objects.hash(promptTokens, completionTokens, totalTokens); - } + @Override + public int hashCode() { + return Objects.hash(promptTokens, completionTokens, totalTokens); + } - @Override - public String toString() { - return "TokenUsage{" + - "promptTokens=" + promptTokens + - ", completionTokens=" + completionTokens + - ", totalTokens=" + totalTokens + - '}'; - } + @Override + public String toString() { + return "TokenUsage{" + + "promptTokens=" + + promptTokens + + ", completionTokens=" + + completionTokens + + ", totalTokens=" + + totalTokens + + '}'; + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java b/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java index 78812b6..9cd8cbc 100644 --- a/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/UpdateMediaGovernanceConfigRequest.java @@ -18,72 +18,91 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; import java.util.Objects; /** * Request to update per-tenant media governance configuration. * - *

Fields set to {@code null} are omitted from the JSON payload, - * allowing partial updates. + *

Fields set to {@code null} are omitted from the JSON payload, allowing partial updates. */ @JsonInclude(JsonInclude.Include.NON_NULL) @JsonIgnoreProperties(ignoreUnknown = true) public class UpdateMediaGovernanceConfigRequest { - @JsonProperty("enabled") + @JsonProperty("enabled") + private Boolean enabled; + + @JsonProperty("allowed_analyzers") + private List allowedAnalyzers; + + public UpdateMediaGovernanceConfigRequest() {} + + public Boolean getEnabled() { + return enabled; + } + + public void setEnabled(Boolean enabled) { + this.enabled = enabled; + } + + public List getAllowedAnalyzers() { + return allowedAnalyzers; + } + + public void setAllowedAnalyzers(List allowedAnalyzers) { + this.allowedAnalyzers = allowedAnalyzers; + } + + public static Builder builder() { + return new Builder(); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UpdateMediaGovernanceConfigRequest that = (UpdateMediaGovernanceConfigRequest) o; + return Objects.equals(enabled, that.enabled) + && Objects.equals(allowedAnalyzers, that.allowedAnalyzers); + } + + @Override + public int hashCode() { + return Objects.hash(enabled, allowedAnalyzers); + } + + @Override + public String toString() { + return "UpdateMediaGovernanceConfigRequest{" + + "enabled=" + + enabled + + ", allowedAnalyzers=" + + allowedAnalyzers + + '}'; + } + + public static final class Builder { private Boolean enabled; - - @JsonProperty("allowed_analyzers") private List allowedAnalyzers; - public UpdateMediaGovernanceConfigRequest() {} - - public Boolean getEnabled() { return enabled; } - public void setEnabled(Boolean enabled) { this.enabled = enabled; } - - public List getAllowedAnalyzers() { return allowedAnalyzers; } - public void setAllowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; } - - public static Builder builder() { return new Builder(); } + private Builder() {} - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - UpdateMediaGovernanceConfigRequest that = (UpdateMediaGovernanceConfigRequest) o; - return Objects.equals(enabled, that.enabled) && - Objects.equals(allowedAnalyzers, that.allowedAnalyzers); + public Builder enabled(Boolean enabled) { + this.enabled = enabled; + return this; } - @Override - public int hashCode() { - return Objects.hash(enabled, allowedAnalyzers); + public Builder allowedAnalyzers(List allowedAnalyzers) { + this.allowedAnalyzers = allowedAnalyzers; + return this; } - @Override - public String toString() { - return "UpdateMediaGovernanceConfigRequest{" + - "enabled=" + enabled + - ", allowedAnalyzers=" + allowedAnalyzers + - '}'; - } - - public static final class Builder { - private Boolean enabled; - private List allowedAnalyzers; - - private Builder() {} - - public Builder enabled(Boolean enabled) { this.enabled = enabled; return this; } - public Builder allowedAnalyzers(List allowedAnalyzers) { this.allowedAnalyzers = allowedAnalyzers; return this; } - - public UpdateMediaGovernanceConfigRequest build() { - UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); - request.enabled = this.enabled; - request.allowedAnalyzers = this.allowedAnalyzers; - return request; - } + public UpdateMediaGovernanceConfigRequest build() { + UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); + request.enabled = this.enabled; + request.allowedAnalyzers = this.allowedAnalyzers; + return request; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/UpdatePlanRequest.java b/src/main/java/com/getaxonflow/sdk/types/UpdatePlanRequest.java index 517e1e6..940ae29 100644 --- a/src/main/java/com/getaxonflow/sdk/types/UpdatePlanRequest.java +++ b/src/main/java/com/getaxonflow/sdk/types/UpdatePlanRequest.java @@ -17,17 +17,17 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Request for updating a multi-agent plan. * - *

The version field is used for optimistic concurrency control. - * If the version does not match the current server version, a - * {@link com.getaxonflow.sdk.exceptions.VersionConflictException} is thrown. + *

The version field is used for optimistic concurrency control. If the version does not match + * the current server version, a {@link com.getaxonflow.sdk.exceptions.VersionConflictException} is + * thrown. * *

Example usage: + * *

{@code
  * UpdatePlanRequest request = UpdatePlanRequest.builder()
  *     .version(2)
@@ -41,126 +41,128 @@
 @JsonInclude(JsonInclude.Include.NON_NULL)
 public final class UpdatePlanRequest {
 
-    @JsonProperty("version")
-    private final int version;
-
-    @JsonProperty("execution_mode")
-    private final ExecutionMode executionMode;
-
-    @JsonProperty("domain")
-    private final String domain;
-
-    private UpdatePlanRequest(Builder builder) {
-        this.version = builder.version;
-        this.executionMode = builder.executionMode;
-        this.domain = builder.domain;
-    }
+  @JsonProperty("version")
+  private final int version;
+
+  @JsonProperty("execution_mode")
+  private final ExecutionMode executionMode;
+
+  @JsonProperty("domain")
+  private final String domain;
+
+  private UpdatePlanRequest(Builder builder) {
+    this.version = builder.version;
+    this.executionMode = builder.executionMode;
+    this.domain = builder.domain;
+  }
+
+  /**
+   * Returns the expected version for optimistic concurrency control.
+   *
+   * @return the version number
+   */
+  public int getVersion() {
+    return version;
+  }
+
+  /**
+   * Returns the new execution mode for the plan.
+   *
+   * @return the execution mode, or null if not being changed
+   */
+  public ExecutionMode getExecutionMode() {
+    return executionMode;
+  }
+
+  /**
+   * Returns the new domain for the plan.
+   *
+   * @return the domain, or null if not being changed
+   */
+  public String getDomain() {
+    return domain;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    UpdatePlanRequest that = (UpdatePlanRequest) o;
+    return version == that.version
+        && executionMode == that.executionMode
+        && Objects.equals(domain, that.domain);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(version, executionMode, domain);
+  }
+
+  @Override
+  public String toString() {
+    return "UpdatePlanRequest{"
+        + "version="
+        + version
+        + ", executionMode="
+        + executionMode
+        + ", domain='"
+        + domain
+        + '\''
+        + '}';
+  }
+
+  /** Builder for UpdatePlanRequest. */
+  public static final class Builder {
+    private int version;
+    private ExecutionMode executionMode;
+    private String domain;
+
+    private Builder() {}
 
     /**
-     * Returns the expected version for optimistic concurrency control.
+     * Sets the expected version for optimistic concurrency control.
      *
-     * @return the version number
+     * @param version the current version of the plan
+     * @return this builder
      */
-    public int getVersion() {
-        return version;
+    public Builder version(int version) {
+      this.version = version;
+      return this;
     }
 
     /**
-     * Returns the new execution mode for the plan.
+     * Sets the new execution mode for the plan.
      *
-     * @return the execution mode, or null if not being changed
+     * @param executionMode the execution mode
+     * @return this builder
      */
-    public ExecutionMode getExecutionMode() {
-        return executionMode;
+    public Builder executionMode(ExecutionMode executionMode) {
+      this.executionMode = executionMode;
+      return this;
     }
 
     /**
-     * Returns the new domain for the plan.
+     * Sets the new domain for the plan.
      *
-     * @return the domain, or null if not being changed
+     * @param domain the domain identifier
+     * @return this builder
      */
-    public String getDomain() {
-        return domain;
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        UpdatePlanRequest that = (UpdatePlanRequest) o;
-        return version == that.version &&
-               executionMode == that.executionMode &&
-               Objects.equals(domain, that.domain);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(version, executionMode, domain);
-    }
-
-    @Override
-    public String toString() {
-        return "UpdatePlanRequest{" +
-               "version=" + version +
-               ", executionMode=" + executionMode +
-               ", domain='" + domain + '\'' +
-               '}';
+    public Builder domain(String domain) {
+      this.domain = domain;
+      return this;
     }
 
     /**
-     * Builder for UpdatePlanRequest.
+     * Builds the UpdatePlanRequest.
+     *
+     * @return a new UpdatePlanRequest instance
      */
-    public static final class Builder {
-        private int version;
-        private ExecutionMode executionMode;
-        private String domain;
-
-        private Builder() {}
-
-        /**
-         * Sets the expected version for optimistic concurrency control.
-         *
-         * @param version the current version of the plan
-         * @return this builder
-         */
-        public Builder version(int version) {
-            this.version = version;
-            return this;
-        }
-
-        /**
-         * Sets the new execution mode for the plan.
-         *
-         * @param executionMode the execution mode
-         * @return this builder
-         */
-        public Builder executionMode(ExecutionMode executionMode) {
-            this.executionMode = executionMode;
-            return this;
-        }
-
-        /**
-         * Sets the new domain for the plan.
-         *
-         * @param domain the domain identifier
-         * @return this builder
-         */
-        public Builder domain(String domain) {
-            this.domain = domain;
-            return this;
-        }
-
-        /**
-         * Builds the UpdatePlanRequest.
-         *
-         * @return a new UpdatePlanRequest instance
-         */
-        public UpdatePlanRequest build() {
-            return new UpdatePlanRequest(this);
-        }
+    public UpdatePlanRequest build() {
+      return new UpdatePlanRequest(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/UpdatePlanResponse.java b/src/main/java/com/getaxonflow/sdk/types/UpdatePlanResponse.java
index 2bc6175..605c43e 100644
--- a/src/main/java/com/getaxonflow/sdk/types/UpdatePlanResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/UpdatePlanResponse.java
@@ -17,97 +17,100 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Response from updating a multi-agent plan.
- */
+/** Response from updating a multi-agent plan. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class UpdatePlanResponse {
 
-    @JsonProperty("plan_id")
-    private final String planId;
+  @JsonProperty("plan_id")
+  private final String planId;
 
-    @JsonProperty("version")
-    private final int version;
+  @JsonProperty("version")
+  private final int version;
 
-    @JsonProperty("status")
-    private final String status;
+  @JsonProperty("status")
+  private final String status;
 
-    @JsonProperty("success")
-    private final boolean success;
+  @JsonProperty("success")
+  private final boolean success;
 
-    public UpdatePlanResponse(
-            @JsonProperty("plan_id") String planId,
-            @JsonProperty("version") int version,
-            @JsonProperty("status") String status,
-            @JsonProperty("success") boolean success) {
-        this.planId = planId;
-        this.version = version;
-        this.status = status;
-        this.success = success;
-    }
+  public UpdatePlanResponse(
+      @JsonProperty("plan_id") String planId,
+      @JsonProperty("version") int version,
+      @JsonProperty("status") String status,
+      @JsonProperty("success") boolean success) {
+    this.planId = planId;
+    this.version = version;
+    this.status = status;
+    this.success = success;
+  }
 
-    /**
-     * Returns the ID of the updated plan.
-     *
-     * @return the plan ID
-     */
-    public String getPlanId() {
-        return planId;
-    }
+  /**
+   * Returns the ID of the updated plan.
+   *
+   * @return the plan ID
+   */
+  public String getPlanId() {
+    return planId;
+  }
 
-    /**
-     * Returns the new version number after the update.
-     *
-     * @return the version number
-     */
-    public int getVersion() {
-        return version;
-    }
+  /**
+   * Returns the new version number after the update.
+   *
+   * @return the version number
+   */
+  public int getVersion() {
+    return version;
+  }
 
-    /**
-     * Returns the status of the plan after the update.
-     *
-     * @return the status
-     */
-    public String getStatus() {
-        return status;
-    }
+  /**
+   * Returns the status of the plan after the update.
+   *
+   * @return the status
+   */
+  public String getStatus() {
+    return status;
+  }
 
-    /**
-     * Returns whether the update was successful.
-     *
-     * @return true if the update succeeded
-     */
-    public boolean isSuccess() {
-        return success;
-    }
+  /**
+   * Returns whether the update was successful.
+   *
+   * @return true if the update succeeded
+   */
+  public boolean isSuccess() {
+    return success;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        UpdatePlanResponse that = (UpdatePlanResponse) o;
-        return version == that.version &&
-               success == that.success &&
-               Objects.equals(planId, that.planId) &&
-               Objects.equals(status, that.status);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    UpdatePlanResponse that = (UpdatePlanResponse) o;
+    return version == that.version
+        && success == that.success
+        && Objects.equals(planId, that.planId)
+        && Objects.equals(status, that.status);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(planId, version, status, success);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(planId, version, status, success);
+  }
 
-    @Override
-    public String toString() {
-        return "UpdatePlanResponse{" +
-               "planId='" + planId + '\'' +
-               ", version=" + version +
-               ", status='" + status + '\'' +
-               ", success=" + success +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "UpdatePlanResponse{"
+        + "planId='"
+        + planId
+        + '\''
+        + ", version="
+        + version
+        + ", status='"
+        + status
+        + '\''
+        + ", success="
+        + success
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeFile.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeFile.java
index b237e24..a37fd80 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeFile.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeFile.java
@@ -17,111 +17,113 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * A code file to include in a PR.
- */
+/** A code file to include in a PR. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class CodeFile {
 
-    @JsonProperty("path")
-    private final String path;
-
-    @JsonProperty("content")
-    private final String content;
-
-    @JsonProperty("language")
-    private final String language;
-
-    @JsonProperty("action")
-    private final FileAction action;
-
-    public CodeFile(
-            @JsonProperty("path") String path,
-            @JsonProperty("content") String content,
-            @JsonProperty("language") String language,
-            @JsonProperty("action") FileAction action) {
-        this.path = Objects.requireNonNull(path, "path is required");
-        this.content = Objects.requireNonNull(content, "content is required");
-        this.language = language;
-        this.action = Objects.requireNonNull(action, "action is required");
-    }
-
-    public String getPath() {
-        return path;
-    }
-
-    public String getContent() {
-        return content;
-    }
-
-    public String getLanguage() {
-        return language;
-    }
-
-    public FileAction getAction() {
-        return action;
-    }
-
-    public static Builder builder() {
-        return new Builder();
+  @JsonProperty("path")
+  private final String path;
+
+  @JsonProperty("content")
+  private final String content;
+
+  @JsonProperty("language")
+  private final String language;
+
+  @JsonProperty("action")
+  private final FileAction action;
+
+  public CodeFile(
+      @JsonProperty("path") String path,
+      @JsonProperty("content") String content,
+      @JsonProperty("language") String language,
+      @JsonProperty("action") FileAction action) {
+    this.path = Objects.requireNonNull(path, "path is required");
+    this.content = Objects.requireNonNull(content, "content is required");
+    this.language = language;
+    this.action = Objects.requireNonNull(action, "action is required");
+  }
+
+  public String getPath() {
+    return path;
+  }
+
+  public String getContent() {
+    return content;
+  }
+
+  public String getLanguage() {
+    return language;
+  }
+
+  public FileAction getAction() {
+    return action;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String path;
+    private String content;
+    private String language;
+    private FileAction action;
+
+    public Builder path(String path) {
+      this.path = path;
+      return this;
     }
 
-    public static class Builder {
-        private String path;
-        private String content;
-        private String language;
-        private FileAction action;
-
-        public Builder path(String path) {
-            this.path = path;
-            return this;
-        }
-
-        public Builder content(String content) {
-            this.content = content;
-            return this;
-        }
-
-        public Builder language(String language) {
-            this.language = language;
-            return this;
-        }
-
-        public Builder action(FileAction action) {
-            this.action = action;
-            return this;
-        }
-
-        public CodeFile build() {
-            return new CodeFile(path, content, language, action);
-        }
+    public Builder content(String content) {
+      this.content = content;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        CodeFile codeFile = (CodeFile) o;
-        return Objects.equals(path, codeFile.path) &&
-               Objects.equals(content, codeFile.content) &&
-               Objects.equals(language, codeFile.language) &&
-               action == codeFile.action;
+    public Builder language(String language) {
+      this.language = language;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(path, content, language, action);
+    public Builder action(FileAction action) {
+      this.action = action;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "CodeFile{" +
-               "path='" + path + '\'' +
-               ", language='" + language + '\'' +
-               ", action=" + action +
-               '}';
+    public CodeFile build() {
+      return new CodeFile(path, content, language, action);
     }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CodeFile codeFile = (CodeFile) o;
+    return Objects.equals(path, codeFile.path)
+        && Objects.equals(content, codeFile.content)
+        && Objects.equals(language, codeFile.language)
+        && action == codeFile.action;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(path, content, language, action);
+  }
+
+  @Override
+  public String toString() {
+    return "CodeFile{"
+        + "path='"
+        + path
+        + '\''
+        + ", language='"
+        + language
+        + '\''
+        + ", action="
+        + action
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceMetrics.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceMetrics.java
index ecab6a5..ae74951 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceMetrics.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceMetrics.java
@@ -17,105 +17,139 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.time.Instant;
 import java.util.Objects;
 
-/**
- * Aggregated code governance metrics for a tenant.
- */
+/** Aggregated code governance metrics for a tenant. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class CodeGovernanceMetrics {
 
-    @JsonProperty("tenant_id")
-    private final String tenantId;
-
-    @JsonProperty("total_prs")
-    private final int totalPrs;
-
-    @JsonProperty("open_prs")
-    private final int openPrs;
-
-    @JsonProperty("merged_prs")
-    private final int mergedPrs;
-
-    @JsonProperty("closed_prs")
-    private final int closedPrs;
-
-    @JsonProperty("total_files")
-    private final int totalFiles;
-
-    @JsonProperty("total_secrets_detected")
-    private final int totalSecretsDetected;
-
-    @JsonProperty("total_unsafe_patterns")
-    private final int totalUnsafePatterns;
-
-    @JsonProperty("first_pr_at")
-    private final Instant firstPrAt;
-
-    @JsonProperty("last_pr_at")
-    private final Instant lastPrAt;
-
-    public CodeGovernanceMetrics(
-            @JsonProperty("tenant_id") String tenantId,
-            @JsonProperty("total_prs") int totalPrs,
-            @JsonProperty("open_prs") int openPrs,
-            @JsonProperty("merged_prs") int mergedPrs,
-            @JsonProperty("closed_prs") int closedPrs,
-            @JsonProperty("total_files") int totalFiles,
-            @JsonProperty("total_secrets_detected") int totalSecretsDetected,
-            @JsonProperty("total_unsafe_patterns") int totalUnsafePatterns,
-            @JsonProperty("first_pr_at") Instant firstPrAt,
-            @JsonProperty("last_pr_at") Instant lastPrAt) {
-        this.tenantId = tenantId;
-        this.totalPrs = totalPrs;
-        this.openPrs = openPrs;
-        this.mergedPrs = mergedPrs;
-        this.closedPrs = closedPrs;
-        this.totalFiles = totalFiles;
-        this.totalSecretsDetected = totalSecretsDetected;
-        this.totalUnsafePatterns = totalUnsafePatterns;
-        this.firstPrAt = firstPrAt;
-        this.lastPrAt = lastPrAt;
-    }
-
-    public String getTenantId() { return tenantId; }
-    public int getTotalPrs() { return totalPrs; }
-    public int getOpenPrs() { return openPrs; }
-    public int getMergedPrs() { return mergedPrs; }
-    public int getClosedPrs() { return closedPrs; }
-    public int getTotalFiles() { return totalFiles; }
-    public int getTotalSecretsDetected() { return totalSecretsDetected; }
-    public int getTotalUnsafePatterns() { return totalUnsafePatterns; }
-    public Instant getFirstPrAt() { return firstPrAt; }
-    public Instant getLastPrAt() { return lastPrAt; }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        CodeGovernanceMetrics that = (CodeGovernanceMetrics) o;
-        return totalPrs == that.totalPrs &&
-               Objects.equals(tenantId, that.tenantId);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(tenantId, totalPrs);
-    }
-
-    @Override
-    public String toString() {
-        return "CodeGovernanceMetrics{" +
-               "tenantId='" + tenantId + '\'' +
-               ", totalPrs=" + totalPrs +
-               ", openPrs=" + openPrs +
-               ", mergedPrs=" + mergedPrs +
-               ", closedPrs=" + closedPrs +
-               ", totalFiles=" + totalFiles +
-               ", totalSecretsDetected=" + totalSecretsDetected +
-               ", totalUnsafePatterns=" + totalUnsafePatterns +
-               '}';
-    }
+  @JsonProperty("tenant_id")
+  private final String tenantId;
+
+  @JsonProperty("total_prs")
+  private final int totalPrs;
+
+  @JsonProperty("open_prs")
+  private final int openPrs;
+
+  @JsonProperty("merged_prs")
+  private final int mergedPrs;
+
+  @JsonProperty("closed_prs")
+  private final int closedPrs;
+
+  @JsonProperty("total_files")
+  private final int totalFiles;
+
+  @JsonProperty("total_secrets_detected")
+  private final int totalSecretsDetected;
+
+  @JsonProperty("total_unsafe_patterns")
+  private final int totalUnsafePatterns;
+
+  @JsonProperty("first_pr_at")
+  private final Instant firstPrAt;
+
+  @JsonProperty("last_pr_at")
+  private final Instant lastPrAt;
+
+  public CodeGovernanceMetrics(
+      @JsonProperty("tenant_id") String tenantId,
+      @JsonProperty("total_prs") int totalPrs,
+      @JsonProperty("open_prs") int openPrs,
+      @JsonProperty("merged_prs") int mergedPrs,
+      @JsonProperty("closed_prs") int closedPrs,
+      @JsonProperty("total_files") int totalFiles,
+      @JsonProperty("total_secrets_detected") int totalSecretsDetected,
+      @JsonProperty("total_unsafe_patterns") int totalUnsafePatterns,
+      @JsonProperty("first_pr_at") Instant firstPrAt,
+      @JsonProperty("last_pr_at") Instant lastPrAt) {
+    this.tenantId = tenantId;
+    this.totalPrs = totalPrs;
+    this.openPrs = openPrs;
+    this.mergedPrs = mergedPrs;
+    this.closedPrs = closedPrs;
+    this.totalFiles = totalFiles;
+    this.totalSecretsDetected = totalSecretsDetected;
+    this.totalUnsafePatterns = totalUnsafePatterns;
+    this.firstPrAt = firstPrAt;
+    this.lastPrAt = lastPrAt;
+  }
+
+  public String getTenantId() {
+    return tenantId;
+  }
+
+  public int getTotalPrs() {
+    return totalPrs;
+  }
+
+  public int getOpenPrs() {
+    return openPrs;
+  }
+
+  public int getMergedPrs() {
+    return mergedPrs;
+  }
+
+  public int getClosedPrs() {
+    return closedPrs;
+  }
+
+  public int getTotalFiles() {
+    return totalFiles;
+  }
+
+  public int getTotalSecretsDetected() {
+    return totalSecretsDetected;
+  }
+
+  public int getTotalUnsafePatterns() {
+    return totalUnsafePatterns;
+  }
+
+  public Instant getFirstPrAt() {
+    return firstPrAt;
+  }
+
+  public Instant getLastPrAt() {
+    return lastPrAt;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CodeGovernanceMetrics that = (CodeGovernanceMetrics) o;
+    return totalPrs == that.totalPrs && Objects.equals(tenantId, that.tenantId);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(tenantId, totalPrs);
+  }
+
+  @Override
+  public String toString() {
+    return "CodeGovernanceMetrics{"
+        + "tenantId='"
+        + tenantId
+        + '\''
+        + ", totalPrs="
+        + totalPrs
+        + ", openPrs="
+        + openPrs
+        + ", mergedPrs="
+        + mergedPrs
+        + ", closedPrs="
+        + closedPrs
+        + ", totalFiles="
+        + totalFiles
+        + ", totalSecretsDetected="
+        + totalSecretsDetected
+        + ", totalUnsafePatterns="
+        + totalUnsafePatterns
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderRequest.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderRequest.java
index 953c27b..6a776a2 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderRequest.java
@@ -17,144 +17,147 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Request to configure a Git provider.
- */
+/** Request to configure a Git provider. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ConfigureGitProviderRequest {
 
-    @JsonProperty("type")
-    private final GitProviderType type;
-
-    @JsonProperty("token")
-    private final String token;
-
-    @JsonProperty("base_url")
-    private final String baseUrl;
-
-    @JsonProperty("app_id")
-    private final Integer appId;
-
-    @JsonProperty("installation_id")
-    private final Integer installationId;
-
-    @JsonProperty("private_key")
-    private final String privateKey;
-
-    public ConfigureGitProviderRequest(
-            @JsonProperty("type") GitProviderType type,
-            @JsonProperty("token") String token,
-            @JsonProperty("base_url") String baseUrl,
-            @JsonProperty("app_id") Integer appId,
-            @JsonProperty("installation_id") Integer installationId,
-            @JsonProperty("private_key") String privateKey) {
-        this.type = Objects.requireNonNull(type, "type is required");
-        this.token = token;
-        this.baseUrl = baseUrl;
-        this.appId = appId;
-        this.installationId = installationId;
-        this.privateKey = privateKey;
-    }
-
-    public GitProviderType getType() {
-        return type;
-    }
-
-    public String getToken() {
-        return token;
-    }
-
-    public String getBaseUrl() {
-        return baseUrl;
-    }
-
-    public Integer getAppId() {
-        return appId;
-    }
-
-    public Integer getInstallationId() {
-        return installationId;
+  @JsonProperty("type")
+  private final GitProviderType type;
+
+  @JsonProperty("token")
+  private final String token;
+
+  @JsonProperty("base_url")
+  private final String baseUrl;
+
+  @JsonProperty("app_id")
+  private final Integer appId;
+
+  @JsonProperty("installation_id")
+  private final Integer installationId;
+
+  @JsonProperty("private_key")
+  private final String privateKey;
+
+  public ConfigureGitProviderRequest(
+      @JsonProperty("type") GitProviderType type,
+      @JsonProperty("token") String token,
+      @JsonProperty("base_url") String baseUrl,
+      @JsonProperty("app_id") Integer appId,
+      @JsonProperty("installation_id") Integer installationId,
+      @JsonProperty("private_key") String privateKey) {
+    this.type = Objects.requireNonNull(type, "type is required");
+    this.token = token;
+    this.baseUrl = baseUrl;
+    this.appId = appId;
+    this.installationId = installationId;
+    this.privateKey = privateKey;
+  }
+
+  public GitProviderType getType() {
+    return type;
+  }
+
+  public String getToken() {
+    return token;
+  }
+
+  public String getBaseUrl() {
+    return baseUrl;
+  }
+
+  public Integer getAppId() {
+    return appId;
+  }
+
+  public Integer getInstallationId() {
+    return installationId;
+  }
+
+  public String getPrivateKey() {
+    return privateKey;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private GitProviderType type;
+    private String token;
+    private String baseUrl;
+    private Integer appId;
+    private Integer installationId;
+    private String privateKey;
+
+    public Builder type(GitProviderType type) {
+      this.type = type;
+      return this;
     }
 
-    public String getPrivateKey() {
-        return privateKey;
+    public Builder token(String token) {
+      this.token = token;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    public Builder baseUrl(String baseUrl) {
+      this.baseUrl = baseUrl;
+      return this;
     }
 
-    public static class Builder {
-        private GitProviderType type;
-        private String token;
-        private String baseUrl;
-        private Integer appId;
-        private Integer installationId;
-        private String privateKey;
-
-        public Builder type(GitProviderType type) {
-            this.type = type;
-            return this;
-        }
-
-        public Builder token(String token) {
-            this.token = token;
-            return this;
-        }
-
-        public Builder baseUrl(String baseUrl) {
-            this.baseUrl = baseUrl;
-            return this;
-        }
-
-        public Builder appId(Integer appId) {
-            this.appId = appId;
-            return this;
-        }
-
-        public Builder installationId(Integer installationId) {
-            this.installationId = installationId;
-            return this;
-        }
-
-        public Builder privateKey(String privateKey) {
-            this.privateKey = privateKey;
-            return this;
-        }
-
-        public ConfigureGitProviderRequest build() {
-            return new ConfigureGitProviderRequest(type, token, baseUrl, appId, installationId, privateKey);
-        }
+    public Builder appId(Integer appId) {
+      this.appId = appId;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ConfigureGitProviderRequest that = (ConfigureGitProviderRequest) o;
-        return type == that.type &&
-               Objects.equals(token, that.token) &&
-               Objects.equals(baseUrl, that.baseUrl) &&
-               Objects.equals(appId, that.appId) &&
-               Objects.equals(installationId, that.installationId) &&
-               Objects.equals(privateKey, that.privateKey);
+    public Builder installationId(Integer installationId) {
+      this.installationId = installationId;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(type, token, baseUrl, appId, installationId, privateKey);
+    public Builder privateKey(String privateKey) {
+      this.privateKey = privateKey;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "ConfigureGitProviderRequest{" +
-               "type=" + type +
-               ", baseUrl='" + baseUrl + '\'' +
-               ", appId=" + appId +
-               ", installationId=" + installationId +
-               '}';
+    public ConfigureGitProviderRequest build() {
+      return new ConfigureGitProviderRequest(
+          type, token, baseUrl, appId, installationId, privateKey);
     }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ConfigureGitProviderRequest that = (ConfigureGitProviderRequest) o;
+    return type == that.type
+        && Objects.equals(token, that.token)
+        && Objects.equals(baseUrl, that.baseUrl)
+        && Objects.equals(appId, that.appId)
+        && Objects.equals(installationId, that.installationId)
+        && Objects.equals(privateKey, that.privateKey);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(type, token, baseUrl, appId, installationId, privateKey);
+  }
+
+  @Override
+  public String toString() {
+    return "ConfigureGitProviderRequest{"
+        + "type="
+        + type
+        + ", baseUrl='"
+        + baseUrl
+        + '\''
+        + ", appId="
+        + appId
+        + ", installationId="
+        + installationId
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderResponse.java
index 2b5b3b1..284ef42 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ConfigureGitProviderResponse.java
@@ -17,54 +17,54 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Response from Git provider configuration.
- */
+/** Response from Git provider configuration. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ConfigureGitProviderResponse {
 
-    @JsonProperty("message")
-    private final String message;
+  @JsonProperty("message")
+  private final String message;
 
-    @JsonProperty("type")
-    private final String type;
+  @JsonProperty("type")
+  private final String type;
 
-    public ConfigureGitProviderResponse(
-            @JsonProperty("message") String message,
-            @JsonProperty("type") String type) {
-        this.message = message != null ? message : "";
-        this.type = type != null ? type : "";
-    }
+  public ConfigureGitProviderResponse(
+      @JsonProperty("message") String message, @JsonProperty("type") String type) {
+    this.message = message != null ? message : "";
+    this.type = type != null ? type : "";
+  }
 
-    public String getMessage() {
-        return message;
-    }
+  public String getMessage() {
+    return message;
+  }
 
-    public String getType() {
-        return type;
-    }
+  public String getType() {
+    return type;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ConfigureGitProviderResponse that = (ConfigureGitProviderResponse) o;
-        return Objects.equals(message, that.message) && Objects.equals(type, that.type);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ConfigureGitProviderResponse that = (ConfigureGitProviderResponse) o;
+    return Objects.equals(message, that.message) && Objects.equals(type, that.type);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(message, type);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(message, type);
+  }
 
-    @Override
-    public String toString() {
-        return "ConfigureGitProviderResponse{" +
-               "message='" + message + '\'' +
-               ", type='" + type + '\'' +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "ConfigureGitProviderResponse{"
+        + "message='"
+        + message
+        + '\''
+        + ", type='"
+        + type
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRRequest.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRRequest.java
index d2cb3a5..f29ec0e 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRRequest.java
@@ -17,163 +17,270 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Request to create a PR from LLM-generated code.
- */
+/** Request to create a PR from LLM-generated code. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class CreatePRRequest {
 
-    @JsonProperty("owner")
-    private final String owner;
-
-    @JsonProperty("repo")
-    private final String repo;
-
-    @JsonProperty("title")
-    private final String title;
-
-    @JsonProperty("description")
-    private final String description;
-
-    @JsonProperty("base_branch")
-    private final String baseBranch;
-
-    @JsonProperty("branch_name")
-    private final String branchName;
-
-    @JsonProperty("draft")
-    private final boolean draft;
-
-    @JsonProperty("files")
-    private final List files;
-
-    @JsonProperty("agent_request_id")
-    private final String agentRequestId;
-
-    @JsonProperty("model")
-    private final String model;
-
-    @JsonProperty("policies_checked")
-    private final List policiesChecked;
-
-    @JsonProperty("secrets_detected")
-    private final Integer secretsDetected;
-
-    @JsonProperty("unsafe_patterns")
-    private final Integer unsafePatterns;
-
-    public CreatePRRequest(
-            @JsonProperty("owner") String owner,
-            @JsonProperty("repo") String repo,
-            @JsonProperty("title") String title,
-            @JsonProperty("description") String description,
-            @JsonProperty("base_branch") String baseBranch,
-            @JsonProperty("branch_name") String branchName,
-            @JsonProperty("draft") boolean draft,
-            @JsonProperty("files") List files,
-            @JsonProperty("agent_request_id") String agentRequestId,
-            @JsonProperty("model") String model,
-            @JsonProperty("policies_checked") List policiesChecked,
-            @JsonProperty("secrets_detected") Integer secretsDetected,
-            @JsonProperty("unsafe_patterns") Integer unsafePatterns) {
-        this.owner = Objects.requireNonNull(owner, "owner is required");
-        this.repo = Objects.requireNonNull(repo, "repo is required");
-        this.title = Objects.requireNonNull(title, "title is required");
-        this.description = description;
-        this.baseBranch = baseBranch;
-        this.branchName = branchName;
-        this.draft = draft;
-        this.files = files != null ? Collections.unmodifiableList(files) : Collections.emptyList();
-        this.agentRequestId = agentRequestId;
-        this.model = model;
-        this.policiesChecked = policiesChecked != null ? Collections.unmodifiableList(policiesChecked) : null;
-        this.secretsDetected = secretsDetected;
-        this.unsafePatterns = unsafePatterns;
+  @JsonProperty("owner")
+  private final String owner;
+
+  @JsonProperty("repo")
+  private final String repo;
+
+  @JsonProperty("title")
+  private final String title;
+
+  @JsonProperty("description")
+  private final String description;
+
+  @JsonProperty("base_branch")
+  private final String baseBranch;
+
+  @JsonProperty("branch_name")
+  private final String branchName;
+
+  @JsonProperty("draft")
+  private final boolean draft;
+
+  @JsonProperty("files")
+  private final List files;
+
+  @JsonProperty("agent_request_id")
+  private final String agentRequestId;
+
+  @JsonProperty("model")
+  private final String model;
+
+  @JsonProperty("policies_checked")
+  private final List policiesChecked;
+
+  @JsonProperty("secrets_detected")
+  private final Integer secretsDetected;
+
+  @JsonProperty("unsafe_patterns")
+  private final Integer unsafePatterns;
+
+  public CreatePRRequest(
+      @JsonProperty("owner") String owner,
+      @JsonProperty("repo") String repo,
+      @JsonProperty("title") String title,
+      @JsonProperty("description") String description,
+      @JsonProperty("base_branch") String baseBranch,
+      @JsonProperty("branch_name") String branchName,
+      @JsonProperty("draft") boolean draft,
+      @JsonProperty("files") List files,
+      @JsonProperty("agent_request_id") String agentRequestId,
+      @JsonProperty("model") String model,
+      @JsonProperty("policies_checked") List policiesChecked,
+      @JsonProperty("secrets_detected") Integer secretsDetected,
+      @JsonProperty("unsafe_patterns") Integer unsafePatterns) {
+    this.owner = Objects.requireNonNull(owner, "owner is required");
+    this.repo = Objects.requireNonNull(repo, "repo is required");
+    this.title = Objects.requireNonNull(title, "title is required");
+    this.description = description;
+    this.baseBranch = baseBranch;
+    this.branchName = branchName;
+    this.draft = draft;
+    this.files = files != null ? Collections.unmodifiableList(files) : Collections.emptyList();
+    this.agentRequestId = agentRequestId;
+    this.model = model;
+    this.policiesChecked =
+        policiesChecked != null ? Collections.unmodifiableList(policiesChecked) : null;
+    this.secretsDetected = secretsDetected;
+    this.unsafePatterns = unsafePatterns;
+  }
+
+  public String getOwner() {
+    return owner;
+  }
+
+  public String getRepo() {
+    return repo;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public String getBaseBranch() {
+    return baseBranch;
+  }
+
+  public String getBranchName() {
+    return branchName;
+  }
+
+  public boolean isDraft() {
+    return draft;
+  }
+
+  public List getFiles() {
+    return files;
+  }
+
+  public String getAgentRequestId() {
+    return agentRequestId;
+  }
+
+  public String getModel() {
+    return model;
+  }
+
+  public List getPoliciesChecked() {
+    return policiesChecked;
+  }
+
+  public Integer getSecretsDetected() {
+    return secretsDetected;
+  }
+
+  public Integer getUnsafePatterns() {
+    return unsafePatterns;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private String owner;
+    private String repo;
+    private String title;
+    private String description;
+    private String baseBranch;
+    private String branchName;
+    private boolean draft;
+    private List files;
+    private String agentRequestId;
+    private String model;
+    private List policiesChecked;
+    private Integer secretsDetected;
+    private Integer unsafePatterns;
+
+    public Builder owner(String owner) {
+      this.owner = owner;
+      return this;
     }
 
-    public String getOwner() { return owner; }
-    public String getRepo() { return repo; }
-    public String getTitle() { return title; }
-    public String getDescription() { return description; }
-    public String getBaseBranch() { return baseBranch; }
-    public String getBranchName() { return branchName; }
-    public boolean isDraft() { return draft; }
-    public List getFiles() { return files; }
-    public String getAgentRequestId() { return agentRequestId; }
-    public String getModel() { return model; }
-    public List getPoliciesChecked() { return policiesChecked; }
-    public Integer getSecretsDetected() { return secretsDetected; }
-    public Integer getUnsafePatterns() { return unsafePatterns; }
-
-    public static Builder builder() {
-        return new Builder();
+    public Builder repo(String repo) {
+      this.repo = repo;
+      return this;
     }
 
-    public static class Builder {
-        private String owner;
-        private String repo;
-        private String title;
-        private String description;
-        private String baseBranch;
-        private String branchName;
-        private boolean draft;
-        private List files;
-        private String agentRequestId;
-        private String model;
-        private List policiesChecked;
-        private Integer secretsDetected;
-        private Integer unsafePatterns;
-
-        public Builder owner(String owner) { this.owner = owner; return this; }
-        public Builder repo(String repo) { this.repo = repo; return this; }
-        public Builder title(String title) { this.title = title; return this; }
-        public Builder description(String description) { this.description = description; return this; }
-        public Builder baseBranch(String baseBranch) { this.baseBranch = baseBranch; return this; }
-        public Builder branchName(String branchName) { this.branchName = branchName; return this; }
-        public Builder draft(boolean draft) { this.draft = draft; return this; }
-        public Builder files(List files) { this.files = files; return this; }
-        public Builder agentRequestId(String agentRequestId) { this.agentRequestId = agentRequestId; return this; }
-        public Builder model(String model) { this.model = model; return this; }
-        public Builder policiesChecked(List policiesChecked) { this.policiesChecked = policiesChecked; return this; }
-        public Builder secretsDetected(Integer secretsDetected) { this.secretsDetected = secretsDetected; return this; }
-        public Builder unsafePatterns(Integer unsafePatterns) { this.unsafePatterns = unsafePatterns; return this; }
-
-        public CreatePRRequest build() {
-            return new CreatePRRequest(owner, repo, title, description, baseBranch, branchName,
-                    draft, files, agentRequestId, model, policiesChecked, secretsDetected, unsafePatterns);
-        }
+    public Builder title(String title) {
+      this.title = title;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        CreatePRRequest that = (CreatePRRequest) o;
-        return draft == that.draft &&
-               Objects.equals(owner, that.owner) &&
-               Objects.equals(repo, that.repo) &&
-               Objects.equals(title, that.title) &&
-               Objects.equals(files, that.files);
+    public Builder description(String description) {
+      this.description = description;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(owner, repo, title, draft, files);
+    public Builder baseBranch(String baseBranch) {
+      this.baseBranch = baseBranch;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "CreatePRRequest{" +
-               "owner='" + owner + '\'' +
-               ", repo='" + repo + '\'' +
-               ", title='" + title + '\'' +
-               ", draft=" + draft +
-               ", filesCount=" + (files != null ? files.size() : 0) +
-               '}';
+    public Builder branchName(String branchName) {
+      this.branchName = branchName;
+      return this;
     }
+
+    public Builder draft(boolean draft) {
+      this.draft = draft;
+      return this;
+    }
+
+    public Builder files(List files) {
+      this.files = files;
+      return this;
+    }
+
+    public Builder agentRequestId(String agentRequestId) {
+      this.agentRequestId = agentRequestId;
+      return this;
+    }
+
+    public Builder model(String model) {
+      this.model = model;
+      return this;
+    }
+
+    public Builder policiesChecked(List policiesChecked) {
+      this.policiesChecked = policiesChecked;
+      return this;
+    }
+
+    public Builder secretsDetected(Integer secretsDetected) {
+      this.secretsDetected = secretsDetected;
+      return this;
+    }
+
+    public Builder unsafePatterns(Integer unsafePatterns) {
+      this.unsafePatterns = unsafePatterns;
+      return this;
+    }
+
+    public CreatePRRequest build() {
+      return new CreatePRRequest(
+          owner,
+          repo,
+          title,
+          description,
+          baseBranch,
+          branchName,
+          draft,
+          files,
+          agentRequestId,
+          model,
+          policiesChecked,
+          secretsDetected,
+          unsafePatterns);
+    }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CreatePRRequest that = (CreatePRRequest) o;
+    return draft == that.draft
+        && Objects.equals(owner, that.owner)
+        && Objects.equals(repo, that.repo)
+        && Objects.equals(title, that.title)
+        && Objects.equals(files, that.files);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(owner, repo, title, draft, files);
+  }
+
+  @Override
+  public String toString() {
+    return "CreatePRRequest{"
+        + "owner='"
+        + owner
+        + '\''
+        + ", repo='"
+        + repo
+        + '\''
+        + ", title='"
+        + title
+        + '\''
+        + ", draft="
+        + draft
+        + ", filesCount="
+        + (files != null ? files.size() : 0)
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRResponse.java
index c82d0bd..3de25b7 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/CreatePRResponse.java
@@ -17,78 +17,99 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.time.Instant;
 import java.util.Objects;
 
-/**
- * Response from PR creation.
- */
+/** Response from PR creation. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class CreatePRResponse {
 
-    @JsonProperty("pr_id")
-    private final String prId;
-
-    @JsonProperty("pr_number")
-    private final int prNumber;
-
-    @JsonProperty("pr_url")
-    private final String prUrl;
-
-    @JsonProperty("state")
-    private final String state;
-
-    @JsonProperty("head_branch")
-    private final String headBranch;
-
-    @JsonProperty("created_at")
-    private final Instant createdAt;
-
-    public CreatePRResponse(
-            @JsonProperty("pr_id") String prId,
-            @JsonProperty("pr_number") int prNumber,
-            @JsonProperty("pr_url") String prUrl,
-            @JsonProperty("state") String state,
-            @JsonProperty("head_branch") String headBranch,
-            @JsonProperty("created_at") Instant createdAt) {
-        this.prId = prId;
-        this.prNumber = prNumber;
-        this.prUrl = prUrl;
-        this.state = state;
-        this.headBranch = headBranch;
-        this.createdAt = createdAt;
-    }
-
-    public String getPrId() { return prId; }
-    public int getPrNumber() { return prNumber; }
-    public String getPrUrl() { return prUrl; }
-    public String getState() { return state; }
-    public String getHeadBranch() { return headBranch; }
-    public Instant getCreatedAt() { return createdAt; }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        CreatePRResponse that = (CreatePRResponse) o;
-        return prNumber == that.prNumber &&
-               Objects.equals(prId, that.prId) &&
-               Objects.equals(prUrl, that.prUrl);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(prId, prNumber, prUrl);
-    }
-
-    @Override
-    public String toString() {
-        return "CreatePRResponse{" +
-               "prId='" + prId + '\'' +
-               ", prNumber=" + prNumber +
-               ", prUrl='" + prUrl + '\'' +
-               ", state='" + state + '\'' +
-               '}';
-    }
+  @JsonProperty("pr_id")
+  private final String prId;
+
+  @JsonProperty("pr_number")
+  private final int prNumber;
+
+  @JsonProperty("pr_url")
+  private final String prUrl;
+
+  @JsonProperty("state")
+  private final String state;
+
+  @JsonProperty("head_branch")
+  private final String headBranch;
+
+  @JsonProperty("created_at")
+  private final Instant createdAt;
+
+  public CreatePRResponse(
+      @JsonProperty("pr_id") String prId,
+      @JsonProperty("pr_number") int prNumber,
+      @JsonProperty("pr_url") String prUrl,
+      @JsonProperty("state") String state,
+      @JsonProperty("head_branch") String headBranch,
+      @JsonProperty("created_at") Instant createdAt) {
+    this.prId = prId;
+    this.prNumber = prNumber;
+    this.prUrl = prUrl;
+    this.state = state;
+    this.headBranch = headBranch;
+    this.createdAt = createdAt;
+  }
+
+  public String getPrId() {
+    return prId;
+  }
+
+  public int getPrNumber() {
+    return prNumber;
+  }
+
+  public String getPrUrl() {
+    return prUrl;
+  }
+
+  public String getState() {
+    return state;
+  }
+
+  public String getHeadBranch() {
+    return headBranch;
+  }
+
+  public Instant getCreatedAt() {
+    return createdAt;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CreatePRResponse that = (CreatePRResponse) o;
+    return prNumber == that.prNumber
+        && Objects.equals(prId, that.prId)
+        && Objects.equals(prUrl, that.prUrl);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(prId, prNumber, prUrl);
+  }
+
+  @Override
+  public String toString() {
+    return "CreatePRResponse{"
+        + "prId='"
+        + prId
+        + '\''
+        + ", prNumber="
+        + prNumber
+        + ", prUrl='"
+        + prUrl
+        + '\''
+        + ", state='"
+        + state
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportOptions.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportOptions.java
index e18b610..e9819bf 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportOptions.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportOptions.java
@@ -17,50 +17,48 @@
 
 import java.time.Instant;
 
-/**
- * Options for exporting code governance data.
- */
+/** Options for exporting code governance data. */
 public class ExportOptions {
-    private String format = "json";
-    private Instant startDate;
-    private Instant endDate;
-    private String state;
+  private String format = "json";
+  private Instant startDate;
+  private Instant endDate;
+  private String state;
 
-    public ExportOptions() {}
+  public ExportOptions() {}
 
-    public String getFormat() {
-        return format;
-    }
+  public String getFormat() {
+    return format;
+  }
 
-    public ExportOptions setFormat(String format) {
-        this.format = format;
-        return this;
-    }
+  public ExportOptions setFormat(String format) {
+    this.format = format;
+    return this;
+  }
 
-    public Instant getStartDate() {
-        return startDate;
-    }
+  public Instant getStartDate() {
+    return startDate;
+  }
 
-    public ExportOptions setStartDate(Instant startDate) {
-        this.startDate = startDate;
-        return this;
-    }
+  public ExportOptions setStartDate(Instant startDate) {
+    this.startDate = startDate;
+    return this;
+  }
 
-    public Instant getEndDate() {
-        return endDate;
-    }
+  public Instant getEndDate() {
+    return endDate;
+  }
 
-    public ExportOptions setEndDate(Instant endDate) {
-        this.endDate = endDate;
-        return this;
-    }
+  public ExportOptions setEndDate(Instant endDate) {
+    this.endDate = endDate;
+    return this;
+  }
 
-    public String getState() {
-        return state;
-    }
+  public String getState() {
+    return state;
+  }
 
-    public ExportOptions setState(String state) {
-        this.state = state;
-        return this;
-    }
+  public ExportOptions setState(String state) {
+    this.state = state;
+    return this;
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportResponse.java
index edef6bb..2725a5c 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ExportResponse.java
@@ -17,58 +17,60 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Response from exporting code governance data.
- */
+/** Response from exporting code governance data. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ExportResponse {
 
-    @JsonProperty("records")
-    private final List records;
+  @JsonProperty("records")
+  private final List records;
+
+  @JsonProperty("count")
+  private final int count;
+
+  @JsonProperty("exported_at")
+  private final String exportedAt;
 
-    @JsonProperty("count")
-    private final int count;
+  public ExportResponse(
+      @JsonProperty("records") List records,
+      @JsonProperty("count") int count,
+      @JsonProperty("exported_at") String exportedAt) {
+    this.records =
+        records != null ? Collections.unmodifiableList(records) : Collections.emptyList();
+    this.count = count;
+    this.exportedAt = exportedAt;
+  }
 
-    @JsonProperty("exported_at")
-    private final String exportedAt;
+  public List getRecords() {
+    return records;
+  }
 
-    public ExportResponse(
-            @JsonProperty("records") List records,
-            @JsonProperty("count") int count,
-            @JsonProperty("exported_at") String exportedAt) {
-        this.records = records != null ? Collections.unmodifiableList(records) : Collections.emptyList();
-        this.count = count;
-        this.exportedAt = exportedAt;
-    }
+  public int getCount() {
+    return count;
+  }
 
-    public List getRecords() { return records; }
-    public int getCount() { return count; }
-    public String getExportedAt() { return exportedAt; }
+  public String getExportedAt() {
+    return exportedAt;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ExportResponse that = (ExportResponse) o;
-        return count == that.count &&
-               Objects.equals(exportedAt, that.exportedAt);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ExportResponse that = (ExportResponse) o;
+    return count == that.count && Objects.equals(exportedAt, that.exportedAt);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(count, exportedAt);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(count, exportedAt);
+  }
 
-    @Override
-    public String toString() {
-        return "ExportResponse{" +
-               "count=" + count +
-               ", exportedAt='" + exportedAt + '\'' +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "ExportResponse{" + "count=" + count + ", exportedAt='" + exportedAt + '\'' + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/FileAction.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/FileAction.java
index 411335a..7ff6880 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/FileAction.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/FileAction.java
@@ -17,31 +17,29 @@
 
 import com.fasterxml.jackson.annotation.JsonValue;
 
-/**
- * File action for PR files.
- */
+/** File action for PR files. */
 public enum FileAction {
-    CREATE("create"),
-    UPDATE("update"),
-    DELETE("delete");
+  CREATE("create"),
+  UPDATE("update"),
+  DELETE("delete");
 
-    private final String value;
+  private final String value;
 
-    FileAction(String value) {
-        this.value = value;
-    }
+  FileAction(String value) {
+    this.value = value;
+  }
 
-    @JsonValue
-    public String getValue() {
-        return value;
-    }
+  @JsonValue
+  public String getValue() {
+    return value;
+  }
 
-    public static FileAction fromValue(String value) {
-        for (FileAction action : values()) {
-            if (action.value.equalsIgnoreCase(value)) {
-                return action;
-            }
-        }
-        throw new IllegalArgumentException("Unknown file action: " + value);
+  public static FileAction fromValue(String value) {
+    for (FileAction action : values()) {
+      if (action.value.equalsIgnoreCase(value)) {
+        return action;
+      }
     }
+    throw new IllegalArgumentException("Unknown file action: " + value);
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderInfo.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderInfo.java
index 901b787..022cac7 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderInfo.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderInfo.java
@@ -17,41 +17,38 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Basic info about a configured Git provider.
- */
+/** Basic info about a configured Git provider. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class GitProviderInfo {
 
-    @JsonProperty("type")
-    private final GitProviderType type;
-
-    public GitProviderInfo(@JsonProperty("type") GitProviderType type) {
-        this.type = type;
-    }
-
-    public GitProviderType getType() {
-        return type;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        GitProviderInfo that = (GitProviderInfo) o;
-        return type == that.type;
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(type);
-    }
-
-    @Override
-    public String toString() {
-        return "GitProviderInfo{type=" + type + '}';
-    }
+  @JsonProperty("type")
+  private final GitProviderType type;
+
+  public GitProviderInfo(@JsonProperty("type") GitProviderType type) {
+    this.type = type;
+  }
+
+  public GitProviderType getType() {
+    return type;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    GitProviderInfo that = (GitProviderInfo) o;
+    return type == that.type;
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(type);
+  }
+
+  @Override
+  public String toString() {
+    return "GitProviderInfo{type=" + type + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderType.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderType.java
index ded2e1a..a8d08c9 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderType.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/GitProviderType.java
@@ -17,31 +17,29 @@
 
 import com.fasterxml.jackson.annotation.JsonValue;
 
-/**
- * Supported Git providers for code governance.
- */
+/** Supported Git providers for code governance. */
 public enum GitProviderType {
-    GITHUB("github"),
-    GITLAB("gitlab"),
-    BITBUCKET("bitbucket");
+  GITHUB("github"),
+  GITLAB("gitlab"),
+  BITBUCKET("bitbucket");
 
-    private final String value;
+  private final String value;
 
-    GitProviderType(String value) {
-        this.value = value;
-    }
+  GitProviderType(String value) {
+    this.value = value;
+  }
 
-    @JsonValue
-    public String getValue() {
-        return value;
-    }
+  @JsonValue
+  public String getValue() {
+    return value;
+  }
 
-    public static GitProviderType fromValue(String value) {
-        for (GitProviderType type : values()) {
-            if (type.value.equalsIgnoreCase(value)) {
-                return type;
-            }
-        }
-        throw new IllegalArgumentException("Unknown Git provider type: " + value);
+  public static GitProviderType fromValue(String value) {
+    for (GitProviderType type : values()) {
+      if (type.value.equalsIgnoreCase(value)) {
+        return type;
+      }
     }
+    throw new IllegalArgumentException("Unknown Git provider type: " + value);
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListGitProvidersResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListGitProvidersResponse.java
index ba4bb5b..ffa39f9 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListGitProvidersResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListGitProvidersResponse.java
@@ -17,56 +17,51 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Response listing configured Git providers.
- */
+/** Response listing configured Git providers. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ListGitProvidersResponse {
 
-    @JsonProperty("providers")
-    private final List providers;
+  @JsonProperty("providers")
+  private final List providers;
 
-    @JsonProperty("count")
-    private final int count;
+  @JsonProperty("count")
+  private final int count;
 
-    public ListGitProvidersResponse(
-            @JsonProperty("providers") List providers,
-            @JsonProperty("count") int count) {
-        this.providers = providers != null ? Collections.unmodifiableList(providers) : Collections.emptyList();
-        this.count = count;
-    }
+  public ListGitProvidersResponse(
+      @JsonProperty("providers") List providers,
+      @JsonProperty("count") int count) {
+    this.providers =
+        providers != null ? Collections.unmodifiableList(providers) : Collections.emptyList();
+    this.count = count;
+  }
 
-    public List getProviders() {
-        return providers;
-    }
+  public List getProviders() {
+    return providers;
+  }
 
-    public int getCount() {
-        return count;
-    }
+  public int getCount() {
+    return count;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ListGitProvidersResponse that = (ListGitProvidersResponse) o;
-        return count == that.count && Objects.equals(providers, that.providers);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ListGitProvidersResponse that = (ListGitProvidersResponse) o;
+    return count == that.count && Objects.equals(providers, that.providers);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(providers, count);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(providers, count);
+  }
 
-    @Override
-    public String toString() {
-        return "ListGitProvidersResponse{" +
-               "providers=" + providers +
-               ", count=" + count +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "ListGitProvidersResponse{" + "providers=" + providers + ", count=" + count + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsOptions.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsOptions.java
index c5a9628..bd4298f 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsOptions.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsOptions.java
@@ -17,64 +17,85 @@
 
 import java.util.Objects;
 
-/**
- * Options for listing PRs.
- */
+/** Options for listing PRs. */
 public final class ListPRsOptions {
 
-    private final Integer limit;
-    private final Integer offset;
-    private final String state;
+  private final Integer limit;
+  private final Integer offset;
+  private final String state;
 
-    private ListPRsOptions(Integer limit, Integer offset, String state) {
-        this.limit = limit;
-        this.offset = offset;
-        this.state = state;
-    }
+  private ListPRsOptions(Integer limit, Integer offset, String state) {
+    this.limit = limit;
+    this.offset = offset;
+    this.state = state;
+  }
 
-    public Integer getLimit() { return limit; }
-    public Integer getOffset() { return offset; }
-    public String getState() { return state; }
+  public Integer getLimit() {
+    return limit;
+  }
 
-    public static Builder builder() {
-        return new Builder();
-    }
+  public Integer getOffset() {
+    return offset;
+  }
+
+  public String getState() {
+    return state;
+  }
 
-    public static class Builder {
-        private Integer limit;
-        private Integer offset;
-        private String state;
+  public static Builder builder() {
+    return new Builder();
+  }
 
-        public Builder limit(Integer limit) { this.limit = limit; return this; }
-        public Builder offset(Integer offset) { this.offset = offset; return this; }
-        public Builder state(String state) { this.state = state; return this; }
+  public static class Builder {
+    private Integer limit;
+    private Integer offset;
+    private String state;
 
-        public ListPRsOptions build() {
-            return new ListPRsOptions(limit, offset, state);
-        }
+    public Builder limit(Integer limit) {
+      this.limit = limit;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ListPRsOptions that = (ListPRsOptions) o;
-        return Objects.equals(limit, that.limit) &&
-               Objects.equals(offset, that.offset) &&
-               Objects.equals(state, that.state);
+    public Builder offset(Integer offset) {
+      this.offset = offset;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(limit, offset, state);
+    public Builder state(String state) {
+      this.state = state;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "ListPRsOptions{" +
-               "limit=" + limit +
-               ", offset=" + offset +
-               ", state='" + state + '\'' +
-               '}';
+    public ListPRsOptions build() {
+      return new ListPRsOptions(limit, offset, state);
     }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ListPRsOptions that = (ListPRsOptions) o;
+    return Objects.equals(limit, that.limit)
+        && Objects.equals(offset, that.offset)
+        && Objects.equals(state, that.state);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(limit, offset, state);
+  }
+
+  @Override
+  public String toString() {
+    return "ListPRsOptions{"
+        + "limit="
+        + limit
+        + ", offset="
+        + offset
+        + ", state='"
+        + state
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsResponse.java
index 9e56333..0735bd4 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ListPRsResponse.java
@@ -17,56 +17,49 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Collections;
 import java.util.List;
 import java.util.Objects;
 
-/**
- * Response listing PRs.
- */
+/** Response listing PRs. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ListPRsResponse {
 
-    @JsonProperty("prs")
-    private final List prs;
+  @JsonProperty("prs")
+  private final List prs;
 
-    @JsonProperty("count")
-    private final int count;
+  @JsonProperty("count")
+  private final int count;
 
-    public ListPRsResponse(
-            @JsonProperty("prs") List prs,
-            @JsonProperty("count") int count) {
-        this.prs = prs != null ? Collections.unmodifiableList(prs) : Collections.emptyList();
-        this.count = count;
-    }
+  public ListPRsResponse(
+      @JsonProperty("prs") List prs, @JsonProperty("count") int count) {
+    this.prs = prs != null ? Collections.unmodifiableList(prs) : Collections.emptyList();
+    this.count = count;
+  }
 
-    public List getPrs() {
-        return prs;
-    }
+  public List getPrs() {
+    return prs;
+  }
 
-    public int getCount() {
-        return count;
-    }
+  public int getCount() {
+    return count;
+  }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ListPRsResponse that = (ListPRsResponse) o;
-        return count == that.count && Objects.equals(prs, that.prs);
-    }
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ListPRsResponse that = (ListPRsResponse) o;
+    return count == that.count && Objects.equals(prs, that.prs);
+  }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(prs, count);
-    }
+  @Override
+  public int hashCode() {
+    return Objects.hash(prs, count);
+  }
 
-    @Override
-    public String toString() {
-        return "ListPRsResponse{" +
-               "prs=" + prs +
-               ", count=" + count +
-               '}';
-    }
+  @Override
+  public String toString() {
+    return "ListPRsResponse{" + "prs=" + prs + ", count=" + count + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/PRRecord.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/PRRecord.java
index 8557bb4..6ca37e2 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/PRRecord.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/PRRecord.java
@@ -17,141 +17,196 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.time.Instant;
 import java.util.Objects;
 
-/**
- * A PR record in the system.
- */
+/** A PR record in the system. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class PRRecord {
 
-    @JsonProperty("id")
-    private final String id;
-
-    @JsonProperty("pr_number")
-    private final int prNumber;
-
-    @JsonProperty("pr_url")
-    private final String prUrl;
-
-    @JsonProperty("title")
-    private final String title;
-
-    @JsonProperty("state")
-    private final String state;
-
-    @JsonProperty("owner")
-    private final String owner;
-
-    @JsonProperty("repo")
-    private final String repo;
-
-    @JsonProperty("head_branch")
-    private final String headBranch;
-
-    @JsonProperty("base_branch")
-    private final String baseBranch;
-
-    @JsonProperty("files_count")
-    private final int filesCount;
-
-    @JsonProperty("secrets_detected")
-    private final int secretsDetected;
-
-    @JsonProperty("unsafe_patterns")
-    private final int unsafePatterns;
-
-    @JsonProperty("created_at")
-    private final Instant createdAt;
-
-    @JsonProperty("closed_at")
-    private final Instant closedAt;
-
-    @JsonProperty("created_by")
-    private final String createdBy;
-
-    @JsonProperty("provider_type")
-    private final String providerType;
-
-    public PRRecord(
-            @JsonProperty("id") String id,
-            @JsonProperty("pr_number") int prNumber,
-            @JsonProperty("pr_url") String prUrl,
-            @JsonProperty("title") String title,
-            @JsonProperty("state") String state,
-            @JsonProperty("owner") String owner,
-            @JsonProperty("repo") String repo,
-            @JsonProperty("head_branch") String headBranch,
-            @JsonProperty("base_branch") String baseBranch,
-            @JsonProperty("files_count") int filesCount,
-            @JsonProperty("secrets_detected") int secretsDetected,
-            @JsonProperty("unsafe_patterns") int unsafePatterns,
-            @JsonProperty("created_at") Instant createdAt,
-            @JsonProperty("closed_at") Instant closedAt,
-            @JsonProperty("created_by") String createdBy,
-            @JsonProperty("provider_type") String providerType) {
-        this.id = id;
-        this.prNumber = prNumber;
-        this.prUrl = prUrl;
-        this.title = title;
-        this.state = state;
-        this.owner = owner;
-        this.repo = repo;
-        this.headBranch = headBranch;
-        this.baseBranch = baseBranch;
-        this.filesCount = filesCount;
-        this.secretsDetected = secretsDetected;
-        this.unsafePatterns = unsafePatterns;
-        this.createdAt = createdAt;
-        this.closedAt = closedAt;
-        this.createdBy = createdBy;
-        this.providerType = providerType;
-    }
-
-    public String getId() { return id; }
-    public int getPrNumber() { return prNumber; }
-    public String getPrUrl() { return prUrl; }
-    public String getTitle() { return title; }
-    public String getState() { return state; }
-    public String getOwner() { return owner; }
-    public String getRepo() { return repo; }
-    public String getHeadBranch() { return headBranch; }
-    public String getBaseBranch() { return baseBranch; }
-    public int getFilesCount() { return filesCount; }
-    public int getSecretsDetected() { return secretsDetected; }
-    public int getUnsafePatterns() { return unsafePatterns; }
-    public Instant getCreatedAt() { return createdAt; }
-    public Instant getClosedAt() { return closedAt; }
-    public String getCreatedBy() { return createdBy; }
-    public String getProviderType() { return providerType; }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        PRRecord prRecord = (PRRecord) o;
-        return prNumber == prRecord.prNumber &&
-               Objects.equals(id, prRecord.id) &&
-               Objects.equals(owner, prRecord.owner) &&
-               Objects.equals(repo, prRecord.repo);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(id, prNumber, owner, repo);
-    }
-
-    @Override
-    public String toString() {
-        return "PRRecord{" +
-               "id='" + id + '\'' +
-               ", prNumber=" + prNumber +
-               ", title='" + title + '\'' +
-               ", state='" + state + '\'' +
-               ", owner='" + owner + '\'' +
-               ", repo='" + repo + '\'' +
-               '}';
-    }
+  @JsonProperty("id")
+  private final String id;
+
+  @JsonProperty("pr_number")
+  private final int prNumber;
+
+  @JsonProperty("pr_url")
+  private final String prUrl;
+
+  @JsonProperty("title")
+  private final String title;
+
+  @JsonProperty("state")
+  private final String state;
+
+  @JsonProperty("owner")
+  private final String owner;
+
+  @JsonProperty("repo")
+  private final String repo;
+
+  @JsonProperty("head_branch")
+  private final String headBranch;
+
+  @JsonProperty("base_branch")
+  private final String baseBranch;
+
+  @JsonProperty("files_count")
+  private final int filesCount;
+
+  @JsonProperty("secrets_detected")
+  private final int secretsDetected;
+
+  @JsonProperty("unsafe_patterns")
+  private final int unsafePatterns;
+
+  @JsonProperty("created_at")
+  private final Instant createdAt;
+
+  @JsonProperty("closed_at")
+  private final Instant closedAt;
+
+  @JsonProperty("created_by")
+  private final String createdBy;
+
+  @JsonProperty("provider_type")
+  private final String providerType;
+
+  public PRRecord(
+      @JsonProperty("id") String id,
+      @JsonProperty("pr_number") int prNumber,
+      @JsonProperty("pr_url") String prUrl,
+      @JsonProperty("title") String title,
+      @JsonProperty("state") String state,
+      @JsonProperty("owner") String owner,
+      @JsonProperty("repo") String repo,
+      @JsonProperty("head_branch") String headBranch,
+      @JsonProperty("base_branch") String baseBranch,
+      @JsonProperty("files_count") int filesCount,
+      @JsonProperty("secrets_detected") int secretsDetected,
+      @JsonProperty("unsafe_patterns") int unsafePatterns,
+      @JsonProperty("created_at") Instant createdAt,
+      @JsonProperty("closed_at") Instant closedAt,
+      @JsonProperty("created_by") String createdBy,
+      @JsonProperty("provider_type") String providerType) {
+    this.id = id;
+    this.prNumber = prNumber;
+    this.prUrl = prUrl;
+    this.title = title;
+    this.state = state;
+    this.owner = owner;
+    this.repo = repo;
+    this.headBranch = headBranch;
+    this.baseBranch = baseBranch;
+    this.filesCount = filesCount;
+    this.secretsDetected = secretsDetected;
+    this.unsafePatterns = unsafePatterns;
+    this.createdAt = createdAt;
+    this.closedAt = closedAt;
+    this.createdBy = createdBy;
+    this.providerType = providerType;
+  }
+
+  public String getId() {
+    return id;
+  }
+
+  public int getPrNumber() {
+    return prNumber;
+  }
+
+  public String getPrUrl() {
+    return prUrl;
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  public String getState() {
+    return state;
+  }
+
+  public String getOwner() {
+    return owner;
+  }
+
+  public String getRepo() {
+    return repo;
+  }
+
+  public String getHeadBranch() {
+    return headBranch;
+  }
+
+  public String getBaseBranch() {
+    return baseBranch;
+  }
+
+  public int getFilesCount() {
+    return filesCount;
+  }
+
+  public int getSecretsDetected() {
+    return secretsDetected;
+  }
+
+  public int getUnsafePatterns() {
+    return unsafePatterns;
+  }
+
+  public Instant getCreatedAt() {
+    return createdAt;
+  }
+
+  public Instant getClosedAt() {
+    return closedAt;
+  }
+
+  public String getCreatedBy() {
+    return createdBy;
+  }
+
+  public String getProviderType() {
+    return providerType;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    PRRecord prRecord = (PRRecord) o;
+    return prNumber == prRecord.prNumber
+        && Objects.equals(id, prRecord.id)
+        && Objects.equals(owner, prRecord.owner)
+        && Objects.equals(repo, prRecord.repo);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(id, prNumber, owner, repo);
+  }
+
+  @Override
+  public String toString() {
+    return "PRRecord{"
+        + "id='"
+        + id
+        + '\''
+        + ", prNumber="
+        + prNumber
+        + ", title='"
+        + title
+        + '\''
+        + ", state='"
+        + state
+        + '\''
+        + ", owner='"
+        + owner
+        + '\''
+        + ", repo='"
+        + repo
+        + '\''
+        + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderRequest.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderRequest.java
index 85903dc..51b74f2 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderRequest.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderRequest.java
@@ -17,142 +17,137 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Request to validate Git provider credentials.
- */
+/** Request to validate Git provider credentials. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ValidateGitProviderRequest {
 
-    @JsonProperty("type")
-    private final GitProviderType type;
-
-    @JsonProperty("token")
-    private final String token;
-
-    @JsonProperty("base_url")
-    private final String baseUrl;
-
-    @JsonProperty("app_id")
-    private final Integer appId;
-
-    @JsonProperty("installation_id")
-    private final Integer installationId;
-
-    @JsonProperty("private_key")
-    private final String privateKey;
-
-    public ValidateGitProviderRequest(
-            @JsonProperty("type") GitProviderType type,
-            @JsonProperty("token") String token,
-            @JsonProperty("base_url") String baseUrl,
-            @JsonProperty("app_id") Integer appId,
-            @JsonProperty("installation_id") Integer installationId,
-            @JsonProperty("private_key") String privateKey) {
-        this.type = Objects.requireNonNull(type, "type is required");
-        this.token = token;
-        this.baseUrl = baseUrl;
-        this.appId = appId;
-        this.installationId = installationId;
-        this.privateKey = privateKey;
-    }
-
-    public GitProviderType getType() {
-        return type;
-    }
-
-    public String getToken() {
-        return token;
-    }
-
-    public String getBaseUrl() {
-        return baseUrl;
-    }
-
-    public Integer getAppId() {
-        return appId;
-    }
-
-    public Integer getInstallationId() {
-        return installationId;
+  @JsonProperty("type")
+  private final GitProviderType type;
+
+  @JsonProperty("token")
+  private final String token;
+
+  @JsonProperty("base_url")
+  private final String baseUrl;
+
+  @JsonProperty("app_id")
+  private final Integer appId;
+
+  @JsonProperty("installation_id")
+  private final Integer installationId;
+
+  @JsonProperty("private_key")
+  private final String privateKey;
+
+  public ValidateGitProviderRequest(
+      @JsonProperty("type") GitProviderType type,
+      @JsonProperty("token") String token,
+      @JsonProperty("base_url") String baseUrl,
+      @JsonProperty("app_id") Integer appId,
+      @JsonProperty("installation_id") Integer installationId,
+      @JsonProperty("private_key") String privateKey) {
+    this.type = Objects.requireNonNull(type, "type is required");
+    this.token = token;
+    this.baseUrl = baseUrl;
+    this.appId = appId;
+    this.installationId = installationId;
+    this.privateKey = privateKey;
+  }
+
+  public GitProviderType getType() {
+    return type;
+  }
+
+  public String getToken() {
+    return token;
+  }
+
+  public String getBaseUrl() {
+    return baseUrl;
+  }
+
+  public Integer getAppId() {
+    return appId;
+  }
+
+  public Integer getInstallationId() {
+    return installationId;
+  }
+
+  public String getPrivateKey() {
+    return privateKey;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  public static class Builder {
+    private GitProviderType type;
+    private String token;
+    private String baseUrl;
+    private Integer appId;
+    private Integer installationId;
+    private String privateKey;
+
+    public Builder type(GitProviderType type) {
+      this.type = type;
+      return this;
     }
 
-    public String getPrivateKey() {
-        return privateKey;
+    public Builder token(String token) {
+      this.token = token;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    public Builder baseUrl(String baseUrl) {
+      this.baseUrl = baseUrl;
+      return this;
     }
 
-    public static class Builder {
-        private GitProviderType type;
-        private String token;
-        private String baseUrl;
-        private Integer appId;
-        private Integer installationId;
-        private String privateKey;
-
-        public Builder type(GitProviderType type) {
-            this.type = type;
-            return this;
-        }
-
-        public Builder token(String token) {
-            this.token = token;
-            return this;
-        }
-
-        public Builder baseUrl(String baseUrl) {
-            this.baseUrl = baseUrl;
-            return this;
-        }
-
-        public Builder appId(Integer appId) {
-            this.appId = appId;
-            return this;
-        }
-
-        public Builder installationId(Integer installationId) {
-            this.installationId = installationId;
-            return this;
-        }
-
-        public Builder privateKey(String privateKey) {
-            this.privateKey = privateKey;
-            return this;
-        }
-
-        public ValidateGitProviderRequest build() {
-            return new ValidateGitProviderRequest(type, token, baseUrl, appId, installationId, privateKey);
-        }
+    public Builder appId(Integer appId) {
+      this.appId = appId;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ValidateGitProviderRequest that = (ValidateGitProviderRequest) o;
-        return type == that.type &&
-               Objects.equals(token, that.token) &&
-               Objects.equals(baseUrl, that.baseUrl) &&
-               Objects.equals(appId, that.appId) &&
-               Objects.equals(installationId, that.installationId) &&
-               Objects.equals(privateKey, that.privateKey);
+    public Builder installationId(Integer installationId) {
+      this.installationId = installationId;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(type, token, baseUrl, appId, installationId, privateKey);
+    public Builder privateKey(String privateKey) {
+      this.privateKey = privateKey;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "ValidateGitProviderRequest{" +
-               "type=" + type +
-               ", baseUrl='" + baseUrl + '\'' +
-               '}';
+    public ValidateGitProviderRequest build() {
+      return new ValidateGitProviderRequest(
+          type, token, baseUrl, appId, installationId, privateKey);
     }
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ValidateGitProviderRequest that = (ValidateGitProviderRequest) o;
+    return type == that.type
+        && Objects.equals(token, that.token)
+        && Objects.equals(baseUrl, that.baseUrl)
+        && Objects.equals(appId, that.appId)
+        && Objects.equals(installationId, that.installationId)
+        && Objects.equals(privateKey, that.privateKey);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(type, token, baseUrl, appId, installationId, privateKey);
+  }
+
+  @Override
+  public String toString() {
+    return "ValidateGitProviderRequest{" + "type=" + type + ", baseUrl='" + baseUrl + '\'' + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderResponse.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderResponse.java
index 35be9f9..999b79f 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderResponse.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/ValidateGitProviderResponse.java
@@ -17,54 +17,47 @@
 
 import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
 import com.fasterxml.jackson.annotation.JsonProperty;
-
 import java.util.Objects;
 
-/**
- * Response from Git provider validation.
- */
+/** Response from Git provider validation. */
 @JsonIgnoreProperties(ignoreUnknown = true)
 public final class ValidateGitProviderResponse {
 
-    @JsonProperty("valid")
-    private final boolean valid;
-
-    @JsonProperty("message")
-    private final String message;
-
-    public ValidateGitProviderResponse(
-            @JsonProperty("valid") boolean valid,
-            @JsonProperty("message") String message) {
-        this.valid = valid;
-        this.message = message != null ? message : "";
-    }
-
-    public boolean isValid() {
-        return valid;
-    }
-
-    public String getMessage() {
-        return message;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        ValidateGitProviderResponse that = (ValidateGitProviderResponse) o;
-        return valid == that.valid && Objects.equals(message, that.message);
-    }
-
-    @Override
-    public int hashCode() {
-        return Objects.hash(valid, message);
-    }
-
-    @Override
-    public String toString() {
-        return "ValidateGitProviderResponse{" +
-               "valid=" + valid +
-               ", message='" + message + '\'' +
-               '}';
-    }
+  @JsonProperty("valid")
+  private final boolean valid;
+
+  @JsonProperty("message")
+  private final String message;
+
+  public ValidateGitProviderResponse(
+      @JsonProperty("valid") boolean valid, @JsonProperty("message") String message) {
+    this.valid = valid;
+    this.message = message != null ? message : "";
+  }
+
+  public boolean isValid() {
+    return valid;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    ValidateGitProviderResponse that = (ValidateGitProviderResponse) o;
+    return valid == that.valid && Objects.equals(message, that.message);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(valid, message);
+  }
+
+  @Override
+  public String toString() {
+    return "ValidateGitProviderResponse{" + "valid=" + valid + ", message='" + message + '\'' + '}';
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/types/codegovernance/package-info.java b/src/main/java/com/getaxonflow/sdk/types/codegovernance/package-info.java
index e176d5f..c8c986b 100644
--- a/src/main/java/com/getaxonflow/sdk/types/codegovernance/package-info.java
+++ b/src/main/java/com/getaxonflow/sdk/types/codegovernance/package-info.java
@@ -18,10 +18,11 @@
  * Code Governance types for enterprise Git provider integration.
  *
  * 

This package provides types for: + * *

    - *
  • Git provider configuration (GitHub, GitLab, Bitbucket)
  • - *
  • Pull request creation from LLM-generated code
  • - *
  • PR tracking and status synchronization
  • + *
  • Git provider configuration (GitHub, GitLab, Bitbucket) + *
  • Pull request creation from LLM-generated code + *
  • PR tracking and status synchronization *
* * @see com.getaxonflow.sdk.AxonFlow#validateGitProvider diff --git a/src/main/java/com/getaxonflow/sdk/types/costcontrols/CostControlTypes.java b/src/main/java/com/getaxonflow/sdk/types/costcontrols/CostControlTypes.java index ba890f7..8c11ada 100644 --- a/src/main/java/com/getaxonflow/sdk/types/costcontrols/CostControlTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/costcontrols/CostControlTypes.java @@ -18,677 +18,1094 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; - import java.util.List; -import java.util.Objects; /** * Cost Controls types for AxonFlow SDK. * *

This class contains all types needed for cost control operations including: + * *

    - *
  • Budget management (create, update, delete, list)
  • - *
  • Budget status and alerts
  • - *
  • Usage tracking (summary, breakdown, records)
  • - *
  • Pricing information
  • + *
  • Budget management (create, update, delete, list) + *
  • Budget status and alerts + *
  • Usage tracking (summary, breakdown, records) + *
  • Pricing information *
*/ public final class CostControlTypes { - private CostControlTypes() { - // Prevent instantiation + private CostControlTypes() { + // Prevent instantiation + } + + // ======================================== + // ENUMS + // ======================================== + + /** Budget scope determines what entity the budget applies to. */ + public enum BudgetScope { + @JsonProperty("organization") + ORGANIZATION("organization"), + @JsonProperty("team") + TEAM("team"), + @JsonProperty("agent") + AGENT("agent"), + @JsonProperty("workflow") + WORKFLOW("workflow"), + @JsonProperty("user") + USER("user"); + + private final String value; + + BudgetScope(String value) { + this.value = value; } - // ======================================== - // ENUMS - // ======================================== + @JsonValue + public String getValue() { + return value; + } - /** - * Budget scope determines what entity the budget applies to. - */ - public enum BudgetScope { - @JsonProperty("organization") ORGANIZATION("organization"), - @JsonProperty("team") TEAM("team"), - @JsonProperty("agent") AGENT("agent"), - @JsonProperty("workflow") WORKFLOW("workflow"), - @JsonProperty("user") USER("user"); + @JsonCreator + public static BudgetScope fromValue(String value) { + for (BudgetScope scope : values()) { + if (scope.value.equals(value)) { + return scope; + } + } + throw new IllegalArgumentException("Unknown budget scope: " + value); + } + } + + /** Budget period determines the time window for budget tracking. */ + public enum BudgetPeriod { + @JsonProperty("daily") + DAILY("daily"), + @JsonProperty("weekly") + WEEKLY("weekly"), + @JsonProperty("monthly") + MONTHLY("monthly"), + @JsonProperty("quarterly") + QUARTERLY("quarterly"), + @JsonProperty("yearly") + YEARLY("yearly"); + + private final String value; + + BudgetPeriod(String value) { + this.value = value; + } - private final String value; + @JsonValue + public String getValue() { + return value; + } - BudgetScope(String value) { - this.value = value; + @JsonCreator + public static BudgetPeriod fromValue(String value) { + for (BudgetPeriod period : values()) { + if (period.value.equals(value)) { + return period; } + } + throw new IllegalArgumentException("Unknown budget period: " + value); + } + } - @JsonValue - public String getValue() { - return value; - } + /** Action to take when budget is exceeded. */ + public enum BudgetOnExceed { + @JsonProperty("warn") + WARN("warn"), + @JsonProperty("block") + BLOCK("block"), + @JsonProperty("downgrade") + DOWNGRADE("downgrade"); + + private final String value; - @JsonCreator - public static BudgetScope fromValue(String value) { - for (BudgetScope scope : values()) { - if (scope.value.equals(value)) { - return scope; - } - } - throw new IllegalArgumentException("Unknown budget scope: " + value); + BudgetOnExceed(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static BudgetOnExceed fromValue(String value) { + for (BudgetOnExceed action : values()) { + if (action.value.equals(value)) { + return action; } + } + throw new IllegalArgumentException("Unknown budget on exceed action: " + value); } + } - /** - * Budget period determines the time window for budget tracking. - */ - public enum BudgetPeriod { - @JsonProperty("daily") DAILY("daily"), - @JsonProperty("weekly") WEEKLY("weekly"), - @JsonProperty("monthly") MONTHLY("monthly"), - @JsonProperty("quarterly") QUARTERLY("quarterly"), - @JsonProperty("yearly") YEARLY("yearly"); + // ======================================== + // BUDGET TYPES + // ======================================== - private final String value; + /** Request to create a new budget. */ + public static class CreateBudgetRequest { + private final String id; + private final String name; + private final BudgetScope scope; - BudgetPeriod(String value) { - this.value = value; - } + @JsonProperty("limit_usd") + private final Double limitUsd; - @JsonValue - public String getValue() { - return value; - } + private final BudgetPeriod period; - @JsonCreator - public static BudgetPeriod fromValue(String value) { - for (BudgetPeriod period : values()) { - if (period.value.equals(value)) { - return period; - } - } - throw new IllegalArgumentException("Unknown budget period: " + value); - } + @JsonProperty("on_exceed") + private final BudgetOnExceed onExceed; + + @JsonProperty("alert_thresholds") + private final List alertThresholds; + + @JsonProperty("scope_id") + private final String scopeId; + + private CreateBudgetRequest(Builder builder) { + this.id = builder.id; + this.name = builder.name; + this.scope = builder.scope; + this.limitUsd = builder.limitUsd; + this.period = builder.period; + this.onExceed = builder.onExceed; + this.alertThresholds = builder.alertThresholds; + this.scopeId = builder.scopeId; } - /** - * Action to take when budget is exceeded. - */ - public enum BudgetOnExceed { - @JsonProperty("warn") WARN("warn"), - @JsonProperty("block") BLOCK("block"), - @JsonProperty("downgrade") DOWNGRADE("downgrade"); + public String getId() { + return id; + } - private final String value; + public String getName() { + return name; + } - BudgetOnExceed(String value) { - this.value = value; - } + public BudgetScope getScope() { + return scope; + } - @JsonValue - public String getValue() { - return value; - } + public Double getLimitUsd() { + return limitUsd; + } - @JsonCreator - public static BudgetOnExceed fromValue(String value) { - for (BudgetOnExceed action : values()) { - if (action.value.equals(value)) { - return action; - } - } - throw new IllegalArgumentException("Unknown budget on exceed action: " + value); - } + public BudgetPeriod getPeriod() { + return period; } - // ======================================== - // BUDGET TYPES - // ======================================== - - /** - * Request to create a new budget. - */ - public static class CreateBudgetRequest { - private final String id; - private final String name; - private final BudgetScope scope; - @JsonProperty("limit_usd") - private final Double limitUsd; - private final BudgetPeriod period; - @JsonProperty("on_exceed") - private final BudgetOnExceed onExceed; - @JsonProperty("alert_thresholds") - private final List alertThresholds; - @JsonProperty("scope_id") - private final String scopeId; - - private CreateBudgetRequest(Builder builder) { - this.id = builder.id; - this.name = builder.name; - this.scope = builder.scope; - this.limitUsd = builder.limitUsd; - this.period = builder.period; - this.onExceed = builder.onExceed; - this.alertThresholds = builder.alertThresholds; - this.scopeId = builder.scopeId; - } + public BudgetOnExceed getOnExceed() { + return onExceed; + } - public String getId() { return id; } - public String getName() { return name; } - public BudgetScope getScope() { return scope; } - public Double getLimitUsd() { return limitUsd; } - public BudgetPeriod getPeriod() { return period; } - public BudgetOnExceed getOnExceed() { return onExceed; } - public List getAlertThresholds() { return alertThresholds; } - public String getScopeId() { return scopeId; } - - public static Builder builder() { return new Builder(); } - - public static class Builder { - private String id; - private String name; - private BudgetScope scope; - private Double limitUsd; - private BudgetPeriod period; - private BudgetOnExceed onExceed; - private List alertThresholds; - private String scopeId; - - public Builder id(String id) { this.id = id; return this; } - public Builder name(String name) { this.name = name; return this; } - public Builder scope(BudgetScope scope) { this.scope = scope; return this; } - public Builder limitUsd(Double limitUsd) { this.limitUsd = limitUsd; return this; } - public Builder period(BudgetPeriod period) { this.period = period; return this; } - public Builder onExceed(BudgetOnExceed onExceed) { this.onExceed = onExceed; return this; } - public Builder alertThresholds(List alertThresholds) { this.alertThresholds = alertThresholds; return this; } - public Builder scopeId(String scopeId) { this.scopeId = scopeId; return this; } - public CreateBudgetRequest build() { return new CreateBudgetRequest(this); } - } + public List getAlertThresholds() { + return alertThresholds; } - /** - * Request to update an existing budget. - */ - public static class UpdateBudgetRequest { - private final String name; - @JsonProperty("limit_usd") - private final Double limitUsd; - @JsonProperty("on_exceed") - private final BudgetOnExceed onExceed; - @JsonProperty("alert_thresholds") - private final List alertThresholds; - - private UpdateBudgetRequest(Builder builder) { - this.name = builder.name; - this.limitUsd = builder.limitUsd; - this.onExceed = builder.onExceed; - this.alertThresholds = builder.alertThresholds; - } + public String getScopeId() { + return scopeId; + } - public String getName() { return name; } - public Double getLimitUsd() { return limitUsd; } - public BudgetOnExceed getOnExceed() { return onExceed; } - public List getAlertThresholds() { return alertThresholds; } + public static Builder builder() { + return new Builder(); + } - public static Builder builder() { return new Builder(); } + public static class Builder { + private String id; + private String name; + private BudgetScope scope; + private Double limitUsd; + private BudgetPeriod period; + private BudgetOnExceed onExceed; + private List alertThresholds; + private String scopeId; + + public Builder id(String id) { + this.id = id; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder scope(BudgetScope scope) { + this.scope = scope; + return this; + } + + public Builder limitUsd(Double limitUsd) { + this.limitUsd = limitUsd; + return this; + } + + public Builder period(BudgetPeriod period) { + this.period = period; + return this; + } + + public Builder onExceed(BudgetOnExceed onExceed) { + this.onExceed = onExceed; + return this; + } + + public Builder alertThresholds(List alertThresholds) { + this.alertThresholds = alertThresholds; + return this; + } + + public Builder scopeId(String scopeId) { + this.scopeId = scopeId; + return this; + } + + public CreateBudgetRequest build() { + return new CreateBudgetRequest(this); + } + } + } - public static class Builder { - private String name; - private Double limitUsd; - private BudgetOnExceed onExceed; - private List alertThresholds; + /** Request to update an existing budget. */ + public static class UpdateBudgetRequest { + private final String name; - public Builder name(String name) { this.name = name; return this; } - public Builder limitUsd(Double limitUsd) { this.limitUsd = limitUsd; return this; } - public Builder onExceed(BudgetOnExceed onExceed) { this.onExceed = onExceed; return this; } - public Builder alertThresholds(List alertThresholds) { this.alertThresholds = alertThresholds; return this; } - public UpdateBudgetRequest build() { return new UpdateBudgetRequest(this); } - } + @JsonProperty("limit_usd") + private final Double limitUsd; + + @JsonProperty("on_exceed") + private final BudgetOnExceed onExceed; + + @JsonProperty("alert_thresholds") + private final List alertThresholds; + + private UpdateBudgetRequest(Builder builder) { + this.name = builder.name; + this.limitUsd = builder.limitUsd; + this.onExceed = builder.onExceed; + this.alertThresholds = builder.alertThresholds; + } + + public String getName() { + return name; } - /** - * Options for listing budgets. - */ - public static class ListBudgetsOptions { - private final BudgetScope scope; - private final Integer limit; - private final Integer offset; + public Double getLimitUsd() { + return limitUsd; + } - private ListBudgetsOptions(Builder builder) { - this.scope = builder.scope; - this.limit = builder.limit; - this.offset = builder.offset; - } + public BudgetOnExceed getOnExceed() { + return onExceed; + } - public BudgetScope getScope() { return scope; } - public Integer getLimit() { return limit; } - public Integer getOffset() { return offset; } + public List getAlertThresholds() { + return alertThresholds; + } - public static Builder builder() { return new Builder(); } + public static Builder builder() { + return new Builder(); + } - public static class Builder { - private BudgetScope scope; - private Integer limit; - private Integer offset; + public static class Builder { + private String name; + private Double limitUsd; + private BudgetOnExceed onExceed; + private List alertThresholds; + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder limitUsd(Double limitUsd) { + this.limitUsd = limitUsd; + return this; + } + + public Builder onExceed(BudgetOnExceed onExceed) { + this.onExceed = onExceed; + return this; + } + + public Builder alertThresholds(List alertThresholds) { + this.alertThresholds = alertThresholds; + return this; + } + + public UpdateBudgetRequest build() { + return new UpdateBudgetRequest(this); + } + } + } + + /** Options for listing budgets. */ + public static class ListBudgetsOptions { + private final BudgetScope scope; + private final Integer limit; + private final Integer offset; + + private ListBudgetsOptions(Builder builder) { + this.scope = builder.scope; + this.limit = builder.limit; + this.offset = builder.offset; + } - public Builder scope(BudgetScope scope) { this.scope = scope; return this; } - public Builder limit(Integer limit) { this.limit = limit; return this; } - public Builder offset(Integer offset) { this.offset = offset; return this; } - public ListBudgetsOptions build() { return new ListBudgetsOptions(this); } - } + public BudgetScope getScope() { + return scope; } - /** - * A budget entity. - */ - public static class Budget { - private String id; - private String name; - private String scope; - @JsonProperty("limit_usd") - private Double limitUsd; - private String period; - @JsonProperty("on_exceed") - private String onExceed; - @JsonProperty("alert_thresholds") - private List alertThresholds; - private Boolean enabled; - @JsonProperty("scope_id") - private String scopeId; - @JsonProperty("created_at") - private String createdAt; - @JsonProperty("updated_at") - private String updatedAt; - - public Budget() {} - - public String getId() { return id; } - public String getName() { return name; } - public String getScope() { return scope; } - public Double getLimitUsd() { return limitUsd; } - public String getPeriod() { return period; } - public String getOnExceed() { return onExceed; } - public List getAlertThresholds() { return alertThresholds; } - public Boolean getEnabled() { return enabled; } - public String getScopeId() { return scopeId; } - public String getCreatedAt() { return createdAt; } - public String getUpdatedAt() { return updatedAt; } - } - - /** - * Response containing a list of budgets. - */ - public static class BudgetsResponse { - private List budgets; - private Integer total; - - public BudgetsResponse() {} - - public List getBudgets() { return budgets; } - public Integer getTotal() { return total; } - } - - // ======================================== - // BUDGET STATUS TYPES - // ======================================== - - /** - * Current status of a budget. - */ - public static class BudgetStatus { - private Budget budget; - @JsonProperty("used_usd") - private Double usedUsd; - @JsonProperty("remaining_usd") - private Double remainingUsd; - private Double percentage; - @JsonProperty("is_exceeded") - private Boolean isExceeded; - @JsonProperty("is_blocked") - private Boolean isBlocked; - @JsonProperty("period_start") - private String periodStart; - @JsonProperty("period_end") - private String periodEnd; - - public BudgetStatus() {} - - public Budget getBudget() { return budget; } - public Double getUsedUsd() { return usedUsd; } - public Double getRemainingUsd() { return remainingUsd; } - public Double getPercentage() { return percentage; } - public Boolean isExceeded() { return isExceeded; } - public Boolean isBlocked() { return isBlocked; } - public String getPeriodStart() { return periodStart; } - public String getPeriodEnd() { return periodEnd; } - } - - // ======================================== - // BUDGET ALERT TYPES - // ======================================== - - /** - * A budget alert. - */ - public static class BudgetAlert { - private String id; - @JsonProperty("budget_id") - private String budgetId; - @JsonProperty("alert_type") - private String alertType; - private Integer threshold; - @JsonProperty("percentage_reached") - private Double percentageReached; - @JsonProperty("amount_usd") - private Double amountUsd; - private String message; - @JsonProperty("created_at") - private String createdAt; - - public BudgetAlert() {} - - public String getId() { return id; } - public String getBudgetId() { return budgetId; } - public String getAlertType() { return alertType; } - public Integer getThreshold() { return threshold; } - public Double getPercentageReached() { return percentageReached; } - public Double getAmountUsd() { return amountUsd; } - public String getMessage() { return message; } - public String getCreatedAt() { return createdAt; } - } - - /** - * Response containing budget alerts. - */ - public static class BudgetAlertsResponse { - private List alerts; - private Integer count; - - public BudgetAlertsResponse() {} - - public List getAlerts() { return alerts; } - public Integer getCount() { return count; } - } - - // ======================================== - // BUDGET CHECK TYPES - // ======================================== - - /** - * Request to check if a request is allowed by budgets. - */ - public static class BudgetCheckRequest { - @JsonProperty("org_id") - private final String orgId; - @JsonProperty("team_id") - private final String teamId; - @JsonProperty("agent_id") - private final String agentId; - @JsonProperty("workflow_id") - private final String workflowId; - @JsonProperty("user_id") - private final String userId; - - private BudgetCheckRequest(Builder builder) { - this.orgId = builder.orgId; - this.teamId = builder.teamId; - this.agentId = builder.agentId; - this.workflowId = builder.workflowId; - this.userId = builder.userId; - } + public Integer getLimit() { + return limit; + } - public String getOrgId() { return orgId; } - public String getTeamId() { return teamId; } - public String getAgentId() { return agentId; } - public String getWorkflowId() { return workflowId; } - public String getUserId() { return userId; } - - public static Builder builder() { return new Builder(); } - - public static class Builder { - private String orgId; - private String teamId; - private String agentId; - private String workflowId; - private String userId; - - public Builder orgId(String orgId) { this.orgId = orgId; return this; } - public Builder teamId(String teamId) { this.teamId = teamId; return this; } - public Builder agentId(String agentId) { this.agentId = agentId; return this; } - public Builder workflowId(String workflowId) { this.workflowId = workflowId; return this; } - public Builder userId(String userId) { this.userId = userId; return this; } - public BudgetCheckRequest build() { return new BudgetCheckRequest(this); } - } + public Integer getOffset() { + return offset; } - /** - * Budget decision result. - */ - public static class BudgetDecision { - private Boolean allowed; - private String action; - private String message; - private List budgets; - - public BudgetDecision() {} - - public Boolean isAllowed() { return allowed; } - public String getAction() { return action; } - public String getMessage() { return message; } - public List getBudgets() { return budgets; } - } - - // ======================================== - // USAGE TYPES - // ======================================== - - /** - * Usage summary for a period. - */ - public static class UsageSummary { - @JsonProperty("total_cost_usd") - private Double totalCostUsd; - @JsonProperty("total_requests") - private Integer totalRequests; - @JsonProperty("total_tokens_in") - private Integer totalTokensIn; - @JsonProperty("total_tokens_out") - private Integer totalTokensOut; - @JsonProperty("average_cost_per_request") - private Double averageCostPerRequest; - private String period; - @JsonProperty("period_start") - private String periodStart; - @JsonProperty("period_end") - private String periodEnd; - - public UsageSummary() {} - - public Double getTotalCostUsd() { return totalCostUsd; } - public Integer getTotalRequests() { return totalRequests; } - public Integer getTotalTokensIn() { return totalTokensIn; } - public Integer getTotalTokensOut() { return totalTokensOut; } - public Double getAverageCostPerRequest() { return averageCostPerRequest; } - public String getPeriod() { return period; } - public String getPeriodStart() { return periodStart; } - public String getPeriodEnd() { return periodEnd; } - } - - /** - * An item in a usage breakdown. - */ - public static class UsageBreakdownItem { - @JsonProperty("group_value") - private String groupValue; - @JsonProperty("cost_usd") - private Double costUsd; - private Double percentage; - @JsonProperty("request_count") - private Integer requestCount; - @JsonProperty("tokens_in") - private Integer tokensIn; - @JsonProperty("tokens_out") - private Integer tokensOut; - - public UsageBreakdownItem() {} - - public String getGroupValue() { return groupValue; } - public Double getCostUsd() { return costUsd; } - public Double getPercentage() { return percentage; } - public Integer getRequestCount() { return requestCount; } - public Integer getTokensIn() { return tokensIn; } - public Integer getTokensOut() { return tokensOut; } - } - - /** - * Usage breakdown by a grouping dimension. - */ - public static class UsageBreakdown { - @JsonProperty("group_by") - private String groupBy; - @JsonProperty("total_cost_usd") - private Double totalCostUsd; - private List items; - private String period; - @JsonProperty("period_start") - private String periodStart; - @JsonProperty("period_end") - private String periodEnd; - - public UsageBreakdown() {} - - public String getGroupBy() { return groupBy; } - public Double getTotalCostUsd() { return totalCostUsd; } - public List getItems() { return items; } - public String getPeriod() { return period; } - public String getPeriodStart() { return periodStart; } - public String getPeriodEnd() { return periodEnd; } - } - - /** - * Options for listing usage records. - */ - public static class ListUsageRecordsOptions { - private final Integer limit; - private final Integer offset; - private final String provider; - private final String model; - - private ListUsageRecordsOptions(Builder builder) { - this.limit = builder.limit; - this.offset = builder.offset; - this.provider = builder.provider; - this.model = builder.model; - } + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private BudgetScope scope; + private Integer limit; + private Integer offset; + + public Builder scope(BudgetScope scope) { + this.scope = scope; + return this; + } + + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + this.offset = offset; + return this; + } + + public ListBudgetsOptions build() { + return new ListBudgetsOptions(this); + } + } + } - public Integer getLimit() { return limit; } - public Integer getOffset() { return offset; } - public String getProvider() { return provider; } - public String getModel() { return model; } + /** A budget entity. */ + public static class Budget { + private String id; + private String name; + private String scope; - public static Builder builder() { return new Builder(); } + @JsonProperty("limit_usd") + private Double limitUsd; - public static class Builder { - private Integer limit; - private Integer offset; - private String provider; - private String model; + private String period; - public Builder limit(Integer limit) { this.limit = limit; return this; } - public Builder offset(Integer offset) { this.offset = offset; return this; } - public Builder provider(String provider) { this.provider = provider; return this; } - public Builder model(String model) { this.model = model; return this; } - public ListUsageRecordsOptions build() { return new ListUsageRecordsOptions(this); } - } + @JsonProperty("on_exceed") + private String onExceed; + + @JsonProperty("alert_thresholds") + private List alertThresholds; + + private Boolean enabled; + + @JsonProperty("scope_id") + private String scopeId; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; + + public Budget() {} + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public String getScope() { + return scope; + } + + public Double getLimitUsd() { + return limitUsd; + } + + public String getPeriod() { + return period; + } + + public String getOnExceed() { + return onExceed; + } + + public List getAlertThresholds() { + return alertThresholds; + } + + public Boolean getEnabled() { + return enabled; + } + + public String getScopeId() { + return scopeId; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + } + + /** Response containing a list of budgets. */ + public static class BudgetsResponse { + private List budgets; + private Integer total; + + public BudgetsResponse() {} + + public List getBudgets() { + return budgets; + } + + public Integer getTotal() { + return total; + } + } + + // ======================================== + // BUDGET STATUS TYPES + // ======================================== + + /** Current status of a budget. */ + public static class BudgetStatus { + private Budget budget; + + @JsonProperty("used_usd") + private Double usedUsd; + + @JsonProperty("remaining_usd") + private Double remainingUsd; + + private Double percentage; + + @JsonProperty("is_exceeded") + private Boolean isExceeded; + + @JsonProperty("is_blocked") + private Boolean isBlocked; + + @JsonProperty("period_start") + private String periodStart; + + @JsonProperty("period_end") + private String periodEnd; + + public BudgetStatus() {} + + public Budget getBudget() { + return budget; + } + + public Double getUsedUsd() { + return usedUsd; + } + + public Double getRemainingUsd() { + return remainingUsd; + } + + public Double getPercentage() { + return percentage; + } + + public Boolean isExceeded() { + return isExceeded; + } + + public Boolean isBlocked() { + return isBlocked; + } + + public String getPeriodStart() { + return periodStart; + } + + public String getPeriodEnd() { + return periodEnd; + } + } + + // ======================================== + // BUDGET ALERT TYPES + // ======================================== + + /** A budget alert. */ + public static class BudgetAlert { + private String id; + + @JsonProperty("budget_id") + private String budgetId; + + @JsonProperty("alert_type") + private String alertType; + + private Integer threshold; + + @JsonProperty("percentage_reached") + private Double percentageReached; + + @JsonProperty("amount_usd") + private Double amountUsd; + + private String message; + + @JsonProperty("created_at") + private String createdAt; + + public BudgetAlert() {} + + public String getId() { + return id; + } + + public String getBudgetId() { + return budgetId; + } + + public String getAlertType() { + return alertType; + } + + public Integer getThreshold() { + return threshold; + } + + public Double getPercentageReached() { + return percentageReached; + } + + public Double getAmountUsd() { + return amountUsd; + } + + public String getMessage() { + return message; + } + + public String getCreatedAt() { + return createdAt; + } + } + + /** Response containing budget alerts. */ + public static class BudgetAlertsResponse { + private List alerts; + private Integer count; + + public BudgetAlertsResponse() {} + + public List getAlerts() { + return alerts; + } + + public Integer getCount() { + return count; + } + } + + // ======================================== + // BUDGET CHECK TYPES + // ======================================== + + /** Request to check if a request is allowed by budgets. */ + public static class BudgetCheckRequest { + @JsonProperty("org_id") + private final String orgId; + + @JsonProperty("team_id") + private final String teamId; + + @JsonProperty("agent_id") + private final String agentId; + + @JsonProperty("workflow_id") + private final String workflowId; + + @JsonProperty("user_id") + private final String userId; + + private BudgetCheckRequest(Builder builder) { + this.orgId = builder.orgId; + this.teamId = builder.teamId; + this.agentId = builder.agentId; + this.workflowId = builder.workflowId; + this.userId = builder.userId; + } + + public String getOrgId() { + return orgId; + } + + public String getTeamId() { + return teamId; + } + + public String getAgentId() { + return agentId; + } + + public String getWorkflowId() { + return workflowId; + } + + public String getUserId() { + return userId; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private String orgId; + private String teamId; + private String agentId; + private String workflowId; + private String userId; + + public Builder orgId(String orgId) { + this.orgId = orgId; + return this; + } + + public Builder teamId(String teamId) { + this.teamId = teamId; + return this; + } + + public Builder agentId(String agentId) { + this.agentId = agentId; + return this; + } + + public Builder workflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } + + public Builder userId(String userId) { + this.userId = userId; + return this; + } + + public BudgetCheckRequest build() { + return new BudgetCheckRequest(this); + } + } + } + + /** Budget decision result. */ + public static class BudgetDecision { + private Boolean allowed; + private String action; + private String message; + private List budgets; + + public BudgetDecision() {} + + public Boolean isAllowed() { + return allowed; + } + + public String getAction() { + return action; + } + + public String getMessage() { + return message; + } + + public List getBudgets() { + return budgets; + } + } + + // ======================================== + // USAGE TYPES + // ======================================== + + /** Usage summary for a period. */ + public static class UsageSummary { + @JsonProperty("total_cost_usd") + private Double totalCostUsd; + + @JsonProperty("total_requests") + private Integer totalRequests; + + @JsonProperty("total_tokens_in") + private Integer totalTokensIn; + + @JsonProperty("total_tokens_out") + private Integer totalTokensOut; + + @JsonProperty("average_cost_per_request") + private Double averageCostPerRequest; + + private String period; + + @JsonProperty("period_start") + private String periodStart; + + @JsonProperty("period_end") + private String periodEnd; + + public UsageSummary() {} + + public Double getTotalCostUsd() { + return totalCostUsd; + } + + public Integer getTotalRequests() { + return totalRequests; + } + + public Integer getTotalTokensIn() { + return totalTokensIn; + } + + public Integer getTotalTokensOut() { + return totalTokensOut; + } + + public Double getAverageCostPerRequest() { + return averageCostPerRequest; + } + + public String getPeriod() { + return period; + } + + public String getPeriodStart() { + return periodStart; + } + + public String getPeriodEnd() { + return periodEnd; + } + } + + /** An item in a usage breakdown. */ + public static class UsageBreakdownItem { + @JsonProperty("group_value") + private String groupValue; + + @JsonProperty("cost_usd") + private Double costUsd; + + private Double percentage; + + @JsonProperty("request_count") + private Integer requestCount; + + @JsonProperty("tokens_in") + private Integer tokensIn; + + @JsonProperty("tokens_out") + private Integer tokensOut; + + public UsageBreakdownItem() {} + + public String getGroupValue() { + return groupValue; + } + + public Double getCostUsd() { + return costUsd; + } + + public Double getPercentage() { + return percentage; + } + + public Integer getRequestCount() { + return requestCount; + } + + public Integer getTokensIn() { + return tokensIn; + } + + public Integer getTokensOut() { + return tokensOut; + } + } + + /** Usage breakdown by a grouping dimension. */ + public static class UsageBreakdown { + @JsonProperty("group_by") + private String groupBy; + + @JsonProperty("total_cost_usd") + private Double totalCostUsd; + + private List items; + private String period; + + @JsonProperty("period_start") + private String periodStart; + + @JsonProperty("period_end") + private String periodEnd; + + public UsageBreakdown() {} + + public String getGroupBy() { + return groupBy; + } + + public Double getTotalCostUsd() { + return totalCostUsd; + } + + public List getItems() { + return items; + } + + public String getPeriod() { + return period; + } + + public String getPeriodStart() { + return periodStart; + } + + public String getPeriodEnd() { + return periodEnd; + } + } + + /** Options for listing usage records. */ + public static class ListUsageRecordsOptions { + private final Integer limit; + private final Integer offset; + private final String provider; + private final String model; + + private ListUsageRecordsOptions(Builder builder) { + this.limit = builder.limit; + this.offset = builder.offset; + this.provider = builder.provider; + this.model = builder.model; + } + + public Integer getLimit() { + return limit; + } + + public Integer getOffset() { + return offset; + } + + public String getProvider() { + return provider; + } + + public String getModel() { + return model; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Integer limit; + private Integer offset; + private String provider; + private String model; + + public Builder limit(Integer limit) { + this.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + this.offset = offset; + return this; + } + + public Builder provider(String provider) { + this.provider = provider; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + public ListUsageRecordsOptions build() { + return new ListUsageRecordsOptions(this); + } + } + } + + /** A single usage record. */ + public static class UsageRecord { + private String id; + private String provider; + private String model; + + @JsonProperty("tokens_in") + private Integer tokensIn; + + @JsonProperty("tokens_out") + private Integer tokensOut; + + @JsonProperty("cost_usd") + private Double costUsd; + + @JsonProperty("request_id") + private String requestId; + + @JsonProperty("org_id") + private String orgId; + + @JsonProperty("agent_id") + private String agentId; + + private String timestamp; + + public UsageRecord() {} + + public String getId() { + return id; + } + + public String getProvider() { + return provider; + } + + public String getModel() { + return model; + } + + public Integer getTokensIn() { + return tokensIn; + } + + public Integer getTokensOut() { + return tokensOut; + } + + public Double getCostUsd() { + return costUsd; + } + + public String getRequestId() { + return requestId; + } + + public String getOrgId() { + return orgId; + } + + public String getAgentId() { + return agentId; + } + + public String getTimestamp() { + return timestamp; + } + } + + /** Response containing usage records. */ + public static class UsageRecordsResponse { + private List records; + private Integer total; + + public UsageRecordsResponse() {} + + public List getRecords() { + return records; + } + + public Integer getTotal() { + return total; + } + } + + // ======================================== + // PRICING TYPES + // ======================================== + + /** Model pricing information. */ + public static class ModelPricing { + @JsonProperty("input_per_1k") + private Double inputPer1k; + + @JsonProperty("output_per_1k") + private Double outputPer1k; + + public ModelPricing() {} + + public Double getInputPer1k() { + return inputPer1k; + } + + public Double getOutputPer1k() { + return outputPer1k; + } + } + + /** Pricing information for a provider/model. */ + public static class PricingInfo { + private String provider; + private String model; + private ModelPricing pricing; + + public PricingInfo() {} + + public String getProvider() { + return provider; + } + + public String getModel() { + return model; + } + + public ModelPricing getPricing() { + return pricing; + } + } + + /** Response containing pricing information. */ + public static class PricingListResponse { + private List pricing; + + public PricingListResponse() {} + + public List getPricing() { + return pricing; } - /** - * A single usage record. - */ - public static class UsageRecord { - private String id; - private String provider; - private String model; - @JsonProperty("tokens_in") - private Integer tokensIn; - @JsonProperty("tokens_out") - private Integer tokensOut; - @JsonProperty("cost_usd") - private Double costUsd; - @JsonProperty("request_id") - private String requestId; - @JsonProperty("org_id") - private String orgId; - @JsonProperty("agent_id") - private String agentId; - private String timestamp; - - public UsageRecord() {} - - public String getId() { return id; } - public String getProvider() { return provider; } - public String getModel() { return model; } - public Integer getTokensIn() { return tokensIn; } - public Integer getTokensOut() { return tokensOut; } - public Double getCostUsd() { return costUsd; } - public String getRequestId() { return requestId; } - public String getOrgId() { return orgId; } - public String getAgentId() { return agentId; } - public String getTimestamp() { return timestamp; } - } - - /** - * Response containing usage records. - */ - public static class UsageRecordsResponse { - private List records; - private Integer total; - - public UsageRecordsResponse() {} - - public List getRecords() { return records; } - public Integer getTotal() { return total; } - } - - // ======================================== - // PRICING TYPES - // ======================================== - - /** - * Model pricing information. - */ - public static class ModelPricing { - @JsonProperty("input_per_1k") - private Double inputPer1k; - @JsonProperty("output_per_1k") - private Double outputPer1k; - - public ModelPricing() {} - - public Double getInputPer1k() { return inputPer1k; } - public Double getOutputPer1k() { return outputPer1k; } - } - - /** - * Pricing information for a provider/model. - */ - public static class PricingInfo { - private String provider; - private String model; - private ModelPricing pricing; - - public PricingInfo() {} - - public String getProvider() { return provider; } - public String getModel() { return model; } - public ModelPricing getPricing() { return pricing; } - } - - /** - * Response containing pricing information. - */ - public static class PricingListResponse { - private List pricing; - - public PricingListResponse() {} - - public List getPricing() { return pricing; } - public void setPricing(List pricing) { this.pricing = pricing; } + public void setPricing(List pricing) { + this.pricing = pricing; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/costcontrols/package-info.java b/src/main/java/com/getaxonflow/sdk/types/costcontrols/package-info.java index 76aa8e6..cce8ed9 100644 --- a/src/main/java/com/getaxonflow/sdk/types/costcontrols/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/costcontrols/package-info.java @@ -18,11 +18,12 @@ * Cost control types for the AxonFlow SDK. * *

This package contains all types needed for cost control operations including: + * *

    - *
  • Budget management (create, update, delete, list)
  • - *
  • Budget status and alerts
  • - *
  • Usage tracking (summary, breakdown, records)
  • - *
  • Pricing information
  • + *
  • Budget management (create, update, delete, list) + *
  • Budget status and alerts + *
  • Usage tracking (summary, breakdown, records) + *
  • Pricing information *
* * @see com.getaxonflow.sdk.types.costcontrols.CostControlTypes diff --git a/src/main/java/com/getaxonflow/sdk/types/execution/ExecutionTypes.java b/src/main/java/com/getaxonflow/sdk/types/execution/ExecutionTypes.java index 02740ba..06c8fa2 100644 --- a/src/main/java/com/getaxonflow/sdk/types/execution/ExecutionTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/execution/ExecutionTypes.java @@ -19,7 +19,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; - import java.time.Instant; import java.util.List; import java.util.Map; @@ -27,695 +26,1055 @@ /** * Unified Execution Tracking Types for AxonFlow SDK. - *

- * These types provide a consistent interface for tracking both Multi-Agent Planning (MAP) - * and Workflow Control Plane (WCP) executions. The unified schema enables consistent - * status tracking, progress reporting, and cost tracking across execution types. - *

- * Issue #1075 - EPIC #1074: Unified Workflow Infrastructure + * + *

These types provide a consistent interface for tracking both Multi-Agent Planning (MAP) and + * Workflow Control Plane (WCP) executions. The unified schema enables consistent status tracking, + * progress reporting, and cost tracking across execution types. + * + *

Issue #1075 - EPIC #1074: Unified Workflow Infrastructure */ public final class ExecutionTypes { - private ExecutionTypes() { - // Utility class, no instances - } + private ExecutionTypes() { + // Utility class, no instances + } - /** - * Execution type distinguishing between MAP plans and WCP workflows. - */ - public enum ExecutionType { - MAP_PLAN("map_plan"), - WCP_WORKFLOW("wcp_workflow"); + /** Execution type distinguishing between MAP plans and WCP workflows. */ + public enum ExecutionType { + MAP_PLAN("map_plan"), + WCP_WORKFLOW("wcp_workflow"); - private final String value; + private final String value; - ExecutionType(String value) { - this.value = value; - } + ExecutionType(String value) { + this.value = value; + } - @JsonValue - public String getValue() { - return value; - } + @JsonValue + public String getValue() { + return value; + } - @JsonCreator - public static ExecutionType fromValue(String value) { - for (ExecutionType type : values()) { - if (type.value.equals(value)) { - return type; - } - } - throw new IllegalArgumentException("Unknown execution type: " + value); + @JsonCreator + public static ExecutionType fromValue(String value) { + for (ExecutionType type : values()) { + if (type.value.equals(value)) { + return type; } + } + throw new IllegalArgumentException("Unknown execution type: " + value); + } + } + + /** Unified execution status values. */ + public enum ExecutionStatusValue { + PENDING("pending"), + RUNNING("running"), + COMPLETED("completed"), + FAILED("failed"), + CANCELLED("cancelled"), + ABORTED("aborted"), // WCP-specific: workflow aborted + EXPIRED("expired"); // MAP-specific: plan expired before execution + + private final String value; + + ExecutionStatusValue(String value) { + this.value = value; } - /** - * Unified execution status values. - */ - public enum ExecutionStatusValue { - PENDING("pending"), - RUNNING("running"), - COMPLETED("completed"), - FAILED("failed"), - CANCELLED("cancelled"), - ABORTED("aborted"), // WCP-specific: workflow aborted - EXPIRED("expired"); // MAP-specific: plan expired before execution + @JsonValue + public String getValue() { + return value; + } - private final String value; + public boolean isTerminal() { + return this == COMPLETED + || this == FAILED + || this == CANCELLED + || this == ABORTED + || this == EXPIRED; + } - ExecutionStatusValue(String value) { - this.value = value; + @JsonCreator + public static ExecutionStatusValue fromValue(String value) { + for (ExecutionStatusValue status : values()) { + if (status.value.equals(value)) { + return status; } + } + throw new IllegalArgumentException("Unknown execution status: " + value); + } + } + + /** Step status values. */ + public enum StepStatusValue { + PENDING("pending"), + RUNNING("running"), + COMPLETED("completed"), + FAILED("failed"), + SKIPPED("skipped"), + BLOCKED("blocked"), // WCP: blocked by policy + APPROVAL("approval"); // WCP: waiting for approval + + private final String value; + + StepStatusValue(String value) { + this.value = value; + } - @JsonValue - public String getValue() { - return value; - } + @JsonValue + public String getValue() { + return value; + } - public boolean isTerminal() { - return this == COMPLETED || this == FAILED || this == CANCELLED || - this == ABORTED || this == EXPIRED; + public boolean isTerminal() { + return this == COMPLETED || this == FAILED || this == SKIPPED; + } + + public boolean isBlocking() { + return this == BLOCKED || this == APPROVAL; + } + + @JsonCreator + public static StepStatusValue fromValue(String value) { + for (StepStatusValue status : values()) { + if (status.value.equals(value)) { + return status; } + } + throw new IllegalArgumentException("Unknown step status: " + value); + } + } + + /** Step type indicating what kind of operation the step performs. */ + public enum UnifiedStepType { + LLM_CALL("llm_call"), + TOOL_CALL("tool_call"), + CONNECTOR_CALL("connector_call"), + HUMAN_TASK("human_task"), + SYNTHESIS("synthesis"), // MAP: result synthesis step + ACTION("action"), // Generic action step + GATE("gate"); // WCP: policy gate evaluation + + private final String value; + + UnifiedStepType(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } - @JsonCreator - public static ExecutionStatusValue fromValue(String value) { - for (ExecutionStatusValue status : values()) { - if (status.value.equals(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown execution status: " + value); + @JsonCreator + public static UnifiedStepType fromValue(String value) { + for (UnifiedStepType type : values()) { + if (type.value.equals(value)) { + return type; } + } + throw new IllegalArgumentException("Unknown step type: " + value); } + } - /** - * Step status values. - */ - public enum StepStatusValue { - PENDING("pending"), - RUNNING("running"), - COMPLETED("completed"), - FAILED("failed"), - SKIPPED("skipped"), - BLOCKED("blocked"), // WCP: blocked by policy - APPROVAL("approval"); // WCP: waiting for approval + /** Gate decision values (applicable to both MAP and WCP). */ + public enum UnifiedGateDecision { + ALLOW("allow"), + BLOCK("block"), + REQUIRE_APPROVAL("require_approval"); - private final String value; + private final String value; - StepStatusValue(String value) { - this.value = value; - } + UnifiedGateDecision(String value) { + this.value = value; + } - @JsonValue - public String getValue() { - return value; - } + @JsonValue + public String getValue() { + return value; + } - public boolean isTerminal() { - return this == COMPLETED || this == FAILED || this == SKIPPED; + @JsonCreator + public static UnifiedGateDecision fromValue(String value) { + for (UnifiedGateDecision decision : values()) { + if (decision.value.equals(value)) { + return decision; } + } + throw new IllegalArgumentException("Unknown gate decision: " + value); + } + } - public boolean isBlocking() { - return this == BLOCKED || this == APPROVAL; - } + /** Approval status for require_approval decisions. */ + public enum UnifiedApprovalStatus { + PENDING("pending"), + APPROVED("approved"), + REJECTED("rejected"); + + private final String value; - @JsonCreator - public static StepStatusValue fromValue(String value) { - for (StepStatusValue status : values()) { - if (status.value.equals(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown step status: " + value); + UnifiedApprovalStatus(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + + @JsonCreator + public static UnifiedApprovalStatus fromValue(String value) { + for (UnifiedApprovalStatus status : values()) { + if (status.value.equals(value)) { + return status; } + } + throw new IllegalArgumentException("Unknown approval status: " + value); + } + } + + /** Detailed information about an individual execution step. */ + public static final class UnifiedStepStatus { + private final String stepId; + private final int stepIndex; + private final String stepName; + private final UnifiedStepType stepType; + private final StepStatusValue status; + private final Instant startedAt; + private final Instant endedAt; + private final String duration; + private final UnifiedGateDecision decision; + private final String decisionReason; + private final List policiesMatched; + private final UnifiedApprovalStatus approvalStatus; + private final String approvedBy; + private final Instant approvedAt; + private final String model; + private final String provider; + private final Double costUsd; + private final Object input; + private final Object output; + private final String resultSummary; + private final String error; + + @JsonCreator + public UnifiedStepStatus( + @JsonProperty("step_id") String stepId, + @JsonProperty("step_index") int stepIndex, + @JsonProperty("step_name") String stepName, + @JsonProperty("step_type") UnifiedStepType stepType, + @JsonProperty("status") StepStatusValue status, + @JsonProperty("started_at") Instant startedAt, + @JsonProperty("ended_at") Instant endedAt, + @JsonProperty("duration") String duration, + @JsonProperty("decision") UnifiedGateDecision decision, + @JsonProperty("decision_reason") String decisionReason, + @JsonProperty("policies_matched") List policiesMatched, + @JsonProperty("approval_status") UnifiedApprovalStatus approvalStatus, + @JsonProperty("approved_by") String approvedBy, + @JsonProperty("approved_at") Instant approvedAt, + @JsonProperty("model") String model, + @JsonProperty("provider") String provider, + @JsonProperty("cost_usd") Double costUsd, + @JsonProperty("input") Object input, + @JsonProperty("output") Object output, + @JsonProperty("result_summary") String resultSummary, + @JsonProperty("error") String error) { + this.stepId = stepId; + this.stepIndex = stepIndex; + this.stepName = stepName; + this.stepType = stepType; + this.status = status; + this.startedAt = startedAt; + this.endedAt = endedAt; + this.duration = duration; + this.decision = decision; + this.decisionReason = decisionReason; + this.policiesMatched = policiesMatched; + this.approvalStatus = approvalStatus; + this.approvedBy = approvedBy; + this.approvedAt = approvedAt; + this.model = model; + this.provider = provider; + this.costUsd = costUsd; + this.input = input; + this.output = output; + this.resultSummary = resultSummary; + this.error = error; } - /** - * Step type indicating what kind of operation the step performs. - */ - public enum UnifiedStepType { - LLM_CALL("llm_call"), - TOOL_CALL("tool_call"), - CONNECTOR_CALL("connector_call"), - HUMAN_TASK("human_task"), - SYNTHESIS("synthesis"), // MAP: result synthesis step - ACTION("action"), // Generic action step - GATE("gate"); // WCP: policy gate evaluation + public String getStepId() { + return stepId; + } - private final String value; + public int getStepIndex() { + return stepIndex; + } - UnifiedStepType(String value) { - this.value = value; - } + public String getStepName() { + return stepName; + } - @JsonValue - public String getValue() { - return value; - } + public UnifiedStepType getStepType() { + return stepType; + } - @JsonCreator - public static UnifiedStepType fromValue(String value) { - for (UnifiedStepType type : values()) { - if (type.value.equals(value)) { - return type; - } - } - throw new IllegalArgumentException("Unknown step type: " + value); - } + public StepStatusValue getStatus() { + return status; } - /** - * Gate decision values (applicable to both MAP and WCP). - */ - public enum UnifiedGateDecision { - ALLOW("allow"), - BLOCK("block"), - REQUIRE_APPROVAL("require_approval"); + public Instant getStartedAt() { + return startedAt; + } - private final String value; + public Instant getEndedAt() { + return endedAt; + } - UnifiedGateDecision(String value) { - this.value = value; - } + public String getDuration() { + return duration; + } - @JsonValue - public String getValue() { - return value; - } + public UnifiedGateDecision getDecision() { + return decision; + } - @JsonCreator - public static UnifiedGateDecision fromValue(String value) { - for (UnifiedGateDecision decision : values()) { - if (decision.value.equals(value)) { - return decision; - } - } - throw new IllegalArgumentException("Unknown gate decision: " + value); - } + public String getDecisionReason() { + return decisionReason; } - /** - * Approval status for require_approval decisions. - */ - public enum UnifiedApprovalStatus { - PENDING("pending"), - APPROVED("approved"), - REJECTED("rejected"); + public List getPoliciesMatched() { + return policiesMatched; + } - private final String value; + public UnifiedApprovalStatus getApprovalStatus() { + return approvalStatus; + } - UnifiedApprovalStatus(String value) { - this.value = value; - } + public String getApprovedBy() { + return approvedBy; + } - @JsonValue - public String getValue() { - return value; - } + public Instant getApprovedAt() { + return approvedAt; + } - @JsonCreator - public static UnifiedApprovalStatus fromValue(String value) { - for (UnifiedApprovalStatus status : values()) { - if (status.value.equals(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown approval status: " + value); - } + public String getModel() { + return model; } - /** - * Detailed information about an individual execution step. - */ - public static final class UnifiedStepStatus { - private final String stepId; - private final int stepIndex; - private final String stepName; - private final UnifiedStepType stepType; - private final StepStatusValue status; - private final Instant startedAt; - private final Instant endedAt; - private final String duration; - private final UnifiedGateDecision decision; - private final String decisionReason; - private final List policiesMatched; - private final UnifiedApprovalStatus approvalStatus; - private final String approvedBy; - private final Instant approvedAt; - private final String model; - private final String provider; - private final Double costUsd; - private final Object input; - private final Object output; - private final String resultSummary; - private final String error; - - @JsonCreator - public UnifiedStepStatus( - @JsonProperty("step_id") String stepId, - @JsonProperty("step_index") int stepIndex, - @JsonProperty("step_name") String stepName, - @JsonProperty("step_type") UnifiedStepType stepType, - @JsonProperty("status") StepStatusValue status, - @JsonProperty("started_at") Instant startedAt, - @JsonProperty("ended_at") Instant endedAt, - @JsonProperty("duration") String duration, - @JsonProperty("decision") UnifiedGateDecision decision, - @JsonProperty("decision_reason") String decisionReason, - @JsonProperty("policies_matched") List policiesMatched, - @JsonProperty("approval_status") UnifiedApprovalStatus approvalStatus, - @JsonProperty("approved_by") String approvedBy, - @JsonProperty("approved_at") Instant approvedAt, - @JsonProperty("model") String model, - @JsonProperty("provider") String provider, - @JsonProperty("cost_usd") Double costUsd, - @JsonProperty("input") Object input, - @JsonProperty("output") Object output, - @JsonProperty("result_summary") String resultSummary, - @JsonProperty("error") String error) { - this.stepId = stepId; - this.stepIndex = stepIndex; - this.stepName = stepName; - this.stepType = stepType; - this.status = status; - this.startedAt = startedAt; - this.endedAt = endedAt; - this.duration = duration; - this.decision = decision; - this.decisionReason = decisionReason; - this.policiesMatched = policiesMatched; - this.approvalStatus = approvalStatus; - this.approvedBy = approvedBy; - this.approvedAt = approvedAt; - this.model = model; - this.provider = provider; - this.costUsd = costUsd; - this.input = input; - this.output = output; - this.resultSummary = resultSummary; - this.error = error; - } + public String getProvider() { + return provider; + } - public String getStepId() { return stepId; } - public int getStepIndex() { return stepIndex; } - public String getStepName() { return stepName; } - public UnifiedStepType getStepType() { return stepType; } - public StepStatusValue getStatus() { return status; } - public Instant getStartedAt() { return startedAt; } - public Instant getEndedAt() { return endedAt; } - public String getDuration() { return duration; } - public UnifiedGateDecision getDecision() { return decision; } - public String getDecisionReason() { return decisionReason; } - public List getPoliciesMatched() { return policiesMatched; } - public UnifiedApprovalStatus getApprovalStatus() { return approvalStatus; } - public String getApprovedBy() { return approvedBy; } - public Instant getApprovedAt() { return approvedAt; } - public String getModel() { return model; } - public String getProvider() { return provider; } - public Double getCostUsd() { return costUsd; } - public Object getInput() { return input; } - public Object getOutput() { return output; } - public String getResultSummary() { return resultSummary; } - public String getError() { return error; } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - UnifiedStepStatus that = (UnifiedStepStatus) o; - return stepIndex == that.stepIndex && Objects.equals(stepId, that.stepId); - } + public Double getCostUsd() { + return costUsd; + } - @Override - public int hashCode() { - return Objects.hash(stepId, stepIndex); - } + public Object getInput() { + return input; + } - public static Builder builder() { - return new Builder(); - } + public Object getOutput() { + return output; + } - public static final class Builder { - private String stepId; - private int stepIndex; - private String stepName; - private UnifiedStepType stepType; - private StepStatusValue status; - private Instant startedAt; - private Instant endedAt; - private String duration; - private UnifiedGateDecision decision; - private String decisionReason; - private List policiesMatched; - private UnifiedApprovalStatus approvalStatus; - private String approvedBy; - private Instant approvedAt; - private String model; - private String provider; - private Double costUsd; - private Object input; - private Object output; - private String resultSummary; - private String error; - - public Builder stepId(String stepId) { this.stepId = stepId; return this; } - public Builder stepIndex(int stepIndex) { this.stepIndex = stepIndex; return this; } - public Builder stepName(String stepName) { this.stepName = stepName; return this; } - public Builder stepType(UnifiedStepType stepType) { this.stepType = stepType; return this; } - public Builder status(StepStatusValue status) { this.status = status; return this; } - public Builder startedAt(Instant startedAt) { this.startedAt = startedAt; return this; } - public Builder endedAt(Instant endedAt) { this.endedAt = endedAt; return this; } - public Builder duration(String duration) { this.duration = duration; return this; } - public Builder decision(UnifiedGateDecision decision) { this.decision = decision; return this; } - public Builder decisionReason(String decisionReason) { this.decisionReason = decisionReason; return this; } - public Builder policiesMatched(List policiesMatched) { this.policiesMatched = policiesMatched; return this; } - public Builder approvalStatus(UnifiedApprovalStatus approvalStatus) { this.approvalStatus = approvalStatus; return this; } - public Builder approvedBy(String approvedBy) { this.approvedBy = approvedBy; return this; } - public Builder approvedAt(Instant approvedAt) { this.approvedAt = approvedAt; return this; } - public Builder model(String model) { this.model = model; return this; } - public Builder provider(String provider) { this.provider = provider; return this; } - public Builder costUsd(Double costUsd) { this.costUsd = costUsd; return this; } - public Builder input(Object input) { this.input = input; return this; } - public Builder output(Object output) { this.output = output; return this; } - public Builder resultSummary(String resultSummary) { this.resultSummary = resultSummary; return this; } - public Builder error(String error) { this.error = error; return this; } - - public UnifiedStepStatus build() { - return new UnifiedStepStatus( - stepId, stepIndex, stepName, stepType, status, startedAt, endedAt, - duration, decision, decisionReason, policiesMatched, approvalStatus, - approvedBy, approvedAt, model, provider, costUsd, input, output, - resultSummary, error - ); - } - } + public String getResultSummary() { + return resultSummary; } - /** - * Unified execution status for both MAP plans and WCP workflows. - */ - public static final class ExecutionStatus { - private final String executionId; - private final ExecutionType executionType; - private final String name; - private final String source; - private final ExecutionStatusValue status; - private final int currentStepIndex; - private final int totalSteps; - private final double progressPercent; - private final Instant startedAt; - private final Instant completedAt; - private final String duration; - private final Double estimatedCostUsd; - private final Double actualCostUsd; - private final List steps; - private final String error; - private final String tenantId; - private final String orgId; - private final String userId; - private final String clientId; - private final Map metadata; - private final Instant createdAt; - private final Instant updatedAt; - - @JsonCreator - public ExecutionStatus( - @JsonProperty("execution_id") String executionId, - @JsonProperty("execution_type") ExecutionType executionType, - @JsonProperty("name") String name, - @JsonProperty("source") String source, - @JsonProperty("status") ExecutionStatusValue status, - @JsonProperty("current_step_index") int currentStepIndex, - @JsonProperty("total_steps") int totalSteps, - @JsonProperty("progress_percent") double progressPercent, - @JsonProperty("started_at") Instant startedAt, - @JsonProperty("completed_at") Instant completedAt, - @JsonProperty("duration") String duration, - @JsonProperty("estimated_cost_usd") Double estimatedCostUsd, - @JsonProperty("actual_cost_usd") Double actualCostUsd, - @JsonProperty("steps") List steps, - @JsonProperty("error") String error, - @JsonProperty("tenant_id") String tenantId, - @JsonProperty("org_id") String orgId, - @JsonProperty("user_id") String userId, - @JsonProperty("client_id") String clientId, - @JsonProperty("metadata") Map metadata, - @JsonProperty("created_at") Instant createdAt, - @JsonProperty("updated_at") Instant updatedAt) { - this.executionId = executionId; - this.executionType = executionType; - this.name = name; - this.source = source; - this.status = status; - this.currentStepIndex = currentStepIndex; - this.totalSteps = totalSteps; - this.progressPercent = progressPercent; - this.startedAt = startedAt; - this.completedAt = completedAt; - this.duration = duration; - this.estimatedCostUsd = estimatedCostUsd; - this.actualCostUsd = actualCostUsd; - this.steps = steps; - this.error = error; - this.tenantId = tenantId; - this.orgId = orgId; - this.userId = userId; - this.clientId = clientId; - this.metadata = metadata; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } + public String getError() { + return error; + } - public String getExecutionId() { return executionId; } - public ExecutionType getExecutionType() { return executionType; } - public String getName() { return name; } - public String getSource() { return source; } - public ExecutionStatusValue getStatus() { return status; } - public int getCurrentStepIndex() { return currentStepIndex; } - public int getTotalSteps() { return totalSteps; } - public double getProgressPercent() { return progressPercent; } - public Instant getStartedAt() { return startedAt; } - public Instant getCompletedAt() { return completedAt; } - public String getDuration() { return duration; } - public Double getEstimatedCostUsd() { return estimatedCostUsd; } - public Double getActualCostUsd() { return actualCostUsd; } - public List getSteps() { return steps; } - public String getError() { return error; } - public String getTenantId() { return tenantId; } - public String getOrgId() { return orgId; } - public String getUserId() { return userId; } - public String getClientId() { return clientId; } - public Map getMetadata() { return metadata; } - public Instant getCreatedAt() { return createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - - /** - * Check if the execution is in a terminal state. - */ - public boolean isTerminal() { - return status.isTerminal(); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UnifiedStepStatus that = (UnifiedStepStatus) o; + return stepIndex == that.stepIndex && Objects.equals(stepId, that.stepId); + } - /** - * Get the currently running step, if any. - */ - public UnifiedStepStatus getCurrentStep() { - if (steps == null) return null; - for (UnifiedStepStatus step : steps) { - if (step.getStatus() == StepStatusValue.RUNNING) { - return step; - } - } - return null; - } + @Override + public int hashCode() { + return Objects.hash(stepId, stepIndex); + } - /** - * Calculate total cost from all steps. - */ - public double calculateTotalCost() { - if (steps == null) return 0.0; - double total = 0.0; - for (UnifiedStepStatus step : steps) { - if (step.getCostUsd() != null) { - total += step.getCostUsd(); - } - } - return total; - } + public static Builder builder() { + return new Builder(); + } - /** - * Check if this is a MAP plan execution. - */ - public boolean isMapPlan() { - return executionType == ExecutionType.MAP_PLAN; - } + public static final class Builder { + private String stepId; + private int stepIndex; + private String stepName; + private UnifiedStepType stepType; + private StepStatusValue status; + private Instant startedAt; + private Instant endedAt; + private String duration; + private UnifiedGateDecision decision; + private String decisionReason; + private List policiesMatched; + private UnifiedApprovalStatus approvalStatus; + private String approvedBy; + private Instant approvedAt; + private String model; + private String provider; + private Double costUsd; + private Object input; + private Object output; + private String resultSummary; + private String error; + + public Builder stepId(String stepId) { + this.stepId = stepId; + return this; + } + + public Builder stepIndex(int stepIndex) { + this.stepIndex = stepIndex; + return this; + } + + public Builder stepName(String stepName) { + this.stepName = stepName; + return this; + } + + public Builder stepType(UnifiedStepType stepType) { + this.stepType = stepType; + return this; + } + + public Builder status(StepStatusValue status) { + this.status = status; + return this; + } + + public Builder startedAt(Instant startedAt) { + this.startedAt = startedAt; + return this; + } + + public Builder endedAt(Instant endedAt) { + this.endedAt = endedAt; + return this; + } + + public Builder duration(String duration) { + this.duration = duration; + return this; + } + + public Builder decision(UnifiedGateDecision decision) { + this.decision = decision; + return this; + } + + public Builder decisionReason(String decisionReason) { + this.decisionReason = decisionReason; + return this; + } + + public Builder policiesMatched(List policiesMatched) { + this.policiesMatched = policiesMatched; + return this; + } + + public Builder approvalStatus(UnifiedApprovalStatus approvalStatus) { + this.approvalStatus = approvalStatus; + return this; + } + + public Builder approvedBy(String approvedBy) { + this.approvedBy = approvedBy; + return this; + } + + public Builder approvedAt(Instant approvedAt) { + this.approvedAt = approvedAt; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + public Builder provider(String provider) { + this.provider = provider; + return this; + } + + public Builder costUsd(Double costUsd) { + this.costUsd = costUsd; + return this; + } + + public Builder input(Object input) { + this.input = input; + return this; + } + + public Builder output(Object output) { + this.output = output; + return this; + } + + public Builder resultSummary(String resultSummary) { + this.resultSummary = resultSummary; + return this; + } + + public Builder error(String error) { + this.error = error; + return this; + } + + public UnifiedStepStatus build() { + return new UnifiedStepStatus( + stepId, + stepIndex, + stepName, + stepType, + status, + startedAt, + endedAt, + duration, + decision, + decisionReason, + policiesMatched, + approvalStatus, + approvedBy, + approvedAt, + model, + provider, + costUsd, + input, + output, + resultSummary, + error); + } + } + } + + /** Unified execution status for both MAP plans and WCP workflows. */ + public static final class ExecutionStatus { + private final String executionId; + private final ExecutionType executionType; + private final String name; + private final String source; + private final ExecutionStatusValue status; + private final int currentStepIndex; + private final int totalSteps; + private final double progressPercent; + private final Instant startedAt; + private final Instant completedAt; + private final String duration; + private final Double estimatedCostUsd; + private final Double actualCostUsd; + private final List steps; + private final String error; + private final String tenantId; + private final String orgId; + private final String userId; + private final String clientId; + private final Map metadata; + private final Instant createdAt; + private final Instant updatedAt; + + @JsonCreator + public ExecutionStatus( + @JsonProperty("execution_id") String executionId, + @JsonProperty("execution_type") ExecutionType executionType, + @JsonProperty("name") String name, + @JsonProperty("source") String source, + @JsonProperty("status") ExecutionStatusValue status, + @JsonProperty("current_step_index") int currentStepIndex, + @JsonProperty("total_steps") int totalSteps, + @JsonProperty("progress_percent") double progressPercent, + @JsonProperty("started_at") Instant startedAt, + @JsonProperty("completed_at") Instant completedAt, + @JsonProperty("duration") String duration, + @JsonProperty("estimated_cost_usd") Double estimatedCostUsd, + @JsonProperty("actual_cost_usd") Double actualCostUsd, + @JsonProperty("steps") List steps, + @JsonProperty("error") String error, + @JsonProperty("tenant_id") String tenantId, + @JsonProperty("org_id") String orgId, + @JsonProperty("user_id") String userId, + @JsonProperty("client_id") String clientId, + @JsonProperty("metadata") Map metadata, + @JsonProperty("created_at") Instant createdAt, + @JsonProperty("updated_at") Instant updatedAt) { + this.executionId = executionId; + this.executionType = executionType; + this.name = name; + this.source = source; + this.status = status; + this.currentStepIndex = currentStepIndex; + this.totalSteps = totalSteps; + this.progressPercent = progressPercent; + this.startedAt = startedAt; + this.completedAt = completedAt; + this.duration = duration; + this.estimatedCostUsd = estimatedCostUsd; + this.actualCostUsd = actualCostUsd; + this.steps = steps; + this.error = error; + this.tenantId = tenantId; + this.orgId = orgId; + this.userId = userId; + this.clientId = clientId; + this.metadata = metadata; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } - /** - * Check if this is a WCP workflow execution. - */ - public boolean isWcpWorkflow() { - return executionType == ExecutionType.WCP_WORKFLOW; - } + public String getExecutionId() { + return executionId; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ExecutionStatus that = (ExecutionStatus) o; - return Objects.equals(executionId, that.executionId); - } + public ExecutionType getExecutionType() { + return executionType; + } - @Override - public int hashCode() { - return Objects.hash(executionId); - } + public String getName() { + return name; + } - public static Builder builder() { - return new Builder(); - } + public String getSource() { + return source; + } - public static final class Builder { - private String executionId; - private ExecutionType executionType; - private String name; - private String source; - private ExecutionStatusValue status; - private int currentStepIndex; - private int totalSteps; - private double progressPercent; - private Instant startedAt; - private Instant completedAt; - private String duration; - private Double estimatedCostUsd; - private Double actualCostUsd; - private List steps; - private String error; - private String tenantId; - private String orgId; - private String userId; - private String clientId; - private Map metadata; - private Instant createdAt; - private Instant updatedAt; - - public Builder executionId(String executionId) { this.executionId = executionId; return this; } - public Builder executionType(ExecutionType executionType) { this.executionType = executionType; return this; } - public Builder name(String name) { this.name = name; return this; } - public Builder source(String source) { this.source = source; return this; } - public Builder status(ExecutionStatusValue status) { this.status = status; return this; } - public Builder currentStepIndex(int currentStepIndex) { this.currentStepIndex = currentStepIndex; return this; } - public Builder totalSteps(int totalSteps) { this.totalSteps = totalSteps; return this; } - public Builder progressPercent(double progressPercent) { this.progressPercent = progressPercent; return this; } - public Builder startedAt(Instant startedAt) { this.startedAt = startedAt; return this; } - public Builder completedAt(Instant completedAt) { this.completedAt = completedAt; return this; } - public Builder duration(String duration) { this.duration = duration; return this; } - public Builder estimatedCostUsd(Double estimatedCostUsd) { this.estimatedCostUsd = estimatedCostUsd; return this; } - public Builder actualCostUsd(Double actualCostUsd) { this.actualCostUsd = actualCostUsd; return this; } - public Builder steps(List steps) { this.steps = steps; return this; } - public Builder error(String error) { this.error = error; return this; } - public Builder tenantId(String tenantId) { this.tenantId = tenantId; return this; } - public Builder orgId(String orgId) { this.orgId = orgId; return this; } - public Builder userId(String userId) { this.userId = userId; return this; } - public Builder clientId(String clientId) { this.clientId = clientId; return this; } - public Builder metadata(Map metadata) { this.metadata = metadata; return this; } - public Builder createdAt(Instant createdAt) { this.createdAt = createdAt; return this; } - public Builder updatedAt(Instant updatedAt) { this.updatedAt = updatedAt; return this; } - - public ExecutionStatus build() { - return new ExecutionStatus( - executionId, executionType, name, source, status, currentStepIndex, - totalSteps, progressPercent, startedAt, completedAt, duration, - estimatedCostUsd, actualCostUsd, steps, error, tenantId, orgId, - userId, clientId, metadata, createdAt, updatedAt - ); - } - } + public ExecutionStatusValue getStatus() { + return status; } - /** - * Request to list executions with optional filters. - */ - public static final class UnifiedListExecutionsRequest { - private final ExecutionType executionType; - private final ExecutionStatusValue status; - private final String tenantId; - private final String orgId; - private final int limit; - private final int offset; - - public UnifiedListExecutionsRequest( - ExecutionType executionType, ExecutionStatusValue status, - String tenantId, String orgId, int limit, int offset) { - this.executionType = executionType; - this.status = status; - this.tenantId = tenantId; - this.orgId = orgId; - this.limit = limit; - this.offset = offset; - } + public int getCurrentStepIndex() { + return currentStepIndex; + } - public ExecutionType getExecutionType() { return executionType; } - public ExecutionStatusValue getStatus() { return status; } - public String getTenantId() { return tenantId; } - public String getOrgId() { return orgId; } - public int getLimit() { return limit; } - public int getOffset() { return offset; } + public int getTotalSteps() { + return totalSteps; + } - public static Builder builder() { - return new Builder(); - } + public double getProgressPercent() { + return progressPercent; + } + + public Instant getStartedAt() { + return startedAt; + } + + public Instant getCompletedAt() { + return completedAt; + } + + public String getDuration() { + return duration; + } + + public Double getEstimatedCostUsd() { + return estimatedCostUsd; + } + + public Double getActualCostUsd() { + return actualCostUsd; + } + + public List getSteps() { + return steps; + } + + public String getError() { + return error; + } + + public String getTenantId() { + return tenantId; + } + + public String getOrgId() { + return orgId; + } + + public String getUserId() { + return userId; + } + + public String getClientId() { + return clientId; + } + + public Map getMetadata() { + return metadata; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + /** Check if the execution is in a terminal state. */ + public boolean isTerminal() { + return status.isTerminal(); + } - public static final class Builder { - private ExecutionType executionType; - private ExecutionStatusValue status; - private String tenantId; - private String orgId; - private int limit = 50; - private int offset = 0; - - public Builder executionType(ExecutionType executionType) { this.executionType = executionType; return this; } - public Builder status(ExecutionStatusValue status) { this.status = status; return this; } - public Builder tenantId(String tenantId) { this.tenantId = tenantId; return this; } - public Builder orgId(String orgId) { this.orgId = orgId; return this; } - public Builder limit(int limit) { this.limit = limit; return this; } - public Builder offset(int offset) { this.offset = offset; return this; } - - public UnifiedListExecutionsRequest build() { - return new UnifiedListExecutionsRequest( - executionType, status, tenantId, orgId, limit, offset - ); - } + /** Get the currently running step, if any. */ + public UnifiedStepStatus getCurrentStep() { + if (steps == null) return null; + for (UnifiedStepStatus step : steps) { + if (step.getStatus() == StepStatusValue.RUNNING) { + return step; } + } + return null; } - /** - * Paginated response for listing executions. - */ - public static final class UnifiedListExecutionsResponse { - private final List executions; - private final int total; - private final int limit; - private final int offset; - private final boolean hasMore; - - @JsonCreator - public UnifiedListExecutionsResponse( - @JsonProperty("executions") List executions, - @JsonProperty("total") int total, - @JsonProperty("limit") int limit, - @JsonProperty("offset") int offset, - @JsonProperty("has_more") boolean hasMore) { - this.executions = executions; - this.total = total; - this.limit = limit; - this.offset = offset; - this.hasMore = hasMore; + /** Calculate total cost from all steps. */ + public double calculateTotalCost() { + if (steps == null) return 0.0; + double total = 0.0; + for (UnifiedStepStatus step : steps) { + if (step.getCostUsd() != null) { + total += step.getCostUsd(); } + } + return total; + } + + /** Check if this is a MAP plan execution. */ + public boolean isMapPlan() { + return executionType == ExecutionType.MAP_PLAN; + } + + /** Check if this is a WCP workflow execution. */ + public boolean isWcpWorkflow() { + return executionType == ExecutionType.WCP_WORKFLOW; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ExecutionStatus that = (ExecutionStatus) o; + return Objects.equals(executionId, that.executionId); + } + + @Override + public int hashCode() { + return Objects.hash(executionId); + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String executionId; + private ExecutionType executionType; + private String name; + private String source; + private ExecutionStatusValue status; + private int currentStepIndex; + private int totalSteps; + private double progressPercent; + private Instant startedAt; + private Instant completedAt; + private String duration; + private Double estimatedCostUsd; + private Double actualCostUsd; + private List steps; + private String error; + private String tenantId; + private String orgId; + private String userId; + private String clientId; + private Map metadata; + private Instant createdAt; + private Instant updatedAt; + + public Builder executionId(String executionId) { + this.executionId = executionId; + return this; + } + + public Builder executionType(ExecutionType executionType) { + this.executionType = executionType; + return this; + } + + public Builder name(String name) { + this.name = name; + return this; + } + + public Builder source(String source) { + this.source = source; + return this; + } + + public Builder status(ExecutionStatusValue status) { + this.status = status; + return this; + } + + public Builder currentStepIndex(int currentStepIndex) { + this.currentStepIndex = currentStepIndex; + return this; + } + + public Builder totalSteps(int totalSteps) { + this.totalSteps = totalSteps; + return this; + } + + public Builder progressPercent(double progressPercent) { + this.progressPercent = progressPercent; + return this; + } + + public Builder startedAt(Instant startedAt) { + this.startedAt = startedAt; + return this; + } + + public Builder completedAt(Instant completedAt) { + this.completedAt = completedAt; + return this; + } + + public Builder duration(String duration) { + this.duration = duration; + return this; + } + + public Builder estimatedCostUsd(Double estimatedCostUsd) { + this.estimatedCostUsd = estimatedCostUsd; + return this; + } + + public Builder actualCostUsd(Double actualCostUsd) { + this.actualCostUsd = actualCostUsd; + return this; + } + + public Builder steps(List steps) { + this.steps = steps; + return this; + } + + public Builder error(String error) { + this.error = error; + return this; + } + + public Builder tenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + public Builder orgId(String orgId) { + this.orgId = orgId; + return this; + } + + public Builder userId(String userId) { + this.userId = userId; + return this; + } + + public Builder clientId(String clientId) { + this.clientId = clientId; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public Builder createdAt(Instant createdAt) { + this.createdAt = createdAt; + return this; + } + + public Builder updatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + return this; + } + + public ExecutionStatus build() { + return new ExecutionStatus( + executionId, + executionType, + name, + source, + status, + currentStepIndex, + totalSteps, + progressPercent, + startedAt, + completedAt, + duration, + estimatedCostUsd, + actualCostUsd, + steps, + error, + tenantId, + orgId, + userId, + clientId, + metadata, + createdAt, + updatedAt); + } + } + } + + /** Request to list executions with optional filters. */ + public static final class UnifiedListExecutionsRequest { + private final ExecutionType executionType; + private final ExecutionStatusValue status; + private final String tenantId; + private final String orgId; + private final int limit; + private final int offset; + + public UnifiedListExecutionsRequest( + ExecutionType executionType, + ExecutionStatusValue status, + String tenantId, + String orgId, + int limit, + int offset) { + this.executionType = executionType; + this.status = status; + this.tenantId = tenantId; + this.orgId = orgId; + this.limit = limit; + this.offset = offset; + } + + public ExecutionType getExecutionType() { + return executionType; + } + + public ExecutionStatusValue getStatus() { + return status; + } + + public String getTenantId() { + return tenantId; + } + + public String getOrgId() { + return orgId; + } + + public int getLimit() { + return limit; + } + + public int getOffset() { + return offset; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private ExecutionType executionType; + private ExecutionStatusValue status; + private String tenantId; + private String orgId; + private int limit = 50; + private int offset = 0; + + public Builder executionType(ExecutionType executionType) { + this.executionType = executionType; + return this; + } + + public Builder status(ExecutionStatusValue status) { + this.status = status; + return this; + } + + public Builder tenantId(String tenantId) { + this.tenantId = tenantId; + return this; + } + + public Builder orgId(String orgId) { + this.orgId = orgId; + return this; + } + + public Builder limit(int limit) { + this.limit = limit; + return this; + } + + public Builder offset(int offset) { + this.offset = offset; + return this; + } + + public UnifiedListExecutionsRequest build() { + return new UnifiedListExecutionsRequest( + executionType, status, tenantId, orgId, limit, offset); + } + } + } + + /** Paginated response for listing executions. */ + public static final class UnifiedListExecutionsResponse { + private final List executions; + private final int total; + private final int limit; + private final int offset; + private final boolean hasMore; + + @JsonCreator + public UnifiedListExecutionsResponse( + @JsonProperty("executions") List executions, + @JsonProperty("total") int total, + @JsonProperty("limit") int limit, + @JsonProperty("offset") int offset, + @JsonProperty("has_more") boolean hasMore) { + this.executions = executions; + this.total = total; + this.limit = limit; + this.offset = offset; + this.hasMore = hasMore; + } + + public List getExecutions() { + return executions; + } + + public int getTotal() { + return total; + } + + public int getLimit() { + return limit; + } + + public int getOffset() { + return offset; + } - public List getExecutions() { return executions; } - public int getTotal() { return total; } - public int getLimit() { return limit; } - public int getOffset() { return offset; } - public boolean isHasMore() { return hasMore; } + public boolean isHasMore() { + return hasMore; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/execution/package-info.java b/src/main/java/com/getaxonflow/sdk/types/execution/package-info.java index 8a7a50d..714ae68 100644 --- a/src/main/java/com/getaxonflow/sdk/types/execution/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/execution/package-info.java @@ -16,10 +16,10 @@ /** * Unified Execution Tracking types for AxonFlow SDK. - *

- * This package provides types for unified tracking of both MAP (Multi-Agent Planning) - * and WCP (Workflow Control Plane) executions through a consistent interface. - *

- * Issue #1075 - EPIC #1074: Unified Workflow Infrastructure + * + *

This package provides types for unified tracking of both MAP (Multi-Agent Planning) and WCP + * (Workflow Control Plane) executions through a consistent interface. + * + *

Issue #1075 - EPIC #1074: Unified Workflow Infrastructure */ package com.getaxonflow.sdk.types.execution; diff --git a/src/main/java/com/getaxonflow/sdk/types/executionreplay/ExecutionReplayTypes.java b/src/main/java/com/getaxonflow/sdk/types/executionreplay/ExecutionReplayTypes.java index 6e829c4..e25128e 100644 --- a/src/main/java/com/getaxonflow/sdk/types/executionreplay/ExecutionReplayTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/executionreplay/ExecutionReplayTypes.java @@ -17,444 +17,709 @@ import com.fasterxml.jackson.annotation.JsonProperty; import java.util.List; -import java.util.Objects; /** * Execution Replay API types for debugging and compliance. * - *

The Execution Replay API captures every step of workflow execution - * for debugging, auditing, and compliance purposes. + *

The Execution Replay API captures every step of workflow execution for debugging, auditing, + * and compliance purposes. */ public final class ExecutionReplayTypes { - private ExecutionReplayTypes() {} + private ExecutionReplayTypes() {} - /** - * Execution summary representing a workflow execution. - */ - public static final class ExecutionSummary { - @JsonProperty("request_id") - private String requestId; + /** Execution summary representing a workflow execution. */ + public static final class ExecutionSummary { + @JsonProperty("request_id") + private String requestId; - @JsonProperty("workflow_name") - private String workflowName; + @JsonProperty("workflow_name") + private String workflowName; - @JsonProperty("status") - private String status; + @JsonProperty("status") + private String status; - @JsonProperty("total_steps") - private int totalSteps; + @JsonProperty("total_steps") + private int totalSteps; - @JsonProperty("completed_steps") - private int completedSteps; + @JsonProperty("completed_steps") + private int completedSteps; - @JsonProperty("started_at") - private String startedAt; + @JsonProperty("started_at") + private String startedAt; - @JsonProperty("completed_at") - private String completedAt; + @JsonProperty("completed_at") + private String completedAt; - @JsonProperty("duration_ms") - private Integer durationMs; + @JsonProperty("duration_ms") + private Integer durationMs; - @JsonProperty("total_tokens") - private int totalTokens; + @JsonProperty("total_tokens") + private int totalTokens; - @JsonProperty("total_cost_usd") - private double totalCostUsd; + @JsonProperty("total_cost_usd") + private double totalCostUsd; - @JsonProperty("org_id") - private String orgId; + @JsonProperty("org_id") + private String orgId; - @JsonProperty("tenant_id") - private String tenantId; + @JsonProperty("tenant_id") + private String tenantId; - @JsonProperty("user_id") - private String userId; + @JsonProperty("user_id") + private String userId; - @JsonProperty("error_message") - private String errorMessage; + @JsonProperty("error_message") + private String errorMessage; - @JsonProperty("input_summary") - private Object inputSummary; + @JsonProperty("input_summary") + private Object inputSummary; - @JsonProperty("output_summary") - private Object outputSummary; + @JsonProperty("output_summary") + private Object outputSummary; - public ExecutionSummary() {} + public ExecutionSummary() {} - public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { this.requestId = requestId; } + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getWorkflowName() { + return workflowName; + } + + public void setWorkflowName(String workflowName) { + this.workflowName = workflowName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public int getTotalSteps() { + return totalSteps; + } + + public void setTotalSteps(int totalSteps) { + this.totalSteps = totalSteps; + } + + public int getCompletedSteps() { + return completedSteps; + } + + public void setCompletedSteps(int completedSteps) { + this.completedSteps = completedSteps; + } + + public String getStartedAt() { + return startedAt; + } + + public void setStartedAt(String startedAt) { + this.startedAt = startedAt; + } + + public String getCompletedAt() { + return completedAt; + } + + public void setCompletedAt(String completedAt) { + this.completedAt = completedAt; + } + + public Integer getDurationMs() { + return durationMs; + } + + public void setDurationMs(Integer durationMs) { + this.durationMs = durationMs; + } + + public int getTotalTokens() { + return totalTokens; + } + + public void setTotalTokens(int totalTokens) { + this.totalTokens = totalTokens; + } + + public double getTotalCostUsd() { + return totalCostUsd; + } + + public void setTotalCostUsd(double totalCostUsd) { + this.totalCostUsd = totalCostUsd; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public Object getInputSummary() { + return inputSummary; + } + + public void setInputSummary(Object inputSummary) { + this.inputSummary = inputSummary; + } + + public Object getOutputSummary() { + return outputSummary; + } + + public void setOutputSummary(Object outputSummary) { + this.outputSummary = outputSummary; + } + } + + /** Execution snapshot representing a step in a workflow execution. */ + public static final class ExecutionSnapshot { + @JsonProperty("request_id") + private String requestId; + + @JsonProperty("step_index") + private int stepIndex; + + @JsonProperty("step_name") + private String stepName; + + @JsonProperty("status") + private String status; + + @JsonProperty("started_at") + private String startedAt; + + @JsonProperty("completed_at") + private String completedAt; + + @JsonProperty("duration_ms") + private Integer durationMs; + + @JsonProperty("provider") + private String provider; + + @JsonProperty("model") + private String model; + + @JsonProperty("tokens_in") + private int tokensIn; + + @JsonProperty("tokens_out") + private int tokensOut; + + @JsonProperty("cost_usd") + private double costUsd; + + @JsonProperty("input") + private Object input; + + @JsonProperty("output") + private Object output; + + @JsonProperty("error_message") + private String errorMessage; + + @JsonProperty("policies_checked") + private List policiesChecked; + + @JsonProperty("policies_triggered") + private List policiesTriggered; + + @JsonProperty("approval_required") + private boolean approvalRequired; + + @JsonProperty("approved_by") + private String approvedBy; + + @JsonProperty("approved_at") + private String approvedAt; + + public ExecutionSnapshot() {} + + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public int getStepIndex() { + return stepIndex; + } + + public void setStepIndex(int stepIndex) { + this.stepIndex = stepIndex; + } - public String getWorkflowName() { return workflowName; } - public void setWorkflowName(String workflowName) { this.workflowName = workflowName; } + public String getStepName() { + return stepName; + } + + public void setStepName(String stepName) { + this.stepName = stepName; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getStartedAt() { + return startedAt; + } + + public void setStartedAt(String startedAt) { + this.startedAt = startedAt; + } + + public String getCompletedAt() { + return completedAt; + } + + public void setCompletedAt(String completedAt) { + this.completedAt = completedAt; + } + + public Integer getDurationMs() { + return durationMs; + } + + public void setDurationMs(Integer durationMs) { + this.durationMs = durationMs; + } + + public String getProvider() { + return provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + public String getModel() { + return model; + } - public int getTotalSteps() { return totalSteps; } - public void setTotalSteps(int totalSteps) { this.totalSteps = totalSteps; } + public void setModel(String model) { + this.model = model; + } - public int getCompletedSteps() { return completedSteps; } - public void setCompletedSteps(int completedSteps) { this.completedSteps = completedSteps; } + public int getTokensIn() { + return tokensIn; + } - public String getStartedAt() { return startedAt; } - public void setStartedAt(String startedAt) { this.startedAt = startedAt; } + public void setTokensIn(int tokensIn) { + this.tokensIn = tokensIn; + } - public String getCompletedAt() { return completedAt; } - public void setCompletedAt(String completedAt) { this.completedAt = completedAt; } + public int getTokensOut() { + return tokensOut; + } - public Integer getDurationMs() { return durationMs; } - public void setDurationMs(Integer durationMs) { this.durationMs = durationMs; } + public void setTokensOut(int tokensOut) { + this.tokensOut = tokensOut; + } - public int getTotalTokens() { return totalTokens; } - public void setTotalTokens(int totalTokens) { this.totalTokens = totalTokens; } + public double getCostUsd() { + return costUsd; + } - public double getTotalCostUsd() { return totalCostUsd; } - public void setTotalCostUsd(double totalCostUsd) { this.totalCostUsd = totalCostUsd; } + public void setCostUsd(double costUsd) { + this.costUsd = costUsd; + } - public String getOrgId() { return orgId; } - public void setOrgId(String orgId) { this.orgId = orgId; } + public Object getInput() { + return input; + } - public String getTenantId() { return tenantId; } - public void setTenantId(String tenantId) { this.tenantId = tenantId; } + public void setInput(Object input) { + this.input = input; + } - public String getUserId() { return userId; } - public void setUserId(String userId) { this.userId = userId; } + public Object getOutput() { + return output; + } - public String getErrorMessage() { return errorMessage; } - public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + public void setOutput(Object output) { + this.output = output; + } - public Object getInputSummary() { return inputSummary; } - public void setInputSummary(Object inputSummary) { this.inputSummary = inputSummary; } + public String getErrorMessage() { + return errorMessage; + } - public Object getOutputSummary() { return outputSummary; } - public void setOutputSummary(Object outputSummary) { this.outputSummary = outputSummary; } + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; } - /** - * Execution snapshot representing a step in a workflow execution. - */ - public static final class ExecutionSnapshot { - @JsonProperty("request_id") - private String requestId; + public List getPoliciesChecked() { + return policiesChecked; + } - @JsonProperty("step_index") - private int stepIndex; + public void setPoliciesChecked(List policiesChecked) { + this.policiesChecked = policiesChecked; + } - @JsonProperty("step_name") - private String stepName; + public List getPoliciesTriggered() { + return policiesTriggered; + } - @JsonProperty("status") - private String status; + public void setPoliciesTriggered(List policiesTriggered) { + this.policiesTriggered = policiesTriggered; + } - @JsonProperty("started_at") - private String startedAt; + public boolean isApprovalRequired() { + return approvalRequired; + } - @JsonProperty("completed_at") - private String completedAt; + public void setApprovalRequired(boolean approvalRequired) { + this.approvalRequired = approvalRequired; + } - @JsonProperty("duration_ms") - private Integer durationMs; + public String getApprovedBy() { + return approvedBy; + } - @JsonProperty("provider") - private String provider; + public void setApprovedBy(String approvedBy) { + this.approvedBy = approvedBy; + } + + public String getApprovedAt() { + return approvedAt; + } - @JsonProperty("model") - private String model; + public void setApprovedAt(String approvedAt) { + this.approvedAt = approvedAt; + } + } - @JsonProperty("tokens_in") - private int tokensIn; + /** Timeline entry for execution visualization. */ + public static final class TimelineEntry { + @JsonProperty("step_index") + private int stepIndex; - @JsonProperty("tokens_out") - private int tokensOut; + @JsonProperty("step_name") + private String stepName; - @JsonProperty("cost_usd") - private double costUsd; + @JsonProperty("status") + private String status; - @JsonProperty("input") - private Object input; + @JsonProperty("started_at") + private String startedAt; - @JsonProperty("output") - private Object output; + @JsonProperty("completed_at") + private String completedAt; - @JsonProperty("error_message") - private String errorMessage; + @JsonProperty("duration_ms") + private Integer durationMs; - @JsonProperty("policies_checked") - private List policiesChecked; + @JsonProperty("has_error") + private boolean hasError; - @JsonProperty("policies_triggered") - private List policiesTriggered; + @JsonProperty("has_approval") + private boolean hasApproval; - @JsonProperty("approval_required") - private boolean approvalRequired; + public TimelineEntry() {} - @JsonProperty("approved_by") - private String approvedBy; + public int getStepIndex() { + return stepIndex; + } - @JsonProperty("approved_at") - private String approvedAt; + public void setStepIndex(int stepIndex) { + this.stepIndex = stepIndex; + } - public ExecutionSnapshot() {} + public String getStepName() { + return stepName; + } - public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { this.requestId = requestId; } + public void setStepName(String stepName) { + this.stepName = stepName; + } - public int getStepIndex() { return stepIndex; } - public void setStepIndex(int stepIndex) { this.stepIndex = stepIndex; } + public String getStatus() { + return status; + } - public String getStepName() { return stepName; } - public void setStepName(String stepName) { this.stepName = stepName; } + public void setStatus(String status) { + this.status = status; + } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + public String getStartedAt() { + return startedAt; + } - public String getStartedAt() { return startedAt; } - public void setStartedAt(String startedAt) { this.startedAt = startedAt; } + public void setStartedAt(String startedAt) { + this.startedAt = startedAt; + } - public String getCompletedAt() { return completedAt; } - public void setCompletedAt(String completedAt) { this.completedAt = completedAt; } + public String getCompletedAt() { + return completedAt; + } - public Integer getDurationMs() { return durationMs; } - public void setDurationMs(Integer durationMs) { this.durationMs = durationMs; } + public void setCompletedAt(String completedAt) { + this.completedAt = completedAt; + } - public String getProvider() { return provider; } - public void setProvider(String provider) { this.provider = provider; } + public Integer getDurationMs() { + return durationMs; + } - public String getModel() { return model; } - public void setModel(String model) { this.model = model; } + public void setDurationMs(Integer durationMs) { + this.durationMs = durationMs; + } - public int getTokensIn() { return tokensIn; } - public void setTokensIn(int tokensIn) { this.tokensIn = tokensIn; } + public boolean hasError() { + return hasError; + } - public int getTokensOut() { return tokensOut; } - public void setTokensOut(int tokensOut) { this.tokensOut = tokensOut; } + public void setHasError(boolean hasError) { + this.hasError = hasError; + } - public double getCostUsd() { return costUsd; } - public void setCostUsd(double costUsd) { this.costUsd = costUsd; } + public boolean hasApproval() { + return hasApproval; + } - public Object getInput() { return input; } - public void setInput(Object input) { this.input = input; } + public void setHasApproval(boolean hasApproval) { + this.hasApproval = hasApproval; + } + } - public Object getOutput() { return output; } - public void setOutput(Object output) { this.output = output; } + /** Response from list executions API. */ + public static final class ListExecutionsResponse { + @JsonProperty("executions") + private List executions; - public String getErrorMessage() { return errorMessage; } - public void setErrorMessage(String errorMessage) { this.errorMessage = errorMessage; } + @JsonProperty("total") + private int total; - public List getPoliciesChecked() { return policiesChecked; } - public void setPoliciesChecked(List policiesChecked) { this.policiesChecked = policiesChecked; } + @JsonProperty("limit") + private int limit; - public List getPoliciesTriggered() { return policiesTriggered; } - public void setPoliciesTriggered(List policiesTriggered) { this.policiesTriggered = policiesTriggered; } + @JsonProperty("offset") + private int offset; - public boolean isApprovalRequired() { return approvalRequired; } - public void setApprovalRequired(boolean approvalRequired) { this.approvalRequired = approvalRequired; } + public ListExecutionsResponse() {} - public String getApprovedBy() { return approvedBy; } - public void setApprovedBy(String approvedBy) { this.approvedBy = approvedBy; } + public List getExecutions() { + return executions; + } - public String getApprovedAt() { return approvedAt; } - public void setApprovedAt(String approvedAt) { this.approvedAt = approvedAt; } + public void setExecutions(List executions) { + this.executions = executions; } - /** - * Timeline entry for execution visualization. - */ - public static final class TimelineEntry { - @JsonProperty("step_index") - private int stepIndex; + public int getTotal() { + return total; + } - @JsonProperty("step_name") - private String stepName; + public void setTotal(int total) { + this.total = total; + } - @JsonProperty("status") - private String status; + public int getLimit() { + return limit; + } - @JsonProperty("started_at") - private String startedAt; + public void setLimit(int limit) { + this.limit = limit; + } - @JsonProperty("completed_at") - private String completedAt; + public int getOffset() { + return offset; + } - @JsonProperty("duration_ms") - private Integer durationMs; + public void setOffset(int offset) { + this.offset = offset; + } + } - @JsonProperty("has_error") - private boolean hasError; + /** Full execution with summary and steps. */ + public static final class ExecutionDetail { + @JsonProperty("summary") + private ExecutionSummary summary; - @JsonProperty("has_approval") - private boolean hasApproval; + @JsonProperty("steps") + private List steps; - public TimelineEntry() {} + public ExecutionDetail() {} - public int getStepIndex() { return stepIndex; } - public void setStepIndex(int stepIndex) { this.stepIndex = stepIndex; } + public ExecutionSummary getSummary() { + return summary; + } - public String getStepName() { return stepName; } - public void setStepName(String stepName) { this.stepName = stepName; } + public void setSummary(ExecutionSummary summary) { + this.summary = summary; + } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + public List getSteps() { + return steps; + } - public String getStartedAt() { return startedAt; } - public void setStartedAt(String startedAt) { this.startedAt = startedAt; } + public void setSteps(List steps) { + this.steps = steps; + } + } - public String getCompletedAt() { return completedAt; } - public void setCompletedAt(String completedAt) { this.completedAt = completedAt; } + /** Options for listing executions. */ + public static final class ListExecutionsOptions { + private Integer limit; + private Integer offset; + private String status; + private String workflowId; + private String startTime; + private String endTime; - public Integer getDurationMs() { return durationMs; } - public void setDurationMs(Integer durationMs) { this.durationMs = durationMs; } + public ListExecutionsOptions() {} - public boolean hasError() { return hasError; } - public void setHasError(boolean hasError) { this.hasError = hasError; } + public Integer getLimit() { + return limit; + } - public boolean hasApproval() { return hasApproval; } - public void setHasApproval(boolean hasApproval) { this.hasApproval = hasApproval; } + public ListExecutionsOptions setLimit(Integer limit) { + this.limit = limit; + return this; } - /** - * Response from list executions API. - */ - public static final class ListExecutionsResponse { - @JsonProperty("executions") - private List executions; + public Integer getOffset() { + return offset; + } - @JsonProperty("total") - private int total; + public ListExecutionsOptions setOffset(Integer offset) { + this.offset = offset; + return this; + } - @JsonProperty("limit") - private int limit; + public String getStatus() { + return status; + } - @JsonProperty("offset") - private int offset; + public ListExecutionsOptions setStatus(String status) { + this.status = status; + return this; + } - public ListExecutionsResponse() {} + public String getWorkflowId() { + return workflowId; + } - public List getExecutions() { return executions; } - public void setExecutions(List executions) { this.executions = executions; } + public ListExecutionsOptions setWorkflowId(String workflowId) { + this.workflowId = workflowId; + return this; + } - public int getTotal() { return total; } - public void setTotal(int total) { this.total = total; } + public String getStartTime() { + return startTime; + } - public int getLimit() { return limit; } - public void setLimit(int limit) { this.limit = limit; } + public ListExecutionsOptions setStartTime(String startTime) { + this.startTime = startTime; + return this; + } - public int getOffset() { return offset; } - public void setOffset(int offset) { this.offset = offset; } + public String getEndTime() { + return endTime; } - /** - * Full execution with summary and steps. - */ - public static final class ExecutionDetail { - @JsonProperty("summary") - private ExecutionSummary summary; + public ListExecutionsOptions setEndTime(String endTime) { + this.endTime = endTime; + return this; + } - @JsonProperty("steps") - private List steps; + public static ListExecutionsOptions builder() { + return new ListExecutionsOptions(); + } + } - public ExecutionDetail() {} + /** Options for exporting an execution. */ + public static final class ExecutionExportOptions { + private String format = "json"; + private boolean includeInput = true; + private boolean includeOutput = true; + private boolean includePolicies = true; - public ExecutionSummary getSummary() { return summary; } - public void setSummary(ExecutionSummary summary) { this.summary = summary; } + public ExecutionExportOptions() {} - public List getSteps() { return steps; } - public void setSteps(List steps) { this.steps = steps; } + public String getFormat() { + return format; } - /** - * Options for listing executions. - */ - public static final class ListExecutionsOptions { - private Integer limit; - private Integer offset; - private String status; - private String workflowId; - private String startTime; - private String endTime; + public ExecutionExportOptions setFormat(String format) { + this.format = format; + return this; + } - public ListExecutionsOptions() {} + public boolean isIncludeInput() { + return includeInput; + } - public Integer getLimit() { return limit; } - public ListExecutionsOptions setLimit(Integer limit) { - this.limit = limit; - return this; - } + public ExecutionExportOptions setIncludeInput(boolean includeInput) { + this.includeInput = includeInput; + return this; + } - public Integer getOffset() { return offset; } - public ListExecutionsOptions setOffset(Integer offset) { - this.offset = offset; - return this; - } + public boolean isIncludeOutput() { + return includeOutput; + } - public String getStatus() { return status; } - public ListExecutionsOptions setStatus(String status) { - this.status = status; - return this; - } + public ExecutionExportOptions setIncludeOutput(boolean includeOutput) { + this.includeOutput = includeOutput; + return this; + } - public String getWorkflowId() { return workflowId; } - public ListExecutionsOptions setWorkflowId(String workflowId) { - this.workflowId = workflowId; - return this; - } + public boolean isIncludePolicies() { + return includePolicies; + } - public String getStartTime() { return startTime; } - public ListExecutionsOptions setStartTime(String startTime) { - this.startTime = startTime; - return this; - } + public ExecutionExportOptions setIncludePolicies(boolean includePolicies) { + this.includePolicies = includePolicies; + return this; + } - public String getEndTime() { return endTime; } - public ListExecutionsOptions setEndTime(String endTime) { - this.endTime = endTime; - return this; - } - - public static ListExecutionsOptions builder() { - return new ListExecutionsOptions(); - } - } - - /** - * Options for exporting an execution. - */ - public static final class ExecutionExportOptions { - private String format = "json"; - private boolean includeInput = true; - private boolean includeOutput = true; - private boolean includePolicies = true; - - public ExecutionExportOptions() {} - - public String getFormat() { return format; } - public ExecutionExportOptions setFormat(String format) { - this.format = format; - return this; - } - - public boolean isIncludeInput() { return includeInput; } - public ExecutionExportOptions setIncludeInput(boolean includeInput) { - this.includeInput = includeInput; - return this; - } - - public boolean isIncludeOutput() { return includeOutput; } - public ExecutionExportOptions setIncludeOutput(boolean includeOutput) { - this.includeOutput = includeOutput; - return this; - } - - public boolean isIncludePolicies() { return includePolicies; } - public ExecutionExportOptions setIncludePolicies(boolean includePolicies) { - this.includePolicies = includePolicies; - return this; - } - - public static ExecutionExportOptions builder() { - return new ExecutionExportOptions(); - } + public static ExecutionExportOptions builder() { + return new ExecutionExportOptions(); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/executionreplay/package-info.java b/src/main/java/com/getaxonflow/sdk/types/executionreplay/package-info.java index d58d352..07df87b 100644 --- a/src/main/java/com/getaxonflow/sdk/types/executionreplay/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/executionreplay/package-info.java @@ -17,8 +17,8 @@ /** * Execution Replay types for debugging and compliance. * - *

The Execution Replay API captures every step of workflow execution - * for debugging, auditing, and compliance purposes. + *

The Execution Replay API captures every step of workflow execution for debugging, auditing, + * and compliance purposes. * * @see com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes */ diff --git a/src/main/java/com/getaxonflow/sdk/types/hitl/HITLTypes.java b/src/main/java/com/getaxonflow/sdk/types/hitl/HITLTypes.java index ba02cfb..d1b1233 100644 --- a/src/main/java/com/getaxonflow/sdk/types/hitl/HITLTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/hitl/HITLTypes.java @@ -17,7 +17,6 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.List; import java.util.Map; @@ -25,410 +24,584 @@ * Human-in-the-Loop (HITL) Queue types for AxonFlow SDK. * *

This class contains all types needed for HITL queue operations including: + * *

    - *
  • Listing pending approval requests
  • - *
  • Getting individual approval request details
  • - *
  • Approving or rejecting requests
  • - *
  • Retrieving dashboard statistics
  • + *
  • Listing pending approval requests + *
  • Getting individual approval request details + *
  • Approving or rejecting requests + *
  • Retrieving dashboard statistics *
* *

Enterprise Feature: Requires AxonFlow Enterprise license. */ public final class HITLTypes { - private HITLTypes() { - // Utility class + private HITLTypes() { + // Utility class + } + + // ======================================================================== + // Approval Request + // ======================================================================== + + /** + * A pending HITL approval request. + * + *

Represents a request that has been paused by a policy trigger and requires human review + * before proceeding. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HITLApprovalRequest { + + @JsonProperty("request_id") + private String requestId; + + @JsonProperty("org_id") + private String orgId; + + @JsonProperty("tenant_id") + private String tenantId; + + @JsonProperty("client_id") + private String clientId; + + @JsonProperty("user_id") + private String userId; + + @JsonProperty("original_query") + private String originalQuery; + + @JsonProperty("request_type") + private String requestType; + + @JsonProperty("request_context") + private Map requestContext; + + @JsonProperty("triggered_policy_id") + private String triggeredPolicyId; + + @JsonProperty("triggered_policy_name") + private String triggeredPolicyName; + + @JsonProperty("trigger_reason") + private String triggerReason; + + @JsonProperty("severity") + private String severity; + + @JsonProperty("eu_ai_act_article") + private String euAiActArticle; + + @JsonProperty("compliance_framework") + private String complianceFramework; + + @JsonProperty("risk_classification") + private String riskClassification; + + @JsonProperty("status") + private String status; + + @JsonProperty("reviewer_id") + private String reviewerId; + + @JsonProperty("reviewer_email") + private String reviewerEmail; + + @JsonProperty("review_comment") + private String reviewComment; + + @JsonProperty("reviewed_at") + private String reviewedAt; + + @JsonProperty("expires_at") + private String expiresAt; + + @JsonProperty("created_at") + private String createdAt; + + @JsonProperty("updated_at") + private String updatedAt; + + public HITLApprovalRequest() {} + + // Getters and setters + public String getRequestId() { + return requestId; + } + + public void setRequestId(String requestId) { + this.requestId = requestId; + } + + public String getOrgId() { + return orgId; + } + + public void setOrgId(String orgId) { + this.orgId = orgId; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public String getClientId() { + return clientId; + } + + public void setClientId(String clientId) { + this.clientId = clientId; + } + + public String getUserId() { + return userId; + } + + public void setUserId(String userId) { + this.userId = userId; + } + + public String getOriginalQuery() { + return originalQuery; + } + + public void setOriginalQuery(String originalQuery) { + this.originalQuery = originalQuery; + } + + public String getRequestType() { + return requestType; + } + + public void setRequestType(String requestType) { + this.requestType = requestType; + } + + public Map getRequestContext() { + return requestContext; + } + + public void setRequestContext(Map requestContext) { + this.requestContext = requestContext; + } + + public String getTriggeredPolicyId() { + return triggeredPolicyId; + } + + public void setTriggeredPolicyId(String triggeredPolicyId) { + this.triggeredPolicyId = triggeredPolicyId; + } + + public String getTriggeredPolicyName() { + return triggeredPolicyName; + } + + public void setTriggeredPolicyName(String triggeredPolicyName) { + this.triggeredPolicyName = triggeredPolicyName; + } + + public String getTriggerReason() { + return triggerReason; + } + + public void setTriggerReason(String triggerReason) { + this.triggerReason = triggerReason; + } + + public String getSeverity() { + return severity; + } + + public void setSeverity(String severity) { + this.severity = severity; + } + + public String getEuAiActArticle() { + return euAiActArticle; + } + + public void setEuAiActArticle(String euAiActArticle) { + this.euAiActArticle = euAiActArticle; + } + + public String getComplianceFramework() { + return complianceFramework; + } + + public void setComplianceFramework(String complianceFramework) { + this.complianceFramework = complianceFramework; + } + + public String getRiskClassification() { + return riskClassification; } - // ======================================================================== - // Approval Request - // ======================================================================== + public void setRiskClassification(String riskClassification) { + this.riskClassification = riskClassification; + } - /** - * A pending HITL approval request. - * - *

Represents a request that has been paused by a policy trigger and - * requires human review before proceeding. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static class HITLApprovalRequest { + public String getStatus() { + return status; + } - @JsonProperty("request_id") - private String requestId; + public void setStatus(String status) { + this.status = status; + } - @JsonProperty("org_id") - private String orgId; + public String getReviewerId() { + return reviewerId; + } - @JsonProperty("tenant_id") - private String tenantId; + public void setReviewerId(String reviewerId) { + this.reviewerId = reviewerId; + } + + public String getReviewerEmail() { + return reviewerEmail; + } + + public void setReviewerEmail(String reviewerEmail) { + this.reviewerEmail = reviewerEmail; + } + + public String getReviewComment() { + return reviewComment; + } + + public void setReviewComment(String reviewComment) { + this.reviewComment = reviewComment; + } + + public String getReviewedAt() { + return reviewedAt; + } + + public void setReviewedAt(String reviewedAt) { + this.reviewedAt = reviewedAt; + } - @JsonProperty("client_id") - private String clientId; + public String getExpiresAt() { + return expiresAt; + } - @JsonProperty("user_id") - private String userId; + public void setExpiresAt(String expiresAt) { + this.expiresAt = expiresAt; + } - @JsonProperty("original_query") - private String originalQuery; + public String getCreatedAt() { + return createdAt; + } - @JsonProperty("request_type") - private String requestType; + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } - @JsonProperty("request_context") - private Map requestContext; + public String getUpdatedAt() { + return updatedAt; + } - @JsonProperty("triggered_policy_id") - private String triggeredPolicyId; + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } + } - @JsonProperty("triggered_policy_name") - private String triggeredPolicyName; + // ======================================================================== + // Queue List Options + // ======================================================================== - @JsonProperty("trigger_reason") - private String triggerReason; + /** Options for listing HITL queue items. */ + public static class HITLQueueListOptions { - @JsonProperty("severity") - private String severity; + private String status; + private String severity; + private Integer limit; + private Integer offset; - @JsonProperty("eu_ai_act_article") - private String euAiActArticle; + public static Builder builder() { + return new Builder(); + } - @JsonProperty("compliance_framework") - private String complianceFramework; + public String getStatus() { + return status; + } - @JsonProperty("risk_classification") - private String riskClassification; + public String getSeverity() { + return severity; + } - @JsonProperty("status") - private String status; + public Integer getLimit() { + return limit; + } - @JsonProperty("reviewer_id") - private String reviewerId; + public Integer getOffset() { + return offset; + } - @JsonProperty("reviewer_email") - private String reviewerEmail; + public static class Builder { + private final HITLQueueListOptions options = new HITLQueueListOptions(); + + /** + * Filters by approval request status (e.g. "pending", "approved", "rejected"). + * + * @param status the status filter + * @return this builder + */ + public Builder status(String status) { + options.status = status; + return this; + } + + /** + * Filters by severity level (e.g. "critical", "high", "medium", "low"). + * + * @param severity the severity filter + * @return this builder + */ + public Builder severity(String severity) { + options.severity = severity; + return this; + } + + /** + * Sets the maximum number of items to return. + * + * @param limit the page size + * @return this builder + */ + public Builder limit(Integer limit) { + options.limit = limit; + return this; + } + + /** + * Sets the offset for pagination. + * + * @param offset the offset + * @return this builder + */ + public Builder offset(Integer offset) { + options.offset = offset; + return this; + } + + public HITLQueueListOptions build() { + return options; + } + } + } - @JsonProperty("review_comment") - private String reviewComment; + // ======================================================================== + // Queue List Response + // ======================================================================== - @JsonProperty("reviewed_at") - private String reviewedAt; + /** Response from listing HITL queue items. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HITLQueueListResponse { - @JsonProperty("expires_at") - private String expiresAt; + @JsonProperty("items") + private List items; - @JsonProperty("created_at") - private String createdAt; + @JsonProperty("total") + private long total; - @JsonProperty("updated_at") - private String updatedAt; + @JsonProperty("has_more") + private boolean hasMore; - public HITLApprovalRequest() {} + public HITLQueueListResponse() {} - // Getters and setters - public String getRequestId() { return requestId; } - public void setRequestId(String requestId) { this.requestId = requestId; } + public List getItems() { + return items; + } - public String getOrgId() { return orgId; } - public void setOrgId(String orgId) { this.orgId = orgId; } + public void setItems(List items) { + this.items = items; + } - public String getTenantId() { return tenantId; } - public void setTenantId(String tenantId) { this.tenantId = tenantId; } + public long getTotal() { + return total; + } - public String getClientId() { return clientId; } - public void setClientId(String clientId) { this.clientId = clientId; } + public void setTotal(long total) { + this.total = total; + } - public String getUserId() { return userId; } - public void setUserId(String userId) { this.userId = userId; } + public boolean isHasMore() { + return hasMore; + } - public String getOriginalQuery() { return originalQuery; } - public void setOriginalQuery(String originalQuery) { this.originalQuery = originalQuery; } + public void setHasMore(boolean hasMore) { + this.hasMore = hasMore; + } + } - public String getRequestType() { return requestType; } - public void setRequestType(String requestType) { this.requestType = requestType; } + // ======================================================================== + // Review Input + // ======================================================================== - public Map getRequestContext() { return requestContext; } - public void setRequestContext(Map requestContext) { this.requestContext = requestContext; } + /** Input for approving or rejecting a HITL request. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HITLReviewInput { - public String getTriggeredPolicyId() { return triggeredPolicyId; } - public void setTriggeredPolicyId(String triggeredPolicyId) { this.triggeredPolicyId = triggeredPolicyId; } + @JsonProperty("reviewer_id") + private String reviewerId; - public String getTriggeredPolicyName() { return triggeredPolicyName; } - public void setTriggeredPolicyName(String triggeredPolicyName) { this.triggeredPolicyName = triggeredPolicyName; } + @JsonProperty("reviewer_email") + private String reviewerEmail; - public String getTriggerReason() { return triggerReason; } - public void setTriggerReason(String triggerReason) { this.triggerReason = triggerReason; } + @JsonProperty("reviewer_role") + private String reviewerRole; - public String getSeverity() { return severity; } - public void setSeverity(String severity) { this.severity = severity; } + @JsonProperty("comment") + private String comment; - public String getEuAiActArticle() { return euAiActArticle; } - public void setEuAiActArticle(String euAiActArticle) { this.euAiActArticle = euAiActArticle; } + public HITLReviewInput() {} - public String getComplianceFramework() { return complianceFramework; } - public void setComplianceFramework(String complianceFramework) { this.complianceFramework = complianceFramework; } + public static Builder builder() { + return new Builder(); + } - public String getRiskClassification() { return riskClassification; } - public void setRiskClassification(String riskClassification) { this.riskClassification = riskClassification; } + public String getReviewerId() { + return reviewerId; + } - public String getStatus() { return status; } - public void setStatus(String status) { this.status = status; } + public void setReviewerId(String reviewerId) { + this.reviewerId = reviewerId; + } - public String getReviewerId() { return reviewerId; } - public void setReviewerId(String reviewerId) { this.reviewerId = reviewerId; } + public String getReviewerEmail() { + return reviewerEmail; + } - public String getReviewerEmail() { return reviewerEmail; } - public void setReviewerEmail(String reviewerEmail) { this.reviewerEmail = reviewerEmail; } + public void setReviewerEmail(String reviewerEmail) { + this.reviewerEmail = reviewerEmail; + } - public String getReviewComment() { return reviewComment; } - public void setReviewComment(String reviewComment) { this.reviewComment = reviewComment; } + public String getReviewerRole() { + return reviewerRole; + } - public String getReviewedAt() { return reviewedAt; } - public void setReviewedAt(String reviewedAt) { this.reviewedAt = reviewedAt; } + public void setReviewerRole(String reviewerRole) { + this.reviewerRole = reviewerRole; + } - public String getExpiresAt() { return expiresAt; } - public void setExpiresAt(String expiresAt) { this.expiresAt = expiresAt; } + public String getComment() { + return comment; + } - public String getCreatedAt() { return createdAt; } - public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + public void setComment(String comment) { + this.comment = comment; + } - public String getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } + public static class Builder { + private final HITLReviewInput input = new HITLReviewInput(); + + /** + * Sets the reviewer's user ID. + * + * @param reviewerId the reviewer ID + * @return this builder + */ + public Builder reviewerId(String reviewerId) { + input.reviewerId = reviewerId; + return this; + } + + /** + * Sets the reviewer's email address. + * + * @param reviewerEmail the reviewer email + * @return this builder + */ + public Builder reviewerEmail(String reviewerEmail) { + input.reviewerEmail = reviewerEmail; + return this; + } + + /** + * Sets the reviewer's role (optional). + * + * @param reviewerRole the reviewer role + * @return this builder + */ + public Builder reviewerRole(String reviewerRole) { + input.reviewerRole = reviewerRole; + return this; + } + + /** + * Sets the review comment (optional). + * + * @param comment the comment + * @return this builder + */ + public Builder comment(String comment) { + input.comment = comment; + return this; + } + + public HITLReviewInput build() { + return input; + } } + } + + // ======================================================================== + // Stats + // ======================================================================== - // ======================================================================== - // Queue List Options - // ======================================================================== + /** HITL dashboard statistics. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static class HITLStats { - /** - * Options for listing HITL queue items. - */ - public static class HITLQueueListOptions { + @JsonProperty("total_pending") + private long totalPending; - private String status; - private String severity; - private Integer limit; - private Integer offset; + @JsonProperty("high_priority") + private long highPriority; - public static Builder builder() { - return new Builder(); - } + @JsonProperty("critical_priority") + private long criticalPriority; - public String getStatus() { return status; } - public String getSeverity() { return severity; } - public Integer getLimit() { return limit; } - public Integer getOffset() { return offset; } + @JsonProperty("oldest_pending_hours") + private Double oldestPendingHours; - public static class Builder { - private final HITLQueueListOptions options = new HITLQueueListOptions(); + public HITLStats() {} - /** - * Filters by approval request status (e.g. "pending", "approved", "rejected"). - * - * @param status the status filter - * @return this builder - */ - public Builder status(String status) { - options.status = status; - return this; - } + public long getTotalPending() { + return totalPending; + } - /** - * Filters by severity level (e.g. "critical", "high", "medium", "low"). - * - * @param severity the severity filter - * @return this builder - */ - public Builder severity(String severity) { - options.severity = severity; - return this; - } + public void setTotalPending(long totalPending) { + this.totalPending = totalPending; + } - /** - * Sets the maximum number of items to return. - * - * @param limit the page size - * @return this builder - */ - public Builder limit(Integer limit) { - options.limit = limit; - return this; - } + public long getHighPriority() { + return highPriority; + } - /** - * Sets the offset for pagination. - * - * @param offset the offset - * @return this builder - */ - public Builder offset(Integer offset) { - options.offset = offset; - return this; - } - - public HITLQueueListOptions build() { - return options; - } - } - } - - // ======================================================================== - // Queue List Response - // ======================================================================== - - /** - * Response from listing HITL queue items. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static class HITLQueueListResponse { - - @JsonProperty("items") - private List items; - - @JsonProperty("total") - private long total; - - @JsonProperty("has_more") - private boolean hasMore; - - public HITLQueueListResponse() {} - - public List getItems() { return items; } - public void setItems(List items) { this.items = items; } - - public long getTotal() { return total; } - public void setTotal(long total) { this.total = total; } - - public boolean isHasMore() { return hasMore; } - public void setHasMore(boolean hasMore) { this.hasMore = hasMore; } - } - - // ======================================================================== - // Review Input - // ======================================================================== - - /** - * Input for approving or rejecting a HITL request. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static class HITLReviewInput { - - @JsonProperty("reviewer_id") - private String reviewerId; - - @JsonProperty("reviewer_email") - private String reviewerEmail; - - @JsonProperty("reviewer_role") - private String reviewerRole; - - @JsonProperty("comment") - private String comment; - - public HITLReviewInput() {} - - public static Builder builder() { - return new Builder(); - } - - public String getReviewerId() { return reviewerId; } - public void setReviewerId(String reviewerId) { this.reviewerId = reviewerId; } - - public String getReviewerEmail() { return reviewerEmail; } - public void setReviewerEmail(String reviewerEmail) { this.reviewerEmail = reviewerEmail; } - - public String getReviewerRole() { return reviewerRole; } - public void setReviewerRole(String reviewerRole) { this.reviewerRole = reviewerRole; } - - public String getComment() { return comment; } - public void setComment(String comment) { this.comment = comment; } - - public static class Builder { - private final HITLReviewInput input = new HITLReviewInput(); - - /** - * Sets the reviewer's user ID. - * - * @param reviewerId the reviewer ID - * @return this builder - */ - public Builder reviewerId(String reviewerId) { - input.reviewerId = reviewerId; - return this; - } - - /** - * Sets the reviewer's email address. - * - * @param reviewerEmail the reviewer email - * @return this builder - */ - public Builder reviewerEmail(String reviewerEmail) { - input.reviewerEmail = reviewerEmail; - return this; - } + public void setHighPriority(long highPriority) { + this.highPriority = highPriority; + } - /** - * Sets the reviewer's role (optional). - * - * @param reviewerRole the reviewer role - * @return this builder - */ - public Builder reviewerRole(String reviewerRole) { - input.reviewerRole = reviewerRole; - return this; - } + public long getCriticalPriority() { + return criticalPriority; + } - /** - * Sets the review comment (optional). - * - * @param comment the comment - * @return this builder - */ - public Builder comment(String comment) { - input.comment = comment; - return this; - } + public void setCriticalPriority(long criticalPriority) { + this.criticalPriority = criticalPriority; + } - public HITLReviewInput build() { - return input; - } - } + public Double getOldestPendingHours() { + return oldestPendingHours; } - // ======================================================================== - // Stats - // ======================================================================== - - /** - * HITL dashboard statistics. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static class HITLStats { - - @JsonProperty("total_pending") - private long totalPending; - - @JsonProperty("high_priority") - private long highPriority; - - @JsonProperty("critical_priority") - private long criticalPriority; - - @JsonProperty("oldest_pending_hours") - private Double oldestPendingHours; - - public HITLStats() {} - - public long getTotalPending() { return totalPending; } - public void setTotalPending(long totalPending) { this.totalPending = totalPending; } - - public long getHighPriority() { return highPriority; } - public void setHighPriority(long highPriority) { this.highPriority = highPriority; } - - public long getCriticalPriority() { return criticalPriority; } - public void setCriticalPriority(long criticalPriority) { this.criticalPriority = criticalPriority; } - - public Double getOldestPendingHours() { return oldestPendingHours; } - public void setOldestPendingHours(Double oldestPendingHours) { this.oldestPendingHours = oldestPendingHours; } + public void setOldestPendingHours(Double oldestPendingHours) { + this.oldestPendingHours = oldestPendingHours; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/hitl/package-info.java b/src/main/java/com/getaxonflow/sdk/types/hitl/package-info.java index 9ea2e02..3dcec88 100644 --- a/src/main/java/com/getaxonflow/sdk/types/hitl/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/hitl/package-info.java @@ -17,8 +17,8 @@ /** * Human-in-the-Loop (HITL) Queue types for AxonFlow SDK. * - *

This package contains types for the HITL approval queue including - * listing, reviewing, approving, and rejecting approval requests. + *

This package contains types for the HITL approval queue including listing, reviewing, + * approving, and rejecting approval requests. * * @see com.getaxonflow.sdk.types.hitl.HITLTypes * @see com.getaxonflow.sdk.AxonFlow#listHITLQueue diff --git a/src/main/java/com/getaxonflow/sdk/types/package-info.java b/src/main/java/com/getaxonflow/sdk/types/package-info.java index 6edc5e9..dd4f579 100644 --- a/src/main/java/com/getaxonflow/sdk/types/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/package-info.java @@ -20,31 +20,35 @@ *

This package contains request/response types for all AxonFlow API operations. * *

Gateway Mode Types

+ * *
    - *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalRequest} - Pre-check request
  • - *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalResult} - Pre-check response
  • - *
  • {@link com.getaxonflow.sdk.types.AuditOptions} - Audit request
  • - *
  • {@link com.getaxonflow.sdk.types.AuditResult} - Audit response
  • + *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalRequest} - Pre-check request + *
  • {@link com.getaxonflow.sdk.types.PolicyApprovalResult} - Pre-check response + *
  • {@link com.getaxonflow.sdk.types.AuditOptions} - Audit request + *
  • {@link com.getaxonflow.sdk.types.AuditResult} - Audit response *
* *

Proxy Mode Types

+ * *
    - *
  • {@link com.getaxonflow.sdk.types.ClientRequest} - Query request
  • - *
  • {@link com.getaxonflow.sdk.types.ClientResponse} - Query response
  • + *
  • {@link com.getaxonflow.sdk.types.ClientRequest} - Query request + *
  • {@link com.getaxonflow.sdk.types.ClientResponse} - Query response *
* *

Planning Types

+ * *
    - *
  • {@link com.getaxonflow.sdk.types.PlanRequest} - Plan generation request
  • - *
  • {@link com.getaxonflow.sdk.types.PlanResponse} - Generated plan
  • - *
  • {@link com.getaxonflow.sdk.types.PlanStep} - Individual plan step
  • + *
  • {@link com.getaxonflow.sdk.types.PlanRequest} - Plan generation request + *
  • {@link com.getaxonflow.sdk.types.PlanResponse} - Generated plan + *
  • {@link com.getaxonflow.sdk.types.PlanStep} - Individual plan step *
* *

Connector Types

+ * *
    - *
  • {@link com.getaxonflow.sdk.types.ConnectorInfo} - Connector metadata
  • - *
  • {@link com.getaxonflow.sdk.types.ConnectorQuery} - Connector query request
  • - *
  • {@link com.getaxonflow.sdk.types.ConnectorResponse} - Connector query response
  • + *
  • {@link com.getaxonflow.sdk.types.ConnectorInfo} - Connector metadata + *
  • {@link com.getaxonflow.sdk.types.ConnectorQuery} - Connector query request + *
  • {@link com.getaxonflow.sdk.types.ConnectorResponse} - Connector query response *
*/ package com.getaxonflow.sdk.types; diff --git a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java index 05ccb9b..f18f248 100644 --- a/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/policies/PolicyTypes.java @@ -17,1192 +17,1724 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; - import java.time.Instant; import java.util.List; import java.util.Map; -/** - * Policy CRUD types for the Unified Policy Architecture v2.0.0. - */ +/** Policy CRUD types for the Unified Policy Architecture v2.0.0. */ public final class PolicyTypes { - private PolicyTypes() {} - - // ======================================================================== - // Media Governance Policy Category Constants - // ======================================================================== - - /** Policy category for media safety (NSFW, violence). */ - public static final String CATEGORY_MEDIA_SAFETY = "media-safety"; - - /** Policy category for media biometric detection (faces, fingerprints). */ - public static final String CATEGORY_MEDIA_BIOMETRIC = "media-biometric"; - - /** Policy category for sensitive document detection. */ - public static final String CATEGORY_MEDIA_DOCUMENT = "media-document"; - - /** Policy category for PII detected in media (OCR text extraction). */ - public static final String CATEGORY_MEDIA_PII = "media-pii"; - - // ======================================================================== - // Enums - // ======================================================================== - - /** - * Policy categories for organization and filtering. - */ - public enum PolicyCategory { - // Static policy categories - Security - SECURITY_SQLI("security-sqli"), - SECURITY_ADMIN("security-admin"), - - // Static policy categories - PII Detection - PII_GLOBAL("pii-global"), - PII_US("pii-us"), - PII_EU("pii-eu"), - PII_INDIA("pii-india"), - PII_SINGAPORE("pii-singapore"), - - // Static policy categories - Code Governance - CODE_SECRETS("code-secrets"), - CODE_UNSAFE("code-unsafe"), - CODE_COMPLIANCE("code-compliance"), - - // Sensitive data category - SENSITIVE_DATA("sensitive-data"), - - // Media governance categories - MEDIA_SAFETY("media-safety"), - MEDIA_BIOMETRIC("media-biometric"), - MEDIA_PII("media-pii"), - MEDIA_DOCUMENT("media-document"), - - // Dynamic policy categories - DYNAMIC_RISK("dynamic-risk"), - DYNAMIC_COMPLIANCE("dynamic-compliance"), - DYNAMIC_SECURITY("dynamic-security"), - DYNAMIC_COST("dynamic-cost"), - DYNAMIC_ACCESS("dynamic-access"); - - private final String value; - - PolicyCategory(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - /** - * Policy tiers determine where policies apply. - */ - public enum PolicyTier { - SYSTEM("system"), - ORGANIZATION("organization"), - TENANT("tenant"); - - private final String value; - - PolicyTier(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - /** - * Override action for policy overrides. - *
    - *
  • BLOCK: Immediately block the request
  • - *
  • REQUIRE_APPROVAL: Pause for human approval (HITL)
  • - *
  • REDACT: Mask sensitive content
  • - *
  • WARN: Log warning, allow request
  • - *
  • LOG: Audit only
  • - *
- */ - public enum OverrideAction { - BLOCK("block"), - REQUIRE_APPROVAL("require_approval"), - REDACT("redact"), - WARN("warn"), - LOG("log"); - - private final String value; - - OverrideAction(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - /** - * Action to take when a policy matches. - *
    - *
  • BLOCK: Immediately block the request
  • - *
  • REQUIRE_APPROVAL: Pause for human approval (HITL)
  • - *
  • REDACT: Mask sensitive content
  • - *
  • WARN: Log warning, allow request
  • - *
  • LOG: Audit only
  • - *
  • ALLOW: Explicitly allow (for overrides)
  • - *
- */ - public enum PolicyAction { - BLOCK("block"), - REQUIRE_APPROVAL("require_approval"), - REDACT("redact"), - WARN("warn"), - LOG("log"), - ALLOW("allow"); - - private final String value; - - PolicyAction(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - /** - * Policy severity levels. - */ - public enum PolicySeverity { - CRITICAL("critical"), - HIGH("high"), - MEDIUM("medium"), - LOW("low"); - - private final String value; - - PolicySeverity(String value) { - this.value = value; - } - - @JsonValue - public String getValue() { - return value; - } - } - - // ======================================================================== - // Static Policy Types - // ======================================================================== - - /** - * Static policy definition. - */ - public static class StaticPolicy { - private String id; - private String name; - private String description; - private PolicyCategory category; - private PolicyTier tier; - private String pattern; - private PolicySeverity severity; - private boolean enabled; - private PolicyAction action; - @JsonProperty("organization_id") - private String organizationId; - @JsonProperty("tenant_id") - private String tenantId; - @JsonProperty("created_at") - private Instant createdAt; - @JsonProperty("updated_at") - private Instant updatedAt; - private Integer version; - @JsonProperty("has_override") - private Boolean hasOverride; - private PolicyOverride override; - - // Getters and setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - public PolicyCategory getCategory() { return category; } - public void setCategory(PolicyCategory category) { this.category = category; } - public PolicyTier getTier() { return tier; } - public void setTier(PolicyTier tier) { this.tier = tier; } - public String getPattern() { return pattern; } - public void setPattern(String pattern) { this.pattern = pattern; } - public PolicySeverity getSeverity() { return severity; } - public void setSeverity(PolicySeverity severity) { this.severity = severity; } - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - public PolicyAction getAction() { return action; } - public void setAction(PolicyAction action) { this.action = action; } - public String getOrganizationId() { return organizationId; } - public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } - public String getTenantId() { return tenantId; } - public void setTenantId(String tenantId) { this.tenantId = tenantId; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } - public Integer getVersion() { return version; } - public void setVersion(Integer version) { this.version = version; } - public Boolean getHasOverride() { return hasOverride; } - public void setHasOverride(Boolean hasOverride) { this.hasOverride = hasOverride; } - public PolicyOverride getOverride() { return override; } - public void setOverride(PolicyOverride override) { this.override = override; } - } - - /** - * Policy override configuration. - */ - public static class PolicyOverride { - @JsonProperty("policy_id") - private String policyId; - @JsonProperty("action_override") - private OverrideAction actionOverride; - @JsonProperty("override_reason") - private String overrideReason; - @JsonProperty("created_by") - private String createdBy; - @JsonProperty("created_at") - private Instant createdAt; - @JsonProperty("expires_at") - private Instant expiresAt; - private boolean active; - - // Getters and setters - public String getPolicyId() { return policyId; } - public void setPolicyId(String policyId) { this.policyId = policyId; } - public OverrideAction getActionOverride() { return actionOverride; } - public void setActionOverride(OverrideAction actionOverride) { this.actionOverride = actionOverride; } - public String getOverrideReason() { return overrideReason; } - public void setOverrideReason(String overrideReason) { this.overrideReason = overrideReason; } - public String getCreatedBy() { return createdBy; } - public void setCreatedBy(String createdBy) { this.createdBy = createdBy; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getExpiresAt() { return expiresAt; } - public void setExpiresAt(Instant expiresAt) { this.expiresAt = expiresAt; } - public boolean isActive() { return active; } - public void setActive(boolean active) { this.active = active; } - } - - /** - * Options for listing static policies. - */ - public static class ListStaticPoliciesOptions { - private PolicyCategory category; - private PolicyTier tier; - private String organizationId; - private Boolean enabled; - private Integer limit; - private Integer offset; - private String sortBy; - private String sortOrder; - private String search; - - public static Builder builder() { - return new Builder(); - } - - public PolicyCategory getCategory() { return category; } - public PolicyTier getTier() { return tier; } - public String getOrganizationId() { return organizationId; } - public Boolean getEnabled() { return enabled; } - public Integer getLimit() { return limit; } - public Integer getOffset() { return offset; } - public String getSortBy() { return sortBy; } - public String getSortOrder() { return sortOrder; } - public String getSearch() { return search; } - - public static class Builder { - private final ListStaticPoliciesOptions options = new ListStaticPoliciesOptions(); - - public Builder category(PolicyCategory category) { - options.category = category; - return this; - } - - public Builder tier(PolicyTier tier) { - options.tier = tier; - return this; - } - - /** - * Filters policies by organization ID (Enterprise). - * - * @param organizationId the organization ID - * @return this builder - */ - public Builder organizationId(String organizationId) { - options.organizationId = organizationId; - return this; - } - - public Builder enabled(Boolean enabled) { - options.enabled = enabled; - return this; - } - - public Builder limit(Integer limit) { - options.limit = limit; - return this; - } - - public Builder offset(Integer offset) { - options.offset = offset; - return this; - } - - public Builder sortBy(String sortBy) { - options.sortBy = sortBy; - return this; - } - - public Builder sortOrder(String sortOrder) { - options.sortOrder = sortOrder; - return this; - } - - public Builder search(String search) { - options.search = search; - return this; - } - - public ListStaticPoliciesOptions build() { - return options; - } - } - } - - /** - * Request to create a new static policy. - */ - public static class CreateStaticPolicyRequest { - private String name; - private String description; - private PolicyCategory category; - private PolicyTier tier = PolicyTier.TENANT; - @JsonProperty("organization_id") - private String organizationId; - private String pattern; - private PolicySeverity severity = PolicySeverity.MEDIUM; - private boolean enabled = true; - private PolicyAction action = PolicyAction.BLOCK; - - public static Builder builder() { - return new Builder(); - } - - public String getName() { return name; } - public String getDescription() { return description; } - public PolicyCategory getCategory() { return category; } - public PolicyTier getTier() { return tier; } - public String getOrganizationId() { return organizationId; } - public String getPattern() { return pattern; } - public PolicySeverity getSeverity() { return severity; } - public boolean isEnabled() { return enabled; } - public PolicyAction getAction() { return action; } - - public static class Builder { - private final CreateStaticPolicyRequest request = new CreateStaticPolicyRequest(); - - public Builder name(String name) { - request.name = name; - return this; - } - - public Builder description(String description) { - request.description = description; - return this; - } - - public Builder category(PolicyCategory category) { - request.category = category; - return this; - } - - public Builder tier(PolicyTier tier) { - request.tier = tier; - return this; - } - - /** - * Sets the organization ID for organization-tier policies (Enterprise). - * - * @param organizationId the organization ID - * @return this builder - */ - public Builder organizationId(String organizationId) { - request.organizationId = organizationId; - return this; - } - - public Builder pattern(String pattern) { - request.pattern = pattern; - return this; - } - - public Builder severity(PolicySeverity severity) { - request.severity = severity; - return this; - } - - public Builder enabled(boolean enabled) { - request.enabled = enabled; - return this; - } - - public Builder action(PolicyAction action) { - request.action = action; - return this; - } - - public CreateStaticPolicyRequest build() { - return request; - } - } - } - - /** - * Request to update an existing static policy. - */ - public static class UpdateStaticPolicyRequest { - private String name; - private String description; - private PolicyCategory category; - private String pattern; - private PolicySeverity severity; - private Boolean enabled; - private PolicyAction action; - - public static Builder builder() { - return new Builder(); - } - - public String getName() { return name; } - public String getDescription() { return description; } - public PolicyCategory getCategory() { return category; } - public String getPattern() { return pattern; } - public PolicySeverity getSeverity() { return severity; } - public Boolean getEnabled() { return enabled; } - public PolicyAction getAction() { return action; } - - public static class Builder { - private final UpdateStaticPolicyRequest request = new UpdateStaticPolicyRequest(); - - public Builder name(String name) { - request.name = name; - return this; - } - - public Builder description(String description) { - request.description = description; - return this; - } - - public Builder category(PolicyCategory category) { - request.category = category; - return this; - } - - public Builder pattern(String pattern) { - request.pattern = pattern; - return this; - } - - public Builder severity(PolicySeverity severity) { - request.severity = severity; - return this; - } - - public Builder enabled(Boolean enabled) { - request.enabled = enabled; - return this; - } - - public Builder action(PolicyAction action) { - request.action = action; - return this; - } - - public UpdateStaticPolicyRequest build() { - return request; - } - } - } - - /** - * Request to create a policy override. - */ - public static class CreatePolicyOverrideRequest { - @JsonProperty("action_override") - private OverrideAction actionOverride; - @JsonProperty("override_reason") - private String overrideReason; - @JsonProperty("expires_at") - private Instant expiresAt; - - public static Builder builder() { - return new Builder(); - } - - public OverrideAction getActionOverride() { return actionOverride; } - public String getOverrideReason() { return overrideReason; } - public Instant getExpiresAt() { return expiresAt; } - - public static class Builder { - private final CreatePolicyOverrideRequest request = new CreatePolicyOverrideRequest(); - - public Builder actionOverride(OverrideAction actionOverride) { - request.actionOverride = actionOverride; - return this; - } - - public Builder overrideReason(String overrideReason) { - request.overrideReason = overrideReason; - return this; - } - - public Builder expiresAt(Instant expiresAt) { - request.expiresAt = expiresAt; - return this; - } - - public CreatePolicyOverrideRequest build() { - return request; - } - } - } - - // ======================================================================== - // Dynamic Policy Types - // ======================================================================== - - /** - * Condition for dynamic policy evaluation. - */ - public static class DynamicPolicyCondition { - private String field; - private String operator; - private Object value; - - public DynamicPolicyCondition() {} - - public DynamicPolicyCondition(String field, String operator, Object value) { - this.field = field; - this.operator = operator; - this.value = value; - } - - public String getField() { return field; } - public void setField(String field) { this.field = field; } - public String getOperator() { return operator; } - public void setOperator(String operator) { this.operator = operator; } - public Object getValue() { return value; } - public void setValue(Object value) { this.value = value; } - } - - /** - * Action to take when dynamic policy conditions are met. - */ - public static class DynamicPolicyAction { - private String type; // "block", "alert", "redact", "log", "route", "modify_risk" - private Map config; - - public DynamicPolicyAction() {} - - public DynamicPolicyAction(String type, Map config) { - this.type = type; - this.config = config; - } - - public String getType() { return type; } - public void setType(String type) { this.type = type; } - public Map getConfig() { return config; } - public void setConfig(Map config) { this.config = config; } - } - - /** - * Dynamic policy definition. - * - *

Dynamic policies are LLM-powered policies that can evaluate complex, - * context-aware rules that can't be expressed with simple regex patterns. - * - *

For provider restrictions (GDPR, HIPAA, RBI compliance), use action config: - *

{@code
-     * List actions = List.of(
-     *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
-     * );
-     * }
- */ - public static class DynamicPolicy { - private String id; - private String name; - private String description; - private String type; // "risk", "content", "user", "cost" - private String category; // "dynamic-risk", "dynamic-compliance", etc. - private PolicyTier tier; - @JsonProperty("organization_id") - private String organizationId; - private List conditions; - private List actions; - private int priority; - private boolean enabled; - @JsonProperty("created_at") - private Instant createdAt; - @JsonProperty("updated_at") - private Instant updatedAt; - - // Getters and setters - public String getId() { return id; } - public void setId(String id) { this.id = id; } - public String getName() { return name; } - public void setName(String name) { this.name = name; } - public String getDescription() { return description; } - public void setDescription(String description) { this.description = description; } - public String getType() { return type; } - public void setType(String type) { this.type = type; } - public String getCategory() { return category; } - public void setCategory(String category) { this.category = category; } - public PolicyTier getTier() { return tier; } - public void setTier(PolicyTier tier) { this.tier = tier; } - public String getOrganizationId() { return organizationId; } - public void setOrganizationId(String organizationId) { this.organizationId = organizationId; } - public List getConditions() { return conditions; } - public void setConditions(List conditions) { this.conditions = conditions; } - public List getActions() { return actions; } - public void setActions(List actions) { this.actions = actions; } - public int getPriority() { return priority; } - public void setPriority(int priority) { this.priority = priority; } - public boolean isEnabled() { return enabled; } - public void setEnabled(boolean enabled) { this.enabled = enabled; } - public Instant getCreatedAt() { return createdAt; } - public void setCreatedAt(Instant createdAt) { this.createdAt = createdAt; } - public Instant getUpdatedAt() { return updatedAt; } - public void setUpdatedAt(Instant updatedAt) { this.updatedAt = updatedAt; } - } - - /** - * Options for listing dynamic policies. - */ - public static class ListDynamicPoliciesOptions { - private String type; // Filter by policy type: "risk", "content", "user", "cost" - private PolicyTier tier; - private String organizationId; - private Boolean enabled; - private Integer limit; - private Integer offset; - private String sortBy; - private String sortOrder; - private String search; - - public static Builder builder() { - return new Builder(); - } - - public String getType() { return type; } - public PolicyTier getTier() { return tier; } - public String getOrganizationId() { return organizationId; } - public Boolean getEnabled() { return enabled; } - public Integer getLimit() { return limit; } - public Integer getOffset() { return offset; } - public String getSortBy() { return sortBy; } - public String getSortOrder() { return sortOrder; } - public String getSearch() { return search; } - - public static class Builder { - private final ListDynamicPoliciesOptions options = new ListDynamicPoliciesOptions(); - - /** - * Filter by policy type: "risk", "content", "user", "cost". - * - * @param type the policy type - * @return this builder - */ - public Builder type(String type) { - options.type = type; - return this; - } - - /** - * Filters policies by tier. - * - * @param tier the policy tier - * @return this builder - */ - public Builder tier(PolicyTier tier) { - options.tier = tier; - return this; - } - - /** - * Filters policies by organization ID (Enterprise). - * - * @param organizationId the organization ID - * @return this builder - */ - public Builder organizationId(String organizationId) { - options.organizationId = organizationId; - return this; - } - - public Builder enabled(Boolean enabled) { - options.enabled = enabled; - return this; - } - - public Builder limit(Integer limit) { - options.limit = limit; - return this; - } - - public Builder offset(Integer offset) { - options.offset = offset; - return this; - } - - public Builder sortBy(String sortBy) { - options.sortBy = sortBy; - return this; - } - - public Builder sortOrder(String sortOrder) { - options.sortOrder = sortOrder; - return this; - } - - public Builder search(String search) { - options.search = search; - return this; - } - - public ListDynamicPoliciesOptions build() { - return options; - } - } - } - - /** - * Request to create a dynamic policy. - * - *

For provider restrictions, use action config with "allowed_providers" key: - *

{@code
-     * List actions = List.of(
-     *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
-     * );
-     * }
- */ - public static class CreateDynamicPolicyRequest { - private String name; - private String description; - private String type; // "risk", "content", "user", "cost" - private String category; // "dynamic-risk", "dynamic-compliance", etc. - private PolicyTier tier = PolicyTier.TENANT; - @JsonProperty("organization_id") - private String organizationId; - private List conditions; - private List actions; - private int priority; - private boolean enabled = true; - - public static Builder builder() { - return new Builder(); - } - - public String getName() { return name; } - public String getDescription() { return description; } - public String getType() { return type; } - public String getCategory() { return category; } - public PolicyTier getTier() { return tier; } - public String getOrganizationId() { return organizationId; } - public List getConditions() { return conditions; } - public List getActions() { return actions; } - public int getPriority() { return priority; } - public boolean isEnabled() { return enabled; } - - public static class Builder { - private final CreateDynamicPolicyRequest request = new CreateDynamicPolicyRequest(); - - public Builder name(String name) { - request.name = name; - return this; - } - - public Builder description(String description) { - request.description = description; - return this; - } - - /** - * Sets the policy type: "risk", "content", "user", "cost". - * - * @param type the policy type - * @return this builder - */ - public Builder type(String type) { - request.type = type; - return this; - } - - /** - * Sets the policy category. Must start with "dynamic-". - * Examples: "dynamic-risk", "dynamic-compliance", "dynamic-security", "dynamic-cost", "dynamic-access". - * - * @param category the policy category - * @return this builder - */ - public Builder category(String category) { - request.category = category; - return this; - } - - /** - * Sets the policy tier. Defaults to {@link PolicyTier#TENANT}. - * - * @param tier the policy tier - * @return this builder - */ - public Builder tier(PolicyTier tier) { - request.tier = tier; - return this; - } - - /** - * Sets the organization ID for organization-tier policies (Enterprise). - * - * @param organizationId the organization ID - * @return this builder - */ - public Builder organizationId(String organizationId) { - request.organizationId = organizationId; - return this; - } - - public Builder conditions(List conditions) { - request.conditions = conditions; - return this; - } - - public Builder actions(List actions) { - request.actions = actions; - return this; - } - - public Builder priority(int priority) { - request.priority = priority; - return this; - } - - public Builder enabled(boolean enabled) { - request.enabled = enabled; - return this; - } - - public CreateDynamicPolicyRequest build() { - return request; - } - } - } - - /** - * Request to update a dynamic policy. - * - *

For provider restrictions, use action config with "allowed_providers" key: - *

{@code
-     * List actions = List.of(
-     *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
-     * );
-     * }
- */ - public static class UpdateDynamicPolicyRequest { - private String name; - private String description; - private String type; - private String category; - private PolicyTier tier; - @JsonProperty("organization_id") - private String organizationId; - private List conditions; - private List actions; - private Integer priority; - private Boolean enabled; - - public static Builder builder() { - return new Builder(); - } - - public String getName() { return name; } - public String getDescription() { return description; } - public String getType() { return type; } - public String getCategory() { return category; } - public PolicyTier getTier() { return tier; } - public String getOrganizationId() { return organizationId; } - public List getConditions() { return conditions; } - public List getActions() { return actions; } - public Integer getPriority() { return priority; } - public Boolean getEnabled() { return enabled; } - - public static class Builder { - private final UpdateDynamicPolicyRequest request = new UpdateDynamicPolicyRequest(); - - public Builder name(String name) { - request.name = name; - return this; - } - - public Builder description(String description) { - request.description = description; - return this; - } - - public Builder type(String type) { - request.type = type; - return this; - } - - /** - * Sets the policy category. Must start with "dynamic-" if specified. - * - * @param category the policy category - * @return this builder - */ - public Builder category(String category) { - request.category = category; - return this; - } - - /** - * Sets the policy tier. - * - * @param tier the policy tier - * @return this builder - */ - public Builder tier(PolicyTier tier) { - request.tier = tier; - return this; - } - - /** - * Sets the organization ID for organization-tier policies (Enterprise). - * - * @param organizationId the organization ID - * @return this builder - */ - public Builder organizationId(String organizationId) { - request.organizationId = organizationId; - return this; - } - - public Builder conditions(List conditions) { - request.conditions = conditions; - return this; - } - - public Builder actions(List actions) { - request.actions = actions; - return this; - } - - public Builder priority(Integer priority) { - request.priority = priority; - return this; - } - - public Builder enabled(Boolean enabled) { - request.enabled = enabled; - return this; - } - - public UpdateDynamicPolicyRequest build() { - return request; - } - } - } - - // ======================================================================== - // Pattern Testing Types - // ======================================================================== - - /** - * Result of testing a regex pattern. - */ - public static class TestPatternResult { - private boolean valid; - private String error; - private String pattern; - private List inputs; - private List matches; - - public boolean isValid() { return valid; } - public void setValid(boolean valid) { this.valid = valid; } - public String getError() { return error; } - public void setError(String error) { this.error = error; } - public String getPattern() { return pattern; } - public void setPattern(String pattern) { this.pattern = pattern; } - public List getInputs() { return inputs; } - public void setInputs(List inputs) { this.inputs = inputs; } - public List getMatches() { return matches; } - public void setMatches(List matches) { this.matches = matches; } - } - - /** - * Individual pattern match result. - */ - public static class TestPatternMatch { - private String input; - private boolean matched; - @JsonProperty("matched_text") - private String matchedText; - private Integer position; - - public String getInput() { return input; } - public void setInput(String input) { this.input = input; } - public boolean isMatched() { return matched; } - public void setMatched(boolean matched) { this.matched = matched; } - public String getMatchedText() { return matchedText; } - public void setMatchedText(String matchedText) { this.matchedText = matchedText; } - public Integer getPosition() { return position; } - public void setPosition(Integer position) { this.position = position; } - } - - // ======================================================================== - // Policy Version Types - // ======================================================================== - - /** - * Policy version history entry. - */ - public static class PolicyVersion { - private int version; - @JsonProperty("changed_by") - private String changedBy; - @JsonProperty("changed_at") - private Instant changedAt; - @JsonProperty("change_type") - private String changeType; - @JsonProperty("change_description") - private String changeDescription; - @JsonProperty("previous_values") - private Map previousValues; - @JsonProperty("new_values") - private Map newValues; - - public int getVersion() { return version; } - public void setVersion(int version) { this.version = version; } - public String getChangedBy() { return changedBy; } - public void setChangedBy(String changedBy) { this.changedBy = changedBy; } - public Instant getChangedAt() { return changedAt; } - public void setChangedAt(Instant changedAt) { this.changedAt = changedAt; } - public String getChangeType() { return changeType; } - public void setChangeType(String changeType) { this.changeType = changeType; } - public String getChangeDescription() { return changeDescription; } - public void setChangeDescription(String changeDescription) { this.changeDescription = changeDescription; } - public Map getPreviousValues() { return previousValues; } - public void setPreviousValues(Map previousValues) { this.previousValues = previousValues; } - public Map getNewValues() { return newValues; } - public void setNewValues(Map newValues) { this.newValues = newValues; } - } - - /** - * Options for getting effective policies. - */ - public static class EffectivePoliciesOptions { - private PolicyCategory category; - private boolean includeDisabled; - private boolean includeOverridden; - - public static Builder builder() { - return new Builder(); - } - - public PolicyCategory getCategory() { return category; } - public boolean isIncludeDisabled() { return includeDisabled; } - public boolean isIncludeOverridden() { return includeOverridden; } - - public static class Builder { - private final EffectivePoliciesOptions options = new EffectivePoliciesOptions(); - - public Builder category(PolicyCategory category) { - options.category = category; - return this; - } - - public Builder includeDisabled(boolean includeDisabled) { - options.includeDisabled = includeDisabled; - return this; - } - - public Builder includeOverridden(boolean includeOverridden) { - options.includeOverridden = includeOverridden; - return this; - } - - public EffectivePoliciesOptions build() { - return options; - } - } - } - - // ======================================================================== - // Response Wrappers - // ======================================================================== - - /** - * Wrapper for list static policies response. - */ - public static class StaticPoliciesResponse { - private List policies; - - public List getPolicies() { return policies; } - public void setPolicies(List policies) { this.policies = policies; } - } - - /** - * Wrapper for effective policies response. - */ - public static class EffectivePoliciesResponse { - @JsonProperty("static") - private List staticPolicies; - @JsonProperty("dynamic") - private List dynamicPolicies; - - public List getStaticPolicies() { return staticPolicies; } - public void setStaticPolicies(List staticPolicies) { this.staticPolicies = staticPolicies; } - public List getDynamicPolicies() { return dynamicPolicies; } - public void setDynamicPolicies(List dynamicPolicies) { this.dynamicPolicies = dynamicPolicies; } - } - - /** - * Wrapper for list dynamic policies response. - * Agent proxy (Issue #886) returns {"policies": [...]} wrapper. - */ - public static class DynamicPoliciesResponse { - private List policies; - - public List getPolicies() { return policies; } - public void setPolicies(List policies) { this.policies = policies; } - } - - /** - * Wrapper for single dynamic policy response. - * Agent proxy (Issue #886) returns {"policy": {...}} wrapper. - */ - public static class DynamicPolicyResponse { - private DynamicPolicy policy; - - public DynamicPolicy getPolicy() { return policy; } - public void setPolicy(DynamicPolicy policy) { this.policy = policy; } + private PolicyTypes() {} + + // ======================================================================== + // Media Governance Policy Category Constants + // ======================================================================== + + /** Policy category for media safety (NSFW, violence). */ + public static final String CATEGORY_MEDIA_SAFETY = "media-safety"; + + /** Policy category for media biometric detection (faces, fingerprints). */ + public static final String CATEGORY_MEDIA_BIOMETRIC = "media-biometric"; + + /** Policy category for sensitive document detection. */ + public static final String CATEGORY_MEDIA_DOCUMENT = "media-document"; + + /** Policy category for PII detected in media (OCR text extraction). */ + public static final String CATEGORY_MEDIA_PII = "media-pii"; + + // ======================================================================== + // Enums + // ======================================================================== + + /** Policy categories for organization and filtering. */ + public enum PolicyCategory { + // Static policy categories - Security + SECURITY_SQLI("security-sqli"), + SECURITY_ADMIN("security-admin"), + + // Static policy categories - PII Detection + PII_GLOBAL("pii-global"), + PII_US("pii-us"), + PII_EU("pii-eu"), + PII_INDIA("pii-india"), + PII_SINGAPORE("pii-singapore"), + + // Static policy categories - Code Governance + CODE_SECRETS("code-secrets"), + CODE_UNSAFE("code-unsafe"), + CODE_COMPLIANCE("code-compliance"), + + // Sensitive data category + SENSITIVE_DATA("sensitive-data"), + + // Media governance categories + MEDIA_SAFETY("media-safety"), + MEDIA_BIOMETRIC("media-biometric"), + MEDIA_PII("media-pii"), + MEDIA_DOCUMENT("media-document"), + + // Dynamic policy categories + DYNAMIC_RISK("dynamic-risk"), + DYNAMIC_COMPLIANCE("dynamic-compliance"), + DYNAMIC_SECURITY("dynamic-security"), + DYNAMIC_COST("dynamic-cost"), + DYNAMIC_ACCESS("dynamic-access"); + + private final String value; + + PolicyCategory(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** Policy tiers determine where policies apply. */ + public enum PolicyTier { + SYSTEM("system"), + ORGANIZATION("organization"), + TENANT("tenant"); + + private final String value; + + PolicyTier(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Override action for policy overrides. + * + *
    + *
  • BLOCK: Immediately block the request + *
  • REQUIRE_APPROVAL: Pause for human approval (HITL) + *
  • REDACT: Mask sensitive content + *
  • WARN: Log warning, allow request + *
  • LOG: Audit only + *
+ */ + public enum OverrideAction { + BLOCK("block"), + REQUIRE_APPROVAL("require_approval"), + REDACT("redact"), + WARN("warn"), + LOG("log"); + + private final String value; + + OverrideAction(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** + * Action to take when a policy matches. + * + *
    + *
  • BLOCK: Immediately block the request + *
  • REQUIRE_APPROVAL: Pause for human approval (HITL) + *
  • REDACT: Mask sensitive content + *
  • WARN: Log warning, allow request + *
  • LOG: Audit only + *
  • ALLOW: Explicitly allow (for overrides) + *
+ */ + public enum PolicyAction { + BLOCK("block"), + REQUIRE_APPROVAL("require_approval"), + REDACT("redact"), + WARN("warn"), + LOG("log"), + ALLOW("allow"); + + private final String value; + + PolicyAction(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + /** Policy severity levels. */ + public enum PolicySeverity { + CRITICAL("critical"), + HIGH("high"), + MEDIUM("medium"), + LOW("low"); + + private final String value; + + PolicySeverity(String value) { + this.value = value; + } + + @JsonValue + public String getValue() { + return value; + } + } + + // ======================================================================== + // Static Policy Types + // ======================================================================== + + /** Static policy definition. */ + public static class StaticPolicy { + private String id; + private String name; + private String description; + private PolicyCategory category; + private PolicyTier tier; + private String pattern; + private PolicySeverity severity; + private boolean enabled; + private PolicyAction action; + + @JsonProperty("organization_id") + private String organizationId; + + @JsonProperty("tenant_id") + private String tenantId; + + @JsonProperty("created_at") + private Instant createdAt; + + @JsonProperty("updated_at") + private Instant updatedAt; + + private Integer version; + + @JsonProperty("has_override") + private Boolean hasOverride; + + private PolicyOverride override; + + // Getters and setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public PolicyCategory getCategory() { + return category; + } + + public void setCategory(PolicyCategory category) { + this.category = category; + } + + public PolicyTier getTier() { + return tier; + } + + public void setTier(PolicyTier tier) { + this.tier = tier; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public PolicySeverity getSeverity() { + return severity; + } + + public void setSeverity(PolicySeverity severity) { + this.severity = severity; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public PolicyAction getAction() { + return action; + } + + public void setAction(PolicyAction action) { + this.action = action; + } + + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + + public String getTenantId() { + return tenantId; + } + + public void setTenantId(String tenantId) { + this.tenantId = tenantId; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + + public Integer getVersion() { + return version; + } + + public void setVersion(Integer version) { + this.version = version; + } + + public Boolean getHasOverride() { + return hasOverride; + } + + public void setHasOverride(Boolean hasOverride) { + this.hasOverride = hasOverride; + } + + public PolicyOverride getOverride() { + return override; + } + + public void setOverride(PolicyOverride override) { + this.override = override; + } + } + + /** Policy override configuration. */ + public static class PolicyOverride { + @JsonProperty("policy_id") + private String policyId; + + @JsonProperty("action_override") + private OverrideAction actionOverride; + + @JsonProperty("override_reason") + private String overrideReason; + + @JsonProperty("created_by") + private String createdBy; + + @JsonProperty("created_at") + private Instant createdAt; + + @JsonProperty("expires_at") + private Instant expiresAt; + + private boolean active; + + // Getters and setters + public String getPolicyId() { + return policyId; + } + + public void setPolicyId(String policyId) { + this.policyId = policyId; + } + + public OverrideAction getActionOverride() { + return actionOverride; + } + + public void setActionOverride(OverrideAction actionOverride) { + this.actionOverride = actionOverride; + } + + public String getOverrideReason() { + return overrideReason; + } + + public void setOverrideReason(String overrideReason) { + this.overrideReason = overrideReason; + } + + public String getCreatedBy() { + return createdBy; + } + + public void setCreatedBy(String createdBy) { + this.createdBy = createdBy; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getExpiresAt() { + return expiresAt; + } + + public void setExpiresAt(Instant expiresAt) { + this.expiresAt = expiresAt; + } + + public boolean isActive() { + return active; + } + + public void setActive(boolean active) { + this.active = active; + } + } + + /** Options for listing static policies. */ + public static class ListStaticPoliciesOptions { + private PolicyCategory category; + private PolicyTier tier; + private String organizationId; + private Boolean enabled; + private Integer limit; + private Integer offset; + private String sortBy; + private String sortOrder; + private String search; + + public static Builder builder() { + return new Builder(); + } + + public PolicyCategory getCategory() { + return category; + } + + public PolicyTier getTier() { + return tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public Boolean getEnabled() { + return enabled; + } + + public Integer getLimit() { + return limit; + } + + public Integer getOffset() { + return offset; + } + + public String getSortBy() { + return sortBy; + } + + public String getSortOrder() { + return sortOrder; + } + + public String getSearch() { + return search; + } + + public static class Builder { + private final ListStaticPoliciesOptions options = new ListStaticPoliciesOptions(); + + public Builder category(PolicyCategory category) { + options.category = category; + return this; + } + + public Builder tier(PolicyTier tier) { + options.tier = tier; + return this; + } + + /** + * Filters policies by organization ID (Enterprise). + * + * @param organizationId the organization ID + * @return this builder + */ + public Builder organizationId(String organizationId) { + options.organizationId = organizationId; + return this; + } + + public Builder enabled(Boolean enabled) { + options.enabled = enabled; + return this; + } + + public Builder limit(Integer limit) { + options.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + options.offset = offset; + return this; + } + + public Builder sortBy(String sortBy) { + options.sortBy = sortBy; + return this; + } + + public Builder sortOrder(String sortOrder) { + options.sortOrder = sortOrder; + return this; + } + + public Builder search(String search) { + options.search = search; + return this; + } + + public ListStaticPoliciesOptions build() { + return options; + } + } + } + + /** Request to create a new static policy. */ + public static class CreateStaticPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private PolicyTier tier = PolicyTier.TENANT; + + @JsonProperty("organization_id") + private String organizationId; + + private String pattern; + private PolicySeverity severity = PolicySeverity.MEDIUM; + private boolean enabled = true; + private PolicyAction action = PolicyAction.BLOCK; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public PolicyCategory getCategory() { + return category; + } + + public PolicyTier getTier() { + return tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public String getPattern() { + return pattern; + } + + public PolicySeverity getSeverity() { + return severity; + } + + public boolean isEnabled() { + return enabled; + } + + public PolicyAction getAction() { + return action; + } + + public static class Builder { + private final CreateStaticPolicyRequest request = new CreateStaticPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder tier(PolicyTier tier) { + request.tier = tier; + return this; + } + + /** + * Sets the organization ID for organization-tier policies (Enterprise). + * + * @param organizationId the organization ID + * @return this builder + */ + public Builder organizationId(String organizationId) { + request.organizationId = organizationId; + return this; + } + + public Builder pattern(String pattern) { + request.pattern = pattern; + return this; + } + + public Builder severity(PolicySeverity severity) { + request.severity = severity; + return this; + } + + public Builder enabled(boolean enabled) { + request.enabled = enabled; + return this; + } + + public Builder action(PolicyAction action) { + request.action = action; + return this; + } + + public CreateStaticPolicyRequest build() { + return request; + } + } + } + + /** Request to update an existing static policy. */ + public static class UpdateStaticPolicyRequest { + private String name; + private String description; + private PolicyCategory category; + private String pattern; + private PolicySeverity severity; + private Boolean enabled; + private PolicyAction action; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public PolicyCategory getCategory() { + return category; + } + + public String getPattern() { + return pattern; + } + + public PolicySeverity getSeverity() { + return severity; + } + + public Boolean getEnabled() { + return enabled; + } + + public PolicyAction getAction() { + return action; + } + + public static class Builder { + private final UpdateStaticPolicyRequest request = new UpdateStaticPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder category(PolicyCategory category) { + request.category = category; + return this; + } + + public Builder pattern(String pattern) { + request.pattern = pattern; + return this; + } + + public Builder severity(PolicySeverity severity) { + request.severity = severity; + return this; + } + + public Builder enabled(Boolean enabled) { + request.enabled = enabled; + return this; + } + + public Builder action(PolicyAction action) { + request.action = action; + return this; + } + + public UpdateStaticPolicyRequest build() { + return request; + } + } + } + + /** Request to create a policy override. */ + public static class CreatePolicyOverrideRequest { + @JsonProperty("action_override") + private OverrideAction actionOverride; + + @JsonProperty("override_reason") + private String overrideReason; + + @JsonProperty("expires_at") + private Instant expiresAt; + + public static Builder builder() { + return new Builder(); + } + + public OverrideAction getActionOverride() { + return actionOverride; + } + + public String getOverrideReason() { + return overrideReason; + } + + public Instant getExpiresAt() { + return expiresAt; + } + + public static class Builder { + private final CreatePolicyOverrideRequest request = new CreatePolicyOverrideRequest(); + + public Builder actionOverride(OverrideAction actionOverride) { + request.actionOverride = actionOverride; + return this; + } + + public Builder overrideReason(String overrideReason) { + request.overrideReason = overrideReason; + return this; + } + + public Builder expiresAt(Instant expiresAt) { + request.expiresAt = expiresAt; + return this; + } + + public CreatePolicyOverrideRequest build() { + return request; + } + } + } + + // ======================================================================== + // Dynamic Policy Types + // ======================================================================== + + /** Condition for dynamic policy evaluation. */ + public static class DynamicPolicyCondition { + private String field; + private String operator; + private Object value; + + public DynamicPolicyCondition() {} + + public DynamicPolicyCondition(String field, String operator, Object value) { + this.field = field; + this.operator = operator; + this.value = value; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + + public String getOperator() { + return operator; + } + + public void setOperator(String operator) { + this.operator = operator; + } + + public Object getValue() { + return value; + } + + public void setValue(Object value) { + this.value = value; + } + } + + /** Action to take when dynamic policy conditions are met. */ + public static class DynamicPolicyAction { + private String type; // "block", "alert", "redact", "log", "route", "modify_risk" + private Map config; + + public DynamicPolicyAction() {} + + public DynamicPolicyAction(String type, Map config) { + this.type = type; + this.config = config; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public Map getConfig() { + return config; + } + + public void setConfig(Map config) { + this.config = config; + } + } + + /** + * Dynamic policy definition. + * + *

Dynamic policies are LLM-powered policies that can evaluate complex, context-aware rules + * that can't be expressed with simple regex patterns. + * + *

For provider restrictions (GDPR, HIPAA, RBI compliance), use action config: + * + *

{@code
+   * List actions = List.of(
+   *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
+   * );
+   * }
+ */ + public static class DynamicPolicy { + private String id; + private String name; + private String description; + private String type; // "risk", "content", "user", "cost" + private String category; // "dynamic-risk", "dynamic-compliance", etc. + private PolicyTier tier; + + @JsonProperty("organization_id") + private String organizationId; + + private List conditions; + private List actions; + private int priority; + private boolean enabled; + + @JsonProperty("created_at") + private Instant createdAt; + + @JsonProperty("updated_at") + private Instant updatedAt; + + // Getters and setters + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public PolicyTier getTier() { + return tier; + } + + public void setTier(PolicyTier tier) { + this.tier = tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public void setOrganizationId(String organizationId) { + this.organizationId = organizationId; + } + + public List getConditions() { + return conditions; + } + + public void setConditions(List conditions) { + this.conditions = conditions; + } + + public List getActions() { + return actions; + } + + public void setActions(List actions) { + this.actions = actions; + } + + public int getPriority() { + return priority; + } + + public void setPriority(int priority) { + this.priority = priority; + } + + public boolean isEnabled() { + return enabled; + } + + public void setEnabled(boolean enabled) { + this.enabled = enabled; + } + + public Instant getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(Instant createdAt) { + this.createdAt = createdAt; + } + + public Instant getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(Instant updatedAt) { + this.updatedAt = updatedAt; + } + } + + /** Options for listing dynamic policies. */ + public static class ListDynamicPoliciesOptions { + private String type; // Filter by policy type: "risk", "content", "user", "cost" + private PolicyTier tier; + private String organizationId; + private Boolean enabled; + private Integer limit; + private Integer offset; + private String sortBy; + private String sortOrder; + private String search; + + public static Builder builder() { + return new Builder(); + } + + public String getType() { + return type; + } + + public PolicyTier getTier() { + return tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public Boolean getEnabled() { + return enabled; + } + + public Integer getLimit() { + return limit; + } + + public Integer getOffset() { + return offset; + } + + public String getSortBy() { + return sortBy; + } + + public String getSortOrder() { + return sortOrder; + } + + public String getSearch() { + return search; + } + + public static class Builder { + private final ListDynamicPoliciesOptions options = new ListDynamicPoliciesOptions(); + + /** + * Filter by policy type: "risk", "content", "user", "cost". + * + * @param type the policy type + * @return this builder + */ + public Builder type(String type) { + options.type = type; + return this; + } + + /** + * Filters policies by tier. + * + * @param tier the policy tier + * @return this builder + */ + public Builder tier(PolicyTier tier) { + options.tier = tier; + return this; + } + + /** + * Filters policies by organization ID (Enterprise). + * + * @param organizationId the organization ID + * @return this builder + */ + public Builder organizationId(String organizationId) { + options.organizationId = organizationId; + return this; + } + + public Builder enabled(Boolean enabled) { + options.enabled = enabled; + return this; + } + + public Builder limit(Integer limit) { + options.limit = limit; + return this; + } + + public Builder offset(Integer offset) { + options.offset = offset; + return this; + } + + public Builder sortBy(String sortBy) { + options.sortBy = sortBy; + return this; + } + + public Builder sortOrder(String sortOrder) { + options.sortOrder = sortOrder; + return this; + } + + public Builder search(String search) { + options.search = search; + return this; + } + + public ListDynamicPoliciesOptions build() { + return options; + } + } + } + + /** + * Request to create a dynamic policy. + * + *

For provider restrictions, use action config with "allowed_providers" key: + * + *

{@code
+   * List actions = List.of(
+   *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
+   * );
+   * }
+ */ + public static class CreateDynamicPolicyRequest { + private String name; + private String description; + private String type; // "risk", "content", "user", "cost" + private String category; // "dynamic-risk", "dynamic-compliance", etc. + private PolicyTier tier = PolicyTier.TENANT; + + @JsonProperty("organization_id") + private String organizationId; + + private List conditions; + private List actions; + private int priority; + private boolean enabled = true; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getType() { + return type; + } + + public String getCategory() { + return category; + } + + public PolicyTier getTier() { + return tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public List getConditions() { + return conditions; + } + + public List getActions() { + return actions; + } + + public int getPriority() { + return priority; + } + + public boolean isEnabled() { + return enabled; + } + + public static class Builder { + private final CreateDynamicPolicyRequest request = new CreateDynamicPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + /** + * Sets the policy type: "risk", "content", "user", "cost". + * + * @param type the policy type + * @return this builder + */ + public Builder type(String type) { + request.type = type; + return this; + } + + /** + * Sets the policy category. Must start with "dynamic-". Examples: "dynamic-risk", + * "dynamic-compliance", "dynamic-security", "dynamic-cost", "dynamic-access". + * + * @param category the policy category + * @return this builder + */ + public Builder category(String category) { + request.category = category; + return this; + } + + /** + * Sets the policy tier. Defaults to {@link PolicyTier#TENANT}. + * + * @param tier the policy tier + * @return this builder + */ + public Builder tier(PolicyTier tier) { + request.tier = tier; + return this; + } + + /** + * Sets the organization ID for organization-tier policies (Enterprise). + * + * @param organizationId the organization ID + * @return this builder + */ + public Builder organizationId(String organizationId) { + request.organizationId = organizationId; + return this; + } + + public Builder conditions(List conditions) { + request.conditions = conditions; + return this; + } + + public Builder actions(List actions) { + request.actions = actions; + return this; + } + + public Builder priority(int priority) { + request.priority = priority; + return this; + } + + public Builder enabled(boolean enabled) { + request.enabled = enabled; + return this; + } + + public CreateDynamicPolicyRequest build() { + return request; + } + } + } + + /** + * Request to update a dynamic policy. + * + *

For provider restrictions, use action config with "allowed_providers" key: + * + *

{@code
+   * List actions = List.of(
+   *     new DynamicPolicyAction("route", Map.of("allowed_providers", List.of("ollama", "azure-eu")))
+   * );
+   * }
+ */ + public static class UpdateDynamicPolicyRequest { + private String name; + private String description; + private String type; + private String category; + private PolicyTier tier; + + @JsonProperty("organization_id") + private String organizationId; + + private List conditions; + private List actions; + private Integer priority; + private Boolean enabled; + + public static Builder builder() { + return new Builder(); + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public String getType() { + return type; + } + + public String getCategory() { + return category; + } + + public PolicyTier getTier() { + return tier; + } + + public String getOrganizationId() { + return organizationId; + } + + public List getConditions() { + return conditions; + } + + public List getActions() { + return actions; + } + + public Integer getPriority() { + return priority; + } + + public Boolean getEnabled() { + return enabled; + } + + public static class Builder { + private final UpdateDynamicPolicyRequest request = new UpdateDynamicPolicyRequest(); + + public Builder name(String name) { + request.name = name; + return this; + } + + public Builder description(String description) { + request.description = description; + return this; + } + + public Builder type(String type) { + request.type = type; + return this; + } + + /** + * Sets the policy category. Must start with "dynamic-" if specified. + * + * @param category the policy category + * @return this builder + */ + public Builder category(String category) { + request.category = category; + return this; + } + + /** + * Sets the policy tier. + * + * @param tier the policy tier + * @return this builder + */ + public Builder tier(PolicyTier tier) { + request.tier = tier; + return this; + } + + /** + * Sets the organization ID for organization-tier policies (Enterprise). + * + * @param organizationId the organization ID + * @return this builder + */ + public Builder organizationId(String organizationId) { + request.organizationId = organizationId; + return this; + } + + public Builder conditions(List conditions) { + request.conditions = conditions; + return this; + } + + public Builder actions(List actions) { + request.actions = actions; + return this; + } + + public Builder priority(Integer priority) { + request.priority = priority; + return this; + } + + public Builder enabled(Boolean enabled) { + request.enabled = enabled; + return this; + } + + public UpdateDynamicPolicyRequest build() { + return request; + } + } + } + + // ======================================================================== + // Pattern Testing Types + // ======================================================================== + + /** Result of testing a regex pattern. */ + public static class TestPatternResult { + private boolean valid; + private String error; + private String pattern; + private List inputs; + private List matches; + + public boolean isValid() { + return valid; + } + + public void setValid(boolean valid) { + this.valid = valid; + } + + public String getError() { + return error; + } + + public void setError(String error) { + this.error = error; + } + + public String getPattern() { + return pattern; + } + + public void setPattern(String pattern) { + this.pattern = pattern; + } + + public List getInputs() { + return inputs; + } + + public void setInputs(List inputs) { + this.inputs = inputs; + } + + public List getMatches() { + return matches; + } + + public void setMatches(List matches) { + this.matches = matches; + } + } + + /** Individual pattern match result. */ + public static class TestPatternMatch { + private String input; + private boolean matched; + + @JsonProperty("matched_text") + private String matchedText; + + private Integer position; + + public String getInput() { + return input; + } + + public void setInput(String input) { + this.input = input; + } + + public boolean isMatched() { + return matched; + } + + public void setMatched(boolean matched) { + this.matched = matched; + } + + public String getMatchedText() { + return matchedText; + } + + public void setMatchedText(String matchedText) { + this.matchedText = matchedText; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + } + + // ======================================================================== + // Policy Version Types + // ======================================================================== + + /** Policy version history entry. */ + public static class PolicyVersion { + private int version; + + @JsonProperty("changed_by") + private String changedBy; + + @JsonProperty("changed_at") + private Instant changedAt; + + @JsonProperty("change_type") + private String changeType; + + @JsonProperty("change_description") + private String changeDescription; + + @JsonProperty("previous_values") + private Map previousValues; + + @JsonProperty("new_values") + private Map newValues; + + public int getVersion() { + return version; + } + + public void setVersion(int version) { + this.version = version; + } + + public String getChangedBy() { + return changedBy; + } + + public void setChangedBy(String changedBy) { + this.changedBy = changedBy; + } + + public Instant getChangedAt() { + return changedAt; + } + + public void setChangedAt(Instant changedAt) { + this.changedAt = changedAt; + } + + public String getChangeType() { + return changeType; + } + + public void setChangeType(String changeType) { + this.changeType = changeType; + } + + public String getChangeDescription() { + return changeDescription; + } + + public void setChangeDescription(String changeDescription) { + this.changeDescription = changeDescription; + } + + public Map getPreviousValues() { + return previousValues; + } + + public void setPreviousValues(Map previousValues) { + this.previousValues = previousValues; + } + + public Map getNewValues() { + return newValues; + } + + public void setNewValues(Map newValues) { + this.newValues = newValues; + } + } + + /** Options for getting effective policies. */ + public static class EffectivePoliciesOptions { + private PolicyCategory category; + private boolean includeDisabled; + private boolean includeOverridden; + + public static Builder builder() { + return new Builder(); + } + + public PolicyCategory getCategory() { + return category; + } + + public boolean isIncludeDisabled() { + return includeDisabled; + } + + public boolean isIncludeOverridden() { + return includeOverridden; + } + + public static class Builder { + private final EffectivePoliciesOptions options = new EffectivePoliciesOptions(); + + public Builder category(PolicyCategory category) { + options.category = category; + return this; + } + + public Builder includeDisabled(boolean includeDisabled) { + options.includeDisabled = includeDisabled; + return this; + } + + public Builder includeOverridden(boolean includeOverridden) { + options.includeOverridden = includeOverridden; + return this; + } + + public EffectivePoliciesOptions build() { + return options; + } + } + } + + // ======================================================================== + // Response Wrappers + // ======================================================================== + + /** Wrapper for list static policies response. */ + public static class StaticPoliciesResponse { + private List policies; + + public List getPolicies() { + return policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + } + + /** Wrapper for effective policies response. */ + public static class EffectivePoliciesResponse { + @JsonProperty("static") + private List staticPolicies; + + @JsonProperty("dynamic") + private List dynamicPolicies; + + public List getStaticPolicies() { + return staticPolicies; + } + + public void setStaticPolicies(List staticPolicies) { + this.staticPolicies = staticPolicies; + } + + public List getDynamicPolicies() { + return dynamicPolicies; + } + + public void setDynamicPolicies(List dynamicPolicies) { + this.dynamicPolicies = dynamicPolicies; + } + } + + /** + * Wrapper for list dynamic policies response. Agent proxy (Issue #886) returns {"policies": + * [...]} wrapper. + */ + public static class DynamicPoliciesResponse { + private List policies; + + public List getPolicies() { + return policies; + } + + public void setPolicies(List policies) { + this.policies = policies; + } + } + + /** + * Wrapper for single dynamic policy response. Agent proxy (Issue #886) returns {"policy": {...}} + * wrapper. + */ + public static class DynamicPolicyResponse { + private DynamicPolicy policy; + + public DynamicPolicy getPolicy() { + return policy; + } + + public void setPolicy(DynamicPolicy policy) { + this.policy = policy; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/webhook/WebhookTypes.java b/src/main/java/com/getaxonflow/sdk/types/webhook/WebhookTypes.java index 720d284..07692eb 100644 --- a/src/main/java/com/getaxonflow/sdk/types/webhook/WebhookTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/webhook/WebhookTypes.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; @@ -27,359 +26,367 @@ * Webhook subscription types for AxonFlow SDK. * *

This class contains all types needed for webhook CRUD operations including: + * *

    - *
  • Creating webhook subscriptions
  • - *
  • Updating webhook subscriptions
  • - *
  • Listing webhook subscriptions
  • + *
  • Creating webhook subscriptions + *
  • Updating webhook subscriptions + *
  • Listing webhook subscriptions *
*/ public final class WebhookTypes { - private WebhookTypes() { - // Utility class - } - - /** - * Request to create a new webhook subscription. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class CreateWebhookRequest { - - @JsonProperty("url") - private final String url; - - @JsonProperty("events") - private final List events; - - @JsonProperty("secret") - private final String secret; - - @JsonProperty("active") - private final boolean active; - - @JsonCreator - public CreateWebhookRequest( - @JsonProperty("url") String url, - @JsonProperty("events") List events, - @JsonProperty("secret") String secret, - @JsonProperty("active") boolean active) { - this.url = Objects.requireNonNull(url, "url is required"); - this.events = events != null ? Collections.unmodifiableList(events) : Collections.emptyList(); - this.secret = secret; - this.active = active; - } - - public String getUrl() { - return url; - } - - public List getEvents() { - return events; - } - - public String getSecret() { - return secret; - } - - public boolean isActive() { - return active; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String url; - private List events; - private String secret; - private boolean active = true; - - public Builder url(String url) { - this.url = url; - return this; - } - - public Builder events(List events) { - this.events = events; - return this; - } - - public Builder secret(String secret) { - this.secret = secret; - return this; - } - - public Builder active(boolean active) { - this.active = active; - return this; - } - - public CreateWebhookRequest build() { - return new CreateWebhookRequest(url, events, secret, active); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - CreateWebhookRequest that = (CreateWebhookRequest) o; - return active == that.active && - Objects.equals(url, that.url) && - Objects.equals(events, that.events) && - Objects.equals(secret, that.secret); - } - - @Override - public int hashCode() { - return Objects.hash(url, events, secret, active); - } - - @Override - public String toString() { - return "CreateWebhookRequest{" + - "url='" + url + '\'' + - ", events=" + events + - ", active=" + active + - '}'; - } - } - - /** - * A webhook subscription. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class WebhookSubscription { - - @JsonProperty("id") - private final String id; - - @JsonProperty("url") - private final String url; - - @JsonProperty("events") - private final List events; - - @JsonProperty("active") - private final boolean active; - - @JsonProperty("created_at") - private final String createdAt; - - @JsonProperty("updated_at") - private final String updatedAt; - - @JsonCreator - public WebhookSubscription( - @JsonProperty("id") String id, - @JsonProperty("url") String url, - @JsonProperty("events") List events, - @JsonProperty("active") boolean active, - @JsonProperty("created_at") String createdAt, - @JsonProperty("updated_at") String updatedAt) { - this.id = id; - this.url = url; - this.events = events != null ? Collections.unmodifiableList(events) : Collections.emptyList(); - this.active = active; - this.createdAt = createdAt; - this.updatedAt = updatedAt; - } - - public String getId() { - return id; - } - - public String getUrl() { - return url; - } - - public List getEvents() { - return events; - } - - public boolean isActive() { - return active; - } - - public String getCreatedAt() { - return createdAt; - } - - public String getUpdatedAt() { - return updatedAt; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - WebhookSubscription that = (WebhookSubscription) o; - return active == that.active && - Objects.equals(id, that.id) && - Objects.equals(url, that.url) && - Objects.equals(events, that.events) && - Objects.equals(createdAt, that.createdAt) && - Objects.equals(updatedAt, that.updatedAt); - } - - @Override - public int hashCode() { - return Objects.hash(id, url, events, active, createdAt, updatedAt); - } - - @Override - public String toString() { - return "WebhookSubscription{" + - "id='" + id + '\'' + - ", url='" + url + '\'' + - ", events=" + events + - ", active=" + active + - ", createdAt='" + createdAt + '\'' + - ", updatedAt='" + updatedAt + '\'' + - '}'; - } - } - - /** - * Request to update an existing webhook subscription. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class UpdateWebhookRequest { - - @JsonProperty("url") - private final String url; - - @JsonProperty("events") - private final List events; - - @JsonProperty("active") - private final Boolean active; - - @JsonCreator - public UpdateWebhookRequest( - @JsonProperty("url") String url, - @JsonProperty("events") List events, - @JsonProperty("active") Boolean active) { - this.url = url; - this.events = events != null ? Collections.unmodifiableList(events) : null; - this.active = active; - } - - public String getUrl() { - return url; - } - - public List getEvents() { - return events; - } - - public Boolean getActive() { - return active; - } - - public static Builder builder() { - return new Builder(); - } - - public static final class Builder { - private String url; - private List events; - private Boolean active; - - public Builder url(String url) { - this.url = url; - return this; - } - - public Builder events(List events) { - this.events = events; - return this; - } - - public Builder active(Boolean active) { - this.active = active; - return this; - } - - public UpdateWebhookRequest build() { - return new UpdateWebhookRequest(url, events, active); - } - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - UpdateWebhookRequest that = (UpdateWebhookRequest) o; - return Objects.equals(url, that.url) && - Objects.equals(events, that.events) && - Objects.equals(active, that.active); - } - - @Override - public int hashCode() { - return Objects.hash(url, events, active); - } - - @Override - public String toString() { - return "UpdateWebhookRequest{" + - "url='" + url + '\'' + - ", events=" + events + - ", active=" + active + - '}'; - } - } - - /** - * Response containing a list of webhook subscriptions. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class ListWebhooksResponse { - - @JsonProperty("webhooks") - private final List webhooks; - - @JsonProperty("total") - private final int total; - - @JsonCreator - public ListWebhooksResponse( - @JsonProperty("webhooks") List webhooks, - @JsonProperty("total") int total) { - this.webhooks = webhooks != null ? Collections.unmodifiableList(webhooks) : Collections.emptyList(); - this.total = total; - } - - public List getWebhooks() { - return webhooks; - } - - public int getTotal() { - return total; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ListWebhooksResponse that = (ListWebhooksResponse) o; - return total == that.total && - Objects.equals(webhooks, that.webhooks); - } - - @Override - public int hashCode() { - return Objects.hash(webhooks, total); - } - - @Override - public String toString() { - return "ListWebhooksResponse{" + - "webhooks=" + webhooks + - ", total=" + total + - '}'; - } + private WebhookTypes() { + // Utility class + } + + /** Request to create a new webhook subscription. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class CreateWebhookRequest { + + @JsonProperty("url") + private final String url; + + @JsonProperty("events") + private final List events; + + @JsonProperty("secret") + private final String secret; + + @JsonProperty("active") + private final boolean active; + + @JsonCreator + public CreateWebhookRequest( + @JsonProperty("url") String url, + @JsonProperty("events") List events, + @JsonProperty("secret") String secret, + @JsonProperty("active") boolean active) { + this.url = Objects.requireNonNull(url, "url is required"); + this.events = events != null ? Collections.unmodifiableList(events) : Collections.emptyList(); + this.secret = secret; + this.active = active; + } + + public String getUrl() { + return url; + } + + public List getEvents() { + return events; + } + + public String getSecret() { + return secret; + } + + public boolean isActive() { + return active; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String url; + private List events; + private String secret; + private boolean active = true; + + public Builder url(String url) { + this.url = url; + return this; + } + + public Builder events(List events) { + this.events = events; + return this; + } + + public Builder secret(String secret) { + this.secret = secret; + return this; + } + + public Builder active(boolean active) { + this.active = active; + return this; + } + + public CreateWebhookRequest build() { + return new CreateWebhookRequest(url, events, secret, active); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CreateWebhookRequest that = (CreateWebhookRequest) o; + return active == that.active + && Objects.equals(url, that.url) + && Objects.equals(events, that.events) + && Objects.equals(secret, that.secret); + } + + @Override + public int hashCode() { + return Objects.hash(url, events, secret, active); + } + + @Override + public String toString() { + return "CreateWebhookRequest{" + + "url='" + + url + + '\'' + + ", events=" + + events + + ", active=" + + active + + '}'; + } + } + + /** A webhook subscription. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class WebhookSubscription { + + @JsonProperty("id") + private final String id; + + @JsonProperty("url") + private final String url; + + @JsonProperty("events") + private final List events; + + @JsonProperty("active") + private final boolean active; + + @JsonProperty("created_at") + private final String createdAt; + + @JsonProperty("updated_at") + private final String updatedAt; + + @JsonCreator + public WebhookSubscription( + @JsonProperty("id") String id, + @JsonProperty("url") String url, + @JsonProperty("events") List events, + @JsonProperty("active") boolean active, + @JsonProperty("created_at") String createdAt, + @JsonProperty("updated_at") String updatedAt) { + this.id = id; + this.url = url; + this.events = events != null ? Collections.unmodifiableList(events) : Collections.emptyList(); + this.active = active; + this.createdAt = createdAt; + this.updatedAt = updatedAt; + } + + public String getId() { + return id; + } + + public String getUrl() { + return url; + } + + public List getEvents() { + return events; + } + + public boolean isActive() { + return active; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + WebhookSubscription that = (WebhookSubscription) o; + return active == that.active + && Objects.equals(id, that.id) + && Objects.equals(url, that.url) + && Objects.equals(events, that.events) + && Objects.equals(createdAt, that.createdAt) + && Objects.equals(updatedAt, that.updatedAt); + } + + @Override + public int hashCode() { + return Objects.hash(id, url, events, active, createdAt, updatedAt); + } + + @Override + public String toString() { + return "WebhookSubscription{" + + "id='" + + id + + '\'' + + ", url='" + + url + + '\'' + + ", events=" + + events + + ", active=" + + active + + ", createdAt='" + + createdAt + + '\'' + + ", updatedAt='" + + updatedAt + + '\'' + + '}'; + } + } + + /** Request to update an existing webhook subscription. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class UpdateWebhookRequest { + + @JsonProperty("url") + private final String url; + + @JsonProperty("events") + private final List events; + + @JsonProperty("active") + private final Boolean active; + + @JsonCreator + public UpdateWebhookRequest( + @JsonProperty("url") String url, + @JsonProperty("events") List events, + @JsonProperty("active") Boolean active) { + this.url = url; + this.events = events != null ? Collections.unmodifiableList(events) : null; + this.active = active; + } + + public String getUrl() { + return url; + } + + public List getEvents() { + return events; + } + + public Boolean getActive() { + return active; + } + + public static Builder builder() { + return new Builder(); + } + + public static final class Builder { + private String url; + private List events; + private Boolean active; + + public Builder url(String url) { + this.url = url; + return this; + } + + public Builder events(List events) { + this.events = events; + return this; + } + + public Builder active(Boolean active) { + this.active = active; + return this; + } + + public UpdateWebhookRequest build() { + return new UpdateWebhookRequest(url, events, active); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + UpdateWebhookRequest that = (UpdateWebhookRequest) o; + return Objects.equals(url, that.url) + && Objects.equals(events, that.events) + && Objects.equals(active, that.active); + } + + @Override + public int hashCode() { + return Objects.hash(url, events, active); + } + + @Override + public String toString() { + return "UpdateWebhookRequest{" + + "url='" + + url + + '\'' + + ", events=" + + events + + ", active=" + + active + + '}'; + } + } + + /** Response containing a list of webhook subscriptions. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ListWebhooksResponse { + + @JsonProperty("webhooks") + private final List webhooks; + + @JsonProperty("total") + private final int total; + + @JsonCreator + public ListWebhooksResponse( + @JsonProperty("webhooks") List webhooks, + @JsonProperty("total") int total) { + this.webhooks = + webhooks != null ? Collections.unmodifiableList(webhooks) : Collections.emptyList(); + this.total = total; + } + + public List getWebhooks() { + return webhooks; + } + + public int getTotal() { + return total; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ListWebhooksResponse that = (ListWebhooksResponse) o; + return total == that.total && Objects.equals(webhooks, that.webhooks); + } + + @Override + public int hashCode() { + return Objects.hash(webhooks, total); + } + + @Override + public String toString() { + return "ListWebhooksResponse{" + "webhooks=" + webhooks + ", total=" + total + '}'; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/webhook/package-info.java b/src/main/java/com/getaxonflow/sdk/types/webhook/package-info.java index 454c611..68533b0 100644 --- a/src/main/java/com/getaxonflow/sdk/types/webhook/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/webhook/package-info.java @@ -17,8 +17,8 @@ /** * Webhook subscription types for AxonFlow SDK. * - *

This package contains types for managing webhook subscriptions including - * create, read, update, delete, and list operations. + *

This package contains types for managing webhook subscriptions including create, read, update, + * delete, and list operations. * * @see com.getaxonflow.sdk.types.webhook.WebhookTypes * @see com.getaxonflow.sdk.AxonFlow#createWebhook diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/PlanExecutionResponse.java b/src/main/java/com/getaxonflow/sdk/types/workflow/PlanExecutionResponse.java index 37d7e0d..43ff836 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/PlanExecutionResponse.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/PlanExecutionResponse.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.time.Instant; import java.util.Collections; import java.util.List; @@ -28,339 +27,360 @@ /** * Response from executing a plan in Multi-Agent Planning (MAP). * - *

Contains the execution result, policy evaluation information, - * and metadata about the plan execution. + *

Contains the execution result, policy evaluation information, and metadata about the plan + * execution. * * @since 2.3.0 */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PlanExecutionResponse { - @JsonProperty("plan_id") - private final String planId; + @JsonProperty("plan_id") + private final String planId; + + @JsonProperty("status") + private final String status; + + @JsonProperty("result") + private final String result; + + @JsonProperty("steps_completed") + private final int stepsCompleted; + + @JsonProperty("total_steps") + private final int totalSteps; + + @JsonProperty("started_at") + private final Instant startedAt; + + @JsonProperty("completed_at") + private final Instant completedAt; + + @JsonProperty("step_results") + private final List stepResults; + + @JsonProperty("policy_info") + private final PolicyEvaluationResult policyInfo; + + @JsonProperty("metadata") + private final Map metadata; + + @JsonCreator + public PlanExecutionResponse( + @JsonProperty("plan_id") String planId, + @JsonProperty("status") String status, + @JsonProperty("result") String result, + @JsonProperty("steps_completed") int stepsCompleted, + @JsonProperty("total_steps") int totalSteps, + @JsonProperty("started_at") Instant startedAt, + @JsonProperty("completed_at") Instant completedAt, + @JsonProperty("step_results") List stepResults, + @JsonProperty("policy_info") PolicyEvaluationResult policyInfo, + @JsonProperty("metadata") Map metadata) { + this.planId = planId; + this.status = status; + this.result = result; + this.stepsCompleted = stepsCompleted; + this.totalSteps = totalSteps; + this.startedAt = startedAt; + this.completedAt = completedAt; + this.stepResults = + stepResults != null ? Collections.unmodifiableList(stepResults) : Collections.emptyList(); + this.policyInfo = policyInfo; + this.metadata = + metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + } + + /** + * Returns the unique identifier of the executed plan. + * + * @return the plan ID + */ + public String getPlanId() { + return planId; + } + + /** + * Returns the current execution status. + * + *

Possible values: "pending", "in_progress", "completed", "failed", "blocked". + * + * @return the execution status + */ + public String getStatus() { + return status; + } + + /** + * Returns the execution result or error message. + * + * @return the result string, or null if not yet completed + */ + public String getResult() { + return result; + } + + /** + * Returns the number of steps that have been completed. + * + * @return the count of completed steps + */ + public int getStepsCompleted() { + return stepsCompleted; + } + + /** + * Returns the total number of steps in the plan. + * + * @return the total step count + */ + public int getTotalSteps() { + return totalSteps; + } + + /** + * Returns when the plan execution started. + * + * @return the start timestamp, or null if not yet started + */ + public Instant getStartedAt() { + return startedAt; + } + + /** + * Returns when the plan execution completed. + * + * @return the completion timestamp, or null if not yet completed + */ + public Instant getCompletedAt() { + return completedAt; + } + + /** + * Returns the results of individual steps. + * + * @return immutable list of step results + */ + public List getStepResults() { + return stepResults; + } + + /** + * Returns the policy evaluation information for this execution. + * + *

Contains details about which policies were applied, the risk score, and any required + * actions. + * + * @return the policy evaluation result, or null if no policy evaluation was performed + * @since 2.3.0 + */ + public PolicyEvaluationResult getPolicyInfo() { + return policyInfo; + } + + /** + * Returns additional metadata about the execution. + * + * @return immutable map of metadata + */ + public Map getMetadata() { + return metadata; + } + + /** + * Checks if the plan execution completed successfully. + * + * @return true if status is "completed" + */ + public boolean isCompleted() { + return "completed".equalsIgnoreCase(status); + } + + /** + * Checks if the plan execution failed. + * + * @return true if status is "failed" + */ + public boolean isFailed() { + return "failed".equalsIgnoreCase(status); + } + + /** + * Checks if the plan execution was blocked by policy. + * + * @return true if status is "blocked" + */ + public boolean isBlocked() { + return "blocked".equalsIgnoreCase(status); + } + + /** + * Checks if the plan execution is still in progress. + * + * @return true if status is "in_progress" or "pending" + */ + public boolean isInProgress() { + return "in_progress".equalsIgnoreCase(status) || "pending".equalsIgnoreCase(status); + } + + /** + * Calculates the progress percentage. + * + * @return progress as a value between 0.0 and 1.0 + */ + public double getProgress() { + if (totalSteps == 0) { + return 0.0; + } + return (double) stepsCompleted / totalSteps; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PlanExecutionResponse that = (PlanExecutionResponse) o; + return stepsCompleted == that.stepsCompleted + && totalSteps == that.totalSteps + && Objects.equals(planId, that.planId) + && Objects.equals(status, that.status) + && Objects.equals(result, that.result) + && Objects.equals(startedAt, that.startedAt) + && Objects.equals(completedAt, that.completedAt) + && Objects.equals(stepResults, that.stepResults) + && Objects.equals(policyInfo, that.policyInfo) + && Objects.equals(metadata, that.metadata); + } + + @Override + public int hashCode() { + return Objects.hash( + planId, + status, + result, + stepsCompleted, + totalSteps, + startedAt, + completedAt, + stepResults, + policyInfo, + metadata); + } + + @Override + public String toString() { + return "PlanExecutionResponse{" + + "planId='" + + planId + + '\'' + + ", status='" + + status + + '\'' + + ", stepsCompleted=" + + stepsCompleted + + ", totalSteps=" + + totalSteps + + ", policyInfo=" + + policyInfo + + '}'; + } + + /** Result of an individual step execution. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class StepResult { + + @JsonProperty("step_index") + private final int stepIndex; + + @JsonProperty("step_name") + private final String stepName; @JsonProperty("status") private final String status; - @JsonProperty("result") - private final String result; - - @JsonProperty("steps_completed") - private final int stepsCompleted; - - @JsonProperty("total_steps") - private final int totalSteps; - - @JsonProperty("started_at") - private final Instant startedAt; - - @JsonProperty("completed_at") - private final Instant completedAt; - - @JsonProperty("step_results") - private final List stepResults; + @JsonProperty("output") + private final String output; - @JsonProperty("policy_info") - private final PolicyEvaluationResult policyInfo; + @JsonProperty("error") + private final String error; - @JsonProperty("metadata") - private final Map metadata; + @JsonProperty("duration_ms") + private final long durationMs; @JsonCreator - public PlanExecutionResponse( - @JsonProperty("plan_id") String planId, - @JsonProperty("status") String status, - @JsonProperty("result") String result, - @JsonProperty("steps_completed") int stepsCompleted, - @JsonProperty("total_steps") int totalSteps, - @JsonProperty("started_at") Instant startedAt, - @JsonProperty("completed_at") Instant completedAt, - @JsonProperty("step_results") List stepResults, - @JsonProperty("policy_info") PolicyEvaluationResult policyInfo, - @JsonProperty("metadata") Map metadata) { - this.planId = planId; - this.status = status; - this.result = result; - this.stepsCompleted = stepsCompleted; - this.totalSteps = totalSteps; - this.startedAt = startedAt; - this.completedAt = completedAt; - this.stepResults = stepResults != null ? Collections.unmodifiableList(stepResults) : Collections.emptyList(); - this.policyInfo = policyInfo; - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - } - - /** - * Returns the unique identifier of the executed plan. - * - * @return the plan ID - */ - public String getPlanId() { - return planId; - } - - /** - * Returns the current execution status. - * - *

Possible values: "pending", "in_progress", "completed", "failed", "blocked". - * - * @return the execution status - */ - public String getStatus() { - return status; - } - - /** - * Returns the execution result or error message. - * - * @return the result string, or null if not yet completed - */ - public String getResult() { - return result; + public StepResult( + @JsonProperty("step_index") int stepIndex, + @JsonProperty("step_name") String stepName, + @JsonProperty("status") String status, + @JsonProperty("output") String output, + @JsonProperty("error") String error, + @JsonProperty("duration_ms") long durationMs) { + this.stepIndex = stepIndex; + this.stepName = stepName; + this.status = status; + this.output = output; + this.error = error; + this.durationMs = durationMs; } - /** - * Returns the number of steps that have been completed. - * - * @return the count of completed steps - */ - public int getStepsCompleted() { - return stepsCompleted; + public int getStepIndex() { + return stepIndex; } - /** - * Returns the total number of steps in the plan. - * - * @return the total step count - */ - public int getTotalSteps() { - return totalSteps; + public String getStepName() { + return stepName; } - /** - * Returns when the plan execution started. - * - * @return the start timestamp, or null if not yet started - */ - public Instant getStartedAt() { - return startedAt; - } - - /** - * Returns when the plan execution completed. - * - * @return the completion timestamp, or null if not yet completed - */ - public Instant getCompletedAt() { - return completedAt; - } - - /** - * Returns the results of individual steps. - * - * @return immutable list of step results - */ - public List getStepResults() { - return stepResults; - } - - /** - * Returns the policy evaluation information for this execution. - * - *

Contains details about which policies were applied, the risk score, - * and any required actions. - * - * @return the policy evaluation result, or null if no policy evaluation was performed - * @since 2.3.0 - */ - public PolicyEvaluationResult getPolicyInfo() { - return policyInfo; - } - - /** - * Returns additional metadata about the execution. - * - * @return immutable map of metadata - */ - public Map getMetadata() { - return metadata; - } - - /** - * Checks if the plan execution completed successfully. - * - * @return true if status is "completed" - */ - public boolean isCompleted() { - return "completed".equalsIgnoreCase(status); + public String getStatus() { + return status; } - /** - * Checks if the plan execution failed. - * - * @return true if status is "failed" - */ - public boolean isFailed() { - return "failed".equalsIgnoreCase(status); + public String getOutput() { + return output; } - /** - * Checks if the plan execution was blocked by policy. - * - * @return true if status is "blocked" - */ - public boolean isBlocked() { - return "blocked".equalsIgnoreCase(status); + public String getError() { + return error; } - /** - * Checks if the plan execution is still in progress. - * - * @return true if status is "in_progress" or "pending" - */ - public boolean isInProgress() { - return "in_progress".equalsIgnoreCase(status) || "pending".equalsIgnoreCase(status); + public long getDurationMs() { + return durationMs; } - /** - * Calculates the progress percentage. - * - * @return progress as a value between 0.0 and 1.0 - */ - public double getProgress() { - if (totalSteps == 0) { - return 0.0; - } - return (double) stepsCompleted / totalSteps; + public boolean isSuccess() { + return "completed".equalsIgnoreCase(status) || "success".equalsIgnoreCase(status); } @Override public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PlanExecutionResponse that = (PlanExecutionResponse) o; - return stepsCompleted == that.stepsCompleted && - totalSteps == that.totalSteps && - Objects.equals(planId, that.planId) && - Objects.equals(status, that.status) && - Objects.equals(result, that.result) && - Objects.equals(startedAt, that.startedAt) && - Objects.equals(completedAt, that.completedAt) && - Objects.equals(stepResults, that.stepResults) && - Objects.equals(policyInfo, that.policyInfo) && - Objects.equals(metadata, that.metadata); + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + StepResult that = (StepResult) o; + return stepIndex == that.stepIndex + && durationMs == that.durationMs + && Objects.equals(stepName, that.stepName) + && Objects.equals(status, that.status) + && Objects.equals(output, that.output) + && Objects.equals(error, that.error); } @Override public int hashCode() { - return Objects.hash(planId, status, result, stepsCompleted, totalSteps, - startedAt, completedAt, stepResults, policyInfo, metadata); + return Objects.hash(stepIndex, stepName, status, output, error, durationMs); } @Override public String toString() { - return "PlanExecutionResponse{" + - "planId='" + planId + '\'' + - ", status='" + status + '\'' + - ", stepsCompleted=" + stepsCompleted + - ", totalSteps=" + totalSteps + - ", policyInfo=" + policyInfo + - '}'; - } - - /** - * Result of an individual step execution. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class StepResult { - - @JsonProperty("step_index") - private final int stepIndex; - - @JsonProperty("step_name") - private final String stepName; - - @JsonProperty("status") - private final String status; - - @JsonProperty("output") - private final String output; - - @JsonProperty("error") - private final String error; - - @JsonProperty("duration_ms") - private final long durationMs; - - @JsonCreator - public StepResult( - @JsonProperty("step_index") int stepIndex, - @JsonProperty("step_name") String stepName, - @JsonProperty("status") String status, - @JsonProperty("output") String output, - @JsonProperty("error") String error, - @JsonProperty("duration_ms") long durationMs) { - this.stepIndex = stepIndex; - this.stepName = stepName; - this.status = status; - this.output = output; - this.error = error; - this.durationMs = durationMs; - } - - public int getStepIndex() { - return stepIndex; - } - - public String getStepName() { - return stepName; - } - - public String getStatus() { - return status; - } - - public String getOutput() { - return output; - } - - public String getError() { - return error; - } - - public long getDurationMs() { - return durationMs; - } - - public boolean isSuccess() { - return "completed".equalsIgnoreCase(status) || "success".equalsIgnoreCase(status); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - StepResult that = (StepResult) o; - return stepIndex == that.stepIndex && - durationMs == that.durationMs && - Objects.equals(stepName, that.stepName) && - Objects.equals(status, that.status) && - Objects.equals(output, that.output) && - Objects.equals(error, that.error); - } - - @Override - public int hashCode() { - return Objects.hash(stepIndex, stepName, status, output, error, durationMs); - } - - @Override - public String toString() { - return "StepResult{" + - "stepIndex=" + stepIndex + - ", stepName='" + stepName + '\'' + - ", status='" + status + '\'' + - '}'; - } + return "StepResult{" + + "stepIndex=" + + stepIndex + + ", stepName='" + + stepName + + '\'' + + ", status='" + + status + + '\'' + + '}'; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyEvaluationResult.java b/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyEvaluationResult.java index 4484da9..0367b2b 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyEvaluationResult.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyEvaluationResult.java @@ -18,7 +18,6 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Collections; import java.util.List; import java.util.Objects; @@ -26,200 +25,211 @@ /** * Result of a policy evaluation during workflow execution. * - *

Contains detailed information about whether a step or plan execution - * was allowed based on policy checks, including risk assessment and - * any required actions. + *

Contains detailed information about whether a step or plan execution was allowed based on + * policy checks, including risk assessment and any required actions. * * @since 2.3.0 */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PolicyEvaluationResult { - @JsonProperty("allowed") - private final boolean allowed; - - @JsonProperty("applied_policies") - private final List appliedPolicies; - - @JsonProperty("risk_score") - private final double riskScore; - - @JsonProperty("required_actions") - private final List requiredActions; - - @JsonProperty("processing_time_ms") - private final long processingTimeMs; - - @JsonProperty("database_accessed") - private final Boolean databaseAccessed; - - @JsonCreator - public PolicyEvaluationResult( - @JsonProperty("allowed") boolean allowed, - @JsonProperty("applied_policies") List appliedPolicies, - @JsonProperty("risk_score") double riskScore, - @JsonProperty("required_actions") List requiredActions, - @JsonProperty("processing_time_ms") long processingTimeMs, - @JsonProperty("database_accessed") Boolean databaseAccessed) { - this.allowed = allowed; - this.appliedPolicies = appliedPolicies != null ? Collections.unmodifiableList(appliedPolicies) : Collections.emptyList(); - this.riskScore = riskScore; - this.requiredActions = requiredActions != null ? Collections.unmodifiableList(requiredActions) : Collections.emptyList(); - this.processingTimeMs = processingTimeMs; - this.databaseAccessed = databaseAccessed; - } - - /** - * Returns whether the operation was allowed by policy evaluation. - * - * @return true if the operation is allowed, false if blocked - */ - public boolean isAllowed() { - return allowed; - } - - /** - * Returns the list of policies that were applied during evaluation. - * - * @return immutable list of applied policy identifiers - */ - public List getAppliedPolicies() { - return appliedPolicies; - } - - /** - * Returns the calculated risk score for this operation. - * - *

Risk scores typically range from 0.0 (no risk) to 1.0 (high risk). - * - * @return the risk score - */ - public double getRiskScore() { - return riskScore; - } - - /** - * Returns the list of actions required before the operation can proceed. - * - *

Examples include "approval_required", "audit_required", "rate_limit_exceeded". - * - * @return immutable list of required action identifiers - */ - public List getRequiredActions() { - return requiredActions; - } - - /** - * Returns the time taken to evaluate policies in milliseconds. - * - * @return processing time in milliseconds - */ - public long getProcessingTimeMs() { - return processingTimeMs; - } - - /** - * Returns whether a database was accessed during policy evaluation. - * - *

This is useful for tracking whether dynamic policy lookups were performed. - * - * @return true if database was accessed, false otherwise, null if unknown - */ - public Boolean getDatabaseAccessed() { - return databaseAccessed; + @JsonProperty("allowed") + private final boolean allowed; + + @JsonProperty("applied_policies") + private final List appliedPolicies; + + @JsonProperty("risk_score") + private final double riskScore; + + @JsonProperty("required_actions") + private final List requiredActions; + + @JsonProperty("processing_time_ms") + private final long processingTimeMs; + + @JsonProperty("database_accessed") + private final Boolean databaseAccessed; + + @JsonCreator + public PolicyEvaluationResult( + @JsonProperty("allowed") boolean allowed, + @JsonProperty("applied_policies") List appliedPolicies, + @JsonProperty("risk_score") double riskScore, + @JsonProperty("required_actions") List requiredActions, + @JsonProperty("processing_time_ms") long processingTimeMs, + @JsonProperty("database_accessed") Boolean databaseAccessed) { + this.allowed = allowed; + this.appliedPolicies = + appliedPolicies != null + ? Collections.unmodifiableList(appliedPolicies) + : Collections.emptyList(); + this.riskScore = riskScore; + this.requiredActions = + requiredActions != null + ? Collections.unmodifiableList(requiredActions) + : Collections.emptyList(); + this.processingTimeMs = processingTimeMs; + this.databaseAccessed = databaseAccessed; + } + + /** + * Returns whether the operation was allowed by policy evaluation. + * + * @return true if the operation is allowed, false if blocked + */ + public boolean isAllowed() { + return allowed; + } + + /** + * Returns the list of policies that were applied during evaluation. + * + * @return immutable list of applied policy identifiers + */ + public List getAppliedPolicies() { + return appliedPolicies; + } + + /** + * Returns the calculated risk score for this operation. + * + *

Risk scores typically range from 0.0 (no risk) to 1.0 (high risk). + * + * @return the risk score + */ + public double getRiskScore() { + return riskScore; + } + + /** + * Returns the list of actions required before the operation can proceed. + * + *

Examples include "approval_required", "audit_required", "rate_limit_exceeded". + * + * @return immutable list of required action identifiers + */ + public List getRequiredActions() { + return requiredActions; + } + + /** + * Returns the time taken to evaluate policies in milliseconds. + * + * @return processing time in milliseconds + */ + public long getProcessingTimeMs() { + return processingTimeMs; + } + + /** + * Returns whether a database was accessed during policy evaluation. + * + *

This is useful for tracking whether dynamic policy lookups were performed. + * + * @return true if database was accessed, false otherwise, null if unknown + */ + public Boolean getDatabaseAccessed() { + return databaseAccessed; + } + + /** + * Convenience method to check if database was accessed. + * + * @return true if database was definitely accessed, false otherwise + */ + public boolean wasDatabaseAccessed() { + return Boolean.TRUE.equals(databaseAccessed); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyEvaluationResult that = (PolicyEvaluationResult) o; + return allowed == that.allowed + && Double.compare(that.riskScore, riskScore) == 0 + && processingTimeMs == that.processingTimeMs + && Objects.equals(appliedPolicies, that.appliedPolicies) + && Objects.equals(requiredActions, that.requiredActions) + && Objects.equals(databaseAccessed, that.databaseAccessed); + } + + @Override + public int hashCode() { + return Objects.hash( + allowed, appliedPolicies, riskScore, requiredActions, processingTimeMs, databaseAccessed); + } + + @Override + public String toString() { + return "PolicyEvaluationResult{" + + "allowed=" + + allowed + + ", appliedPolicies=" + + appliedPolicies + + ", riskScore=" + + riskScore + + ", requiredActions=" + + requiredActions + + ", processingTimeMs=" + + processingTimeMs + + ", databaseAccessed=" + + databaseAccessed + + '}'; + } + + /** + * Creates a new builder for PolicyEvaluationResult. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for PolicyEvaluationResult. */ + public static final class Builder { + private boolean allowed; + private List appliedPolicies; + private double riskScore; + private List requiredActions; + private long processingTimeMs; + private Boolean databaseAccessed; + + public Builder allowed(boolean allowed) { + this.allowed = allowed; + return this; } - /** - * Convenience method to check if database was accessed. - * - * @return true if database was definitely accessed, false otherwise - */ - public boolean wasDatabaseAccessed() { - return Boolean.TRUE.equals(databaseAccessed); + public Builder appliedPolicies(List appliedPolicies) { + this.appliedPolicies = appliedPolicies; + return this; } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PolicyEvaluationResult that = (PolicyEvaluationResult) o; - return allowed == that.allowed && - Double.compare(that.riskScore, riskScore) == 0 && - processingTimeMs == that.processingTimeMs && - Objects.equals(appliedPolicies, that.appliedPolicies) && - Objects.equals(requiredActions, that.requiredActions) && - Objects.equals(databaseAccessed, that.databaseAccessed); + public Builder riskScore(double riskScore) { + this.riskScore = riskScore; + return this; } - @Override - public int hashCode() { - return Objects.hash(allowed, appliedPolicies, riskScore, requiredActions, processingTimeMs, databaseAccessed); + public Builder requiredActions(List requiredActions) { + this.requiredActions = requiredActions; + return this; } - @Override - public String toString() { - return "PolicyEvaluationResult{" + - "allowed=" + allowed + - ", appliedPolicies=" + appliedPolicies + - ", riskScore=" + riskScore + - ", requiredActions=" + requiredActions + - ", processingTimeMs=" + processingTimeMs + - ", databaseAccessed=" + databaseAccessed + - '}'; + public Builder processingTimeMs(long processingTimeMs) { + this.processingTimeMs = processingTimeMs; + return this; } - /** - * Creates a new builder for PolicyEvaluationResult. - * - * @return a new builder instance - */ - public static Builder builder() { - return new Builder(); + public Builder databaseAccessed(Boolean databaseAccessed) { + this.databaseAccessed = databaseAccessed; + return this; } - /** - * Builder for PolicyEvaluationResult. - */ - public static final class Builder { - private boolean allowed; - private List appliedPolicies; - private double riskScore; - private List requiredActions; - private long processingTimeMs; - private Boolean databaseAccessed; - - public Builder allowed(boolean allowed) { - this.allowed = allowed; - return this; - } - - public Builder appliedPolicies(List appliedPolicies) { - this.appliedPolicies = appliedPolicies; - return this; - } - - public Builder riskScore(double riskScore) { - this.riskScore = riskScore; - return this; - } - - public Builder requiredActions(List requiredActions) { - this.requiredActions = requiredActions; - return this; - } - - public Builder processingTimeMs(long processingTimeMs) { - this.processingTimeMs = processingTimeMs; - return this; - } - - public Builder databaseAccessed(Boolean databaseAccessed) { - this.databaseAccessed = databaseAccessed; - return this; - } - - public PolicyEvaluationResult build() { - return new PolicyEvaluationResult(allowed, appliedPolicies, riskScore, requiredActions, processingTimeMs, databaseAccessed); - } + public PolicyEvaluationResult build() { + return new PolicyEvaluationResult( + allowed, appliedPolicies, riskScore, requiredActions, processingTimeMs, databaseAccessed); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyMatch.java b/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyMatch.java index eba5859..267a5e8 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyMatch.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/PolicyMatch.java @@ -18,169 +18,174 @@ import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; - import java.util.Objects; /** * Represents a policy that was matched during workflow step gate evaluation. * - *

Contains information about which policy matched, the action taken, - * and the reason for the match. + *

Contains information about which policy matched, the action taken, and the reason for the + * match. * * @since 2.3.0 */ @JsonIgnoreProperties(ignoreUnknown = true) public final class PolicyMatch { - @JsonProperty("policy_id") - private final String policyId; - - @JsonProperty("policy_name") - private final String policyName; - - @JsonProperty("action") - private final String action; - - @JsonProperty("reason") - private final String reason; - - @JsonCreator - public PolicyMatch( - @JsonProperty("policy_id") String policyId, - @JsonProperty("policy_name") String policyName, - @JsonProperty("action") String action, - @JsonProperty("reason") String reason) { - this.policyId = policyId; - this.policyName = policyName; - this.action = action; - this.reason = reason; - } - - /** - * Returns the unique identifier of the matched policy. - * - * @return the policy ID - */ - public String getPolicyId() { - return policyId; - } - - /** - * Returns the human-readable name of the matched policy. - * - * @return the policy name - */ - public String getPolicyName() { - return policyName; - } - - /** - * Returns the action taken as a result of this policy match. - * - *

Common actions include "allow", "block", "require_approval", "redact". - * - * @return the action taken - */ - public String getAction() { - return action; - } - - /** - * Returns the reason why this policy was matched. - * - *

Provides context about what triggered the policy match, - * useful for debugging and audit purposes. - * - * @return the reason for the match - */ - public String getReason() { - return reason; - } - - /** - * Checks if this policy match resulted in a blocking action. - * - * @return true if the action is "block" - */ - public boolean isBlocking() { - return "block".equalsIgnoreCase(action); - } - - /** - * Checks if this policy match requires approval. - * - * @return true if the action is "require_approval" - */ - public boolean requiresApproval() { - return "require_approval".equalsIgnoreCase(action); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PolicyMatch that = (PolicyMatch) o; - return Objects.equals(policyId, that.policyId) && - Objects.equals(policyName, that.policyName) && - Objects.equals(action, that.action) && - Objects.equals(reason, that.reason); + @JsonProperty("policy_id") + private final String policyId; + + @JsonProperty("policy_name") + private final String policyName; + + @JsonProperty("action") + private final String action; + + @JsonProperty("reason") + private final String reason; + + @JsonCreator + public PolicyMatch( + @JsonProperty("policy_id") String policyId, + @JsonProperty("policy_name") String policyName, + @JsonProperty("action") String action, + @JsonProperty("reason") String reason) { + this.policyId = policyId; + this.policyName = policyName; + this.action = action; + this.reason = reason; + } + + /** + * Returns the unique identifier of the matched policy. + * + * @return the policy ID + */ + public String getPolicyId() { + return policyId; + } + + /** + * Returns the human-readable name of the matched policy. + * + * @return the policy name + */ + public String getPolicyName() { + return policyName; + } + + /** + * Returns the action taken as a result of this policy match. + * + *

Common actions include "allow", "block", "require_approval", "redact". + * + * @return the action taken + */ + public String getAction() { + return action; + } + + /** + * Returns the reason why this policy was matched. + * + *

Provides context about what triggered the policy match, useful for debugging and audit + * purposes. + * + * @return the reason for the match + */ + public String getReason() { + return reason; + } + + /** + * Checks if this policy match resulted in a blocking action. + * + * @return true if the action is "block" + */ + public boolean isBlocking() { + return "block".equalsIgnoreCase(action); + } + + /** + * Checks if this policy match requires approval. + * + * @return true if the action is "require_approval" + */ + public boolean requiresApproval() { + return "require_approval".equalsIgnoreCase(action); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PolicyMatch that = (PolicyMatch) o; + return Objects.equals(policyId, that.policyId) + && Objects.equals(policyName, that.policyName) + && Objects.equals(action, that.action) + && Objects.equals(reason, that.reason); + } + + @Override + public int hashCode() { + return Objects.hash(policyId, policyName, action, reason); + } + + @Override + public String toString() { + return "PolicyMatch{" + + "policyId='" + + policyId + + '\'' + + ", policyName='" + + policyName + + '\'' + + ", action='" + + action + + '\'' + + ", reason='" + + reason + + '\'' + + '}'; + } + + /** + * Creates a new builder for PolicyMatch. + * + * @return a new builder instance + */ + public static Builder builder() { + return new Builder(); + } + + /** Builder for PolicyMatch. */ + public static final class Builder { + private String policyId; + private String policyName; + private String action; + private String reason; + + public Builder policyId(String policyId) { + this.policyId = policyId; + return this; } - @Override - public int hashCode() { - return Objects.hash(policyId, policyName, action, reason); + public Builder policyName(String policyName) { + this.policyName = policyName; + return this; } - @Override - public String toString() { - return "PolicyMatch{" + - "policyId='" + policyId + '\'' + - ", policyName='" + policyName + '\'' + - ", action='" + action + '\'' + - ", reason='" + reason + '\'' + - '}'; + public Builder action(String action) { + this.action = action; + return this; } - /** - * Creates a new builder for PolicyMatch. - * - * @return a new builder instance - */ - public static Builder builder() { - return new Builder(); + public Builder reason(String reason) { + this.reason = reason; + return this; } - /** - * Builder for PolicyMatch. - */ - public static final class Builder { - private String policyId; - private String policyName; - private String action; - private String reason; - - public Builder policyId(String policyId) { - this.policyId = policyId; - return this; - } - - public Builder policyName(String policyName) { - this.policyName = policyName; - return this; - } - - public Builder action(String action) { - this.action = action; - return this; - } - - public Builder reason(String reason) { - this.reason = reason; - return this; - } - - public PolicyMatch build() { - return new PolicyMatch(policyId, policyName, action, reason); - } + public PolicyMatch build() { + return new PolicyMatch(policyId, policyName, action, reason); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java index 35c6860..460305f 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/WorkflowTypes.java @@ -20,7 +20,6 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.annotation.JsonValue; - import java.time.Instant; import java.util.Collections; import java.util.HashMap; @@ -31,1372 +30,1398 @@ /** * Workflow Control Plane types for AxonFlow SDK. * - *

The Workflow Control Plane provides governance gates for external orchestrators - * like LangChain, LangGraph, and CrewAI. + *

The Workflow Control Plane provides governance gates for external orchestrators like + * LangChain, LangGraph, and CrewAI. * *

"LangChain runs the workflow. AxonFlow decides when it's allowed to move forward." */ public final class WorkflowTypes { - private WorkflowTypes() { - // Utility class + private WorkflowTypes() { + // Utility class + } + + /** Workflow status values. */ + public enum WorkflowStatus { + @JsonProperty("in_progress") + IN_PROGRESS("in_progress"), + @JsonProperty("completed") + COMPLETED("completed"), + @JsonProperty("aborted") + ABORTED("aborted"), + @JsonProperty("failed") + FAILED("failed"); + + private final String value; + + WorkflowStatus(String value) { + this.value = value; } - /** - * Workflow status values. - */ - public enum WorkflowStatus { - @JsonProperty("in_progress") - IN_PROGRESS("in_progress"), - @JsonProperty("completed") - COMPLETED("completed"), - @JsonProperty("aborted") - ABORTED("aborted"), - @JsonProperty("failed") - FAILED("failed"); - - private final String value; - - WorkflowStatus(String value) { - this.value = value; - } + @JsonValue + public String getValue() { + return value; + } - @JsonValue - public String getValue() { - return value; + @JsonCreator + public static WorkflowStatus fromValue(String value) { + for (WorkflowStatus status : values()) { + if (status.value.equals(value)) { + return status; } + } + throw new IllegalArgumentException("Unknown workflow status: " + value); + } + } + + /** Source of the workflow (which orchestrator is running it). */ + public enum WorkflowSource { + @JsonProperty("langgraph") + LANGGRAPH("langgraph"), + @JsonProperty("langchain") + LANGCHAIN("langchain"), + @JsonProperty("crewai") + CREWAI("crewai"), + @JsonProperty("external") + EXTERNAL("external"); + + private final String value; + + WorkflowSource(String value) { + this.value = value; + } - @JsonCreator - public static WorkflowStatus fromValue(String value) { - for (WorkflowStatus status : values()) { - if (status.value.equals(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown workflow status: " + value); - } + @JsonValue + public String getValue() { + return value; } - /** - * Source of the workflow (which orchestrator is running it). - */ - public enum WorkflowSource { - @JsonProperty("langgraph") - LANGGRAPH("langgraph"), - @JsonProperty("langchain") - LANGCHAIN("langchain"), - @JsonProperty("crewai") - CREWAI("crewai"), - @JsonProperty("external") - EXTERNAL("external"); - - private final String value; - - WorkflowSource(String value) { - this.value = value; + @JsonCreator + public static WorkflowSource fromValue(String value) { + for (WorkflowSource source : values()) { + if (source.value.equals(value)) { + return source; } + } + throw new IllegalArgumentException("Unknown workflow source: " + value); + } + } - @JsonValue - public String getValue() { - return value; - } + /** Gate decision values returned by step gate checks. */ + public enum GateDecision { + @JsonProperty("allow") + ALLOW("allow"), + @JsonProperty("block") + BLOCK("block"), + @JsonProperty("require_approval") + REQUIRE_APPROVAL("require_approval"); - @JsonCreator - public static WorkflowSource fromValue(String value) { - for (WorkflowSource source : values()) { - if (source.value.equals(value)) { - return source; - } - } - throw new IllegalArgumentException("Unknown workflow source: " + value); - } - } + private final String value; - /** - * Gate decision values returned by step gate checks. - */ - public enum GateDecision { - @JsonProperty("allow") - ALLOW("allow"), - @JsonProperty("block") - BLOCK("block"), - @JsonProperty("require_approval") - REQUIRE_APPROVAL("require_approval"); - - private final String value; - - GateDecision(String value) { - this.value = value; - } + GateDecision(String value) { + this.value = value; + } - @JsonValue - public String getValue() { - return value; - } + @JsonValue + public String getValue() { + return value; + } - @JsonCreator - public static GateDecision fromValue(String value) { - for (GateDecision decision : values()) { - if (decision.value.equals(value)) { - return decision; - } - } - throw new IllegalArgumentException("Unknown gate decision: " + value); + @JsonCreator + public static GateDecision fromValue(String value) { + for (GateDecision decision : values()) { + if (decision.value.equals(value)) { + return decision; } + } + throw new IllegalArgumentException("Unknown gate decision: " + value); } + } - /** - * Approval status for steps requiring human approval. - */ - public enum ApprovalStatus { - @JsonProperty("pending") - PENDING("pending"), - @JsonProperty("approved") - APPROVED("approved"), - @JsonProperty("rejected") - REJECTED("rejected"); - - private final String value; - - ApprovalStatus(String value) { - this.value = value; - } + /** Approval status for steps requiring human approval. */ + public enum ApprovalStatus { + @JsonProperty("pending") + PENDING("pending"), + @JsonProperty("approved") + APPROVED("approved"), + @JsonProperty("rejected") + REJECTED("rejected"); - @JsonValue - public String getValue() { - return value; - } + private final String value; - @JsonCreator - public static ApprovalStatus fromValue(String value) { - for (ApprovalStatus status : values()) { - if (status.value.equals(value)) { - return status; - } - } - throw new IllegalArgumentException("Unknown approval status: " + value); - } + ApprovalStatus(String value) { + this.value = value; } - /** - * Step type indicating what kind of operation the step performs. - */ - public enum StepType { - @JsonProperty("llm_call") - LLM_CALL("llm_call"), - @JsonProperty("tool_call") - TOOL_CALL("tool_call"), - @JsonProperty("connector_call") - CONNECTOR_CALL("connector_call"), - @JsonProperty("human_task") - HUMAN_TASK("human_task"); - - private final String value; - - StepType(String value) { - this.value = value; - } + @JsonValue + public String getValue() { + return value; + } - @JsonValue - public String getValue() { - return value; + @JsonCreator + public static ApprovalStatus fromValue(String value) { + for (ApprovalStatus status : values()) { + if (status.value.equals(value)) { + return status; } + } + throw new IllegalArgumentException("Unknown approval status: " + value); + } + } + + /** Step type indicating what kind of operation the step performs. */ + public enum StepType { + @JsonProperty("llm_call") + LLM_CALL("llm_call"), + @JsonProperty("tool_call") + TOOL_CALL("tool_call"), + @JsonProperty("connector_call") + CONNECTOR_CALL("connector_call"), + @JsonProperty("human_task") + HUMAN_TASK("human_task"); + + private final String value; + + StepType(String value) { + this.value = value; + } - @JsonCreator - public static StepType fromValue(String value) { - for (StepType type : values()) { - if (type.value.equals(value)) { - return type; - } - } - throw new IllegalArgumentException("Unknown step type: " + value); - } + @JsonValue + public String getValue() { + return value; } - /** - * Request to create a new workflow. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class CreateWorkflowRequest { + @JsonCreator + public static StepType fromValue(String value) { + for (StepType type : values()) { + if (type.value.equals(value)) { + return type; + } + } + throw new IllegalArgumentException("Unknown step type: " + value); + } + } - @JsonProperty("workflow_name") - private final String workflowName; + /** Request to create a new workflow. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class CreateWorkflowRequest { - @JsonProperty("source") - private final WorkflowSource source; + @JsonProperty("workflow_name") + private final String workflowName; - @JsonProperty("metadata") - private final Map metadata; + @JsonProperty("source") + private final WorkflowSource source; - @JsonProperty("trace_id") - private final String traceId; + @JsonProperty("metadata") + private final Map metadata; - /** - * Backward-compatible constructor without traceId. - */ - public CreateWorkflowRequest(String workflowName, WorkflowSource source, - Map metadata) { - this(workflowName, source, metadata, null); - } + @JsonProperty("trace_id") + private final String traceId; - @JsonCreator - public CreateWorkflowRequest( - @JsonProperty("workflow_name") String workflowName, - @JsonProperty("source") WorkflowSource source, - @JsonProperty("metadata") Map metadata, - @JsonProperty("trace_id") String traceId) { - this.workflowName = Objects.requireNonNull(workflowName, "workflowName is required"); - this.source = source != null ? source : WorkflowSource.EXTERNAL; - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.traceId = traceId; - } + /** Backward-compatible constructor without traceId. */ + public CreateWorkflowRequest( + String workflowName, WorkflowSource source, Map metadata) { + this(workflowName, source, metadata, null); + } - public String getWorkflowName() { - return workflowName; - } + @JsonCreator + public CreateWorkflowRequest( + @JsonProperty("workflow_name") String workflowName, + @JsonProperty("source") WorkflowSource source, + @JsonProperty("metadata") Map metadata, + @JsonProperty("trace_id") String traceId) { + this.workflowName = Objects.requireNonNull(workflowName, "workflowName is required"); + this.source = source != null ? source : WorkflowSource.EXTERNAL; + this.metadata = + metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.traceId = traceId; + } - public WorkflowSource getSource() { - return source; - } + public String getWorkflowName() { + return workflowName; + } - public Map getMetadata() { - return metadata; - } + public WorkflowSource getSource() { + return source; + } - public String getTraceId() { - return traceId; - } + public Map getMetadata() { + return metadata; + } - public static Builder builder() { - return new Builder(); - } + public String getTraceId() { + return traceId; + } - public static final class Builder { - private String workflowName; - private WorkflowSource source = WorkflowSource.EXTERNAL; - private Map metadata; - private String traceId; - - public Builder workflowName(String workflowName) { - this.workflowName = workflowName; - return this; - } - - public Builder source(WorkflowSource source) { - this.source = source; - return this; - } - - public Builder metadata(Map metadata) { - this.metadata = metadata; - return this; - } - - public Builder traceId(String traceId) { - this.traceId = traceId; - return this; - } - - public CreateWorkflowRequest build() { - return new CreateWorkflowRequest(workflowName, source, metadata, traceId); - } - } + public static Builder builder() { + return new Builder(); } - /** - * Response from creating a workflow. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class CreateWorkflowResponse { + public static final class Builder { + private String workflowName; + private WorkflowSource source = WorkflowSource.EXTERNAL; + private Map metadata; + private String traceId; + + public Builder workflowName(String workflowName) { + this.workflowName = workflowName; + return this; + } + + public Builder source(WorkflowSource source) { + this.source = source; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public Builder traceId(String traceId) { + this.traceId = traceId; + return this; + } + + public CreateWorkflowRequest build() { + return new CreateWorkflowRequest(workflowName, source, metadata, traceId); + } + } + } - @JsonProperty("workflow_id") - private final String workflowId; + /** Response from creating a workflow. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class CreateWorkflowResponse { - @JsonProperty("workflow_name") - private final String workflowName; + @JsonProperty("workflow_id") + private final String workflowId; - @JsonProperty("source") - private final WorkflowSource source; + @JsonProperty("workflow_name") + private final String workflowName; - @JsonProperty("status") - private final WorkflowStatus status; + @JsonProperty("source") + private final WorkflowSource source; - @JsonProperty("created_at") - private final Instant createdAt; + @JsonProperty("status") + private final WorkflowStatus status; - @JsonProperty("trace_id") - private final String traceId; + @JsonProperty("created_at") + private final Instant createdAt; - /** - * Backward-compatible constructor without traceId. - */ - public CreateWorkflowResponse(String workflowId, String workflowName, - WorkflowSource source, WorkflowStatus status, - Instant createdAt) { - this(workflowId, workflowName, source, status, createdAt, null); - } + @JsonProperty("trace_id") + private final String traceId; - @JsonCreator - public CreateWorkflowResponse( - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("workflow_name") String workflowName, - @JsonProperty("source") WorkflowSource source, - @JsonProperty("status") WorkflowStatus status, - @JsonProperty("created_at") Instant createdAt, - @JsonProperty("trace_id") String traceId) { - this.workflowId = workflowId; - this.workflowName = workflowName; - this.source = source; - this.status = status; - this.createdAt = createdAt; - this.traceId = traceId; - } + /** Backward-compatible constructor without traceId. */ + public CreateWorkflowResponse( + String workflowId, + String workflowName, + WorkflowSource source, + WorkflowStatus status, + Instant createdAt) { + this(workflowId, workflowName, source, status, createdAt, null); + } - public String getWorkflowId() { - return workflowId; - } + @JsonCreator + public CreateWorkflowResponse( + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("workflow_name") String workflowName, + @JsonProperty("source") WorkflowSource source, + @JsonProperty("status") WorkflowStatus status, + @JsonProperty("created_at") Instant createdAt, + @JsonProperty("trace_id") String traceId) { + this.workflowId = workflowId; + this.workflowName = workflowName; + this.source = source; + this.status = status; + this.createdAt = createdAt; + this.traceId = traceId; + } - public String getWorkflowName() { - return workflowName; - } + public String getWorkflowId() { + return workflowId; + } - public WorkflowSource getSource() { - return source; - } + public String getWorkflowName() { + return workflowName; + } - public WorkflowStatus getStatus() { - return status; - } + public WorkflowSource getSource() { + return source; + } - public Instant getCreatedAt() { - return createdAt; - } + public WorkflowStatus getStatus() { + return status; + } - public String getTraceId() { - return traceId; - } + public Instant getCreatedAt() { + return createdAt; } - /** - * Tool-level context for per-tool governance within tool_call steps. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class ToolContext { + public String getTraceId() { + return traceId; + } + } - @JsonProperty("tool_name") - private final String toolName; + /** Tool-level context for per-tool governance within tool_call steps. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ToolContext { - @JsonProperty("tool_type") - private final String toolType; + @JsonProperty("tool_name") + private final String toolName; - @JsonProperty("tool_input") - private final Map toolInput; + @JsonProperty("tool_type") + private final String toolType; - private ToolContext(Builder builder) { - this.toolName = builder.toolName; - this.toolType = builder.toolType; - this.toolInput = builder.toolInput != null ? Collections.unmodifiableMap(new HashMap<>(builder.toolInput)) : null; - } + @JsonProperty("tool_input") + private final Map toolInput; - @JsonCreator - public ToolContext( - @JsonProperty("tool_name") String toolName, - @JsonProperty("tool_type") String toolType, - @JsonProperty("tool_input") Map toolInput) { - this.toolName = toolName; - this.toolType = toolType; - this.toolInput = toolInput != null ? Collections.unmodifiableMap(new HashMap<>(toolInput)) : null; - } + private ToolContext(Builder builder) { + this.toolName = builder.toolName; + this.toolType = builder.toolType; + this.toolInput = + builder.toolInput != null + ? Collections.unmodifiableMap(new HashMap<>(builder.toolInput)) + : null; + } - public String getToolName() { return toolName; } - public String getToolType() { return toolType; } - public Map getToolInput() { return toolInput; } + @JsonCreator + public ToolContext( + @JsonProperty("tool_name") String toolName, + @JsonProperty("tool_type") String toolType, + @JsonProperty("tool_input") Map toolInput) { + this.toolName = toolName; + this.toolType = toolType; + this.toolInput = + toolInput != null ? Collections.unmodifiableMap(new HashMap<>(toolInput)) : null; + } - public static Builder builder(String toolName) { - return new Builder(toolName); - } + public String getToolName() { + return toolName; + } - public static final class Builder { - private final String toolName; - private String toolType; - private Map toolInput; + public String getToolType() { + return toolType; + } - public Builder(String toolName) { - this.toolName = Objects.requireNonNull(toolName, "toolName must not be null"); - } + public Map getToolInput() { + return toolInput; + } - public Builder toolType(String toolType) { this.toolType = toolType; return this; } - public Builder toolInput(Map toolInput) { this.toolInput = toolInput; return this; } - public ToolContext build() { return new ToolContext(this); } - } + public static Builder builder(String toolName) { + return new Builder(toolName); } - /** - * Request to check if a step is allowed to proceed. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class StepGateRequest { + public static final class Builder { + private final String toolName; + private String toolType; + private Map toolInput; - @JsonProperty("step_name") - private final String stepName; + public Builder(String toolName) { + this.toolName = Objects.requireNonNull(toolName, "toolName must not be null"); + } - @JsonProperty("step_type") - private final StepType stepType; + public Builder toolType(String toolType) { + this.toolType = toolType; + return this; + } - @JsonProperty("step_input") - private final Map stepInput; + public Builder toolInput(Map toolInput) { + this.toolInput = toolInput; + return this; + } - @JsonProperty("model") - private final String model; + public ToolContext build() { + return new ToolContext(this); + } + } + } - @JsonProperty("provider") - private final String provider; + /** Request to check if a step is allowed to proceed. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class StepGateRequest { - @JsonProperty("tool_context") - private final ToolContext toolContext; + @JsonProperty("step_name") + private final String stepName; - /** - * Backward-compatible constructor without toolContext. - */ - public StepGateRequest(String stepName, StepType stepType, - Map stepInput, String model, - String provider) { - this(stepName, stepType, stepInput, model, provider, null); - } + @JsonProperty("step_type") + private final StepType stepType; - @JsonCreator - public StepGateRequest( - @JsonProperty("step_name") String stepName, - @JsonProperty("step_type") StepType stepType, - @JsonProperty("step_input") Map stepInput, - @JsonProperty("model") String model, - @JsonProperty("provider") String provider, - @JsonProperty("tool_context") ToolContext toolContext) { - this.stepName = stepName; - this.stepType = Objects.requireNonNull(stepType, "stepType is required"); - this.stepInput = stepInput != null ? Collections.unmodifiableMap(stepInput) : Collections.emptyMap(); - this.model = model; - this.provider = provider; - this.toolContext = toolContext; - } + @JsonProperty("step_input") + private final Map stepInput; - public String getStepName() { - return stepName; - } + @JsonProperty("model") + private final String model; - public StepType getStepType() { - return stepType; - } + @JsonProperty("provider") + private final String provider; - public Map getStepInput() { - return stepInput; - } + @JsonProperty("tool_context") + private final ToolContext toolContext; - public String getModel() { - return model; - } + /** Backward-compatible constructor without toolContext. */ + public StepGateRequest( + String stepName, + StepType stepType, + Map stepInput, + String model, + String provider) { + this(stepName, stepType, stepInput, model, provider, null); + } - public String getProvider() { - return provider; - } + @JsonCreator + public StepGateRequest( + @JsonProperty("step_name") String stepName, + @JsonProperty("step_type") StepType stepType, + @JsonProperty("step_input") Map stepInput, + @JsonProperty("model") String model, + @JsonProperty("provider") String provider, + @JsonProperty("tool_context") ToolContext toolContext) { + this.stepName = stepName; + this.stepType = Objects.requireNonNull(stepType, "stepType is required"); + this.stepInput = + stepInput != null ? Collections.unmodifiableMap(stepInput) : Collections.emptyMap(); + this.model = model; + this.provider = provider; + this.toolContext = toolContext; + } - public ToolContext getToolContext() { - return toolContext; - } + public String getStepName() { + return stepName; + } - public static Builder builder() { - return new Builder(); - } + public StepType getStepType() { + return stepType; + } - public static final class Builder { - private String stepName; - private StepType stepType; - private Map stepInput; - private String model; - private String provider; - private ToolContext toolContext; - - public Builder stepName(String stepName) { - this.stepName = stepName; - return this; - } - - public Builder stepType(StepType stepType) { - this.stepType = stepType; - return this; - } - - public Builder stepInput(Map stepInput) { - this.stepInput = stepInput; - return this; - } - - public Builder model(String model) { - this.model = model; - return this; - } - - public Builder provider(String provider) { - this.provider = provider; - return this; - } - - public Builder toolContext(ToolContext toolContext) { - this.toolContext = toolContext; - return this; - } - - public StepGateRequest build() { - return new StepGateRequest(stepName, stepType, stepInput, model, provider, toolContext); - } - } + public Map getStepInput() { + return stepInput; } - /** - * Response from a step gate check. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class StepGateResponse { - - @JsonProperty("decision") - private final GateDecision decision; - - @JsonProperty("step_id") - private final String stepId; - - @JsonProperty("reason") - private final String reason; - - @JsonProperty("policy_ids") - private final List policyIds; - - @JsonProperty("approval_url") - private final String approvalUrl; - - @JsonProperty("policies_evaluated") - private final List policiesEvaluated; - - @JsonProperty("policies_matched") - private final List policiesMatched; - - @JsonCreator - public StepGateResponse( - @JsonProperty("decision") GateDecision decision, - @JsonProperty("step_id") String stepId, - @JsonProperty("reason") String reason, - @JsonProperty("policy_ids") List policyIds, - @JsonProperty("approval_url") String approvalUrl, - @JsonProperty("policies_evaluated") List policiesEvaluated, - @JsonProperty("policies_matched") List policiesMatched) { - this.decision = decision; - this.stepId = stepId; - this.reason = reason; - this.policyIds = policyIds != null ? Collections.unmodifiableList(policyIds) : Collections.emptyList(); - this.approvalUrl = approvalUrl; - this.policiesEvaluated = policiesEvaluated != null ? Collections.unmodifiableList(policiesEvaluated) : Collections.emptyList(); - this.policiesMatched = policiesMatched != null ? Collections.unmodifiableList(policiesMatched) : Collections.emptyList(); - } + public String getModel() { + return model; + } - public GateDecision getDecision() { - return decision; - } + public String getProvider() { + return provider; + } - public String getStepId() { - return stepId; - } + public ToolContext getToolContext() { + return toolContext; + } - public String getReason() { - return reason; - } + public static Builder builder() { + return new Builder(); + } - public List getPolicyIds() { - return policyIds; - } + public static final class Builder { + private String stepName; + private StepType stepType; + private Map stepInput; + private String model; + private String provider; + private ToolContext toolContext; + + public Builder stepName(String stepName) { + this.stepName = stepName; + return this; + } + + public Builder stepType(StepType stepType) { + this.stepType = stepType; + return this; + } + + public Builder stepInput(Map stepInput) { + this.stepInput = stepInput; + return this; + } + + public Builder model(String model) { + this.model = model; + return this; + } + + public Builder provider(String provider) { + this.provider = provider; + return this; + } + + public Builder toolContext(ToolContext toolContext) { + this.toolContext = toolContext; + return this; + } + + public StepGateRequest build() { + return new StepGateRequest(stepName, stepType, stepInput, model, provider, toolContext); + } + } + } + + /** Response from a step gate check. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class StepGateResponse { + + @JsonProperty("decision") + private final GateDecision decision; + + @JsonProperty("step_id") + private final String stepId; + + @JsonProperty("reason") + private final String reason; + + @JsonProperty("policy_ids") + private final List policyIds; + + @JsonProperty("approval_url") + private final String approvalUrl; + + @JsonProperty("policies_evaluated") + private final List policiesEvaluated; + + @JsonProperty("policies_matched") + private final List policiesMatched; + + @JsonCreator + public StepGateResponse( + @JsonProperty("decision") GateDecision decision, + @JsonProperty("step_id") String stepId, + @JsonProperty("reason") String reason, + @JsonProperty("policy_ids") List policyIds, + @JsonProperty("approval_url") String approvalUrl, + @JsonProperty("policies_evaluated") List policiesEvaluated, + @JsonProperty("policies_matched") List policiesMatched) { + this.decision = decision; + this.stepId = stepId; + this.reason = reason; + this.policyIds = + policyIds != null ? Collections.unmodifiableList(policyIds) : Collections.emptyList(); + this.approvalUrl = approvalUrl; + this.policiesEvaluated = + policiesEvaluated != null + ? Collections.unmodifiableList(policiesEvaluated) + : Collections.emptyList(); + this.policiesMatched = + policiesMatched != null + ? Collections.unmodifiableList(policiesMatched) + : Collections.emptyList(); + } - public String getApprovalUrl() { - return approvalUrl; - } + public GateDecision getDecision() { + return decision; + } - /** - * Returns all policies that were evaluated during the gate check. - * - * @return immutable list of evaluated policies - * @since 2.3.0 - */ - public List getPoliciesEvaluated() { - return policiesEvaluated; - } + public String getStepId() { + return stepId; + } - /** - * Returns policies that matched and influenced the decision. - * - * @return immutable list of matched policies - * @since 2.3.0 - */ - public List getPoliciesMatched() { - return policiesMatched; - } + public String getReason() { + return reason; + } - public boolean isAllowed() { - return decision == GateDecision.ALLOW; - } + public List getPolicyIds() { + return policyIds; + } - public boolean isBlocked() { - return decision == GateDecision.BLOCK; - } + public String getApprovalUrl() { + return approvalUrl; + } - public boolean requiresApproval() { - return decision == GateDecision.REQUIRE_APPROVAL; - } + /** + * Returns all policies that were evaluated during the gate check. + * + * @return immutable list of evaluated policies + * @since 2.3.0 + */ + public List getPoliciesEvaluated() { + return policiesEvaluated; } /** - * Information about a workflow step. + * Returns policies that matched and influenced the decision. + * + * @return immutable list of matched policies + * @since 2.3.0 */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class WorkflowStepInfo { - - @JsonProperty("step_id") - private final String stepId; - - @JsonProperty("step_index") - private final int stepIndex; - - @JsonProperty("step_name") - private final String stepName; - - @JsonProperty("step_type") - private final StepType stepType; - - @JsonProperty("decision") - private final GateDecision decision; - - @JsonProperty("decision_reason") - private final String decisionReason; - - @JsonProperty("approval_status") - private final ApprovalStatus approvalStatus; - - @JsonProperty("approved_by") - private final String approvedBy; - - @JsonProperty("gate_checked_at") - private final Instant gateCheckedAt; - - @JsonProperty("completed_at") - private final Instant completedAt; - - @JsonCreator - public WorkflowStepInfo( - @JsonProperty("step_id") String stepId, - @JsonProperty("step_index") int stepIndex, - @JsonProperty("step_name") String stepName, - @JsonProperty("step_type") StepType stepType, - @JsonProperty("decision") GateDecision decision, - @JsonProperty("decision_reason") String decisionReason, - @JsonProperty("approval_status") ApprovalStatus approvalStatus, - @JsonProperty("approved_by") String approvedBy, - @JsonProperty("gate_checked_at") Instant gateCheckedAt, - @JsonProperty("completed_at") Instant completedAt) { - this.stepId = stepId; - this.stepIndex = stepIndex; - this.stepName = stepName; - this.stepType = stepType; - this.decision = decision; - this.decisionReason = decisionReason; - this.approvalStatus = approvalStatus; - this.approvedBy = approvedBy; - this.gateCheckedAt = gateCheckedAt; - this.completedAt = completedAt; - } + public List getPoliciesMatched() { + return policiesMatched; + } - public String getStepId() { - return stepId; - } + public boolean isAllowed() { + return decision == GateDecision.ALLOW; + } - public int getStepIndex() { - return stepIndex; - } + public boolean isBlocked() { + return decision == GateDecision.BLOCK; + } - public String getStepName() { - return stepName; - } + public boolean requiresApproval() { + return decision == GateDecision.REQUIRE_APPROVAL; + } + } + + /** Information about a workflow step. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class WorkflowStepInfo { + + @JsonProperty("step_id") + private final String stepId; + + @JsonProperty("step_index") + private final int stepIndex; + + @JsonProperty("step_name") + private final String stepName; + + @JsonProperty("step_type") + private final StepType stepType; + + @JsonProperty("decision") + private final GateDecision decision; + + @JsonProperty("decision_reason") + private final String decisionReason; + + @JsonProperty("approval_status") + private final ApprovalStatus approvalStatus; + + @JsonProperty("approved_by") + private final String approvedBy; + + @JsonProperty("gate_checked_at") + private final Instant gateCheckedAt; + + @JsonProperty("completed_at") + private final Instant completedAt; + + @JsonCreator + public WorkflowStepInfo( + @JsonProperty("step_id") String stepId, + @JsonProperty("step_index") int stepIndex, + @JsonProperty("step_name") String stepName, + @JsonProperty("step_type") StepType stepType, + @JsonProperty("decision") GateDecision decision, + @JsonProperty("decision_reason") String decisionReason, + @JsonProperty("approval_status") ApprovalStatus approvalStatus, + @JsonProperty("approved_by") String approvedBy, + @JsonProperty("gate_checked_at") Instant gateCheckedAt, + @JsonProperty("completed_at") Instant completedAt) { + this.stepId = stepId; + this.stepIndex = stepIndex; + this.stepName = stepName; + this.stepType = stepType; + this.decision = decision; + this.decisionReason = decisionReason; + this.approvalStatus = approvalStatus; + this.approvedBy = approvedBy; + this.gateCheckedAt = gateCheckedAt; + this.completedAt = completedAt; + } - public StepType getStepType() { - return stepType; - } + public String getStepId() { + return stepId; + } - public GateDecision getDecision() { - return decision; - } + public int getStepIndex() { + return stepIndex; + } - public String getDecisionReason() { - return decisionReason; - } + public String getStepName() { + return stepName; + } - public ApprovalStatus getApprovalStatus() { - return approvalStatus; - } + public StepType getStepType() { + return stepType; + } - public String getApprovedBy() { - return approvedBy; - } + public GateDecision getDecision() { + return decision; + } - public Instant getGateCheckedAt() { - return gateCheckedAt; - } + public String getDecisionReason() { + return decisionReason; + } - public Instant getCompletedAt() { - return completedAt; - } + public ApprovalStatus getApprovalStatus() { + return approvalStatus; } - /** - * Response containing workflow status. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class WorkflowStatusResponse { + public String getApprovedBy() { + return approvedBy; + } - @JsonProperty("workflow_id") - private final String workflowId; + public Instant getGateCheckedAt() { + return gateCheckedAt; + } - @JsonProperty("workflow_name") - private final String workflowName; + public Instant getCompletedAt() { + return completedAt; + } + } + + /** Response containing workflow status. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class WorkflowStatusResponse { + + @JsonProperty("workflow_id") + private final String workflowId; + + @JsonProperty("workflow_name") + private final String workflowName; + + @JsonProperty("source") + private final WorkflowSource source; + + @JsonProperty("status") + private final WorkflowStatus status; + + @JsonProperty("current_step_index") + private final int currentStepIndex; + + @JsonProperty("total_steps") + private final Integer totalSteps; + + @JsonProperty("started_at") + private final Instant startedAt; + + @JsonProperty("completed_at") + private final Instant completedAt; + + @JsonProperty("steps") + private final List steps; + + @JsonProperty("trace_id") + private final String traceId; + + /** Backward-compatible constructor without traceId. */ + public WorkflowStatusResponse( + String workflowId, + String workflowName, + WorkflowSource source, + WorkflowStatus status, + int currentStepIndex, + Integer totalSteps, + Instant startedAt, + Instant completedAt, + List steps) { + this( + workflowId, + workflowName, + source, + status, + currentStepIndex, + totalSteps, + startedAt, + completedAt, + steps, + null); + } - @JsonProperty("source") - private final WorkflowSource source; + @JsonCreator + public WorkflowStatusResponse( + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("workflow_name") String workflowName, + @JsonProperty("source") WorkflowSource source, + @JsonProperty("status") WorkflowStatus status, + @JsonProperty("current_step_index") int currentStepIndex, + @JsonProperty("total_steps") Integer totalSteps, + @JsonProperty("started_at") Instant startedAt, + @JsonProperty("completed_at") Instant completedAt, + @JsonProperty("steps") List steps, + @JsonProperty("trace_id") String traceId) { + this.workflowId = workflowId; + this.workflowName = workflowName; + this.source = source; + this.status = status; + this.currentStepIndex = currentStepIndex; + this.totalSteps = totalSteps; + this.startedAt = startedAt; + this.completedAt = completedAt; + this.steps = steps != null ? Collections.unmodifiableList(steps) : Collections.emptyList(); + this.traceId = traceId; + } - @JsonProperty("status") - private final WorkflowStatus status; + public String getWorkflowId() { + return workflowId; + } - @JsonProperty("current_step_index") - private final int currentStepIndex; + public String getWorkflowName() { + return workflowName; + } - @JsonProperty("total_steps") - private final Integer totalSteps; + public WorkflowSource getSource() { + return source; + } - @JsonProperty("started_at") - private final Instant startedAt; + public WorkflowStatus getStatus() { + return status; + } - @JsonProperty("completed_at") - private final Instant completedAt; + public int getCurrentStepIndex() { + return currentStepIndex; + } - @JsonProperty("steps") - private final List steps; + public Integer getTotalSteps() { + return totalSteps; + } - @JsonProperty("trace_id") - private final String traceId; + public Instant getStartedAt() { + return startedAt; + } - /** - * Backward-compatible constructor without traceId. - */ - public WorkflowStatusResponse(String workflowId, String workflowName, - WorkflowSource source, WorkflowStatus status, - int currentStepIndex, Integer totalSteps, - Instant startedAt, Instant completedAt, - List steps) { - this(workflowId, workflowName, source, status, currentStepIndex, - totalSteps, startedAt, completedAt, steps, null); - } + public Instant getCompletedAt() { + return completedAt; + } - @JsonCreator - public WorkflowStatusResponse( - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("workflow_name") String workflowName, - @JsonProperty("source") WorkflowSource source, - @JsonProperty("status") WorkflowStatus status, - @JsonProperty("current_step_index") int currentStepIndex, - @JsonProperty("total_steps") Integer totalSteps, - @JsonProperty("started_at") Instant startedAt, - @JsonProperty("completed_at") Instant completedAt, - @JsonProperty("steps") List steps, - @JsonProperty("trace_id") String traceId) { - this.workflowId = workflowId; - this.workflowName = workflowName; - this.source = source; - this.status = status; - this.currentStepIndex = currentStepIndex; - this.totalSteps = totalSteps; - this.startedAt = startedAt; - this.completedAt = completedAt; - this.steps = steps != null ? Collections.unmodifiableList(steps) : Collections.emptyList(); - this.traceId = traceId; - } + public List getSteps() { + return steps; + } - public String getWorkflowId() { - return workflowId; - } + public String getTraceId() { + return traceId; + } - public String getWorkflowName() { - return workflowName; - } + public boolean isTerminal() { + return status == WorkflowStatus.COMPLETED + || status == WorkflowStatus.ABORTED + || status == WorkflowStatus.FAILED; + } + } - public WorkflowSource getSource() { - return source; - } + /** Options for listing workflows. */ + public static final class ListWorkflowsOptions { - public WorkflowStatus getStatus() { - return status; - } + private final WorkflowStatus status; + private final WorkflowSource source; + private final int limit; + private final int offset; + private final String traceId; - public int getCurrentStepIndex() { - return currentStepIndex; - } + /** Backward-compatible constructor without traceId. */ + public ListWorkflowsOptions( + WorkflowStatus status, WorkflowSource source, int limit, int offset) { + this(status, source, limit, offset, null); + } - public Integer getTotalSteps() { - return totalSteps; - } + public ListWorkflowsOptions( + WorkflowStatus status, WorkflowSource source, int limit, int offset, String traceId) { + this.status = status; + this.source = source; + this.limit = limit > 0 ? limit : 50; + this.offset = Math.max(offset, 0); + this.traceId = traceId; + } - public Instant getStartedAt() { - return startedAt; - } + public WorkflowStatus getStatus() { + return status; + } - public Instant getCompletedAt() { - return completedAt; - } + public WorkflowSource getSource() { + return source; + } - public List getSteps() { - return steps; - } + public int getLimit() { + return limit; + } - public String getTraceId() { - return traceId; - } + public int getOffset() { + return offset; + } - public boolean isTerminal() { - return status == WorkflowStatus.COMPLETED || - status == WorkflowStatus.ABORTED || - status == WorkflowStatus.FAILED; - } + public String getTraceId() { + return traceId; } - /** - * Options for listing workflows. - */ - public static final class ListWorkflowsOptions { - - private final WorkflowStatus status; - private final WorkflowSource source; - private final int limit; - private final int offset; - private final String traceId; - - /** - * Backward-compatible constructor without traceId. - */ - public ListWorkflowsOptions(WorkflowStatus status, WorkflowSource source, int limit, int offset) { - this(status, source, limit, offset, null); - } + public static Builder builder() { + return new Builder(); + } - public ListWorkflowsOptions(WorkflowStatus status, WorkflowSource source, int limit, int offset, String traceId) { - this.status = status; - this.source = source; - this.limit = limit > 0 ? limit : 50; - this.offset = Math.max(offset, 0); - this.traceId = traceId; - } + public static final class Builder { + private WorkflowStatus status; + private WorkflowSource source; + private int limit = 50; + private int offset = 0; + private String traceId; + + public Builder status(WorkflowStatus status) { + this.status = status; + return this; + } + + public Builder source(WorkflowSource source) { + this.source = source; + return this; + } + + public Builder limit(int limit) { + this.limit = limit; + return this; + } + + public Builder offset(int offset) { + this.offset = offset; + return this; + } + + public Builder traceId(String traceId) { + this.traceId = traceId; + return this; + } + + public ListWorkflowsOptions build() { + return new ListWorkflowsOptions(status, source, limit, offset, traceId); + } + } + } - public WorkflowStatus getStatus() { - return status; - } + /** Response from listing workflows. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ListWorkflowsResponse { - public WorkflowSource getSource() { - return source; - } + @JsonProperty("workflows") + private final List workflows; - public int getLimit() { - return limit; - } + @JsonProperty("total") + private final int total; - public int getOffset() { - return offset; - } + @JsonCreator + public ListWorkflowsResponse( + @JsonProperty("workflows") List workflows, + @JsonProperty("total") int total) { + this.workflows = + workflows != null ? Collections.unmodifiableList(workflows) : Collections.emptyList(); + this.total = total; + } - public String getTraceId() { - return traceId; - } + public List getWorkflows() { + return workflows; + } - public static Builder builder() { - return new Builder(); - } + public int getTotal() { + return total; + } + } + + /** Request to mark a step as completed. */ + @JsonIgnoreProperties(ignoreUnknown = true) + @JsonInclude(JsonInclude.Include.NON_NULL) + public static final class MarkStepCompletedRequest { + + @JsonProperty("output") + private final Map output; + + @JsonProperty("metadata") + private final Map metadata; + + @JsonProperty("tokens_in") + private final Integer tokensIn; + + @JsonProperty("tokens_out") + private final Integer tokensOut; + + @JsonProperty("cost_usd") + private final Double costUsd; + + @JsonCreator + public MarkStepCompletedRequest( + @JsonProperty("output") Map output, + @JsonProperty("metadata") Map metadata, + @JsonProperty("tokens_in") Integer tokensIn, + @JsonProperty("tokens_out") Integer tokensOut, + @JsonProperty("cost_usd") Double costUsd) { + this.output = output != null ? Collections.unmodifiableMap(output) : Collections.emptyMap(); + this.metadata = + metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); + this.tokensIn = tokensIn; + this.tokensOut = tokensOut; + this.costUsd = costUsd; + } - public static final class Builder { - private WorkflowStatus status; - private WorkflowSource source; - private int limit = 50; - private int offset = 0; - private String traceId; - - public Builder status(WorkflowStatus status) { - this.status = status; - return this; - } - - public Builder source(WorkflowSource source) { - this.source = source; - return this; - } - - public Builder limit(int limit) { - this.limit = limit; - return this; - } - - public Builder offset(int offset) { - this.offset = offset; - return this; - } - - public Builder traceId(String traceId) { - this.traceId = traceId; - return this; - } - - public ListWorkflowsOptions build() { - return new ListWorkflowsOptions(status, source, limit, offset, traceId); - } - } + public Map getOutput() { + return output; + } + + public Map getMetadata() { + return metadata; } /** - * Response from listing workflows. + * Returns the number of input tokens consumed by the step. + * + * @return input token count, or null if not provided + * @since 3.6.0 */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class ListWorkflowsResponse { - - @JsonProperty("workflows") - private final List workflows; - - @JsonProperty("total") - private final int total; - - @JsonCreator - public ListWorkflowsResponse( - @JsonProperty("workflows") List workflows, - @JsonProperty("total") int total) { - this.workflows = workflows != null ? Collections.unmodifiableList(workflows) : Collections.emptyList(); - this.total = total; - } - - public List getWorkflows() { - return workflows; - } + public Integer getTokensIn() { + return tokensIn; + } - public int getTotal() { - return total; - } + /** + * Returns the number of output tokens produced by the step. + * + * @return output token count, or null if not provided + * @since 3.6.0 + */ + public Integer getTokensOut() { + return tokensOut; } /** - * Request to mark a step as completed. + * Returns the cost in USD incurred by the step. + * + * @return cost in USD, or null if not provided + * @since 3.6.0 */ - @JsonIgnoreProperties(ignoreUnknown = true) - @JsonInclude(JsonInclude.Include.NON_NULL) - public static final class MarkStepCompletedRequest { - - @JsonProperty("output") - private final Map output; - - @JsonProperty("metadata") - private final Map metadata; - - @JsonProperty("tokens_in") - private final Integer tokensIn; - - @JsonProperty("tokens_out") - private final Integer tokensOut; - - @JsonProperty("cost_usd") - private final Double costUsd; - - @JsonCreator - public MarkStepCompletedRequest( - @JsonProperty("output") Map output, - @JsonProperty("metadata") Map metadata, - @JsonProperty("tokens_in") Integer tokensIn, - @JsonProperty("tokens_out") Integer tokensOut, - @JsonProperty("cost_usd") Double costUsd) { - this.output = output != null ? Collections.unmodifiableMap(output) : Collections.emptyMap(); - this.metadata = metadata != null ? Collections.unmodifiableMap(metadata) : Collections.emptyMap(); - this.tokensIn = tokensIn; - this.tokensOut = tokensOut; - this.costUsd = costUsd; - } + public Double getCostUsd() { + return costUsd; + } - public Map getOutput() { - return output; - } + public static Builder builder() { + return new Builder(); + } - public Map getMetadata() { - return metadata; - } + public static final class Builder { + private Map output; + private Map metadata; + private Integer tokensIn; + private Integer tokensOut; + private Double costUsd; + + public Builder output(Map output) { + this.output = output; + return this; + } + + public Builder metadata(Map metadata) { + this.metadata = metadata; + return this; + } + + public Builder tokensIn(Integer tokensIn) { + this.tokensIn = tokensIn; + return this; + } + + public Builder tokensOut(Integer tokensOut) { + this.tokensOut = tokensOut; + return this; + } + + public Builder costUsd(Double costUsd) { + this.costUsd = costUsd; + return this; + } + + public MarkStepCompletedRequest build() { + return new MarkStepCompletedRequest(output, metadata, tokensIn, tokensOut, costUsd); + } + } + } - /** - * Returns the number of input tokens consumed by the step. - * - * @return input token count, or null if not provided - * @since 3.6.0 - */ - public Integer getTokensIn() { - return tokensIn; - } + /** Request to abort a workflow. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class AbortWorkflowRequest { - /** - * Returns the number of output tokens produced by the step. - * - * @return output token count, or null if not provided - * @since 3.6.0 - */ - public Integer getTokensOut() { - return tokensOut; - } + @JsonProperty("reason") + private final String reason; - /** - * Returns the cost in USD incurred by the step. - * - * @return cost in USD, or null if not provided - * @since 3.6.0 - */ - public Double getCostUsd() { - return costUsd; - } + @JsonCreator + public AbortWorkflowRequest(@JsonProperty("reason") String reason) { + this.reason = reason; + } - public static Builder builder() { - return new Builder(); - } + public String getReason() { + return reason; + } - public static final class Builder { - private Map output; - private Map metadata; - private Integer tokensIn; - private Integer tokensOut; - private Double costUsd; - - public Builder output(Map output) { - this.output = output; - return this; - } - - public Builder metadata(Map metadata) { - this.metadata = metadata; - return this; - } - - public Builder tokensIn(Integer tokensIn) { - this.tokensIn = tokensIn; - return this; - } - - public Builder tokensOut(Integer tokensOut) { - this.tokensOut = tokensOut; - return this; - } - - public Builder costUsd(Double costUsd) { - this.costUsd = costUsd; - return this; - } - - public MarkStepCompletedRequest build() { - return new MarkStepCompletedRequest(output, metadata, tokensIn, tokensOut, costUsd); - } - } + public static AbortWorkflowRequest withReason(String reason) { + return new AbortWorkflowRequest(reason); } + } - /** - * Request to abort a workflow. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class AbortWorkflowRequest { + // ======================================================================== + // WCP Approval Types + // ======================================================================== - @JsonProperty("reason") - private final String reason; + /** Response from approving a workflow step. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class ApproveStepResponse { - @JsonCreator - public AbortWorkflowRequest(@JsonProperty("reason") String reason) { - this.reason = reason; - } + @JsonProperty("workflow_id") + private final String workflowId; - public String getReason() { - return reason; - } + @JsonProperty("step_id") + private final String stepId; - public static AbortWorkflowRequest withReason(String reason) { - return new AbortWorkflowRequest(reason); - } + @JsonProperty("status") + private final String status; + + @JsonCreator + public ApproveStepResponse( + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("step_id") String stepId, + @JsonProperty("status") String status) { + this.workflowId = workflowId; + this.stepId = stepId; + this.status = status; } - // ======================================================================== - // WCP Approval Types - // ======================================================================== + public String getWorkflowId() { + return workflowId; + } - /** - * Response from approving a workflow step. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class ApproveStepResponse { - - @JsonProperty("workflow_id") - private final String workflowId; - - @JsonProperty("step_id") - private final String stepId; - - @JsonProperty("status") - private final String status; - - @JsonCreator - public ApproveStepResponse( - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("step_id") String stepId, - @JsonProperty("status") String status) { - this.workflowId = workflowId; - this.stepId = stepId; - this.status = status; - } + public String getStepId() { + return stepId; + } - public String getWorkflowId() { - return workflowId; - } + public String getStatus() { + return status; + } - public String getStepId() { - return stepId; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ApproveStepResponse that = (ApproveStepResponse) o; + return Objects.equals(workflowId, that.workflowId) + && Objects.equals(stepId, that.stepId) + && Objects.equals(status, that.status); + } - public String getStatus() { - return status; - } + @Override + public int hashCode() { + return Objects.hash(workflowId, stepId, status); + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ApproveStepResponse that = (ApproveStepResponse) o; - return Objects.equals(workflowId, that.workflowId) && - Objects.equals(stepId, that.stepId) && - Objects.equals(status, that.status); - } + @Override + public String toString() { + return "ApproveStepResponse{" + + "workflowId='" + + workflowId + + '\'' + + ", stepId='" + + stepId + + '\'' + + ", status='" + + status + + '\'' + + '}'; + } + } - @Override - public int hashCode() { - return Objects.hash(workflowId, stepId, status); - } + /** Response from rejecting a workflow step. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class RejectStepResponse { - @Override - public String toString() { - return "ApproveStepResponse{" + - "workflowId='" + workflowId + '\'' + - ", stepId='" + stepId + '\'' + - ", status='" + status + '\'' + - '}'; - } - } + @JsonProperty("workflow_id") + private final String workflowId; - /** - * Response from rejecting a workflow step. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class RejectStepResponse { - - @JsonProperty("workflow_id") - private final String workflowId; - - @JsonProperty("step_id") - private final String stepId; - - @JsonProperty("status") - private final String status; - - @JsonCreator - public RejectStepResponse( - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("step_id") String stepId, - @JsonProperty("status") String status) { - this.workflowId = workflowId; - this.stepId = stepId; - this.status = status; - } + @JsonProperty("step_id") + private final String stepId; - public String getWorkflowId() { - return workflowId; - } + @JsonProperty("status") + private final String status; - public String getStepId() { - return stepId; - } + @JsonCreator + public RejectStepResponse( + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("step_id") String stepId, + @JsonProperty("status") String status) { + this.workflowId = workflowId; + this.stepId = stepId; + this.status = status; + } - public String getStatus() { - return status; - } + public String getWorkflowId() { + return workflowId; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RejectStepResponse that = (RejectStepResponse) o; - return Objects.equals(workflowId, that.workflowId) && - Objects.equals(stepId, that.stepId) && - Objects.equals(status, that.status); - } + public String getStepId() { + return stepId; + } - @Override - public int hashCode() { - return Objects.hash(workflowId, stepId, status); - } + public String getStatus() { + return status; + } - @Override - public String toString() { - return "RejectStepResponse{" + - "workflowId='" + workflowId + '\'' + - ", stepId='" + stepId + '\'' + - ", status='" + status + '\'' + - '}'; - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + RejectStepResponse that = (RejectStepResponse) o; + return Objects.equals(workflowId, that.workflowId) + && Objects.equals(stepId, that.stepId) + && Objects.equals(status, that.status); } - /** - * A pending approval for a workflow step. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class PendingApproval { - - @JsonProperty("workflow_id") - private final String workflowId; - - @JsonProperty("workflow_name") - private final String workflowName; - - @JsonProperty("step_id") - private final String stepId; - - @JsonProperty("step_name") - private final String stepName; - - @JsonProperty("step_type") - private final String stepType; - - @JsonProperty("created_at") - private final String createdAt; - - @JsonCreator - public PendingApproval( - @JsonProperty("workflow_id") String workflowId, - @JsonProperty("workflow_name") String workflowName, - @JsonProperty("step_id") String stepId, - @JsonProperty("step_name") String stepName, - @JsonProperty("step_type") String stepType, - @JsonProperty("created_at") String createdAt) { - this.workflowId = workflowId; - this.workflowName = workflowName; - this.stepId = stepId; - this.stepName = stepName; - this.stepType = stepType; - this.createdAt = createdAt; - } + @Override + public int hashCode() { + return Objects.hash(workflowId, stepId, status); + } - public String getWorkflowId() { - return workflowId; - } + @Override + public String toString() { + return "RejectStepResponse{" + + "workflowId='" + + workflowId + + '\'' + + ", stepId='" + + stepId + + '\'' + + ", status='" + + status + + '\'' + + '}'; + } + } + + /** A pending approval for a workflow step. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class PendingApproval { + + @JsonProperty("workflow_id") + private final String workflowId; + + @JsonProperty("workflow_name") + private final String workflowName; + + @JsonProperty("step_id") + private final String stepId; + + @JsonProperty("step_name") + private final String stepName; + + @JsonProperty("step_type") + private final String stepType; + + @JsonProperty("created_at") + private final String createdAt; + + @JsonCreator + public PendingApproval( + @JsonProperty("workflow_id") String workflowId, + @JsonProperty("workflow_name") String workflowName, + @JsonProperty("step_id") String stepId, + @JsonProperty("step_name") String stepName, + @JsonProperty("step_type") String stepType, + @JsonProperty("created_at") String createdAt) { + this.workflowId = workflowId; + this.workflowName = workflowName; + this.stepId = stepId; + this.stepName = stepName; + this.stepType = stepType; + this.createdAt = createdAt; + } - public String getWorkflowName() { - return workflowName; - } + public String getWorkflowId() { + return workflowId; + } - public String getStepId() { - return stepId; - } + public String getWorkflowName() { + return workflowName; + } - public String getStepName() { - return stepName; - } + public String getStepId() { + return stepId; + } - public String getStepType() { - return stepType; - } + public String getStepName() { + return stepName; + } - public String getCreatedAt() { - return createdAt; - } + public String getStepType() { + return stepType; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PendingApproval that = (PendingApproval) o; - return Objects.equals(workflowId, that.workflowId) && - Objects.equals(workflowName, that.workflowName) && - Objects.equals(stepId, that.stepId) && - Objects.equals(stepName, that.stepName) && - Objects.equals(stepType, that.stepType) && - Objects.equals(createdAt, that.createdAt); - } + public String getCreatedAt() { + return createdAt; + } - @Override - public int hashCode() { - return Objects.hash(workflowId, workflowName, stepId, stepName, stepType, createdAt); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PendingApproval that = (PendingApproval) o; + return Objects.equals(workflowId, that.workflowId) + && Objects.equals(workflowName, that.workflowName) + && Objects.equals(stepId, that.stepId) + && Objects.equals(stepName, that.stepName) + && Objects.equals(stepType, that.stepType) + && Objects.equals(createdAt, that.createdAt); + } - @Override - public String toString() { - return "PendingApproval{" + - "workflowId='" + workflowId + '\'' + - ", workflowName='" + workflowName + '\'' + - ", stepId='" + stepId + '\'' + - ", stepName='" + stepName + '\'' + - ", stepType='" + stepType + '\'' + - ", createdAt='" + createdAt + '\'' + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(workflowId, workflowName, stepId, stepName, stepType, createdAt); } - /** - * Response containing a list of pending approvals. - */ - @JsonIgnoreProperties(ignoreUnknown = true) - public static final class PendingApprovalsResponse { + @Override + public String toString() { + return "PendingApproval{" + + "workflowId='" + + workflowId + + '\'' + + ", workflowName='" + + workflowName + + '\'' + + ", stepId='" + + stepId + + '\'' + + ", stepName='" + + stepName + + '\'' + + ", stepType='" + + stepType + + '\'' + + ", createdAt='" + + createdAt + + '\'' + + '}'; + } + } - @JsonProperty("approvals") - private final List approvals; + /** Response containing a list of pending approvals. */ + @JsonIgnoreProperties(ignoreUnknown = true) + public static final class PendingApprovalsResponse { - @JsonProperty("total") - private final int total; + @JsonProperty("approvals") + private final List approvals; - @JsonCreator - public PendingApprovalsResponse( - @JsonProperty("approvals") List approvals, - @JsonProperty("total") int total) { - this.approvals = approvals != null ? Collections.unmodifiableList(approvals) : Collections.emptyList(); - this.total = total; - } + @JsonProperty("total") + private final int total; - public List getApprovals() { - return approvals; - } + @JsonCreator + public PendingApprovalsResponse( + @JsonProperty("approvals") List approvals, + @JsonProperty("total") int total) { + this.approvals = + approvals != null ? Collections.unmodifiableList(approvals) : Collections.emptyList(); + this.total = total; + } - public int getTotal() { - return total; - } + public List getApprovals() { + return approvals; + } - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - PendingApprovalsResponse that = (PendingApprovalsResponse) o; - return total == that.total && - Objects.equals(approvals, that.approvals); - } + public int getTotal() { + return total; + } - @Override - public int hashCode() { - return Objects.hash(approvals, total); - } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + PendingApprovalsResponse that = (PendingApprovalsResponse) o; + return total == that.total && Objects.equals(approvals, that.approvals); + } - @Override - public String toString() { - return "PendingApprovalsResponse{" + - "approvals=" + approvals + - ", total=" + total + - '}'; - } + @Override + public int hashCode() { + return Objects.hash(approvals, total); + } + + @Override + public String toString() { + return "PendingApprovalsResponse{" + "approvals=" + approvals + ", total=" + total + '}'; } + } } diff --git a/src/main/java/com/getaxonflow/sdk/types/workflow/package-info.java b/src/main/java/com/getaxonflow/sdk/types/workflow/package-info.java index 4f785b3..e416f4a 100644 --- a/src/main/java/com/getaxonflow/sdk/types/workflow/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/types/workflow/package-info.java @@ -17,21 +17,24 @@ /** * Workflow Control Plane types for AxonFlow SDK. * - *

The Workflow Control Plane provides governance gates for external orchestrators - * like LangChain, LangGraph, and CrewAI. These types define the request/response - * structures for registering workflows, checking step gates, and managing workflow - * lifecycle. + *

The Workflow Control Plane provides governance gates for external orchestrators like + * LangChain, LangGraph, and CrewAI. These types define the request/response structures for + * registering workflows, checking step gates, and managing workflow lifecycle. * *

"LangChain runs the workflow. AxonFlow decides when it's allowed to move forward." * *

Policy Enforcement Types (v2.3.0)

+ * *
    - *
  • {@link com.getaxonflow.sdk.types.workflow.PolicyEvaluationResult} - Result of policy evaluation during execution
  • - *
  • {@link com.getaxonflow.sdk.types.workflow.PolicyMatch} - Information about a matched policy
  • - *
  • {@link com.getaxonflow.sdk.types.workflow.PlanExecutionResponse} - Response from MAP plan execution with policy info
  • + *
  • {@link com.getaxonflow.sdk.types.workflow.PolicyEvaluationResult} - Result of policy + * evaluation during execution + *
  • {@link com.getaxonflow.sdk.types.workflow.PolicyMatch} - Information about a matched policy + *
  • {@link com.getaxonflow.sdk.types.workflow.PlanExecutionResponse} - Response from MAP plan + * execution with policy info *
* *

Example Usage

+ * *
{@code
  * // Create a workflow
  * CreateWorkflowResponse workflow = axonflow.createWorkflow(
diff --git a/src/main/java/com/getaxonflow/sdk/util/CacheConfig.java b/src/main/java/com/getaxonflow/sdk/util/CacheConfig.java
index 4ad1cca..c0a5ed2 100644
--- a/src/main/java/com/getaxonflow/sdk/util/CacheConfig.java
+++ b/src/main/java/com/getaxonflow/sdk/util/CacheConfig.java
@@ -18,118 +18,108 @@
 import java.time.Duration;
 import java.util.Objects;
 
-/**
- * Configuration for caching behavior.
- */
+/** Configuration for caching behavior. */
 public final class CacheConfig {
 
-    /** Default TTL for cached entries. */
-    public static final Duration DEFAULT_TTL = Duration.ofSeconds(60);
-
-    /** Default maximum cache size. */
-    public static final int DEFAULT_MAX_SIZE = 1000;
-
-    private final boolean enabled;
-    private final Duration ttl;
-    private final int maxSize;
-
-    private CacheConfig(Builder builder) {
-        this.enabled = builder.enabled;
-        this.ttl = builder.ttl;
-        this.maxSize = builder.maxSize;
-    }
-
-    /**
-     * Returns default cache configuration.
-     *
-     * @return default configuration with caching enabled
-     */
-    public static CacheConfig defaults() {
-        return builder().build();
-    }
-
-    /**
-     * Returns configuration with caching disabled.
-     *
-     * @return configuration with caching disabled
-     */
-    public static CacheConfig disabled() {
-        return builder().enabled(false).build();
-    }
-
-    public boolean isEnabled() {
-        return enabled;
-    }
-
-    public Duration getTtl() {
-        return ttl;
-    }
-
-    public int getMaxSize() {
-        return maxSize;
-    }
-
-    public static Builder builder() {
-        return new Builder();
-    }
-
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        CacheConfig that = (CacheConfig) o;
-        return enabled == that.enabled &&
-               maxSize == that.maxSize &&
-               Objects.equals(ttl, that.ttl);
+  /** Default TTL for cached entries. */
+  public static final Duration DEFAULT_TTL = Duration.ofSeconds(60);
+
+  /** Default maximum cache size. */
+  public static final int DEFAULT_MAX_SIZE = 1000;
+
+  private final boolean enabled;
+  private final Duration ttl;
+  private final int maxSize;
+
+  private CacheConfig(Builder builder) {
+    this.enabled = builder.enabled;
+    this.ttl = builder.ttl;
+    this.maxSize = builder.maxSize;
+  }
+
+  /**
+   * Returns default cache configuration.
+   *
+   * @return default configuration with caching enabled
+   */
+  public static CacheConfig defaults() {
+    return builder().build();
+  }
+
+  /**
+   * Returns configuration with caching disabled.
+   *
+   * @return configuration with caching disabled
+   */
+  public static CacheConfig disabled() {
+    return builder().enabled(false).build();
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public Duration getTtl() {
+    return ttl;
+  }
+
+  public int getMaxSize() {
+    return maxSize;
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    CacheConfig that = (CacheConfig) o;
+    return enabled == that.enabled && maxSize == that.maxSize && Objects.equals(ttl, that.ttl);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(enabled, ttl, maxSize);
+  }
+
+  @Override
+  public String toString() {
+    return "CacheConfig{" + "enabled=" + enabled + ", ttl=" + ttl + ", maxSize=" + maxSize + '}';
+  }
+
+  /** Builder for CacheConfig. */
+  public static final class Builder {
+    private boolean enabled = true;
+    private Duration ttl = DEFAULT_TTL;
+    private int maxSize = DEFAULT_MAX_SIZE;
+
+    private Builder() {}
+
+    public Builder enabled(boolean enabled) {
+      this.enabled = enabled;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(enabled, ttl, maxSize);
+    public Builder ttl(Duration ttl) {
+      if (ttl == null || ttl.isNegative()) {
+        throw new IllegalArgumentException("ttl must be non-negative");
+      }
+      this.ttl = ttl;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "CacheConfig{" +
-               "enabled=" + enabled +
-               ", ttl=" + ttl +
-               ", maxSize=" + maxSize +
-               '}';
+    public Builder maxSize(int maxSize) {
+      if (maxSize < 1) {
+        throw new IllegalArgumentException("maxSize must be at least 1");
+      }
+      this.maxSize = maxSize;
+      return this;
     }
 
-    /**
-     * Builder for CacheConfig.
-     */
-    public static final class Builder {
-        private boolean enabled = true;
-        private Duration ttl = DEFAULT_TTL;
-        private int maxSize = DEFAULT_MAX_SIZE;
-
-        private Builder() {}
-
-        public Builder enabled(boolean enabled) {
-            this.enabled = enabled;
-            return this;
-        }
-
-        public Builder ttl(Duration ttl) {
-            if (ttl == null || ttl.isNegative()) {
-                throw new IllegalArgumentException("ttl must be non-negative");
-            }
-            this.ttl = ttl;
-            return this;
-        }
-
-        public Builder maxSize(int maxSize) {
-            if (maxSize < 1) {
-                throw new IllegalArgumentException("maxSize must be at least 1");
-            }
-            this.maxSize = maxSize;
-            return this;
-        }
-
-        public CacheConfig build() {
-            return new CacheConfig(this);
-        }
+    public CacheConfig build() {
+      return new CacheConfig(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/util/HttpClientFactory.java b/src/main/java/com/getaxonflow/sdk/util/HttpClientFactory.java
index 3806c8b..ced5419 100644
--- a/src/main/java/com/getaxonflow/sdk/util/HttpClientFactory.java
+++ b/src/main/java/com/getaxonflow/sdk/util/HttpClientFactory.java
@@ -16,96 +16,106 @@
 package com.getaxonflow.sdk.util;
 
 import com.getaxonflow.sdk.AxonFlowConfig;
-import okhttp3.OkHttpClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import javax.net.ssl.SSLContext;
-import javax.net.ssl.TrustManager;
-import javax.net.ssl.X509TrustManager;
 import java.security.SecureRandom;
 import java.security.cert.X509Certificate;
 import java.util.concurrent.TimeUnit;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.X509TrustManager;
+import okhttp3.OkHttpClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-/**
- * Factory for creating configured HTTP clients.
- */
+/** Factory for creating configured HTTP clients. */
 public final class HttpClientFactory {
 
-    private static final Logger logger = LoggerFactory.getLogger(HttpClientFactory.class);
+  private static final Logger logger = LoggerFactory.getLogger(HttpClientFactory.class);
 
-    private HttpClientFactory() {
-        // Utility class
-    }
+  private HttpClientFactory() {
+    // Utility class
+  }
 
-    /**
-     * Creates an OkHttpClient configured according to the SDK configuration.
-     *
-     * @param config the SDK configuration
-     * @return a configured OkHttpClient
-     */
-    public static OkHttpClient create(AxonFlowConfig config) {
-        OkHttpClient.Builder builder = new OkHttpClient.Builder()
+  /**
+   * Creates an OkHttpClient configured according to the SDK configuration.
+   *
+   * @param config the SDK configuration
+   * @return a configured OkHttpClient
+   */
+  public static OkHttpClient create(AxonFlowConfig config) {
+    OkHttpClient.Builder builder =
+        new OkHttpClient.Builder()
             .connectTimeout(config.getTimeout().toMillis(), TimeUnit.MILLISECONDS)
             .readTimeout(config.getTimeout().toMillis(), TimeUnit.MILLISECONDS)
             .writeTimeout(config.getTimeout().toMillis(), TimeUnit.MILLISECONDS)
             .callTimeout(config.getTimeout().toMillis() * 2, TimeUnit.MILLISECONDS);
 
-        if (config.isInsecureSkipVerify()) {
-            configureInsecureSsl(builder);
-        }
-
-        if (config.isDebug()) {
-            builder.addInterceptor(chain -> {
-                okhttp3.Request request = chain.request();
-                logger.debug("Request: {} {}", request.method(), request.url());
-                okhttp3.Response response = chain.proceed(request);
-                logger.debug("Response: {} {} ({}ms)",
-                    response.code(), response.message(),
-                    response.receivedResponseAtMillis() - response.sentRequestAtMillis());
-                return response;
-            });
-        }
+    if (config.isInsecureSkipVerify()) {
+      configureInsecureSsl(builder);
+    }
 
-        return builder.build();
+    if (config.isDebug()) {
+      builder.addInterceptor(
+          chain -> {
+            okhttp3.Request request = chain.request();
+            logger.debug("Request: {} {}", request.method(), request.url());
+            okhttp3.Response response = chain.proceed(request);
+            logger.debug(
+                "Response: {} {} ({}ms)",
+                response.code(),
+                response.message(),
+                response.receivedResponseAtMillis() - response.sentRequestAtMillis());
+            return response;
+          });
     }
 
-    @SuppressWarnings({"java:S4830", "java:S5527"}) // Intentionally trusting all certificates when insecureSkipVerify is enabled
-    private static void configureInsecureSsl(OkHttpClient.Builder builder) {
-        try {
-            // CodeQL: java/insecure-trustmanager -- suppressed: opt-in for development/self-signed certificates.
-            // This trust manager is only activated when the user explicitly sets insecureSkipVerify=true
-            // in AxonFlowConfig. It is never used by default.
-            TrustManager[] trustAllCerts = new TrustManager[]{
-                new X509TrustManager() { // lgtm[java/insecure-trustmanager]
-                    @Override
-                    public void checkClientTrusted(X509Certificate[] chain, String authType) {
-                        // Intentionally empty: trust all client certificates when insecureSkipVerify is enabled
-                    }
+    return builder.build();
+  }
+
+  @SuppressWarnings({
+    "java:S4830",
+    "java:S5527"
+  }) // Intentionally trusting all certificates when insecureSkipVerify is enabled
+  private static void configureInsecureSsl(OkHttpClient.Builder builder) {
+    try {
+      // CodeQL: java/insecure-trustmanager -- suppressed: opt-in for development/self-signed
+      // certificates.
+      // This trust manager is only activated when the user explicitly sets insecureSkipVerify=true
+      // in AxonFlowConfig. It is never used by default.
+      TrustManager[] trustAllCerts =
+          new TrustManager[] {
+            new X509TrustManager() { // lgtm[java/insecure-trustmanager]
+              @Override
+              public void checkClientTrusted(X509Certificate[] chain, String authType) {
+                // Intentionally empty: trust all client certificates when insecureSkipVerify is
+                // enabled
+              }
 
-                    @Override
-                    public void checkServerTrusted(X509Certificate[] chain, String authType) {
-                        // Intentionally empty: trust all server certificates when insecureSkipVerify is enabled
-                    }
+              @Override
+              public void checkServerTrusted(X509Certificate[] chain, String authType) {
+                // Intentionally empty: trust all server certificates when insecureSkipVerify is
+                // enabled
+              }
 
-                    @Override
-                    public X509Certificate[] getAcceptedIssuers() {
-                        return new X509Certificate[0];
-                    }
-                }
-            };
+              @Override
+              public X509Certificate[] getAcceptedIssuers() {
+                return new X509Certificate[0];
+              }
+            }
+          };
 
-            SSLContext sslContext = SSLContext.getInstance("TLS");
-            sslContext.init(null, trustAllCerts, new SecureRandom());
+      SSLContext sslContext = SSLContext.getInstance("TLS");
+      sslContext.init(null, trustAllCerts, new SecureRandom());
 
-            builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]);
-            builder.hostnameVerifier((hostname, session) -> true); // lgtm[java/insecure-hostname-verifier]
+      builder.sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustAllCerts[0]);
+      builder.hostnameVerifier(
+          (hostname, session) -> true); // lgtm[java/insecure-hostname-verifier]
 
-            logger.warn("SSL certificate verification is DISABLED (insecureSkipVerify=true). "
-                + "Do NOT use this in production. This is intended only for development environments "
-                + "with self-signed certificates.");
-        } catch (Exception e) {
-            logger.error("Failed to configure insecure SSL", e);
-        }
+      logger.warn(
+          "SSL certificate verification is DISABLED (insecureSkipVerify=true). "
+              + "Do NOT use this in production. This is intended only for development environments "
+              + "with self-signed certificates.");
+    } catch (Exception e) {
+      logger.error("Failed to configure insecure SSL", e);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/util/ResponseCache.java b/src/main/java/com/getaxonflow/sdk/util/ResponseCache.java
index c68e7b6..bb1aec3 100644
--- a/src/main/java/com/getaxonflow/sdk/util/ResponseCache.java
+++ b/src/main/java/com/getaxonflow/sdk/util/ResponseCache.java
@@ -17,13 +17,12 @@
 
 import com.github.benmanes.caffeine.cache.Cache;
 import com.github.benmanes.caffeine.cache.Caffeine;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.nio.charset.StandardCharsets;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Optional;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /**
  * Thread-safe cache for API responses.
@@ -32,150 +31,149 @@
  */
 public final class ResponseCache {
 
-    private static final Logger logger = LoggerFactory.getLogger(ResponseCache.class);
-
-    private final Cache cache;
-    private final boolean enabled;
-
-    /**
-     * Creates a new response cache.
-     *
-     * @param config the cache configuration
-     */
-    public ResponseCache(CacheConfig config) {
-        this.enabled = config.isEnabled();
-        if (enabled) {
-            this.cache = Caffeine.newBuilder()
-                .maximumSize(config.getMaxSize())
-                .expireAfterWrite(config.getTtl())
-                .recordStats()
-                .build();
-        } else {
-            this.cache = null;
-        }
+  private static final Logger logger = LoggerFactory.getLogger(ResponseCache.class);
+
+  private final Cache cache;
+  private final boolean enabled;
+
+  /**
+   * Creates a new response cache.
+   *
+   * @param config the cache configuration
+   */
+  public ResponseCache(CacheConfig config) {
+    this.enabled = config.isEnabled();
+    if (enabled) {
+      this.cache =
+          Caffeine.newBuilder()
+              .maximumSize(config.getMaxSize())
+              .expireAfterWrite(config.getTtl())
+              .recordStats()
+              .build();
+    } else {
+      this.cache = null;
     }
-
-    /**
-     * Gets a cached response.
-     *
-     * @param       the response type
-     * @param cacheKey the cache key
-     * @param type     the expected response type
-     * @return the cached response, or empty if not found
-     */
-    @SuppressWarnings("unchecked")
-    public  Optional get(String cacheKey, Class type) {
-        if (!enabled || cache == null) {
-            return Optional.empty();
-        }
-
-        CachedResponse cached = cache.getIfPresent(cacheKey);
-        if (cached != null && type.isInstance(cached.getResponse())) {
-            logger.debug("Cache hit for key: {}", cacheKey);
-            return Optional.of((T) cached.getResponse());
-        }
-
-        logger.debug("Cache miss for key: {}", cacheKey);
-        return Optional.empty();
+  }
+
+  /**
+   * Gets a cached response.
+   *
+   * @param  the response type
+   * @param cacheKey the cache key
+   * @param type the expected response type
+   * @return the cached response, or empty if not found
+   */
+  @SuppressWarnings("unchecked")
+  public  Optional get(String cacheKey, Class type) {
+    if (!enabled || cache == null) {
+      return Optional.empty();
     }
 
-    /**
-     * Stores a response in the cache.
-     *
-     * @param cacheKey the cache key
-     * @param response the response to cache
-     */
-    public void put(String cacheKey, Object response) {
-        if (!enabled || cache == null || response == null) {
-            return;
-        }
-
-        cache.put(cacheKey, new CachedResponse(response));
-        logger.debug("Cached response for key: {}", cacheKey);
+    CachedResponse cached = cache.getIfPresent(cacheKey);
+    if (cached != null && type.isInstance(cached.getResponse())) {
+      logger.debug("Cache hit for key: {}", cacheKey);
+      return Optional.of((T) cached.getResponse());
     }
 
-    /**
-     * Invalidates a specific cache entry.
-     *
-     * @param cacheKey the cache key to invalidate
-     */
-    public void invalidate(String cacheKey) {
-        if (cache != null) {
-            cache.invalidate(cacheKey);
-        }
+    logger.debug("Cache miss for key: {}", cacheKey);
+    return Optional.empty();
+  }
+
+  /**
+   * Stores a response in the cache.
+   *
+   * @param cacheKey the cache key
+   * @param response the response to cache
+   */
+  public void put(String cacheKey, Object response) {
+    if (!enabled || cache == null || response == null) {
+      return;
     }
 
-    /**
-     * Clears all cached entries.
-     */
-    public void clear() {
-        if (cache != null) {
-            cache.invalidateAll();
-        }
+    cache.put(cacheKey, new CachedResponse(response));
+    logger.debug("Cached response for key: {}", cacheKey);
+  }
+
+  /**
+   * Invalidates a specific cache entry.
+   *
+   * @param cacheKey the cache key to invalidate
+   */
+  public void invalidate(String cacheKey) {
+    if (cache != null) {
+      cache.invalidate(cacheKey);
     }
+  }
 
-    /**
-     * Generates a cache key from request parameters.
-     *
-     * @param requestType the type of request
-     * @param query       the query string
-     * @param userToken   the user token
-     * @return a unique cache key
-     */
-    public static String generateKey(String requestType, String query, String userToken) {
-        String input = String.format("%s:%s:%s",
+  /** Clears all cached entries. */
+  public void clear() {
+    if (cache != null) {
+      cache.invalidateAll();
+    }
+  }
+
+  /**
+   * Generates a cache key from request parameters.
+   *
+   * @param requestType the type of request
+   * @param query the query string
+   * @param userToken the user token
+   * @return a unique cache key
+   */
+  public static String generateKey(String requestType, String query, String userToken) {
+    String input =
+        String.format(
+            "%s:%s:%s",
             requestType != null ? requestType : "",
             query != null ? query : "",
             userToken != null ? userToken : "");
 
-        try {
-            MessageDigest digest = MessageDigest.getInstance("SHA-256");
-            byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
-            StringBuilder hexString = new StringBuilder();
-            for (byte b : hash) {
-                String hex = Integer.toHexString(0xff & b);
-                if (hex.length() == 1) {
-                    hexString.append('0');
-                }
-                hexString.append(hex);
-            }
-            return hexString.toString();
-        } catch (NoSuchAlgorithmException e) {
-            // Fall back to simple hash if SHA-256 not available
-            return String.valueOf(input.hashCode());
+    try {
+      MessageDigest digest = MessageDigest.getInstance("SHA-256");
+      byte[] hash = digest.digest(input.getBytes(StandardCharsets.UTF_8));
+      StringBuilder hexString = new StringBuilder();
+      for (byte b : hash) {
+        String hex = Integer.toHexString(0xff & b);
+        if (hex.length() == 1) {
+          hexString.append('0');
         }
+        hexString.append(hex);
+      }
+      return hexString.toString();
+    } catch (NoSuchAlgorithmException e) {
+      // Fall back to simple hash if SHA-256 not available
+      return String.valueOf(input.hashCode());
     }
-
-    /**
-     * Returns cache statistics.
-     *
-     * @return cache statistics string
-     */
-    public String getStats() {
-        if (cache == null) {
-            return "Cache disabled";
-        }
-        return cache.stats().toString();
+  }
+
+  /**
+   * Returns cache statistics.
+   *
+   * @return cache statistics string
+   */
+  public String getStats() {
+    if (cache == null) {
+      return "Cache disabled";
     }
+    return cache.stats().toString();
+  }
 
-    /**
-     * Wrapper for cached responses.
-     */
-    private static final class CachedResponse {
-        private final Object response;
-        private final long cachedAt;
+  /** Wrapper for cached responses. */
+  private static final class CachedResponse {
+    private final Object response;
+    private final long cachedAt;
 
-        CachedResponse(Object response) {
-            this.response = response;
-            this.cachedAt = System.currentTimeMillis();
-        }
+    CachedResponse(Object response) {
+      this.response = response;
+      this.cachedAt = System.currentTimeMillis();
+    }
 
-        Object getResponse() {
-            return response;
-        }
+    Object getResponse() {
+      return response;
+    }
 
-        long getCachedAt() {
-            return cachedAt;
-        }
+    long getCachedAt() {
+      return cachedAt;
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/util/RetryConfig.java b/src/main/java/com/getaxonflow/sdk/util/RetryConfig.java
index d51aad4..25bfcba 100644
--- a/src/main/java/com/getaxonflow/sdk/util/RetryConfig.java
+++ b/src/main/java/com/getaxonflow/sdk/util/RetryConfig.java
@@ -18,175 +18,176 @@
 import java.time.Duration;
 import java.util.Objects;
 
-/**
- * Configuration for retry behavior.
- */
+/** Configuration for retry behavior. */
 public final class RetryConfig {
 
-    /** Default maximum retry attempts. */
-    public static final int DEFAULT_MAX_ATTEMPTS = 3;
-
-    /** Default initial delay between retries. */
-    public static final Duration DEFAULT_INITIAL_DELAY = Duration.ofSeconds(1);
-
-    /** Default maximum delay between retries. */
-    public static final Duration DEFAULT_MAX_DELAY = Duration.ofSeconds(30);
-
-    /** Default exponential backoff multiplier. */
-    public static final double DEFAULT_MULTIPLIER = 2.0;
-
-    private final boolean enabled;
-    private final int maxAttempts;
-    private final Duration initialDelay;
-    private final Duration maxDelay;
-    private final double multiplier;
-
-    private RetryConfig(Builder builder) {
-        this.enabled = builder.enabled;
-        this.maxAttempts = builder.maxAttempts;
-        this.initialDelay = builder.initialDelay;
-        this.maxDelay = builder.maxDelay;
-        this.multiplier = builder.multiplier;
+  /** Default maximum retry attempts. */
+  public static final int DEFAULT_MAX_ATTEMPTS = 3;
+
+  /** Default initial delay between retries. */
+  public static final Duration DEFAULT_INITIAL_DELAY = Duration.ofSeconds(1);
+
+  /** Default maximum delay between retries. */
+  public static final Duration DEFAULT_MAX_DELAY = Duration.ofSeconds(30);
+
+  /** Default exponential backoff multiplier. */
+  public static final double DEFAULT_MULTIPLIER = 2.0;
+
+  private final boolean enabled;
+  private final int maxAttempts;
+  private final Duration initialDelay;
+  private final Duration maxDelay;
+  private final double multiplier;
+
+  private RetryConfig(Builder builder) {
+    this.enabled = builder.enabled;
+    this.maxAttempts = builder.maxAttempts;
+    this.initialDelay = builder.initialDelay;
+    this.maxDelay = builder.maxDelay;
+    this.multiplier = builder.multiplier;
+  }
+
+  /**
+   * Returns default retry configuration.
+   *
+   * @return default configuration with retries enabled
+   */
+  public static RetryConfig defaults() {
+    return builder().build();
+  }
+
+  /**
+   * Returns configuration with retries disabled.
+   *
+   * @return configuration with retries disabled
+   */
+  public static RetryConfig disabled() {
+    return builder().enabled(false).build();
+  }
+
+  public boolean isEnabled() {
+    return enabled;
+  }
+
+  public int getMaxAttempts() {
+    return maxAttempts;
+  }
+
+  public Duration getInitialDelay() {
+    return initialDelay;
+  }
+
+  public Duration getMaxDelay() {
+    return maxDelay;
+  }
+
+  public double getMultiplier() {
+    return multiplier;
+  }
+
+  /**
+   * Calculates the delay for a given attempt number using exponential backoff.
+   *
+   * @param attempt the attempt number (1-based)
+   * @return the delay duration
+   */
+  public Duration getDelayForAttempt(int attempt) {
+    if (attempt <= 1) {
+      return initialDelay;
     }
-
-    /**
-     * Returns default retry configuration.
-     *
-     * @return default configuration with retries enabled
-     */
-    public static RetryConfig defaults() {
-        return builder().build();
-    }
-
-    /**
-     * Returns configuration with retries disabled.
-     *
-     * @return configuration with retries disabled
-     */
-    public static RetryConfig disabled() {
-        return builder().enabled(false).build();
-    }
-
-    public boolean isEnabled() {
-        return enabled;
-    }
-
-    public int getMaxAttempts() {
-        return maxAttempts;
-    }
-
-    public Duration getInitialDelay() {
-        return initialDelay;
-    }
-
-    public Duration getMaxDelay() {
-        return maxDelay;
-    }
-
-    public double getMultiplier() {
-        return multiplier;
-    }
-
-    /**
-     * Calculates the delay for a given attempt number using exponential backoff.
-     *
-     * @param attempt the attempt number (1-based)
-     * @return the delay duration
-     */
-    public Duration getDelayForAttempt(int attempt) {
-        if (attempt <= 1) {
-            return initialDelay;
-        }
-        long delayMs = (long) (initialDelay.toMillis() * Math.pow(multiplier, attempt - 1));
-        return Duration.ofMillis(Math.min(delayMs, maxDelay.toMillis()));
+    long delayMs = (long) (initialDelay.toMillis() * Math.pow(multiplier, attempt - 1));
+    return Duration.ofMillis(Math.min(delayMs, maxDelay.toMillis()));
+  }
+
+  public static Builder builder() {
+    return new Builder();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (this == o) return true;
+    if (o == null || getClass() != o.getClass()) return false;
+    RetryConfig that = (RetryConfig) o;
+    return enabled == that.enabled
+        && maxAttempts == that.maxAttempts
+        && Double.compare(that.multiplier, multiplier) == 0
+        && Objects.equals(initialDelay, that.initialDelay)
+        && Objects.equals(maxDelay, that.maxDelay);
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hash(enabled, maxAttempts, initialDelay, maxDelay, multiplier);
+  }
+
+  @Override
+  public String toString() {
+    return "RetryConfig{"
+        + "enabled="
+        + enabled
+        + ", maxAttempts="
+        + maxAttempts
+        + ", initialDelay="
+        + initialDelay
+        + ", maxDelay="
+        + maxDelay
+        + ", multiplier="
+        + multiplier
+        + '}';
+  }
+
+  /** Builder for RetryConfig. */
+  public static final class Builder {
+    private boolean enabled = true;
+    private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
+    private Duration initialDelay = DEFAULT_INITIAL_DELAY;
+    private Duration maxDelay = DEFAULT_MAX_DELAY;
+    private double multiplier = DEFAULT_MULTIPLIER;
+
+    private Builder() {}
+
+    public Builder enabled(boolean enabled) {
+      this.enabled = enabled;
+      return this;
     }
 
-    public static Builder builder() {
-        return new Builder();
+    public Builder maxAttempts(int maxAttempts) {
+      if (maxAttempts < 1) {
+        throw new IllegalArgumentException("maxAttempts must be at least 1");
+      }
+      if (maxAttempts > 10) {
+        throw new IllegalArgumentException("maxAttempts cannot exceed 10");
+      }
+      this.maxAttempts = maxAttempts;
+      return this;
     }
 
-    @Override
-    public boolean equals(Object o) {
-        if (this == o) return true;
-        if (o == null || getClass() != o.getClass()) return false;
-        RetryConfig that = (RetryConfig) o;
-        return enabled == that.enabled &&
-               maxAttempts == that.maxAttempts &&
-               Double.compare(that.multiplier, multiplier) == 0 &&
-               Objects.equals(initialDelay, that.initialDelay) &&
-               Objects.equals(maxDelay, that.maxDelay);
+    public Builder initialDelay(Duration initialDelay) {
+      if (initialDelay == null || initialDelay.isNegative()) {
+        throw new IllegalArgumentException("initialDelay must be non-negative");
+      }
+      this.initialDelay = initialDelay;
+      return this;
     }
 
-    @Override
-    public int hashCode() {
-        return Objects.hash(enabled, maxAttempts, initialDelay, maxDelay, multiplier);
+    public Builder maxDelay(Duration maxDelay) {
+      if (maxDelay == null || maxDelay.isNegative()) {
+        throw new IllegalArgumentException("maxDelay must be non-negative");
+      }
+      this.maxDelay = maxDelay;
+      return this;
     }
 
-    @Override
-    public String toString() {
-        return "RetryConfig{" +
-               "enabled=" + enabled +
-               ", maxAttempts=" + maxAttempts +
-               ", initialDelay=" + initialDelay +
-               ", maxDelay=" + maxDelay +
-               ", multiplier=" + multiplier +
-               '}';
+    public Builder multiplier(double multiplier) {
+      if (multiplier < 1.0) {
+        throw new IllegalArgumentException("multiplier must be at least 1.0");
+      }
+      this.multiplier = multiplier;
+      return this;
     }
 
-    /**
-     * Builder for RetryConfig.
-     */
-    public static final class Builder {
-        private boolean enabled = true;
-        private int maxAttempts = DEFAULT_MAX_ATTEMPTS;
-        private Duration initialDelay = DEFAULT_INITIAL_DELAY;
-        private Duration maxDelay = DEFAULT_MAX_DELAY;
-        private double multiplier = DEFAULT_MULTIPLIER;
-
-        private Builder() {}
-
-        public Builder enabled(boolean enabled) {
-            this.enabled = enabled;
-            return this;
-        }
-
-        public Builder maxAttempts(int maxAttempts) {
-            if (maxAttempts < 1) {
-                throw new IllegalArgumentException("maxAttempts must be at least 1");
-            }
-            if (maxAttempts > 10) {
-                throw new IllegalArgumentException("maxAttempts cannot exceed 10");
-            }
-            this.maxAttempts = maxAttempts;
-            return this;
-        }
-
-        public Builder initialDelay(Duration initialDelay) {
-            if (initialDelay == null || initialDelay.isNegative()) {
-                throw new IllegalArgumentException("initialDelay must be non-negative");
-            }
-            this.initialDelay = initialDelay;
-            return this;
-        }
-
-        public Builder maxDelay(Duration maxDelay) {
-            if (maxDelay == null || maxDelay.isNegative()) {
-                throw new IllegalArgumentException("maxDelay must be non-negative");
-            }
-            this.maxDelay = maxDelay;
-            return this;
-        }
-
-        public Builder multiplier(double multiplier) {
-            if (multiplier < 1.0) {
-                throw new IllegalArgumentException("multiplier must be at least 1.0");
-            }
-            this.multiplier = multiplier;
-            return this;
-        }
-
-        public RetryConfig build() {
-            return new RetryConfig(this);
-        }
+    public RetryConfig build() {
+      return new RetryConfig(this);
     }
+  }
 }
diff --git a/src/main/java/com/getaxonflow/sdk/util/RetryExecutor.java b/src/main/java/com/getaxonflow/sdk/util/RetryExecutor.java
index 363523a..3bb4c25 100644
--- a/src/main/java/com/getaxonflow/sdk/util/RetryExecutor.java
+++ b/src/main/java/com/getaxonflow/sdk/util/RetryExecutor.java
@@ -16,153 +16,157 @@
 package com.getaxonflow.sdk.util;
 
 import com.getaxonflow.sdk.exceptions.*;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
 import java.io.IOException;
 import java.net.SocketTimeoutException;
 import java.time.Duration;
 import java.util.concurrent.Callable;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
-/**
- * Executes operations with retry logic and exponential backoff.
- */
+/** Executes operations with retry logic and exponential backoff. */
 public final class RetryExecutor {
 
-    private static final Logger logger = LoggerFactory.getLogger(RetryExecutor.class);
-
-    private final RetryConfig config;
-
-    /**
-     * Creates a new retry executor.
-     *
-     * @param config the retry configuration
-     */
-    public RetryExecutor(RetryConfig config) {
-        this.config = config != null ? config : RetryConfig.defaults();
+  private static final Logger logger = LoggerFactory.getLogger(RetryExecutor.class);
+
+  private final RetryConfig config;
+
+  /**
+   * Creates a new retry executor.
+   *
+   * @param config the retry configuration
+   */
+  public RetryExecutor(RetryConfig config) {
+    this.config = config != null ? config : RetryConfig.defaults();
+  }
+
+  /**
+   * Executes an operation with retry logic.
+   *
+   * @param  the return type
+   * @param operation the operation to execute
+   * @param context a description of the operation for logging
+   * @return the operation result
+   * @throws AxonFlowException if all retries fail
+   */
+  public  T execute(Callable operation, String context) throws AxonFlowException {
+    if (!config.isEnabled()) {
+      return executeOnce(operation, context);
     }
 
-    /**
-     * Executes an operation with retry logic.
-     *
-     * @param        the return type
-     * @param operation the operation to execute
-     * @param context   a description of the operation for logging
-     * @return the operation result
-     * @throws AxonFlowException if all retries fail
-     */
-    public  T execute(Callable operation, String context) throws AxonFlowException {
-        if (!config.isEnabled()) {
-            return executeOnce(operation, context);
-        }
+    Exception lastException = null;
+    for (int attempt = 1; attempt <= config.getMaxAttempts(); attempt++) {
+      try {
+        return operation.call();
+      } catch (Exception e) {
+        lastException = e;
 
-        Exception lastException = null;
-        for (int attempt = 1; attempt <= config.getMaxAttempts(); attempt++) {
-            try {
-                return operation.call();
-            } catch (Exception e) {
-                lastException = e;
-
-                if (!isRetryable(e)) {
-                    throw wrapException(e, context);
-                }
-
-                if (attempt < config.getMaxAttempts()) {
-                    Duration delay = config.getDelayForAttempt(attempt);
-                    logger.warn("Attempt {}/{} failed for {}, retrying in {}ms: {}",
-                        attempt, config.getMaxAttempts(), context, delay.toMillis(), e.getMessage());
-                    sleep(delay);
-                } else {
-                    logger.error("All {} attempts failed for {}", config.getMaxAttempts(), context);
-                }
-            }
+        if (!isRetryable(e)) {
+          throw wrapException(e, context);
         }
 
-        throw wrapException(lastException, context);
-    }
-
-    private  T executeOnce(Callable operation, String context) throws AxonFlowException {
-        try {
-            return operation.call();
-        } catch (Exception e) {
-            throw wrapException(e, context);
+        if (attempt < config.getMaxAttempts()) {
+          Duration delay = config.getDelayForAttempt(attempt);
+          logger.warn(
+              "Attempt {}/{} failed for {}, retrying in {}ms: {}",
+              attempt,
+              config.getMaxAttempts(),
+              context,
+              delay.toMillis(),
+              e.getMessage());
+          sleep(delay);
+        } else {
+          logger.error("All {} attempts failed for {}", config.getMaxAttempts(), context);
         }
+      }
     }
 
-    /**
-     * Determines if an exception is retryable.
-     *
-     * 

Retryable exceptions include: - *

    - *
  • Connection/network errors
  • - *
  • Timeouts
  • - *
  • Server errors (5xx)
  • - *
  • Rate limiting (429)
  • - *
- * - *

Non-retryable exceptions include: - *

    - *
  • Authentication errors (401, 403)
  • - *
  • Client errors (400, 404)
  • - *
  • Policy violations
  • - *
- * - * @param e the exception to check - * @return true if the operation should be retried - */ - private boolean isRetryable(Exception e) { - // Don't retry authentication or policy errors - if (e instanceof AuthenticationException || - e instanceof PolicyViolationException || - e instanceof ConfigurationException) { - return false; - } + throw wrapException(lastException, context); + } - // Retry connection and timeout errors - if (e instanceof ConnectionException || - e instanceof TimeoutException || - e instanceof SocketTimeoutException || - e instanceof IOException) { - return true; - } + private T executeOnce(Callable operation, String context) throws AxonFlowException { + try { + return operation.call(); + } catch (Exception e) { + throw wrapException(e, context); + } + } + + /** + * Determines if an exception is retryable. + * + *

Retryable exceptions include: + * + *

    + *
  • Connection/network errors + *
  • Timeouts + *
  • Server errors (5xx) + *
  • Rate limiting (429) + *
+ * + *

Non-retryable exceptions include: + * + *

    + *
  • Authentication errors (401, 403) + *
  • Client errors (400, 404) + *
  • Policy violations + *
+ * + * @param e the exception to check + * @return true if the operation should be retried + */ + private boolean isRetryable(Exception e) { + // Don't retry authentication or policy errors + if (e instanceof AuthenticationException + || e instanceof PolicyViolationException + || e instanceof ConfigurationException) { + return false; + } - // Retry rate limit errors - if (e instanceof RateLimitException) { - return true; - } + // Retry connection and timeout errors + if (e instanceof ConnectionException + || e instanceof TimeoutException + || e instanceof SocketTimeoutException + || e instanceof IOException) { + return true; + } - // Retry server errors (5xx) - if (e instanceof AxonFlowException) { - int statusCode = ((AxonFlowException) e).getStatusCode(); - return statusCode >= 500 && statusCode < 600; - } + // Retry rate limit errors + if (e instanceof RateLimitException) { + return true; + } - // Default to not retrying unknown errors - return false; + // Retry server errors (5xx) + if (e instanceof AxonFlowException) { + int statusCode = ((AxonFlowException) e).getStatusCode(); + return statusCode >= 500 && statusCode < 600; } - private AxonFlowException wrapException(Exception e, String context) { - if (e instanceof AxonFlowException) { - return (AxonFlowException) e; - } + // Default to not retrying unknown errors + return false; + } - if (e instanceof SocketTimeoutException) { - return new TimeoutException("Request timed out: " + context, e); - } + private AxonFlowException wrapException(Exception e, String context) { + if (e instanceof AxonFlowException) { + return (AxonFlowException) e; + } - if (e instanceof IOException) { - return new ConnectionException("Connection failed: " + context, e); - } + if (e instanceof SocketTimeoutException) { + return new TimeoutException("Request timed out: " + context, e); + } - return new AxonFlowException("Operation failed: " + context, e); + if (e instanceof IOException) { + return new ConnectionException("Connection failed: " + context, e); } - private void sleep(Duration duration) { - try { - Thread.sleep(duration.toMillis()); - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - throw new AxonFlowException("Retry interrupted", e); - } + return new AxonFlowException("Operation failed: " + context, e); + } + + private void sleep(Duration duration) { + try { + Thread.sleep(duration.toMillis()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new AxonFlowException("Retry interrupted", e); } + } } diff --git a/src/main/java/com/getaxonflow/sdk/util/package-info.java b/src/main/java/com/getaxonflow/sdk/util/package-info.java index 443ee35..85dfbdc 100644 --- a/src/main/java/com/getaxonflow/sdk/util/package-info.java +++ b/src/main/java/com/getaxonflow/sdk/util/package-info.java @@ -17,13 +17,13 @@ /** * Utility classes for the AxonFlow SDK. * - *

This package contains internal utilities for HTTP handling, - * caching, and retry logic. + *

This package contains internal utilities for HTTP handling, caching, and retry logic. * *

Configuration Classes

+ * *
    - *
  • {@link com.getaxonflow.sdk.util.RetryConfig} - Retry behavior configuration
  • - *
  • {@link com.getaxonflow.sdk.util.CacheConfig} - Response caching configuration
  • + *
  • {@link com.getaxonflow.sdk.util.RetryConfig} - Retry behavior configuration + *
  • {@link com.getaxonflow.sdk.util.CacheConfig} - Response caching configuration *
*/ package com.getaxonflow.sdk.util; diff --git a/src/test/java/com/getaxonflow/sdk/AuditReadTest.java b/src/test/java/com/getaxonflow/sdk/AuditReadTest.java index 433dbcf..fd8d72e 100644 --- a/src/test/java/com/getaxonflow/sdk/AuditReadTest.java +++ b/src/test/java/com/getaxonflow/sdk/AuditReadTest.java @@ -15,580 +15,639 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; - import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.concurrent.CompletableFuture; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; /** - * Tests for Audit Log Read Methods. - * Part of Issue #878 - Add audit log read capabilities to SDK. + * Tests for Audit Log Read Methods. Part of Issue #878 - Add audit log read capabilities to SDK. */ @WireMockTest @DisplayName("Audit Log Read Methods") class AuditReadTest { - private AxonFlow axonflow; - - private static final String SAMPLE_AUDIT_ENTRY_1 = - "{" + - "\"id\": \"audit-1\"," + - "\"request_id\": \"req-1\"," + - "\"timestamp\": \"2026-01-05T10:00:00Z\"," + - "\"user_email\": \"user@example.com\"," + - "\"client_id\": \"client-1\"," + - "\"tenant_id\": \"tenant-1\"," + - "\"request_type\": \"llm_chat\"," + - "\"query_summary\": \"Test query\"," + - "\"success\": true," + - "\"blocked\": false," + - "\"risk_score\": 0.1," + - "\"provider\": \"openai\"," + - "\"model\": \"gpt-4\"," + - "\"tokens_used\": 150," + - "\"latency_ms\": 250," + - "\"policy_violations\": []," + - "\"metadata\": {}" + - "}"; - - private static final String SAMPLE_AUDIT_ENTRY_2 = - "{" + - "\"id\": \"audit-2\"," + - "\"request_id\": \"req-2\"," + - "\"timestamp\": \"2026-01-05T11:00:00Z\"," + - "\"user_email\": \"user@example.com\"," + - "\"client_id\": \"client-1\"," + - "\"tenant_id\": \"tenant-1\"," + - "\"request_type\": \"llm_chat\"," + - "\"query_summary\": \"Blocked query\"," + - "\"success\": false," + - "\"blocked\": true," + - "\"risk_score\": 0.9," + - "\"provider\": \"openai\"," + - "\"model\": \"gpt-4\"," + - "\"tokens_used\": 0," + - "\"latency_ms\": 50," + - "\"policy_violations\": [\"policy-1\"]," + - "\"metadata\": {\"reason\": \"pii_detected\"}" + - "}"; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .build()); + private AxonFlow axonflow; + + private static final String SAMPLE_AUDIT_ENTRY_1 = + "{" + + "\"id\": \"audit-1\"," + + "\"request_id\": \"req-1\"," + + "\"timestamp\": \"2026-01-05T10:00:00Z\"," + + "\"user_email\": \"user@example.com\"," + + "\"client_id\": \"client-1\"," + + "\"tenant_id\": \"tenant-1\"," + + "\"request_type\": \"llm_chat\"," + + "\"query_summary\": \"Test query\"," + + "\"success\": true," + + "\"blocked\": false," + + "\"risk_score\": 0.1," + + "\"provider\": \"openai\"," + + "\"model\": \"gpt-4\"," + + "\"tokens_used\": 150," + + "\"latency_ms\": 250," + + "\"policy_violations\": []," + + "\"metadata\": {}" + + "}"; + + private static final String SAMPLE_AUDIT_ENTRY_2 = + "{" + + "\"id\": \"audit-2\"," + + "\"request_id\": \"req-2\"," + + "\"timestamp\": \"2026-01-05T11:00:00Z\"," + + "\"user_email\": \"user@example.com\"," + + "\"client_id\": \"client-1\"," + + "\"tenant_id\": \"tenant-1\"," + + "\"request_type\": \"llm_chat\"," + + "\"query_summary\": \"Blocked query\"," + + "\"success\": false," + + "\"blocked\": true," + + "\"risk_score\": 0.9," + + "\"provider\": \"openai\"," + + "\"model\": \"gpt-4\"," + + "\"tokens_used\": 0," + + "\"latency_ms\": 50," + + "\"policy_violations\": [\"policy-1\"]," + + "\"metadata\": {\"reason\": \"pii_detected\"}" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // searchAuditLogs Tests + // ======================================================================== + + @Nested + @DisplayName("searchAuditLogs") + class SearchAuditLogs { + + @Test + @DisplayName("should search audit logs with all filters") + void searchAuditLogsWithAllFilters() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "," + SAMPLE_AUDIT_ENTRY_2 + "]"))); + + AuditSearchRequest request = + AuditSearchRequest.builder() + .userEmail("user@example.com") + .clientId("client-1") + .startTime(Instant.now().minus(7, ChronoUnit.DAYS)) + .endTime(Instant.now()) + .requestType("llm_chat") + .limit(50) + .offset(10) + .build(); + + AuditSearchResponse response = axonflow.searchAuditLogs(request); + + assertThat(response.getEntries()).hasSize(2); + assertThat(response.getEntries().get(0).getId()).isEqualTo("audit-1"); + assertThat(response.getEntries().get(1).isBlocked()).isTrue(); + + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/search")) + .withRequestBody(containing("\"user_email\":\"user@example.com\"")) + .withRequestBody(containing("\"client_id\":\"client-1\"")) + .withRequestBody(containing("\"request_type\":\"llm_chat\"")) + .withRequestBody(containing("\"limit\":50")) + .withRequestBody(containing("\"offset\":10"))); + } + + @Test + @DisplayName("should use default limit when not specified") + void searchAuditLogsWithDefaultLimit() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.getLimit()).isEqualTo(100); + + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/search")) + .withRequestBody(containing("\"limit\":100"))); } - // ======================================================================== - // searchAuditLogs Tests - // ======================================================================== - - @Nested - @DisplayName("searchAuditLogs") - class SearchAuditLogs { - - @Test - @DisplayName("should search audit logs with all filters") - void searchAuditLogsWithAllFilters() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "," + SAMPLE_AUDIT_ENTRY_2 + "]"))); - - AuditSearchRequest request = AuditSearchRequest.builder() - .userEmail("user@example.com") - .clientId("client-1") - .startTime(Instant.now().minus(7, ChronoUnit.DAYS)) - .endTime(Instant.now()) - .requestType("llm_chat") - .limit(50) - .offset(10) - .build(); - - AuditSearchResponse response = axonflow.searchAuditLogs(request); - - assertThat(response.getEntries()).hasSize(2); - assertThat(response.getEntries().get(0).getId()).isEqualTo("audit-1"); - assertThat(response.getEntries().get(1).isBlocked()).isTrue(); - - verify(postRequestedFor(urlEqualTo("/api/v1/audit/search")) - .withRequestBody(containing("\"user_email\":\"user@example.com\"")) - .withRequestBody(containing("\"client_id\":\"client-1\"")) - .withRequestBody(containing("\"request_type\":\"llm_chat\"")) - .withRequestBody(containing("\"limit\":50")) - .withRequestBody(containing("\"offset\":10"))); - } - - @Test - @DisplayName("should use default limit when not specified") - void searchAuditLogsWithDefaultLimit() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.getLimit()).isEqualTo(100); - - verify(postRequestedFor(urlEqualTo("/api/v1/audit/search")) - .withRequestBody(containing("\"limit\":100"))); - } - - @Test - @DisplayName("should cap limit at 1000") - void searchAuditLogsWithCapLimit() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - AuditSearchRequest request = AuditSearchRequest.builder() - .limit(5000) - .build(); - - axonflow.searchAuditLogs(request); - - verify(postRequestedFor(urlEqualTo("/api/v1/audit/search")) - .withRequestBody(containing("\"limit\":1000"))); - } - - @Test - @DisplayName("should handle empty results") - void searchAuditLogsWithEmptyResults() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.getEntries()).isEmpty(); - assertThat(response.getTotal()).isZero(); - } - - @Test - @DisplayName("should handle wrapped response format") - void searchAuditLogsWithWrappedResponse() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"entries\": [" + SAMPLE_AUDIT_ENTRY_1 + "], \"total\": 100, \"limit\": 10, \"offset\": 0}"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.getEntries()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(100); - assertThat(response.getLimit()).isEqualTo(10); - } - - @Test - @DisplayName("should throw on 400 error") - void searchAuditLogsWithBadRequest() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(400) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"invalid request\"}"))); - - assertThatThrownBy(() -> axonflow.searchAuditLogs()) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("should throw on 401 error") - void searchAuditLogsWithUnauthorized() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(401) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"unauthorized\"}"))); - - assertThatThrownBy(() -> axonflow.searchAuditLogs()) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("should throw on 500 error") - void searchAuditLogsWithServerError() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"server error\"}"))); - - assertThatThrownBy(() -> axonflow.searchAuditLogs()) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("should parse dates correctly") - void searchAuditLogsWithDateParsing() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.getEntries().get(0).getTimestamp()).isNotNull(); - assertThat(response.getEntries().get(0).getTimestamp().toString()).startsWith("2026-01-05"); - } - - @Test - @DisplayName("should include offset in request when > 0") - void searchAuditLogsWithOffset() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - AuditSearchRequest request = AuditSearchRequest.builder() - .offset(50) - .build(); - - axonflow.searchAuditLogs(request); - - verify(postRequestedFor(urlEqualTo("/api/v1/audit/search")) - .withRequestBody(containing("\"offset\":50"))); - } - - @Test - @DisplayName("should parse policy violations correctly") - void searchAuditLogsWithPolicyViolations() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_2 + "]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.getEntries().get(0).getPolicyViolations()).containsExactly("policy-1"); - } - - @Test - @DisplayName("async should complete successfully") - void searchAuditLogsAsync() throws Exception { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - CompletableFuture future = axonflow.searchAuditLogsAsync(null); - AuditSearchResponse response = future.get(); - - assertThat(response.getEntries()).hasSize(1); - } + @Test + @DisplayName("should cap limit at 1000") + void searchAuditLogsWithCapLimit() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + AuditSearchRequest request = AuditSearchRequest.builder().limit(5000).build(); + + axonflow.searchAuditLogs(request); + + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/search")) + .withRequestBody(containing("\"limit\":1000"))); + } + + @Test + @DisplayName("should handle empty results") + void searchAuditLogsWithEmptyResults() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.getEntries()).isEmpty(); + assertThat(response.getTotal()).isZero(); + } + + @Test + @DisplayName("should handle wrapped response format") + void searchAuditLogsWithWrappedResponse() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"entries\": [" + + SAMPLE_AUDIT_ENTRY_1 + + "], \"total\": 100, \"limit\": 10, \"offset\": 0}"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.getEntries()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(100); + assertThat(response.getLimit()).isEqualTo(10); + } + + @Test + @DisplayName("should throw on 400 error") + void searchAuditLogsWithBadRequest() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(400) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"invalid request\"}"))); + + assertThatThrownBy(() -> axonflow.searchAuditLogs()).isInstanceOf(Exception.class); + } + + @Test + @DisplayName("should throw on 401 error") + void searchAuditLogsWithUnauthorized() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(401) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"unauthorized\"}"))); + + assertThatThrownBy(() -> axonflow.searchAuditLogs()).isInstanceOf(Exception.class); + } + + @Test + @DisplayName("should throw on 500 error") + void searchAuditLogsWithServerError() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"server error\"}"))); + + assertThatThrownBy(() -> axonflow.searchAuditLogs()).isInstanceOf(Exception.class); + } + + @Test + @DisplayName("should parse dates correctly") + void searchAuditLogsWithDateParsing() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.getEntries().get(0).getTimestamp()).isNotNull(); + assertThat(response.getEntries().get(0).getTimestamp().toString()).startsWith("2026-01-05"); + } + + @Test + @DisplayName("should include offset in request when > 0") + void searchAuditLogsWithOffset() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + AuditSearchRequest request = AuditSearchRequest.builder().offset(50).build(); + + axonflow.searchAuditLogs(request); + + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/search")) + .withRequestBody(containing("\"offset\":50"))); } - // ======================================================================== - // getAuditLogsByTenant Tests - // ======================================================================== - - @Nested - @DisplayName("getAuditLogsByTenant") - class GetAuditLogsByTenant { - - @Test - @DisplayName("should get audit logs for tenant with defaults") - void getAuditLogsByTenantWithDefaults() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .withQueryParam("limit", equalTo("50")) - .withQueryParam("offset", equalTo("0")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "," + SAMPLE_AUDIT_ENTRY_2 + "]"))); - - AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); - - assertThat(response.getEntries()).hasSize(2); - assertThat(response.getLimit()).isEqualTo(50); - } - - @Test - @DisplayName("should get audit logs with custom options") - void getAuditLogsByTenantWithCustomOptions() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .withQueryParam("limit", equalTo("100")) - .withQueryParam("offset", equalTo("25")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - AuditQueryOptions options = AuditQueryOptions.builder() - .limit(100) - .offset(25) - .build(); - - AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc", options); - - assertThat(response.getLimit()).isEqualTo(100); - assertThat(response.getOffset()).isEqualTo(25); - } - - @Test - @DisplayName("should throw error for empty tenant ID") - void getAuditLogsByTenantWithEmptyId() { - assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("tenantId is required"); - } - - @Test - @DisplayName("should throw error for null tenant ID") - void getAuditLogsByTenantWithNullId() { - assertThatThrownBy(() -> axonflow.getAuditLogsByTenant(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessage("tenantId is required"); - } - - @Test - @DisplayName("should cap limit at 1000") - void getAuditLogsByTenantWithCapLimit() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .withQueryParam("limit", equalTo("1000")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - AuditQueryOptions options = AuditQueryOptions.builder() - .limit(5000) - .build(); - - axonflow.getAuditLogsByTenant("tenant-abc", options); - - verify(getRequestedFor(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .withQueryParam("limit", equalTo("1000"))); - } - - @Test - @DisplayName("should handle empty results") - void getAuditLogsByTenantWithEmptyResults() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); - - assertThat(response.getEntries()).isEmpty(); - } - - @Test - @DisplayName("should handle wrapped response format") - void getAuditLogsByTenantWithWrappedResponse() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"entries\": [" + SAMPLE_AUDIT_ENTRY_1 + "], \"total\": 50, \"limit\": 50, \"offset\": 0}"))); - - AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); - - assertThat(response.getTotal()).isEqualTo(50); - } - - @Test - @DisplayName("should throw on 404 error") - void getAuditLogsByTenantWithNotFound() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/nonexistent")) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"tenant not found\"}"))); - - assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("nonexistent")) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("should throw on 403 error") - void getAuditLogsByTenantWithForbidden() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/other-tenant")) - .willReturn(aResponse() - .withStatus(403) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"forbidden\"}"))); - - assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("other-tenant")) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("should URL encode tenant ID") - void getAuditLogsByTenantWithUrlEncoding() { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant%2Fwith%2Fslashes")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - axonflow.getAuditLogsByTenant("tenant/with/slashes"); - - verify(getRequestedFor(urlPathEqualTo("/api/v1/audit/tenant/tenant%2Fwith%2Fslashes"))); - } - - @Test - @DisplayName("async should complete successfully") - void getAuditLogsByTenantAsync() throws Exception { - stubFor(get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - CompletableFuture future = axonflow.getAuditLogsByTenantAsync("tenant-abc", null); - AuditSearchResponse response = future.get(); - - assertThat(response.getEntries()).hasSize(1); - } + @Test + @DisplayName("should parse policy violations correctly") + void searchAuditLogsWithPolicyViolations() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_2 + "]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.getEntries().get(0).getPolicyViolations()).containsExactly("policy-1"); + } + + @Test + @DisplayName("async should complete successfully") + void searchAuditLogsAsync() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + CompletableFuture future = axonflow.searchAuditLogsAsync(null); + AuditSearchResponse response = future.get(); + + assertThat(response.getEntries()).hasSize(1); + } + } + + // ======================================================================== + // getAuditLogsByTenant Tests + // ======================================================================== + + @Nested + @DisplayName("getAuditLogsByTenant") + class GetAuditLogsByTenant { + + @Test + @DisplayName("should get audit logs for tenant with defaults") + void getAuditLogsByTenantWithDefaults() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .withQueryParam("limit", equalTo("50")) + .withQueryParam("offset", equalTo("0")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "," + SAMPLE_AUDIT_ENTRY_2 + "]"))); + + AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); + + assertThat(response.getEntries()).hasSize(2); + assertThat(response.getLimit()).isEqualTo(50); + } + + @Test + @DisplayName("should get audit logs with custom options") + void getAuditLogsByTenantWithCustomOptions() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .withQueryParam("limit", equalTo("100")) + .withQueryParam("offset", equalTo("25")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + AuditQueryOptions options = AuditQueryOptions.builder().limit(100).offset(25).build(); + + AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc", options); + + assertThat(response.getLimit()).isEqualTo(100); + assertThat(response.getOffset()).isEqualTo(25); + } + + @Test + @DisplayName("should throw error for empty tenant ID") + void getAuditLogsByTenantWithEmptyId() { + assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tenantId is required"); + } + + @Test + @DisplayName("should throw error for null tenant ID") + void getAuditLogsByTenantWithNullId() { + assertThatThrownBy(() -> axonflow.getAuditLogsByTenant(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessage("tenantId is required"); + } + + @Test + @DisplayName("should cap limit at 1000") + void getAuditLogsByTenantWithCapLimit() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .withQueryParam("limit", equalTo("1000")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + AuditQueryOptions options = AuditQueryOptions.builder().limit(5000).build(); + + axonflow.getAuditLogsByTenant("tenant-abc", options); + + verify( + getRequestedFor(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .withQueryParam("limit", equalTo("1000"))); + } + + @Test + @DisplayName("should handle empty results") + void getAuditLogsByTenantWithEmptyResults() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); + + assertThat(response.getEntries()).isEmpty(); + } + + @Test + @DisplayName("should handle wrapped response format") + void getAuditLogsByTenantWithWrappedResponse() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"entries\": [" + + SAMPLE_AUDIT_ENTRY_1 + + "], \"total\": 50, \"limit\": 50, \"offset\": 0}"))); + + AuditSearchResponse response = axonflow.getAuditLogsByTenant("tenant-abc"); + + assertThat(response.getTotal()).isEqualTo(50); + } + + @Test + @DisplayName("should throw on 404 error") + void getAuditLogsByTenantWithNotFound() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/nonexistent")) + .willReturn( + aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"tenant not found\"}"))); + + assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("nonexistent")) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("should throw on 403 error") + void getAuditLogsByTenantWithForbidden() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/other-tenant")) + .willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"forbidden\"}"))); + + assertThatThrownBy(() -> axonflow.getAuditLogsByTenant("other-tenant")) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("should URL encode tenant ID") + void getAuditLogsByTenantWithUrlEncoding() { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant%2Fwith%2Fslashes")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + axonflow.getAuditLogsByTenant("tenant/with/slashes"); + + verify(getRequestedFor(urlPathEqualTo("/api/v1/audit/tenant/tenant%2Fwith%2Fslashes"))); + } + + @Test + @DisplayName("async should complete successfully") + void getAuditLogsByTenantAsync() throws Exception { + stubFor( + get(urlPathEqualTo("/api/v1/audit/tenant/tenant-abc")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + CompletableFuture future = + axonflow.getAuditLogsByTenantAsync("tenant-abc", null); + AuditSearchResponse response = future.get(); + + assertThat(response.getEntries()).hasSize(1); + } + } + + // ======================================================================== + // Type Validation Tests + // ======================================================================== + + @Nested + @DisplayName("Type Validation") + class TypeValidation { + + @Test + @DisplayName("should parse all AuditLogEntry fields correctly") + void parseAllAuditLogEntryFields() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + AuditLogEntry entry = response.getEntries().get(0); + + assertThat(entry.getId()).isEqualTo("audit-1"); + assertThat(entry.getRequestId()).isEqualTo("req-1"); + assertThat(entry.getUserEmail()).isEqualTo("user@example.com"); + assertThat(entry.getClientId()).isEqualTo("client-1"); + assertThat(entry.getTenantId()).isEqualTo("tenant-1"); + assertThat(entry.getRequestType()).isEqualTo("llm_chat"); + assertThat(entry.getQuerySummary()).isEqualTo("Test query"); + assertThat(entry.isSuccess()).isTrue(); + assertThat(entry.isBlocked()).isFalse(); + assertThat(entry.getRiskScore()).isEqualTo(0.1); + assertThat(entry.getProvider()).isEqualTo("openai"); + assertThat(entry.getModel()).isEqualTo("gpt-4"); + assertThat(entry.getTokensUsed()).isEqualTo(150); + assertThat(entry.getLatencyMs()).isEqualTo(250); + assertThat(entry.getPolicyViolations()).isEmpty(); + assertThat(entry.getMetadata()).isEmpty(); + } + + @Test + @DisplayName("should handle missing optional fields with defaults") + void handleMissingOptionalFields() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "[{\"id\": \"audit-minimal\", \"timestamp\": \"2026-01-05T10:00:00Z\"}]"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + AuditLogEntry entry = response.getEntries().get(0); + + assertThat(entry.getId()).isEqualTo("audit-minimal"); + assertThat(entry.getRequestId()).isEmpty(); + assertThat(entry.getUserEmail()).isEmpty(); + assertThat(entry.isSuccess()).isTrue(); + assertThat(entry.isBlocked()).isFalse(); + assertThat(entry.getRiskScore()).isZero(); + assertThat(entry.getTokensUsed()).isZero(); + assertThat(entry.getPolicyViolations()).isEmpty(); + assertThat(entry.getMetadata()).isEmpty(); + } + + @Test + @DisplayName("AuditSearchResponse hasMore should work correctly") + void auditSearchResponseHasMore() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"entries\": [" + + SAMPLE_AUDIT_ENTRY_1 + + "], \"total\": 100, \"limit\": 10, \"offset\": 0}"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.hasMore()).isTrue(); + } + + @Test + @DisplayName("AuditSearchResponse hasMore should return false when no more results") + void auditSearchResponseHasMoreFalse() { + stubFor( + post(urlEqualTo("/api/v1/audit/search")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"entries\": [" + + SAMPLE_AUDIT_ENTRY_1 + + "], \"total\": 1, \"limit\": 10, \"offset\": 0}"))); + + AuditSearchResponse response = axonflow.searchAuditLogs(); + + assertThat(response.hasMore()).isFalse(); + } + + @Test + @DisplayName("AuditQueryOptions defaults should be correct") + void auditQueryOptionsDefaults() { + AuditQueryOptions options = AuditQueryOptions.defaults(); + + assertThat(options.getLimit()).isEqualTo(50); + assertThat(options.getOffset()).isZero(); } - // ======================================================================== - // Type Validation Tests - // ======================================================================== - - @Nested - @DisplayName("Type Validation") - class TypeValidation { - - @Test - @DisplayName("should parse all AuditLogEntry fields correctly") - void parseAllAuditLogEntryFields() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" + SAMPLE_AUDIT_ENTRY_1 + "]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - AuditLogEntry entry = response.getEntries().get(0); - - assertThat(entry.getId()).isEqualTo("audit-1"); - assertThat(entry.getRequestId()).isEqualTo("req-1"); - assertThat(entry.getUserEmail()).isEqualTo("user@example.com"); - assertThat(entry.getClientId()).isEqualTo("client-1"); - assertThat(entry.getTenantId()).isEqualTo("tenant-1"); - assertThat(entry.getRequestType()).isEqualTo("llm_chat"); - assertThat(entry.getQuerySummary()).isEqualTo("Test query"); - assertThat(entry.isSuccess()).isTrue(); - assertThat(entry.isBlocked()).isFalse(); - assertThat(entry.getRiskScore()).isEqualTo(0.1); - assertThat(entry.getProvider()).isEqualTo("openai"); - assertThat(entry.getModel()).isEqualTo("gpt-4"); - assertThat(entry.getTokensUsed()).isEqualTo(150); - assertThat(entry.getLatencyMs()).isEqualTo(250); - assertThat(entry.getPolicyViolations()).isEmpty(); - assertThat(entry.getMetadata()).isEmpty(); - } - - @Test - @DisplayName("should handle missing optional fields with defaults") - void handleMissingOptionalFields() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[{\"id\": \"audit-minimal\", \"timestamp\": \"2026-01-05T10:00:00Z\"}]"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - AuditLogEntry entry = response.getEntries().get(0); - - assertThat(entry.getId()).isEqualTo("audit-minimal"); - assertThat(entry.getRequestId()).isEmpty(); - assertThat(entry.getUserEmail()).isEmpty(); - assertThat(entry.isSuccess()).isTrue(); - assertThat(entry.isBlocked()).isFalse(); - assertThat(entry.getRiskScore()).isZero(); - assertThat(entry.getTokensUsed()).isZero(); - assertThat(entry.getPolicyViolations()).isEmpty(); - assertThat(entry.getMetadata()).isEmpty(); - } - - @Test - @DisplayName("AuditSearchResponse hasMore should work correctly") - void auditSearchResponseHasMore() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"entries\": [" + SAMPLE_AUDIT_ENTRY_1 + "], \"total\": 100, \"limit\": 10, \"offset\": 0}"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.hasMore()).isTrue(); - } - - @Test - @DisplayName("AuditSearchResponse hasMore should return false when no more results") - void auditSearchResponseHasMoreFalse() { - stubFor(post(urlEqualTo("/api/v1/audit/search")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"entries\": [" + SAMPLE_AUDIT_ENTRY_1 + "], \"total\": 1, \"limit\": 10, \"offset\": 0}"))); - - AuditSearchResponse response = axonflow.searchAuditLogs(); - - assertThat(response.hasMore()).isFalse(); - } - - @Test - @DisplayName("AuditQueryOptions defaults should be correct") - void auditQueryOptionsDefaults() { - AuditQueryOptions options = AuditQueryOptions.defaults(); - - assertThat(options.getLimit()).isEqualTo(50); - assertThat(options.getOffset()).isZero(); - } - - @Test - @DisplayName("AuditSearchRequest builder should set all fields") - void auditSearchRequestBuilder() { - Instant start = Instant.now().minus(1, ChronoUnit.DAYS); - Instant end = Instant.now(); - - AuditSearchRequest request = AuditSearchRequest.builder() - .userEmail("test@example.com") - .clientId("client-123") - .startTime(start) - .endTime(end) - .requestType("llm_chat") - .limit(50) - .offset(10) - .build(); - - assertThat(request.getUserEmail()).isEqualTo("test@example.com"); - assertThat(request.getClientId()).isEqualTo("client-123"); - assertThat(request.getStartTime()).isEqualTo(start.toString()); - assertThat(request.getEndTime()).isEqualTo(end.toString()); - assertThat(request.getRequestType()).isEqualTo("llm_chat"); - assertThat(request.getLimit()).isEqualTo(50); - assertThat(request.getOffset()).isEqualTo(10); - } + @Test + @DisplayName("AuditSearchRequest builder should set all fields") + void auditSearchRequestBuilder() { + Instant start = Instant.now().minus(1, ChronoUnit.DAYS); + Instant end = Instant.now(); + + AuditSearchRequest request = + AuditSearchRequest.builder() + .userEmail("test@example.com") + .clientId("client-123") + .startTime(start) + .endTime(end) + .requestType("llm_chat") + .limit(50) + .offset(10) + .build(); + + assertThat(request.getUserEmail()).isEqualTo("test@example.com"); + assertThat(request.getClientId()).isEqualTo("client-123"); + assertThat(request.getStartTime()).isEqualTo(start.toString()); + assertThat(request.getEndTime()).isEqualTo(end.toString()); + assertThat(request.getRequestType()).isEqualTo("llm_chat"); + assertThat(request.getLimit()).isEqualTo(50); + assertThat(request.getOffset()).isEqualTo(10); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/AuditToolCallTest.java b/src/test/java/com/getaxonflow/sdk/AuditToolCallTest.java index 5dd2e10..8632364 100644 --- a/src/test/java/com/getaxonflow/sdk/AuditToolCallTest.java +++ b/src/test/java/com/getaxonflow/sdk/AuditToolCallTest.java @@ -15,58 +15,61 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.exceptions.AxonFlowException; import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.util.Arrays; import java.util.HashMap; -import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for auditToolCall method. - */ +/** Tests for auditToolCall method. */ @WireMockTest @DisplayName("Audit Tool Call") class AuditToolCallTest { - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - @Test - @DisplayName("should audit tool call with all fields") - void shouldAuditToolCallWithAllFields() { - stubFor(post(urlEqualTo("/api/v1/audit/tool-call")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"audit_id\":\"aud_tc_001\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:00:00Z\"}"))); - - Map input = new HashMap<>(); - input.put("query", "latest news"); - input.put("limit", 10); - - Map output = new HashMap<>(); - output.put("results", 5); - output.put("source", "web"); - - AuditToolCallRequest request = AuditToolCallRequest.builder() + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + } + + @Test + @DisplayName("should audit tool call with all fields") + void shouldAuditToolCallWithAllFields() { + stubFor( + post(urlEqualTo("/api/v1/audit/tool-call")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"audit_id\":\"aud_tc_001\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:00:00Z\"}"))); + + Map input = new HashMap<>(); + input.put("query", "latest news"); + input.put("limit", 10); + + Map output = new HashMap<>(); + output.put("results", 5); + output.put("source", "web"); + + AuditToolCallRequest request = + AuditToolCallRequest.builder() .toolName("web_search") .toolType("function") .input(input) @@ -80,14 +83,15 @@ void shouldAuditToolCallWithAllFields() { .errorMessage(null) .build(); - AuditToolCallResponse response = axonflow.auditToolCall(request); + AuditToolCallResponse response = axonflow.auditToolCall(request); - assertThat(response).isNotNull(); - assertThat(response.getAuditId()).isEqualTo("aud_tc_001"); - assertThat(response.getStatus()).isEqualTo("recorded"); - assertThat(response.getTimestamp()).isEqualTo("2026-03-14T12:00:00Z"); + assertThat(response).isNotNull(); + assertThat(response.getAuditId()).isEqualTo("aud_tc_001"); + assertThat(response.getStatus()).isEqualTo("recorded"); + assertThat(response.getTimestamp()).isEqualTo("2026-03-14T12:00:00Z"); - verify(postRequestedFor(urlEqualTo("/api/v1/audit/tool-call")) + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/tool-call")) .withRequestBody(matchingJsonPath("$.tool_name", equalTo("web_search"))) .withRequestBody(matchingJsonPath("$.tool_type", equalTo("function"))) .withRequestBody(matchingJsonPath("$.workflow_id", equalTo("wf_123"))) @@ -97,140 +101,146 @@ void shouldAuditToolCallWithAllFields() { .withRequestBody(matchingJsonPath("$.success", equalTo("true"))) .withRequestBody(matchingJsonPath("$.policies_applied[0]", equalTo("policy_a"))) .withRequestBody(matchingJsonPath("$.policies_applied[1]", equalTo("policy_b")))); - } - - @Test - @DisplayName("should audit tool call with required fields only") - void shouldAuditToolCallWithRequiredFieldsOnly() { - stubFor(post(urlEqualTo("/api/v1/audit/tool-call")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"audit_id\":\"aud_tc_002\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:01:00Z\"}"))); - - AuditToolCallRequest request = AuditToolCallRequest.builder() - .toolName("db_lookup") - .build(); - - AuditToolCallResponse response = axonflow.auditToolCall(request); - - assertThat(response).isNotNull(); - assertThat(response.getAuditId()).isEqualTo("aud_tc_002"); - assertThat(response.getStatus()).isEqualTo("recorded"); - - verify(postRequestedFor(urlEqualTo("/api/v1/audit/tool-call")) + } + + @Test + @DisplayName("should audit tool call with required fields only") + void shouldAuditToolCallWithRequiredFieldsOnly() { + stubFor( + post(urlEqualTo("/api/v1/audit/tool-call")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"audit_id\":\"aud_tc_002\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:01:00Z\"}"))); + + AuditToolCallRequest request = AuditToolCallRequest.builder().toolName("db_lookup").build(); + + AuditToolCallResponse response = axonflow.auditToolCall(request); + + assertThat(response).isNotNull(); + assertThat(response.getAuditId()).isEqualTo("aud_tc_002"); + assertThat(response.getStatus()).isEqualTo("recorded"); + + verify( + postRequestedFor(urlEqualTo("/api/v1/audit/tool-call")) .withRequestBody(matchingJsonPath("$.tool_name", equalTo("db_lookup")))); - } - - @Test - @DisplayName("should reject null request") - void shouldRejectNullRequest() { - assertThatThrownBy(() -> axonflow.auditToolCall(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("request cannot be null"); - } - - @Test - @DisplayName("should reject null tool name") - void shouldRejectNullToolName() { - assertThatThrownBy(() -> AuditToolCallRequest.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("toolName cannot be null"); - } - - @Test - @DisplayName("should reject empty tool name") - void shouldRejectEmptyToolName() { - assertThatThrownBy(() -> AuditToolCallRequest.builder().toolName("").build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("toolName cannot be empty"); - } - - @Test - @DisplayName("should handle server error") - void shouldHandleServerError() { - stubFor(post(urlEqualTo("/api/v1/audit/tool-call")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - AuditToolCallRequest request = AuditToolCallRequest.builder() - .toolName("failing_tool") - .build(); - - assertThatThrownBy(() -> axonflow.auditToolCall(request)) - .isInstanceOf(AxonFlowException.class); - } - - @Test - @DisplayName("auditToolCallAsync should return future") - void auditToolCallAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/audit/tool-call")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"audit_id\":\"aud_tc_async\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:02:00Z\"}"))); - - AuditToolCallRequest request = AuditToolCallRequest.builder() - .toolName("async_tool") - .toolType("mcp") + } + + @Test + @DisplayName("should reject null request") + void shouldRejectNullRequest() { + assertThatThrownBy(() -> axonflow.auditToolCall(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request cannot be null"); + } + + @Test + @DisplayName("should reject null tool name") + void shouldRejectNullToolName() { + assertThatThrownBy(() -> AuditToolCallRequest.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("toolName cannot be null"); + } + + @Test + @DisplayName("should reject empty tool name") + void shouldRejectEmptyToolName() { + assertThatThrownBy(() -> AuditToolCallRequest.builder().toolName("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("toolName cannot be empty"); + } + + @Test + @DisplayName("should handle server error") + void shouldHandleServerError() { + stubFor( + post(urlEqualTo("/api/v1/audit/tool-call")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + AuditToolCallRequest request = AuditToolCallRequest.builder().toolName("failing_tool").build(); + + assertThatThrownBy(() -> axonflow.auditToolCall(request)).isInstanceOf(AxonFlowException.class); + } + + @Test + @DisplayName("auditToolCallAsync should return future") + void auditToolCallAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/audit/tool-call")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"audit_id\":\"aud_tc_async\",\"status\":\"recorded\",\"timestamp\":\"2026-03-14T12:02:00Z\"}"))); + + AuditToolCallRequest request = + AuditToolCallRequest.builder().toolName("async_tool").toolType("mcp").build(); + + CompletableFuture future = axonflow.auditToolCallAsync(request); + AuditToolCallResponse response = future.get(); + + assertThat(response).isNotNull(); + assertThat(response.getAuditId()).isEqualTo("aud_tc_async"); + assertThat(response.getStatus()).isEqualTo("recorded"); + } + + @Test + @DisplayName("request should have correct equals and hashCode") + void requestShouldHaveCorrectEqualsAndHashCode() { + AuditToolCallRequest req1 = + AuditToolCallRequest.builder().toolName("tool_a").toolType("function").build(); + AuditToolCallRequest req2 = + AuditToolCallRequest.builder().toolName("tool_a").toolType("function").build(); + AuditToolCallRequest req3 = AuditToolCallRequest.builder().toolName("tool_b").build(); + + assertThat(req1).isEqualTo(req2); + assertThat(req1.hashCode()).isEqualTo(req2.hashCode()); + assertThat(req1).isNotEqualTo(req3); + } + + @Test + @DisplayName("response should have correct equals and hashCode") + void responseShouldHaveCorrectEqualsAndHashCode() { + AuditToolCallResponse res1 = new AuditToolCallResponse("id1", "ok", "2026-01-01T00:00:00Z"); + AuditToolCallResponse res2 = new AuditToolCallResponse("id1", "ok", "2026-01-01T00:00:00Z"); + AuditToolCallResponse res3 = new AuditToolCallResponse("id2", "ok", "2026-01-01T00:00:00Z"); + + assertThat(res1).isEqualTo(res2); + assertThat(res1.hashCode()).isEqualTo(res2.hashCode()); + assertThat(res1).isNotEqualTo(res3); + } + + @Test + @DisplayName("request toString should include key fields") + void requestToStringShouldIncludeKeyFields() { + AuditToolCallRequest request = + AuditToolCallRequest.builder() + .toolName("my_tool") + .toolType("api") + .workflowId("wf_1") .build(); - CompletableFuture future = axonflow.auditToolCallAsync(request); - AuditToolCallResponse response = future.get(); - - assertThat(response).isNotNull(); - assertThat(response.getAuditId()).isEqualTo("aud_tc_async"); - assertThat(response.getStatus()).isEqualTo("recorded"); - } - - @Test - @DisplayName("request should have correct equals and hashCode") - void requestShouldHaveCorrectEqualsAndHashCode() { - AuditToolCallRequest req1 = AuditToolCallRequest.builder() - .toolName("tool_a").toolType("function").build(); - AuditToolCallRequest req2 = AuditToolCallRequest.builder() - .toolName("tool_a").toolType("function").build(); - AuditToolCallRequest req3 = AuditToolCallRequest.builder() - .toolName("tool_b").build(); - - assertThat(req1).isEqualTo(req2); - assertThat(req1.hashCode()).isEqualTo(req2.hashCode()); - assertThat(req1).isNotEqualTo(req3); - } - - @Test - @DisplayName("response should have correct equals and hashCode") - void responseShouldHaveCorrectEqualsAndHashCode() { - AuditToolCallResponse res1 = new AuditToolCallResponse("id1", "ok", "2026-01-01T00:00:00Z"); - AuditToolCallResponse res2 = new AuditToolCallResponse("id1", "ok", "2026-01-01T00:00:00Z"); - AuditToolCallResponse res3 = new AuditToolCallResponse("id2", "ok", "2026-01-01T00:00:00Z"); - - assertThat(res1).isEqualTo(res2); - assertThat(res1.hashCode()).isEqualTo(res2.hashCode()); - assertThat(res1).isNotEqualTo(res3); - } - - @Test - @DisplayName("request toString should include key fields") - void requestToStringShouldIncludeKeyFields() { - AuditToolCallRequest request = AuditToolCallRequest.builder() - .toolName("my_tool").toolType("api").workflowId("wf_1").build(); - - String str = request.toString(); - assertThat(str).contains("my_tool"); - assertThat(str).contains("api"); - assertThat(str).contains("wf_1"); - } - - @Test - @DisplayName("response toString should include key fields") - void responseToStringShouldIncludeKeyFields() { - AuditToolCallResponse response = new AuditToolCallResponse("aud_1", "recorded", "2026-01-01T00:00:00Z"); - - String str = response.toString(); - assertThat(str).contains("aud_1"); - assertThat(str).contains("recorded"); - } + String str = request.toString(); + assertThat(str).contains("my_tool"); + assertThat(str).contains("api"); + assertThat(str).contains("wf_1"); + } + + @Test + @DisplayName("response toString should include key fields") + void responseToStringShouldIncludeKeyFields() { + AuditToolCallResponse response = + new AuditToolCallResponse("aud_1", "recorded", "2026-01-01T00:00:00Z"); + + String str = response.toString(); + assertThat(str).contains("aud_1"); + assertThat(str).contains("recorded"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/AxonFlowConfigTest.java b/src/test/java/com/getaxonflow/sdk/AxonFlowConfigTest.java index 07c4ef9..2ed2700 100644 --- a/src/test/java/com/getaxonflow/sdk/AxonFlowConfigTest.java +++ b/src/test/java/com/getaxonflow/sdk/AxonFlowConfigTest.java @@ -15,49 +15,41 @@ */ package com.getaxonflow.sdk; -import com.getaxonflow.sdk.exceptions.ConfigurationException; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.Mode; import com.getaxonflow.sdk.util.CacheConfig; import com.getaxonflow.sdk.util.RetryConfig; -import org.junit.jupiter.api.Test; +import java.time.Duration; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import java.time.Duration; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("AxonFlowConfig") class AxonFlowConfigTest { - @Test - @DisplayName("should create config with minimal localhost settings") - void shouldCreateMinimalLocalhostConfig() { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .build(); + @Test + @DisplayName("should create config with minimal localhost settings") + void shouldCreateMinimalLocalhostConfig() { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("http://localhost:8080").build(); - assertThat(config.getEndpoint()).isEqualTo("http://localhost:8080"); - assertThat(config.isLocalhost()).isTrue(); - assertThat(config.getMode()).isEqualTo(Mode.PRODUCTION); - assertThat(config.getTimeout()).isEqualTo(AxonFlowConfig.DEFAULT_TIMEOUT); - } - - @Test - @DisplayName("should create config with all settings") - void shouldCreateFullConfig() { - RetryConfig retryConfig = RetryConfig.builder() - .maxAttempts(5) - .initialDelay(Duration.ofMillis(500)) - .build(); + assertThat(config.getEndpoint()).isEqualTo("http://localhost:8080"); + assertThat(config.isLocalhost()).isTrue(); + assertThat(config.getMode()).isEqualTo(Mode.PRODUCTION); + assertThat(config.getTimeout()).isEqualTo(AxonFlowConfig.DEFAULT_TIMEOUT); + } - CacheConfig cacheConfig = CacheConfig.builder() - .ttl(Duration.ofMinutes(5)) - .maxSize(500) - .build(); + @Test + @DisplayName("should create config with all settings") + void shouldCreateFullConfig() { + RetryConfig retryConfig = + RetryConfig.builder().maxAttempts(5).initialDelay(Duration.ofMillis(500)).build(); + + CacheConfig cacheConfig = CacheConfig.builder().ttl(Duration.ofMinutes(5)).maxSize(500).build(); - AxonFlowConfig config = AxonFlowConfig.builder() + AxonFlowConfig config = + AxonFlowConfig.builder() .endpoint("https://api.example.com") .clientId("test-client") .clientSecret("test-secret") @@ -70,146 +62,133 @@ void shouldCreateFullConfig() { .userAgent("custom-agent/1.0") .build(); - assertThat(config.getEndpoint()).isEqualTo("https://api.example.com"); - assertThat(config.getClientId()).isEqualTo("test-client"); - assertThat(config.getClientSecret()).isEqualTo("test-secret"); - assertThat(config.getMode()).isEqualTo(Mode.SANDBOX); - assertThat(config.getTimeout()).isEqualTo(Duration.ofSeconds(30)); - assertThat(config.isDebug()).isTrue(); - assertThat(config.isInsecureSkipVerify()).isTrue(); - assertThat(config.getRetryConfig()).isEqualTo(retryConfig); - assertThat(config.getCacheConfig()).isEqualTo(cacheConfig); - assertThat(config.getUserAgent()).isEqualTo("custom-agent/1.0"); - } - - @Test - @DisplayName("should normalize URL by removing trailing slash") - void shouldNormalizeUrl() { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("http://localhost:8080/") - .build(); - - assertThat(config.getEndpoint()).isEqualTo("http://localhost:8080"); - } - - @ParameterizedTest - @ValueSource(strings = {"http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"}) - @DisplayName("should detect localhost URLs") - void shouldDetectLocalhost(String url) { - AxonFlowConfig config = AxonFlowConfig.builder() + assertThat(config.getEndpoint()).isEqualTo("https://api.example.com"); + assertThat(config.getClientId()).isEqualTo("test-client"); + assertThat(config.getClientSecret()).isEqualTo("test-secret"); + assertThat(config.getMode()).isEqualTo(Mode.SANDBOX); + assertThat(config.getTimeout()).isEqualTo(Duration.ofSeconds(30)); + assertThat(config.isDebug()).isTrue(); + assertThat(config.isInsecureSkipVerify()).isTrue(); + assertThat(config.getRetryConfig()).isEqualTo(retryConfig); + assertThat(config.getCacheConfig()).isEqualTo(cacheConfig); + assertThat(config.getUserAgent()).isEqualTo("custom-agent/1.0"); + } + + @Test + @DisplayName("should normalize URL by removing trailing slash") + void shouldNormalizeUrl() { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("http://localhost:8080/").build(); + + assertThat(config.getEndpoint()).isEqualTo("http://localhost:8080"); + } + + @ParameterizedTest + @ValueSource(strings = {"http://localhost:8080", "http://127.0.0.1:8080", "http://[::1]:8080"}) + @DisplayName("should detect localhost URLs") + void shouldDetectLocalhost(String url) { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint(url).build(); + + assertThat(config.isLocalhost()).isTrue(); + } + + @ParameterizedTest + @ValueSource(strings = {"https://api.example.com", "https://staging.getaxonflow.com"}) + @DisplayName("should detect non-localhost URLs") + void shouldDetectNonLocalhost(String url) { + AxonFlowConfig config = + AxonFlowConfig.builder() .endpoint(url) + .clientId("test-client") + .clientSecret("test-secret") .build(); - assertThat(config.isLocalhost()).isTrue(); - } + assertThat(config.isLocalhost()).isFalse(); + } - @ParameterizedTest - @ValueSource(strings = {"https://api.example.com", "https://staging.getaxonflow.com"}) - @DisplayName("should detect non-localhost URLs") - void shouldDetectNonLocalhost(String url) { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint(url) - .clientId("test-client").clientSecret("test-secret") - .build(); + @Test + @DisplayName("should allow non-localhost without credentials (community mode)") + void shouldAllowNonLocalhostWithoutCredentials() { + // Community mode: credentials are optional for any endpoint + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("https://api.example.com").build(); - assertThat(config.isLocalhost()).isFalse(); - } + assertThat(config.hasCredentials()).isFalse(); + assertThat(config.getEndpoint()).isEqualTo("https://api.example.com"); + } - @Test - @DisplayName("should allow non-localhost without credentials (community mode)") - void shouldAllowNonLocalhostWithoutCredentials() { - // Community mode: credentials are optional for any endpoint - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("https://api.example.com") - .build(); - - assertThat(config.hasCredentials()).isFalse(); - assertThat(config.getEndpoint()).isEqualTo("https://api.example.com"); - } - - @Test - @DisplayName("should accept client credentials for non-localhost") - void shouldAcceptClientCredentialsForNonLocalhost() { - AxonFlowConfig config = AxonFlowConfig.builder() + @Test + @DisplayName("should accept client credentials for non-localhost") + void shouldAcceptClientCredentialsForNonLocalhost() { + AxonFlowConfig config = + AxonFlowConfig.builder() .endpoint("https://api.example.com") .clientId("test-client") .clientSecret("test-secret") .build(); - assertThat(config.getClientId()).isEqualTo("test-client"); - assertThat(config.getClientSecret()).isEqualTo("test-secret"); - } - - @Test - @DisplayName("should use default retry config") - void shouldUseDefaultRetryConfig() { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .build(); - - assertThat(config.getRetryConfig()).isNotNull(); - assertThat(config.getRetryConfig().isEnabled()).isTrue(); - assertThat(config.getRetryConfig().getMaxAttempts()).isEqualTo(RetryConfig.DEFAULT_MAX_ATTEMPTS); - } - - @Test - @DisplayName("should use default cache config") - void shouldUseDefaultCacheConfig() { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .build(); - - assertThat(config.getCacheConfig()).isNotNull(); - assertThat(config.getCacheConfig().isEnabled()).isTrue(); - assertThat(config.getCacheConfig().getTtl()).isEqualTo(CacheConfig.DEFAULT_TTL); - } - - @Test - @DisplayName("should use default user agent") - void shouldUseDefaultUserAgent() { - AxonFlowConfig config = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .build(); - - assertThat(config.getUserAgent()).startsWith("axonflow-sdk-java/"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - AxonFlowConfig config1 = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .clientId("test") - .build(); - - AxonFlowConfig config2 = AxonFlowConfig.builder() - .endpoint("http://localhost:8080") - .clientId("test") - .build(); - - AxonFlowConfig config3 = AxonFlowConfig.builder() - .endpoint("http://localhost:8081") - .build(); - - assertThat(config1).isEqualTo(config2); - assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); - assertThat(config1).isNotEqualTo(config3); - } - - @Test - @DisplayName("should have meaningful toString") - void shouldHaveMeaningfulToString() { - AxonFlowConfig config = AxonFlowConfig.builder() + assertThat(config.getClientId()).isEqualTo("test-client"); + assertThat(config.getClientSecret()).isEqualTo("test-secret"); + } + + @Test + @DisplayName("should use default retry config") + void shouldUseDefaultRetryConfig() { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("http://localhost:8080").build(); + + assertThat(config.getRetryConfig()).isNotNull(); + assertThat(config.getRetryConfig().isEnabled()).isTrue(); + assertThat(config.getRetryConfig().getMaxAttempts()) + .isEqualTo(RetryConfig.DEFAULT_MAX_ATTEMPTS); + } + + @Test + @DisplayName("should use default cache config") + void shouldUseDefaultCacheConfig() { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("http://localhost:8080").build(); + + assertThat(config.getCacheConfig()).isNotNull(); + assertThat(config.getCacheConfig().isEnabled()).isTrue(); + assertThat(config.getCacheConfig().getTtl()).isEqualTo(CacheConfig.DEFAULT_TTL); + } + + @Test + @DisplayName("should use default user agent") + void shouldUseDefaultUserAgent() { + AxonFlowConfig config = AxonFlowConfig.builder().endpoint("http://localhost:8080").build(); + + assertThat(config.getUserAgent()).startsWith("axonflow-sdk-java/"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + AxonFlowConfig config1 = + AxonFlowConfig.builder().endpoint("http://localhost:8080").clientId("test").build(); + + AxonFlowConfig config2 = + AxonFlowConfig.builder().endpoint("http://localhost:8080").clientId("test").build(); + + AxonFlowConfig config3 = AxonFlowConfig.builder().endpoint("http://localhost:8081").build(); + + assertThat(config1).isEqualTo(config2); + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); + assertThat(config1).isNotEqualTo(config3); + } + + @Test + @DisplayName("should have meaningful toString") + void shouldHaveMeaningfulToString() { + AxonFlowConfig config = + AxonFlowConfig.builder() .endpoint("http://localhost:8080") .clientId("test-client") .mode(Mode.SANDBOX) .build(); - String str = config.toString(); - assertThat(str).contains("localhost:8080"); - assertThat(str).contains("test-client"); - assertThat(str).contains("SANDBOX"); - // Should not contain secrets - assertThat(str).doesNotContain("secret"); - } + String str = config.toString(); + assertThat(str).contains("localhost:8080"); + assertThat(str).contains("test-client"); + assertThat(str).contains("SANDBOX"); + // Should not contain secrets + assertThat(str).doesNotContain("secret"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java index 338a01f..8b2b1e2 100644 --- a/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java +++ b/src/test/java/com/getaxonflow/sdk/AxonFlowTest.java @@ -15,1099 +15,1195 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.exceptions.*; import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @WireMockTest @DisplayName("AxonFlow Client") class AxonFlowTest { - private AxonFlow axonflow; - private String baseUrl; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - baseUrl = wmRuntimeInfo.getHttpBaseUrl(); - // Add credentials for Gateway Mode tests (enterprise features) - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(baseUrl) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - // ======================================================================== - // Factory Methods - // ======================================================================== - - @Test - @DisplayName("builder should return AxonFlowConfig.Builder") - void builderShouldReturnConfigBuilder() { - AxonFlowConfig.Builder builder = AxonFlow.builder(); - assertThat(builder).isNotNull(); - } - - @Test - @DisplayName("create should require non-null config") - void createShouldRequireConfig() { - assertThatThrownBy(() -> AxonFlow.create(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("sandbox should create client in sandbox mode") - void sandboxShouldCreateSandboxClient(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow sandbox = AxonFlow.sandbox(wmRuntimeInfo.getHttpBaseUrl()); - assertThat(sandbox.getConfig().getMode()).isEqualTo(Mode.SANDBOX); - } - - // ======================================================================== - // Health Check - // ======================================================================== - - @Test - @DisplayName("healthCheck should return status") - void healthCheckShouldReturnStatus() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\":\"healthy\",\"version\":\"1.0.0\"}"))); - - HealthStatus health = axonflow.healthCheck(); - - assertThat(health.isHealthy()).isTrue(); - assertThat(health.getVersion()).isEqualTo("1.0.0"); - } - - @Test - @DisplayName("healthCheckAsync should return future") - void healthCheckAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\":\"healthy\"}"))); - - CompletableFuture future = axonflow.healthCheckAsync(); - HealthStatus health = future.get(); - - assertThat(health.isHealthy()).isTrue(); - } - - // ======================================================================== - // Gateway Mode - Pre-check - // ======================================================================== - - @Test - @DisplayName("getPolicyApprovedContext should require non-null request") - void getPolicyApprovedContextShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.getPolicyApprovedContext(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("preCheck should be alias for getPolicyApprovedContext") - void preCheckShouldBeAlias() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); - - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user-123") - .query("test") - .build(); - - PolicyApprovalResult result = axonflow.preCheck(request); - - assertThat(result.isApproved()).isTrue(); - } - - @Test - @DisplayName("getPolicyApprovedContextAsync should return future") - void getPolicyApprovedContextAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); - - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user-123") - .query("test") - .build(); - - CompletableFuture future = axonflow.getPolicyApprovedContextAsync(request); - PolicyApprovalResult result = future.get(); - - assertThat(result.isApproved()).isTrue(); - } - - @Test - @DisplayName("getPolicyApprovedContext should auto-populate clientId from config") - void getPolicyApprovedContextShouldAutoPopulateClientId(WireMockRuntimeInfo wmRuntimeInfo) { - // Create client with clientId configured - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("my-client-id") - .clientSecret("my-secret") - .build()); - - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); - - // Request WITHOUT explicit clientId - SDK should auto-populate from config - PolicyApprovalRequest request = PolicyApprovalRequest.builder() + private AxonFlow axonflow; + private String baseUrl; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + baseUrl = wmRuntimeInfo.getHttpBaseUrl(); + // Add credentials for Gateway Mode tests (enterprise features) + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(baseUrl) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + } + + // ======================================================================== + // Factory Methods + // ======================================================================== + + @Test + @DisplayName("builder should return AxonFlowConfig.Builder") + void builderShouldReturnConfigBuilder() { + AxonFlowConfig.Builder builder = AxonFlow.builder(); + assertThat(builder).isNotNull(); + } + + @Test + @DisplayName("create should require non-null config") + void createShouldRequireConfig() { + assertThatThrownBy(() -> AxonFlow.create(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("sandbox should create client in sandbox mode") + void sandboxShouldCreateSandboxClient(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow sandbox = AxonFlow.sandbox(wmRuntimeInfo.getHttpBaseUrl()); + assertThat(sandbox.getConfig().getMode()).isEqualTo(Mode.SANDBOX); + } + + // ======================================================================== + // Health Check + // ======================================================================== + + @Test + @DisplayName("healthCheck should return status") + void healthCheckShouldReturnStatus() { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"healthy\",\"version\":\"1.0.0\"}"))); + + HealthStatus health = axonflow.healthCheck(); + + assertThat(health.isHealthy()).isTrue(); + assertThat(health.getVersion()).isEqualTo("1.0.0"); + } + + @Test + @DisplayName("healthCheckAsync should return future") + void healthCheckAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"healthy\"}"))); + + CompletableFuture future = axonflow.healthCheckAsync(); + HealthStatus health = future.get(); + + assertThat(health.isHealthy()).isTrue(); + } + + // ======================================================================== + // Gateway Mode - Pre-check + // ======================================================================== + + @Test + @DisplayName("getPolicyApprovedContext should require non-null request") + void getPolicyApprovedContextShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.getPolicyApprovedContext(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("preCheck should be alias for getPolicyApprovedContext") + void preCheckShouldBeAlias() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); + + PolicyApprovalRequest request = + PolicyApprovalRequest.builder().userToken("user-123").query("test").build(); + + PolicyApprovalResult result = axonflow.preCheck(request); + + assertThat(result.isApproved()).isTrue(); + } + + @Test + @DisplayName("getPolicyApprovedContextAsync should return future") + void getPolicyApprovedContextAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); + + PolicyApprovalRequest request = + PolicyApprovalRequest.builder().userToken("user-123").query("test").build(); + + CompletableFuture future = + axonflow.getPolicyApprovedContextAsync(request); + PolicyApprovalResult result = future.get(); + + assertThat(result.isApproved()).isTrue(); + } + + @Test + @DisplayName("getPolicyApprovedContext should auto-populate clientId from config") + void getPolicyApprovedContextShouldAutoPopulateClientId(WireMockRuntimeInfo wmRuntimeInfo) { + // Create client with clientId configured + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("my-client-id") + .clientSecret("my-secret") + .build()); + + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); + + // Request WITHOUT explicit clientId - SDK should auto-populate from config + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() .userToken("user-123") .query("What is the capital of France?") .build(); - PolicyApprovalResult result = client.getPolicyApprovedContext(request); + PolicyApprovalResult result = client.getPolicyApprovedContext(request); - assertThat(result.isApproved()).isTrue(); + assertThat(result.isApproved()).isTrue(); - // Verify clientId was sent in request body (server requires this) - verify(postRequestedFor(urlEqualTo("/api/policy/pre-check")) + // Verify clientId was sent in request body (server requires this) + verify( + postRequestedFor(urlEqualTo("/api/policy/pre-check")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("my-client-id")))); - } - - @Test - @DisplayName("getPolicyApprovedContext should use explicit clientId if provided") - void getPolicyApprovedContextShouldUseExplicitClientId(WireMockRuntimeInfo wmRuntimeInfo) { - // Create client with clientId configured - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("config-client-id") - .clientSecret("my-secret") - .build()); - - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); - - // Request WITH explicit clientId - should use this one, not config - PolicyApprovalRequest request = PolicyApprovalRequest.builder() + } + + @Test + @DisplayName("getPolicyApprovedContext should use explicit clientId if provided") + void getPolicyApprovedContextShouldUseExplicitClientId(WireMockRuntimeInfo wmRuntimeInfo) { + // Create client with clientId configured + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("config-client-id") + .clientSecret("my-secret") + .build()); + + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"context_id\":\"ctx_123\",\"approved\":true}"))); + + // Request WITH explicit clientId - should use this one, not config + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() .userToken("user-123") .query("What is the capital of France?") .clientId("explicit-client-id") .build(); - PolicyApprovalResult result = client.getPolicyApprovedContext(request); + PolicyApprovalResult result = client.getPolicyApprovedContext(request); - assertThat(result.isApproved()).isTrue(); + assertThat(result.isApproved()).isTrue(); - // Verify explicit clientId was sent (not the config one) - verify(postRequestedFor(urlEqualTo("/api/policy/pre-check")) + // Verify explicit clientId was sent (not the config one) + verify( + postRequestedFor(urlEqualTo("/api/policy/pre-check")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("explicit-client-id")))); - } - - // ======================================================================== - // Gateway Mode - Audit - // ======================================================================== - - @Test - @DisplayName("auditLLMCall should require non-null options") - void auditLLMCallShouldRequireOptions() { - assertThatThrownBy(() -> axonflow.auditLLMCall(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("auditLLMCallAsync should return future") - void auditLLMCallAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/audit/llm-call")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"audit_id\":\"audit_123\"}"))); - - AuditOptions options = AuditOptions.builder() - .contextId("ctx_123").clientId("test-client") - .build(); - - CompletableFuture future = axonflow.auditLLMCallAsync(options); - AuditResult result = future.get(); - - assertThat(result.isSuccess()).isTrue(); - } - - // ======================================================================== - // Proxy Mode - proxyLLMCall - // ======================================================================== - - @Test - @DisplayName("proxyLLMCall should require non-null request") - void proxyLLMCallShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.proxyLLMCall(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("proxyLLMCallAsync should return future") - void proxyLLMCallAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - ClientRequest request = ClientRequest.builder() - .query("test") - .build(); - - CompletableFuture future = axonflow.proxyLLMCallAsync(request); - ClientResponse response = future.get(); - - assertThat(response.isSuccess()).isTrue(); - } - - @Test - @DisplayName("proxyLLMCall should auto-inject clientId from config when not set in request") - void proxyLLMCallShouldAutoInjectClientId() { - // Stub to verify the request contains client_id from config - stubFor(post(urlEqualTo("/api/request")) + } + + // ======================================================================== + // Gateway Mode - Audit + // ======================================================================== + + @Test + @DisplayName("auditLLMCall should require non-null options") + void auditLLMCallShouldRequireOptions() { + assertThatThrownBy(() -> axonflow.auditLLMCall(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("auditLLMCallAsync should return future") + void auditLLMCallAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/audit/llm-call")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"audit_id\":\"audit_123\"}"))); + + AuditOptions options = + AuditOptions.builder().contextId("ctx_123").clientId("test-client").build(); + + CompletableFuture future = axonflow.auditLLMCallAsync(options); + AuditResult result = future.get(); + + assertThat(result.isSuccess()).isTrue(); + } + + // ======================================================================== + // Proxy Mode - proxyLLMCall + // ======================================================================== + + @Test + @DisplayName("proxyLLMCall should require non-null request") + void proxyLLMCallShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.proxyLLMCall(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("proxyLLMCallAsync should return future") + void proxyLLMCallAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + ClientRequest request = ClientRequest.builder().query("test").build(); + + CompletableFuture future = axonflow.proxyLLMCallAsync(request); + ClientResponse response = future.get(); + + assertThat(response.isSuccess()).isTrue(); + } + + @Test + @DisplayName("proxyLLMCall should auto-inject clientId from config when not set in request") + void proxyLLMCallShouldAutoInjectClientId() { + // Stub to verify the request contains client_id from config + stubFor( + post(urlEqualTo("/api/request")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("test-client"))) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - // Build request WITHOUT clientId - ClientRequest request = ClientRequest.builder() + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + // Build request WITHOUT clientId + ClientRequest request = + ClientRequest.builder() .query("test query") .userToken("user-123") .requestType(RequestType.CHAT) .build(); - // The SDK should auto-inject clientId from config - ClientResponse response = axonflow.proxyLLMCall(request); + // The SDK should auto-inject clientId from config + ClientResponse response = axonflow.proxyLLMCall(request); - assertThat(response.isSuccess()).isTrue(); + assertThat(response.isSuccess()).isTrue(); - // Verify the request was made with client_id - verify(postRequestedFor(urlEqualTo("/api/request")) + // Verify the request was made with client_id + verify( + postRequestedFor(urlEqualTo("/api/request")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("test-client")))); - } - - @Test - @DisplayName("proxyLLMCall should preserve clientId when explicitly set in request") - void proxyLLMCallShouldPreserveExplicitClientId() { - // Stub to verify the request contains explicit client_id - stubFor(post(urlEqualTo("/api/request")) + } + + @Test + @DisplayName("proxyLLMCall should preserve clientId when explicitly set in request") + void proxyLLMCallShouldPreserveExplicitClientId() { + // Stub to verify the request contains explicit client_id + stubFor( + post(urlEqualTo("/api/request")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("explicit-client"))) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - // Build request WITH explicit clientId - ClientRequest request = ClientRequest.builder() + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + // Build request WITH explicit clientId + ClientRequest request = + ClientRequest.builder() .query("test query") .userToken("user-123") .clientId("explicit-client") .requestType(RequestType.CHAT) .build(); - ClientResponse response = axonflow.proxyLLMCall(request); + ClientResponse response = axonflow.proxyLLMCall(request); - assertThat(response.isSuccess()).isTrue(); + assertThat(response.isSuccess()).isTrue(); - // Verify the request was made with explicit client_id (not overwritten) - verify(postRequestedFor(urlEqualTo("/api/request")) + // Verify the request was made with explicit client_id (not overwritten) + verify( + postRequestedFor(urlEqualTo("/api/request")) .withRequestBody(matchingJsonPath("$.client_id", equalTo("explicit-client")))); - } - - // ======================================================================== - // Multi-Agent Planning - // ======================================================================== - - @Test - @DisplayName("generatePlan should require non-null request") - void generatePlanShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.generatePlan(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("generatePlanAsync should return future") - void generatePlanAsyncShouldReturnFuture() throws Exception { - // Now uses Agent API endpoint with request_type: multi-agent-plan - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"plan_id\":\"plan_123\",\"data\":{\"steps\":[]}}"))); - - PlanRequest request = PlanRequest.builder() - .objective("test") - .build(); - - CompletableFuture future = axonflow.generatePlanAsync(request); - PlanResponse response = future.get(); - - assertThat(response.getPlanId()).isEqualTo("plan_123"); - } - - @Test - @DisplayName("executePlan should require non-null planId") - void executePlanShouldRequirePlanId() { - assertThatThrownBy(() -> axonflow.executePlan(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("executePlan should execute plan via Agent API") - void executePlanShouldExecutePlan() { - // executePlan now uses /api/request with request_type: "execute-plan" (matches Go SDK) - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"result\":\"Plan executed successfully\"}"))); - - PlanResponse response = axonflow.executePlan("plan_123"); - - assertThat(response.getPlanId()).isEqualTo("plan_123"); - assertThat(response.getStatus()).isEqualTo("completed"); - assertThat(response.getResult()).isEqualTo("Plan executed successfully"); - - // Verify correct request format - verify(postRequestedFor(urlEqualTo("/api/request")) + } + + // ======================================================================== + // Multi-Agent Planning + // ======================================================================== + + @Test + @DisplayName("generatePlan should require non-null request") + void generatePlanShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.generatePlan(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("generatePlanAsync should return future") + void generatePlanAsyncShouldReturnFuture() throws Exception { + // Now uses Agent API endpoint with request_type: multi-agent-plan + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"plan_id\":\"plan_123\",\"data\":{\"steps\":[]}}"))); + + PlanRequest request = PlanRequest.builder().objective("test").build(); + + CompletableFuture future = axonflow.generatePlanAsync(request); + PlanResponse response = future.get(); + + assertThat(response.getPlanId()).isEqualTo("plan_123"); + } + + @Test + @DisplayName("executePlan should require non-null planId") + void executePlanShouldRequirePlanId() { + assertThatThrownBy(() -> axonflow.executePlan(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("executePlan should execute plan via Agent API") + void executePlanShouldExecutePlan() { + // executePlan now uses /api/request with request_type: "execute-plan" (matches Go SDK) + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"result\":\"Plan executed successfully\"}"))); + + PlanResponse response = axonflow.executePlan("plan_123"); + + assertThat(response.getPlanId()).isEqualTo("plan_123"); + assertThat(response.getStatus()).isEqualTo("completed"); + assertThat(response.getResult()).isEqualTo("Plan executed successfully"); + + // Verify correct request format + verify( + postRequestedFor(urlEqualTo("/api/request")) .withRequestBody(matchingJsonPath("$.request_type", equalTo("execute-plan"))) .withRequestBody(matchingJsonPath("$.context.plan_id", equalTo("plan_123")))); - } - - @Test - @DisplayName("getPlanStatus should require non-null planId") - void getPlanStatusShouldRequirePlanId() { - assertThatThrownBy(() -> axonflow.getPlanStatus(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("getPlanStatus should return plan status") - void getPlanStatusShouldReturnStatus() { - stubFor(get(urlEqualTo("/api/v1/plan/plan_123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"plan_id\":\"plan_123\",\"status\":\"pending\"}"))); - - PlanResponse response = axonflow.getPlanStatus("plan_123"); - - assertThat(response.getStatus()).isEqualTo("pending"); - } - - @Test - @DisplayName("executePlan should throw when nested data.success is false") - void executePlanShouldThrowOnNestedDataFailure() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"success\":false,\"error\":\"Step 2 timed out\"}}"))); - - assertThatThrownBy(() -> axonflow.executePlan("plan_fail")) - .isInstanceOf(PlanExecutionException.class) - .hasMessageContaining("Step 2 timed out"); - } - - @Test - @DisplayName("executePlan should use metadata.status when data.status is absent") - void executePlanShouldFallbackToMetadataStatus() { - // No data.status, but metadata.status is present — should use metadata.status - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"result\":\"done\",\"metadata\":{\"status\":\"awaiting_approval\"}}"))); - - PlanResponse response = axonflow.executePlan("plan_meta"); - - assertThat(response.getStatus()).isEqualTo("awaiting_approval"); - } - - @Test - @DisplayName("isApproved should return false when approved field is null") - void isApprovedShouldReturnFalseWhenNull() { - // Construct a ResumePlanResponse with null approved field - ResumePlanResponse response = new ResumePlanResponse( + } + + @Test + @DisplayName("getPlanStatus should require non-null planId") + void getPlanStatusShouldRequirePlanId() { + assertThatThrownBy(() -> axonflow.getPlanStatus(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("getPlanStatus should return plan status") + void getPlanStatusShouldReturnStatus() { + stubFor( + get(urlEqualTo("/api/v1/plan/plan_123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"plan_id\":\"plan_123\",\"status\":\"pending\"}"))); + + PlanResponse response = axonflow.getPlanStatus("plan_123"); + + assertThat(response.getStatus()).isEqualTo("pending"); + } + + @Test + @DisplayName("executePlan should throw when nested data.success is false") + void executePlanShouldThrowOnNestedDataFailure() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"success\":false,\"error\":\"Step 2 timed out\"}}"))); + + assertThatThrownBy(() -> axonflow.executePlan("plan_fail")) + .isInstanceOf(PlanExecutionException.class) + .hasMessageContaining("Step 2 timed out"); + } + + @Test + @DisplayName("executePlan should use metadata.status when data.status is absent") + void executePlanShouldFallbackToMetadataStatus() { + // No data.status, but metadata.status is present — should use metadata.status + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"result\":\"done\",\"metadata\":{\"status\":\"awaiting_approval\"}}"))); + + PlanResponse response = axonflow.executePlan("plan_meta"); + + assertThat(response.getStatus()).isEqualTo("awaiting_approval"); + } + + @Test + @DisplayName("isApproved should return false when approved field is null") + void isApprovedShouldReturnFalseWhenNull() { + // Construct a ResumePlanResponse with null approved field + ResumePlanResponse response = + new ResumePlanResponse( "plan_123", "wf_456", "in_progress", null, "Pending review", 2, "Step 2", 5); - // Must return false (not throw NPE) - assertThat(response.isApproved()).isFalse(); - } - - // ======================================================================== - // Orchestrator Health Check - // ======================================================================== - - @Test - @DisplayName("orchestratorHealthCheck should return healthy status") - void orchestratorHealthCheckShouldReturnHealthyStatus(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\":\"healthy\",\"version\":\"2.5.0\"}"))); - - HealthStatus health = client.orchestratorHealthCheck(); - - assertThat(health.isHealthy()).isTrue(); - assertThat(health.getVersion()).isEqualTo("2.5.0"); - } - - @Test - @DisplayName("orchestratorHealthCheck should return unhealthy on non-200") - void orchestratorHealthCheckShouldReturnUnhealthyOnError(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(503) - .withBody("{\"status\":\"unhealthy\"}"))); - - HealthStatus health = client.orchestratorHealthCheck(); - - assertThat(health.isHealthy()).isFalse(); - } - - @Test - @DisplayName("orchestratorHealthCheckAsync should return future") - void orchestratorHealthCheckAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\":\"healthy\"}"))); - - CompletableFuture future = client.orchestratorHealthCheckAsync(); - HealthStatus health = future.get(); - - assertThat(health.isHealthy()).isTrue(); - } - - // ======================================================================== - // MCP Connectors - // ======================================================================== - - @Test - @DisplayName("listConnectorsAsync should return future") - void listConnectorsAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/connectors")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[]"))); - - CompletableFuture> future = axonflow.listConnectorsAsync(); - List connectors = future.get(); - - assertThat(connectors).isEmpty(); - } - - @Test - @DisplayName("installConnector should require non-null connectorId") - void installConnectorShouldRequireConnectorId() { - assertThatThrownBy(() -> axonflow.installConnector(null, null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("installConnector should install connector") - void installConnectorShouldInstall() { - stubFor(post(urlEqualTo("/api/v1/connectors/salesforce/install")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"salesforce\",\"name\":\"Salesforce\",\"installed\":true}"))); - - ConnectorInfo info = axonflow.installConnector("salesforce", Map.of("key", "value")); - - assertThat(info.getId()).isEqualTo("salesforce"); - assertThat(info.isInstalled()).isTrue(); - } - - @Test - @DisplayName("installConnector should handle null config") - void installConnectorShouldHandleNullConfig() { - stubFor(post(urlEqualTo("/api/v1/connectors/salesforce/install")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"salesforce\",\"name\":\"Salesforce\",\"installed\":true}"))); - - ConnectorInfo info = axonflow.installConnector("salesforce", null); - - assertThat(info.getId()).isEqualTo("salesforce"); - } - - @Test - @DisplayName("uninstallConnector should require non-null connectorName") - void uninstallConnectorShouldRequireConnectorName() { - assertThatThrownBy(() -> axonflow.uninstallConnector(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("uninstallConnector should uninstall connector") - void uninstallConnectorShouldUninstall(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(delete(urlEqualTo("/api/v1/connectors/salesforce")) - .willReturn(aResponse() - .withStatus(204))); - - // Should not throw - client.uninstallConnector("salesforce"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/connectors/salesforce"))); - } - - @Test - @DisplayName("uninstallConnector should handle 200 response") - void uninstallConnectorShouldHandle200(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(delete(urlEqualTo("/api/v1/connectors/postgres")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Should not throw - client.uninstallConnector("postgres"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/connectors/postgres"))); - } - - @Test - @DisplayName("queryConnector should require non-null query") - void queryConnectorShouldRequireQuery() { - assertThatThrownBy(() -> axonflow.queryConnector(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("queryConnector should throw on failure") - void queryConnectorShouldThrowOnFailure() { - // MCP connector queries now use /api/request with request_type: "mcp-query" - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"error\":\"Connector not found\",\"blocked\":false}"))); - - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("unknown") - .operation("test") - .build(); - - assertThatThrownBy(() -> axonflow.queryConnector(query)) - .isInstanceOf(ConnectorException.class) - .hasMessageContaining("Connector not found"); - } - - @Test - @DisplayName("queryConnectorAsync should return future") - void queryConnectorAsyncShouldReturnFuture() throws Exception { - // MCP connector queries now use /api/request with request_type: "mcp-query" - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":[],\"blocked\":false}"))); - - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("salesforce") - .operation("list") - .build(); - - CompletableFuture future = axonflow.queryConnectorAsync(query); - ConnectorResponse response = future.get(); - - assertThat(response.isSuccess()).isTrue(); - } - - // ======================================================================== - // Error Handling - // ======================================================================== - - @Test - @DisplayName("should handle 401 Unauthorized") - void shouldHandle401() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(401) - .withBody("{\"error\":\"Invalid credentials\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AuthenticationException.class) - .hasMessageContaining("Invalid credentials"); - } - - @Test - @DisplayName("should handle 403 Forbidden") - void shouldHandle403() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(403) - .withBody("{\"error\":\"Access denied\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AuthenticationException.class); - } - - @Test - @DisplayName("should handle 403 with policy violation") - void shouldHandle403PolicyViolation() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(403) - .withBody("{\"error\":\"blocked by policy\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(PolicyViolationException.class); - } - - @Test - @DisplayName("should handle 429 Rate Limit") - void shouldHandle429() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(429) - .withBody("{\"error\":\"Rate limit exceeded\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(RateLimitException.class); - } - - @Test - @DisplayName("should handle 408 Timeout") - void shouldHandle408() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(408) - .withBody("{\"error\":\"Request timeout\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(TimeoutException.class); - } - - @Test - @DisplayName("should handle 504 Gateway Timeout") - void shouldHandle504() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(504) - .withBody("{\"error\":\"Gateway timeout\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(TimeoutException.class); - } - - @Test - @DisplayName("should handle 500 Internal Server Error") - void shouldHandle500() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(500) - .withBody("{\"message\":\"Internal error\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AxonFlowException.class) - .hasMessageContaining("Internal error"); - } - - @Test - @DisplayName("should handle non-JSON error body") - void shouldHandleNonJsonErrorBody() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(500) - .withBody("Service unavailable"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AxonFlowException.class) - .hasMessageContaining("Service unavailable"); - } - - @Test - @DisplayName("should handle empty error body") - void shouldHandleEmptyErrorBody() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(500) - .withBody(""))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AxonFlowException.class); - } - - @Test - @DisplayName("should handle block_reason in error body") - void shouldHandleBlockReason() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(500) - .withBody("{\"block_reason\":\"PII detected\"}"))); - - assertThatThrownBy(() -> axonflow.healthCheck()) - .isInstanceOf(AxonFlowException.class) - .hasMessageContaining("PII detected"); - } - - // ======================================================================== - // Cache Operations - // ======================================================================== - - @Test - @DisplayName("getCacheStats should return stats") - void getCacheStatsShouldReturnStats() { - String stats = axonflow.getCacheStats(); - assertThat(stats).isNotEmpty(); - } - - @Test - @DisplayName("clearCache should clear cache") - void clearCacheShouldClearCache() { - axonflow.clearCache(); - // Should not throw - } - - // ======================================================================== - // Configuration - // ======================================================================== - - @Test - @DisplayName("getConfig should return configuration") - void getConfigShouldReturnConfig() { - AxonFlowConfig config = axonflow.getConfig(); - assertThat(config.getEndpoint()).isEqualTo(baseUrl); - } - - // ======================================================================== - // Close - // ======================================================================== - - @Test - @DisplayName("close should release resources") - void closeShouldReleaseResources() { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .build()); - client.close(); - // Should not throw - } - - // ======================================================================== - // Authentication Headers (note: localhost URLs skip auth by design) - // ======================================================================== - - @Test - @DisplayName("should send auth headers when credentials are configured") - void shouldSendAuthHeadersWithCredentials(WireMockRuntimeInfo wmRuntimeInfo) { - // Auth headers are sent when credentials are configured - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"status\":\"healthy\"}"))); - - client.healthCheck(); - - // Verify OAuth2 Basic auth header is sent when credentials are configured - String expectedBasic = "Basic " + java.util.Base64.getEncoder().encodeToString( - "test-client:test-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8) - ); - verify(getRequestedFor(urlEqualTo("/health")) - .withHeader("Authorization", equalTo(expectedBasic))); - } - - @Test - @DisplayName("should include mode header") - void shouldIncludeModeHeader(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .mode(Mode.SANDBOX) - .build()); - - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"status\":\"healthy\"}"))); - - client.healthCheck(); - - verify(getRequestedFor(urlEqualTo("/health")) - .withHeader("X-AxonFlow-Mode", equalTo("sandbox"))); - } - - @Test - @DisplayName("should store credentials in config for non-localhost") - void shouldStoreCredentialsInConfig() { - AxonFlowConfig config = AxonFlowConfig.builder() + // Must return false (not throw NPE) + assertThat(response.isApproved()).isFalse(); + } + + // ======================================================================== + // Orchestrator Health Check + // ======================================================================== + + @Test + @DisplayName("orchestratorHealthCheck should return healthy status") + void orchestratorHealthCheckShouldReturnHealthyStatus(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"healthy\",\"version\":\"2.5.0\"}"))); + + HealthStatus health = client.orchestratorHealthCheck(); + + assertThat(health.isHealthy()).isTrue(); + assertThat(health.getVersion()).isEqualTo("2.5.0"); + } + + @Test + @DisplayName("orchestratorHealthCheck should return unhealthy on non-200") + void orchestratorHealthCheckShouldReturnUnhealthyOnError(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(503).withBody("{\"status\":\"unhealthy\"}"))); + + HealthStatus health = client.orchestratorHealthCheck(); + + assertThat(health.isHealthy()).isFalse(); + } + + @Test + @DisplayName("orchestratorHealthCheckAsync should return future") + void orchestratorHealthCheckAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) + throws Exception { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\":\"healthy\"}"))); + + CompletableFuture future = client.orchestratorHealthCheckAsync(); + HealthStatus health = future.get(); + + assertThat(health.isHealthy()).isTrue(); + } + + // ======================================================================== + // MCP Connectors + // ======================================================================== + + @Test + @DisplayName("listConnectorsAsync should return future") + void listConnectorsAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/connectors")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("[]"))); + + CompletableFuture> future = axonflow.listConnectorsAsync(); + List connectors = future.get(); + + assertThat(connectors).isEmpty(); + } + + @Test + @DisplayName("installConnector should require non-null connectorId") + void installConnectorShouldRequireConnectorId() { + assertThatThrownBy(() -> axonflow.installConnector(null, null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("installConnector should install connector") + void installConnectorShouldInstall() { + stubFor( + post(urlEqualTo("/api/v1/connectors/salesforce/install")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"salesforce\",\"name\":\"Salesforce\",\"installed\":true}"))); + + ConnectorInfo info = axonflow.installConnector("salesforce", Map.of("key", "value")); + + assertThat(info.getId()).isEqualTo("salesforce"); + assertThat(info.isInstalled()).isTrue(); + } + + @Test + @DisplayName("installConnector should handle null config") + void installConnectorShouldHandleNullConfig() { + stubFor( + post(urlEqualTo("/api/v1/connectors/salesforce/install")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"salesforce\",\"name\":\"Salesforce\",\"installed\":true}"))); + + ConnectorInfo info = axonflow.installConnector("salesforce", null); + + assertThat(info.getId()).isEqualTo("salesforce"); + } + + @Test + @DisplayName("uninstallConnector should require non-null connectorName") + void uninstallConnectorShouldRequireConnectorName() { + assertThatThrownBy(() -> axonflow.uninstallConnector(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("uninstallConnector should uninstall connector") + void uninstallConnectorShouldUninstall(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + delete(urlEqualTo("/api/v1/connectors/salesforce")) + .willReturn(aResponse().withStatus(204))); + + // Should not throw + client.uninstallConnector("salesforce"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/connectors/salesforce"))); + } + + @Test + @DisplayName("uninstallConnector should handle 200 response") + void uninstallConnectorShouldHandle200(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + delete(urlEqualTo("/api/v1/connectors/postgres")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Should not throw + client.uninstallConnector("postgres"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/connectors/postgres"))); + } + + @Test + @DisplayName("queryConnector should require non-null query") + void queryConnectorShouldRequireQuery() { + assertThatThrownBy(() -> axonflow.queryConnector(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("queryConnector should throw on failure") + void queryConnectorShouldThrowOnFailure() { + // MCP connector queries now use /api/request with request_type: "mcp-query" + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"error\":\"Connector not found\",\"blocked\":false}"))); + + ConnectorQuery query = + ConnectorQuery.builder().connectorId("unknown").operation("test").build(); + + assertThatThrownBy(() -> axonflow.queryConnector(query)) + .isInstanceOf(ConnectorException.class) + .hasMessageContaining("Connector not found"); + } + + @Test + @DisplayName("queryConnectorAsync should return future") + void queryConnectorAsyncShouldReturnFuture() throws Exception { + // MCP connector queries now use /api/request with request_type: "mcp-query" + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"data\":[],\"blocked\":false}"))); + + ConnectorQuery query = + ConnectorQuery.builder().connectorId("salesforce").operation("list").build(); + + CompletableFuture future = axonflow.queryConnectorAsync(query); + ConnectorResponse response = future.get(); + + assertThat(response.isSuccess()).isTrue(); + } + + // ======================================================================== + // Error Handling + // ======================================================================== + + @Test + @DisplayName("should handle 401 Unauthorized") + void shouldHandle401() { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse().withStatus(401).withBody("{\"error\":\"Invalid credentials\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()) + .isInstanceOf(AuthenticationException.class) + .hasMessageContaining("Invalid credentials"); + } + + @Test + @DisplayName("should handle 403 Forbidden") + void shouldHandle403() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(403).withBody("{\"error\":\"Access denied\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(AuthenticationException.class); + } + + @Test + @DisplayName("should handle 403 with policy violation") + void shouldHandle403PolicyViolation() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(403).withBody("{\"error\":\"blocked by policy\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(PolicyViolationException.class); + } + + @Test + @DisplayName("should handle 429 Rate Limit") + void shouldHandle429() { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse().withStatus(429).withBody("{\"error\":\"Rate limit exceeded\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(RateLimitException.class); + } + + @Test + @DisplayName("should handle 408 Timeout") + void shouldHandle408() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(408).withBody("{\"error\":\"Request timeout\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(TimeoutException.class); + } + + @Test + @DisplayName("should handle 504 Gateway Timeout") + void shouldHandle504() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(504).withBody("{\"error\":\"Gateway timeout\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(TimeoutException.class); + } + + @Test + @DisplayName("should handle 500 Internal Server Error") + void shouldHandle500() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(500).withBody("{\"message\":\"Internal error\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()) + .isInstanceOf(AxonFlowException.class) + .hasMessageContaining("Internal error"); + } + + @Test + @DisplayName("should handle non-JSON error body") + void shouldHandleNonJsonErrorBody() { + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(500).withBody("Service unavailable"))); + + assertThatThrownBy(() -> axonflow.healthCheck()) + .isInstanceOf(AxonFlowException.class) + .hasMessageContaining("Service unavailable"); + } + + @Test + @DisplayName("should handle empty error body") + void shouldHandleEmptyErrorBody() { + stubFor(get(urlEqualTo("/health")).willReturn(aResponse().withStatus(500).withBody(""))); + + assertThatThrownBy(() -> axonflow.healthCheck()).isInstanceOf(AxonFlowException.class); + } + + @Test + @DisplayName("should handle block_reason in error body") + void shouldHandleBlockReason() { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse().withStatus(500).withBody("{\"block_reason\":\"PII detected\"}"))); + + assertThatThrownBy(() -> axonflow.healthCheck()) + .isInstanceOf(AxonFlowException.class) + .hasMessageContaining("PII detected"); + } + + // ======================================================================== + // Cache Operations + // ======================================================================== + + @Test + @DisplayName("getCacheStats should return stats") + void getCacheStatsShouldReturnStats() { + String stats = axonflow.getCacheStats(); + assertThat(stats).isNotEmpty(); + } + + @Test + @DisplayName("clearCache should clear cache") + void clearCacheShouldClearCache() { + axonflow.clearCache(); + // Should not throw + } + + // ======================================================================== + // Configuration + // ======================================================================== + + @Test + @DisplayName("getConfig should return configuration") + void getConfigShouldReturnConfig() { + AxonFlowConfig config = axonflow.getConfig(); + assertThat(config.getEndpoint()).isEqualTo(baseUrl); + } + + // ======================================================================== + // Close + // ======================================================================== + + @Test + @DisplayName("close should release resources") + void closeShouldReleaseResources() { + AxonFlow client = AxonFlow.create(AxonFlowConfig.builder().endpoint(baseUrl).build()); + client.close(); + // Should not throw + } + + // ======================================================================== + // Authentication Headers (note: localhost URLs skip auth by design) + // ======================================================================== + + @Test + @DisplayName("should send auth headers when credentials are configured") + void shouldSendAuthHeadersWithCredentials(WireMockRuntimeInfo wmRuntimeInfo) { + // Auth headers are sent when credentials are configured + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(200).withBody("{\"status\":\"healthy\"}"))); + + client.healthCheck(); + + // Verify OAuth2 Basic auth header is sent when credentials are configured + String expectedBasic = + "Basic " + + java.util.Base64.getEncoder() + .encodeToString( + "test-client:test-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + verify( + getRequestedFor(urlEqualTo("/health")).withHeader("Authorization", equalTo(expectedBasic))); + } + + @Test + @DisplayName("should include mode header") + void shouldIncludeModeHeader(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .mode(Mode.SANDBOX) + .build()); + + stubFor( + get(urlEqualTo("/health")) + .willReturn(aResponse().withStatus(200).withBody("{\"status\":\"healthy\"}"))); + + client.healthCheck(); + + verify( + getRequestedFor(urlEqualTo("/health")).withHeader("X-AxonFlow-Mode", equalTo("sandbox"))); + } + + @Test + @DisplayName("should store credentials in config for non-localhost") + void shouldStoreCredentialsInConfig() { + AxonFlowConfig config = + AxonFlowConfig.builder() .endpoint("https://api.axonflow.com") .clientId("test-client") .clientSecret("test-secret") .build(); - assertThat(config.getClientId()).isEqualTo("test-client"); - assertThat(config.getClientSecret()).isEqualTo("test-secret"); - assertThat(config.isLocalhost()).isFalse(); - } - - // ======================================================================== - // Execution Replay - List Executions - // ======================================================================== - - @Test - @DisplayName("listExecutions should return empty list") - void listExecutionsShouldReturnEmptyList(WireMockRuntimeInfo wmRuntimeInfo) { - // Create client with orchestrator URL pointing to WireMock - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/executions")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"executions\":[],\"total\":0,\"limit\":50,\"offset\":0}"))); - - var response = client.listExecutions(); - - assertThat(response.getExecutions()).isEmpty(); - assertThat(response.getTotal()).isEqualTo(0); - assertThat(response.getLimit()).isEqualTo(50); - } - - @Test - @DisplayName("listExecutions should return executions with filter") - void listExecutionsShouldReturnExecutionsWithFilter(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/executions")) + assertThat(config.getClientId()).isEqualTo("test-client"); + assertThat(config.getClientSecret()).isEqualTo("test-secret"); + assertThat(config.isLocalhost()).isFalse(); + } + + // ======================================================================== + // Execution Replay - List Executions + // ======================================================================== + + @Test + @DisplayName("listExecutions should return empty list") + void listExecutionsShouldReturnEmptyList(WireMockRuntimeInfo wmRuntimeInfo) { + // Create client with orchestrator URL pointing to WireMock + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/executions")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"executions\":[],\"total\":0,\"limit\":50,\"offset\":0}"))); + + var response = client.listExecutions(); + + assertThat(response.getExecutions()).isEmpty(); + assertThat(response.getTotal()).isEqualTo(0); + assertThat(response.getLimit()).isEqualTo(50); + } + + @Test + @DisplayName("listExecutions should return executions with filter") + void listExecutionsShouldReturnExecutionsWithFilter(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/executions")) .withQueryParam("status", equalTo("completed")) .withQueryParam("limit", equalTo("10")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"executions\":[{\"request_id\":\"exec-123\",\"workflow_name\":\"test\",\"status\":\"completed\",\"total_steps\":1,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:00Z\",\"total_tokens\":50,\"total_cost_usd\":0.001}],\"total\":1,\"limit\":10,\"offset\":0}"))); - - var options = com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.ListExecutionsOptions.builder() + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"executions\":[{\"request_id\":\"exec-123\",\"workflow_name\":\"test\",\"status\":\"completed\",\"total_steps\":1,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:00Z\",\"total_tokens\":50,\"total_cost_usd\":0.001}],\"total\":1,\"limit\":10,\"offset\":0}"))); + + var options = + com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.ListExecutionsOptions + .builder() .setStatus("completed") .setLimit(10); - var response = client.listExecutions(options); - - assertThat(response.getExecutions()).hasSize(1); - assertThat(response.getExecutions().get(0).getRequestId()).isEqualTo("exec-123"); - assertThat(response.getExecutions().get(0).getStatus()).isEqualTo("completed"); - } - - // ======================================================================== - // Execution Replay - Get Execution - // ======================================================================== - - @Test - @DisplayName("getExecution should return execution detail") - void getExecutionShouldReturnExecutionDetail(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/executions/exec-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"summary\":{\"request_id\":\"exec-123\",\"workflow_name\":\"test\",\"status\":\"completed\",\"total_steps\":2,\"completed_steps\":2,\"started_at\":\"2026-01-03T12:00:00Z\",\"total_tokens\":100,\"total_cost_usd\":0.005},\"steps\":[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"tokens_in\":10,\"tokens_out\":20,\"cost_usd\":0.001}]}"))); - - var detail = client.getExecution("exec-123"); - - assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-123"); - assertThat(detail.getSummary().getStatus()).isEqualTo("completed"); - assertThat(detail.getSteps()).hasSize(1); - assertThat(detail.getSteps().get(0).getStepName()).isEqualTo("greet"); - } - - // ======================================================================== - // Execution Replay - Get Execution Steps - // ======================================================================== - - @Test - @DisplayName("getExecutionSteps should return step snapshots") - void getExecutionStepsShouldReturnSnapshots(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/executions/exec-123/steps")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"step1\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"tokens_in\":10,\"tokens_out\":15,\"cost_usd\":0.001},{\"request_id\":\"exec-123\",\"step_index\":1,\"step_name\":\"step2\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:01Z\",\"tokens_in\":15,\"tokens_out\":20,\"cost_usd\":0.002}]"))); - - var steps = client.getExecutionSteps("exec-123"); - - assertThat(steps).hasSize(2); - assertThat(steps.get(0).getStepName()).isEqualTo("step1"); - assertThat(steps.get(1).getStepName()).isEqualTo("step2"); - } - - // ======================================================================== - // Execution Replay - Get Execution Timeline - // ======================================================================== - - @Test - @DisplayName("getExecutionTimeline should return timeline entries") - void getExecutionTimelineShouldReturnEntries(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/executions/exec-123/timeline")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[{\"step_index\":0,\"step_name\":\"start\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"has_error\":false,\"has_approval\":false},{\"step_index\":1,\"step_name\":\"approve\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:01Z\",\"has_error\":false,\"has_approval\":true}]"))); - - var timeline = client.getExecutionTimeline("exec-123"); - - assertThat(timeline).hasSize(2); - assertThat(timeline.get(0).getStepName()).isEqualTo("start"); - assertThat(timeline.get(0).hasApproval()).isFalse(); - assertThat(timeline.get(1).getStepName()).isEqualTo("approve"); - assertThat(timeline.get(1).hasApproval()).isTrue(); - } - - // ======================================================================== - // Execution Replay - Export Execution - // ======================================================================== - - @Test - @DisplayName("exportExecution should return export data") - void exportExecutionShouldReturnExportData(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/executions/exec-123/export")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"execution_id\":\"exec-123\",\"workflow_name\":\"test\",\"exported_at\":\"2026-01-03T12:00:00Z\"}"))); - - var export = client.exportExecution("exec-123"); - - assertThat(export.get("execution_id")).isEqualTo("exec-123"); - assertThat(export.get("workflow_name")).isEqualTo("test"); - } - - // ======================================================================== - // Execution Replay - Delete Execution - // ======================================================================== - - @Test - @DisplayName("deleteExecution should succeed") - void deleteExecutionShouldSucceed(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(delete(urlEqualTo("/api/v1/executions/exec-123")) - .willReturn(aResponse() - .withStatus(204))); - - // Should not throw - client.deleteExecution("exec-123"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/executions/exec-123"))); - } - - // ======================================================================== - // Cost Controls - Budgets - // ======================================================================== - - @Test - @DisplayName("createBudget should create a budget") - void createBudgetShouldCreateBudget(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(post(urlEqualTo("/api/v1/budgets")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); - - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() + var response = client.listExecutions(options); + + assertThat(response.getExecutions()).hasSize(1); + assertThat(response.getExecutions().get(0).getRequestId()).isEqualTo("exec-123"); + assertThat(response.getExecutions().get(0).getStatus()).isEqualTo("completed"); + } + + // ======================================================================== + // Execution Replay - Get Execution + // ======================================================================== + + @Test + @DisplayName("getExecution should return execution detail") + void getExecutionShouldReturnExecutionDetail(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/executions/exec-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"summary\":{\"request_id\":\"exec-123\",\"workflow_name\":\"test\",\"status\":\"completed\",\"total_steps\":2,\"completed_steps\":2,\"started_at\":\"2026-01-03T12:00:00Z\",\"total_tokens\":100,\"total_cost_usd\":0.005},\"steps\":[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"tokens_in\":10,\"tokens_out\":20,\"cost_usd\":0.001}]}"))); + + var detail = client.getExecution("exec-123"); + + assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-123"); + assertThat(detail.getSummary().getStatus()).isEqualTo("completed"); + assertThat(detail.getSteps()).hasSize(1); + assertThat(detail.getSteps().get(0).getStepName()).isEqualTo("greet"); + } + + // ======================================================================== + // Execution Replay - Get Execution Steps + // ======================================================================== + + @Test + @DisplayName("getExecutionSteps should return step snapshots") + void getExecutionStepsShouldReturnSnapshots(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/executions/exec-123/steps")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"step1\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"tokens_in\":10,\"tokens_out\":15,\"cost_usd\":0.001},{\"request_id\":\"exec-123\",\"step_index\":1,\"step_name\":\"step2\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:01Z\",\"tokens_in\":15,\"tokens_out\":20,\"cost_usd\":0.002}]"))); + + var steps = client.getExecutionSteps("exec-123"); + + assertThat(steps).hasSize(2); + assertThat(steps.get(0).getStepName()).isEqualTo("step1"); + assertThat(steps.get(1).getStepName()).isEqualTo("step2"); + } + + // ======================================================================== + // Execution Replay - Get Execution Timeline + // ======================================================================== + + @Test + @DisplayName("getExecutionTimeline should return timeline entries") + void getExecutionTimelineShouldReturnEntries(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/executions/exec-123/timeline")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "[{\"step_index\":0,\"step_name\":\"start\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\",\"has_error\":false,\"has_approval\":false},{\"step_index\":1,\"step_name\":\"approve\",\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:01Z\",\"has_error\":false,\"has_approval\":true}]"))); + + var timeline = client.getExecutionTimeline("exec-123"); + + assertThat(timeline).hasSize(2); + assertThat(timeline.get(0).getStepName()).isEqualTo("start"); + assertThat(timeline.get(0).hasApproval()).isFalse(); + assertThat(timeline.get(1).getStepName()).isEqualTo("approve"); + assertThat(timeline.get(1).hasApproval()).isTrue(); + } + + // ======================================================================== + // Execution Replay - Export Execution + // ======================================================================== + + @Test + @DisplayName("exportExecution should return export data") + void exportExecutionShouldReturnExportData(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/executions/exec-123/export")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"execution_id\":\"exec-123\",\"workflow_name\":\"test\",\"exported_at\":\"2026-01-03T12:00:00Z\"}"))); + + var export = client.exportExecution("exec-123"); + + assertThat(export.get("execution_id")).isEqualTo("exec-123"); + assertThat(export.get("workflow_name")).isEqualTo("test"); + } + + // ======================================================================== + // Execution Replay - Delete Execution + // ======================================================================== + + @Test + @DisplayName("deleteExecution should succeed") + void deleteExecutionShouldSucceed(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + delete(urlEqualTo("/api/v1/executions/exec-123")).willReturn(aResponse().withStatus(204))); + + // Should not throw + client.deleteExecution("exec-123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/executions/exec-123"))); + } + + // ======================================================================== + // Cost Controls - Budgets + // ======================================================================== + + @Test + @DisplayName("createBudget should create a budget") + void createBudgetShouldCreateBudget(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + post(urlEqualTo("/api/v1/budgets")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); + + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() .id("budget-123") .name("Test Budget") .scope(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION) @@ -1117,230 +1213,286 @@ void createBudgetShouldCreateBudget(WireMockRuntimeInfo wmRuntimeInfo) { .alertThresholds(List.of(50, 80, 100)) .build(); - var budget = client.createBudget(request); - - assertThat(budget.getId()).isEqualTo("budget-123"); - assertThat(budget.getName()).isEqualTo("Test Budget"); - assertThat(budget.getLimitUsd()).isEqualTo(100.0); - } - - @Test - @DisplayName("getBudget should return budget by ID") - void getBudgetShouldReturnBudget(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/budgets/budget-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); - - var budget = client.getBudget("budget-123"); - - assertThat(budget.getId()).isEqualTo("budget-123"); - assertThat(budget.getName()).isEqualTo("Test Budget"); - } - - @Test - @DisplayName("listBudgets should return list of budgets") - void listBudgetsShouldReturnList(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/budgets")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"budgets\":[{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}],\"total\":1}"))); - - var response = client.listBudgets(); - - assertThat(response.getBudgets()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(1); - } - - @Test - @DisplayName("deleteBudget should delete a budget") - void deleteBudgetShouldDelete(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(delete(urlEqualTo("/api/v1/budgets/budget-123")) - .willReturn(aResponse() - .withStatus(204))); - - client.deleteBudget("budget-123"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/budgets/budget-123"))); - } - - @Test - @DisplayName("getBudgetStatus should return budget status") - void getBudgetStatusShouldReturnStatus(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/budgets/budget-123/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"budget_id\":\"budget-123\",\"used_usd\":45.50,\"remaining_usd\":54.50,\"usage_percent\":45.5,\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\",\"is_exceeded\":false}"))); - - var status = client.getBudgetStatus("budget-123"); - - assertThat(status.getUsedUsd()).isEqualTo(45.50); - assertThat(status.getRemainingUsd()).isEqualTo(54.50); - assertThat(status.isExceeded()).isFalse(); - } - - @Test - @DisplayName("getBudgetAlerts should return budget alerts") - void getBudgetAlertsShouldReturnAlerts(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/budgets/budget-123/alerts")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"alerts\":[],\"count\":0}"))); - - var response = client.getBudgetAlerts("budget-123"); - - assertThat(response.getAlerts()).isEmpty(); - assertThat(response.getCount()).isEqualTo(0); - } - - @Test - @DisplayName("checkBudget should return budget decision") - void checkBudgetShouldReturnDecision(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(post(urlEqualTo("/api/v1/budgets/check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\":true,\"budget_id\":\"budget-123\",\"message\":\"Within budget\"}"))); - - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetCheckRequest.builder() + var budget = client.createBudget(request); + + assertThat(budget.getId()).isEqualTo("budget-123"); + assertThat(budget.getName()).isEqualTo("Test Budget"); + assertThat(budget.getLimitUsd()).isEqualTo(100.0); + } + + @Test + @DisplayName("getBudget should return budget by ID") + void getBudgetShouldReturnBudget(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/budgets/budget-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); + + var budget = client.getBudget("budget-123"); + + assertThat(budget.getId()).isEqualTo("budget-123"); + assertThat(budget.getName()).isEqualTo("Test Budget"); + } + + @Test + @DisplayName("listBudgets should return list of budgets") + void listBudgetsShouldReturnList(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/budgets")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"budgets\":[{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}],\"total\":1}"))); + + var response = client.listBudgets(); + + assertThat(response.getBudgets()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(1); + } + + @Test + @DisplayName("deleteBudget should delete a budget") + void deleteBudgetShouldDelete(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + delete(urlEqualTo("/api/v1/budgets/budget-123")).willReturn(aResponse().withStatus(204))); + + client.deleteBudget("budget-123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/budgets/budget-123"))); + } + + @Test + @DisplayName("getBudgetStatus should return budget status") + void getBudgetStatusShouldReturnStatus(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/budgets/budget-123/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"budget_id\":\"budget-123\",\"used_usd\":45.50,\"remaining_usd\":54.50,\"usage_percent\":45.5,\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\",\"is_exceeded\":false}"))); + + var status = client.getBudgetStatus("budget-123"); + + assertThat(status.getUsedUsd()).isEqualTo(45.50); + assertThat(status.getRemainingUsd()).isEqualTo(54.50); + assertThat(status.isExceeded()).isFalse(); + } + + @Test + @DisplayName("getBudgetAlerts should return budget alerts") + void getBudgetAlertsShouldReturnAlerts(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/budgets/budget-123/alerts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"alerts\":[],\"count\":0}"))); + + var response = client.getBudgetAlerts("budget-123"); + + assertThat(response.getAlerts()).isEmpty(); + assertThat(response.getCount()).isEqualTo(0); + } + + @Test + @DisplayName("checkBudget should return budget decision") + void checkBudgetShouldReturnDecision(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + post(urlEqualTo("/api/v1/budgets/check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\":true,\"budget_id\":\"budget-123\",\"message\":\"Within budget\"}"))); + + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetCheckRequest.builder() .orgId("org-123") .build(); - var decision = client.checkBudget(request); - - assertThat(decision.isAllowed()).isTrue(); - assertThat(decision.getMessage()).isEqualTo("Within budget"); - } - - // ======================================================================== - // Cost Controls - Usage - // ======================================================================== - - @Test - @DisplayName("getUsageSummary should return usage summary") - void getUsageSummaryShouldReturnSummary(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/usage")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"total_cost_usd\":125.50,\"total_tokens_in\":50000,\"total_tokens_out\":25000,\"total_requests\":100,\"period\":\"monthly\",\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\"}"))); - - var summary = client.getUsageSummary(); - - assertThat(summary.getTotalCostUsd()).isEqualTo(125.50); - assertThat(summary.getTotalTokensIn()).isEqualTo(50000); - assertThat(summary.getTotalTokensOut()).isEqualTo(25000); - assertThat(summary.getTotalRequests()).isEqualTo(100); - } - - @Test - @DisplayName("getUsageBreakdown should return usage breakdown") - void getUsageBreakdownShouldReturnBreakdown(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/usage/breakdown")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"items\":[{\"dimension\":\"openai\",\"cost_usd\":80.0,\"input_tokens\":30000,\"output_tokens\":15000,\"requests\":60}],\"group_by\":\"provider\",\"period\":\"monthly\"}"))); - - var breakdown = client.getUsageBreakdown("provider", "monthly"); - - assertThat(breakdown.getItems()).hasSize(1); - assertThat(breakdown.getGroupBy()).isEqualTo("provider"); - } - - @Test - @DisplayName("listUsageRecords should return usage records") - void listUsageRecordsShouldReturnRecords(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/usage/records")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"records\":[],\"total\":0}"))); - - var response = client.listUsageRecords(); - - assertThat(response.getRecords()).isEmpty(); - assertThat(response.getTotal()).isEqualTo(0); - } - - // ======================================================================== - // Cost Controls - Async Methods - // ======================================================================== - - @Test - @DisplayName("createBudgetAsync should return future") - void createBudgetAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(post(urlEqualTo("/api/v1/budgets")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); - - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() + var decision = client.checkBudget(request); + + assertThat(decision.isAllowed()).isTrue(); + assertThat(decision.getMessage()).isEqualTo("Within budget"); + } + + // ======================================================================== + // Cost Controls - Usage + // ======================================================================== + + @Test + @DisplayName("getUsageSummary should return usage summary") + void getUsageSummaryShouldReturnSummary(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/usage")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"total_cost_usd\":125.50,\"total_tokens_in\":50000,\"total_tokens_out\":25000,\"total_requests\":100,\"period\":\"monthly\",\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\"}"))); + + var summary = client.getUsageSummary(); + + assertThat(summary.getTotalCostUsd()).isEqualTo(125.50); + assertThat(summary.getTotalTokensIn()).isEqualTo(50000); + assertThat(summary.getTotalTokensOut()).isEqualTo(25000); + assertThat(summary.getTotalRequests()).isEqualTo(100); + } + + @Test + @DisplayName("getUsageBreakdown should return usage breakdown") + void getUsageBreakdownShouldReturnBreakdown(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/usage/breakdown")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"items\":[{\"dimension\":\"openai\",\"cost_usd\":80.0,\"input_tokens\":30000,\"output_tokens\":15000,\"requests\":60}],\"group_by\":\"provider\",\"period\":\"monthly\"}"))); + + var breakdown = client.getUsageBreakdown("provider", "monthly"); + + assertThat(breakdown.getItems()).hasSize(1); + assertThat(breakdown.getGroupBy()).isEqualTo("provider"); + } + + @Test + @DisplayName("listUsageRecords should return usage records") + void listUsageRecordsShouldReturnRecords(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/usage/records")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"records\":[],\"total\":0}"))); + + var response = client.listUsageRecords(); + + assertThat(response.getRecords()).isEmpty(); + assertThat(response.getTotal()).isEqualTo(0); + } + + // ======================================================================== + // Cost Controls - Async Methods + // ======================================================================== + + @Test + @DisplayName("createBudgetAsync should return future") + void createBudgetAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + post(urlEqualTo("/api/v1/budgets")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); + + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() .id("budget-123") .name("Test Budget") .scope(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION) @@ -1350,139 +1502,184 @@ void createBudgetAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) thro .alertThresholds(List.of(50, 80, 100)) .build(); - var future = client.createBudgetAsync(request); - var budget = future.get(); - - assertThat(budget.getId()).isEqualTo("budget-123"); - } - - @Test - @DisplayName("getBudgetAsync should return future") - void getBudgetAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlEqualTo("/api/v1/budgets/budget-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); - - var future = client.getBudgetAsync("budget-123"); - var budget = future.get(); - - assertThat(budget.getId()).isEqualTo("budget-123"); - } - - @Test - @DisplayName("getUsageSummaryAsync should return future") - void getUsageSummaryAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(baseUrl) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - stubFor(get(urlPathEqualTo("/api/v1/usage")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"total_cost_usd\":125.50,\"total_tokens_in\":50000,\"total_tokens_out\":25000,\"total_requests\":100,\"period\":\"monthly\",\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\"}"))); - - var future = client.getUsageSummaryAsync("monthly"); - var summary = future.get(); - - assertThat(summary.getTotalCostUsd()).isEqualTo(125.50); - } - - // ======================================== - // COST CONTROLS - ENUM UNIT TESTS - // ======================================== - - @Test - @DisplayName("BudgetScope fromValue should return correct enum") - void budgetScopeFromValueShouldWork() { - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("organization")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("team")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("agent")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.AGENT); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("workflow")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.WORKFLOW); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("user")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.USER); - } - - @Test - @DisplayName("BudgetScope getValue should return correct string") - void budgetScopeGetValueShouldWork() { - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION.getValue()) - .isEqualTo("organization"); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM.getValue()) - .isEqualTo("team"); - } - - @Test - @DisplayName("BudgetPeriod fromValue should return correct enum") - void budgetPeriodFromValueShouldWork() { - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("daily")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.DAILY); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("weekly")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.WEEKLY); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("monthly")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.MONTHLY); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("quarterly")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.QUARTERLY); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("yearly")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.YEARLY); - } - - @Test - @DisplayName("BudgetOnExceed fromValue should return correct enum") - void budgetOnExceedFromValueShouldWork() { - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue("warn")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.WARN); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue("block")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.BLOCK); - assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue("downgrade")) - .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE); - } - - @Test - @DisplayName("BudgetScope fromValue should throw for invalid value") - void budgetScopeFromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> - com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget scope"); - } - - @Test - @DisplayName("BudgetPeriod fromValue should throw for invalid value") - void budgetPeriodFromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> - com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget period"); - } - - @Test - @DisplayName("BudgetOnExceed fromValue should throw for invalid value") - void budgetOnExceedFromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> - com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget on exceed action"); - } - - @Test - @DisplayName("CreateBudgetRequest builder should set all fields") - void createBudgetRequestBuilderShouldSetAllFields() { - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() + var future = client.createBudgetAsync(request); + var budget = future.get(); + + assertThat(budget.getId()).isEqualTo("budget-123"); + } + + @Test + @DisplayName("getBudgetAsync should return future") + void getBudgetAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlEqualTo("/api/v1/budgets/budget-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"budget-123\",\"name\":\"Test Budget\",\"scope\":\"organization\",\"limit_usd\":100.0,\"period\":\"monthly\",\"on_exceed\":\"warn\",\"alert_thresholds\":[50,80,100],\"created_at\":\"2026-01-03T12:00:00Z\",\"updated_at\":\"2026-01-03T12:00:00Z\"}"))); + + var future = client.getBudgetAsync("budget-123"); + var budget = future.get(); + + assertThat(budget.getId()).isEqualTo("budget-123"); + } + + @Test + @DisplayName("getUsageSummaryAsync should return future") + void getUsageSummaryAsyncShouldReturnFuture(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(baseUrl) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + stubFor( + get(urlPathEqualTo("/api/v1/usage")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"total_cost_usd\":125.50,\"total_tokens_in\":50000,\"total_tokens_out\":25000,\"total_requests\":100,\"period\":\"monthly\",\"period_start\":\"2026-01-01T00:00:00Z\",\"period_end\":\"2026-01-31T23:59:59Z\"}"))); + + var future = client.getUsageSummaryAsync("monthly"); + var summary = future.get(); + + assertThat(summary.getTotalCostUsd()).isEqualTo(125.50); + } + + // ======================================== + // COST CONTROLS - ENUM UNIT TESTS + // ======================================== + + @Test + @DisplayName("BudgetScope fromValue should return correct enum") + void budgetScopeFromValueShouldWork() { + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue( + "organization")) + .isEqualTo( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("team")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("agent")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.AGENT); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue( + "workflow")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.WORKFLOW); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue("user")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.USER); + } + + @Test + @DisplayName("BudgetScope getValue should return correct string") + void budgetScopeGetValueShouldWork() { + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.ORGANIZATION + .getValue()) + .isEqualTo("organization"); + assertThat(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM.getValue()) + .isEqualTo("team"); + } + + @Test + @DisplayName("BudgetPeriod fromValue should return correct enum") + void budgetPeriodFromValueShouldWork() { + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue("daily")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.DAILY); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue( + "weekly")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.WEEKLY); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue( + "monthly")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.MONTHLY); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue( + "quarterly")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.QUARTERLY); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue( + "yearly")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.YEARLY); + } + + @Test + @DisplayName("BudgetOnExceed fromValue should return correct enum") + void budgetOnExceedFromValueShouldWork() { + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue( + "warn")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.WARN); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue( + "block")) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.BLOCK); + assertThat( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue( + "downgrade")) + .isEqualTo( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE); + } + + @Test + @DisplayName("BudgetScope fromValue should throw for invalid value") + void budgetScopeFromValueShouldThrowForInvalid() { + assertThatThrownBy( + () -> + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.fromValue( + "invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget scope"); + } + + @Test + @DisplayName("BudgetPeriod fromValue should throw for invalid value") + void budgetPeriodFromValueShouldThrowForInvalid() { + assertThatThrownBy( + () -> + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.fromValue( + "invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget period"); + } + + @Test + @DisplayName("BudgetOnExceed fromValue should throw for invalid value") + void budgetOnExceedFromValueShouldThrowForInvalid() { + assertThatThrownBy( + () -> + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.fromValue( + "invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget on exceed action"); + } + + @Test + @DisplayName("CreateBudgetRequest builder should set all fields") + void createBudgetRequestBuilderShouldSetAllFields() { + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.CreateBudgetRequest.builder() .id("budget-1") .name("My Budget") .scope(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM) @@ -1493,36 +1690,44 @@ void createBudgetRequestBuilderShouldSetAllFields() { .alertThresholds(List.of(25, 50, 75)) .build(); - assertThat(request.getId()).isEqualTo("budget-1"); - assertThat(request.getName()).isEqualTo("My Budget"); - assertThat(request.getScope()).isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM); - assertThat(request.getScopeId()).isEqualTo("team-123"); - assertThat(request.getLimitUsd()).isEqualTo(500.0); - assertThat(request.getPeriod()).isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.WEEKLY); - assertThat(request.getOnExceed()).isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.BLOCK); - assertThat(request.getAlertThresholds()).containsExactly(25, 50, 75); - } - - @Test - @DisplayName("UpdateBudgetRequest builder should set all fields") - void updateBudgetRequestBuilderShouldSetAllFields() { - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.UpdateBudgetRequest.builder() + assertThat(request.getId()).isEqualTo("budget-1"); + assertThat(request.getName()).isEqualTo("My Budget"); + assertThat(request.getScope()) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetScope.TEAM); + assertThat(request.getScopeId()).isEqualTo("team-123"); + assertThat(request.getLimitUsd()).isEqualTo(500.0); + assertThat(request.getPeriod()) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetPeriod.WEEKLY); + assertThat(request.getOnExceed()) + .isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.BLOCK); + assertThat(request.getAlertThresholds()).containsExactly(25, 50, 75); + } + + @Test + @DisplayName("UpdateBudgetRequest builder should set all fields") + void updateBudgetRequestBuilderShouldSetAllFields() { + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.UpdateBudgetRequest.builder() .name("Updated Budget") .limitUsd(1000.0) - .onExceed(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE) + .onExceed( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE) .alertThresholds(List.of(80, 90, 100)) .build(); - assertThat(request.getName()).isEqualTo("Updated Budget"); - assertThat(request.getLimitUsd()).isEqualTo(1000.0); - assertThat(request.getOnExceed()).isEqualTo(com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE); - assertThat(request.getAlertThresholds()).containsExactly(80, 90, 100); - } - - @Test - @DisplayName("BudgetCheckRequest builder should set all fields") - void budgetCheckRequestBuilderShouldSetAllFields() { - var request = com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetCheckRequest.builder() + assertThat(request.getName()).isEqualTo("Updated Budget"); + assertThat(request.getLimitUsd()).isEqualTo(1000.0); + assertThat(request.getOnExceed()) + .isEqualTo( + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetOnExceed.DOWNGRADE); + assertThat(request.getAlertThresholds()).containsExactly(80, 90, 100); + } + + @Test + @DisplayName("BudgetCheckRequest builder should set all fields") + void budgetCheckRequestBuilderShouldSetAllFields() { + var request = + com.getaxonflow.sdk.types.costcontrols.CostControlTypes.BudgetCheckRequest.builder() .orgId("org-1") .teamId("team-1") .agentId("agent-1") @@ -1530,1038 +1735,1157 @@ void budgetCheckRequestBuilderShouldSetAllFields() { .userId("user-1") .build(); - assertThat(request.getOrgId()).isEqualTo("org-1"); - assertThat(request.getTeamId()).isEqualTo("team-1"); - assertThat(request.getAgentId()).isEqualTo("agent-1"); - assertThat(request.getWorkflowId()).isEqualTo("workflow-1"); - assertThat(request.getUserId()).isEqualTo("user-1"); - } - - // ======================================================================== - // MCP Query/Execute Tests (Policy Enforcement) - // ======================================================================== - - @Test - @DisplayName("mcpQuery should return response with policy info") - void mcpQueryShouldReturnResponseWithPolicyInfo() { - stubFor(post(urlEqualTo("/mcp/resources/query")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [{\"id\": 1, \"name\": \"Test\"}], " + - "\"redacted\": false, \"policy_info\": {\"policies_evaluated\": 5, " + - "\"blocked\": false, \"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); - - ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM users"); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.isRedacted()).isFalse(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(5); - } - - @Test - @DisplayName("mcpQuery should return redacted response") - void mcpQueryShouldReturnRedactedResponse() { - stubFor(post(urlEqualTo("/mcp/resources/query")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [{\"id\": 1, \"ssn\": \"***REDACTED***\"}], " + - "\"redacted\": true, \"redacted_fields\": [\"data[0].ssn\"], " + - "\"policy_info\": {\"policies_evaluated\": 5, \"blocked\": false, " + - "\"redactions_applied\": 1, \"processing_time_ms\": 3}}"))); - - ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM customers"); - - assertThat(response.isRedacted()).isTrue(); - assertThat(response.getRedactedFields()).contains("data[0].ssn"); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getRedactionsApplied()).isEqualTo(1); - } - - @Test - @DisplayName("mcpQuery should throw exception when blocked") - void mcpQueryShouldThrowExceptionWhenBlocked() { - stubFor(post(urlEqualTo("/mcp/resources/query")) - .willReturn(aResponse() - .withStatus(403) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Request blocked: SQLi detected\"}"))); - - assertThatThrownBy(() -> axonflow.mcpQuery("postgres", "SELECT * FROM users; DROP TABLE users;--")) - .isInstanceOf(ConnectorException.class) - .hasMessageContaining("blocked"); - } - - @Test - @DisplayName("mcpExecute should return response with policy info") - void mcpExecuteShouldReturnResponseWithPolicyInfo() { - stubFor(post(urlEqualTo("/mcp/resources/query")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": {\"affected_rows\": 1}, " + - "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - ConnectorResponse response = axonflow.mcpExecute("postgres", "UPDATE users SET name = $1 WHERE id = $2"); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(3); - } - - // ======================================================================== - // MCP Check Input/Output Tests (Policy Pre-validation) - // ======================================================================== - - @Test - @DisplayName("mcpCheckInput should return allowed response") - void mcpCheckInputShouldReturnAllowedResponse() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-input")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 3, " + - "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - MCPCheckInputResponse response = axonflow.mcpCheckInput("postgres", "SELECT * FROM users"); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(3); - assertThat(response.getBlockReason()).isNull(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(3); - } - - @Test - @DisplayName("mcpCheckInput with options should send operation and parameters") - void mcpCheckInputWithOptionsShouldSendOperationAndParameters() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-input")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 5, " + - "\"policy_info\": {\"policies_evaluated\": 5, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); - - Map options = Map.of( - "operation", "execute", - "parameters", Map.of("limit", 100) - ); - MCPCheckInputResponse response = axonflow.mcpCheckInput("postgres", "UPDATE users SET name = $1", options); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(5); - - verify(postRequestedFor(urlEqualTo("/api/v1/mcp/check-input")) + assertThat(request.getOrgId()).isEqualTo("org-1"); + assertThat(request.getTeamId()).isEqualTo("team-1"); + assertThat(request.getAgentId()).isEqualTo("agent-1"); + assertThat(request.getWorkflowId()).isEqualTo("workflow-1"); + assertThat(request.getUserId()).isEqualTo("user-1"); + } + + // ======================================================================== + // MCP Query/Execute Tests (Policy Enforcement) + // ======================================================================== + + @Test + @DisplayName("mcpQuery should return response with policy info") + void mcpQueryShouldReturnResponseWithPolicyInfo() { + stubFor( + post(urlEqualTo("/mcp/resources/query")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [{\"id\": 1, \"name\": \"Test\"}], " + + "\"redacted\": false, \"policy_info\": {\"policies_evaluated\": 5, " + + "\"blocked\": false, \"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM users"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.isRedacted()).isFalse(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(5); + } + + @Test + @DisplayName("mcpQuery should return redacted response") + void mcpQueryShouldReturnRedactedResponse() { + stubFor( + post(urlEqualTo("/mcp/resources/query")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [{\"id\": 1, \"ssn\": \"***REDACTED***\"}], " + + "\"redacted\": true, \"redacted_fields\": [\"data[0].ssn\"], " + + "\"policy_info\": {\"policies_evaluated\": 5, \"blocked\": false, " + + "\"redactions_applied\": 1, \"processing_time_ms\": 3}}"))); + + ConnectorResponse response = axonflow.mcpQuery("postgres", "SELECT * FROM customers"); + + assertThat(response.isRedacted()).isTrue(); + assertThat(response.getRedactedFields()).contains("data[0].ssn"); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getRedactionsApplied()).isEqualTo(1); + } + + @Test + @DisplayName("mcpQuery should throw exception when blocked") + void mcpQueryShouldThrowExceptionWhenBlocked() { + stubFor( + post(urlEqualTo("/mcp/resources/query")) + .willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Request blocked: SQLi detected\"}"))); + + assertThatThrownBy( + () -> axonflow.mcpQuery("postgres", "SELECT * FROM users; DROP TABLE users;--")) + .isInstanceOf(ConnectorException.class) + .hasMessageContaining("blocked"); + } + + @Test + @DisplayName("mcpExecute should return response with policy info") + void mcpExecuteShouldReturnResponseWithPolicyInfo() { + stubFor( + post(urlEqualTo("/mcp/resources/query")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": {\"affected_rows\": 1}, " + + "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + ConnectorResponse response = + axonflow.mcpExecute("postgres", "UPDATE users SET name = $1 WHERE id = $2"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(3); + } + + // ======================================================================== + // MCP Check Input/Output Tests (Policy Pre-validation) + // ======================================================================== + + @Test + @DisplayName("mcpCheckInput should return allowed response") + void mcpCheckInputShouldReturnAllowedResponse() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 3, " + + "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + MCPCheckInputResponse response = axonflow.mcpCheckInput("postgres", "SELECT * FROM users"); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(3); + assertThat(response.getBlockReason()).isNull(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(3); + } + + @Test + @DisplayName("mcpCheckInput with options should send operation and parameters") + void mcpCheckInputWithOptionsShouldSendOperationAndParameters() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 5, " + + "\"policy_info\": {\"policies_evaluated\": 5, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + Map options = + Map.of("operation", "execute", "parameters", Map.of("limit", 100)); + MCPCheckInputResponse response = + axonflow.mcpCheckInput("postgres", "UPDATE users SET name = $1", options); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(5); + + verify( + postRequestedFor(urlEqualTo("/api/v1/mcp/check-input")) .withRequestBody(containing("\"connector_type\":\"postgres\"")) .withRequestBody(containing("\"statement\":\"UPDATE users SET name = $1\"")) .withRequestBody(containing("\"operation\":\"execute\""))); - } - - @Test - @DisplayName("mcpCheckInput should handle 403 as blocked result") - void mcpCheckInputShouldHandle403AsBlockedResult() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-input")) - .willReturn(aResponse() - .withStatus(403) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": false, \"block_reason\": \"SQL injection detected\", " + - "\"policies_evaluated\": 3, " + - "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": true, " + - "\"block_reason\": \"SQL injection detected\", " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - MCPCheckInputResponse response = axonflow.mcpCheckInput("postgres", "SELECT * FROM users; DROP TABLE users;--"); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("SQL injection detected"); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().isBlocked()).isTrue(); - } - - @Test - @DisplayName("mcpCheckInput should throw on 500 error") - void mcpCheckInputShouldThrowOn500Error() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-input")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.mcpCheckInput("postgres", "SELECT 1")) - .isInstanceOf(ConnectorException.class) - .hasMessageContaining("Internal server error"); - } - - @Test - @DisplayName("mcpCheckInput should require non-null connectorType") - void mcpCheckInputShouldRequireConnectorType() { - assertThatThrownBy(() -> axonflow.mcpCheckInput(null, "SELECT 1")) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("mcpCheckInput should require non-null statement") - void mcpCheckInputShouldRequireStatement() { - assertThatThrownBy(() -> axonflow.mcpCheckInput("postgres", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("mcpCheckInputAsync should return future") - void mcpCheckInputAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/mcp/check-input")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 2, " + - "\"policy_info\": {\"policies_evaluated\": 2, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - CompletableFuture future = axonflow.mcpCheckInputAsync("postgres", "SELECT 1"); - MCPCheckInputResponse response = future.get(); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(2); - } - - @Test - @DisplayName("mcpCheckOutput should return allowed response") - void mcpCheckOutputShouldReturnAllowedResponse() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 4, " + - "\"policy_info\": {\"policies_evaluated\": 4, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 3}}"))); - - List> responseData = List.of( - Map.of("id", 1, "name", "Alice"), - Map.of("id", 2, "name", "Bob") - ); - MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(4); - assertThat(response.getBlockReason()).isNull(); - assertThat(response.getPolicyInfo()).isNotNull(); - } - - @Test - @DisplayName("mcpCheckOutput with options should send message, metadata, and row_count") - void mcpCheckOutputWithOptionsShouldSendOptions() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 6, " + - "\"policy_info\": {\"policies_evaluated\": 6, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); - - List> responseData = List.of( - Map.of("id", 1, "name", "Alice") - ); - Map options = Map.of( - "message", "Query completed", - "metadata", Map.of("source", "analytics"), - "row_count", 1 - ); - MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData, options); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(6); - - verify(postRequestedFor(urlEqualTo("/api/v1/mcp/check-output")) + } + + @Test + @DisplayName("mcpCheckInput should handle 403 as blocked result") + void mcpCheckInputShouldHandle403AsBlockedResult() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": false, \"block_reason\": \"SQL injection detected\", " + + "\"policies_evaluated\": 3, " + + "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": true, " + + "\"block_reason\": \"SQL injection detected\", " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + MCPCheckInputResponse response = + axonflow.mcpCheckInput("postgres", "SELECT * FROM users; DROP TABLE users;--"); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("SQL injection detected"); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().isBlocked()).isTrue(); + } + + @Test + @DisplayName("mcpCheckInput should throw on 500 error") + void mcpCheckInputShouldThrowOn500Error() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.mcpCheckInput("postgres", "SELECT 1")) + .isInstanceOf(ConnectorException.class) + .hasMessageContaining("Internal server error"); + } + + @Test + @DisplayName("mcpCheckInput should require non-null connectorType") + void mcpCheckInputShouldRequireConnectorType() { + assertThatThrownBy(() -> axonflow.mcpCheckInput(null, "SELECT 1")) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("mcpCheckInput should require non-null statement") + void mcpCheckInputShouldRequireStatement() { + assertThatThrownBy(() -> axonflow.mcpCheckInput("postgres", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("mcpCheckInputAsync should return future") + void mcpCheckInputAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-input")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 2, " + + "\"policy_info\": {\"policies_evaluated\": 2, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + CompletableFuture future = + axonflow.mcpCheckInputAsync("postgres", "SELECT 1"); + MCPCheckInputResponse response = future.get(); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(2); + } + + @Test + @DisplayName("mcpCheckOutput should return allowed response") + void mcpCheckOutputShouldReturnAllowedResponse() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 4, " + + "\"policy_info\": {\"policies_evaluated\": 4, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 3}}"))); + + List> responseData = + List.of(Map.of("id", 1, "name", "Alice"), Map.of("id", 2, "name", "Bob")); + MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(4); + assertThat(response.getBlockReason()).isNull(); + assertThat(response.getPolicyInfo()).isNotNull(); + } + + @Test + @DisplayName("mcpCheckOutput with options should send message, metadata, and row_count") + void mcpCheckOutputWithOptionsShouldSendOptions() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 6, " + + "\"policy_info\": {\"policies_evaluated\": 6, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + List> responseData = List.of(Map.of("id", 1, "name", "Alice")); + Map options = + Map.of( + "message", + "Query completed", + "metadata", + Map.of("source", "analytics"), + "row_count", + 1); + MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData, options); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(6); + + verify( + postRequestedFor(urlEqualTo("/api/v1/mcp/check-output")) .withRequestBody(containing("\"connector_type\":\"postgres\"")) .withRequestBody(containing("\"message\":\"Query completed\"")) .withRequestBody(containing("\"row_count\":1"))); - } - - @Test - @DisplayName("mcpCheckOutput should handle 403 as blocked result") - void mcpCheckOutputShouldHandle403AsBlockedResult() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(403) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": false, \"block_reason\": \"PII detected in output\", " + - "\"policies_evaluated\": 4, " + - "\"redacted_data\": [{\"id\": 1, \"ssn\": \"***REDACTED***\"}], " + - "\"policy_info\": {\"policies_evaluated\": 4, \"blocked\": true, " + - "\"block_reason\": \"PII detected in output\", " + - "\"redactions_applied\": 1, \"processing_time_ms\": 5}}"))); - - List> responseData = List.of( - Map.of("id", 1, "ssn", "123-45-6789") - ); - MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("PII detected in output"); - assertThat(response.getRedactedData()).isNotNull(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().isBlocked()).isTrue(); - } - - @Test - @DisplayName("mcpCheckOutput should handle response with exfiltration info") - void mcpCheckOutputShouldHandleExfiltrationInfo() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 3, " + - "\"exfiltration_info\": {\"rows_returned\": 10, \"row_limit\": 1000, " + - "\"bytes_returned\": 2048, \"byte_limit\": 1048576, \"within_limits\": true}, " + - "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); - - List> responseData = List.of(Map.of("id", 1)); - MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getExfiltrationInfo()).isNotNull(); - assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(10); - assertThat(response.getExfiltrationInfo().isWithinLimits()).isTrue(); - } - - @Test - @DisplayName("mcpCheckOutput should throw on 500 error") - void mcpCheckOutputShouldThrowOn500Error() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.mcpCheckOutput("postgres", List.of(Map.of("id", 1)))) - .isInstanceOf(ConnectorException.class) - .hasMessageContaining("Internal server error"); - } - - @Test - @DisplayName("mcpCheckOutput should require non-null connectorType") - void mcpCheckOutputShouldRequireConnectorType() { - assertThatThrownBy(() -> axonflow.mcpCheckOutput(null, List.of())) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("mcpCheckOutput should allow null responseData for execute-style requests") - void mcpCheckOutputShouldAllowNullResponseData() { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 1, " + - "\"policy_info\": {\"policies_evaluated\": 1, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - Map options = new HashMap<>(); - options.put("message", "3 rows updated"); - - MCPCheckOutputResponse resp = axonflow.mcpCheckOutput("postgres", null, options); - assertThat(resp.isAllowed()).isTrue(); - } - - @Test - @DisplayName("mcpCheckOutputAsync should return future") - void mcpCheckOutputAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/mcp/check-output")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\": true, \"policies_evaluated\": 2, " + - "\"policy_info\": {\"policies_evaluated\": 2, \"blocked\": false, " + - "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); - - CompletableFuture future = axonflow.mcpCheckOutputAsync( - "postgres", List.of(Map.of("id", 1))); - MCPCheckOutputResponse response = future.get(); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(2); - } - - // ======================================================================== - // Rollback Plan - // ======================================================================== - - @Test - @DisplayName("rollbackPlan should require non-null planId") - void rollbackPlanShouldRequirePlanId() { - assertThatThrownBy(() -> axonflow.rollbackPlan(null, 1)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("rollbackPlan should return rollback response") - void rollbackPlanShouldReturnResponse() { - stubFor(post(urlEqualTo("/api/v1/plan/plan_123/rollback/2")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"plan_id\":\"plan_123\",\"version\":2,\"previous_version\":3,\"status\":\"rolled_back\"}"))); - - RollbackPlanResponse response = axonflow.rollbackPlan("plan_123", 2); - - assertThat(response.getPlanId()).isEqualTo("plan_123"); - assertThat(response.getVersion()).isEqualTo(2); - assertThat(response.getPreviousVersion()).isEqualTo(3); - assertThat(response.getStatus()).isEqualTo("rolled_back"); - } - - @Test - @DisplayName("rollbackPlanAsync should return future") - void rollbackPlanAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/plan/plan_456/rollback/1")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"plan_id\":\"plan_456\",\"version\":1,\"previous_version\":3,\"status\":\"rolled_back\"}"))); - - CompletableFuture future = axonflow.rollbackPlanAsync("plan_456", 1); - RollbackPlanResponse response = future.get(); - - assertThat(response.getPlanId()).isEqualTo("plan_456"); - assertThat(response.getVersion()).isEqualTo(1); - } - - // ======================================================================== - // WCP Approval Methods - // ======================================================================== - - @Test - @DisplayName("approveStep should require non-null workflowId") - void approveStepShouldRequireWorkflowId() { - assertThatThrownBy(() -> axonflow.approveStep(null, "step-1")) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("approveStep should require non-null stepId") - void approveStepShouldRequireStepId() { - assertThatThrownBy(() -> axonflow.approveStep("wf-1", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("approveStep should return approval response") - void approveStepShouldReturnResponse() { - stubFor(post(urlEqualTo("/api/v1/workflow-control/wf-123/steps/step-1/approve")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"workflow_id\":\"wf-123\",\"step_id\":\"step-1\",\"status\":\"approved\"}"))); - - com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse response = - axonflow.approveStep("wf-123", "step-1"); - - assertThat(response.getWorkflowId()).isEqualTo("wf-123"); - assertThat(response.getStepId()).isEqualTo("step-1"); - assertThat(response.getStatus()).isEqualTo("approved"); - } - - @Test - @DisplayName("approveStepAsync should return future") - void approveStepAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/workflow-control/wf-456/steps/step-2/approve")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"approved\"}"))); - - CompletableFuture future = - axonflow.approveStepAsync("wf-456", "step-2"); - com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse response = future.get(); - - assertThat(response.getWorkflowId()).isEqualTo("wf-456"); - assertThat(response.getStatus()).isEqualTo("approved"); - } - - @Test - @DisplayName("rejectStep should require non-null workflowId") - void rejectStepShouldRequireWorkflowId() { - assertThatThrownBy(() -> axonflow.rejectStep(null, "step-1")) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("rejectStep should require non-null stepId") - void rejectStepShouldRequireStepId() { - assertThatThrownBy(() -> axonflow.rejectStep("wf-1", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("rejectStep should return rejection response") - void rejectStepShouldReturnResponse() { - stubFor(post(urlEqualTo("/api/v1/workflow-control/wf-123/steps/step-1/reject")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"workflow_id\":\"wf-123\",\"step_id\":\"step-1\",\"status\":\"rejected\"}"))); - - com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse response = - axonflow.rejectStep("wf-123", "step-1"); - - assertThat(response.getWorkflowId()).isEqualTo("wf-123"); - assertThat(response.getStepId()).isEqualTo("step-1"); - assertThat(response.getStatus()).isEqualTo("rejected"); - } - - @Test - @DisplayName("rejectStepAsync should return future") - void rejectStepAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/workflow-control/wf-789/steps/step-3/reject")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"workflow_id\":\"wf-789\",\"step_id\":\"step-3\",\"status\":\"rejected\"}"))); - - CompletableFuture future = - axonflow.rejectStepAsync("wf-789", "step-3"); - com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse response = future.get(); - - assertThat(response.getWorkflowId()).isEqualTo("wf-789"); - assertThat(response.getStatus()).isEqualTo("rejected"); - } - - @Test - @DisplayName("getPendingApprovals should return pending approvals") - void getPendingApprovalsShouldReturnApprovals() { - stubFor(get(urlEqualTo("/api/v1/workflow-control/pending-approvals")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"approvals\":[{\"workflow_id\":\"wf-1\",\"workflow_name\":\"Review\"," - + "\"step_id\":\"s-1\",\"step_name\":\"Generate\"," - + "\"step_type\":\"llm_call\",\"created_at\":\"2026-02-07T10:00:00Z\"}],\"total\":1}"))); - - com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = - axonflow.getPendingApprovals(); - - assertThat(response.getTotal()).isEqualTo(1); - assertThat(response.getApprovals()).hasSize(1); - assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); - assertThat(response.getApprovals().get(0).getStepName()).isEqualTo("Generate"); - } - - @Test - @DisplayName("getPendingApprovals with limit should add query parameter") - void getPendingApprovalsWithLimitShouldAddQueryParam() { - stubFor(get(urlEqualTo("/api/v1/workflow-control/pending-approvals?limit=10")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"approvals\":[],\"total\":0}"))); - - com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = - axonflow.getPendingApprovals(10); - - assertThat(response.getTotal()).isEqualTo(0); - assertThat(response.getApprovals()).isEmpty(); - } - - @Test - @DisplayName("getPendingApprovalsAsync should return future") - void getPendingApprovalsAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/workflow-control/pending-approvals?limit=5")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"approvals\":[],\"total\":0}"))); - - CompletableFuture future = - axonflow.getPendingApprovalsAsync(5); - com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = future.get(); - - assertThat(response.getTotal()).isEqualTo(0); - } - - // ======================================================================== - // Webhook CRUD Methods - // ======================================================================== - - @Test - @DisplayName("createWebhook should require non-null request") - void createWebhookShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.createWebhook(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("createWebhook should return created subscription") - void createWebhookShouldReturnSubscription() { - stubFor(post(urlEqualTo("/api/v1/webhooks")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-123\",\"url\":\"https://example.com/hook\"," - + "\"events\":[\"step.blocked\"],\"active\":true," - + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:00:00Z\"}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest request = - com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest.builder() - .url("https://example.com/hook") - .events(List.of("step.blocked")) - .secret("my-secret") - .active(true) - .build(); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = - axonflow.createWebhook(request); - - assertThat(subscription.getId()).isEqualTo("wh-123"); - assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); - assertThat(subscription.getEvents()).containsExactly("step.blocked"); - assertThat(subscription.isActive()).isTrue(); - } - - @Test - @DisplayName("createWebhookAsync should return future") - void createWebhookAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/webhooks")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-456\",\"url\":\"https://example.com\"," - + "\"events\":[],\"active\":true}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest request = - com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest.builder() - .url("https://example.com") - .build(); - - CompletableFuture future = - axonflow.createWebhookAsync(request); - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); - - assertThat(subscription.getId()).isEqualTo("wh-456"); - } - - @Test - @DisplayName("getWebhook should require non-null webhookId") - void getWebhookShouldRequireWebhookId() { - assertThatThrownBy(() -> axonflow.getWebhook(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("getWebhook should return subscription") - void getWebhookShouldReturnSubscription() { - stubFor(get(urlEqualTo("/api/v1/webhooks/wh-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-123\",\"url\":\"https://example.com/hook\"," - + "\"events\":[\"workflow.completed\"],\"active\":true," - + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T11:00:00Z\"}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = - axonflow.getWebhook("wh-123"); - - assertThat(subscription.getId()).isEqualTo("wh-123"); - assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); - assertThat(subscription.getEvents()).containsExactly("workflow.completed"); - } - - @Test - @DisplayName("getWebhookAsync should return future") - void getWebhookAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/webhooks/wh-789")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-789\",\"url\":\"https://example.com\"," - + "\"events\":[],\"active\":true}"))); - - CompletableFuture future = - axonflow.getWebhookAsync("wh-789"); - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); - - assertThat(subscription.getId()).isEqualTo("wh-789"); - } - - @Test - @DisplayName("updateWebhook should require non-null webhookId") - void updateWebhookShouldRequireWebhookId() { - assertThatThrownBy(() -> axonflow.updateWebhook(null, - com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder().build())) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("updateWebhook should require non-null request") - void updateWebhookShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.updateWebhook("wh-1", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("updateWebhook should return updated subscription") - void updateWebhookShouldReturnUpdatedSubscription() { - stubFor(put(urlEqualTo("/api/v1/webhooks/wh-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-123\",\"url\":\"https://new-url.com/hook\"," - + "\"events\":[\"step.approved\"],\"active\":false," - + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T12:00:00Z\"}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest request = - com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder() - .url("https://new-url.com/hook") - .events(List.of("step.approved")) - .active(false) - .build(); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = - axonflow.updateWebhook("wh-123", request); - - assertThat(subscription.getId()).isEqualTo("wh-123"); - assertThat(subscription.getUrl()).isEqualTo("https://new-url.com/hook"); - assertThat(subscription.isActive()).isFalse(); - } - - @Test - @DisplayName("updateWebhookAsync should return future") - void updateWebhookAsyncShouldReturnFuture() throws Exception { - stubFor(put(urlEqualTo("/api/v1/webhooks/wh-456")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"id\":\"wh-456\",\"url\":\"https://example.com\"," - + "\"events\":[],\"active\":true}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest request = - com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder() - .active(true) - .build(); - - CompletableFuture future = - axonflow.updateWebhookAsync("wh-456", request); - com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); - - assertThat(subscription.getId()).isEqualTo("wh-456"); - } - - @Test - @DisplayName("deleteWebhook should require non-null webhookId") - void deleteWebhookShouldRequireWebhookId() { - assertThatThrownBy(() -> axonflow.deleteWebhook(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("deleteWebhook should call delete endpoint") - void deleteWebhookShouldCallDeleteEndpoint() { - stubFor(delete(urlEqualTo("/api/v1/webhooks/wh-123")) - .willReturn(aResponse() - .withStatus(204))); - - axonflow.deleteWebhook("wh-123"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/webhooks/wh-123"))); - } - - @Test - @DisplayName("deleteWebhookAsync should return future") - void deleteWebhookAsyncShouldReturnFuture() throws Exception { - stubFor(delete(urlEqualTo("/api/v1/webhooks/wh-456")) - .willReturn(aResponse() - .withStatus(204))); - - CompletableFuture future = axonflow.deleteWebhookAsync("wh-456"); + } + + @Test + @DisplayName("mcpCheckOutput should handle 403 as blocked result") + void mcpCheckOutputShouldHandle403AsBlockedResult() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": false, \"block_reason\": \"PII detected in output\", " + + "\"policies_evaluated\": 4, " + + "\"redacted_data\": [{\"id\": 1, \"ssn\": \"***REDACTED***\"}], " + + "\"policy_info\": {\"policies_evaluated\": 4, \"blocked\": true, " + + "\"block_reason\": \"PII detected in output\", " + + "\"redactions_applied\": 1, \"processing_time_ms\": 5}}"))); + + List> responseData = List.of(Map.of("id", 1, "ssn", "123-45-6789")); + MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("PII detected in output"); + assertThat(response.getRedactedData()).isNotNull(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().isBlocked()).isTrue(); + } + + @Test + @DisplayName("mcpCheckOutput should handle response with exfiltration info") + void mcpCheckOutputShouldHandleExfiltrationInfo() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 3, " + + "\"exfiltration_info\": {\"rows_returned\": 10, \"row_limit\": 1000, " + + "\"bytes_returned\": 2048, \"byte_limit\": 1048576, \"within_limits\": true}, " + + "\"policy_info\": {\"policies_evaluated\": 3, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 2}}"))); + + List> responseData = List.of(Map.of("id", 1)); + MCPCheckOutputResponse response = axonflow.mcpCheckOutput("postgres", responseData); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getExfiltrationInfo()).isNotNull(); + assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(10); + assertThat(response.getExfiltrationInfo().isWithinLimits()).isTrue(); + } + + @Test + @DisplayName("mcpCheckOutput should throw on 500 error") + void mcpCheckOutputShouldThrowOn500Error() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.mcpCheckOutput("postgres", List.of(Map.of("id", 1)))) + .isInstanceOf(ConnectorException.class) + .hasMessageContaining("Internal server error"); + } + + @Test + @DisplayName("mcpCheckOutput should require non-null connectorType") + void mcpCheckOutputShouldRequireConnectorType() { + assertThatThrownBy(() -> axonflow.mcpCheckOutput(null, List.of())) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("mcpCheckOutput should allow null responseData for execute-style requests") + void mcpCheckOutputShouldAllowNullResponseData() { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 1, " + + "\"policy_info\": {\"policies_evaluated\": 1, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + Map options = new HashMap<>(); + options.put("message", "3 rows updated"); + + MCPCheckOutputResponse resp = axonflow.mcpCheckOutput("postgres", null, options); + assertThat(resp.isAllowed()).isTrue(); + } + + @Test + @DisplayName("mcpCheckOutputAsync should return future") + void mcpCheckOutputAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/mcp/check-output")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\": true, \"policies_evaluated\": 2, " + + "\"policy_info\": {\"policies_evaluated\": 2, \"blocked\": false, " + + "\"redactions_applied\": 0, \"processing_time_ms\": 1}}"))); + + CompletableFuture future = + axonflow.mcpCheckOutputAsync("postgres", List.of(Map.of("id", 1))); + MCPCheckOutputResponse response = future.get(); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(2); + } + + // ======================================================================== + // Rollback Plan + // ======================================================================== + + @Test + @DisplayName("rollbackPlan should require non-null planId") + void rollbackPlanShouldRequirePlanId() { + assertThatThrownBy(() -> axonflow.rollbackPlan(null, 1)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("rollbackPlan should return rollback response") + void rollbackPlanShouldReturnResponse() { + stubFor( + post(urlEqualTo("/api/v1/plan/plan_123/rollback/2")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"plan_id\":\"plan_123\",\"version\":2,\"previous_version\":3,\"status\":\"rolled_back\"}"))); + + RollbackPlanResponse response = axonflow.rollbackPlan("plan_123", 2); + + assertThat(response.getPlanId()).isEqualTo("plan_123"); + assertThat(response.getVersion()).isEqualTo(2); + assertThat(response.getPreviousVersion()).isEqualTo(3); + assertThat(response.getStatus()).isEqualTo("rolled_back"); + } + + @Test + @DisplayName("rollbackPlanAsync should return future") + void rollbackPlanAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/plan/plan_456/rollback/1")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"plan_id\":\"plan_456\",\"version\":1,\"previous_version\":3,\"status\":\"rolled_back\"}"))); + + CompletableFuture future = axonflow.rollbackPlanAsync("plan_456", 1); + RollbackPlanResponse response = future.get(); + + assertThat(response.getPlanId()).isEqualTo("plan_456"); + assertThat(response.getVersion()).isEqualTo(1); + } + + // ======================================================================== + // WCP Approval Methods + // ======================================================================== + + @Test + @DisplayName("approveStep should require non-null workflowId") + void approveStepShouldRequireWorkflowId() { + assertThatThrownBy(() -> axonflow.approveStep(null, "step-1")) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("approveStep should require non-null stepId") + void approveStepShouldRequireStepId() { + assertThatThrownBy(() -> axonflow.approveStep("wf-1", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("approveStep should return approval response") + void approveStepShouldReturnResponse() { + stubFor( + post(urlEqualTo("/api/v1/workflow-control/wf-123/steps/step-1/approve")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"workflow_id\":\"wf-123\",\"step_id\":\"step-1\",\"status\":\"approved\"}"))); + + com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse response = + axonflow.approveStep("wf-123", "step-1"); + + assertThat(response.getWorkflowId()).isEqualTo("wf-123"); + assertThat(response.getStepId()).isEqualTo("step-1"); + assertThat(response.getStatus()).isEqualTo("approved"); + } + + @Test + @DisplayName("approveStepAsync should return future") + void approveStepAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/workflow-control/wf-456/steps/step-2/approve")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"approved\"}"))); + + CompletableFuture future = + axonflow.approveStepAsync("wf-456", "step-2"); + com.getaxonflow.sdk.types.workflow.WorkflowTypes.ApproveStepResponse response = future.get(); + + assertThat(response.getWorkflowId()).isEqualTo("wf-456"); + assertThat(response.getStatus()).isEqualTo("approved"); + } + + @Test + @DisplayName("rejectStep should require non-null workflowId") + void rejectStepShouldRequireWorkflowId() { + assertThatThrownBy(() -> axonflow.rejectStep(null, "step-1")) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("rejectStep should require non-null stepId") + void rejectStepShouldRequireStepId() { + assertThatThrownBy(() -> axonflow.rejectStep("wf-1", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("rejectStep should return rejection response") + void rejectStepShouldReturnResponse() { + stubFor( + post(urlEqualTo("/api/v1/workflow-control/wf-123/steps/step-1/reject")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"workflow_id\":\"wf-123\",\"step_id\":\"step-1\",\"status\":\"rejected\"}"))); + + com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse response = + axonflow.rejectStep("wf-123", "step-1"); + + assertThat(response.getWorkflowId()).isEqualTo("wf-123"); + assertThat(response.getStepId()).isEqualTo("step-1"); + assertThat(response.getStatus()).isEqualTo("rejected"); + } + + @Test + @DisplayName("rejectStepAsync should return future") + void rejectStepAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/workflow-control/wf-789/steps/step-3/reject")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"workflow_id\":\"wf-789\",\"step_id\":\"step-3\",\"status\":\"rejected\"}"))); + + CompletableFuture future = + axonflow.rejectStepAsync("wf-789", "step-3"); + com.getaxonflow.sdk.types.workflow.WorkflowTypes.RejectStepResponse response = future.get(); + + assertThat(response.getWorkflowId()).isEqualTo("wf-789"); + assertThat(response.getStatus()).isEqualTo("rejected"); + } + + @Test + @DisplayName("getPendingApprovals should return pending approvals") + void getPendingApprovalsShouldReturnApprovals() { + stubFor( + get(urlEqualTo("/api/v1/workflow-control/pending-approvals")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"approvals\":[{\"workflow_id\":\"wf-1\",\"workflow_name\":\"Review\"," + + "\"step_id\":\"s-1\",\"step_name\":\"Generate\"," + + "\"step_type\":\"llm_call\",\"created_at\":\"2026-02-07T10:00:00Z\"}],\"total\":1}"))); + + com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = + axonflow.getPendingApprovals(); + + assertThat(response.getTotal()).isEqualTo(1); + assertThat(response.getApprovals()).hasSize(1); + assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); + assertThat(response.getApprovals().get(0).getStepName()).isEqualTo("Generate"); + } + + @Test + @DisplayName("getPendingApprovals with limit should add query parameter") + void getPendingApprovalsWithLimitShouldAddQueryParam() { + stubFor( + get(urlEqualTo("/api/v1/workflow-control/pending-approvals?limit=10")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"approvals\":[],\"total\":0}"))); + + com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = + axonflow.getPendingApprovals(10); + + assertThat(response.getTotal()).isEqualTo(0); + assertThat(response.getApprovals()).isEmpty(); + } + + @Test + @DisplayName("getPendingApprovalsAsync should return future") + void getPendingApprovalsAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/workflow-control/pending-approvals?limit=5")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"approvals\":[],\"total\":0}"))); + + CompletableFuture + future = axonflow.getPendingApprovalsAsync(5); + com.getaxonflow.sdk.types.workflow.WorkflowTypes.PendingApprovalsResponse response = future.get(); - verify(deleteRequestedFor(urlEqualTo("/api/v1/webhooks/wh-456"))); - } - - @Test - @DisplayName("listWebhooks should return list of subscriptions") - void listWebhooksShouldReturnList() { - stubFor(get(urlEqualTo("/api/v1/webhooks")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"webhooks\":[{\"id\":\"wh-1\",\"url\":\"https://example.com\"," - + "\"events\":[\"step.blocked\"],\"active\":true}," - + "{\"id\":\"wh-2\",\"url\":\"https://other.com\"," - + "\"events\":[\"workflow.completed\"],\"active\":false}],\"total\":2}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = - axonflow.listWebhooks(); - - assertThat(response.getTotal()).isEqualTo(2); - assertThat(response.getWebhooks()).hasSize(2); - assertThat(response.getWebhooks().get(0).getId()).isEqualTo("wh-1"); - assertThat(response.getWebhooks().get(1).getId()).isEqualTo("wh-2"); - } - - @Test - @DisplayName("listWebhooksAsync should return future") - void listWebhooksAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/webhooks")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"webhooks\":[],\"total\":0}"))); - - CompletableFuture future = - axonflow.listWebhooksAsync(); - com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = future.get(); - - assertThat(response.getTotal()).isEqualTo(0); - assertThat(response.getWebhooks()).isEmpty(); - } - - @Test - @DisplayName("listWebhooks should return empty list when no webhooks exist") - void listWebhooksShouldReturnEmptyList() { - stubFor(get(urlEqualTo("/api/v1/webhooks")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"webhooks\":[],\"total\":0}"))); - - com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = - axonflow.listWebhooks(); - - assertThat(response.getTotal()).isEqualTo(0); - assertThat(response.getWebhooks()).isEmpty(); - } - - // ======================================================================== - // Unified Execution Streaming (SSE) - // ======================================================================== - - @Test - @DisplayName("streamExecutionStatus should throw NullPointerException for null executionId") - void streamExecutionStatusShouldRejectNullId() { - assertThatThrownBy(() -> axonflow.streamExecutionStatus(null, status -> {})) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("streamExecutionStatus should throw NullPointerException for null callback") - void streamExecutionStatusShouldRejectNullCallback() { - assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("streamExecutionStatus should invoke callback for each SSE event") - void streamExecutionStatusShouldInvokeCallback() { - String runningEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + assertThat(response.getTotal()).isEqualTo(0); + } + + // ======================================================================== + // Webhook CRUD Methods + // ======================================================================== + + @Test + @DisplayName("createWebhook should require non-null request") + void createWebhookShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.createWebhook(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("createWebhook should return created subscription") + void createWebhookShouldReturnSubscription() { + stubFor( + post(urlEqualTo("/api/v1/webhooks")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-123\",\"url\":\"https://example.com/hook\"," + + "\"events\":[\"step.blocked\"],\"active\":true," + + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:00:00Z\"}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest request = + com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest.builder() + .url("https://example.com/hook") + .events(List.of("step.blocked")) + .secret("my-secret") + .active(true) + .build(); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = + axonflow.createWebhook(request); + + assertThat(subscription.getId()).isEqualTo("wh-123"); + assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); + assertThat(subscription.getEvents()).containsExactly("step.blocked"); + assertThat(subscription.isActive()).isTrue(); + } + + @Test + @DisplayName("createWebhookAsync should return future") + void createWebhookAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/webhooks")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-456\",\"url\":\"https://example.com\"," + + "\"events\":[],\"active\":true}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest request = + com.getaxonflow.sdk.types.webhook.WebhookTypes.CreateWebhookRequest.builder() + .url("https://example.com") + .build(); + + CompletableFuture future = + axonflow.createWebhookAsync(request); + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); + + assertThat(subscription.getId()).isEqualTo("wh-456"); + } + + @Test + @DisplayName("getWebhook should require non-null webhookId") + void getWebhookShouldRequireWebhookId() { + assertThatThrownBy(() -> axonflow.getWebhook(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("getWebhook should return subscription") + void getWebhookShouldReturnSubscription() { + stubFor( + get(urlEqualTo("/api/v1/webhooks/wh-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-123\",\"url\":\"https://example.com/hook\"," + + "\"events\":[\"workflow.completed\"],\"active\":true," + + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T11:00:00Z\"}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = + axonflow.getWebhook("wh-123"); + + assertThat(subscription.getId()).isEqualTo("wh-123"); + assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); + assertThat(subscription.getEvents()).containsExactly("workflow.completed"); + } + + @Test + @DisplayName("getWebhookAsync should return future") + void getWebhookAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/webhooks/wh-789")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-789\",\"url\":\"https://example.com\"," + + "\"events\":[],\"active\":true}"))); + + CompletableFuture future = + axonflow.getWebhookAsync("wh-789"); + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); + + assertThat(subscription.getId()).isEqualTo("wh-789"); + } + + @Test + @DisplayName("updateWebhook should require non-null webhookId") + void updateWebhookShouldRequireWebhookId() { + assertThatThrownBy( + () -> + axonflow.updateWebhook( + null, + com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder() + .build())) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateWebhook should require non-null request") + void updateWebhookShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.updateWebhook("wh-1", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateWebhook should return updated subscription") + void updateWebhookShouldReturnUpdatedSubscription() { + stubFor( + put(urlEqualTo("/api/v1/webhooks/wh-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-123\",\"url\":\"https://new-url.com/hook\"," + + "\"events\":[\"step.approved\"],\"active\":false," + + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T12:00:00Z\"}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest request = + com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder() + .url("https://new-url.com/hook") + .events(List.of("step.approved")) + .active(false) + .build(); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = + axonflow.updateWebhook("wh-123", request); + + assertThat(subscription.getId()).isEqualTo("wh-123"); + assertThat(subscription.getUrl()).isEqualTo("https://new-url.com/hook"); + assertThat(subscription.isActive()).isFalse(); + } + + @Test + @DisplayName("updateWebhookAsync should return future") + void updateWebhookAsyncShouldReturnFuture() throws Exception { + stubFor( + put(urlEqualTo("/api/v1/webhooks/wh-456")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"id\":\"wh-456\",\"url\":\"https://example.com\"," + + "\"events\":[],\"active\":true}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest request = + com.getaxonflow.sdk.types.webhook.WebhookTypes.UpdateWebhookRequest.builder() + .active(true) + .build(); + + CompletableFuture future = + axonflow.updateWebhookAsync("wh-456", request); + com.getaxonflow.sdk.types.webhook.WebhookTypes.WebhookSubscription subscription = future.get(); + + assertThat(subscription.getId()).isEqualTo("wh-456"); + } + + @Test + @DisplayName("deleteWebhook should require non-null webhookId") + void deleteWebhookShouldRequireWebhookId() { + assertThatThrownBy(() -> axonflow.deleteWebhook(null)).isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("deleteWebhook should call delete endpoint") + void deleteWebhookShouldCallDeleteEndpoint() { + stubFor(delete(urlEqualTo("/api/v1/webhooks/wh-123")).willReturn(aResponse().withStatus(204))); + + axonflow.deleteWebhook("wh-123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/webhooks/wh-123"))); + } + + @Test + @DisplayName("deleteWebhookAsync should return future") + void deleteWebhookAsyncShouldReturnFuture() throws Exception { + stubFor(delete(urlEqualTo("/api/v1/webhooks/wh-456")).willReturn(aResponse().withStatus(204))); + + CompletableFuture future = axonflow.deleteWebhookAsync("wh-456"); + future.get(); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/webhooks/wh-456"))); + } + + @Test + @DisplayName("listWebhooks should return list of subscriptions") + void listWebhooksShouldReturnList() { + stubFor( + get(urlEqualTo("/api/v1/webhooks")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"webhooks\":[{\"id\":\"wh-1\",\"url\":\"https://example.com\"," + + "\"events\":[\"step.blocked\"],\"active\":true}," + + "{\"id\":\"wh-2\",\"url\":\"https://other.com\"," + + "\"events\":[\"workflow.completed\"],\"active\":false}],\"total\":2}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = + axonflow.listWebhooks(); + + assertThat(response.getTotal()).isEqualTo(2); + assertThat(response.getWebhooks()).hasSize(2); + assertThat(response.getWebhooks().get(0).getId()).isEqualTo("wh-1"); + assertThat(response.getWebhooks().get(1).getId()).isEqualTo("wh-2"); + } + + @Test + @DisplayName("listWebhooksAsync should return future") + void listWebhooksAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/webhooks")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"webhooks\":[],\"total\":0}"))); + + CompletableFuture future = + axonflow.listWebhooksAsync(); + com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = future.get(); + + assertThat(response.getTotal()).isEqualTo(0); + assertThat(response.getWebhooks()).isEmpty(); + } + + @Test + @DisplayName("listWebhooks should return empty list when no webhooks exist") + void listWebhooksShouldReturnEmptyList() { + stubFor( + get(urlEqualTo("/api/v1/webhooks")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"webhooks\":[],\"total\":0}"))); + + com.getaxonflow.sdk.types.webhook.WebhookTypes.ListWebhooksResponse response = + axonflow.listWebhooks(); + + assertThat(response.getTotal()).isEqualTo(0); + assertThat(response.getWebhooks()).isEmpty(); + } + + // ======================================================================== + // Unified Execution Streaming (SSE) + // ======================================================================== + + @Test + @DisplayName("streamExecutionStatus should throw NullPointerException for null executionId") + void streamExecutionStatusShouldRejectNullId() { + assertThatThrownBy(() -> axonflow.streamExecutionStatus(null, status -> {})) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("streamExecutionStatus should throw NullPointerException for null callback") + void streamExecutionStatusShouldRejectNullCallback() { + assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("streamExecutionStatus should invoke callback for each SSE event") + void streamExecutionStatusShouldInvokeCallback() { + String runningEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + "\"name\":\"Test\",\"status\":\"running\",\"current_step_index\":0," + "\"total_steps\":3,\"progress_percent\":33.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"steps\":[],\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:00:00Z\"}\n\n"; - String completedEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + String completedEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + "\"name\":\"Test\",\"status\":\"completed\",\"current_step_index\":2," + "\"total_steps\":3,\"progress_percent\":100.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"completed_at\":\"2026-02-07T10:01:00Z\",\"steps\":[]," + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:01:00Z\"}\n\n"; - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(runningEvent + completedEvent))); - - java.util.List updates = - new java.util.ArrayList<>(); - axonflow.streamExecutionStatus("exec_123", updates::add); - - assertThat(updates).hasSize(2); - assertThat(updates.get(0).getStatus().getValue()).isEqualTo("running"); - assertThat(updates.get(0).getProgressPercent()).isEqualTo(33.0); - assertThat(updates.get(1).getStatus().getValue()).isEqualTo("completed"); - assertThat(updates.get(1).getProgressPercent()).isEqualTo(100.0); - } - - @Test - @DisplayName("streamExecutionStatus should stop on failed terminal status") - void streamExecutionStatusShouldStopOnFailed() { - String failedEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"wcp_workflow\"," + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(runningEvent + completedEvent))); + + java.util.List updates = + new java.util.ArrayList<>(); + axonflow.streamExecutionStatus("exec_123", updates::add); + + assertThat(updates).hasSize(2); + assertThat(updates.get(0).getStatus().getValue()).isEqualTo("running"); + assertThat(updates.get(0).getProgressPercent()).isEqualTo(33.0); + assertThat(updates.get(1).getStatus().getValue()).isEqualTo("completed"); + assertThat(updates.get(1).getProgressPercent()).isEqualTo(100.0); + } + + @Test + @DisplayName("streamExecutionStatus should stop on failed terminal status") + void streamExecutionStatusShouldStopOnFailed() { + String failedEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"wcp_workflow\"," + "\"name\":\"Test\",\"status\":\"failed\",\"current_step_index\":1," + "\"total_steps\":3,\"progress_percent\":33.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"error\":\"Step 2 timed out\",\"steps\":[]," + "\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:00:30Z\"}\n\n"; - // Add extra data after failed - should not be consumed - String extraEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"wcp_workflow\"," + // Add extra data after failed - should not be consumed + String extraEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"wcp_workflow\"," + "\"name\":\"Test\",\"status\":\"running\",\"current_step_index\":2," + "\"total_steps\":3,\"progress_percent\":66.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"steps\":[],\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:00:45Z\"}\n\n"; - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(failedEvent + extraEvent))); - - java.util.List updates = - new java.util.ArrayList<>(); - axonflow.streamExecutionStatus("exec_123", updates::add); - - assertThat(updates).hasSize(1); - assertThat(updates.get(0).getStatus().getValue()).isEqualTo("failed"); - assertThat(updates.get(0).getError()).isEqualTo("Step 2 timed out"); - } - - @Test - @DisplayName("streamExecutionStatus should skip [DONE] sentinel") - void streamExecutionStatusShouldSkipDone() { - String completedEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(failedEvent + extraEvent))); + + java.util.List updates = + new java.util.ArrayList<>(); + axonflow.streamExecutionStatus("exec_123", updates::add); + + assertThat(updates).hasSize(1); + assertThat(updates.get(0).getStatus().getValue()).isEqualTo("failed"); + assertThat(updates.get(0).getError()).isEqualTo("Step 2 timed out"); + } + + @Test + @DisplayName("streamExecutionStatus should skip [DONE] sentinel") + void streamExecutionStatusShouldSkipDone() { + String completedEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + "\"name\":\"Test\",\"status\":\"completed\",\"current_step_index\":2," + "\"total_steps\":2,\"progress_percent\":100.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"steps\":[],\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:01:00Z\"}\n\n"; - String doneEvent = "data: [DONE]\n\n"; - - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(completedEvent + doneEvent))); - - java.util.List updates = - new java.util.ArrayList<>(); - axonflow.streamExecutionStatus("exec_123", updates::add); - - assertThat(updates).hasSize(1); - assertThat(updates.get(0).getStatus().getValue()).isEqualTo("completed"); - } - - @Test - @DisplayName("streamExecutionStatus should throw on 401") - void streamExecutionStatusShouldThrowOn401() { - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(401) - .withBody("Unauthorized"))); - - assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", status -> {})) - .isInstanceOf(AuthenticationException.class); - } - - @Test - @DisplayName("streamExecutionStatus should throw on 404") - void streamExecutionStatusShouldThrowOn404() { - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(404) - .withBody("{\"error\":\"Execution not found\"}"))); - - assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", status -> {})) - .isInstanceOf(AxonFlowException.class); - } - - @Test - @DisplayName("streamExecutionStatus should handle malformed JSON gracefully") - void streamExecutionStatusShouldHandleMalformedJson() { - String malformedEvent = "data: {invalid json}\n\n"; - String completedEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + String doneEvent = "data: [DONE]\n\n"; + + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(completedEvent + doneEvent))); + + java.util.List updates = + new java.util.ArrayList<>(); + axonflow.streamExecutionStatus("exec_123", updates::add); + + assertThat(updates).hasSize(1); + assertThat(updates.get(0).getStatus().getValue()).isEqualTo("completed"); + } + + @Test + @DisplayName("streamExecutionStatus should throw on 401") + void streamExecutionStatusShouldThrowOn401() { + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn(aResponse().withStatus(401).withBody("Unauthorized"))); + + assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", status -> {})) + .isInstanceOf(AuthenticationException.class); + } + + @Test + @DisplayName("streamExecutionStatus should throw on 404") + void streamExecutionStatusShouldThrowOn404() { + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse().withStatus(404).withBody("{\"error\":\"Execution not found\"}"))); + + assertThatThrownBy(() -> axonflow.streamExecutionStatus("exec_123", status -> {})) + .isInstanceOf(AxonFlowException.class); + } + + @Test + @DisplayName("streamExecutionStatus should handle malformed JSON gracefully") + void streamExecutionStatusShouldHandleMalformedJson() { + String malformedEvent = "data: {invalid json}\n\n"; + String completedEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + "\"name\":\"Test\",\"status\":\"completed\",\"current_step_index\":1," + "\"total_steps\":1,\"progress_percent\":100.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"steps\":[],\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:01:00Z\"}\n\n"; - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(malformedEvent + completedEvent))); - - java.util.List updates = - new java.util.ArrayList<>(); - axonflow.streamExecutionStatus("exec_123", updates::add); - - // Should skip malformed event and get the completed one - assertThat(updates).hasSize(1); - assertThat(updates.get(0).getStatus().getValue()).isEqualTo("completed"); - } - - @Test - @DisplayName("streamExecutionStatus should send correct request headers") - void streamExecutionStatusShouldSendCorrectHeaders() { - String completedEvent = "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(malformedEvent + completedEvent))); + + java.util.List updates = + new java.util.ArrayList<>(); + axonflow.streamExecutionStatus("exec_123", updates::add); + + // Should skip malformed event and get the completed one + assertThat(updates).hasSize(1); + assertThat(updates.get(0).getStatus().getValue()).isEqualTo("completed"); + } + + @Test + @DisplayName("streamExecutionStatus should send correct request headers") + void streamExecutionStatusShouldSendCorrectHeaders() { + String completedEvent = + "data: {\"execution_id\":\"exec_123\",\"execution_type\":\"map_plan\"," + "\"name\":\"Test\",\"status\":\"completed\",\"current_step_index\":0," + "\"total_steps\":1,\"progress_percent\":100.0,\"started_at\":\"2026-02-07T10:00:00Z\"," + "\"steps\":[],\"created_at\":\"2026-02-07T10:00:00Z\",\"updated_at\":\"2026-02-07T10:01:00Z\"}\n\n"; - stubFor(get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "text/event-stream") - .withBody(completedEvent))); + stubFor( + get(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "text/event-stream") + .withBody(completedEvent))); - axonflow.streamExecutionStatus("exec_123", status -> {}); + axonflow.streamExecutionStatus("exec_123", status -> {}); - verify(getRequestedFor(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) + verify( + getRequestedFor(urlEqualTo("/api/v1/unified/executions/exec_123/stream")) .withHeader("Accept", equalTo("text/event-stream"))); - } - - // ======================================================================== - // Media Cache Skip - // ======================================================================== - - @Test - @DisplayName("proxyLLMCall should skip cache with media") - void proxyLLMCallShouldSkipCacheWithMedia() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - MediaContent mediaItem = MediaContent.builder() + } + + // ======================================================================== + // Media Cache Skip + // ======================================================================== + + @Test + @DisplayName("proxyLLMCall should skip cache with media") + void proxyLLMCallShouldSkipCacheWithMedia() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + MediaContent mediaItem = + MediaContent.builder() .source("base64") .mimeType("image/png") .base64Data("dGVzdC1pbWFnZQ==") .build(); - ClientRequest request = ClientRequest.builder() + ClientRequest request = + ClientRequest.builder() .query("describe image") .userToken("user-123") .requestType(RequestType.CHAT) .media(List.of(mediaItem)) .build(); - // First call - axonflow.proxyLLMCall(request); - // Second call — should NOT use cache - axonflow.proxyLLMCall(request); - - // Both calls should hit the server (no caching for media) - verify(exactly(2), postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("proxyLLMCall should use cache without media") - void proxyLLMCallShouldUseCacheWithoutMedia() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - ClientRequest request = ClientRequest.builder() + // First call + axonflow.proxyLLMCall(request); + // Second call — should NOT use cache + axonflow.proxyLLMCall(request); + + // Both calls should hit the server (no caching for media) + verify(exactly(2), postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("proxyLLMCall should use cache without media") + void proxyLLMCallShouldUseCacheWithoutMedia() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + ClientRequest request = + ClientRequest.builder() .query("hello") .userToken("user-123") .requestType(RequestType.CHAT) .build(); - // First call - axonflow.proxyLLMCall(request); - // Second call — should use cache - axonflow.proxyLLMCall(request); + // First call + axonflow.proxyLLMCall(request); + // Second call — should use cache + axonflow.proxyLLMCall(request); - // Only one call should hit the server (second cached) - verify(exactly(1), postRequestedFor(urlEqualTo("/api/request"))); - } + // Only one call should hit the server (second cached) + verify(exactly(1), postRequestedFor(urlEqualTo("/api/request"))); + } } diff --git a/src/test/java/com/getaxonflow/sdk/CircuitBreakerTest.java b/src/test/java/com/getaxonflow/sdk/CircuitBreakerTest.java index b54dee7..88d8122 100644 --- a/src/test/java/com/getaxonflow/sdk/CircuitBreakerTest.java +++ b/src/test/java/com/getaxonflow/sdk/CircuitBreakerTest.java @@ -15,319 +15,360 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.exceptions.AxonFlowException; import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import java.util.concurrent.CompletableFuture; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for circuit breaker observability methods. - */ +/** Tests for circuit breaker observability methods. */ @WireMockTest @DisplayName("Circuit Breaker Observability") class CircuitBreakerTest { - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - // ======================================================================== - // getCircuitBreakerStatus - // ======================================================================== - - @Test - @DisplayName("should get circuit breaker status with active circuits") - void shouldGetCircuitBreakerStatus() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"active_circuits\":[{\"scope\":\"provider\",\"scope_id\":\"openai\",\"state\":\"open\",\"error_count\":15}],\"count\":1,\"emergency_stop_active\":false}}"))); - - CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); - - assertThat(status).isNotNull(); - assertThat(status.getCount()).isEqualTo(1); - assertThat(status.isEmergencyStopActive()).isFalse(); - assertThat(status.getActiveCircuits()).hasSize(1); - assertThat(status.getActiveCircuits().get(0).get("scope")).isEqualTo("provider"); - assertThat(status.getActiveCircuits().get(0).get("scope_id")).isEqualTo("openai"); - - verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/status"))); - } - - @Test - @DisplayName("should get circuit breaker status with no active circuits") - void shouldGetCircuitBreakerStatusEmpty() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}}"))); - - CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); - - assertThat(status).isNotNull(); - assertThat(status.getCount()).isEqualTo(0); - assertThat(status.getActiveCircuits()).isEmpty(); - } - - @Test - @DisplayName("should get circuit breaker status with emergency stop active") - void shouldGetCircuitBreakerStatusEmergencyStop() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":true}}"))); - - CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); - - assertThat(status.isEmergencyStopActive()).isTrue(); - } - - @Test - @DisplayName("getCircuitBreakerStatusAsync should return future") - void getCircuitBreakerStatusAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}}"))); - - CompletableFuture future = axonflow.getCircuitBreakerStatusAsync(); - CircuitBreakerStatusResponse status = future.get(); - - assertThat(status).isNotNull(); - assertThat(status.getCount()).isEqualTo(0); - } - - @Test - @DisplayName("should handle server error on status") - void shouldHandleServerErrorOnStatus() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getCircuitBreakerStatus()) - .isInstanceOf(AxonFlowException.class); - } - - // ======================================================================== - // getCircuitBreakerHistory - // ======================================================================== - - @Test - @DisplayName("should get circuit breaker history") - void shouldGetCircuitBreakerHistory() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/history?limit=10")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"history\":[{\"id\":\"cb_001\",\"org_id\":\"org_1\",\"scope\":\"provider\",\"scope_id\":\"openai\",\"state\":\"open\",\"trip_reason\":\"error_threshold\",\"tripped_by\":\"system\",\"tripped_at\":\"2026-03-16T10:00:00Z\",\"expires_at\":\"2026-03-16T10:05:00Z\",\"reset_by\":null,\"reset_at\":null,\"error_count\":15,\"violation_count\":0}],\"count\":1}}"))); - - CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(10); - - assertThat(history).isNotNull(); - assertThat(history.getCount()).isEqualTo(1); - assertThat(history.getHistory()).hasSize(1); - - CircuitBreakerHistoryEntry entry = history.getHistory().get(0); - assertThat(entry.getId()).isEqualTo("cb_001"); - assertThat(entry.getOrgId()).isEqualTo("org_1"); - assertThat(entry.getScope()).isEqualTo("provider"); - assertThat(entry.getScopeId()).isEqualTo("openai"); - assertThat(entry.getState()).isEqualTo("open"); - assertThat(entry.getTripReason()).isEqualTo("error_threshold"); - assertThat(entry.getTrippedBy()).isEqualTo("system"); - assertThat(entry.getTrippedAt()).isEqualTo("2026-03-16T10:00:00Z"); - assertThat(entry.getExpiresAt()).isEqualTo("2026-03-16T10:05:00Z"); - assertThat(entry.getResetBy()).isNull(); - assertThat(entry.getResetAt()).isNull(); - assertThat(entry.getErrorCount()).isEqualTo(15); - assertThat(entry.getViolationCount()).isEqualTo(0); - - verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/history?limit=10"))); - } - - @Test - @DisplayName("should get empty circuit breaker history") - void shouldGetEmptyCircuitBreakerHistory() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/history?limit=50")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"history\":[],\"count\":0}}"))); - - CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50); - - assertThat(history).isNotNull(); - assertThat(history.getCount()).isEqualTo(0); - assertThat(history.getHistory()).isEmpty(); - } - - @Test - @DisplayName("should reject invalid limit") - void shouldRejectInvalidLimit() { - assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(0)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("limit must be at least 1"); - - assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(-1)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("limit must be at least 1"); - } - - @Test - @DisplayName("getCircuitBreakerHistoryAsync should return future") - void getCircuitBreakerHistoryAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/history?limit=25")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"history\":[],\"count\":0}}"))); - - CompletableFuture future = axonflow.getCircuitBreakerHistoryAsync(25); - CircuitBreakerHistoryResponse history = future.get(); - - assertThat(history).isNotNull(); - assertThat(history.getCount()).isEqualTo(0); - } - - @Test - @DisplayName("should handle server error on history") - void shouldHandleServerErrorOnHistory() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/history?limit=10")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(10)) - .isInstanceOf(AxonFlowException.class); - } - - // ======================================================================== - // getCircuitBreakerConfig - // ======================================================================== - - @Test - @DisplayName("should get circuit breaker config for tenant") - void shouldGetCircuitBreakerConfig() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=tenant_123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"source\":\"tenant_override\",\"error_threshold\":10,\"violation_threshold\":5,\"window_seconds\":300,\"default_timeout_seconds\":60,\"max_timeout_seconds\":600,\"enable_auto_recovery\":true,\"tenant_id\":\"tenant_123\",\"overrides\":{\"provider_openai\":{\"error_threshold\":20}}}}"))); - - CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123"); - - assertThat(config).isNotNull(); - assertThat(config.getSource()).isEqualTo("tenant_override"); - assertThat(config.getErrorThreshold()).isEqualTo(10); - assertThat(config.getViolationThreshold()).isEqualTo(5); - assertThat(config.getWindowSeconds()).isEqualTo(300); - assertThat(config.getDefaultTimeoutSeconds()).isEqualTo(60); - assertThat(config.getMaxTimeoutSeconds()).isEqualTo(600); - assertThat(config.isEnableAutoRecovery()).isTrue(); - assertThat(config.getTenantId()).isEqualTo("tenant_123"); - assertThat(config.getOverrides()).isNotNull(); - assertThat(config.getOverrides()).containsKey("provider_openai"); - - verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=tenant_123"))); - } - - @Test - @DisplayName("should get circuit breaker config with defaults") - void shouldGetCircuitBreakerConfigDefaults() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=new_tenant")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"source\":\"default\",\"error_threshold\":5,\"violation_threshold\":3,\"window_seconds\":60,\"default_timeout_seconds\":30,\"max_timeout_seconds\":300,\"enable_auto_recovery\":false,\"tenant_id\":\"new_tenant\"}}"))); - - CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("new_tenant"); - - assertThat(config).isNotNull(); - assertThat(config.getSource()).isEqualTo("default"); - assertThat(config.getOverrides()).isNull(); - } - - @Test - @DisplayName("should reject null tenantId for getConfig") - void shouldRejectNullTenantIdForGetConfig() { - assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("tenantId cannot be null"); - } - - @Test - @DisplayName("should reject empty tenantId for getConfig") - void shouldRejectEmptyTenantIdForGetConfig() { - assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("tenantId cannot be empty"); - } - - @Test - @DisplayName("getCircuitBreakerConfigAsync should return future") - void getCircuitBreakerConfigAsyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=async_tenant")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"source\":\"default\",\"error_threshold\":5,\"violation_threshold\":3,\"window_seconds\":60,\"default_timeout_seconds\":30,\"max_timeout_seconds\":300,\"enable_auto_recovery\":false,\"tenant_id\":\"async_tenant\"}}"))); - - CompletableFuture future = axonflow.getCircuitBreakerConfigAsync("async_tenant"); - CircuitBreakerConfig config = future.get(); - - assertThat(config).isNotNull(); - assertThat(config.getTenantId()).isEqualTo("async_tenant"); - } - - @Test - @DisplayName("should handle server error on getConfig") - void shouldHandleServerErrorOnGetConfig() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=bad_tenant")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig("bad_tenant")) - .isInstanceOf(AxonFlowException.class); - } - - // ======================================================================== - // updateCircuitBreakerConfig - // ======================================================================== - - @Test - @DisplayName("should update circuit breaker config") - void shouldUpdateCircuitBreakerConfig() { - stubFor(put(urlEqualTo("/api/v1/circuit-breaker/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"tenant_id\":\"tenant_123\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); - - CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder() + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + } + + // ======================================================================== + // getCircuitBreakerStatus + // ======================================================================== + + @Test + @DisplayName("should get circuit breaker status with active circuits") + void shouldGetCircuitBreakerStatus() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"active_circuits\":[{\"scope\":\"provider\",\"scope_id\":\"openai\",\"state\":\"open\",\"error_count\":15}],\"count\":1,\"emergency_stop_active\":false}}"))); + + CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); + + assertThat(status).isNotNull(); + assertThat(status.getCount()).isEqualTo(1); + assertThat(status.isEmergencyStopActive()).isFalse(); + assertThat(status.getActiveCircuits()).hasSize(1); + assertThat(status.getActiveCircuits().get(0).get("scope")).isEqualTo("provider"); + assertThat(status.getActiveCircuits().get(0).get("scope_id")).isEqualTo("openai"); + + verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/status"))); + } + + @Test + @DisplayName("should get circuit breaker status with no active circuits") + void shouldGetCircuitBreakerStatusEmpty() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}}"))); + + CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); + + assertThat(status).isNotNull(); + assertThat(status.getCount()).isEqualTo(0); + assertThat(status.getActiveCircuits()).isEmpty(); + } + + @Test + @DisplayName("should get circuit breaker status with emergency stop active") + void shouldGetCircuitBreakerStatusEmergencyStop() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":true}}"))); + + CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); + + assertThat(status.isEmergencyStopActive()).isTrue(); + } + + @Test + @DisplayName("getCircuitBreakerStatusAsync should return future") + void getCircuitBreakerStatusAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}}"))); + + CompletableFuture future = + axonflow.getCircuitBreakerStatusAsync(); + CircuitBreakerStatusResponse status = future.get(); + + assertThat(status).isNotNull(); + assertThat(status.getCount()).isEqualTo(0); + } + + @Test + @DisplayName("should handle server error on status") + void shouldHandleServerErrorOnStatus() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getCircuitBreakerStatus()) + .isInstanceOf(AxonFlowException.class); + } + + // ======================================================================== + // getCircuitBreakerHistory + // ======================================================================== + + @Test + @DisplayName("should get circuit breaker history") + void shouldGetCircuitBreakerHistory() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/history?limit=10")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"history\":[{\"id\":\"cb_001\",\"org_id\":\"org_1\",\"scope\":\"provider\",\"scope_id\":\"openai\",\"state\":\"open\",\"trip_reason\":\"error_threshold\",\"tripped_by\":\"system\",\"tripped_at\":\"2026-03-16T10:00:00Z\",\"expires_at\":\"2026-03-16T10:05:00Z\",\"reset_by\":null,\"reset_at\":null,\"error_count\":15,\"violation_count\":0}],\"count\":1}}"))); + + CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(10); + + assertThat(history).isNotNull(); + assertThat(history.getCount()).isEqualTo(1); + assertThat(history.getHistory()).hasSize(1); + + CircuitBreakerHistoryEntry entry = history.getHistory().get(0); + assertThat(entry.getId()).isEqualTo("cb_001"); + assertThat(entry.getOrgId()).isEqualTo("org_1"); + assertThat(entry.getScope()).isEqualTo("provider"); + assertThat(entry.getScopeId()).isEqualTo("openai"); + assertThat(entry.getState()).isEqualTo("open"); + assertThat(entry.getTripReason()).isEqualTo("error_threshold"); + assertThat(entry.getTrippedBy()).isEqualTo("system"); + assertThat(entry.getTrippedAt()).isEqualTo("2026-03-16T10:00:00Z"); + assertThat(entry.getExpiresAt()).isEqualTo("2026-03-16T10:05:00Z"); + assertThat(entry.getResetBy()).isNull(); + assertThat(entry.getResetAt()).isNull(); + assertThat(entry.getErrorCount()).isEqualTo(15); + assertThat(entry.getViolationCount()).isEqualTo(0); + + verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/history?limit=10"))); + } + + @Test + @DisplayName("should get empty circuit breaker history") + void shouldGetEmptyCircuitBreakerHistory() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/history?limit=50")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"data\":{\"history\":[],\"count\":0}}"))); + + CircuitBreakerHistoryResponse history = axonflow.getCircuitBreakerHistory(50); + + assertThat(history).isNotNull(); + assertThat(history.getCount()).isEqualTo(0); + assertThat(history.getHistory()).isEmpty(); + } + + @Test + @DisplayName("should reject invalid limit") + void shouldRejectInvalidLimit() { + assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(0)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("limit must be at least 1"); + + assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(-1)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("limit must be at least 1"); + } + + @Test + @DisplayName("getCircuitBreakerHistoryAsync should return future") + void getCircuitBreakerHistoryAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/history?limit=25")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"data\":{\"history\":[],\"count\":0}}"))); + + CompletableFuture future = + axonflow.getCircuitBreakerHistoryAsync(25); + CircuitBreakerHistoryResponse history = future.get(); + + assertThat(history).isNotNull(); + assertThat(history.getCount()).isEqualTo(0); + } + + @Test + @DisplayName("should handle server error on history") + void shouldHandleServerErrorOnHistory() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/history?limit=10")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getCircuitBreakerHistory(10)) + .isInstanceOf(AxonFlowException.class); + } + + // ======================================================================== + // getCircuitBreakerConfig + // ======================================================================== + + @Test + @DisplayName("should get circuit breaker config for tenant") + void shouldGetCircuitBreakerConfig() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=tenant_123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"source\":\"tenant_override\",\"error_threshold\":10,\"violation_threshold\":5,\"window_seconds\":300,\"default_timeout_seconds\":60,\"max_timeout_seconds\":600,\"enable_auto_recovery\":true,\"tenant_id\":\"tenant_123\",\"overrides\":{\"provider_openai\":{\"error_threshold\":20}}}}"))); + + CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("tenant_123"); + + assertThat(config).isNotNull(); + assertThat(config.getSource()).isEqualTo("tenant_override"); + assertThat(config.getErrorThreshold()).isEqualTo(10); + assertThat(config.getViolationThreshold()).isEqualTo(5); + assertThat(config.getWindowSeconds()).isEqualTo(300); + assertThat(config.getDefaultTimeoutSeconds()).isEqualTo(60); + assertThat(config.getMaxTimeoutSeconds()).isEqualTo(600); + assertThat(config.isEnableAutoRecovery()).isTrue(); + assertThat(config.getTenantId()).isEqualTo("tenant_123"); + assertThat(config.getOverrides()).isNotNull(); + assertThat(config.getOverrides()).containsKey("provider_openai"); + + verify(getRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=tenant_123"))); + } + + @Test + @DisplayName("should get circuit breaker config with defaults") + void shouldGetCircuitBreakerConfigDefaults() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=new_tenant")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"source\":\"default\",\"error_threshold\":5,\"violation_threshold\":3,\"window_seconds\":60,\"default_timeout_seconds\":30,\"max_timeout_seconds\":300,\"enable_auto_recovery\":false,\"tenant_id\":\"new_tenant\"}}"))); + + CircuitBreakerConfig config = axonflow.getCircuitBreakerConfig("new_tenant"); + + assertThat(config).isNotNull(); + assertThat(config.getSource()).isEqualTo("default"); + assertThat(config.getOverrides()).isNull(); + } + + @Test + @DisplayName("should reject null tenantId for getConfig") + void shouldRejectNullTenantIdForGetConfig() { + assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("tenantId cannot be null"); + } + + @Test + @DisplayName("should reject empty tenantId for getConfig") + void shouldRejectEmptyTenantIdForGetConfig() { + assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("tenantId cannot be empty"); + } + + @Test + @DisplayName("getCircuitBreakerConfigAsync should return future") + void getCircuitBreakerConfigAsyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=async_tenant")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"source\":\"default\",\"error_threshold\":5,\"violation_threshold\":3,\"window_seconds\":60,\"default_timeout_seconds\":30,\"max_timeout_seconds\":300,\"enable_auto_recovery\":false,\"tenant_id\":\"async_tenant\"}}"))); + + CompletableFuture future = + axonflow.getCircuitBreakerConfigAsync("async_tenant"); + CircuitBreakerConfig config = future.get(); + + assertThat(config).isNotNull(); + assertThat(config.getTenantId()).isEqualTo("async_tenant"); + } + + @Test + @DisplayName("should handle server error on getConfig") + void shouldHandleServerErrorOnGetConfig() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/config?tenant_id=bad_tenant")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getCircuitBreakerConfig("bad_tenant")) + .isInstanceOf(AxonFlowException.class); + } + + // ======================================================================== + // updateCircuitBreakerConfig + // ======================================================================== + + @Test + @DisplayName("should update circuit breaker config") + void shouldUpdateCircuitBreakerConfig() { + stubFor( + put(urlEqualTo("/api/v1/circuit-breaker/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"tenant_id\":\"tenant_123\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); + + CircuitBreakerConfigUpdate update = + CircuitBreakerConfigUpdate.builder() .tenantId("tenant_123") .errorThreshold(10) .violationThreshold(5) @@ -337,121 +378,129 @@ void shouldUpdateCircuitBreakerConfig() { .enableAutoRecovery(true) .build(); - CircuitBreakerConfigUpdateResponse result = axonflow.updateCircuitBreakerConfig(update); + CircuitBreakerConfigUpdateResponse result = axonflow.updateCircuitBreakerConfig(update); - assertThat(result).isNotNull(); - assertThat(result.getTenantId()).isEqualTo("tenant_123"); - assertThat(result.getMessage()).isNotEmpty(); + assertThat(result).isNotNull(); + assertThat(result.getTenantId()).isEqualTo("tenant_123"); + assertThat(result.getMessage()).isNotEmpty(); - verify(putRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config")) + verify( + putRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config")) .withRequestBody(matchingJsonPath("$.tenant_id", equalTo("tenant_123"))) .withRequestBody(matchingJsonPath("$.error_threshold", equalTo("10"))) .withRequestBody(matchingJsonPath("$.violation_threshold", equalTo("5")))); - } - - @Test - @DisplayName("should update circuit breaker config with partial fields") - void shouldUpdateCircuitBreakerConfigPartial() { - stubFor(put(urlEqualTo("/api/v1/circuit-breaker/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"tenant_id\":\"tenant_456\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); - - CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder() - .tenantId("tenant_456") - .errorThreshold(20) - .build(); - - CircuitBreakerConfigUpdateResponse result = axonflow.updateCircuitBreakerConfig(update); - - assertThat(result).isNotNull(); - assertThat(result.getTenantId()).isEqualTo("tenant_456"); - - verify(putRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config")) + } + + @Test + @DisplayName("should update circuit breaker config with partial fields") + void shouldUpdateCircuitBreakerConfigPartial() { + stubFor( + put(urlEqualTo("/api/v1/circuit-breaker/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"tenant_id\":\"tenant_456\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); + + CircuitBreakerConfigUpdate update = + CircuitBreakerConfigUpdate.builder().tenantId("tenant_456").errorThreshold(20).build(); + + CircuitBreakerConfigUpdateResponse result = axonflow.updateCircuitBreakerConfig(update); + + assertThat(result).isNotNull(); + assertThat(result.getTenantId()).isEqualTo("tenant_456"); + + verify( + putRequestedFor(urlEqualTo("/api/v1/circuit-breaker/config")) .withRequestBody(matchingJsonPath("$.tenant_id", equalTo("tenant_456"))) .withRequestBody(matchingJsonPath("$.error_threshold", equalTo("20")))); - } - - @Test - @DisplayName("should reject null config for update") - void shouldRejectNullConfigForUpdate() { - assertThatThrownBy(() -> axonflow.updateCircuitBreakerConfig(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("config cannot be null"); - } - - @Test - @DisplayName("should reject null tenantId in config update builder") - void shouldRejectNullTenantIdInConfigUpdateBuilder() { - assertThatThrownBy(() -> CircuitBreakerConfigUpdate.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("tenantId cannot be null"); - } - - @Test - @DisplayName("should reject empty tenantId in config update builder") - void shouldRejectEmptyTenantIdInConfigUpdateBuilder() { - assertThatThrownBy(() -> CircuitBreakerConfigUpdate.builder().tenantId("").build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("tenantId cannot be empty"); - } - - @Test - @DisplayName("updateCircuitBreakerConfigAsync should return future") - void updateCircuitBreakerConfigAsyncShouldReturnFuture() throws Exception { - stubFor(put(urlEqualTo("/api/v1/circuit-breaker/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"tenant_id\":\"tenant_async\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); - - CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder() - .tenantId("tenant_async") - .errorThreshold(10) - .build(); - - CompletableFuture future = axonflow.updateCircuitBreakerConfigAsync(update); - CircuitBreakerConfigUpdateResponse result = future.get(); - - assertThat(result).isNotNull(); - assertThat(result.getTenantId()).isEqualTo("tenant_async"); - } - - @Test - @DisplayName("should handle server error on updateConfig") - void shouldHandleServerErrorOnUpdateConfig() { - stubFor(put(urlEqualTo("/api/v1/circuit-breaker/config")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - CircuitBreakerConfigUpdate update = CircuitBreakerConfigUpdate.builder() - .tenantId("failing_tenant") - .errorThreshold(10) - .build(); - - assertThatThrownBy(() -> axonflow.updateCircuitBreakerConfig(update)) - .isInstanceOf(AxonFlowException.class); - } - - // ======================================================================== - // Response without wrapper (fallback) - // ======================================================================== - - @Test - @DisplayName("should handle unwrapped response for status") - void shouldHandleUnwrappedResponseForStatus() { - stubFor(get(urlEqualTo("/api/v1/circuit-breaker/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}"))); - - CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); - - assertThat(status).isNotNull(); - assertThat(status.getCount()).isEqualTo(0); - } + } + + @Test + @DisplayName("should reject null config for update") + void shouldRejectNullConfigForUpdate() { + assertThatThrownBy(() -> axonflow.updateCircuitBreakerConfig(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("config cannot be null"); + } + + @Test + @DisplayName("should reject null tenantId in config update builder") + void shouldRejectNullTenantIdInConfigUpdateBuilder() { + assertThatThrownBy(() -> CircuitBreakerConfigUpdate.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("tenantId cannot be null"); + } + + @Test + @DisplayName("should reject empty tenantId in config update builder") + void shouldRejectEmptyTenantIdInConfigUpdateBuilder() { + assertThatThrownBy(() -> CircuitBreakerConfigUpdate.builder().tenantId("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("tenantId cannot be empty"); + } + + @Test + @DisplayName("updateCircuitBreakerConfigAsync should return future") + void updateCircuitBreakerConfigAsyncShouldReturnFuture() throws Exception { + stubFor( + put(urlEqualTo("/api/v1/circuit-breaker/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"tenant_id\":\"tenant_async\",\"message\":\"Circuit breaker config updated for tenant\"}}"))); + + CircuitBreakerConfigUpdate update = + CircuitBreakerConfigUpdate.builder().tenantId("tenant_async").errorThreshold(10).build(); + + CompletableFuture future = + axonflow.updateCircuitBreakerConfigAsync(update); + CircuitBreakerConfigUpdateResponse result = future.get(); + + assertThat(result).isNotNull(); + assertThat(result.getTenantId()).isEqualTo("tenant_async"); + } + + @Test + @DisplayName("should handle server error on updateConfig") + void shouldHandleServerErrorOnUpdateConfig() { + stubFor( + put(urlEqualTo("/api/v1/circuit-breaker/config")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + CircuitBreakerConfigUpdate update = + CircuitBreakerConfigUpdate.builder().tenantId("failing_tenant").errorThreshold(10).build(); + + assertThatThrownBy(() -> axonflow.updateCircuitBreakerConfig(update)) + .isInstanceOf(AxonFlowException.class); + } + + // ======================================================================== + // Response without wrapper (fallback) + // ======================================================================== + + @Test + @DisplayName("should handle unwrapped response for status") + void shouldHandleUnwrappedResponseForStatus() { + stubFor( + get(urlEqualTo("/api/v1/circuit-breaker/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"active_circuits\":[],\"count\":0,\"emergency_stop_active\":false}"))); + + CircuitBreakerStatusResponse status = axonflow.getCircuitBreakerStatus(); + + assertThat(status).isNotNull(); + assertThat(status.getCount()).isEqualTo(0); + } } diff --git a/src/test/java/com/getaxonflow/sdk/CodeGovernanceTest.java b/src/test/java/com/getaxonflow/sdk/CodeGovernanceTest.java index 5129e5c..81d812a 100644 --- a/src/test/java/com/getaxonflow/sdk/CodeGovernanceTest.java +++ b/src/test/java/com/getaxonflow/sdk/CodeGovernanceTest.java @@ -15,349 +15,341 @@ */ package com.getaxonflow.sdk; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.codegovernance.*; +import java.time.Instant; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.time.Instant; -import java.util.List; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("Code Governance Types") class CodeGovernanceTest { - // ======================================================================== - // GitProviderType Enum - // ======================================================================== - - @Nested - @DisplayName("GitProviderType") - class GitProviderTypeTests { - - @Test - @DisplayName("getValue should return correct string") - void getValueShouldReturnCorrectString() { - assertThat(GitProviderType.GITHUB.getValue()).isEqualTo("github"); - assertThat(GitProviderType.GITLAB.getValue()).isEqualTo("gitlab"); - assertThat(GitProviderType.BITBUCKET.getValue()).isEqualTo("bitbucket"); - } - - @Test - @DisplayName("fromValue should return correct enum") - void fromValueShouldReturnCorrectEnum() { - assertThat(GitProviderType.fromValue("github")).isEqualTo(GitProviderType.GITHUB); - assertThat(GitProviderType.fromValue("gitlab")).isEqualTo(GitProviderType.GITLAB); - assertThat(GitProviderType.fromValue("bitbucket")).isEqualTo(GitProviderType.BITBUCKET); - } - - @Test - @DisplayName("fromValue should be case insensitive") - void fromValueShouldBeCaseInsensitive() { - assertThat(GitProviderType.fromValue("GITHUB")).isEqualTo(GitProviderType.GITHUB); - assertThat(GitProviderType.fromValue("GitHub")).isEqualTo(GitProviderType.GITHUB); - } - - @Test - @DisplayName("fromValue should throw for invalid value") - void fromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> GitProviderType.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown Git provider type"); - } + // ======================================================================== + // GitProviderType Enum + // ======================================================================== + + @Nested + @DisplayName("GitProviderType") + class GitProviderTypeTests { + + @Test + @DisplayName("getValue should return correct string") + void getValueShouldReturnCorrectString() { + assertThat(GitProviderType.GITHUB.getValue()).isEqualTo("github"); + assertThat(GitProviderType.GITLAB.getValue()).isEqualTo("gitlab"); + assertThat(GitProviderType.BITBUCKET.getValue()).isEqualTo("bitbucket"); + } + + @Test + @DisplayName("fromValue should return correct enum") + void fromValueShouldReturnCorrectEnum() { + assertThat(GitProviderType.fromValue("github")).isEqualTo(GitProviderType.GITHUB); + assertThat(GitProviderType.fromValue("gitlab")).isEqualTo(GitProviderType.GITLAB); + assertThat(GitProviderType.fromValue("bitbucket")).isEqualTo(GitProviderType.BITBUCKET); + } + + @Test + @DisplayName("fromValue should be case insensitive") + void fromValueShouldBeCaseInsensitive() { + assertThat(GitProviderType.fromValue("GITHUB")).isEqualTo(GitProviderType.GITHUB); + assertThat(GitProviderType.fromValue("GitHub")).isEqualTo(GitProviderType.GITHUB); + } + + @Test + @DisplayName("fromValue should throw for invalid value") + void fromValueShouldThrowForInvalid() { + assertThatThrownBy(() -> GitProviderType.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown Git provider type"); + } + } + + // ======================================================================== + // FileAction Enum + // ======================================================================== + + @Nested + @DisplayName("FileAction") + class FileActionTests { + + @Test + @DisplayName("getValue should return correct string") + void getValueShouldReturnCorrectString() { + assertThat(FileAction.CREATE.getValue()).isEqualTo("create"); + assertThat(FileAction.UPDATE.getValue()).isEqualTo("update"); + assertThat(FileAction.DELETE.getValue()).isEqualTo("delete"); + } + + @Test + @DisplayName("fromValue should return correct enum") + void fromValueShouldReturnCorrectEnum() { + assertThat(FileAction.fromValue("create")).isEqualTo(FileAction.CREATE); + assertThat(FileAction.fromValue("update")).isEqualTo(FileAction.UPDATE); + assertThat(FileAction.fromValue("delete")).isEqualTo(FileAction.DELETE); } - // ======================================================================== - // FileAction Enum - // ======================================================================== - - @Nested - @DisplayName("FileAction") - class FileActionTests { - - @Test - @DisplayName("getValue should return correct string") - void getValueShouldReturnCorrectString() { - assertThat(FileAction.CREATE.getValue()).isEqualTo("create"); - assertThat(FileAction.UPDATE.getValue()).isEqualTo("update"); - assertThat(FileAction.DELETE.getValue()).isEqualTo("delete"); - } - - @Test - @DisplayName("fromValue should return correct enum") - void fromValueShouldReturnCorrectEnum() { - assertThat(FileAction.fromValue("create")).isEqualTo(FileAction.CREATE); - assertThat(FileAction.fromValue("update")).isEqualTo(FileAction.UPDATE); - assertThat(FileAction.fromValue("delete")).isEqualTo(FileAction.DELETE); - } - - @Test - @DisplayName("fromValue should be case insensitive") - void fromValueShouldBeCaseInsensitive() { - assertThat(FileAction.fromValue("CREATE")).isEqualTo(FileAction.CREATE); - assertThat(FileAction.fromValue("Update")).isEqualTo(FileAction.UPDATE); - } - - @Test - @DisplayName("fromValue should throw for invalid value") - void fromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> FileAction.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown file action"); - } + @Test + @DisplayName("fromValue should be case insensitive") + void fromValueShouldBeCaseInsensitive() { + assertThat(FileAction.fromValue("CREATE")).isEqualTo(FileAction.CREATE); + assertThat(FileAction.fromValue("Update")).isEqualTo(FileAction.UPDATE); } - // ======================================================================== - // CodeFile - // ======================================================================== - - @Nested - @DisplayName("CodeFile") - class CodeFileTests { - - @Test - @DisplayName("builder should create CodeFile with all fields") - void builderShouldCreateCodeFile() { - CodeFile file = CodeFile.builder() - .path("src/main/java/App.java") - .content("public class App {}") - .action(FileAction.CREATE) - .language("java") - .build(); - - assertThat(file.getPath()).isEqualTo("src/main/java/App.java"); - assertThat(file.getContent()).isEqualTo("public class App {}"); - assertThat(file.getAction()).isEqualTo(FileAction.CREATE); - assertThat(file.getLanguage()).isEqualTo("java"); - } - - @Test - @DisplayName("builder should create CodeFile with minimal fields") - void builderShouldCreateMinimalCodeFile() { - CodeFile file = CodeFile.builder() - .path("src/test.py") - .content("print('hello')") - .action(FileAction.UPDATE) - .build(); - - assertThat(file.getPath()).isEqualTo("src/test.py"); - assertThat(file.getLanguage()).isNull(); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void equalsAndHashCodeShouldWork() { - CodeFile file1 = CodeFile.builder() - .path("src/App.java") - .content("code") - .action(FileAction.CREATE) - .build(); - - CodeFile file2 = CodeFile.builder() - .path("src/App.java") - .content("code") - .action(FileAction.CREATE) - .build(); - - assertThat(file1).isEqualTo(file2); - assertThat(file1.hashCode()).isEqualTo(file2.hashCode()); - } - - @Test - @DisplayName("toString should return non-empty string") - void toStringShouldWork() { - CodeFile file = CodeFile.builder() - .path("src/App.java") - .content("code") - .action(FileAction.CREATE) - .build(); - - assertThat(file.toString()).contains("src/App.java"); - } + @Test + @DisplayName("fromValue should throw for invalid value") + void fromValueShouldThrowForInvalid() { + assertThatThrownBy(() -> FileAction.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown file action"); + } + } + + // ======================================================================== + // CodeFile + // ======================================================================== + + @Nested + @DisplayName("CodeFile") + class CodeFileTests { + + @Test + @DisplayName("builder should create CodeFile with all fields") + void builderShouldCreateCodeFile() { + CodeFile file = + CodeFile.builder() + .path("src/main/java/App.java") + .content("public class App {}") + .action(FileAction.CREATE) + .language("java") + .build(); + + assertThat(file.getPath()).isEqualTo("src/main/java/App.java"); + assertThat(file.getContent()).isEqualTo("public class App {}"); + assertThat(file.getAction()).isEqualTo(FileAction.CREATE); + assertThat(file.getLanguage()).isEqualTo("java"); } - // ======================================================================== - // CreatePRRequest - // ======================================================================== - - @Nested - @DisplayName("CreatePRRequest") - class CreatePRRequestTests { - - @Test - @DisplayName("builder should create request with all fields") - void builderShouldCreateRequest() { - CodeFile file = CodeFile.builder() - .path("src/App.java") - .content("code") - .action(FileAction.CREATE) - .build(); - - CreatePRRequest request = CreatePRRequest.builder() - .owner("owner") - .repo("repo") - .title("Add feature") - .description("Feature description") - .baseBranch("main") - .branchName("feature-branch") - .draft(false) - .files(List.of(file)) - .agentRequestId("req-123") - .model("gpt-4") - .build(); - - assertThat(request.getOwner()).isEqualTo("owner"); - assertThat(request.getRepo()).isEqualTo("repo"); - assertThat(request.getTitle()).isEqualTo("Add feature"); - assertThat(request.getDescription()).isEqualTo("Feature description"); - assertThat(request.getBaseBranch()).isEqualTo("main"); - assertThat(request.getBranchName()).isEqualTo("feature-branch"); - assertThat(request.isDraft()).isFalse(); - assertThat(request.getFiles()).hasSize(1); - assertThat(request.getAgentRequestId()).isEqualTo("req-123"); - assertThat(request.getModel()).isEqualTo("gpt-4"); - } - - @Test - @DisplayName("equals and hashCode should work") - void equalsAndHashCodeShouldWork() { - CreatePRRequest r1 = CreatePRRequest.builder() - .owner("owner").repo("repo").title("title").build(); - CreatePRRequest r2 = CreatePRRequest.builder() - .owner("owner").repo("repo").title("title").build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } - - @Test - @DisplayName("toString should work") - void toStringShouldWork() { - CreatePRRequest request = CreatePRRequest.builder() - .owner("owner").repo("repo").title("title").build(); - - assertThat(request.toString()).contains("owner"); - } + @Test + @DisplayName("builder should create CodeFile with minimal fields") + void builderShouldCreateMinimalCodeFile() { + CodeFile file = + CodeFile.builder() + .path("src/test.py") + .content("print('hello')") + .action(FileAction.UPDATE) + .build(); + + assertThat(file.getPath()).isEqualTo("src/test.py"); + assertThat(file.getLanguage()).isNull(); } - // ======================================================================== - // CreatePRResponse - // ======================================================================== - - @Nested - @DisplayName("CreatePRResponse") - class CreatePRResponseTests { - - @Test - @DisplayName("constructor should create response with all fields") - void constructorShouldCreateResponse() { - Instant now = Instant.now(); - CreatePRResponse response = new CreatePRResponse( - "pr-123", 42, "https://github.com/owner/repo/pull/42", - "open", "feature-branch", now); - - assertThat(response.getPrId()).isEqualTo("pr-123"); - assertThat(response.getPrNumber()).isEqualTo(42); - assertThat(response.getPrUrl()).isEqualTo("https://github.com/owner/repo/pull/42"); - assertThat(response.getState()).isEqualTo("open"); - assertThat(response.getHeadBranch()).isEqualTo("feature-branch"); - assertThat(response.getCreatedAt()).isEqualTo(now); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void equalsAndHashCodeShouldWork() { - Instant now = Instant.now(); - CreatePRResponse r1 = new CreatePRResponse("pr-123", 42, "url", "open", "branch", now); - CreatePRResponse r2 = new CreatePRResponse("pr-123", 42, "url", "open", "branch", now); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } - - @Test - @DisplayName("toString should return non-empty string") - void toStringShouldWork() { - CreatePRResponse response = new CreatePRResponse( - "pr-123", 42, "url", "open", "branch", Instant.now()); - - assertThat(response.toString()).contains("pr-123"); - } + @Test + @DisplayName("equals and hashCode should work correctly") + void equalsAndHashCodeShouldWork() { + CodeFile file1 = + CodeFile.builder().path("src/App.java").content("code").action(FileAction.CREATE).build(); + + CodeFile file2 = + CodeFile.builder().path("src/App.java").content("code").action(FileAction.CREATE).build(); + + assertThat(file1).isEqualTo(file2); + assertThat(file1.hashCode()).isEqualTo(file2.hashCode()); } - // ======================================================================== - // ValidateGitProviderRequest - // ======================================================================== - - @Nested - @DisplayName("ValidateGitProviderRequest") - class ValidateGitProviderRequestTests { - - @Test - @DisplayName("builder should create request") - void builderShouldCreateRequest() { - ValidateGitProviderRequest request = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("ghp_xxx") - .baseUrl("https://github.com") - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); - assertThat(request.getToken()).isEqualTo("ghp_xxx"); - assertThat(request.getBaseUrl()).isEqualTo("https://github.com"); - } + @Test + @DisplayName("toString should return non-empty string") + void toStringShouldWork() { + CodeFile file = + CodeFile.builder().path("src/App.java").content("code").action(FileAction.CREATE).build(); + + assertThat(file.toString()).contains("src/App.java"); + } + } + + // ======================================================================== + // CreatePRRequest + // ======================================================================== + + @Nested + @DisplayName("CreatePRRequest") + class CreatePRRequestTests { + + @Test + @DisplayName("builder should create request with all fields") + void builderShouldCreateRequest() { + CodeFile file = + CodeFile.builder().path("src/App.java").content("code").action(FileAction.CREATE).build(); + + CreatePRRequest request = + CreatePRRequest.builder() + .owner("owner") + .repo("repo") + .title("Add feature") + .description("Feature description") + .baseBranch("main") + .branchName("feature-branch") + .draft(false) + .files(List.of(file)) + .agentRequestId("req-123") + .model("gpt-4") + .build(); + + assertThat(request.getOwner()).isEqualTo("owner"); + assertThat(request.getRepo()).isEqualTo("repo"); + assertThat(request.getTitle()).isEqualTo("Add feature"); + assertThat(request.getDescription()).isEqualTo("Feature description"); + assertThat(request.getBaseBranch()).isEqualTo("main"); + assertThat(request.getBranchName()).isEqualTo("feature-branch"); + assertThat(request.isDraft()).isFalse(); + assertThat(request.getFiles()).hasSize(1); + assertThat(request.getAgentRequestId()).isEqualTo("req-123"); + assertThat(request.getModel()).isEqualTo("gpt-4"); } - // ======================================================================== - // ValidateGitProviderResponse - // ======================================================================== + @Test + @DisplayName("equals and hashCode should work") + void equalsAndHashCodeShouldWork() { + CreatePRRequest r1 = + CreatePRRequest.builder().owner("owner").repo("repo").title("title").build(); + CreatePRRequest r2 = + CreatePRRequest.builder().owner("owner").repo("repo").title("title").build(); - @Nested - @DisplayName("ValidateGitProviderResponse") - class ValidateGitProviderResponseTests { + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } - @Test - @DisplayName("constructor should create response with all fields") - void constructorShouldCreateResponse() { - ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, "Validation successful"); + @Test + @DisplayName("toString should work") + void toStringShouldWork() { + CreatePRRequest request = + CreatePRRequest.builder().owner("owner").repo("repo").title("title").build(); - assertThat(response.isValid()).isTrue(); - assertThat(response.getMessage()).isEqualTo("Validation successful"); - } + assertThat(request.toString()).contains("owner"); + } + } + + // ======================================================================== + // CreatePRResponse + // ======================================================================== + + @Nested + @DisplayName("CreatePRResponse") + class CreatePRResponseTests { + + @Test + @DisplayName("constructor should create response with all fields") + void constructorShouldCreateResponse() { + Instant now = Instant.now(); + CreatePRResponse response = + new CreatePRResponse( + "pr-123", 42, "https://github.com/owner/repo/pull/42", "open", "feature-branch", now); + + assertThat(response.getPrId()).isEqualTo("pr-123"); + assertThat(response.getPrNumber()).isEqualTo(42); + assertThat(response.getPrUrl()).isEqualTo("https://github.com/owner/repo/pull/42"); + assertThat(response.getState()).isEqualTo("open"); + assertThat(response.getHeadBranch()).isEqualTo("feature-branch"); + assertThat(response.getCreatedAt()).isEqualTo(now); + } - @Test - @DisplayName("equals and hashCode should work") - void equalsAndHashCodeShouldWork() { - ValidateGitProviderResponse r1 = new ValidateGitProviderResponse(true, "ok"); - ValidateGitProviderResponse r2 = new ValidateGitProviderResponse(true, "ok"); + @Test + @DisplayName("equals and hashCode should work correctly") + void equalsAndHashCodeShouldWork() { + Instant now = Instant.now(); + CreatePRResponse r1 = new CreatePRResponse("pr-123", 42, "url", "open", "branch", now); + CreatePRResponse r2 = new CreatePRResponse("pr-123", 42, "url", "open", "branch", now); - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); } - // ======================================================================== - // ConfigureGitProviderRequest - // ======================================================================== - - @Nested - @DisplayName("ConfigureGitProviderRequest") - class ConfigureGitProviderRequestTests { - - @Test - @DisplayName("builder should create request") - void builderShouldCreateRequest() { - ConfigureGitProviderRequest request = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("ghp_xxx") - .baseUrl("https://github.com") - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); - assertThat(request.getToken()).isEqualTo("ghp_xxx"); - assertThat(request.getBaseUrl()).isEqualTo("https://github.com"); - } + @Test + @DisplayName("toString should return non-empty string") + void toStringShouldWork() { + CreatePRResponse response = + new CreatePRResponse("pr-123", 42, "url", "open", "branch", Instant.now()); + + assertThat(response.toString()).contains("pr-123"); } + } + + // ======================================================================== + // ValidateGitProviderRequest + // ======================================================================== + + @Nested + @DisplayName("ValidateGitProviderRequest") + class ValidateGitProviderRequestTests { + + @Test + @DisplayName("builder should create request") + void builderShouldCreateRequest() { + ValidateGitProviderRequest request = + ValidateGitProviderRequest.builder() + .type(GitProviderType.GITHUB) + .token("ghp_xxx") + .baseUrl("https://github.com") + .build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); + assertThat(request.getToken()).isEqualTo("ghp_xxx"); + assertThat(request.getBaseUrl()).isEqualTo("https://github.com"); + } + } + + // ======================================================================== + // ValidateGitProviderResponse + // ======================================================================== + + @Nested + @DisplayName("ValidateGitProviderResponse") + class ValidateGitProviderResponseTests { + @Test + @DisplayName("constructor should create response with all fields") + void constructorShouldCreateResponse() { + ValidateGitProviderResponse response = + new ValidateGitProviderResponse(true, "Validation successful"); + + assertThat(response.isValid()).isTrue(); + assertThat(response.getMessage()).isEqualTo("Validation successful"); + } + + @Test + @DisplayName("equals and hashCode should work") + void equalsAndHashCodeShouldWork() { + ValidateGitProviderResponse r1 = new ValidateGitProviderResponse(true, "ok"); + ValidateGitProviderResponse r2 = new ValidateGitProviderResponse(true, "ok"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } + } + + // ======================================================================== + // ConfigureGitProviderRequest + // ======================================================================== + + @Nested + @DisplayName("ConfigureGitProviderRequest") + class ConfigureGitProviderRequestTests { + + @Test + @DisplayName("builder should create request") + void builderShouldCreateRequest() { + ConfigureGitProviderRequest request = + ConfigureGitProviderRequest.builder() + .type(GitProviderType.GITHUB) + .token("ghp_xxx") + .baseUrl("https://github.com") + .build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); + assertThat(request.getToken()).isEqualTo("ghp_xxx"); + assertThat(request.getBaseUrl()).isEqualTo("https://github.com"); + } + } } diff --git a/src/test/java/com/getaxonflow/sdk/HITLTest.java b/src/test/java/com/getaxonflow/sdk/HITLTest.java index b83abe4..5cfcda9 100644 --- a/src/test/java/com/getaxonflow/sdk/HITLTest.java +++ b/src/test/java/com/getaxonflow/sdk/HITLTest.java @@ -15,415 +15,460 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.hitl.HITLTypes.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for HITL (Human-in-the-Loop) Queue API methods. - */ +/** Tests for HITL (Human-in-the-Loop) Queue API methods. */ @WireMockTest @DisplayName("HITL Queue Methods") class HITLTest { - private AxonFlow axonflow; - - private static final String SAMPLE_APPROVAL_REQUEST = - "{" + - "\"request_id\": \"hitl_req_001\"," + - "\"org_id\": \"org_123\"," + - "\"tenant_id\": \"tenant_456\"," + - "\"client_id\": \"client_789\"," + - "\"user_id\": \"user_abc\"," + - "\"original_query\": \"Transfer $50,000 to account 12345\"," + - "\"request_type\": \"llm_chat\"," + - "\"request_context\": {\"session_id\": \"sess_001\"}," + - "\"triggered_policy_id\": \"pol_high_value\"," + - "\"triggered_policy_name\": \"High Value Transaction Check\"," + - "\"trigger_reason\": \"Transaction amount exceeds $10,000 threshold\"," + - "\"severity\": \"high\"," + - "\"eu_ai_act_article\": \"Article 14\"," + - "\"compliance_framework\": \"EU AI Act\"," + - "\"risk_classification\": \"high-risk\"," + - "\"status\": \"pending\"," + - "\"expires_at\": \"2026-02-13T00:00:00Z\"," + - "\"created_at\": \"2026-02-12T12:00:00Z\"," + - "\"updated_at\": \"2026-02-12T12:00:00Z\"" + - "}"; - - private static final String SAMPLE_APPROVAL_REQUEST_2 = - "{" + - "\"request_id\": \"hitl_req_002\"," + - "\"org_id\": \"org_123\"," + - "\"tenant_id\": \"tenant_456\"," + - "\"client_id\": \"client_789\"," + - "\"original_query\": \"Access patient medical records\"," + - "\"request_type\": \"llm_chat\"," + - "\"triggered_policy_id\": \"pol_hipaa\"," + - "\"triggered_policy_name\": \"HIPAA PHI Access Control\"," + - "\"trigger_reason\": \"PHI access requires human approval\"," + - "\"severity\": \"critical\"," + - "\"status\": \"pending\"," + - "\"expires_at\": \"2026-02-13T00:00:00Z\"," + - "\"created_at\": \"2026-02-12T12:30:00Z\"," + - "\"updated_at\": \"2026-02-12T12:30:00Z\"" + - "}"; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .build()); + private AxonFlow axonflow; + + private static final String SAMPLE_APPROVAL_REQUEST = + "{" + + "\"request_id\": \"hitl_req_001\"," + + "\"org_id\": \"org_123\"," + + "\"tenant_id\": \"tenant_456\"," + + "\"client_id\": \"client_789\"," + + "\"user_id\": \"user_abc\"," + + "\"original_query\": \"Transfer $50,000 to account 12345\"," + + "\"request_type\": \"llm_chat\"," + + "\"request_context\": {\"session_id\": \"sess_001\"}," + + "\"triggered_policy_id\": \"pol_high_value\"," + + "\"triggered_policy_name\": \"High Value Transaction Check\"," + + "\"trigger_reason\": \"Transaction amount exceeds $10,000 threshold\"," + + "\"severity\": \"high\"," + + "\"eu_ai_act_article\": \"Article 14\"," + + "\"compliance_framework\": \"EU AI Act\"," + + "\"risk_classification\": \"high-risk\"," + + "\"status\": \"pending\"," + + "\"expires_at\": \"2026-02-13T00:00:00Z\"," + + "\"created_at\": \"2026-02-12T12:00:00Z\"," + + "\"updated_at\": \"2026-02-12T12:00:00Z\"" + + "}"; + + private static final String SAMPLE_APPROVAL_REQUEST_2 = + "{" + + "\"request_id\": \"hitl_req_002\"," + + "\"org_id\": \"org_123\"," + + "\"tenant_id\": \"tenant_456\"," + + "\"client_id\": \"client_789\"," + + "\"original_query\": \"Access patient medical records\"," + + "\"request_type\": \"llm_chat\"," + + "\"triggered_policy_id\": \"pol_hipaa\"," + + "\"triggered_policy_name\": \"HIPAA PHI Access Control\"," + + "\"trigger_reason\": \"PHI access requires human approval\"," + + "\"severity\": \"critical\"," + + "\"status\": \"pending\"," + + "\"expires_at\": \"2026-02-13T00:00:00Z\"," + + "\"created_at\": \"2026-02-12T12:30:00Z\"," + + "\"updated_at\": \"2026-02-12T12:30:00Z\"" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // listHITLQueue Tests + // ======================================================================== + + @Nested + @DisplayName("listHITLQueue") + class ListHITLQueue { + + @Test + @DisplayName("should return approval requests from queue") + void shouldReturnApprovalRequests() { + stubFor( + get(urlPathEqualTo("/api/v1/hitl/queue")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [" + + SAMPLE_APPROVAL_REQUEST + + "," + + SAMPLE_APPROVAL_REQUEST_2 + + "], \"meta\": {\"total\": 2, \"limit\": 50, \"offset\": 0, \"has_more\": false}}"))); + + HITLQueueListResponse result = axonflow.listHITLQueue(); + + assertThat(result.getItems()).hasSize(2); + assertThat(result.getTotal()).isEqualTo(2); + assertThat(result.isHasMore()).isFalse(); + + HITLApprovalRequest first = result.getItems().get(0); + assertThat(first.getRequestId()).isEqualTo("hitl_req_001"); + assertThat(first.getOrgId()).isEqualTo("org_123"); + assertThat(first.getTenantId()).isEqualTo("tenant_456"); + assertThat(first.getOriginalQuery()).isEqualTo("Transfer $50,000 to account 12345"); + assertThat(first.getTriggeredPolicyName()).isEqualTo("High Value Transaction Check"); + assertThat(first.getSeverity()).isEqualTo("high"); + assertThat(first.getStatus()).isEqualTo("pending"); + assertThat(first.getEuAiActArticle()).isEqualTo("Article 14"); + assertThat(first.getComplianceFramework()).isEqualTo("EU AI Act"); + assertThat(first.getRiskClassification()).isEqualTo("high-risk"); + } + + @Test + @DisplayName("should return empty list when no items") + void shouldReturnEmptyList() { + stubFor( + get(urlPathEqualTo("/api/v1/hitl/queue")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [], \"meta\": {\"total\": 0, \"has_more\": false}}"))); + + HITLQueueListResponse result = axonflow.listHITLQueue(); + + assertThat(result.getItems()).isEmpty(); + assertThat(result.getTotal()).isEqualTo(0); } - // ======================================================================== - // listHITLQueue Tests - // ======================================================================== - - @Nested - @DisplayName("listHITLQueue") - class ListHITLQueue { - - @Test - @DisplayName("should return approval requests from queue") - void shouldReturnApprovalRequests() { - stubFor(get(urlPathEqualTo("/api/v1/hitl/queue")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [" + - SAMPLE_APPROVAL_REQUEST + "," + SAMPLE_APPROVAL_REQUEST_2 + - "], \"meta\": {\"total\": 2, \"limit\": 50, \"offset\": 0, \"has_more\": false}}"))); - - HITLQueueListResponse result = axonflow.listHITLQueue(); - - assertThat(result.getItems()).hasSize(2); - assertThat(result.getTotal()).isEqualTo(2); - assertThat(result.isHasMore()).isFalse(); - - HITLApprovalRequest first = result.getItems().get(0); - assertThat(first.getRequestId()).isEqualTo("hitl_req_001"); - assertThat(first.getOrgId()).isEqualTo("org_123"); - assertThat(first.getTenantId()).isEqualTo("tenant_456"); - assertThat(first.getOriginalQuery()).isEqualTo("Transfer $50,000 to account 12345"); - assertThat(first.getTriggeredPolicyName()).isEqualTo("High Value Transaction Check"); - assertThat(first.getSeverity()).isEqualTo("high"); - assertThat(first.getStatus()).isEqualTo("pending"); - assertThat(first.getEuAiActArticle()).isEqualTo("Article 14"); - assertThat(first.getComplianceFramework()).isEqualTo("EU AI Act"); - assertThat(first.getRiskClassification()).isEqualTo("high-risk"); - } - - @Test - @DisplayName("should return empty list when no items") - void shouldReturnEmptyList() { - stubFor(get(urlPathEqualTo("/api/v1/hitl/queue")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [], \"meta\": {\"total\": 0, \"has_more\": false}}"))); - - HITLQueueListResponse result = axonflow.listHITLQueue(); - - assertThat(result.getItems()).isEmpty(); - assertThat(result.getTotal()).isEqualTo(0); - } - - @Test - @DisplayName("should include query params when options provided") - void shouldIncludeQueryParams() { - stubFor(get(urlPathEqualTo("/api/v1/hitl/queue")) - .withQueryParam("status", equalTo("pending")) - .withQueryParam("severity", equalTo("critical")) - .withQueryParam("limit", equalTo("10")) - .withQueryParam("offset", equalTo("5")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [" + SAMPLE_APPROVAL_REQUEST_2 + - "], \"meta\": {\"total\": 1, \"has_more\": false}}"))); - - HITLQueueListOptions opts = HITLQueueListOptions.builder() - .status("pending") - .severity("critical") - .limit(10) - .offset(5) - .build(); - - HITLQueueListResponse result = axonflow.listHITLQueue(opts); - - assertThat(result.getItems()).hasSize(1); - assertThat(result.getItems().get(0).getSeverity()).isEqualTo("critical"); - } - - @Test - @DisplayName("should handle has_more pagination flag") - void shouldHandleHasMore() { - stubFor(get(urlPathEqualTo("/api/v1/hitl/queue")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": [" + SAMPLE_APPROVAL_REQUEST + - "], \"meta\": {\"total\": 100, \"limit\": 1, \"offset\": 0, \"has_more\": true}}"))); - - HITLQueueListResponse result = axonflow.listHITLQueue( - HITLQueueListOptions.builder().limit(1).build()); - - assertThat(result.getItems()).hasSize(1); - assertThat(result.getTotal()).isEqualTo(100); - assertThat(result.isHasMore()).isTrue(); - } + @Test + @DisplayName("should include query params when options provided") + void shouldIncludeQueryParams() { + stubFor( + get(urlPathEqualTo("/api/v1/hitl/queue")) + .withQueryParam("status", equalTo("pending")) + .withQueryParam("severity", equalTo("critical")) + .withQueryParam("limit", equalTo("10")) + .withQueryParam("offset", equalTo("5")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [" + + SAMPLE_APPROVAL_REQUEST_2 + + "], \"meta\": {\"total\": 1, \"has_more\": false}}"))); + + HITLQueueListOptions opts = + HITLQueueListOptions.builder() + .status("pending") + .severity("critical") + .limit(10) + .offset(5) + .build(); + + HITLQueueListResponse result = axonflow.listHITLQueue(opts); + + assertThat(result.getItems()).hasSize(1); + assertThat(result.getItems().get(0).getSeverity()).isEqualTo("critical"); + } + + @Test + @DisplayName("should handle has_more pagination flag") + void shouldHandleHasMore() { + stubFor( + get(urlPathEqualTo("/api/v1/hitl/queue")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": [" + + SAMPLE_APPROVAL_REQUEST + + "], \"meta\": {\"total\": 100, \"limit\": 1, \"offset\": 0, \"has_more\": true}}"))); + + HITLQueueListResponse result = + axonflow.listHITLQueue(HITLQueueListOptions.builder().limit(1).build()); + + assertThat(result.getItems()).hasSize(1); + assertThat(result.getTotal()).isEqualTo(100); + assertThat(result.isHasMore()).isTrue(); + } + } + + // ======================================================================== + // getHITLRequest Tests + // ======================================================================== + + @Nested + @DisplayName("getHITLRequest") + class GetHITLRequest { + + @Test + @DisplayName("should return approval request by ID") + void shouldReturnRequestById() { + stubFor( + get(urlEqualTo("/api/v1/hitl/queue/hitl_req_001")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\": true, \"data\": " + SAMPLE_APPROVAL_REQUEST + "}"))); + + HITLApprovalRequest result = axonflow.getHITLRequest("hitl_req_001"); + + assertThat(result.getRequestId()).isEqualTo("hitl_req_001"); + assertThat(result.getTriggeredPolicyId()).isEqualTo("pol_high_value"); + assertThat(result.getTriggerReason()) + .isEqualTo("Transaction amount exceeds $10,000 threshold"); + assertThat(result.getUserId()).isEqualTo("user_abc"); + assertThat(result.getRequestContext()).containsEntry("session_id", "sess_001"); } - // ======================================================================== - // getHITLRequest Tests - // ======================================================================== - - @Nested - @DisplayName("getHITLRequest") - class GetHITLRequest { - - @Test - @DisplayName("should return approval request by ID") - void shouldReturnRequestById() { - stubFor(get(urlEqualTo("/api/v1/hitl/queue/hitl_req_001")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": " + SAMPLE_APPROVAL_REQUEST + "}"))); - - HITLApprovalRequest result = axonflow.getHITLRequest("hitl_req_001"); - - assertThat(result.getRequestId()).isEqualTo("hitl_req_001"); - assertThat(result.getTriggeredPolicyId()).isEqualTo("pol_high_value"); - assertThat(result.getTriggerReason()).isEqualTo("Transaction amount exceeds $10,000 threshold"); - assertThat(result.getUserId()).isEqualTo("user_abc"); - assertThat(result.getRequestContext()).containsEntry("session_id", "sess_001"); - } - - @Test - @DisplayName("should require non-null requestId") - void shouldRequireRequestId() { - assertThatThrownBy(() -> axonflow.getHITLRequest(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should throw on 404 not found") - void shouldThrowOnNotFound() { - stubFor(get(urlEqualTo("/api/v1/hitl/queue/nonexistent")) - .willReturn(aResponse() - .withStatus(404) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Approval request not found\"}"))); - - assertThatThrownBy(() -> axonflow.getHITLRequest("nonexistent")) - .isInstanceOf(Exception.class); - } + @Test + @DisplayName("should require non-null requestId") + void shouldRequireRequestId() { + assertThatThrownBy(() -> axonflow.getHITLRequest(null)) + .isInstanceOf(NullPointerException.class); } - // ======================================================================== - // approveHITLRequest Tests - // ======================================================================== - - @Nested - @DisplayName("approveHITLRequest") - class ApproveHITLRequest { - - @Test - @DisplayName("should send approve request with review input") - void shouldSendApproveRequest() { - stubFor(post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true}"))); - - HITLReviewInput review = HITLReviewInput.builder() - .reviewerId("reviewer_001") - .reviewerEmail("reviewer@example.com") - .reviewerRole("compliance_officer") - .comment("Approved after manual verification") - .build(); - - axonflow.approveHITLRequest("hitl_req_001", review); - - verify(postRequestedFor(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) - .withHeader("Content-Type", containing("application/json")) - .withRequestBody(containing("\"reviewer_id\":\"reviewer_001\"")) - .withRequestBody(containing("\"reviewer_email\":\"reviewer@example.com\"")) - .withRequestBody(containing("\"reviewer_role\":\"compliance_officer\"")) - .withRequestBody(containing("\"comment\":\"Approved after manual verification\""))); - } - - @Test - @DisplayName("should require non-null requestId") - void shouldRequireRequestId() { - HITLReviewInput review = HITLReviewInput.builder() - .reviewerId("reviewer_001") - .reviewerEmail("reviewer@example.com") - .build(); - - assertThatThrownBy(() -> axonflow.approveHITLRequest(null, review)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should require non-null review") - void shouldRequireReview() { - assertThatThrownBy(() -> axonflow.approveHITLRequest("hitl_req_001", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should handle server error") - void shouldHandleServerError() { - stubFor(post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Internal server error\"}"))); - - HITLReviewInput review = HITLReviewInput.builder() - .reviewerId("reviewer_001") - .reviewerEmail("reviewer@example.com") - .build(); - - assertThatThrownBy(() -> axonflow.approveHITLRequest("hitl_req_001", review)) - .isInstanceOf(Exception.class); - } + @Test + @DisplayName("should throw on 404 not found") + void shouldThrowOnNotFound() { + stubFor( + get(urlEqualTo("/api/v1/hitl/queue/nonexistent")) + .willReturn( + aResponse() + .withStatus(404) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Approval request not found\"}"))); + + assertThatThrownBy(() -> axonflow.getHITLRequest("nonexistent")) + .isInstanceOf(Exception.class); + } + } + + // ======================================================================== + // approveHITLRequest Tests + // ======================================================================== + + @Nested + @DisplayName("approveHITLRequest") + class ApproveHITLRequest { + + @Test + @DisplayName("should send approve request with review input") + void shouldSendApproveRequest() { + stubFor( + post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\": true}"))); + + HITLReviewInput review = + HITLReviewInput.builder() + .reviewerId("reviewer_001") + .reviewerEmail("reviewer@example.com") + .reviewerRole("compliance_officer") + .comment("Approved after manual verification") + .build(); + + axonflow.approveHITLRequest("hitl_req_001", review); + + verify( + postRequestedFor(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(containing("\"reviewer_id\":\"reviewer_001\"")) + .withRequestBody(containing("\"reviewer_email\":\"reviewer@example.com\"")) + .withRequestBody(containing("\"reviewer_role\":\"compliance_officer\"")) + .withRequestBody(containing("\"comment\":\"Approved after manual verification\""))); + } + + @Test + @DisplayName("should require non-null requestId") + void shouldRequireRequestId() { + HITLReviewInput review = + HITLReviewInput.builder() + .reviewerId("reviewer_001") + .reviewerEmail("reviewer@example.com") + .build(); + + assertThatThrownBy(() -> axonflow.approveHITLRequest(null, review)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should require non-null review") + void shouldRequireReview() { + assertThatThrownBy(() -> axonflow.approveHITLRequest("hitl_req_001", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should handle server error") + void shouldHandleServerError() { + stubFor( + post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/approve")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + HITLReviewInput review = + HITLReviewInput.builder() + .reviewerId("reviewer_001") + .reviewerEmail("reviewer@example.com") + .build(); + + assertThatThrownBy(() -> axonflow.approveHITLRequest("hitl_req_001", review)) + .isInstanceOf(Exception.class); + } + } + + // ======================================================================== + // rejectHITLRequest Tests + // ======================================================================== + + @Nested + @DisplayName("rejectHITLRequest") + class RejectHITLRequest { + + @Test + @DisplayName("should send reject request with review input") + void shouldSendRejectRequest() { + stubFor( + post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/reject")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\": true}"))); + + HITLReviewInput review = + HITLReviewInput.builder() + .reviewerId("reviewer_002") + .reviewerEmail("admin@example.com") + .comment("Rejected: suspicious transaction pattern") + .build(); + + axonflow.rejectHITLRequest("hitl_req_001", review); + + verify( + postRequestedFor(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/reject")) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(containing("\"reviewer_id\":\"reviewer_002\"")) + .withRequestBody(containing("\"reviewer_email\":\"admin@example.com\"")) + .withRequestBody( + containing("\"comment\":\"Rejected: suspicious transaction pattern\""))); + } + + @Test + @DisplayName("should require non-null requestId") + void shouldRequireRequestId() { + HITLReviewInput review = + HITLReviewInput.builder() + .reviewerId("reviewer_001") + .reviewerEmail("reviewer@example.com") + .build(); + + assertThatThrownBy(() -> axonflow.rejectHITLRequest(null, review)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should require non-null review") + void shouldRequireReview() { + assertThatThrownBy(() -> axonflow.rejectHITLRequest("hitl_req_001", null)) + .isInstanceOf(NullPointerException.class); + } + } + + // ======================================================================== + // getHITLStats Tests + // ======================================================================== + + @Nested + @DisplayName("getHITLStats") + class GetHITLStats { + + @Test + @DisplayName("should return parsed stats") + void shouldReturnParsedStats() { + stubFor( + get(urlEqualTo("/api/v1/hitl/stats")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": {" + + "\"total_pending\": 42," + + "\"high_priority\": 8," + + "\"critical_priority\": 3," + + "\"oldest_pending_hours\": 12.5" + + "}}"))); + + HITLStats stats = axonflow.getHITLStats(); + + assertThat(stats.getTotalPending()).isEqualTo(42); + assertThat(stats.getHighPriority()).isEqualTo(8); + assertThat(stats.getCriticalPriority()).isEqualTo(3); + assertThat(stats.getOldestPendingHours()).isEqualTo(12.5); } - // ======================================================================== - // rejectHITLRequest Tests - // ======================================================================== - - @Nested - @DisplayName("rejectHITLRequest") - class RejectHITLRequest { - - @Test - @DisplayName("should send reject request with review input") - void shouldSendRejectRequest() { - stubFor(post(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/reject")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true}"))); - - HITLReviewInput review = HITLReviewInput.builder() - .reviewerId("reviewer_002") - .reviewerEmail("admin@example.com") - .comment("Rejected: suspicious transaction pattern") - .build(); - - axonflow.rejectHITLRequest("hitl_req_001", review); - - verify(postRequestedFor(urlEqualTo("/api/v1/hitl/queue/hitl_req_001/reject")) - .withHeader("Content-Type", containing("application/json")) - .withRequestBody(containing("\"reviewer_id\":\"reviewer_002\"")) - .withRequestBody(containing("\"reviewer_email\":\"admin@example.com\"")) - .withRequestBody(containing("\"comment\":\"Rejected: suspicious transaction pattern\""))); - } - - @Test - @DisplayName("should require non-null requestId") - void shouldRequireRequestId() { - HITLReviewInput review = HITLReviewInput.builder() - .reviewerId("reviewer_001") - .reviewerEmail("reviewer@example.com") - .build(); - - assertThatThrownBy(() -> axonflow.rejectHITLRequest(null, review)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should require non-null review") - void shouldRequireReview() { - assertThatThrownBy(() -> axonflow.rejectHITLRequest("hitl_req_001", null)) - .isInstanceOf(NullPointerException.class); - } + @Test + @DisplayName("should handle null oldest_pending_hours") + void shouldHandleNullOldestPendingHours() { + stubFor( + get(urlEqualTo("/api/v1/hitl/stats")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\": true, \"data\": {" + + "\"total_pending\": 0," + + "\"high_priority\": 0," + + "\"critical_priority\": 0," + + "\"oldest_pending_hours\": null" + + "}}"))); + + HITLStats stats = axonflow.getHITLStats(); + + assertThat(stats.getTotalPending()).isEqualTo(0); + assertThat(stats.getOldestPendingHours()).isNull(); } - // ======================================================================== - // getHITLStats Tests - // ======================================================================== - - @Nested - @DisplayName("getHITLStats") - class GetHITLStats { - - @Test - @DisplayName("should return parsed stats") - void shouldReturnParsedStats() { - stubFor(get(urlEqualTo("/api/v1/hitl/stats")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": {" + - "\"total_pending\": 42," + - "\"high_priority\": 8," + - "\"critical_priority\": 3," + - "\"oldest_pending_hours\": 12.5" + - "}}"))); - - HITLStats stats = axonflow.getHITLStats(); - - assertThat(stats.getTotalPending()).isEqualTo(42); - assertThat(stats.getHighPriority()).isEqualTo(8); - assertThat(stats.getCriticalPriority()).isEqualTo(3); - assertThat(stats.getOldestPendingHours()).isEqualTo(12.5); - } - - @Test - @DisplayName("should handle null oldest_pending_hours") - void shouldHandleNullOldestPendingHours() { - stubFor(get(urlEqualTo("/api/v1/hitl/stats")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\": true, \"data\": {" + - "\"total_pending\": 0," + - "\"high_priority\": 0," + - "\"critical_priority\": 0," + - "\"oldest_pending_hours\": null" + - "}}"))); - - HITLStats stats = axonflow.getHITLStats(); - - assertThat(stats.getTotalPending()).isEqualTo(0); - assertThat(stats.getOldestPendingHours()).isNull(); - } - - @Test - @DisplayName("should handle stats without data wrapper") - void shouldHandleStatsWithoutWrapper() { - stubFor(get(urlEqualTo("/api/v1/hitl/stats")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" + - "\"total_pending\": 5," + - "\"high_priority\": 2," + - "\"critical_priority\": 1," + - "\"oldest_pending_hours\": 3.7" + - "}"))); - - HITLStats stats = axonflow.getHITLStats(); - - assertThat(stats.getTotalPending()).isEqualTo(5); - assertThat(stats.getHighPriority()).isEqualTo(2); - assertThat(stats.getCriticalPriority()).isEqualTo(1); - assertThat(stats.getOldestPendingHours()).isEqualTo(3.7); - } + @Test + @DisplayName("should handle stats without data wrapper") + void shouldHandleStatsWithoutWrapper() { + stubFor( + get(urlEqualTo("/api/v1/hitl/stats")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"total_pending\": 5," + + "\"high_priority\": 2," + + "\"critical_priority\": 1," + + "\"oldest_pending_hours\": 3.7" + + "}"))); + + HITLStats stats = axonflow.getHITLStats(); + + assertThat(stats.getTotalPending()).isEqualTo(5); + assertThat(stats.getHighPriority()).isEqualTo(2); + assertThat(stats.getCriticalPriority()).isEqualTo(1); + assertThat(stats.getOldestPendingHours()).isEqualTo(3.7); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java b/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java index 55cfee3..573a0c5 100644 --- a/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java +++ b/src/test/java/com/getaxonflow/sdk/MediaGovernanceTest.java @@ -15,298 +15,324 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.MediaGovernanceConfig; import com.getaxonflow.sdk.types.MediaGovernanceStatus; import com.getaxonflow.sdk.types.UpdateMediaGovernanceConfigRequest; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.List; +import java.util.concurrent.CompletableFuture; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.List; -import java.util.concurrent.CompletableFuture; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for Media Governance Config API methods on the AxonFlow client. - */ +/** Tests for Media Governance Config API methods on the AxonFlow client. */ @WireMockTest @DisplayName("Media Governance API Methods") class MediaGovernanceTest { - private AxonFlow axonflow; - - private static final String SAMPLE_CONFIG_JSON = - "{" + - "\"tenant_id\": \"tenant_001\"," + - "\"enabled\": true," + - "\"allowed_analyzers\": [\"nsfw\", \"biometric\", \"ocr\"]," + - "\"updated_at\": \"2026-02-18T10:00:00Z\"," + - "\"updated_by\": \"admin@example.com\"" + - "}"; - - private static final String SAMPLE_STATUS_JSON = - "{" + - "\"available\": true," + - "\"enabled_by_default\": false," + - "\"per_tenant_control\": true," + - "\"tier\": \"enterprise\"" + - "}"; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .build()); + private AxonFlow axonflow; + + private static final String SAMPLE_CONFIG_JSON = + "{" + + "\"tenant_id\": \"tenant_001\"," + + "\"enabled\": true," + + "\"allowed_analyzers\": [\"nsfw\", \"biometric\", \"ocr\"]," + + "\"updated_at\": \"2026-02-18T10:00:00Z\"," + + "\"updated_by\": \"admin@example.com\"" + + "}"; + + private static final String SAMPLE_STATUS_JSON = + "{" + + "\"available\": true," + + "\"enabled_by_default\": false," + + "\"per_tenant_control\": true," + + "\"tier\": \"enterprise\"" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // getMediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("getMediaGovernanceConfig") + class GetMediaGovernanceConfig { + + @Test + @DisplayName("should return media governance config") + void shouldReturnConfig() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); + } + + @Test + @DisplayName("should return disabled config") + void shouldReturnDisabledConfig() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"tenant_id\": \"tenant_002\", \"enabled\": false, \"allowed_analyzers\": []}"))); + + MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); + + assertThat(config.getTenantId()).isEqualTo("tenant_002"); + assertThat(config.isEnabled()).isFalse(); + assertThat(config.getAllowedAnalyzers()).isEmpty(); + } + + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getMediaGovernanceConfig()).isInstanceOf(Exception.class); + } + + @Test + @DisplayName("async should return future with config") + void asyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + CompletableFuture future = axonflow.getMediaGovernanceConfigAsync(); + MediaGovernanceConfig config = future.get(); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + } + } + + // ======================================================================== + // updateMediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("updateMediaGovernanceConfig") + class UpdateMediaGovernanceConfig { + + @Test + @DisplayName("should send PUT request and return updated config") + void shouldUpdateConfig() { + stubFor( + put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric", "ocr")) + .build(); + + MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + + verify( + putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) + .withHeader("Content-Type", containing("application/json")) + .withRequestBody(containing("\"enabled\":true")) + .withRequestBody(containing("\"allowed_analyzers\""))); + } + + @Test + @DisplayName("should send partial update with only enabled") + void shouldSendPartialUpdate() { + stubFor( + put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"tenant_id\": \"tenant_001\", \"enabled\": false, \"allowed_analyzers\": [\"nsfw\"]}"))); + + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().enabled(false).build(); + + MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); + + assertThat(config.isEnabled()).isFalse(); + + // Verify null fields are not sent (NON_NULL inclusion) + verify( + putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("should require non-null request") + void shouldRequireNonNullRequest() { + assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request cannot be null"); + } + + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor( + put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(403) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Forbidden: insufficient permissions\"}"))); + + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().enabled(true).build(); + + assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(request)) + .isInstanceOf(Exception.class); + } + + @Test + @DisplayName("async should return future with updated config") + void asyncShouldReturnFuture() throws Exception { + stubFor( + put(urlEqualTo("/api/v1/media-governance/config")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_CONFIG_JSON))); + + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + CompletableFuture future = + axonflow.updateMediaGovernanceConfigAsync(request); + MediaGovernanceConfig config = future.get(); + + assertThat(config.isEnabled()).isTrue(); + } + } + + // ======================================================================== + // getMediaGovernanceStatus + // ======================================================================== + + @Nested + @DisplayName("getMediaGovernanceStatus") + class GetMediaGovernanceStatus { + + @Test + @DisplayName("should return media governance platform status") + void shouldReturnStatus() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATUS_JSON))); + + MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isFalse(); + assertThat(status.isPerTenantControl()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); } - // ======================================================================== - // getMediaGovernanceConfig - // ======================================================================== - - @Nested - @DisplayName("getMediaGovernanceConfig") - class GetMediaGovernanceConfig { - - @Test - @DisplayName("should return media governance config") - void shouldReturnConfig() { - stubFor(get(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_CONFIG_JSON))); - - MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); - - assertThat(config.getTenantId()).isEqualTo("tenant_001"); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); - assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); - assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); - } - - @Test - @DisplayName("should return disabled config") - void shouldReturnDisabledConfig() { - stubFor(get(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"tenant_id\": \"tenant_002\", \"enabled\": false, \"allowed_analyzers\": []}"))); - - MediaGovernanceConfig config = axonflow.getMediaGovernanceConfig(); - - assertThat(config.getTenantId()).isEqualTo("tenant_002"); - assertThat(config.isEnabled()).isFalse(); - assertThat(config.getAllowedAnalyzers()).isEmpty(); - } - - @Test - @DisplayName("should throw on server error") - void shouldThrowOnServerError() { - stubFor(get(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getMediaGovernanceConfig()) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("async should return future with config") - void asyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_CONFIG_JSON))); - - CompletableFuture future = axonflow.getMediaGovernanceConfigAsync(); - MediaGovernanceConfig config = future.get(); - - assertThat(config.getTenantId()).isEqualTo("tenant_001"); - assertThat(config.isEnabled()).isTrue(); - } + @Test + @DisplayName("should return unavailable status") + void shouldReturnUnavailableStatus() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"available\": false, \"enabled_by_default\": false, \"per_tenant_control\": false, \"tier\": \"community\"}"))); + + MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); + + assertThat(status.isAvailable()).isFalse(); + assertThat(status.getTier()).isEqualTo("community"); } - // ======================================================================== - // updateMediaGovernanceConfig - // ======================================================================== - - @Nested - @DisplayName("updateMediaGovernanceConfig") - class UpdateMediaGovernanceConfig { - - @Test - @DisplayName("should send PUT request and return updated config") - void shouldUpdateConfig() { - stubFor(put(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_CONFIG_JSON))); - - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw", "biometric", "ocr")) - .build(); - - MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); - - assertThat(config.getTenantId()).isEqualTo("tenant_001"); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); - - verify(putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) - .withHeader("Content-Type", containing("application/json")) - .withRequestBody(containing("\"enabled\":true")) - .withRequestBody(containing("\"allowed_analyzers\""))); - } - - @Test - @DisplayName("should send partial update with only enabled") - void shouldSendPartialUpdate() { - stubFor(put(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"tenant_id\": \"tenant_001\", \"enabled\": false, \"allowed_analyzers\": [\"nsfw\"]}"))); - - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(false) - .build(); - - MediaGovernanceConfig config = axonflow.updateMediaGovernanceConfig(request); - - assertThat(config.isEnabled()).isFalse(); - - // Verify null fields are not sent (NON_NULL inclusion) - verify(putRequestedFor(urlEqualTo("/api/v1/media-governance/config")) - .withRequestBody(containing("\"enabled\":false"))); - } - - @Test - @DisplayName("should require non-null request") - void shouldRequireNonNullRequest() { - assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("request cannot be null"); - } - - @Test - @DisplayName("should throw on server error") - void shouldThrowOnServerError() { - stubFor(put(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(403) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Forbidden: insufficient permissions\"}"))); - - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .build(); - - assertThatThrownBy(() -> axonflow.updateMediaGovernanceConfig(request)) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("async should return future with updated config") - void asyncShouldReturnFuture() throws Exception { - stubFor(put(urlEqualTo("/api/v1/media-governance/config")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_CONFIG_JSON))); - - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw")) - .build(); - - CompletableFuture future = - axonflow.updateMediaGovernanceConfigAsync(request); - MediaGovernanceConfig config = future.get(); - - assertThat(config.isEnabled()).isTrue(); - } + @Test + @DisplayName("should throw on server error") + void shouldThrowOnServerError() { + stubFor( + get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\": \"Internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.getMediaGovernanceStatus()).isInstanceOf(Exception.class); } - // ======================================================================== - // getMediaGovernanceStatus - // ======================================================================== - - @Nested - @DisplayName("getMediaGovernanceStatus") - class GetMediaGovernanceStatus { - - @Test - @DisplayName("should return media governance platform status") - void shouldReturnStatus() { - stubFor(get(urlEqualTo("/api/v1/media-governance/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_STATUS_JSON))); - - MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); - - assertThat(status.isAvailable()).isTrue(); - assertThat(status.isEnabledByDefault()).isFalse(); - assertThat(status.isPerTenantControl()).isTrue(); - assertThat(status.getTier()).isEqualTo("enterprise"); - } - - @Test - @DisplayName("should return unavailable status") - void shouldReturnUnavailableStatus() { - stubFor(get(urlEqualTo("/api/v1/media-governance/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"available\": false, \"enabled_by_default\": false, \"per_tenant_control\": false, \"tier\": \"community\"}"))); - - MediaGovernanceStatus status = axonflow.getMediaGovernanceStatus(); - - assertThat(status.isAvailable()).isFalse(); - assertThat(status.getTier()).isEqualTo("community"); - } - - @Test - @DisplayName("should throw on server error") - void shouldThrowOnServerError() { - stubFor(get(urlEqualTo("/api/v1/media-governance/status")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\": \"Internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getMediaGovernanceStatus()) - .isInstanceOf(Exception.class); - } - - @Test - @DisplayName("async should return future with status") - void asyncShouldReturnFuture() throws Exception { - stubFor(get(urlEqualTo("/api/v1/media-governance/status")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_STATUS_JSON))); - - CompletableFuture future = axonflow.getMediaGovernanceStatusAsync(); - MediaGovernanceStatus status = future.get(); - - assertThat(status.isAvailable()).isTrue(); - assertThat(status.getTier()).isEqualTo("enterprise"); - } + @Test + @DisplayName("async should return future with status") + void asyncShouldReturnFuture() throws Exception { + stubFor( + get(urlEqualTo("/api/v1/media-governance/status")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATUS_JSON))); + + CompletableFuture future = axonflow.getMediaGovernanceStatusAsync(); + MediaGovernanceStatus status = future.get(); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/PolicySimulationTest.java b/src/test/java/com/getaxonflow/sdk/PolicySimulationTest.java index a569577..35e8c10 100644 --- a/src/test/java/com/getaxonflow/sdk/PolicySimulationTest.java +++ b/src/test/java/com/getaxonflow/sdk/PolicySimulationTest.java @@ -15,105 +15,116 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.exceptions.AxonFlowException; import com.getaxonflow.sdk.simulation.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for policy simulation methods. - */ +/** Tests for policy simulation methods. */ @WireMockTest @DisplayName("Policy Simulation") class PolicySimulationTest { - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - // ======================================================================== - // simulatePolicies - // ======================================================================== - - @Test - @DisplayName("should simulate policies and return blocked result") - void shouldSimulatePoliciesBlocked() { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"allowed\":false,\"applied_policies\":[\"block-pii\",\"block-financial\"],\"risk_score\":0.85,\"required_actions\":[\"redact_pii\"],\"processing_time_ms\":12,\"total_policies\":5,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\",\"daily_usage\":{\"used\":3,\"limit\":100}}}"))); - - SimulatePoliciesResponse result = axonflow.simulatePolicies( + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + } + + // ======================================================================== + // simulatePolicies + // ======================================================================== + + @Test + @DisplayName("should simulate policies and return blocked result") + void shouldSimulatePoliciesBlocked() { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"allowed\":false,\"applied_policies\":[\"block-pii\",\"block-financial\"],\"risk_score\":0.85,\"required_actions\":[\"redact_pii\"],\"processing_time_ms\":12,\"total_policies\":5,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\",\"daily_usage\":{\"used\":3,\"limit\":100}}}"))); + + SimulatePoliciesResponse result = + axonflow.simulatePolicies( SimulatePoliciesRequest.builder() .query("My SSN is 123-45-6789") .requestType("query") .build()); - assertThat(result).isNotNull(); - assertThat(result.isAllowed()).isFalse(); - assertThat(result.getAppliedPolicies()).containsExactly("block-pii", "block-financial"); - assertThat(result.getRiskScore()).isEqualTo(0.85); - assertThat(result.getRequiredActions()).containsExactly("redact_pii"); - assertThat(result.getProcessingTimeMs()).isEqualTo(12); - assertThat(result.getTotalPolicies()).isEqualTo(5); - assertThat(result.isDryRun()).isTrue(); - assertThat(result.getSimulatedAt()).isEqualTo("2026-03-24T10:00:00Z"); - assertThat(result.getTier()).isEqualTo("evaluation"); - assertThat(result.getDailyUsage()).isNotNull(); - assertThat(result.getDailyUsage().getUsed()).isEqualTo(3); - assertThat(result.getDailyUsage().getLimit()).isEqualTo(100); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/simulate")) + assertThat(result).isNotNull(); + assertThat(result.isAllowed()).isFalse(); + assertThat(result.getAppliedPolicies()).containsExactly("block-pii", "block-financial"); + assertThat(result.getRiskScore()).isEqualTo(0.85); + assertThat(result.getRequiredActions()).containsExactly("redact_pii"); + assertThat(result.getProcessingTimeMs()).isEqualTo(12); + assertThat(result.getTotalPolicies()).isEqualTo(5); + assertThat(result.isDryRun()).isTrue(); + assertThat(result.getSimulatedAt()).isEqualTo("2026-03-24T10:00:00Z"); + assertThat(result.getTier()).isEqualTo("evaluation"); + assertThat(result.getDailyUsage()).isNotNull(); + assertThat(result.getDailyUsage().getUsed()).isEqualTo(3); + assertThat(result.getDailyUsage().getLimit()).isEqualTo(100); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/simulate")) .withRequestBody(matchingJsonPath("$.query", equalTo("My SSN is 123-45-6789"))) .withRequestBody(matchingJsonPath("$.request_type", equalTo("query")))); - } - - @Test - @DisplayName("should simulate policies and return allowed result") - void shouldSimulatePoliciesAllowed() { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.1,\"required_actions\":[],\"processing_time_ms\":5,\"total_policies\":5,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\",\"daily_usage\":{\"used\":1,\"limit\":100}}}"))); - - SimulatePoliciesResponse result = axonflow.simulatePolicies( - SimulatePoliciesRequest.builder() - .query("What is the weather?") - .build()); - - assertThat(result.isAllowed()).isTrue(); - assertThat(result.getAppliedPolicies()).isEmpty(); - assertThat(result.getRiskScore()).isEqualTo(0.1); - } - - @Test - @DisplayName("should simulate policies with user and context") - void shouldSimulatePoliciesWithContext() { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"allowed\":false,\"applied_policies\":[\"geo-block\"],\"risk_score\":0.9,\"required_actions\":[],\"processing_time_ms\":8,\"total_policies\":3,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); - - SimulatePoliciesResponse result = axonflow.simulatePolicies( + } + + @Test + @DisplayName("should simulate policies and return allowed result") + void shouldSimulatePoliciesAllowed() { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.1,\"required_actions\":[],\"processing_time_ms\":5,\"total_policies\":5,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\",\"daily_usage\":{\"used\":1,\"limit\":100}}}"))); + + SimulatePoliciesResponse result = + axonflow.simulatePolicies( + SimulatePoliciesRequest.builder().query("What is the weather?").build()); + + assertThat(result.isAllowed()).isTrue(); + assertThat(result.getAppliedPolicies()).isEmpty(); + assertThat(result.getRiskScore()).isEqualTo(0.1); + } + + @Test + @DisplayName("should simulate policies with user and context") + void shouldSimulatePoliciesWithContext() { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"allowed\":false,\"applied_policies\":[\"geo-block\"],\"risk_score\":0.9,\"required_actions\":[],\"processing_time_ms\":8,\"total_policies\":3,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); + + SimulatePoliciesResponse result = + axonflow.simulatePolicies( SimulatePoliciesRequest.builder() .query("Execute trade") .requestType("execute") @@ -121,403 +132,459 @@ void shouldSimulatePoliciesWithContext() { .context(Map.of("region", "restricted")) .build()); - assertThat(result).isNotNull(); - assertThat(result.isAllowed()).isFalse(); - assertThat(result.getAppliedPolicies()).containsExactly("geo-block"); + assertThat(result).isNotNull(); + assertThat(result.isAllowed()).isFalse(); + assertThat(result.getAppliedPolicies()).containsExactly("geo-block"); - verify(postRequestedFor(urlEqualTo("/api/v1/policies/simulate")) + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/simulate")) .withRequestBody(matchingJsonPath("$.user.role", equalTo("analyst"))) .withRequestBody(matchingJsonPath("$.context.region", equalTo("restricted")))); - } - - @Test - @DisplayName("should reject null request for simulatePolicies") - void shouldRejectNullRequestForSimulate() { - assertThatThrownBy(() -> axonflow.simulatePolicies(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("request cannot be null"); - } - - @Test - @DisplayName("should reject null query in SimulatePoliciesRequest builder") - void shouldRejectNullQueryInBuilder() { - assertThatThrownBy(() -> SimulatePoliciesRequest.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("query cannot be null"); - } - - @Test - @DisplayName("should reject empty query in SimulatePoliciesRequest builder") - void shouldRejectEmptyQueryInBuilder() { - assertThatThrownBy(() -> SimulatePoliciesRequest.builder().query("").build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("query cannot be empty"); - } - - @Test - @DisplayName("simulatePoliciesAsync should return future") - void simulatePoliciesAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.0,\"required_actions\":[],\"processing_time_ms\":3,\"total_policies\":2,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - CompletableFuture future = axonflow.simulatePoliciesAsync( - SimulatePoliciesRequest.builder().query("Hello").build()); - SimulatePoliciesResponse result = future.get(); - - assertThat(result).isNotNull(); - assertThat(result.isAllowed()).isTrue(); - } - - @Test - @DisplayName("should handle server error on simulatePolicies") - void shouldHandleServerErrorOnSimulate() { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.simulatePolicies( - SimulatePoliciesRequest.builder().query("test").build())) - .isInstanceOf(AxonFlowException.class); - } - - @Test - @DisplayName("should handle unwrapped response for simulatePolicies") - void shouldHandleUnwrappedResponseForSimulate() { - stubFor(post(urlEqualTo("/api/v1/policies/simulate")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.0,\"required_actions\":[],\"processing_time_ms\":2,\"total_policies\":1,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}"))); - - SimulatePoliciesResponse result = axonflow.simulatePolicies( - SimulatePoliciesRequest.builder().query("test").build()); - - assertThat(result).isNotNull(); - assertThat(result.isAllowed()).isTrue(); - } - - // ======================================================================== - // getPolicyImpactReport - // ======================================================================== - - @Test - @DisplayName("should get policy impact report") - void shouldGetPolicyImpactReport() { - stubFor(post(urlEqualTo("/api/v1/policies/impact-report")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"policy_id\":\"policy_block_pii\",\"policy_name\":\"block-pii\",\"total_inputs\":3,\"matched\":2,\"blocked\":2,\"match_rate\":0.667,\"block_rate\":0.667,\"results\":[{\"input_index\":0,\"matched\":true,\"blocked\":true,\"actions\":[\"block\"]},{\"input_index\":1,\"matched\":false,\"blocked\":false,\"actions\":[\"allow\"]},{\"input_index\":2,\"matched\":true,\"blocked\":true,\"actions\":[\"block\"]}],\"processing_time_ms\":25,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - ImpactReportResponse report = axonflow.getPolicyImpactReport( + } + + @Test + @DisplayName("should reject null request for simulatePolicies") + void shouldRejectNullRequestForSimulate() { + assertThatThrownBy(() -> axonflow.simulatePolicies(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request cannot be null"); + } + + @Test + @DisplayName("should reject null query in SimulatePoliciesRequest builder") + void shouldRejectNullQueryInBuilder() { + assertThatThrownBy(() -> SimulatePoliciesRequest.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("query cannot be null"); + } + + @Test + @DisplayName("should reject empty query in SimulatePoliciesRequest builder") + void shouldRejectEmptyQueryInBuilder() { + assertThatThrownBy(() -> SimulatePoliciesRequest.builder().query("").build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("query cannot be empty"); + } + + @Test + @DisplayName("simulatePoliciesAsync should return future") + void simulatePoliciesAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.0,\"required_actions\":[],\"processing_time_ms\":3,\"total_policies\":2,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + CompletableFuture future = + axonflow.simulatePoliciesAsync(SimulatePoliciesRequest.builder().query("Hello").build()); + SimulatePoliciesResponse result = future.get(); + + assertThat(result).isNotNull(); + assertThat(result.isAllowed()).isTrue(); + } + + @Test + @DisplayName("should handle server error on simulatePolicies") + void shouldHandleServerErrorOnSimulate() { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy( + () -> + axonflow.simulatePolicies(SimulatePoliciesRequest.builder().query("test").build())) + .isInstanceOf(AxonFlowException.class); + } + + @Test + @DisplayName("should handle unwrapped response for simulatePolicies") + void shouldHandleUnwrappedResponseForSimulate() { + stubFor( + post(urlEqualTo("/api/v1/policies/simulate")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"allowed\":true,\"applied_policies\":[],\"risk_score\":0.0,\"required_actions\":[],\"processing_time_ms\":2,\"total_policies\":1,\"dry_run\":true,\"simulated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}"))); + + SimulatePoliciesResponse result = + axonflow.simulatePolicies(SimulatePoliciesRequest.builder().query("test").build()); + + assertThat(result).isNotNull(); + assertThat(result.isAllowed()).isTrue(); + } + + // ======================================================================== + // getPolicyImpactReport + // ======================================================================== + + @Test + @DisplayName("should get policy impact report") + void shouldGetPolicyImpactReport() { + stubFor( + post(urlEqualTo("/api/v1/policies/impact-report")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"policy_id\":\"policy_block_pii\",\"policy_name\":\"block-pii\",\"total_inputs\":3,\"matched\":2,\"blocked\":2,\"match_rate\":0.667,\"block_rate\":0.667,\"results\":[{\"input_index\":0,\"matched\":true,\"blocked\":true,\"actions\":[\"block\"]},{\"input_index\":1,\"matched\":false,\"blocked\":false,\"actions\":[\"allow\"]},{\"input_index\":2,\"matched\":true,\"blocked\":true,\"actions\":[\"block\"]}],\"processing_time_ms\":25,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + ImpactReportResponse report = + axonflow.getPolicyImpactReport( ImpactReportRequest.builder() .policyId("policy_block_pii") - .inputs(List.of( - ImpactReportInput.builder().query("My SSN is 123-45-6789").build(), - ImpactReportInput.builder().query("What is the weather?").build(), - ImpactReportInput.builder().query("My email is test@example.com").build())) + .inputs( + List.of( + ImpactReportInput.builder().query("My SSN is 123-45-6789").build(), + ImpactReportInput.builder().query("What is the weather?").build(), + ImpactReportInput.builder().query("My email is test@example.com").build())) .build()); - assertThat(report).isNotNull(); - assertThat(report.getPolicyId()).isEqualTo("policy_block_pii"); - assertThat(report.getTotalInputs()).isEqualTo(3); - assertThat(report.getMatched()).isEqualTo(2); - assertThat(report.getBlocked()).isEqualTo(2); - assertThat(report.getMatchRate()).isEqualTo(0.667); - assertThat(report.getBlockRate()).isEqualTo(0.667); - assertThat(report.getPolicyName()).isEqualTo("block-pii"); - assertThat(report.getResults()).hasSize(3); - assertThat(report.getResults().get(0).getInputIndex()).isEqualTo(0); - assertThat(report.getResults().get(0).isMatched()).isTrue(); - assertThat(report.getResults().get(0).isBlocked()).isTrue(); - assertThat(report.getResults().get(0).getActions()).containsExactly("block"); - assertThat(report.getResults().get(1).getInputIndex()).isEqualTo(1); - assertThat(report.getResults().get(1).isMatched()).isFalse(); - assertThat(report.getResults().get(1).getActions()).containsExactly("allow"); - assertThat(report.getProcessingTimeMs()).isEqualTo(25); - assertThat(report.getGeneratedAt()).isEqualTo("2026-03-24T10:00:00Z"); - assertThat(report.getTier()).isEqualTo("evaluation"); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/impact-report")) + assertThat(report).isNotNull(); + assertThat(report.getPolicyId()).isEqualTo("policy_block_pii"); + assertThat(report.getTotalInputs()).isEqualTo(3); + assertThat(report.getMatched()).isEqualTo(2); + assertThat(report.getBlocked()).isEqualTo(2); + assertThat(report.getMatchRate()).isEqualTo(0.667); + assertThat(report.getBlockRate()).isEqualTo(0.667); + assertThat(report.getPolicyName()).isEqualTo("block-pii"); + assertThat(report.getResults()).hasSize(3); + assertThat(report.getResults().get(0).getInputIndex()).isEqualTo(0); + assertThat(report.getResults().get(0).isMatched()).isTrue(); + assertThat(report.getResults().get(0).isBlocked()).isTrue(); + assertThat(report.getResults().get(0).getActions()).containsExactly("block"); + assertThat(report.getResults().get(1).getInputIndex()).isEqualTo(1); + assertThat(report.getResults().get(1).isMatched()).isFalse(); + assertThat(report.getResults().get(1).getActions()).containsExactly("allow"); + assertThat(report.getProcessingTimeMs()).isEqualTo(25); + assertThat(report.getGeneratedAt()).isEqualTo("2026-03-24T10:00:00Z"); + assertThat(report.getTier()).isEqualTo("evaluation"); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/impact-report")) .withRequestBody(matchingJsonPath("$.policy_id", equalTo("policy_block_pii")))); - } - - @Test - @DisplayName("should get impact report with no matches") - void shouldGetImpactReportNoMatches() { - stubFor(post(urlEqualTo("/api/v1/policies/impact-report")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"policy_id\":\"policy_strict\",\"total_inputs\":1,\"matched\":0,\"blocked\":0,\"match_rate\":0.0,\"block_rate\":0.0,\"results\":[{\"input_index\":0,\"matched\":false,\"blocked\":false,\"actions\":[\"allow\"]}],\"processing_time_ms\":3,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); - - ImpactReportResponse report = axonflow.getPolicyImpactReport( + } + + @Test + @DisplayName("should get impact report with no matches") + void shouldGetImpactReportNoMatches() { + stubFor( + post(urlEqualTo("/api/v1/policies/impact-report")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"policy_id\":\"policy_strict\",\"total_inputs\":1,\"matched\":0,\"blocked\":0,\"match_rate\":0.0,\"block_rate\":0.0,\"results\":[{\"input_index\":0,\"matched\":false,\"blocked\":false,\"actions\":[\"allow\"]}],\"processing_time_ms\":3,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); + + ImpactReportResponse report = + axonflow.getPolicyImpactReport( ImpactReportRequest.builder() .policyId("policy_strict") .inputs(List.of(ImpactReportInput.builder().query("Hello world").build())) .build()); - assertThat(report.getMatched()).isEqualTo(0); - assertThat(report.getMatchRate()).isEqualTo(0.0); - } - - @Test - @DisplayName("should reject null request for getPolicyImpactReport") - void shouldRejectNullRequestForImpactReport() { - assertThatThrownBy(() -> axonflow.getPolicyImpactReport(null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("request cannot be null"); - } - - @Test - @DisplayName("should reject null policyId in ImpactReportRequest builder") - void shouldRejectNullPolicyIdInImpactReportBuilder() { - assertThatThrownBy(() -> ImpactReportRequest.builder() - .inputs(List.of(ImpactReportInput.builder().query("test").build())) - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("policyId cannot be null"); - } - - @Test - @DisplayName("should reject empty policyId in ImpactReportRequest builder") - void shouldRejectEmptyPolicyIdInImpactReportBuilder() { - assertThatThrownBy(() -> ImpactReportRequest.builder() - .policyId("") - .inputs(List.of(ImpactReportInput.builder().query("test").build())) - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("policyId cannot be empty"); - } - - @Test - @DisplayName("should reject empty inputs in ImpactReportRequest builder") - void shouldRejectEmptyInputsInImpactReportBuilder() { - assertThatThrownBy(() -> ImpactReportRequest.builder() - .policyId("policy_1") - .inputs(List.of()) - .build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("inputs cannot be empty"); - } - - @Test - @DisplayName("should reject null inputs in ImpactReportRequest builder") - void shouldRejectNullInputsInImpactReportBuilder() { - assertThatThrownBy(() -> ImpactReportRequest.builder() - .policyId("policy_1") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("inputs cannot be null"); - } - - @Test - @DisplayName("getPolicyImpactReportAsync should return future") - void getPolicyImpactReportAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/policies/impact-report")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"policy_id\":\"p1\",\"total_inputs\":1,\"matched\":0,\"blocked\":0,\"match_rate\":0.0,\"block_rate\":0.0,\"results\":[],\"processing_time_ms\":2,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - CompletableFuture future = axonflow.getPolicyImpactReportAsync( + assertThat(report.getMatched()).isEqualTo(0); + assertThat(report.getMatchRate()).isEqualTo(0.0); + } + + @Test + @DisplayName("should reject null request for getPolicyImpactReport") + void shouldRejectNullRequestForImpactReport() { + assertThatThrownBy(() -> axonflow.getPolicyImpactReport(null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("request cannot be null"); + } + + @Test + @DisplayName("should reject null policyId in ImpactReportRequest builder") + void shouldRejectNullPolicyIdInImpactReportBuilder() { + assertThatThrownBy( + () -> + ImpactReportRequest.builder() + .inputs(List.of(ImpactReportInput.builder().query("test").build())) + .build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("policyId cannot be null"); + } + + @Test + @DisplayName("should reject empty policyId in ImpactReportRequest builder") + void shouldRejectEmptyPolicyIdInImpactReportBuilder() { + assertThatThrownBy( + () -> + ImpactReportRequest.builder() + .policyId("") + .inputs(List.of(ImpactReportInput.builder().query("test").build())) + .build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("policyId cannot be empty"); + } + + @Test + @DisplayName("should reject empty inputs in ImpactReportRequest builder") + void shouldRejectEmptyInputsInImpactReportBuilder() { + assertThatThrownBy( + () -> ImpactReportRequest.builder().policyId("policy_1").inputs(List.of()).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("inputs cannot be empty"); + } + + @Test + @DisplayName("should reject null inputs in ImpactReportRequest builder") + void shouldRejectNullInputsInImpactReportBuilder() { + assertThatThrownBy(() -> ImpactReportRequest.builder().policyId("policy_1").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("inputs cannot be null"); + } + + @Test + @DisplayName("getPolicyImpactReportAsync should return future") + void getPolicyImpactReportAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/policies/impact-report")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"policy_id\":\"p1\",\"total_inputs\":1,\"matched\":0,\"blocked\":0,\"match_rate\":0.0,\"block_rate\":0.0,\"results\":[],\"processing_time_ms\":2,\"generated_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + CompletableFuture future = + axonflow.getPolicyImpactReportAsync( ImpactReportRequest.builder() .policyId("p1") .inputs(List.of(ImpactReportInput.builder().query("test").build())) .build()); - ImpactReportResponse report = future.get(); - - assertThat(report).isNotNull(); - assertThat(report.getPolicyId()).isEqualTo("p1"); - } - - @Test - @DisplayName("should handle server error on getPolicyImpactReport") - void shouldHandleServerErrorOnImpactReport() { - stubFor(post(urlEqualTo("/api/v1/policies/impact-report")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.getPolicyImpactReport( - ImpactReportRequest.builder() - .policyId("p1") - .inputs(List.of(ImpactReportInput.builder().query("test").build())) - .build())) - .isInstanceOf(AxonFlowException.class); - } - - // ======================================================================== - // detectPolicyConflicts - // ======================================================================== - - @Test - @DisplayName("should detect policy conflicts") - void shouldDetectPolicyConflicts() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) + ImpactReportResponse report = future.get(); + + assertThat(report).isNotNull(); + assertThat(report.getPolicyId()).isEqualTo("p1"); + } + + @Test + @DisplayName("should handle server error on getPolicyImpactReport") + void shouldHandleServerErrorOnImpactReport() { + stubFor( + post(urlEqualTo("/api/v1/policies/impact-report")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy( + () -> + axonflow.getPolicyImpactReport( + ImpactReportRequest.builder() + .policyId("p1") + .inputs(List.of(ImpactReportInput.builder().query("test").build())) + .build())) + .isInstanceOf(AxonFlowException.class); + } + + // ======================================================================== + // detectPolicyConflicts + // ======================================================================== + + @Test + @DisplayName("should detect policy conflicts") + void shouldDetectPolicyConflicts() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"policy_block_pii\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[{\"policy_a\":{\"id\":\"policy_block_pii\",\"name\":\"block-pii\",\"type\":\"deny\"},\"policy_b\":{\"id\":\"policy_allow_internal\",\"name\":\"allow-internal\",\"type\":\"allow\"},\"conflict_type\":\"action_conflict\",\"description\":\"Policy 'block-pii' blocks requests that 'allow-internal' would allow\",\"severity\":\"high\",\"overlapping_field\":\"input.content\"}],\"total_policies\":8,\"conflict_count\":1,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy_block_pii"); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(1); - assertThat(result.getTotalPolicies()).isEqualTo(8); - assertThat(result.getCheckedAt()).isEqualTo("2026-03-24T10:00:00Z"); - assertThat(result.getTier()).isEqualTo("evaluation"); - assertThat(result.getConflicts()).hasSize(1); - - PolicyConflict conflict = result.getConflicts().get(0); - assertThat(conflict.getConflictType()).isEqualTo("action_conflict"); - assertThat(conflict.getSeverity()).isEqualTo("high"); - assertThat(conflict.getDescription()).contains("block-pii"); - assertThat(conflict.getOverlappingField()).isEqualTo("input.content"); - assertThat(conflict.getPolicyA()).isNotNull(); - assertThat(conflict.getPolicyA().getId()).isEqualTo("policy_block_pii"); - assertThat(conflict.getPolicyA().getName()).isEqualTo("block-pii"); - assertThat(conflict.getPolicyA().getType()).isEqualTo("deny"); - assertThat(conflict.getPolicyB()).isNotNull(); - assertThat(conflict.getPolicyB().getId()).isEqualTo("policy_allow_internal"); - assertThat(conflict.getPolicyB().getName()).isEqualTo("allow-internal"); - assertThat(conflict.getPolicyB().getType()).isEqualTo("allow"); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[{\"policy_a\":{\"id\":\"policy_block_pii\",\"name\":\"block-pii\",\"type\":\"deny\"},\"policy_b\":{\"id\":\"policy_allow_internal\",\"name\":\"allow-internal\",\"type\":\"allow\"},\"conflict_type\":\"action_conflict\",\"description\":\"Policy 'block-pii' blocks requests that 'allow-internal' would allow\",\"severity\":\"high\",\"overlapping_field\":\"input.content\"}],\"total_policies\":8,\"conflict_count\":1,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy_block_pii"); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(1); + assertThat(result.getTotalPolicies()).isEqualTo(8); + assertThat(result.getCheckedAt()).isEqualTo("2026-03-24T10:00:00Z"); + assertThat(result.getTier()).isEqualTo("evaluation"); + assertThat(result.getConflicts()).hasSize(1); + + PolicyConflict conflict = result.getConflicts().get(0); + assertThat(conflict.getConflictType()).isEqualTo("action_conflict"); + assertThat(conflict.getSeverity()).isEqualTo("high"); + assertThat(conflict.getDescription()).contains("block-pii"); + assertThat(conflict.getOverlappingField()).isEqualTo("input.content"); + assertThat(conflict.getPolicyA()).isNotNull(); + assertThat(conflict.getPolicyA().getId()).isEqualTo("policy_block_pii"); + assertThat(conflict.getPolicyA().getName()).isEqualTo("block-pii"); + assertThat(conflict.getPolicyA().getType()).isEqualTo("deny"); + assertThat(conflict.getPolicyB()).isNotNull(); + assertThat(conflict.getPolicyB().getId()).isEqualTo("policy_allow_internal"); + assertThat(conflict.getPolicyB().getName()).isEqualTo("allow-internal"); + assertThat(conflict.getPolicyB().getType()).isEqualTo("allow"); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"policy_block_pii\""))); - } + } - @Test - @DisplayName("should detect no conflicts") - void shouldDetectNoConflicts() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) + @Test + @DisplayName("should detect no conflicts") + void shouldDetectNoConflicts() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"policy_safe\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":5,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy_safe"); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(0); - assertThat(result.getConflicts()).isEmpty(); - assertThat(result.getTotalPolicies()).isEqualTo(5); - } - - @Test - @DisplayName("should scan all policies when policyId is null") - void shouldScanAllPoliciesWhenNull() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":5,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts(null); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(0); - assertThat(result.getConflicts()).isEmpty(); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":5,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"enterprise\"}}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy_safe"); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(0); + assertThat(result.getConflicts()).isEmpty(); + assertThat(result.getTotalPolicies()).isEqualTo(5); + } + + @Test + @DisplayName("should scan all policies when policyId is null") + void shouldScanAllPoliciesWhenNull() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":5,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts(null); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(0); + assertThat(result.getConflicts()).isEmpty(); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(equalToJson("{}"))); - } - - @Test - @DisplayName("should scan all policies with no-arg overload") - void shouldScanAllPoliciesNoArg() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":3,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts(); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(0); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) + } + + @Test + @DisplayName("should scan all policies with no-arg overload") + void shouldScanAllPoliciesNoArg() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":3,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts(); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(0); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(equalToJson("{}"))); - } - - @Test - @DisplayName("should reject empty policyId for detectPolicyConflicts") - void shouldRejectEmptyPolicyIdForConflicts() { - assertThatThrownBy(() -> axonflow.detectPolicyConflicts("")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("policyId cannot be empty"); - } - - @Test - @DisplayName("detectPolicyConflictsAsync should return future") - void detectPolicyConflictsAsyncShouldReturnFuture() throws Exception { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) + } + + @Test + @DisplayName("should reject empty policyId for detectPolicyConflicts") + void shouldRejectEmptyPolicyIdForConflicts() { + assertThatThrownBy(() -> axonflow.detectPolicyConflicts("")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("policyId cannot be empty"); + } + + @Test + @DisplayName("detectPolicyConflictsAsync should return future") + void detectPolicyConflictsAsyncShouldReturnFuture() throws Exception { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"async_policy\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":3,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - CompletableFuture future = axonflow.detectPolicyConflictsAsync("async_policy"); - PolicyConflictResponse result = future.get(); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(0); - } - - @Test - @DisplayName("should handle server error on detectPolicyConflicts") - void shouldHandleServerErrorOnConflicts() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":3,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + CompletableFuture future = + axonflow.detectPolicyConflictsAsync("async_policy"); + PolicyConflictResponse result = future.get(); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(0); + } + + @Test + @DisplayName("should handle server error on detectPolicyConflicts") + void shouldHandleServerErrorOnConflicts() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"bad_policy\"")) - .willReturn(aResponse() - .withStatus(500) - .withHeader("Content-Type", "application/json") - .withBody("{\"error\":\"internal server error\"}"))); - - assertThatThrownBy(() -> axonflow.detectPolicyConflicts("bad_policy")) - .isInstanceOf(AxonFlowException.class); - } - - @Test - @DisplayName("should handle unwrapped response for detectPolicyConflicts") - void shouldHandleUnwrappedResponseForConflicts() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(500) + .withHeader("Content-Type", "application/json") + .withBody("{\"error\":\"internal server error\"}"))); + + assertThatThrownBy(() -> axonflow.detectPolicyConflicts("bad_policy")) + .isInstanceOf(AxonFlowException.class); + } + + @Test + @DisplayName("should handle unwrapped response for detectPolicyConflicts") + void shouldHandleUnwrappedResponseForConflicts() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"unwrapped\"")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"conflicts\":[],\"total_policies\":2,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts("unwrapped"); - - assertThat(result).isNotNull(); - assertThat(result.getConflictCount()).isEqualTo(0); - } - - @Test - @DisplayName("should send policyId with special characters in request body") - void shouldSendPolicyIdWithSpecialCharactersInBody() { - stubFor(post(urlEqualTo("/api/v1/policies/conflicts")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":1,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); - - PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy with spaces"); - - assertThat(result).isNotNull(); - - verify(postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"conflicts\":[],\"total_policies\":2,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts("unwrapped"); + + assertThat(result).isNotNull(); + assertThat(result.getConflictCount()).isEqualTo(0); + } + + @Test + @DisplayName("should send policyId with special characters in request body") + void shouldSendPolicyIdWithSpecialCharactersInBody() { + stubFor( + post(urlEqualTo("/api/v1/policies/conflicts")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"data\":{\"conflicts\":[],\"total_policies\":1,\"conflict_count\":0,\"checked_at\":\"2026-03-24T10:00:00Z\",\"tier\":\"evaluation\"}}"))); + + PolicyConflictResponse result = axonflow.detectPolicyConflicts("policy with spaces"); + + assertThat(result).isNotNull(); + + verify( + postRequestedFor(urlEqualTo("/api/v1/policies/conflicts")) .withRequestBody(containing("\"policy_id\":\"policy with spaces\""))); - } + } } diff --git a/src/test/java/com/getaxonflow/sdk/PolicyTest.java b/src/test/java/com/getaxonflow/sdk/PolicyTest.java index 44771f2..2304eae 100644 --- a/src/test/java/com/getaxonflow/sdk/PolicyTest.java +++ b/src/test/java/com/getaxonflow/sdk/PolicyTest.java @@ -15,715 +15,768 @@ */ package com.getaxonflow.sdk; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.policies.PolicyTypes.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; - import java.util.Arrays; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for Policy CRUD methods. - * Part of Unified Policy Architecture v2.0.0. - */ +/** Tests for Policy CRUD methods. Part of Unified Policy Architecture v2.0.0. */ @WireMockTest @DisplayName("Policy CRUD Methods") class PolicyTest { - private AxonFlow axonflow; - - private static final String SAMPLE_STATIC_POLICY = - "{" + - "\"id\": \"pol_123\"," + - "\"name\": \"Block SQL Injection\"," + - "\"description\": \"Blocks SQL injection attempts\"," + - "\"category\": \"security-sqli\"," + - "\"tier\": \"system\"," + - "\"pattern\": \"(?i)(union\\\\s+select|drop\\\\s+table)\"," + - "\"severity\": \"critical\"," + - "\"enabled\": true," + - "\"action\": \"block\"," + - "\"created_at\": \"2025-01-01T00:00:00Z\"," + - "\"updated_at\": \"2025-01-01T00:00:00Z\"," + - "\"version\": 1" + - "}"; - - private static final String SAMPLE_DYNAMIC_POLICY = - "{" + - "\"id\": \"dpol_456\"," + - "\"name\": \"Rate Limit API\"," + - "\"description\": \"Rate limit API calls\"," + - "\"type\": \"cost\"," + - "\"conditions\": [{\"field\": \"requests_per_minute\", \"operator\": \"greater_than\", \"value\": 100}]," + - "\"actions\": [{\"type\": \"block\", \"config\": {\"reason\": \"Rate limit exceeded\"}}]," + - "\"priority\": 50," + - "\"enabled\": true," + - "\"created_at\": \"2025-01-01T00:00:00Z\"," + - "\"updated_at\": \"2025-01-01T00:00:00Z\"" + - "}"; - - private static final String SAMPLE_OVERRIDE = - "{" + - "\"policy_id\": \"pol_123\"," + - "\"action_override\": \"warn\"," + - "\"override_reason\": \"Testing override\"," + - "\"created_at\": \"2025-01-01T00:00:00Z\"," + - "\"active\": true" + - "}"; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .endpoint(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - } - - // ======================================================================== - // Static Policy Tests - // ======================================================================== - - @Nested - @DisplayName("Static Policies") - class StaticPolicies { - - @Test - @DisplayName("listStaticPolicies should return policies") - void listStaticPoliciesShouldReturnPolicies() { - stubFor(get(urlPathEqualTo("/api/v1/static-policies")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); - - List policies = axonflow.listStaticPolicies(); - - assertThat(policies).hasSize(1); - assertThat(policies.get(0).getId()).isEqualTo("pol_123"); - assertThat(policies.get(0).getName()).isEqualTo("Block SQL Injection"); - } - - @Test - @DisplayName("listStaticPolicies should return empty list when policies is null") - void listStaticPoliciesShouldReturnEmptyListWhenNull() { - stubFor(get(urlPathEqualTo("/api/v1/static-policies")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": null}"))); - - List policies = axonflow.listStaticPolicies(); - - assertThat(policies).isEmpty(); - } - - @Test - @DisplayName("listStaticPolicies with filters should include query params") - void listStaticPoliciesWithFiltersShouldIncludeQueryParams() { - stubFor(get(urlPathEqualTo("/api/v1/static-policies")) - .withQueryParam("category", equalTo("security-sqli")) - .withQueryParam("tier", equalTo("system")) - .withQueryParam("enabled", equalTo("true")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); - - ListStaticPoliciesOptions options = ListStaticPoliciesOptions.builder() - .category(PolicyCategory.SECURITY_SQLI) - .tier(PolicyTier.SYSTEM) - .enabled(true) - .build(); - - List policies = axonflow.listStaticPolicies(options); - - assertThat(policies).hasSize(1); - } - - @Test - @DisplayName("getStaticPolicy should return policy by ID") - void getStaticPolicyShouldReturnPolicyById() { - stubFor(get(urlEqualTo("/api/v1/static-policies/pol_123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_STATIC_POLICY))); - - StaticPolicy policy = axonflow.getStaticPolicy("pol_123"); - - assertThat(policy.getId()).isEqualTo("pol_123"); - assertThat(policy.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); - } - - @Test - @DisplayName("getStaticPolicy should require non-null policyId") - void getStaticPolicyShouldRequirePolicyId() { - assertThatThrownBy(() -> axonflow.getStaticPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("createStaticPolicy should create and return policy") - void createStaticPolicyShouldCreateAndReturnPolicy() { - stubFor(post(urlEqualTo("/api/v1/static-policies")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_STATIC_POLICY))); - - CreateStaticPolicyRequest request = CreateStaticPolicyRequest.builder() - .name("Block SQL Injection") - .category(PolicyCategory.SECURITY_SQLI) - .pattern("(?i)(union\\\\s+select|drop\\\\s+table)") - .severity(PolicySeverity.CRITICAL) - .build(); - - StaticPolicy policy = axonflow.createStaticPolicy(request); - - assertThat(policy.getId()).isEqualTo("pol_123"); - - verify(postRequestedFor(urlEqualTo("/api/v1/static-policies")) - .withHeader("Content-Type", containing("application/json"))); - } - - @Test - @DisplayName("createStaticPolicy should require non-null request") - void createStaticPolicyShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.createStaticPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("updateStaticPolicy should update and return policy") - void updateStaticPolicyShouldUpdateAndReturnPolicy() { - String updatedPolicy = SAMPLE_STATIC_POLICY.replace("\"severity\": \"critical\"", "\"severity\": \"high\""); - stubFor(put(urlEqualTo("/api/v1/static-policies/pol_123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(updatedPolicy))); - - UpdateStaticPolicyRequest request = UpdateStaticPolicyRequest.builder() - .severity(PolicySeverity.HIGH) - .build(); - - StaticPolicy policy = axonflow.updateStaticPolicy("pol_123", request); - - assertThat(policy.getSeverity()).isEqualTo(PolicySeverity.HIGH); - - verify(putRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); - } - - @Test - @DisplayName("deleteStaticPolicy should delete policy") - void deleteStaticPolicyShouldDeletePolicy() { - stubFor(delete(urlEqualTo("/api/v1/static-policies/pol_123")) - .willReturn(aResponse() - .withStatus(204))); - - axonflow.deleteStaticPolicy("pol_123"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); - } - - @Test - @DisplayName("deleteStaticPolicy should require non-null policyId") - void deleteStaticPolicyShouldRequirePolicyId() { - assertThatThrownBy(() -> axonflow.deleteStaticPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("toggleStaticPolicy should toggle enabled status") - void toggleStaticPolicyShouldToggleEnabledStatus() { - String toggledPolicy = SAMPLE_STATIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); - stubFor(patch(urlEqualTo("/api/v1/static-policies/pol_123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(toggledPolicy))); - - StaticPolicy policy = axonflow.toggleStaticPolicy("pol_123", false); - - assertThat(policy.isEnabled()).isFalse(); - - verify(patchRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123")) - .withRequestBody(containing("\"enabled\":false"))); - } - - @Test - @DisplayName("getEffectiveStaticPolicies should return effective policies") - void getEffectiveStaticPoliciesShouldReturnEffectivePolicies() { - stubFor(get(urlPathEqualTo("/api/v1/static-policies/effective")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"static\": [" + SAMPLE_STATIC_POLICY + "], \"dynamic\": []}"))); - - List policies = axonflow.getEffectiveStaticPolicies(); - - assertThat(policies).hasSize(1); - } - - @Test - @DisplayName("getEffectiveStaticPolicies should return empty list when static is null") - void getEffectiveStaticPoliciesShouldReturnEmptyListWhenNull() { - // Issue #40: Handle null policies list - stubFor(get(urlPathEqualTo("/api/v1/static-policies/effective")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"static\": null, \"dynamic\": []}"))); - - List policies = axonflow.getEffectiveStaticPolicies(); - - assertThat(policies).isEmpty(); - } - - @Test - @DisplayName("testPattern should test pattern against inputs") - void testPatternShouldTestPatternAgainstInputs() { - String responseBody = - "{" + - "\"valid\": true," + - "\"matches\": [" + - "{\"input\": \"SELECT * FROM users\", \"matched\": true}," + - "{\"input\": \"Hello world\", \"matched\": false}" + - "]" + - "}"; - - stubFor(post(urlEqualTo("/api/v1/static-policies/test")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseBody))); - - TestPatternResult result = axonflow.testPattern( - "(?i)select", - Arrays.asList("SELECT * FROM users", "Hello world") - ); - - assertThat(result.isValid()).isTrue(); - assertThat(result.getMatches()).hasSize(2); - assertThat(result.getMatches().get(0).isMatched()).isTrue(); - assertThat(result.getMatches().get(1).isMatched()).isFalse(); - } - - @Test - @DisplayName("testPattern should require non-null parameters") - void testPatternShouldRequireParameters() { - assertThatThrownBy(() -> axonflow.testPattern(null, Arrays.asList("test"))) - .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> axonflow.testPattern("pattern", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("getStaticPolicyVersions should return version history") - void getStaticPolicyVersionsShouldReturnVersionHistory() { - String responseBody = - "{" + - "\"policy_id\": \"pol_123\"," + - "\"versions\": [" + - "{\"version\": 2, \"changed_at\": \"2025-01-02T00:00:00Z\", \"change_type\": \"updated\"}," + - "{\"version\": 1, \"changed_at\": \"2025-01-01T00:00:00Z\", \"change_type\": \"created\"}" + - "]," + - "\"count\": 2" + - "}"; - - stubFor(get(urlEqualTo("/api/v1/static-policies/pol_123/versions")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseBody))); - - List versions = axonflow.getStaticPolicyVersions("pol_123"); - - assertThat(versions).hasSize(2); - assertThat(versions.get(0).getVersion()).isEqualTo(2); - } - } - - // ======================================================================== - // Policy Override Tests - // ======================================================================== - - @Nested - @DisplayName("Policy Overrides") - class PolicyOverrides { - - @Test - @DisplayName("createPolicyOverride should create override") - void createPolicyOverrideShouldCreateOverride() { - stubFor(post(urlEqualTo("/api/v1/static-policies/pol_123/override")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody(SAMPLE_OVERRIDE))); - - CreatePolicyOverrideRequest request = CreatePolicyOverrideRequest.builder() - .actionOverride(OverrideAction.WARN) - .overrideReason("Testing override") - .build(); - - PolicyOverride override = axonflow.createPolicyOverride("pol_123", request); - - assertThat(override.getActionOverride()).isEqualTo(OverrideAction.WARN); - assertThat(override.getOverrideReason()).isEqualTo("Testing override"); - - verify(postRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); - } - - @Test - @DisplayName("createPolicyOverride should require non-null parameters") - void createPolicyOverrideShouldRequireParameters() { - CreatePolicyOverrideRequest request = CreatePolicyOverrideRequest.builder() - .actionOverride(OverrideAction.WARN) - .build(); - - assertThatThrownBy(() -> axonflow.createPolicyOverride(null, request)) - .isInstanceOf(NullPointerException.class); - assertThatThrownBy(() -> axonflow.createPolicyOverride("pol_123", null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("deletePolicyOverride should delete override") - void deletePolicyOverrideShouldDeleteOverride() { - stubFor(delete(urlEqualTo("/api/v1/static-policies/pol_123/override")) - .willReturn(aResponse() - .withStatus(204))); - - axonflow.deletePolicyOverride("pol_123"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); - } - - @Test - @DisplayName("deletePolicyOverride should require non-null policyId") - void deletePolicyOverrideShouldRequirePolicyId() { - assertThatThrownBy(() -> axonflow.deletePolicyOverride(null)) - .isInstanceOf(NullPointerException.class); - } - } - - // ======================================================================== - // Dynamic Policy Tests - // ======================================================================== - - @Nested - @DisplayName("Dynamic Policies") - class DynamicPolicies { - - @Test - @DisplayName("listDynamicPolicies should return policies") - void listDynamicPoliciesShouldReturnPolicies() { - // Agent proxy (Issue #886) returns {"policies": [...]} wrapper - stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); - - List policies = axonflow.listDynamicPolicies(); - - assertThat(policies).hasSize(1); - assertThat(policies.get(0).getId()).isEqualTo("dpol_456"); - assertThat(policies.get(0).getName()).isEqualTo("Rate Limit API"); - } - - @Test - @DisplayName("listDynamicPolicies should return empty list when policies is null") - void listDynamicPoliciesShouldReturnEmptyListWhenNull() { - // Issue #40: Handle null policies list - stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": null}"))); - - List policies = axonflow.listDynamicPolicies(); - - assertThat(policies).isEmpty(); - } - - @Test - @DisplayName("listDynamicPolicies with filters should include query params") - void listDynamicPoliciesWithFiltersShouldIncludeQueryParams() { - // Agent proxy (Issue #886) returns {"policies": [...]} wrapper - stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies")) - .withQueryParam("type", equalTo("cost")) - .withQueryParam("enabled", equalTo("true")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); - - ListDynamicPoliciesOptions options = ListDynamicPoliciesOptions.builder() - .type("cost") - .enabled(true) - .build(); - - axonflow.listDynamicPolicies(options); - - verify(getRequestedFor(urlPathEqualTo("/api/v1/dynamic-policies")) - .withQueryParam("type", equalTo("cost"))); - } - - @Test - @DisplayName("getDynamicPolicy should return policy by ID") - void getDynamicPolicyShouldReturnPolicyById() { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - stubFor(get(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); - - DynamicPolicy policy = axonflow.getDynamicPolicy("dpol_456"); - - assertThat(policy.getId()).isEqualTo("dpol_456"); - assertThat(policy.getType()).isEqualTo("cost"); - } - - @Test - @DisplayName("getDynamicPolicy should require non-null policyId") - void getDynamicPolicyShouldRequirePolicyId() { - assertThatThrownBy(() -> axonflow.getDynamicPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("createDynamicPolicy should create and return policy") - void createDynamicPolicyShouldCreateAndReturnPolicy() { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - stubFor(post(urlEqualTo("/api/v1/dynamic-policies")) - .willReturn(aResponse() - .withStatus(201) - .withHeader("Content-Type", "application/json") - .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); - - CreateDynamicPolicyRequest request = CreateDynamicPolicyRequest.builder() - .name("Rate Limit API") - .type("cost") - .conditions(List.of(new DynamicPolicyCondition("requests_per_minute", "greater_than", 100))) - .actions(List.of(new DynamicPolicyAction("block", Map.of("reason", "Rate limit exceeded")))) - .priority(50) - .build(); - - DynamicPolicy policy = axonflow.createDynamicPolicy(request); - - assertThat(policy.getId()).isEqualTo("dpol_456"); - - verify(postRequestedFor(urlEqualTo("/api/v1/dynamic-policies"))); - } - - @Test - @DisplayName("createDynamicPolicy should require non-null request") - void createDynamicPolicyShouldRequireRequest() { - assertThatThrownBy(() -> axonflow.createDynamicPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("updateDynamicPolicy should update and return policy") - void updateDynamicPolicyShouldUpdateAndReturnPolicy() { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - stubFor(put(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); - - UpdateDynamicPolicyRequest request = UpdateDynamicPolicyRequest.builder() - .conditions(List.of(new DynamicPolicyCondition("requests_per_minute", "greater_than", 200))) - .build(); - - DynamicPolicy policy = axonflow.updateDynamicPolicy("dpol_456", request); - - assertThat(policy).isNotNull(); - - verify(putRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))); - } - - @Test - @DisplayName("deleteDynamicPolicy should delete policy") - void deleteDynamicPolicyShouldDeletePolicy() { - stubFor(delete(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) - .willReturn(aResponse() - .withStatus(204))); - - axonflow.deleteDynamicPolicy("dpol_456"); - - verify(deleteRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))); - } - - @Test - @DisplayName("deleteDynamicPolicy should require non-null policyId") - void deleteDynamicPolicyShouldRequirePolicyId() { - assertThatThrownBy(() -> axonflow.deleteDynamicPolicy(null)) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("toggleDynamicPolicy should toggle enabled status") - void toggleDynamicPolicyShouldToggleEnabledStatus() { - // Agent proxy (Issue #886) returns {"policy": {...}} wrapper - // Note: toggleDynamicPolicy uses PUT (not PATCH) to match API specification - String toggledPolicy = SAMPLE_DYNAMIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); - stubFor(put(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policy\": " + toggledPolicy + "}"))); - - DynamicPolicy policy = axonflow.toggleDynamicPolicy("dpol_456", false); - - assertThat(policy.isEnabled()).isFalse(); - - verify(putRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) - .withRequestBody(containing("\"enabled\":false"))); - } - - @Test - @DisplayName("getEffectiveDynamicPolicies should return effective policies") - void getEffectiveDynamicPoliciesShouldReturnEffectivePolicies() { - // Agent proxy (Issue #886) returns {"policies": [...]} wrapper - stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies/effective")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); - - List policies = axonflow.getEffectiveDynamicPolicies(); - - assertThat(policies).hasSize(1); - } - - @Test - @DisplayName("getEffectiveDynamicPolicies should return empty list when policies is null") - void getEffectiveDynamicPoliciesShouldReturnEmptyListWhenNull() { - // Issue #40: Handle null policies list - stubFor(get(urlPathEqualTo("/api/v1/dynamic-policies/effective")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"policies\": null}"))); - - List policies = axonflow.getEffectiveDynamicPolicies(); - - assertThat(policies).isEmpty(); - } - } - - // ======================================================================== - // Type Validation Tests - // ======================================================================== - - @Nested - @DisplayName("Policy Types") - class PolicyTypes { - - @Test - @DisplayName("CreateStaticPolicyRequest should have defaults") - void createStaticPolicyRequestShouldHaveDefaults() { - CreateStaticPolicyRequest request = CreateStaticPolicyRequest.builder() - .name("Test Policy") - .category(PolicyCategory.PII_GLOBAL) - .pattern("\\d{3}-\\d{2}-\\d{4}") - .build(); - - assertThat(request.getName()).isEqualTo("Test Policy"); - assertThat(request.getCategory()).isEqualTo(PolicyCategory.PII_GLOBAL); - assertThat(request.isEnabled()).isTrue(); - assertThat(request.getSeverity()).isEqualTo(PolicySeverity.MEDIUM); - assertThat(request.getAction()).isEqualTo(PolicyAction.BLOCK); - } - - @Test - @DisplayName("CreateDynamicPolicyRequest should have defaults") - void createDynamicPolicyRequestShouldHaveDefaults() { - CreateDynamicPolicyRequest request = CreateDynamicPolicyRequest.builder() - .name("Test Dynamic") - .type("risk") - .conditions(List.of(new DynamicPolicyCondition("risk_score", "greater_than", 0.8))) - .actions(List.of(new DynamicPolicyAction("warn", Map.of("threshold", 0.8)))) - .priority(10) - .build(); - - assertThat(request.getName()).isEqualTo("Test Dynamic"); - assertThat(request.isEnabled()).isTrue(); - assertThat(request.getType()).isEqualTo("risk"); - } - - @Test - @DisplayName("ListStaticPoliciesOptions should build correctly") - void listStaticPoliciesOptionsShouldBuildCorrectly() { - ListStaticPoliciesOptions options = ListStaticPoliciesOptions.builder() - .category(PolicyCategory.SECURITY_SQLI) - .tier(PolicyTier.SYSTEM) - .enabled(true) - .limit(10) - .offset(0) - .sortBy("name") - .sortOrder("asc") - .search("sql") - .build(); - - assertThat(options.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); - assertThat(options.getTier()).isEqualTo(PolicyTier.SYSTEM); - assertThat(options.getEnabled()).isTrue(); - assertThat(options.getLimit()).isEqualTo(10); - assertThat(options.getOffset()).isEqualTo(0); - assertThat(options.getSortBy()).isEqualTo("name"); - assertThat(options.getSortOrder()).isEqualTo("asc"); - assertThat(options.getSearch()).isEqualTo("sql"); - } - - @Test - @DisplayName("PolicyCategory enum values should serialize correctly") - void policyCategoryEnumValuesShouldSerializeCorrectly() { - assertThat(PolicyCategory.SECURITY_SQLI.getValue()).isEqualTo("security-sqli"); - assertThat(PolicyCategory.PII_GLOBAL.getValue()).isEqualTo("pii-global"); - assertThat(PolicyCategory.DYNAMIC_COST.getValue()).isEqualTo("dynamic-cost"); - assertThat(PolicyCategory.CODE_SECRETS.getValue()).isEqualTo("code-secrets"); - } - - @Test - @DisplayName("PolicyTier enum values should serialize correctly") - void policyTierEnumValuesShouldSerializeCorrectly() { - assertThat(PolicyTier.SYSTEM.getValue()).isEqualTo("system"); - assertThat(PolicyTier.ORGANIZATION.getValue()).isEqualTo("organization"); - assertThat(PolicyTier.TENANT.getValue()).isEqualTo("tenant"); - } - - @Test - @DisplayName("OverrideAction enum values should serialize correctly") - void overrideActionEnumValuesShouldSerializeCorrectly() { - assertThat(OverrideAction.BLOCK.getValue()).isEqualTo("block"); - assertThat(OverrideAction.REQUIRE_APPROVAL.getValue()).isEqualTo("require_approval"); - assertThat(OverrideAction.REDACT.getValue()).isEqualTo("redact"); - assertThat(OverrideAction.WARN.getValue()).isEqualTo("warn"); - assertThat(OverrideAction.LOG.getValue()).isEqualTo("log"); - } - - @Test - @DisplayName("PolicyAction enum values should serialize correctly") - void policyActionEnumValuesShouldSerializeCorrectly() { - assertThat(PolicyAction.BLOCK.getValue()).isEqualTo("block"); - assertThat(PolicyAction.REQUIRE_APPROVAL.getValue()).isEqualTo("require_approval"); - assertThat(PolicyAction.REDACT.getValue()).isEqualTo("redact"); - assertThat(PolicyAction.WARN.getValue()).isEqualTo("warn"); - assertThat(PolicyAction.LOG.getValue()).isEqualTo("log"); - assertThat(PolicyAction.ALLOW.getValue()).isEqualTo("allow"); - } + private AxonFlow axonflow; + + private static final String SAMPLE_STATIC_POLICY = + "{" + + "\"id\": \"pol_123\"," + + "\"name\": \"Block SQL Injection\"," + + "\"description\": \"Blocks SQL injection attempts\"," + + "\"category\": \"security-sqli\"," + + "\"tier\": \"system\"," + + "\"pattern\": \"(?i)(union\\\\s+select|drop\\\\s+table)\"," + + "\"severity\": \"critical\"," + + "\"enabled\": true," + + "\"action\": \"block\"," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"updated_at\": \"2025-01-01T00:00:00Z\"," + + "\"version\": 1" + + "}"; + + private static final String SAMPLE_DYNAMIC_POLICY = + "{" + + "\"id\": \"dpol_456\"," + + "\"name\": \"Rate Limit API\"," + + "\"description\": \"Rate limit API calls\"," + + "\"type\": \"cost\"," + + "\"conditions\": [{\"field\": \"requests_per_minute\", \"operator\": \"greater_than\", \"value\": 100}]," + + "\"actions\": [{\"type\": \"block\", \"config\": {\"reason\": \"Rate limit exceeded\"}}]," + + "\"priority\": 50," + + "\"enabled\": true," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"updated_at\": \"2025-01-01T00:00:00Z\"" + + "}"; + + private static final String SAMPLE_OVERRIDE = + "{" + + "\"policy_id\": \"pol_123\"," + + "\"action_override\": \"warn\"," + + "\"override_reason\": \"Testing override\"," + + "\"created_at\": \"2025-01-01T00:00:00Z\"," + + "\"active\": true" + + "}"; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .endpoint(wmRuntimeInfo.getHttpBaseUrl()) + .build()); + } + + // ======================================================================== + // Static Policy Tests + // ======================================================================== + + @Nested + @DisplayName("Static Policies") + class StaticPolicies { + + @Test + @DisplayName("listStaticPolicies should return policies") + void listStaticPoliciesShouldReturnPolicies() { + stubFor( + get(urlPathEqualTo("/api/v1/static-policies")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); + + List policies = axonflow.listStaticPolicies(); + + assertThat(policies).hasSize(1); + assertThat(policies.get(0).getId()).isEqualTo("pol_123"); + assertThat(policies.get(0).getName()).isEqualTo("Block SQL Injection"); + } + + @Test + @DisplayName("listStaticPolicies should return empty list when policies is null") + void listStaticPoliciesShouldReturnEmptyListWhenNull() { + stubFor( + get(urlPathEqualTo("/api/v1/static-policies")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": null}"))); + + List policies = axonflow.listStaticPolicies(); + + assertThat(policies).isEmpty(); + } + + @Test + @DisplayName("listStaticPolicies with filters should include query params") + void listStaticPoliciesWithFiltersShouldIncludeQueryParams() { + stubFor( + get(urlPathEqualTo("/api/v1/static-policies")) + .withQueryParam("category", equalTo("security-sqli")) + .withQueryParam("tier", equalTo("system")) + .withQueryParam("enabled", equalTo("true")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_STATIC_POLICY + "]}"))); + + ListStaticPoliciesOptions options = + ListStaticPoliciesOptions.builder() + .category(PolicyCategory.SECURITY_SQLI) + .tier(PolicyTier.SYSTEM) + .enabled(true) + .build(); + + List policies = axonflow.listStaticPolicies(options); + + assertThat(policies).hasSize(1); + } + + @Test + @DisplayName("getStaticPolicy should return policy by ID") + void getStaticPolicyShouldReturnPolicyById() { + stubFor( + get(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATIC_POLICY))); + + StaticPolicy policy = axonflow.getStaticPolicy("pol_123"); + + assertThat(policy.getId()).isEqualTo("pol_123"); + assertThat(policy.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); + } + + @Test + @DisplayName("getStaticPolicy should require non-null policyId") + void getStaticPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.getStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("createStaticPolicy should create and return policy") + void createStaticPolicyShouldCreateAndReturnPolicy() { + stubFor( + post(urlEqualTo("/api/v1/static-policies")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_STATIC_POLICY))); + + CreateStaticPolicyRequest request = + CreateStaticPolicyRequest.builder() + .name("Block SQL Injection") + .category(PolicyCategory.SECURITY_SQLI) + .pattern("(?i)(union\\\\s+select|drop\\\\s+table)") + .severity(PolicySeverity.CRITICAL) + .build(); + + StaticPolicy policy = axonflow.createStaticPolicy(request); + + assertThat(policy.getId()).isEqualTo("pol_123"); + + verify( + postRequestedFor(urlEqualTo("/api/v1/static-policies")) + .withHeader("Content-Type", containing("application/json"))); + } + + @Test + @DisplayName("createStaticPolicy should require non-null request") + void createStaticPolicyShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.createStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateStaticPolicy should update and return policy") + void updateStaticPolicyShouldUpdateAndReturnPolicy() { + String updatedPolicy = + SAMPLE_STATIC_POLICY.replace("\"severity\": \"critical\"", "\"severity\": \"high\""); + stubFor( + put(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(updatedPolicy))); + + UpdateStaticPolicyRequest request = + UpdateStaticPolicyRequest.builder().severity(PolicySeverity.HIGH).build(); + + StaticPolicy policy = axonflow.updateStaticPolicy("pol_123", request); + + assertThat(policy.getSeverity()).isEqualTo(PolicySeverity.HIGH); + + verify(putRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); + } + + @Test + @DisplayName("deleteStaticPolicy should delete policy") + void deleteStaticPolicyShouldDeletePolicy() { + stubFor( + delete(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn(aResponse().withStatus(204))); + + axonflow.deleteStaticPolicy("pol_123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123"))); + } + + @Test + @DisplayName("deleteStaticPolicy should require non-null policyId") + void deleteStaticPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deleteStaticPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("toggleStaticPolicy should toggle enabled status") + void toggleStaticPolicyShouldToggleEnabledStatus() { + String toggledPolicy = + SAMPLE_STATIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); + stubFor( + patch(urlEqualTo("/api/v1/static-policies/pol_123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(toggledPolicy))); + + StaticPolicy policy = axonflow.toggleStaticPolicy("pol_123", false); + + assertThat(policy.isEnabled()).isFalse(); + + verify( + patchRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("getEffectiveStaticPolicies should return effective policies") + void getEffectiveStaticPoliciesShouldReturnEffectivePolicies() { + stubFor( + get(urlPathEqualTo("/api/v1/static-policies/effective")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"static\": [" + SAMPLE_STATIC_POLICY + "], \"dynamic\": []}"))); + + List policies = axonflow.getEffectiveStaticPolicies(); + + assertThat(policies).hasSize(1); + } + + @Test + @DisplayName("getEffectiveStaticPolicies should return empty list when static is null") + void getEffectiveStaticPoliciesShouldReturnEmptyListWhenNull() { + // Issue #40: Handle null policies list + stubFor( + get(urlPathEqualTo("/api/v1/static-policies/effective")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"static\": null, \"dynamic\": []}"))); + + List policies = axonflow.getEffectiveStaticPolicies(); + + assertThat(policies).isEmpty(); + } + + @Test + @DisplayName("testPattern should test pattern against inputs") + void testPatternShouldTestPatternAgainstInputs() { + String responseBody = + "{" + + "\"valid\": true," + + "\"matches\": [" + + "{\"input\": \"SELECT * FROM users\", \"matched\": true}," + + "{\"input\": \"Hello world\", \"matched\": false}" + + "]" + + "}"; + + stubFor( + post(urlEqualTo("/api/v1/static-policies/test")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseBody))); + + TestPatternResult result = + axonflow.testPattern("(?i)select", Arrays.asList("SELECT * FROM users", "Hello world")); + + assertThat(result.isValid()).isTrue(); + assertThat(result.getMatches()).hasSize(2); + assertThat(result.getMatches().get(0).isMatched()).isTrue(); + assertThat(result.getMatches().get(1).isMatched()).isFalse(); + } + + @Test + @DisplayName("testPattern should require non-null parameters") + void testPatternShouldRequireParameters() { + assertThatThrownBy(() -> axonflow.testPattern(null, Arrays.asList("test"))) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> axonflow.testPattern("pattern", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("getStaticPolicyVersions should return version history") + void getStaticPolicyVersionsShouldReturnVersionHistory() { + String responseBody = + "{" + + "\"policy_id\": \"pol_123\"," + + "\"versions\": [" + + "{\"version\": 2, \"changed_at\": \"2025-01-02T00:00:00Z\", \"change_type\": \"updated\"}," + + "{\"version\": 1, \"changed_at\": \"2025-01-01T00:00:00Z\", \"change_type\": \"created\"}" + + "]," + + "\"count\": 2" + + "}"; + + stubFor( + get(urlEqualTo("/api/v1/static-policies/pol_123/versions")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseBody))); + + List versions = axonflow.getStaticPolicyVersions("pol_123"); + + assertThat(versions).hasSize(2); + assertThat(versions.get(0).getVersion()).isEqualTo(2); + } + } + + // ======================================================================== + // Policy Override Tests + // ======================================================================== + + @Nested + @DisplayName("Policy Overrides") + class PolicyOverrides { + + @Test + @DisplayName("createPolicyOverride should create override") + void createPolicyOverrideShouldCreateOverride() { + stubFor( + post(urlEqualTo("/api/v1/static-policies/pol_123/override")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody(SAMPLE_OVERRIDE))); + + CreatePolicyOverrideRequest request = + CreatePolicyOverrideRequest.builder() + .actionOverride(OverrideAction.WARN) + .overrideReason("Testing override") + .build(); + + PolicyOverride override = axonflow.createPolicyOverride("pol_123", request); + + assertThat(override.getActionOverride()).isEqualTo(OverrideAction.WARN); + assertThat(override.getOverrideReason()).isEqualTo("Testing override"); + + verify(postRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); + } + + @Test + @DisplayName("createPolicyOverride should require non-null parameters") + void createPolicyOverrideShouldRequireParameters() { + CreatePolicyOverrideRequest request = + CreatePolicyOverrideRequest.builder().actionOverride(OverrideAction.WARN).build(); + + assertThatThrownBy(() -> axonflow.createPolicyOverride(null, request)) + .isInstanceOf(NullPointerException.class); + assertThatThrownBy(() -> axonflow.createPolicyOverride("pol_123", null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("deletePolicyOverride should delete override") + void deletePolicyOverrideShouldDeleteOverride() { + stubFor( + delete(urlEqualTo("/api/v1/static-policies/pol_123/override")) + .willReturn(aResponse().withStatus(204))); + + axonflow.deletePolicyOverride("pol_123"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/static-policies/pol_123/override"))); + } + + @Test + @DisplayName("deletePolicyOverride should require non-null policyId") + void deletePolicyOverrideShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deletePolicyOverride(null)) + .isInstanceOf(NullPointerException.class); + } + } + + // ======================================================================== + // Dynamic Policy Tests + // ======================================================================== + + @Nested + @DisplayName("Dynamic Policies") + class DynamicPolicies { + + @Test + @DisplayName("listDynamicPolicies should return policies") + void listDynamicPoliciesShouldReturnPolicies() { + // Agent proxy (Issue #886) returns {"policies": [...]} wrapper + stubFor( + get(urlPathEqualTo("/api/v1/dynamic-policies")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); + + List policies = axonflow.listDynamicPolicies(); + + assertThat(policies).hasSize(1); + assertThat(policies.get(0).getId()).isEqualTo("dpol_456"); + assertThat(policies.get(0).getName()).isEqualTo("Rate Limit API"); + } + + @Test + @DisplayName("listDynamicPolicies should return empty list when policies is null") + void listDynamicPoliciesShouldReturnEmptyListWhenNull() { + // Issue #40: Handle null policies list + stubFor( + get(urlPathEqualTo("/api/v1/dynamic-policies")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": null}"))); + + List policies = axonflow.listDynamicPolicies(); + + assertThat(policies).isEmpty(); + } + + @Test + @DisplayName("listDynamicPolicies with filters should include query params") + void listDynamicPoliciesWithFiltersShouldIncludeQueryParams() { + // Agent proxy (Issue #886) returns {"policies": [...]} wrapper + stubFor( + get(urlPathEqualTo("/api/v1/dynamic-policies")) + .withQueryParam("type", equalTo("cost")) + .withQueryParam("enabled", equalTo("true")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); + + ListDynamicPoliciesOptions options = + ListDynamicPoliciesOptions.builder().type("cost").enabled(true).build(); + + axonflow.listDynamicPolicies(options); + + verify( + getRequestedFor(urlPathEqualTo("/api/v1/dynamic-policies")) + .withQueryParam("type", equalTo("cost"))); + } + + @Test + @DisplayName("getDynamicPolicy should return policy by ID") + void getDynamicPolicyShouldReturnPolicyById() { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + stubFor( + get(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); + + DynamicPolicy policy = axonflow.getDynamicPolicy("dpol_456"); + + assertThat(policy.getId()).isEqualTo("dpol_456"); + assertThat(policy.getType()).isEqualTo("cost"); + } + + @Test + @DisplayName("getDynamicPolicy should require non-null policyId") + void getDynamicPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.getDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("createDynamicPolicy should create and return policy") + void createDynamicPolicyShouldCreateAndReturnPolicy() { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + stubFor( + post(urlEqualTo("/api/v1/dynamic-policies")) + .willReturn( + aResponse() + .withStatus(201) + .withHeader("Content-Type", "application/json") + .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); + + CreateDynamicPolicyRequest request = + CreateDynamicPolicyRequest.builder() + .name("Rate Limit API") + .type("cost") + .conditions( + List.of(new DynamicPolicyCondition("requests_per_minute", "greater_than", 100))) + .actions( + List.of( + new DynamicPolicyAction("block", Map.of("reason", "Rate limit exceeded")))) + .priority(50) + .build(); + + DynamicPolicy policy = axonflow.createDynamicPolicy(request); + + assertThat(policy.getId()).isEqualTo("dpol_456"); + + verify(postRequestedFor(urlEqualTo("/api/v1/dynamic-policies"))); + } + + @Test + @DisplayName("createDynamicPolicy should require non-null request") + void createDynamicPolicyShouldRequireRequest() { + assertThatThrownBy(() -> axonflow.createDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("updateDynamicPolicy should update and return policy") + void updateDynamicPolicyShouldUpdateAndReturnPolicy() { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + stubFor( + put(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policy\": " + SAMPLE_DYNAMIC_POLICY + "}"))); + + UpdateDynamicPolicyRequest request = + UpdateDynamicPolicyRequest.builder() + .conditions( + List.of(new DynamicPolicyCondition("requests_per_minute", "greater_than", 200))) + .build(); + + DynamicPolicy policy = axonflow.updateDynamicPolicy("dpol_456", request); + + assertThat(policy).isNotNull(); + + verify(putRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))); + } + + @Test + @DisplayName("deleteDynamicPolicy should delete policy") + void deleteDynamicPolicyShouldDeletePolicy() { + stubFor( + delete(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) + .willReturn(aResponse().withStatus(204))); + + axonflow.deleteDynamicPolicy("dpol_456"); + + verify(deleteRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456"))); + } + + @Test + @DisplayName("deleteDynamicPolicy should require non-null policyId") + void deleteDynamicPolicyShouldRequirePolicyId() { + assertThatThrownBy(() -> axonflow.deleteDynamicPolicy(null)) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("toggleDynamicPolicy should toggle enabled status") + void toggleDynamicPolicyShouldToggleEnabledStatus() { + // Agent proxy (Issue #886) returns {"policy": {...}} wrapper + // Note: toggleDynamicPolicy uses PUT (not PATCH) to match API specification + String toggledPolicy = + SAMPLE_DYNAMIC_POLICY.replace("\"enabled\": true", "\"enabled\": false"); + stubFor( + put(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policy\": " + toggledPolicy + "}"))); + + DynamicPolicy policy = axonflow.toggleDynamicPolicy("dpol_456", false); + + assertThat(policy.isEnabled()).isFalse(); + + verify( + putRequestedFor(urlEqualTo("/api/v1/dynamic-policies/dpol_456")) + .withRequestBody(containing("\"enabled\":false"))); + } + + @Test + @DisplayName("getEffectiveDynamicPolicies should return effective policies") + void getEffectiveDynamicPoliciesShouldReturnEffectivePolicies() { + // Agent proxy (Issue #886) returns {"policies": [...]} wrapper + stubFor( + get(urlPathEqualTo("/api/v1/dynamic-policies/effective")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": [" + SAMPLE_DYNAMIC_POLICY + "]}"))); + + List policies = axonflow.getEffectiveDynamicPolicies(); + + assertThat(policies).hasSize(1); + } + + @Test + @DisplayName("getEffectiveDynamicPolicies should return empty list when policies is null") + void getEffectiveDynamicPoliciesShouldReturnEmptyListWhenNull() { + // Issue #40: Handle null policies list + stubFor( + get(urlPathEqualTo("/api/v1/dynamic-policies/effective")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"policies\": null}"))); + + List policies = axonflow.getEffectiveDynamicPolicies(); + + assertThat(policies).isEmpty(); + } + } + + // ======================================================================== + // Type Validation Tests + // ======================================================================== + + @Nested + @DisplayName("Policy Types") + class PolicyTypes { + + @Test + @DisplayName("CreateStaticPolicyRequest should have defaults") + void createStaticPolicyRequestShouldHaveDefaults() { + CreateStaticPolicyRequest request = + CreateStaticPolicyRequest.builder() + .name("Test Policy") + .category(PolicyCategory.PII_GLOBAL) + .pattern("\\d{3}-\\d{2}-\\d{4}") + .build(); + + assertThat(request.getName()).isEqualTo("Test Policy"); + assertThat(request.getCategory()).isEqualTo(PolicyCategory.PII_GLOBAL); + assertThat(request.isEnabled()).isTrue(); + assertThat(request.getSeverity()).isEqualTo(PolicySeverity.MEDIUM); + assertThat(request.getAction()).isEqualTo(PolicyAction.BLOCK); + } + + @Test + @DisplayName("CreateDynamicPolicyRequest should have defaults") + void createDynamicPolicyRequestShouldHaveDefaults() { + CreateDynamicPolicyRequest request = + CreateDynamicPolicyRequest.builder() + .name("Test Dynamic") + .type("risk") + .conditions(List.of(new DynamicPolicyCondition("risk_score", "greater_than", 0.8))) + .actions(List.of(new DynamicPolicyAction("warn", Map.of("threshold", 0.8)))) + .priority(10) + .build(); + + assertThat(request.getName()).isEqualTo("Test Dynamic"); + assertThat(request.isEnabled()).isTrue(); + assertThat(request.getType()).isEqualTo("risk"); + } + + @Test + @DisplayName("ListStaticPoliciesOptions should build correctly") + void listStaticPoliciesOptionsShouldBuildCorrectly() { + ListStaticPoliciesOptions options = + ListStaticPoliciesOptions.builder() + .category(PolicyCategory.SECURITY_SQLI) + .tier(PolicyTier.SYSTEM) + .enabled(true) + .limit(10) + .offset(0) + .sortBy("name") + .sortOrder("asc") + .search("sql") + .build(); + + assertThat(options.getCategory()).isEqualTo(PolicyCategory.SECURITY_SQLI); + assertThat(options.getTier()).isEqualTo(PolicyTier.SYSTEM); + assertThat(options.getEnabled()).isTrue(); + assertThat(options.getLimit()).isEqualTo(10); + assertThat(options.getOffset()).isEqualTo(0); + assertThat(options.getSortBy()).isEqualTo("name"); + assertThat(options.getSortOrder()).isEqualTo("asc"); + assertThat(options.getSearch()).isEqualTo("sql"); + } + + @Test + @DisplayName("PolicyCategory enum values should serialize correctly") + void policyCategoryEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyCategory.SECURITY_SQLI.getValue()).isEqualTo("security-sqli"); + assertThat(PolicyCategory.PII_GLOBAL.getValue()).isEqualTo("pii-global"); + assertThat(PolicyCategory.DYNAMIC_COST.getValue()).isEqualTo("dynamic-cost"); + assertThat(PolicyCategory.CODE_SECRETS.getValue()).isEqualTo("code-secrets"); + } + + @Test + @DisplayName("PolicyTier enum values should serialize correctly") + void policyTierEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyTier.SYSTEM.getValue()).isEqualTo("system"); + assertThat(PolicyTier.ORGANIZATION.getValue()).isEqualTo("organization"); + assertThat(PolicyTier.TENANT.getValue()).isEqualTo("tenant"); + } + + @Test + @DisplayName("OverrideAction enum values should serialize correctly") + void overrideActionEnumValuesShouldSerializeCorrectly() { + assertThat(OverrideAction.BLOCK.getValue()).isEqualTo("block"); + assertThat(OverrideAction.REQUIRE_APPROVAL.getValue()).isEqualTo("require_approval"); + assertThat(OverrideAction.REDACT.getValue()).isEqualTo("redact"); + assertThat(OverrideAction.WARN.getValue()).isEqualTo("warn"); + assertThat(OverrideAction.LOG.getValue()).isEqualTo("log"); + } + + @Test + @DisplayName("PolicyAction enum values should serialize correctly") + void policyActionEnumValuesShouldSerializeCorrectly() { + assertThat(PolicyAction.BLOCK.getValue()).isEqualTo("block"); + assertThat(PolicyAction.REQUIRE_APPROVAL.getValue()).isEqualTo("require_approval"); + assertThat(PolicyAction.REDACT.getValue()).isEqualTo("redact"); + assertThat(PolicyAction.WARN.getValue()).isEqualTo("warn"); + assertThat(PolicyAction.LOG.getValue()).isEqualTo("log"); + assertThat(PolicyAction.ALLOW.getValue()).isEqualTo("allow"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java b/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java index 44774a5..4881b81 100644 --- a/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java +++ b/src/test/java/com/getaxonflow/sdk/SelfHostedZeroConfigTest.java @@ -15,7 +15,9 @@ */ package com.getaxonflow.sdk; -import com.getaxonflow.sdk.exceptions.ConfigurationException; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; @@ -24,507 +26,550 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - /** * Self-Hosted Zero-Config Mode Tests. * - *

Tests for the zero-configuration self-hosted mode where users can run - * AxonFlow without any API keys, license keys, or credentials. + *

Tests for the zero-configuration self-hosted mode where users can run AxonFlow without any API + * keys, license keys, or credentials. * *

This tests the scenario where a first-time user: + * *

    - *
  1. Starts the agent with SELF_HOSTED_MODE=true
  2. - *
  3. Connects the SDK with no credentials
  4. - *
  5. Makes requests that should succeed without authentication
  6. + *
  7. Starts the agent with SELF_HOSTED_MODE=true + *
  8. Connects the SDK with no credentials + *
  9. Makes requests that should succeed without authentication *
*/ @WireMockTest @DisplayName("Self-Hosted Zero-Config Mode Tests") class SelfHostedZeroConfigTest { - // ======================================================================== - // 1. CLIENT INITIALIZATION WITHOUT CREDENTIALS - // ======================================================================== - @Nested - @DisplayName("1. Client Initialization Without Credentials") - class ClientInitializationTests { - - @Test - @DisplayName("should create client with no credentials for localhost") - void shouldCreateClientWithNoCredentialsForLocalhost(WireMockRuntimeInfo wmRuntimeInfo) { - // WireMock runs on localhost - should not require credentials - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - // No clientId, no clientSecret - .build()); - - assertThat(client).isNotNull(); - System.out.println("✅ Client created without credentials for localhost"); - } - - @Test - @DisplayName("should create client with empty credentials for localhost") - void shouldCreateClientWithEmptyCredentialsForLocalhost(WireMockRuntimeInfo wmRuntimeInfo) { - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("") - .clientSecret("") - .build()); - - assertThat(client).isNotNull(); - System.out.println("✅ Client created with empty credentials for localhost"); - } - - @Test - @DisplayName("should allow client creation without credentials for any endpoint (community mode)") - void shouldAllowClientCreationWithoutCredentialsForAnyEndpoint() { - // Community mode: credentials are optional for any endpoint - AxonFlowConfig config = AxonFlowConfig.builder() - .agentUrl("https://my-custom-domain.local") - // No credentials - community mode - .build(); - - assertThat(config.hasCredentials()).isFalse(); - - System.out.println("✅ Community mode works without credentials for any endpoint"); - } + // ======================================================================== + // 1. CLIENT INITIALIZATION WITHOUT CREDENTIALS + // ======================================================================== + @Nested + @DisplayName("1. Client Initialization Without Credentials") + class ClientInitializationTests { + + @Test + @DisplayName("should create client with no credentials for localhost") + void shouldCreateClientWithNoCredentialsForLocalhost(WireMockRuntimeInfo wmRuntimeInfo) { + // WireMock runs on localhost - should not require credentials + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + // No clientId, no clientSecret + .build()); + + assertThat(client).isNotNull(); + System.out.println("✅ Client created without credentials for localhost"); + } + + @Test + @DisplayName("should create client with empty credentials for localhost") + void shouldCreateClientWithEmptyCredentialsForLocalhost(WireMockRuntimeInfo wmRuntimeInfo) { + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("") + .clientSecret("") + .build()); + + assertThat(client).isNotNull(); + System.out.println("✅ Client created with empty credentials for localhost"); + } + + @Test + @DisplayName( + "should allow client creation without credentials for any endpoint (community mode)") + void shouldAllowClientCreationWithoutCredentialsForAnyEndpoint() { + // Community mode: credentials are optional for any endpoint + AxonFlowConfig config = + AxonFlowConfig.builder() + .agentUrl("https://my-custom-domain.local") + // No credentials - community mode + .build(); + + assertThat(config.hasCredentials()).isFalse(); + + System.out.println("✅ Community mode works without credentials for any endpoint"); + } + } + + // ======================================================================== + // 2. GATEWAY MODE (Enterprise Feature - requires credentials) + // ======================================================================== + @Nested + @DisplayName("2. Gateway Mode (Enterprise Feature)") + @WireMockTest + class GatewayModeTests { + + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + // Gateway Mode is an enterprise feature that requires credentials + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); } - // ======================================================================== - // 2. GATEWAY MODE (Enterprise Feature - requires credentials) - // ======================================================================== - @Nested - @DisplayName("2. Gateway Mode (Enterprise Feature)") - @WireMockTest - class GatewayModeTests { - - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - // Gateway Mode is an enterprise feature that requires credentials - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - @Test - @DisplayName("should perform pre-check with empty token") - void shouldPerformPreCheckWithEmptyToken() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_zeroconfig_123\"," - + "\"approved\": true," - + "\"policies\": [\"default_policy\"]" - + "}"))); - - PolicyApprovalResult result = axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken("") // Empty token - zero-config scenario - .query("What is the weather in Paris?") - .build() - ); - - assertThat(result.isApproved()).isTrue(); - assertThat(result.getContextId()).isEqualTo("ctx_zeroconfig_123"); - - // Verify request was made without auth headers - verify(postRequestedFor(urlEqualTo("/api/policy/pre-check")) - .withRequestBody(containing("\"user_token\":\"\""))); - - System.out.println("✅ Pre-check succeeded with empty token"); - } - - @Test - @DisplayName("should perform pre-check with whitespace token") - void shouldPerformPreCheckWithWhitespaceToken() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_whitespace_456\"," - + "\"approved\": true," - + "\"policies\": []" - + "}"))); - - PolicyApprovalResult result = axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken(" ") // Whitespace only - .query("Simple test query") - .build() - ); - - assertThat(result.isApproved()).isTrue(); - System.out.println("✅ Pre-check succeeded with whitespace token"); - } - - @Test - @DisplayName("should complete full Gateway Mode flow without credentials") - void shouldCompleteFullGatewayFlowWithoutCredentials() { - // Step 1: Pre-check - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_fullflow_789\"," - + "\"approved\": true" - + "}"))); - - PolicyApprovalResult preCheck = axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken("") - .query("Analyze quarterly sales data") - .build() - ); - - assertThat(preCheck.getContextId()).isEqualTo("ctx_fullflow_789"); - - // Step 2: Audit - stubFor(post(urlEqualTo("/api/audit/llm-call")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"audit_id\": \"audit_zeroconfig_001\"" - + "}"))); - - AuditResult audit = axonflow.auditLLMCall(AuditOptions.builder() - .contextId(preCheck.getContextId()) - .clientId("default") - .provider("openai") - .model("gpt-4") - .tokenUsage(TokenUsage.of(100, 175)) - .latencyMs(350) - .build() - ); - - assertThat(audit.isSuccess()).isTrue(); - assertThat(audit.getAuditId()).isEqualTo("audit_zeroconfig_001"); - - System.out.println("✅ Full Gateway Mode flow completed without credentials"); - } + @Test + @DisplayName("should perform pre-check with empty token") + void shouldPerformPreCheckWithEmptyToken() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_zeroconfig_123\"," + + "\"approved\": true," + + "\"policies\": [\"default_policy\"]" + + "}"))); + + PolicyApprovalResult result = + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken("") // Empty token - zero-config scenario + .query("What is the weather in Paris?") + .build()); + + assertThat(result.isApproved()).isTrue(); + assertThat(result.getContextId()).isEqualTo("ctx_zeroconfig_123"); + + // Verify request was made without auth headers + verify( + postRequestedFor(urlEqualTo("/api/policy/pre-check")) + .withRequestBody(containing("\"user_token\":\"\""))); + + System.out.println("✅ Pre-check succeeded with empty token"); } - // ======================================================================== - // 3. PROXY MODE WITHOUT AUTHENTICATION - // ======================================================================== - @Nested - @DisplayName("3. Proxy Mode Without Authentication") - @WireMockTest - class ProxyModeTests { - - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - } - - @Test - @DisplayName("should execute query with empty token") - void shouldExecuteQueryWithEmptyToken() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"answer\": \"4\"}," - + "\"blocked\": false" - + "}"))); - - ClientResponse response = axonflow.proxyLLMCall(ClientRequest.builder() - .userToken("") // Empty token - .query("What is 2 + 2?") - .build() - ); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.isBlocked()).isFalse(); - - System.out.println("✅ Query executed with empty token"); - } + @Test + @DisplayName("should perform pre-check with whitespace token") + void shouldPerformPreCheckWithWhitespaceToken() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_whitespace_456\"," + + "\"approved\": true," + + "\"policies\": []" + + "}"))); + + PolicyApprovalResult result = + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken(" ") // Whitespace only + .query("Simple test query") + .build()); + + assertThat(result.isApproved()).isTrue(); + System.out.println("✅ Pre-check succeeded with whitespace token"); } - // ======================================================================== - // 4. POLICY ENFORCEMENT (Enterprise Feature - requires credentials) - // ======================================================================== - @Nested - @DisplayName("4. Policy Enforcement (Enterprise Feature)") - @WireMockTest - class PolicyEnforcementTests { - - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - // Policy enforcement (Gateway Mode) is an enterprise feature - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - } - - @Test - @DisplayName("should block SQL injection with enterprise credentials") - void shouldBlockSqlInjectionWithoutCredentials() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_blocked_001\"," - + "\"approved\": false," - + "\"block_reason\": \"SQL injection detected\"," - + "\"policies\": [\"sql_injection_detection\"]" - + "}"))); - - assertThatThrownBy(() -> axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken("") - .query("SELECT * FROM users; DROP TABLE users;--") - .build() - )).hasMessageContaining("SQL injection"); - - System.out.println("✅ SQL injection blocked without credentials"); - } - - @Test - @DisplayName("should block PII with enterprise credentials") - void shouldBlockPiiWithoutCredentials() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_blocked_002\"," - + "\"approved\": false," - + "\"block_reason\": \"PII detected: SSN\"," - + "\"policies\": [\"pii_detection\"]" - + "}"))); - - assertThatThrownBy(() -> axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken("") - .query("My social security number is 123-45-6789") - .build() - )).hasMessageContaining("PII"); - - System.out.println("✅ PII blocked without credentials"); - } + @Test + @DisplayName("should complete full Gateway Mode flow without credentials") + void shouldCompleteFullGatewayFlowWithoutCredentials() { + // Step 1: Pre-check + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_fullflow_789\"," + + "\"approved\": true" + + "}"))); + + PolicyApprovalResult preCheck = + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken("") + .query("Analyze quarterly sales data") + .build()); + + assertThat(preCheck.getContextId()).isEqualTo("ctx_fullflow_789"); + + // Step 2: Audit + stubFor( + post(urlEqualTo("/api/audit/llm-call")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"audit_id\": \"audit_zeroconfig_001\"" + + "}"))); + + AuditResult audit = + axonflow.auditLLMCall( + AuditOptions.builder() + .contextId(preCheck.getContextId()) + .clientId("default") + .provider("openai") + .model("gpt-4") + .tokenUsage(TokenUsage.of(100, 175)) + .latencyMs(350) + .build()); + + assertThat(audit.isSuccess()).isTrue(); + assertThat(audit.getAuditId()).isEqualTo("audit_zeroconfig_001"); + + System.out.println("✅ Full Gateway Mode flow completed without credentials"); + } + } + + // ======================================================================== + // 3. PROXY MODE WITHOUT AUTHENTICATION + // ======================================================================== + @Nested + @DisplayName("3. Proxy Mode Without Authentication") + @WireMockTest + class ProxyModeTests { + + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); } - // ======================================================================== - // 5. HEALTH CHECK WITHOUT AUTH - // ======================================================================== - @Nested - @DisplayName("5. Health Check Without Authentication") - @WireMockTest - class HealthCheckTests { - - @Test - @DisplayName("should check health without credentials") - void shouldCheckHealthWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"status\": \"healthy\"," - + "\"version\": \"1.0.0\"" - + "}"))); - - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - - HealthStatus health = client.healthCheck(); - - assertThat(health.isHealthy()).isTrue(); - assertThat(health.getVersion()).isEqualTo("1.0.0"); - - System.out.println("✅ Health check succeeded without credentials"); - } + @Test + @DisplayName("should execute query with empty token") + void shouldExecuteQueryWithEmptyToken() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"data\": {\"answer\": \"4\"}," + + "\"blocked\": false" + + "}"))); + + ClientResponse response = + axonflow.proxyLLMCall( + ClientRequest.builder() + .userToken("") // Empty token + .query("What is 2 + 2?") + .build()); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.isBlocked()).isFalse(); + + System.out.println("✅ Query executed with empty token"); + } + } + + // ======================================================================== + // 4. POLICY ENFORCEMENT (Enterprise Feature - requires credentials) + // ======================================================================== + @Nested + @DisplayName("4. Policy Enforcement (Enterprise Feature)") + @WireMockTest + class PolicyEnforcementTests { + + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + // Policy enforcement (Gateway Mode) is an enterprise feature + axonflow = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); } - // ======================================================================== - // 6. FIRST-TIME USER EXPERIENCE (Community Mode) - // ======================================================================== - @Nested - @DisplayName("6. First-Time User Experience (Community Mode)") - @WireMockTest - class FirstTimeUserTests { - - @Test - @DisplayName("should support first-time user with minimal configuration for community features") - void shouldSupportFirstTimeUser(WireMockRuntimeInfo wmRuntimeInfo) { - // Stub health endpoint - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"status\": \"healthy\"}"))); - - // Stub proxyLLMCall endpoint (community feature) - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"answer\": \"Hello world!\"}" - + "}"))); - - // First-time user - minimal configuration (community mode) - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - // No credentials - community mode - .build()); - - // Step 1: Health check should work - HealthStatus health = client.healthCheck(); - assertThat(health.isHealthy()).isTrue(); - - // Step 2: proxyLLMCall should work (community feature) - ClientResponse response = client.proxyLLMCall( - ClientRequest.builder() - .userToken("") - .query("Hello, this is my first query!") - .build() - ); - - assertThat(response.isSuccess()).isTrue(); - - System.out.println("✅ First-time user experience validated (community mode)"); - System.out.println(" - Client creation: OK"); - System.out.println(" - Health check: OK"); - System.out.println(" - Proxy LLM call: OK"); - } + @Test + @DisplayName("should block SQL injection with enterprise credentials") + void shouldBlockSqlInjectionWithoutCredentials() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_blocked_001\"," + + "\"approved\": false," + + "\"block_reason\": \"SQL injection detected\"," + + "\"policies\": [\"sql_injection_detection\"]" + + "}"))); + + assertThatThrownBy( + () -> + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken("") + .query("SELECT * FROM users; DROP TABLE users;--") + .build())) + .hasMessageContaining("SQL injection"); + + System.out.println("✅ SQL injection blocked without credentials"); } - // ======================================================================== - // 7. AUTH HEADERS BASED ON CREDENTIALS - // ======================================================================== - @Nested - @DisplayName("7. Auth Headers Based on Credentials") - @WireMockTest - class AuthHeaderTests { - - @Test - @DisplayName("should send community Basic auth when no credentials configured") - void shouldSendCommunityAuthWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"answer\": \"test\"}" - + "}"))); - - // No credentials - community mode (effective clientId = "community") - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - - client.proxyLLMCall( - ClientRequest.builder() - .userToken("") - .query("Test query") - .build() - ); - - // Basic auth always sent with effective clientId ("community:") - String expectedAuth = "Basic " + java.util.Base64.getEncoder() - .encodeToString("community:".getBytes(java.nio.charset.StandardCharsets.UTF_8)); - verify(postRequestedFor(urlEqualTo("/api/request")) - .withoutHeader("X-License-Key") - .withHeader("Authorization", equalTo(expectedAuth))); - - System.out.println("✅ Community mode: Basic auth with default clientId"); - } - - @Test - @DisplayName("should send auth headers when credentials are configured") - void shouldSendAuthHeadersWithCredentials(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"answer\": \"test\"}" - + "}"))); - - // With credentials - enterprise mode - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("test-client").clientSecret("test-secret") - .build()); - - client.proxyLLMCall( - ClientRequest.builder() - .userToken("") - .query("Test query") - .build() - ); - - // Verify OAuth2 Basic auth header is sent when credentials are configured - String expectedBasic = "Basic " + java.util.Base64.getEncoder().encodeToString( - "test-client:test-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8) - ); - verify(postRequestedFor(urlEqualTo("/api/request")) - .withHeader("Authorization", equalTo(expectedBasic))); - - System.out.println("✅ Auth headers sent when credentials are configured"); - } - - @Test - @DisplayName("should send OAuth2 Basic auth with clientId and clientSecret") - void shouldSendOAuth2BasicAuth(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"answer\": \"test\"}" - + "}"))); - - // With OAuth2 credentials - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .clientId("my-client") - .clientSecret("my-secret") - .build()); - - client.proxyLLMCall( - ClientRequest.builder() - .userToken("") - .query("Test query") - .build() - ); - - // Verify OAuth2 Basic auth header is sent - String expectedBasic = "Basic " + java.util.Base64.getEncoder().encodeToString( - "my-client:my-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8) - ); - verify(postRequestedFor(urlEqualTo("/api/request")) - .withHeader("Authorization", equalTo(expectedBasic))); - - System.out.println("✅ OAuth2 Basic auth header sent correctly"); - } + @Test + @DisplayName("should block PII with enterprise credentials") + void shouldBlockPiiWithoutCredentials() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_blocked_002\"," + + "\"approved\": false," + + "\"block_reason\": \"PII detected: SSN\"," + + "\"policies\": [\"pii_detection\"]" + + "}"))); + + assertThatThrownBy( + () -> + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken("") + .query("My social security number is 123-45-6789") + .build())) + .hasMessageContaining("PII"); + + System.out.println("✅ PII blocked without credentials"); + } + } + + // ======================================================================== + // 5. HEALTH CHECK WITHOUT AUTH + // ======================================================================== + @Nested + @DisplayName("5. Health Check Without Authentication") + @WireMockTest + class HealthCheckTests { + + @Test + @DisplayName("should check health without credentials") + void shouldCheckHealthWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + "\"status\": \"healthy\"," + "\"version\": \"1.0.0\"" + "}"))); + + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + + HealthStatus health = client.healthCheck(); + + assertThat(health.isHealthy()).isTrue(); + assertThat(health.getVersion()).isEqualTo("1.0.0"); + + System.out.println("✅ Health check succeeded without credentials"); + } + } + + // ======================================================================== + // 6. FIRST-TIME USER EXPERIENCE (Community Mode) + // ======================================================================== + @Nested + @DisplayName("6. First-Time User Experience (Community Mode)") + @WireMockTest + class FirstTimeUserTests { + + @Test + @DisplayName("should support first-time user with minimal configuration for community features") + void shouldSupportFirstTimeUser(WireMockRuntimeInfo wmRuntimeInfo) { + // Stub health endpoint + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"status\": \"healthy\"}"))); + + // Stub proxyLLMCall endpoint (community feature) + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"data\": {\"answer\": \"Hello world!\"}" + + "}"))); + + // First-time user - minimal configuration (community mode) + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + // No credentials - community mode + .build()); + + // Step 1: Health check should work + HealthStatus health = client.healthCheck(); + assertThat(health.isHealthy()).isTrue(); + + // Step 2: proxyLLMCall should work (community feature) + ClientResponse response = + client.proxyLLMCall( + ClientRequest.builder() + .userToken("") + .query("Hello, this is my first query!") + .build()); + + assertThat(response.isSuccess()).isTrue(); + + System.out.println("✅ First-time user experience validated (community mode)"); + System.out.println(" - Client creation: OK"); + System.out.println(" - Health check: OK"); + System.out.println(" - Proxy LLM call: OK"); + } + } + + // ======================================================================== + // 7. AUTH HEADERS BASED ON CREDENTIALS + // ======================================================================== + @Nested + @DisplayName("7. Auth Headers Based on Credentials") + @WireMockTest + class AuthHeaderTests { + + @Test + @DisplayName("should send community Basic auth when no credentials configured") + void shouldSendCommunityAuthWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + "\"success\": true," + "\"data\": {\"answer\": \"test\"}" + "}"))); + + // No credentials - community mode (effective clientId = "community") + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + + client.proxyLLMCall(ClientRequest.builder().userToken("").query("Test query").build()); + + // Basic auth always sent with effective clientId ("community:") + String expectedAuth = + "Basic " + + java.util.Base64.getEncoder() + .encodeToString("community:".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + verify( + postRequestedFor(urlEqualTo("/api/request")) + .withoutHeader("X-License-Key") + .withHeader("Authorization", equalTo(expectedAuth))); + + System.out.println("✅ Community mode: Basic auth with default clientId"); + } + + @Test + @DisplayName("should send auth headers when credentials are configured") + void shouldSendAuthHeadersWithCredentials(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + "\"success\": true," + "\"data\": {\"answer\": \"test\"}" + "}"))); + + // With credentials - enterprise mode + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("test-client") + .clientSecret("test-secret") + .build()); + + client.proxyLLMCall(ClientRequest.builder().userToken("").query("Test query").build()); + + // Verify OAuth2 Basic auth header is sent when credentials are configured + String expectedBasic = + "Basic " + + java.util.Base64.getEncoder() + .encodeToString( + "test-client:test-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + verify( + postRequestedFor(urlEqualTo("/api/request")) + .withHeader("Authorization", equalTo(expectedBasic))); + + System.out.println("✅ Auth headers sent when credentials are configured"); + } + @Test + @DisplayName("should send OAuth2 Basic auth with clientId and clientSecret") + void shouldSendOAuth2BasicAuth(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + "\"success\": true," + "\"data\": {\"answer\": \"test\"}" + "}"))); + + // With OAuth2 credentials + AxonFlow client = + AxonFlow.create( + AxonFlowConfig.builder() + .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) + .clientId("my-client") + .clientSecret("my-secret") + .build()); + + client.proxyLLMCall(ClientRequest.builder().userToken("").query("Test query").build()); + + // Verify OAuth2 Basic auth header is sent + String expectedBasic = + "Basic " + + java.util.Base64.getEncoder() + .encodeToString( + "my-client:my-secret".getBytes(java.nio.charset.StandardCharsets.UTF_8)); + verify( + postRequestedFor(urlEqualTo("/api/request")) + .withHeader("Authorization", equalTo(expectedBasic))); + + System.out.println("✅ OAuth2 Basic auth header sent correctly"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java index 316a050..0e49447 100644 --- a/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java +++ b/src/test/java/com/getaxonflow/sdk/adapters/LangGraphAdapterTest.java @@ -15,6 +15,18 @@ */ package com.getaxonflow.sdk.adapters; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.getaxonflow.sdk.types.MCPCheckInputResponse; @@ -31,6 +43,11 @@ import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatus; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStatusResponse; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.WorkflowStepInfo; +import java.time.Instant; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeoutException; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; @@ -40,845 +57,905 @@ import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; -import java.time.Instant; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.TimeoutException; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.doThrow; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @DisplayName("LangGraphAdapter") @ExtendWith(MockitoExtension.class) class LangGraphAdapterTest { - @Mock - private AxonFlow client; + @Mock private AxonFlow client; + + private LangGraphAdapter adapter; + + @BeforeEach + void setUp() { + adapter = LangGraphAdapter.builder(client, "test-workflow").build(); + } + + // ======================================================================== + // Builder + // ======================================================================== + + @Nested + @DisplayName("Builder") + class BuilderTests { + + @Test + @DisplayName("should use default source and autoBlock") + void shouldUseDefaults() { + LangGraphAdapter a = LangGraphAdapter.builder(client, "my-wf").build(); + assertThat(a.getWorkflowId()).isNull(); + } + + @Test + @DisplayName("should accept custom source") + void shouldAcceptCustomSource() { + // Verify the adapter can be built with custom source without error + LangGraphAdapter a = + LangGraphAdapter.builder(client, "wf").source(WorkflowSource.LANGCHAIN).build(); + assertThat(a).isNotNull(); + } + + @Test + @DisplayName("should reject null client") + void shouldRejectNullClient() { + assertThatThrownBy(() -> LangGraphAdapter.builder(null, "wf")) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("client"); + } + + @Test + @DisplayName("should reject null workflowName") + void shouldRejectNullWorkflowName() { + assertThatThrownBy(() -> LangGraphAdapter.builder(client, null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("workflowName"); + } + } + + // ======================================================================== + // startWorkflow + // ======================================================================== + + @Nested + @DisplayName("startWorkflow") + class StartWorkflowTests { + + @Test + @DisplayName("should create workflow and store ID") + void shouldCreateWorkflowAndStoreId() { + mockCreateWorkflow("wf-123"); + + String id = adapter.startWorkflow(); + + assertThat(id).isEqualTo("wf-123"); + assertThat(adapter.getWorkflowId()).isEqualTo("wf-123"); + } + + @Test + @DisplayName("should pass metadata and traceId") + void shouldPassMetadataAndTraceId() { + mockCreateWorkflow("wf-456"); + + Map meta = Map.of("customer", "cust-1"); + adapter.startWorkflow(meta, "trace-abc"); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(CreateWorkflowRequest.class); + verify(client).createWorkflow(captor.capture()); + + CreateWorkflowRequest req = captor.getValue(); + assertThat(req.getWorkflowName()).isEqualTo("test-workflow"); + assertThat(req.getSource()).isEqualTo(WorkflowSource.LANGGRAPH); + assertThat(req.getMetadata()).containsEntry("customer", "cust-1"); + assertThat(req.getTraceId()).isEqualTo("trace-abc"); + } + } + + // ======================================================================== + // checkGate + // ======================================================================== + + @Nested + @DisplayName("checkGate") + class CheckGateTests { + + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-1"); + adapter.startWorkflow(); + } + + @Test + @DisplayName("should return true when allowed") + void shouldReturnTrueWhenAllowed() { + mockStepGate(GateDecision.ALLOW); + + boolean result = adapter.checkGate("generate", "llm_call"); + + assertThat(result).isTrue(); + } + + @Test + @DisplayName("should throw WorkflowBlockedError when blocked and autoBlock=true") + void shouldThrowOnBlockWithAutoBlock() { + mockStepGate(GateDecision.BLOCK, "step-x", "Policy violation", List.of("pol-1")); + + assertThatThrownBy(() -> adapter.checkGate("generate", "llm_call")) + .isInstanceOf(WorkflowBlockedError.class) + .hasMessageContaining("generate") + .hasMessageContaining("blocked"); + } + + @Test + @DisplayName("should return false when blocked and autoBlock=false") + void shouldReturnFalseOnBlockWithoutAutoBlock() { + LangGraphAdapter noBlock = LangGraphAdapter.builder(client, "wf").autoBlock(false).build(); + mockCreateWorkflow("wf-nb"); + noBlock.startWorkflow(); + + mockStepGate(GateDecision.BLOCK); + + boolean result = noBlock.checkGate("generate", "llm_call"); + assertThat(result).isFalse(); + } + + @Test + @DisplayName("should throw WorkflowApprovalRequiredError on require_approval") + void shouldThrowOnApprovalRequired() { + StepGateResponse resp = + new StepGateResponse( + GateDecision.REQUIRE_APPROVAL, + "step-approval", + "Needs review", + Collections.emptyList(), + "https://approve.me", + null, + null); + when(client.stepGate(anyString(), anyString(), any(StepGateRequest.class))).thenReturn(resp); + + assertThatThrownBy(() -> adapter.checkGate("deploy", "human_task")) + .isInstanceOf(WorkflowApprovalRequiredError.class) + .satisfies( + ex -> { + WorkflowApprovalRequiredError err = (WorkflowApprovalRequiredError) ex; + assertThat(err.getStepId()).isEqualTo("step-approval"); + assertThat(err.getApprovalUrl()).isEqualTo("https://approve.me"); + assertThat(err.getReason()).isEqualTo("Needs review"); + }); + } + + @Test + @DisplayName("should throw IllegalStateException when workflow not started") + void shouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + + assertThatThrownBy(() -> fresh.checkGate("step", "llm_call")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("not started"); + } + + @Test + @DisplayName("should pass model and provider from options") + void shouldPassModelAndProvider() { + mockStepGate(GateDecision.ALLOW); + + CheckGateOptions opts = + CheckGateOptions.builder() + .model("gpt-4") + .provider("openai") + .stepInput(Map.of("prompt", "hello")) + .build(); + + adapter.checkGate("generate", "llm_call", opts); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); + verify(client).stepGate(eq("wf-1"), anyString(), captor.capture()); + + StepGateRequest req = captor.getValue(); + assertThat(req.getModel()).isEqualTo("gpt-4"); + assertThat(req.getProvider()).isEqualTo("openai"); + assertThat(req.getStepInput()).containsEntry("prompt", "hello"); + } + + @Test + @DisplayName("should use custom stepId from options") + void shouldUseCustomStepId() { + mockStepGate(GateDecision.ALLOW); + + CheckGateOptions opts = CheckGateOptions.builder().stepId("my-custom-step").build(); + + adapter.checkGate("generate", "llm_call", opts); + + verify(client).stepGate(eq("wf-1"), eq("my-custom-step"), any(StepGateRequest.class)); + } + + @Test + @DisplayName("WorkflowBlockedError should contain policy details") + void blockedErrorShouldContainDetails() { + mockStepGate( + GateDecision.BLOCK, + "step-blocked-1", + "Cost limit exceeded", + List.of("cost-policy", "budget-policy")); + + assertThatThrownBy(() -> adapter.checkGate("expensive", "llm_call")) + .isInstanceOf(WorkflowBlockedError.class) + .satisfies( + ex -> { + WorkflowBlockedError err = (WorkflowBlockedError) ex; + assertThat(err.getStepId()).isEqualTo("step-blocked-1"); + assertThat(err.getReason()).isEqualTo("Cost limit exceeded"); + assertThat(err.getPolicyIds()).containsExactly("cost-policy", "budget-policy"); + }); + } + } + + // ======================================================================== + // stepCompleted + // ======================================================================== + + @Nested + @DisplayName("stepCompleted") + class StepCompletedTests { + + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-sc"); + adapter.startWorkflow(); + } + + @Test + @DisplayName("should call markStepCompleted with matching step ID") + void shouldCallMarkStepCompleted() { + mockStepGate(GateDecision.ALLOW); + adapter.checkGate("analyze", "llm_call"); + + adapter.stepCompleted("analyze"); + + // Step counter is 1 after first checkGate, so step ID should be step-1-analyze + verify(client) + .markStepCompleted( + eq("wf-sc"), eq("step-1-analyze"), any(MarkStepCompletedRequest.class)); + } + + @Test + @DisplayName("should pass output and metadata from options") + void shouldPassOptions() { + mockStepGate(GateDecision.ALLOW); + adapter.checkGate("generate", "llm_call"); + + StepCompletedOptions opts = + StepCompletedOptions.builder() + .output(Map.of("code", "result")) + .metadata(Map.of("key", "val")) + .tokensIn(100) + .tokensOut(200) + .costUsd(0.05) + .build(); + + adapter.stepCompleted("generate", opts); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(MarkStepCompletedRequest.class); + verify(client).markStepCompleted(eq("wf-sc"), anyString(), captor.capture()); + + MarkStepCompletedRequest req = captor.getValue(); + assertThat(req.getOutput()).containsEntry("code", "result"); + assertThat(req.getMetadata()).containsEntry("key", "val"); + assertThat(req.getTokensIn()).isEqualTo(100); + assertThat(req.getTokensOut()).isEqualTo(200); + assertThat(req.getCostUsd()).isEqualTo(0.05); + } + + @Test + @DisplayName("should throw IllegalStateException when workflow not started") + void shouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + + assertThatThrownBy(() -> fresh.stepCompleted("step")) + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("not started"); + } + + @Test + @DisplayName("should use custom stepId from options") + void shouldUseCustomStepId() { + mockStepGate(GateDecision.ALLOW); + adapter.checkGate("generate", "llm_call"); + + StepCompletedOptions opts = StepCompletedOptions.builder().stepId("custom-id").build(); - private LangGraphAdapter adapter; + adapter.stepCompleted("generate", opts); + + verify(client) + .markStepCompleted(eq("wf-sc"), eq("custom-id"), any(MarkStepCompletedRequest.class)); + } + } + + // ======================================================================== + // checkToolGate + // ======================================================================== + + @Nested + @DisplayName("checkToolGate") + class CheckToolGateTests { @BeforeEach - void setUp() { - adapter = LangGraphAdapter.builder(client, "test-workflow").build(); - } - - // ======================================================================== - // Builder - // ======================================================================== - - @Nested - @DisplayName("Builder") - class BuilderTests { - - @Test - @DisplayName("should use default source and autoBlock") - void shouldUseDefaults() { - LangGraphAdapter a = LangGraphAdapter.builder(client, "my-wf").build(); - assertThat(a.getWorkflowId()).isNull(); - } - - @Test - @DisplayName("should accept custom source") - void shouldAcceptCustomSource() { - // Verify the adapter can be built with custom source without error - LangGraphAdapter a = LangGraphAdapter.builder(client, "wf") - .source(WorkflowSource.LANGCHAIN) - .build(); - assertThat(a).isNotNull(); - } - - @Test - @DisplayName("should reject null client") - void shouldRejectNullClient() { - assertThatThrownBy(() -> LangGraphAdapter.builder(null, "wf")) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("client"); - } - - @Test - @DisplayName("should reject null workflowName") - void shouldRejectNullWorkflowName() { - assertThatThrownBy(() -> LangGraphAdapter.builder(client, null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("workflowName"); - } - } - - // ======================================================================== - // startWorkflow - // ======================================================================== - - @Nested - @DisplayName("startWorkflow") - class StartWorkflowTests { - - @Test - @DisplayName("should create workflow and store ID") - void shouldCreateWorkflowAndStoreId() { - mockCreateWorkflow("wf-123"); - - String id = adapter.startWorkflow(); - - assertThat(id).isEqualTo("wf-123"); - assertThat(adapter.getWorkflowId()).isEqualTo("wf-123"); - } - - @Test - @DisplayName("should pass metadata and traceId") - void shouldPassMetadataAndTraceId() { - mockCreateWorkflow("wf-456"); - - Map meta = Map.of("customer", "cust-1"); - adapter.startWorkflow(meta, "trace-abc"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(CreateWorkflowRequest.class); - verify(client).createWorkflow(captor.capture()); - - CreateWorkflowRequest req = captor.getValue(); - assertThat(req.getWorkflowName()).isEqualTo("test-workflow"); - assertThat(req.getSource()).isEqualTo(WorkflowSource.LANGGRAPH); - assertThat(req.getMetadata()).containsEntry("customer", "cust-1"); - assertThat(req.getTraceId()).isEqualTo("trace-abc"); - } - } - - // ======================================================================== - // checkGate - // ======================================================================== - - @Nested - @DisplayName("checkGate") - class CheckGateTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-1"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should return true when allowed") - void shouldReturnTrueWhenAllowed() { - mockStepGate(GateDecision.ALLOW); - - boolean result = adapter.checkGate("generate", "llm_call"); - - assertThat(result).isTrue(); - } - - @Test - @DisplayName("should throw WorkflowBlockedError when blocked and autoBlock=true") - void shouldThrowOnBlockWithAutoBlock() { - mockStepGate(GateDecision.BLOCK, "step-x", "Policy violation", List.of("pol-1")); - - assertThatThrownBy(() -> adapter.checkGate("generate", "llm_call")) - .isInstanceOf(WorkflowBlockedError.class) - .hasMessageContaining("generate") - .hasMessageContaining("blocked"); - } - - @Test - @DisplayName("should return false when blocked and autoBlock=false") - void shouldReturnFalseOnBlockWithoutAutoBlock() { - LangGraphAdapter noBlock = LangGraphAdapter.builder(client, "wf") - .autoBlock(false) - .build(); - mockCreateWorkflow("wf-nb"); - noBlock.startWorkflow(); - - mockStepGate(GateDecision.BLOCK); - - boolean result = noBlock.checkGate("generate", "llm_call"); - assertThat(result).isFalse(); - } - - @Test - @DisplayName("should throw WorkflowApprovalRequiredError on require_approval") - void shouldThrowOnApprovalRequired() { - StepGateResponse resp = new StepGateResponse( - GateDecision.REQUIRE_APPROVAL, "step-approval", "Needs review", - Collections.emptyList(), "https://approve.me", null, null); - when(client.stepGate(anyString(), anyString(), any(StepGateRequest.class))).thenReturn(resp); - - assertThatThrownBy(() -> adapter.checkGate("deploy", "human_task")) - .isInstanceOf(WorkflowApprovalRequiredError.class) - .satisfies(ex -> { - WorkflowApprovalRequiredError err = (WorkflowApprovalRequiredError) ex; - assertThat(err.getStepId()).isEqualTo("step-approval"); - assertThat(err.getApprovalUrl()).isEqualTo("https://approve.me"); - assertThat(err.getReason()).isEqualTo("Needs review"); - }); - } - - @Test - @DisplayName("should throw IllegalStateException when workflow not started") - void shouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - - assertThatThrownBy(() -> fresh.checkGate("step", "llm_call")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("not started"); - } - - @Test - @DisplayName("should pass model and provider from options") - void shouldPassModelAndProvider() { - mockStepGate(GateDecision.ALLOW); - - CheckGateOptions opts = CheckGateOptions.builder() - .model("gpt-4") - .provider("openai") - .stepInput(Map.of("prompt", "hello")) - .build(); - - adapter.checkGate("generate", "llm_call", opts); - - ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); - verify(client).stepGate(eq("wf-1"), anyString(), captor.capture()); - - StepGateRequest req = captor.getValue(); - assertThat(req.getModel()).isEqualTo("gpt-4"); - assertThat(req.getProvider()).isEqualTo("openai"); - assertThat(req.getStepInput()).containsEntry("prompt", "hello"); - } - - @Test - @DisplayName("should use custom stepId from options") - void shouldUseCustomStepId() { - mockStepGate(GateDecision.ALLOW); - - CheckGateOptions opts = CheckGateOptions.builder() - .stepId("my-custom-step") - .build(); - - adapter.checkGate("generate", "llm_call", opts); - - verify(client).stepGate(eq("wf-1"), eq("my-custom-step"), any(StepGateRequest.class)); - } - - @Test - @DisplayName("WorkflowBlockedError should contain policy details") - void blockedErrorShouldContainDetails() { - mockStepGate(GateDecision.BLOCK, "step-blocked-1", "Cost limit exceeded", List.of("cost-policy", "budget-policy")); - - assertThatThrownBy(() -> adapter.checkGate("expensive", "llm_call")) - .isInstanceOf(WorkflowBlockedError.class) - .satisfies(ex -> { - WorkflowBlockedError err = (WorkflowBlockedError) ex; - assertThat(err.getStepId()).isEqualTo("step-blocked-1"); - assertThat(err.getReason()).isEqualTo("Cost limit exceeded"); - assertThat(err.getPolicyIds()).containsExactly("cost-policy", "budget-policy"); - }); - } - } - - // ======================================================================== - // stepCompleted - // ======================================================================== - - @Nested - @DisplayName("stepCompleted") - class StepCompletedTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-sc"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should call markStepCompleted with matching step ID") - void shouldCallMarkStepCompleted() { - mockStepGate(GateDecision.ALLOW); - adapter.checkGate("analyze", "llm_call"); - - adapter.stepCompleted("analyze"); - - // Step counter is 1 after first checkGate, so step ID should be step-1-analyze - verify(client).markStepCompleted(eq("wf-sc"), eq("step-1-analyze"), any(MarkStepCompletedRequest.class)); - } - - @Test - @DisplayName("should pass output and metadata from options") - void shouldPassOptions() { - mockStepGate(GateDecision.ALLOW); - adapter.checkGate("generate", "llm_call"); - - StepCompletedOptions opts = StepCompletedOptions.builder() - .output(Map.of("code", "result")) - .metadata(Map.of("key", "val")) - .tokensIn(100) - .tokensOut(200) - .costUsd(0.05) - .build(); - - adapter.stepCompleted("generate", opts); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MarkStepCompletedRequest.class); - verify(client).markStepCompleted(eq("wf-sc"), anyString(), captor.capture()); - - MarkStepCompletedRequest req = captor.getValue(); - assertThat(req.getOutput()).containsEntry("code", "result"); - assertThat(req.getMetadata()).containsEntry("key", "val"); - assertThat(req.getTokensIn()).isEqualTo(100); - assertThat(req.getTokensOut()).isEqualTo(200); - assertThat(req.getCostUsd()).isEqualTo(0.05); - } - - @Test - @DisplayName("should throw IllegalStateException when workflow not started") - void shouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - - assertThatThrownBy(() -> fresh.stepCompleted("step")) - .isInstanceOf(IllegalStateException.class) - .hasMessageContaining("not started"); - } - - @Test - @DisplayName("should use custom stepId from options") - void shouldUseCustomStepId() { - mockStepGate(GateDecision.ALLOW); - adapter.checkGate("generate", "llm_call"); - - StepCompletedOptions opts = StepCompletedOptions.builder() - .stepId("custom-id") - .build(); - - adapter.stepCompleted("generate", opts); - - verify(client).markStepCompleted(eq("wf-sc"), eq("custom-id"), any(MarkStepCompletedRequest.class)); - } - } - - // ======================================================================== - // checkToolGate - // ======================================================================== - - @Nested - @DisplayName("checkToolGate") - class CheckToolGateTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-tg"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should use default step name tools/{toolName}") - void shouldUseDefaultStepName() { - mockStepGate(GateDecision.ALLOW); - - adapter.checkToolGate("web_search", "function"); - - ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); - verify(client).stepGate(eq("wf-tg"), anyString(), captor.capture()); - - StepGateRequest req = captor.getValue(); - assertThat(req.getStepName()).isEqualTo("tools/web_search"); - assertThat(req.getStepType()).isEqualTo(StepType.TOOL_CALL); - assertThat(req.getToolContext()).isNotNull(); - assertThat(req.getToolContext().getToolName()).isEqualTo("web_search"); - assertThat(req.getToolContext().getToolType()).isEqualTo("function"); - } - - @Test - @DisplayName("should use custom step name from options") - void shouldUseCustomStepName() { - mockStepGate(GateDecision.ALLOW); - - CheckToolGateOptions opts = CheckToolGateOptions.builder() - .stepName("custom-tools/search") - .toolInput(Map.of("query", "test")) - .build(); - - adapter.checkToolGate("web_search", "function", opts); - - ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); - verify(client).stepGate(eq("wf-tg"), anyString(), captor.capture()); - - StepGateRequest req = captor.getValue(); - assertThat(req.getStepName()).isEqualTo("custom-tools/search"); - assertThat(req.getToolContext().getToolInput()).containsEntry("query", "test"); - } - } - - // ======================================================================== - // toolCompleted - // ======================================================================== - - @Nested - @DisplayName("toolCompleted") - class ToolCompletedTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-tc"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should delegate to stepCompleted with default step name") - void shouldDelegateWithDefaultName() { - mockStepGate(GateDecision.ALLOW); - adapter.checkToolGate("calculator", "function"); - - adapter.toolCompleted("calculator"); - - // Step ID should match the one generated in checkToolGate (tools/calculator -> tools-calculator) - verify(client).markStepCompleted(eq("wf-tc"), eq("step-1-tools-calculator"), any(MarkStepCompletedRequest.class)); - } - - @Test - @DisplayName("should pass options through to stepCompleted") - void shouldPassOptions() { - mockStepGate(GateDecision.ALLOW); - adapter.checkToolGate("calc", "function"); - - ToolCompletedOptions opts = ToolCompletedOptions.builder() - .output(Map.of("result", 42)) - .tokensIn(10) - .tokensOut(20) - .costUsd(0.001) - .build(); - - adapter.toolCompleted("calc", opts); - - ArgumentCaptor captor = ArgumentCaptor.forClass(MarkStepCompletedRequest.class); - verify(client).markStepCompleted(eq("wf-tc"), anyString(), captor.capture()); - - MarkStepCompletedRequest req = captor.getValue(); - assertThat(req.getTokensIn()).isEqualTo(10); - assertThat(req.getTokensOut()).isEqualTo(20); - assertThat(req.getCostUsd()).isEqualTo(0.001); - } - } - - // ======================================================================== - // Workflow Lifecycle (complete/abort/fail) - // ======================================================================== - - @Nested - @DisplayName("Workflow Lifecycle") - class LifecycleTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-lc"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("completeWorkflow should delegate to client") - void completeWorkflowShouldDelegate() { - adapter.completeWorkflow(); - verify(client).completeWorkflow("wf-lc"); - } - - @Test - @DisplayName("abortWorkflow should delegate with reason") - void abortWorkflowShouldDelegateWithReason() { - adapter.abortWorkflow("user cancelled"); - verify(client).abortWorkflow("wf-lc", "user cancelled"); - } - - @Test - @DisplayName("failWorkflow should delegate with reason") - void failWorkflowShouldDelegateWithReason() { - adapter.failWorkflow("pipeline error"); - verify(client).failWorkflow("wf-lc", "pipeline error"); - } - - @Test - @DisplayName("completeWorkflow should throw when not started") - void completeShouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - assertThatThrownBy(fresh::completeWorkflow) - .isInstanceOf(IllegalStateException.class); - } - - @Test - @DisplayName("abortWorkflow should throw when not started") - void abortShouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - assertThatThrownBy(() -> fresh.abortWorkflow("reason")) - .isInstanceOf(IllegalStateException.class); - } - - @Test - @DisplayName("failWorkflow should throw when not started") - void failShouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - assertThatThrownBy(() -> fresh.failWorkflow("reason")) - .isInstanceOf(IllegalStateException.class); - } - } - - // ======================================================================== - // Step Counter - // ======================================================================== - - @Nested - @DisplayName("Step Counter") - class StepCounterTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-cnt"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should increment step counter on each checkGate") - void shouldIncrementCounter() { - mockStepGate(GateDecision.ALLOW); - - adapter.checkGate("step-a", "llm_call"); - assertThat(adapter.getStepCounter()).isEqualTo(1); - - adapter.checkGate("step-b", "tool_call"); - assertThat(adapter.getStepCounter()).isEqualTo(2); - - adapter.checkGate("step-c", "llm_call"); - assertThat(adapter.getStepCounter()).isEqualTo(3); - } - - @Test - @DisplayName("should generate step IDs with counter and safe name") - void shouldGenerateStepIds() { - mockStepGate(GateDecision.ALLOW); - - adapter.checkGate("My Step", "llm_call"); - verify(client).stepGate(eq("wf-cnt"), eq("step-1-my-step"), any(StepGateRequest.class)); - - adapter.checkGate("tools/search", "tool_call"); - verify(client).stepGate(eq("wf-cnt"), eq("step-2-tools-search"), any(StepGateRequest.class)); - } - } - - // ======================================================================== - // waitForApproval - // ======================================================================== - - @Nested - @DisplayName("waitForApproval") - class WaitForApprovalTests { - - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-wa"); - adapter.startWorkflow(); - } - - @Test - @DisplayName("should return true when step is approved") - void shouldReturnTrueOnApproval() throws Exception { - WorkflowStepInfo approvedStep = new WorkflowStepInfo( - "step-1", 1, "deploy", StepType.HUMAN_TASK, - GateDecision.REQUIRE_APPROVAL, null, ApprovalStatus.APPROVED, - "admin", Instant.now(), null); - - WorkflowStatusResponse status = new WorkflowStatusResponse( - "wf-wa", "wf", WorkflowSource.LANGGRAPH, WorkflowStatus.IN_PROGRESS, - 1, null, Instant.now(), null, List.of(approvedStep)); - - when(client.getWorkflow("wf-wa")).thenReturn(status); - - boolean result = adapter.waitForApproval("step-1", 50, 5000); - assertThat(result).isTrue(); - } - - @Test - @DisplayName("should return false when step is rejected") - void shouldReturnFalseOnRejection() throws Exception { - WorkflowStepInfo rejectedStep = new WorkflowStepInfo( - "step-1", 1, "deploy", StepType.HUMAN_TASK, - GateDecision.REQUIRE_APPROVAL, null, ApprovalStatus.REJECTED, - null, Instant.now(), null); - - WorkflowStatusResponse status = new WorkflowStatusResponse( - "wf-wa", "wf", WorkflowSource.LANGGRAPH, WorkflowStatus.IN_PROGRESS, - 1, null, Instant.now(), null, List.of(rejectedStep)); - - when(client.getWorkflow("wf-wa")).thenReturn(status); - - boolean result = adapter.waitForApproval("step-1", 50, 5000); - assertThat(result).isFalse(); - } - - @Test - @DisplayName("should throw TimeoutException when approval not received") - void shouldThrowOnTimeout() { - WorkflowStepInfo pendingStep = new WorkflowStepInfo( - "step-1", 1, "deploy", StepType.HUMAN_TASK, - GateDecision.REQUIRE_APPROVAL, null, ApprovalStatus.PENDING, - null, Instant.now(), null); - - WorkflowStatusResponse status = new WorkflowStatusResponse( - "wf-wa", "wf", WorkflowSource.LANGGRAPH, WorkflowStatus.IN_PROGRESS, - 1, null, Instant.now(), null, List.of(pendingStep)); - - when(client.getWorkflow("wf-wa")).thenReturn(status); - - assertThatThrownBy(() -> adapter.waitForApproval("step-1", 50, 120)) - .isInstanceOf(TimeoutException.class) - .hasMessageContaining("timeout"); - } - - @Test - @DisplayName("should throw IllegalStateException when not started") - void shouldThrowWhenNotStarted() { - LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); - assertThatThrownBy(() -> fresh.waitForApproval("step-1", 50, 1000)) - .isInstanceOf(IllegalStateException.class); - } - } - - // ======================================================================== - // close() - // ======================================================================== - - @Nested - @DisplayName("close") - class CloseTests { - - @Test - @DisplayName("should abort workflow if not closed normally") - void shouldAbortIfNotClosedNormally() { - mockCreateWorkflow("wf-close"); - adapter.startWorkflow(); - - adapter.close(); - - verify(client).abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); - } - - @Test - @DisplayName("should not abort if completeWorkflow was called") - void shouldNotAbortIfCompleted() { - mockCreateWorkflow("wf-close"); - adapter.startWorkflow(); - adapter.completeWorkflow(); - - adapter.close(); - - verify(client, never()).abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); - } - - @Test - @DisplayName("should not abort if abortWorkflow was called") - void shouldNotAbortIfAborted() { - mockCreateWorkflow("wf-close"); - adapter.startWorkflow(); - adapter.abortWorkflow("manual"); - - adapter.close(); - - // abortWorkflow was called once (manual), not twice - verify(client, times(1)).abortWorkflow(anyString(), anyString()); - } - - @Test - @DisplayName("should not abort if failWorkflow was called") - void shouldNotAbortIfFailed() { - mockCreateWorkflow("wf-close"); - adapter.startWorkflow(); - adapter.failWorkflow("error"); - - adapter.close(); - - verify(client, never()).abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); - } - - @Test - @DisplayName("should do nothing if workflow was never started") - void shouldDoNothingIfNotStarted() { - adapter.close(); - verify(client, never()).abortWorkflow(anyString(), anyString()); - } - - @Test - @DisplayName("should swallow exceptions during close abort") - void shouldSwallowExceptionsDuringCloseAbort() { - mockCreateWorkflow("wf-close-err"); - adapter.startWorkflow(); - - doThrow(new RuntimeException("network error")) - .when(client).abortWorkflow(anyString(), anyString()); + void startWorkflow() { + mockCreateWorkflow("wf-tg"); + adapter.startWorkflow(); + } + + @Test + @DisplayName("should use default step name tools/{toolName}") + void shouldUseDefaultStepName() { + mockStepGate(GateDecision.ALLOW); - // Should not throw - adapter.close(); - } + adapter.checkToolGate("web_search", "function"); + + ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); + verify(client).stepGate(eq("wf-tg"), anyString(), captor.capture()); + + StepGateRequest req = captor.getValue(); + assertThat(req.getStepName()).isEqualTo("tools/web_search"); + assertThat(req.getStepType()).isEqualTo(StepType.TOOL_CALL); + assertThat(req.getToolContext()).isNotNull(); + assertThat(req.getToolContext().getToolName()).isEqualTo("web_search"); + assertThat(req.getToolContext().getToolType()).isEqualTo("function"); } - // ======================================================================== - // MCPToolInterceptor - // ======================================================================== + @Test + @DisplayName("should use custom step name from options") + void shouldUseCustomStepName() { + mockStepGate(GateDecision.ALLOW); - @Nested - @DisplayName("MCPToolInterceptor") - class MCPToolInterceptorTests { + CheckToolGateOptions opts = + CheckToolGateOptions.builder() + .stepName("custom-tools/search") + .toolInput(Map.of("query", "test")) + .build(); - @BeforeEach - void startWorkflow() { - mockCreateWorkflow("wf-mcp"); - adapter.startWorkflow(); - } + adapter.checkToolGate("web_search", "function", opts); - @Test - @DisplayName("should pass through when input and output are allowed") - void shouldPassThroughWhenAllowed() throws Exception { - MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); - MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + ArgumentCaptor captor = ArgumentCaptor.forClass(StepGateRequest.class); + verify(client).stepGate(eq("wf-tg"), anyString(), captor.capture()); - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); - when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + StepGateRequest req = captor.getValue(); + assertThat(req.getStepName()).isEqualTo("custom-tools/search"); + assertThat(req.getToolContext().getToolInput()).containsEntry("query", "test"); + } + } - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("postgres", "query", Map.of("sql", "SELECT 1")); + // ======================================================================== + // toolCompleted + // ======================================================================== - Object result = interceptor.intercept(request, req -> "result-data"); + @Nested + @DisplayName("toolCompleted") + class ToolCompletedTests { - assertThat(result).isEqualTo("result-data"); - } + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-tc"); + adapter.startWorkflow(); + } - @Test - @DisplayName("should throw PolicyViolationException when input is blocked") - void shouldThrowOnBlockedInput() { - MCPCheckInputResponse blocked = new MCPCheckInputResponse(false, "DROP not allowed", 1, null); - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(blocked); + @Test + @DisplayName("should delegate to stepCompleted with default step name") + void shouldDelegateWithDefaultName() { + mockStepGate(GateDecision.ALLOW); + adapter.checkToolGate("calculator", "function"); - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("postgres", "execute", Map.of("sql", "DROP TABLE")); + adapter.toolCompleted("calculator"); - assertThatThrownBy(() -> interceptor.intercept(request, req -> "ignored")) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("DROP not allowed"); - } + // Step ID should match the one generated in checkToolGate (tools/calculator -> + // tools-calculator) + verify(client) + .markStepCompleted( + eq("wf-tc"), eq("step-1-tools-calculator"), any(MarkStepCompletedRequest.class)); + } - @Test - @DisplayName("should throw PolicyViolationException when output is blocked") - void shouldThrowOnBlockedOutput() throws Exception { - MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); - MCPCheckOutputResponse outputBlocked = new MCPCheckOutputResponse(false, "PII detected", null, 1, null, null); + @Test + @DisplayName("should pass options through to stepCompleted") + void shouldPassOptions() { + mockStepGate(GateDecision.ALLOW); + adapter.checkToolGate("calc", "function"); + + ToolCompletedOptions opts = + ToolCompletedOptions.builder() + .output(Map.of("result", 42)) + .tokensIn(10) + .tokensOut(20) + .costUsd(0.001) + .build(); + + adapter.toolCompleted("calc", opts); + + ArgumentCaptor captor = + ArgumentCaptor.forClass(MarkStepCompletedRequest.class); + verify(client).markStepCompleted(eq("wf-tc"), anyString(), captor.capture()); + + MarkStepCompletedRequest req = captor.getValue(); + assertThat(req.getTokensIn()).isEqualTo(10); + assertThat(req.getTokensOut()).isEqualTo(20); + assertThat(req.getCostUsd()).isEqualTo(0.001); + } + } - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); - when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputBlocked); + // ======================================================================== + // Workflow Lifecycle (complete/abort/fail) + // ======================================================================== - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("postgres", "query", null); + @Nested + @DisplayName("Workflow Lifecycle") + class LifecycleTests { - assertThatThrownBy(() -> interceptor.intercept(request, req -> "sensitive-data")) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("PII detected"); - } + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-lc"); + adapter.startWorkflow(); + } - @Test - @DisplayName("should return redacted data when available") - void shouldReturnRedactedData() throws Exception { - MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); - MCPCheckOutputResponse redacted = new MCPCheckOutputResponse(true, null, "***REDACTED***", 1, null, null); + @Test + @DisplayName("completeWorkflow should delegate to client") + void completeWorkflowShouldDelegate() { + adapter.completeWorkflow(); + verify(client).completeWorkflow("wf-lc"); + } - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); - when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(redacted); + @Test + @DisplayName("abortWorkflow should delegate with reason") + void abortWorkflowShouldDelegateWithReason() { + adapter.abortWorkflow("user cancelled"); + verify(client).abortWorkflow("wf-lc", "user cancelled"); + } - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("db", "query", Map.of("q", "SELECT *")); + @Test + @DisplayName("failWorkflow should delegate with reason") + void failWorkflowShouldDelegateWithReason() { + adapter.failWorkflow("pipeline error"); + verify(client).failWorkflow("wf-lc", "pipeline error"); + } - Object result = interceptor.intercept(request, req -> "raw-data-with-pii"); + @Test + @DisplayName("completeWorkflow should throw when not started") + void completeShouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + assertThatThrownBy(fresh::completeWorkflow).isInstanceOf(IllegalStateException.class); + } - assertThat(result).isEqualTo("***REDACTED***"); - } + @Test + @DisplayName("abortWorkflow should throw when not started") + void abortShouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + assertThatThrownBy(() -> fresh.abortWorkflow("reason")) + .isInstanceOf(IllegalStateException.class); + } - @Test - @DisplayName("should use default connector type serverName.toolName") - void shouldUseDefaultConnectorType() throws Exception { - MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); - MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + @Test + @DisplayName("failWorkflow should throw when not started") + void failShouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + assertThatThrownBy(() -> fresh.failWorkflow("reason")) + .isInstanceOf(IllegalStateException.class); + } + } - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); - when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + // ======================================================================== + // Step Counter + // ======================================================================== - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("myserver", "mytool", Collections.emptyMap()); + @Nested + @DisplayName("Step Counter") + class StepCounterTests { - interceptor.intercept(request, req -> "ok"); + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-cnt"); + adapter.startWorkflow(); + } - verify(client).mcpCheckInput(eq("myserver.mytool"), anyString(), any()); - verify(client).mcpCheckOutput(eq("myserver.mytool"), isNull(), any()); - } + @Test + @DisplayName("should increment step counter on each checkGate") + void shouldIncrementCounter() { + mockStepGate(GateDecision.ALLOW); - @Test - @DisplayName("should use custom connector type function") - void shouldUseCustomConnectorTypeFn() throws Exception { - MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); - MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + adapter.checkGate("step-a", "llm_call"); + assertThat(adapter.getStepCounter()).isEqualTo(1); - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); - when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + adapter.checkGate("step-b", "tool_call"); + assertThat(adapter.getStepCounter()).isEqualTo(2); - MCPInterceptorOptions opts = MCPInterceptorOptions.builder() - .connectorTypeFn(req -> "custom-" + req.getServerName()) - .operation("query") - .build(); + adapter.checkGate("step-c", "llm_call"); + assertThat(adapter.getStepCounter()).isEqualTo(3); + } - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(opts); - MCPToolRequest request = new MCPToolRequest("pg", "read", null); + @Test + @DisplayName("should generate step IDs with counter and safe name") + void shouldGenerateStepIds() { + mockStepGate(GateDecision.ALLOW); - interceptor.intercept(request, req -> "data"); + adapter.checkGate("My Step", "llm_call"); + verify(client).stepGate(eq("wf-cnt"), eq("step-1-my-step"), any(StepGateRequest.class)); - ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); - verify(client).mcpCheckInput(eq("custom-pg"), anyString(), inputOptsCaptor.capture()); - assertThat(inputOptsCaptor.getValue()).containsEntry("operation", "query"); - } + adapter.checkGate("tools/search", "tool_call"); + verify(client).stepGate(eq("wf-cnt"), eq("step-2-tools-search"), any(StepGateRequest.class)); + } + } - @Test - @DisplayName("should not call handler when input is blocked") - void shouldNotCallHandlerWhenInputBlocked() { - MCPCheckInputResponse blocked = new MCPCheckInputResponse(false, "Blocked", 1, null); - when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(blocked); + // ======================================================================== + // waitForApproval + // ======================================================================== - MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); - MCPToolRequest request = new MCPToolRequest("srv", "tool", null); + @Nested + @DisplayName("waitForApproval") + class WaitForApprovalTests { - MCPToolHandler handler = req -> { - throw new AssertionError("Handler should not be called"); - }; + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-wa"); + adapter.startWorkflow(); + } - assertThatThrownBy(() -> interceptor.intercept(request, handler)) - .isInstanceOf(PolicyViolationException.class); - } + @Test + @DisplayName("should return true when step is approved") + void shouldReturnTrueOnApproval() throws Exception { + WorkflowStepInfo approvedStep = + new WorkflowStepInfo( + "step-1", + 1, + "deploy", + StepType.HUMAN_TASK, + GateDecision.REQUIRE_APPROVAL, + null, + ApprovalStatus.APPROVED, + "admin", + Instant.now(), + null); + + WorkflowStatusResponse status = + new WorkflowStatusResponse( + "wf-wa", + "wf", + WorkflowSource.LANGGRAPH, + WorkflowStatus.IN_PROGRESS, + 1, + null, + Instant.now(), + null, + List.of(approvedStep)); + + when(client.getWorkflow("wf-wa")).thenReturn(status); + + boolean result = adapter.waitForApproval("step-1", 50, 5000); + assertThat(result).isTrue(); } - // ======================================================================== - // Helpers - // ======================================================================== + @Test + @DisplayName("should return false when step is rejected") + void shouldReturnFalseOnRejection() throws Exception { + WorkflowStepInfo rejectedStep = + new WorkflowStepInfo( + "step-1", + 1, + "deploy", + StepType.HUMAN_TASK, + GateDecision.REQUIRE_APPROVAL, + null, + ApprovalStatus.REJECTED, + null, + Instant.now(), + null); + + WorkflowStatusResponse status = + new WorkflowStatusResponse( + "wf-wa", + "wf", + WorkflowSource.LANGGRAPH, + WorkflowStatus.IN_PROGRESS, + 1, + null, + Instant.now(), + null, + List.of(rejectedStep)); + + when(client.getWorkflow("wf-wa")).thenReturn(status); + + boolean result = adapter.waitForApproval("step-1", 50, 5000); + assertThat(result).isFalse(); + } - private void mockCreateWorkflow(String workflowId) { - CreateWorkflowResponse resp = new CreateWorkflowResponse( - workflowId, "test-workflow", WorkflowSource.LANGGRAPH, - WorkflowStatus.IN_PROGRESS, Instant.now()); - when(client.createWorkflow(any(CreateWorkflowRequest.class))).thenReturn(resp); + @Test + @DisplayName("should throw TimeoutException when approval not received") + void shouldThrowOnTimeout() { + WorkflowStepInfo pendingStep = + new WorkflowStepInfo( + "step-1", + 1, + "deploy", + StepType.HUMAN_TASK, + GateDecision.REQUIRE_APPROVAL, + null, + ApprovalStatus.PENDING, + null, + Instant.now(), + null); + + WorkflowStatusResponse status = + new WorkflowStatusResponse( + "wf-wa", + "wf", + WorkflowSource.LANGGRAPH, + WorkflowStatus.IN_PROGRESS, + 1, + null, + Instant.now(), + null, + List.of(pendingStep)); + + when(client.getWorkflow("wf-wa")).thenReturn(status); + + assertThatThrownBy(() -> adapter.waitForApproval("step-1", 50, 120)) + .isInstanceOf(TimeoutException.class) + .hasMessageContaining("timeout"); } - private void mockStepGate(GateDecision decision) { - mockStepGate(decision, null, null, Collections.emptyList()); + @Test + @DisplayName("should throw IllegalStateException when not started") + void shouldThrowWhenNotStarted() { + LangGraphAdapter fresh = LangGraphAdapter.builder(client, "wf").build(); + assertThatThrownBy(() -> fresh.waitForApproval("step-1", 50, 1000)) + .isInstanceOf(IllegalStateException.class); } + } + + // ======================================================================== + // close() + // ======================================================================== + + @Nested + @DisplayName("close") + class CloseTests { + + @Test + @DisplayName("should abort workflow if not closed normally") + void shouldAbortIfNotClosedNormally() { + mockCreateWorkflow("wf-close"); + adapter.startWorkflow(); + + adapter.close(); + + verify(client) + .abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); + } + + @Test + @DisplayName("should not abort if completeWorkflow was called") + void shouldNotAbortIfCompleted() { + mockCreateWorkflow("wf-close"); + adapter.startWorkflow(); + adapter.completeWorkflow(); + + adapter.close(); + + verify(client, never()) + .abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); + } + + @Test + @DisplayName("should not abort if abortWorkflow was called") + void shouldNotAbortIfAborted() { + mockCreateWorkflow("wf-close"); + adapter.startWorkflow(); + adapter.abortWorkflow("manual"); + + adapter.close(); + + // abortWorkflow was called once (manual), not twice + verify(client, times(1)).abortWorkflow(anyString(), anyString()); + } + + @Test + @DisplayName("should not abort if failWorkflow was called") + void shouldNotAbortIfFailed() { + mockCreateWorkflow("wf-close"); + adapter.startWorkflow(); + adapter.failWorkflow("error"); + + adapter.close(); + + verify(client, never()) + .abortWorkflow(eq("wf-close"), eq("Adapter closed without explicit completion")); + } + + @Test + @DisplayName("should do nothing if workflow was never started") + void shouldDoNothingIfNotStarted() { + adapter.close(); + verify(client, never()).abortWorkflow(anyString(), anyString()); + } + + @Test + @DisplayName("should swallow exceptions during close abort") + void shouldSwallowExceptionsDuringCloseAbort() { + mockCreateWorkflow("wf-close-err"); + adapter.startWorkflow(); + + doThrow(new RuntimeException("network error")) + .when(client) + .abortWorkflow(anyString(), anyString()); + + // Should not throw + adapter.close(); + } + } + + // ======================================================================== + // MCPToolInterceptor + // ======================================================================== + + @Nested + @DisplayName("MCPToolInterceptor") + class MCPToolInterceptorTests { + + @BeforeEach + void startWorkflow() { + mockCreateWorkflow("wf-mcp"); + adapter.startWorkflow(); + } + + @Test + @DisplayName("should pass through when input and output are allowed") + void shouldPassThroughWhenAllowed() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("postgres", "query", Map.of("sql", "SELECT 1")); + + Object result = interceptor.intercept(request, req -> "result-data"); + + assertThat(result).isEqualTo("result-data"); + } + + @Test + @DisplayName("should throw PolicyViolationException when input is blocked") + void shouldThrowOnBlockedInput() { + MCPCheckInputResponse blocked = new MCPCheckInputResponse(false, "DROP not allowed", 1, null); + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(blocked); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = + new MCPToolRequest("postgres", "execute", Map.of("sql", "DROP TABLE")); + + assertThatThrownBy(() -> interceptor.intercept(request, req -> "ignored")) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("DROP not allowed"); + } + + @Test + @DisplayName("should throw PolicyViolationException when output is blocked") + void shouldThrowOnBlockedOutput() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputBlocked = + new MCPCheckOutputResponse(false, "PII detected", null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputBlocked); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("postgres", "query", null); + + assertThatThrownBy(() -> interceptor.intercept(request, req -> "sensitive-data")) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("PII detected"); + } + + @Test + @DisplayName("should return redacted data when available") + void shouldReturnRedactedData() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse redacted = + new MCPCheckOutputResponse(true, null, "***REDACTED***", 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(redacted); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("db", "query", Map.of("q", "SELECT *")); + + Object result = interceptor.intercept(request, req -> "raw-data-with-pii"); + + assertThat(result).isEqualTo("***REDACTED***"); + } + + @Test + @DisplayName("should use default connector type serverName.toolName") + void shouldUseDefaultConnectorType() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("myserver", "mytool", Collections.emptyMap()); + + interceptor.intercept(request, req -> "ok"); + + verify(client).mcpCheckInput(eq("myserver.mytool"), anyString(), any()); + verify(client).mcpCheckOutput(eq("myserver.mytool"), isNull(), any()); + } + + @Test + @DisplayName("should use custom connector type function") + void shouldUseCustomConnectorTypeFn() throws Exception { + MCPCheckInputResponse inputOk = new MCPCheckInputResponse(true, null, 1, null); + MCPCheckOutputResponse outputOk = new MCPCheckOutputResponse(true, null, null, 1, null, null); + + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(inputOk); + when(client.mcpCheckOutput(anyString(), isNull(), any())).thenReturn(outputOk); + + MCPInterceptorOptions opts = + MCPInterceptorOptions.builder() + .connectorTypeFn(req -> "custom-" + req.getServerName()) + .operation("query") + .build(); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(opts); + MCPToolRequest request = new MCPToolRequest("pg", "read", null); + + interceptor.intercept(request, req -> "data"); + + ArgumentCaptor> inputOptsCaptor = ArgumentCaptor.forClass(Map.class); + verify(client).mcpCheckInput(eq("custom-pg"), anyString(), inputOptsCaptor.capture()); + assertThat(inputOptsCaptor.getValue()).containsEntry("operation", "query"); + } + + @Test + @DisplayName("should not call handler when input is blocked") + void shouldNotCallHandlerWhenInputBlocked() { + MCPCheckInputResponse blocked = new MCPCheckInputResponse(false, "Blocked", 1, null); + when(client.mcpCheckInput(anyString(), anyString(), any())).thenReturn(blocked); + + MCPToolInterceptor interceptor = adapter.mcpToolInterceptor(); + MCPToolRequest request = new MCPToolRequest("srv", "tool", null); + + MCPToolHandler handler = + req -> { + throw new AssertionError("Handler should not be called"); + }; - private void mockStepGate(GateDecision decision, String stepId, String reason, List policyIds) { - StepGateResponse resp = new StepGateResponse( - decision, stepId, reason, policyIds, null, null, null); - when(client.stepGate(anyString(), anyString(), any(StepGateRequest.class))).thenReturn(resp); + assertThatThrownBy(() -> interceptor.intercept(request, handler)) + .isInstanceOf(PolicyViolationException.class); } + } + + // ======================================================================== + // Helpers + // ======================================================================== + + private void mockCreateWorkflow(String workflowId) { + CreateWorkflowResponse resp = + new CreateWorkflowResponse( + workflowId, + "test-workflow", + WorkflowSource.LANGGRAPH, + WorkflowStatus.IN_PROGRESS, + Instant.now()); + when(client.createWorkflow(any(CreateWorkflowRequest.class))).thenReturn(resp); + } + + private void mockStepGate(GateDecision decision) { + mockStepGate(decision, null, null, Collections.emptyList()); + } + + private void mockStepGate( + GateDecision decision, String stepId, String reason, List policyIds) { + StepGateResponse resp = + new StepGateResponse(decision, stepId, reason, policyIds, null, null, null); + when(client.stepGate(anyString(), anyString(), any(StepGateRequest.class))).thenReturn(resp); + } } diff --git a/src/test/java/com/getaxonflow/sdk/exceptions/ExceptionsTest.java b/src/test/java/com/getaxonflow/sdk/exceptions/ExceptionsTest.java index 05d9c2c..d8e0374 100644 --- a/src/test/java/com/getaxonflow/sdk/exceptions/ExceptionsTest.java +++ b/src/test/java/com/getaxonflow/sdk/exceptions/ExceptionsTest.java @@ -15,174 +15,164 @@ */ package com.getaxonflow.sdk.exceptions; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.time.Instant; import java.util.List; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("Exception Classes") class ExceptionsTest { - @Test - @DisplayName("AxonFlowException - should create with message") - void axonFlowExceptionWithMessage() { - AxonFlowException ex = new AxonFlowException("Test error"); - - assertThat(ex.getMessage()).isEqualTo("Test error"); - assertThat(ex.getStatusCode()).isEqualTo(0); - assertThat(ex.getErrorCode()).isNull(); - } - - @Test - @DisplayName("AxonFlowException - should create with full details") - void axonFlowExceptionWithFullDetails() { - AxonFlowException ex = new AxonFlowException("Test error", 500, "INTERNAL_ERROR"); - - assertThat(ex.getMessage()).isEqualTo("Test error"); - assertThat(ex.getStatusCode()).isEqualTo(500); - assertThat(ex.getErrorCode()).isEqualTo("INTERNAL_ERROR"); - } - - @Test - @DisplayName("AxonFlowException - toString should include details") - void axonFlowExceptionToString() { - AxonFlowException ex = new AxonFlowException("Test error", 500, "INTERNAL_ERROR"); - - String str = ex.toString(); - assertThat(str).contains("Test error"); - assertThat(str).contains("500"); - assertThat(str).contains("INTERNAL_ERROR"); - } - - @Test - @DisplayName("AuthenticationException - should set correct status code") - void authenticationException() { - AuthenticationException ex = new AuthenticationException("Invalid credentials"); - - assertThat(ex.getMessage()).isEqualTo("Invalid credentials"); - assertThat(ex.getStatusCode()).isEqualTo(401); - assertThat(ex.getErrorCode()).isEqualTo("AUTHENTICATION_FAILED"); - } - - @Test - @DisplayName("PolicyViolationException - should extract policy name") - void policyViolationExceptionExtractsPolicyName() { - PolicyViolationException ex = new PolicyViolationException( - "Request blocked by policy: sql_injection_detection"); - - assertThat(ex.getPolicyName()).isEqualTo("sql_injection_detection"); - assertThat(ex.getBlockReason()).isEqualTo("Request blocked by policy: sql_injection_detection"); - assertThat(ex.getStatusCode()).isEqualTo(403); - } - - @Test - @DisplayName("PolicyViolationException - should accept explicit policy name") - void policyViolationExceptionWithExplicitPolicy() { - PolicyViolationException ex = new PolicyViolationException( - "PII detected in request", - "pii_detection", - List.of("pii_detection", "rate_limit") - ); - - assertThat(ex.getPolicyName()).isEqualTo("pii_detection"); - assertThat(ex.getPoliciesEvaluated()).containsExactly("pii_detection", "rate_limit"); - } - - @Test - @DisplayName("PolicyViolationException - should handle bracket format") - void policyViolationExceptionBracketFormat() { - PolicyViolationException ex = new PolicyViolationException("[rate_limit] Too many requests"); - - assertThat(ex.getPolicyName()).isEqualTo("rate_limit"); - } - - @Test - @DisplayName("RateLimitException - should calculate retry duration") - void rateLimitExceptionRetryDuration() { - Instant resetTime = Instant.now().plusSeconds(60); - RateLimitException ex = new RateLimitException("Rate limit exceeded", 100, 0, resetTime); - - assertThat(ex.getLimit()).isEqualTo(100); - assertThat(ex.getRemaining()).isEqualTo(0); - assertThat(ex.getResetAt()).isEqualTo(resetTime); - assertThat(ex.getRetryAfter()).isGreaterThan(Duration.ZERO); - assertThat(ex.getStatusCode()).isEqualTo(429); - } - - @Test - @DisplayName("RateLimitException - should handle past reset time") - void rateLimitExceptionPastResetTime() { - Instant pastTime = Instant.now().minusSeconds(60); - RateLimitException ex = new RateLimitException("Rate limit exceeded", 100, 0, pastTime); - - assertThat(ex.getRetryAfter()).isEqualTo(Duration.ZERO); - } - - @Test - @DisplayName("TimeoutException - should include timeout duration") - void timeoutException() { - Duration timeout = Duration.ofSeconds(30); - TimeoutException ex = new TimeoutException("Request timed out", timeout); - - assertThat(ex.getMessage()).isEqualTo("Request timed out"); - assertThat(ex.getTimeout()).isEqualTo(timeout); - assertThat(ex.getErrorCode()).isEqualTo("TIMEOUT"); - } - - @Test - @DisplayName("ConnectionException - should include host and port") - void connectionException() { - ConnectionException ex = new ConnectionException( + @Test + @DisplayName("AxonFlowException - should create with message") + void axonFlowExceptionWithMessage() { + AxonFlowException ex = new AxonFlowException("Test error"); + + assertThat(ex.getMessage()).isEqualTo("Test error"); + assertThat(ex.getStatusCode()).isEqualTo(0); + assertThat(ex.getErrorCode()).isNull(); + } + + @Test + @DisplayName("AxonFlowException - should create with full details") + void axonFlowExceptionWithFullDetails() { + AxonFlowException ex = new AxonFlowException("Test error", 500, "INTERNAL_ERROR"); + + assertThat(ex.getMessage()).isEqualTo("Test error"); + assertThat(ex.getStatusCode()).isEqualTo(500); + assertThat(ex.getErrorCode()).isEqualTo("INTERNAL_ERROR"); + } + + @Test + @DisplayName("AxonFlowException - toString should include details") + void axonFlowExceptionToString() { + AxonFlowException ex = new AxonFlowException("Test error", 500, "INTERNAL_ERROR"); + + String str = ex.toString(); + assertThat(str).contains("Test error"); + assertThat(str).contains("500"); + assertThat(str).contains("INTERNAL_ERROR"); + } + + @Test + @DisplayName("AuthenticationException - should set correct status code") + void authenticationException() { + AuthenticationException ex = new AuthenticationException("Invalid credentials"); + + assertThat(ex.getMessage()).isEqualTo("Invalid credentials"); + assertThat(ex.getStatusCode()).isEqualTo(401); + assertThat(ex.getErrorCode()).isEqualTo("AUTHENTICATION_FAILED"); + } + + @Test + @DisplayName("PolicyViolationException - should extract policy name") + void policyViolationExceptionExtractsPolicyName() { + PolicyViolationException ex = + new PolicyViolationException("Request blocked by policy: sql_injection_detection"); + + assertThat(ex.getPolicyName()).isEqualTo("sql_injection_detection"); + assertThat(ex.getBlockReason()).isEqualTo("Request blocked by policy: sql_injection_detection"); + assertThat(ex.getStatusCode()).isEqualTo(403); + } + + @Test + @DisplayName("PolicyViolationException - should accept explicit policy name") + void policyViolationExceptionWithExplicitPolicy() { + PolicyViolationException ex = + new PolicyViolationException( + "PII detected in request", "pii_detection", List.of("pii_detection", "rate_limit")); + + assertThat(ex.getPolicyName()).isEqualTo("pii_detection"); + assertThat(ex.getPoliciesEvaluated()).containsExactly("pii_detection", "rate_limit"); + } + + @Test + @DisplayName("PolicyViolationException - should handle bracket format") + void policyViolationExceptionBracketFormat() { + PolicyViolationException ex = new PolicyViolationException("[rate_limit] Too many requests"); + + assertThat(ex.getPolicyName()).isEqualTo("rate_limit"); + } + + @Test + @DisplayName("RateLimitException - should calculate retry duration") + void rateLimitExceptionRetryDuration() { + Instant resetTime = Instant.now().plusSeconds(60); + RateLimitException ex = new RateLimitException("Rate limit exceeded", 100, 0, resetTime); + + assertThat(ex.getLimit()).isEqualTo(100); + assertThat(ex.getRemaining()).isEqualTo(0); + assertThat(ex.getResetAt()).isEqualTo(resetTime); + assertThat(ex.getRetryAfter()).isGreaterThan(Duration.ZERO); + assertThat(ex.getStatusCode()).isEqualTo(429); + } + + @Test + @DisplayName("RateLimitException - should handle past reset time") + void rateLimitExceptionPastResetTime() { + Instant pastTime = Instant.now().minusSeconds(60); + RateLimitException ex = new RateLimitException("Rate limit exceeded", 100, 0, pastTime); + + assertThat(ex.getRetryAfter()).isEqualTo(Duration.ZERO); + } + + @Test + @DisplayName("TimeoutException - should include timeout duration") + void timeoutException() { + Duration timeout = Duration.ofSeconds(30); + TimeoutException ex = new TimeoutException("Request timed out", timeout); + + assertThat(ex.getMessage()).isEqualTo("Request timed out"); + assertThat(ex.getTimeout()).isEqualTo(timeout); + assertThat(ex.getErrorCode()).isEqualTo("TIMEOUT"); + } + + @Test + @DisplayName("ConnectionException - should include host and port") + void connectionException() { + ConnectionException ex = + new ConnectionException( "Connection refused", "api.example.com", 8080, - new RuntimeException("Connection refused") - ); - - assertThat(ex.getMessage()).isEqualTo("Connection refused"); - assertThat(ex.getHost()).isEqualTo("api.example.com"); - assertThat(ex.getPort()).isEqualTo(8080); - assertThat(ex.getCause()).isNotNull(); - } - - @Test - @DisplayName("ConfigurationException - should include config key") - void configurationException() { - ConfigurationException ex = new ConfigurationException("Invalid URL format", "agentUrl"); - - assertThat(ex.getMessage()).isEqualTo("Invalid URL format"); - assertThat(ex.getConfigKey()).isEqualTo("agentUrl"); - } - - @Test - @DisplayName("ConnectorException - should include connector details") - void connectorException() { - ConnectorException ex = new ConnectorException( - "Connector query failed", - "salesforce", - "getAccounts" - ); - - assertThat(ex.getMessage()).isEqualTo("Connector query failed"); - assertThat(ex.getConnectorId()).isEqualTo("salesforce"); - assertThat(ex.getOperation()).isEqualTo("getAccounts"); - } - - @Test - @DisplayName("PlanExecutionException - should include plan details") - void planExecutionException() { - PlanExecutionException ex = new PlanExecutionException( - "Step failed", - "plan_123", - "step_002" - ); - - assertThat(ex.getMessage()).isEqualTo("Step failed"); - assertThat(ex.getPlanId()).isEqualTo("plan_123"); - assertThat(ex.getFailedStep()).isEqualTo("step_002"); - } + new RuntimeException("Connection refused")); + + assertThat(ex.getMessage()).isEqualTo("Connection refused"); + assertThat(ex.getHost()).isEqualTo("api.example.com"); + assertThat(ex.getPort()).isEqualTo(8080); + assertThat(ex.getCause()).isNotNull(); + } + + @Test + @DisplayName("ConfigurationException - should include config key") + void configurationException() { + ConfigurationException ex = new ConfigurationException("Invalid URL format", "agentUrl"); + + assertThat(ex.getMessage()).isEqualTo("Invalid URL format"); + assertThat(ex.getConfigKey()).isEqualTo("agentUrl"); + } + + @Test + @DisplayName("ConnectorException - should include connector details") + void connectorException() { + ConnectorException ex = + new ConnectorException("Connector query failed", "salesforce", "getAccounts"); + + assertThat(ex.getMessage()).isEqualTo("Connector query failed"); + assertThat(ex.getConnectorId()).isEqualTo("salesforce"); + assertThat(ex.getOperation()).isEqualTo("getAccounts"); + } + + @Test + @DisplayName("PlanExecutionException - should include plan details") + void planExecutionException() { + PlanExecutionException ex = new PlanExecutionException("Step failed", "plan_123", "step_002"); + + assertThat(ex.getMessage()).isEqualTo("Step failed"); + assertThat(ex.getPlanId()).isEqualTo("plan_123"); + assertThat(ex.getFailedStep()).isEqualTo("step_002"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/integration/AxonFlowIntegrationTest.java b/src/test/java/com/getaxonflow/sdk/integration/AxonFlowIntegrationTest.java index 195b3d2..d7abb01 100644 --- a/src/test/java/com/getaxonflow/sdk/integration/AxonFlowIntegrationTest.java +++ b/src/test/java/com/getaxonflow/sdk/integration/AxonFlowIntegrationTest.java @@ -15,341 +15,357 @@ */ package com.getaxonflow.sdk.integration; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.*; import com.getaxonflow.sdk.exceptions.*; import com.getaxonflow.sdk.types.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.List; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; - -import java.util.List; -import java.util.Map; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; @WireMockTest @DisplayName("AxonFlow Integration Tests") class AxonFlowIntegrationTest { - private AxonFlow axonflow; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - } - - @Test - @DisplayName("health check should return healthy status") - void healthCheckShouldReturnHealthy() { - stubFor(get(urlEqualTo("/health")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"status\": \"healthy\"," - + "\"version\": \"1.0.0\"," - + "\"uptime\": \"24h\"" - + "}"))); - - HealthStatus health = axonflow.healthCheck(); - - assertThat(health.isHealthy()).isTrue(); - assertThat(health.getVersion()).isEqualTo("1.0.0"); - assertThat(health.getUptime()).isEqualTo("24h"); - } - - @Test - @DisplayName("getPolicyApprovedContext should return approval") - void getPolicyApprovedContextShouldReturnApproval() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_abc123\"," - + "\"approved\": true," - + "\"policies\": [\"policy1\", \"policy2\"]," - + "\"processing_time\": \"5.23ms\"" - + "}"))); - - PolicyApprovalResult result = axonflow.getPolicyApprovedContext( + private AxonFlow axonflow; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create(AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + } + + @Test + @DisplayName("health check should return healthy status") + void healthCheckShouldReturnHealthy() { + stubFor( + get(urlEqualTo("/health")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"status\": \"healthy\"," + + "\"version\": \"1.0.0\"," + + "\"uptime\": \"24h\"" + + "}"))); + + HealthStatus health = axonflow.healthCheck(); + + assertThat(health.isHealthy()).isTrue(); + assertThat(health.getVersion()).isEqualTo("1.0.0"); + assertThat(health.getUptime()).isEqualTo("24h"); + } + + @Test + @DisplayName("getPolicyApprovedContext should return approval") + void getPolicyApprovedContextShouldReturnApproval() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_abc123\"," + + "\"approved\": true," + + "\"policies\": [\"policy1\", \"policy2\"]," + + "\"processing_time\": \"5.23ms\"" + + "}"))); + + PolicyApprovalResult result = + axonflow.getPolicyApprovedContext( PolicyApprovalRequest.builder() .userToken("user-123") .query("What is the weather?") - .build() - ); + .build()); - assertThat(result.isApproved()).isTrue(); - assertThat(result.getContextId()).isEqualTo("ctx_abc123"); - assertThat(result.getPolicies()).containsExactly("policy1", "policy2"); + assertThat(result.isApproved()).isTrue(); + assertThat(result.getContextId()).isEqualTo("ctx_abc123"); + assertThat(result.getPolicies()).containsExactly("policy1", "policy2"); - verify(postRequestedFor(urlEqualTo("/api/policy/pre-check")) + verify( + postRequestedFor(urlEqualTo("/api/policy/pre-check")) .withHeader("Content-Type", containing("application/json")) .withRequestBody(containing("\"user_token\":\"user-123\"")) .withRequestBody(containing("\"query\":\"What is the weather?\""))); - } - - @Test - @DisplayName("getPolicyApprovedContext should throw on block") - void getPolicyApprovedContextShouldThrowOnBlock() { - stubFor(post(urlEqualTo("/api/policy/pre-check")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"context_id\": \"ctx_abc123\"," - + "\"approved\": false," - + "\"block_reason\": \"Request blocked by policy: pii_detection\"," - + "\"policies\": [\"pii_detection\"]" - + "}"))); - - assertThatThrownBy(() -> axonflow.getPolicyApprovedContext( - PolicyApprovalRequest.builder() - .userToken("user-123") - .query("My SSN is 123-45-6789") - .build() - )) - .isInstanceOf(PolicyViolationException.class) - .extracting("policyName") - .isEqualTo("pii_detection"); - } - - @Test - @DisplayName("auditLLMCall should record audit") - void auditLLMCallShouldRecordAudit() { - stubFor(post(urlEqualTo("/api/audit/llm-call")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"audit_id\": \"audit_xyz789\"" - + "}"))); - - AuditResult result = axonflow.auditLLMCall(AuditOptions.builder() - .contextId("ctx_abc123") - .clientId("test-client") - .provider("openai") - .model("gpt-4") - .tokenUsage(TokenUsage.of(100, 150)) - .latencyMs(1234) - .build() - ); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getAuditId()).isEqualTo("audit_xyz789"); - - verify(postRequestedFor(urlEqualTo("/api/audit/llm-call")) + } + + @Test + @DisplayName("getPolicyApprovedContext should throw on block") + void getPolicyApprovedContextShouldThrowOnBlock() { + stubFor( + post(urlEqualTo("/api/policy/pre-check")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"context_id\": \"ctx_abc123\"," + + "\"approved\": false," + + "\"block_reason\": \"Request blocked by policy: pii_detection\"," + + "\"policies\": [\"pii_detection\"]" + + "}"))); + + assertThatThrownBy( + () -> + axonflow.getPolicyApprovedContext( + PolicyApprovalRequest.builder() + .userToken("user-123") + .query("My SSN is 123-45-6789") + .build())) + .isInstanceOf(PolicyViolationException.class) + .extracting("policyName") + .isEqualTo("pii_detection"); + } + + @Test + @DisplayName("auditLLMCall should record audit") + void auditLLMCallShouldRecordAudit() { + stubFor( + post(urlEqualTo("/api/audit/llm-call")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + "\"success\": true," + "\"audit_id\": \"audit_xyz789\"" + "}"))); + + AuditResult result = + axonflow.auditLLMCall( + AuditOptions.builder() + .contextId("ctx_abc123") + .clientId("test-client") + .provider("openai") + .model("gpt-4") + .tokenUsage(TokenUsage.of(100, 150)) + .latencyMs(1234) + .build()); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getAuditId()).isEqualTo("audit_xyz789"); + + verify( + postRequestedFor(urlEqualTo("/api/audit/llm-call")) .withRequestBody(containing("\"context_id\":\"ctx_abc123\"")) .withRequestBody(containing("\"provider\":\"openai\""))); - } - - @Test - @DisplayName("proxyLLMCall should return response") - void proxyLLMCallShouldReturnResponse() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": {\"message\": \"The weather is sunny\"}," - + "\"blocked\": false," - + "\"policy_info\": {" - + "\"policies_evaluated\": [\"rate_limit\"]," - + "\"processing_time\": \"12.5ms\"" - + "}" - + "}"))); - - ClientResponse response = axonflow.proxyLLMCall(ClientRequest.builder() - .query("What is the weather?") - .userToken("user-123") - .build() - ); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.isBlocked()).isFalse(); - assertThat(response.getData()).isNotNull(); - } - - @Test - @DisplayName("proxyLLMCall should throw on policy block") - void proxyLLMCallShouldThrowOnPolicyBlock() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": false," - + "\"blocked\": true," - + "\"block_reason\": \"Request blocked by policy: sql_injection_detection\"," - + "\"policy_info\": {" - + "\"policies_evaluated\": [\"sql_injection_detection\"]," - + "\"static_checks\": [\"sql_injection\"]" - + "}" - + "}"))); - - assertThatThrownBy(() -> axonflow.proxyLLMCall(ClientRequest.builder() - .query("SELECT * FROM users; DROP TABLE users;") - .userToken("user-123") - .build() - )) - .isInstanceOf(PolicyViolationException.class) - .extracting("policyName") - .isEqualTo("sql_injection_detection"); - } - - @Test - @DisplayName("should handle authentication error") - void shouldHandleAuthenticationError() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(401) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"error\": \"Invalid credentials\"" - + "}"))); - - assertThatThrownBy(() -> axonflow.proxyLLMCall(ClientRequest.builder() - .query("test") - .build() - )) - .isInstanceOf(AuthenticationException.class); - } - - @Test - @DisplayName("should handle rate limit error") - void shouldHandleRateLimitError() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(429) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"error\": \"Rate limit exceeded\"" - + "}"))); - - assertThatThrownBy(() -> axonflow.proxyLLMCall(ClientRequest.builder() - .query("test") - .build() - )) - .isInstanceOf(RateLimitException.class); - } - - @Test - @DisplayName("generatePlan should return plan") - void generatePlanShouldReturnPlan() { - stubFor(post(urlEqualTo("/api/v1/orchestrator/plan")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"plan_id\": \"plan_123\"," - + "\"steps\": [" - + "{" - + "\"id\": \"step_001\"," - + "\"name\": \"research\"," - + "\"type\": \"llm-call\"" - + "}" - + "]," - + "\"domain\": \"generic\"," - + "\"complexity\": 2," - + "\"status\": \"pending\"" - + "}"))); - - PlanResponse plan = axonflow.generatePlan(PlanRequest.builder() - .objective("Research AI governance") - .domain("generic") - .build() - ); - - assertThat(plan.getPlanId()).isEqualTo("plan_123"); - assertThat(plan.getSteps()).hasSize(1); - assertThat(plan.getDomain()).isEqualTo("generic"); - } - - @Test - @DisplayName("listConnectors should return connectors") - void listConnectorsShouldReturnConnectors() { - stubFor(get(urlEqualTo("/api/v1/connectors")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("[" - + "{" - + "\"id\": \"salesforce\"," - + "\"name\": \"Salesforce\"," - + "\"type\": \"crm\"," - + "\"installed\": true" - + "}," - + "{" - + "\"id\": \"hubspot\"," - + "\"name\": \"HubSpot\"," - + "\"type\": \"crm\"," - + "\"installed\": false" - + "}" - + "]"))); - - List connectors = axonflow.listConnectors(); - - assertThat(connectors).hasSize(2); - assertThat(connectors.get(0).getId()).isEqualTo("salesforce"); - assertThat(connectors.get(0).isInstalled()).isTrue(); - } - - @Test - @DisplayName("queryConnector should return response") - void queryConnectorShouldReturnResponse() { - // MCP connector queries now use /api/request with request_type: "mcp-query" - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": [{\"name\": \"Acme Corp\"}]," - + "\"blocked\": false" - + "}"))); - - ConnectorResponse response = axonflow.queryConnector(ConnectorQuery.builder() - .connectorId("salesforce") - .operation("getAccounts") - .build() - ); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.getConnectorId()).isEqualTo("salesforce"); - } - - @Test - @DisplayName("should cache successful responses") - void shouldCacheSuccessfulResponses() { - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{" - + "\"success\": true," - + "\"data\": \"cached response\"," - + "\"blocked\": false" - + "}"))); - - ClientRequest request = ClientRequest.builder() - .query("test query") - .userToken("user-123") - .build(); - - // First call - should hit the server - axonflow.proxyLLMCall(request); - - // Second call with same parameters - should use cache - axonflow.proxyLLMCall(request); - - // Verify only one request was made - verify(1, postRequestedFor(urlEqualTo("/api/request"))); - } + } + + @Test + @DisplayName("proxyLLMCall should return response") + void proxyLLMCallShouldReturnResponse() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"data\": {\"message\": \"The weather is sunny\"}," + + "\"blocked\": false," + + "\"policy_info\": {" + + "\"policies_evaluated\": [\"rate_limit\"]," + + "\"processing_time\": \"12.5ms\"" + + "}" + + "}"))); + + ClientResponse response = + axonflow.proxyLLMCall( + ClientRequest.builder().query("What is the weather?").userToken("user-123").build()); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.isBlocked()).isFalse(); + assertThat(response.getData()).isNotNull(); + } + + @Test + @DisplayName("proxyLLMCall should throw on policy block") + void proxyLLMCallShouldThrowOnPolicyBlock() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": false," + + "\"blocked\": true," + + "\"block_reason\": \"Request blocked by policy: sql_injection_detection\"," + + "\"policy_info\": {" + + "\"policies_evaluated\": [\"sql_injection_detection\"]," + + "\"static_checks\": [\"sql_injection\"]" + + "}" + + "}"))); + + assertThatThrownBy( + () -> + axonflow.proxyLLMCall( + ClientRequest.builder() + .query("SELECT * FROM users; DROP TABLE users;") + .userToken("user-123") + .build())) + .isInstanceOf(PolicyViolationException.class) + .extracting("policyName") + .isEqualTo("sql_injection_detection"); + } + + @Test + @DisplayName("should handle authentication error") + void shouldHandleAuthenticationError() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(401) + .withHeader("Content-Type", "application/json") + .withBody("{" + "\"error\": \"Invalid credentials\"" + "}"))); + + assertThatThrownBy(() -> axonflow.proxyLLMCall(ClientRequest.builder().query("test").build())) + .isInstanceOf(AuthenticationException.class); + } + + @Test + @DisplayName("should handle rate limit error") + void shouldHandleRateLimitError() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(429) + .withHeader("Content-Type", "application/json") + .withBody("{" + "\"error\": \"Rate limit exceeded\"" + "}"))); + + assertThatThrownBy(() -> axonflow.proxyLLMCall(ClientRequest.builder().query("test").build())) + .isInstanceOf(RateLimitException.class); + } + + @Test + @DisplayName("generatePlan should return plan") + void generatePlanShouldReturnPlan() { + stubFor( + post(urlEqualTo("/api/v1/orchestrator/plan")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"plan_id\": \"plan_123\"," + + "\"steps\": [" + + "{" + + "\"id\": \"step_001\"," + + "\"name\": \"research\"," + + "\"type\": \"llm-call\"" + + "}" + + "]," + + "\"domain\": \"generic\"," + + "\"complexity\": 2," + + "\"status\": \"pending\"" + + "}"))); + + PlanResponse plan = + axonflow.generatePlan( + PlanRequest.builder().objective("Research AI governance").domain("generic").build()); + + assertThat(plan.getPlanId()).isEqualTo("plan_123"); + assertThat(plan.getSteps()).hasSize(1); + assertThat(plan.getDomain()).isEqualTo("generic"); + } + + @Test + @DisplayName("listConnectors should return connectors") + void listConnectorsShouldReturnConnectors() { + stubFor( + get(urlEqualTo("/api/v1/connectors")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "[" + + "{" + + "\"id\": \"salesforce\"," + + "\"name\": \"Salesforce\"," + + "\"type\": \"crm\"," + + "\"installed\": true" + + "}," + + "{" + + "\"id\": \"hubspot\"," + + "\"name\": \"HubSpot\"," + + "\"type\": \"crm\"," + + "\"installed\": false" + + "}" + + "]"))); + + List connectors = axonflow.listConnectors(); + + assertThat(connectors).hasSize(2); + assertThat(connectors.get(0).getId()).isEqualTo("salesforce"); + assertThat(connectors.get(0).isInstalled()).isTrue(); + } + + @Test + @DisplayName("queryConnector should return response") + void queryConnectorShouldReturnResponse() { + // MCP connector queries now use /api/request with request_type: "mcp-query" + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"data\": [{\"name\": \"Acme Corp\"}]," + + "\"blocked\": false" + + "}"))); + + ConnectorResponse response = + axonflow.queryConnector( + ConnectorQuery.builder().connectorId("salesforce").operation("getAccounts").build()); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getConnectorId()).isEqualTo("salesforce"); + } + + @Test + @DisplayName("should cache successful responses") + void shouldCacheSuccessfulResponses() { + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"success\": true," + + "\"data\": \"cached response\"," + + "\"blocked\": false" + + "}"))); + + ClientRequest request = + ClientRequest.builder().query("test query").userToken("user-123").build(); + + // First call - should hit the server + axonflow.proxyLLMCall(request); + + // Second call with same parameters - should use cache + axonflow.proxyLLMCall(request); + + // Verify only one request was made + verify(1, postRequestedFor(urlEqualTo("/api/request"))); + } } diff --git a/src/test/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptorTest.java b/src/test/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptorTest.java index 40dec9b..22308dd 100644 --- a/src/test/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptorTest.java +++ b/src/test/java/com/getaxonflow/sdk/interceptors/AnthropicInterceptorTest.java @@ -6,6 +6,9 @@ */ package com.getaxonflow.sdk.interceptors; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.AxonFlowConfig; import com.getaxonflow.sdk.exceptions.PolicyViolationException; @@ -15,370 +18,386 @@ import com.getaxonflow.sdk.interceptors.AnthropicInterceptor.AnthropicResponse; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("Anthropic Interceptor") class AnthropicInterceptorTest { - @Nested - @DisplayName("Type Tests") - class TypeTests { - - @Test - @DisplayName("AnthropicRequest builder should work correctly") - void testAnthropicRequestBuilder() { - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-opus-20240229") - .maxTokens(2048) - .system("You are a helpful assistant.") - .addUserMessage("Hello!") - .addAssistantMessage("Hi there!") - .addUserMessage("How are you?") - .temperature(0.8) - .topP(0.95) - .topK(40) - .build(); - - assertThat(request.getModel()).isEqualTo("claude-3-opus-20240229"); - assertThat(request.getMaxTokens()).isEqualTo(2048); - assertThat(request.getSystem()).isEqualTo("You are a helpful assistant."); - assertThat(request.getMessages()).hasSize(3); - assertThat(request.getMessages().get(0).getRole()).isEqualTo("user"); - assertThat(request.getMessages().get(1).getRole()).isEqualTo("assistant"); - assertThat(request.getMessages().get(2).getRole()).isEqualTo("user"); - assertThat(request.getTemperature()).isEqualTo(0.8); - assertThat(request.getTopP()).isEqualTo(0.95); - assertThat(request.getTopK()).isEqualTo(40); - } - - @Test - @DisplayName("AnthropicRequest extractPrompt should include system and messages") - void testAnthropicRequestExtractPrompt() { - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-sonnet-20240229") - .maxTokens(1024) - .system("System message") - .addUserMessage("User message") - .addAssistantMessage("Assistant message") - .build(); - - String prompt = request.extractPrompt(); - assertThat(prompt).contains("System message"); - assertThat(prompt).contains("User message"); - assertThat(prompt).contains("Assistant message"); - } - - @Test - @DisplayName("AnthropicRequest should require model") - void testAnthropicRequestRequiresModel() { - assertThatThrownBy(() -> AnthropicRequest.builder() - .maxTokens(1024) - .addUserMessage("Test") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("AnthropicResponse builder should work correctly") - void testAnthropicResponseBuilder() { - AnthropicResponse response = AnthropicResponse.builder() - .id("msg-test-123") - .type("message") - .role("assistant") - .model("claude-3-sonnet-20240229") - .content(List.of( - AnthropicContentBlock.text("First paragraph."), - AnthropicContentBlock.text("Second paragraph.") - )) - .stopReason("end_turn") - .usage(new AnthropicResponse.Usage(100, 50)) - .build(); - - assertThat(response.getId()).isEqualTo("msg-test-123"); - assertThat(response.getType()).isEqualTo("message"); - assertThat(response.getRole()).isEqualTo("assistant"); - assertThat(response.getModel()).isEqualTo("claude-3-sonnet-20240229"); - assertThat(response.getContent()).hasSize(2); - assertThat(response.getStopReason()).isEqualTo("end_turn"); - assertThat(response.getUsage().getInputTokens()).isEqualTo(100); - assertThat(response.getUsage().getOutputTokens()).isEqualTo(50); - } - - @Test - @DisplayName("AnthropicResponse getSummary should truncate long content") - void testAnthropicResponseGetSummaryTruncation() { - String longText = "A".repeat(200); - AnthropicResponse response = AnthropicResponse.builder() - .content(List.of(AnthropicContentBlock.text(longText))) - .build(); - - assertThat(response.getSummary()).hasSize(100); - } - - @Test - @DisplayName("AnthropicResponse getSummary should return empty for no content") - void testAnthropicResponseGetSummaryEmpty() { - AnthropicResponse response = AnthropicResponse.builder() - .content(List.of()) - .build(); - - assertThat(response.getSummary()).isEmpty(); - } - - @Test - @DisplayName("AnthropicMessage factory methods should work correctly") - void testAnthropicMessageFactory() { - AnthropicMessage user = AnthropicMessage.user("User content"); - assertThat(user.getRole()).isEqualTo("user"); - assertThat(user.getContent()).hasSize(1); - assertThat(user.getContent().get(0).getType()).isEqualTo("text"); - assertThat(user.getContent().get(0).getText()).isEqualTo("User content"); - - AnthropicMessage assistant = AnthropicMessage.assistant("Assistant content"); - assertThat(assistant.getRole()).isEqualTo("assistant"); - assertThat(assistant.getContent().get(0).getText()).isEqualTo("Assistant content"); - } - - @Test - @DisplayName("AnthropicContentBlock text factory should work correctly") - void testAnthropicContentBlock() { - AnthropicContentBlock block = AnthropicContentBlock.text("Test text"); - assertThat(block.getType()).isEqualTo("text"); - assertThat(block.getText()).isEqualTo("Test text"); - } + @Nested + @DisplayName("Type Tests") + class TypeTests { + + @Test + @DisplayName("AnthropicRequest builder should work correctly") + void testAnthropicRequestBuilder() { + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-opus-20240229") + .maxTokens(2048) + .system("You are a helpful assistant.") + .addUserMessage("Hello!") + .addAssistantMessage("Hi there!") + .addUserMessage("How are you?") + .temperature(0.8) + .topP(0.95) + .topK(40) + .build(); + + assertThat(request.getModel()).isEqualTo("claude-3-opus-20240229"); + assertThat(request.getMaxTokens()).isEqualTo(2048); + assertThat(request.getSystem()).isEqualTo("You are a helpful assistant."); + assertThat(request.getMessages()).hasSize(3); + assertThat(request.getMessages().get(0).getRole()).isEqualTo("user"); + assertThat(request.getMessages().get(1).getRole()).isEqualTo("assistant"); + assertThat(request.getMessages().get(2).getRole()).isEqualTo("user"); + assertThat(request.getTemperature()).isEqualTo(0.8); + assertThat(request.getTopP()).isEqualTo(0.95); + assertThat(request.getTopK()).isEqualTo(40); + } + + @Test + @DisplayName("AnthropicRequest extractPrompt should include system and messages") + void testAnthropicRequestExtractPrompt() { + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-sonnet-20240229") + .maxTokens(1024) + .system("System message") + .addUserMessage("User message") + .addAssistantMessage("Assistant message") + .build(); + + String prompt = request.extractPrompt(); + assertThat(prompt).contains("System message"); + assertThat(prompt).contains("User message"); + assertThat(prompt).contains("Assistant message"); + } + + @Test + @DisplayName("AnthropicRequest should require model") + void testAnthropicRequestRequiresModel() { + assertThatThrownBy( + () -> AnthropicRequest.builder().maxTokens(1024).addUserMessage("Test").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("AnthropicResponse builder should work correctly") + void testAnthropicResponseBuilder() { + AnthropicResponse response = + AnthropicResponse.builder() + .id("msg-test-123") + .type("message") + .role("assistant") + .model("claude-3-sonnet-20240229") + .content( + List.of( + AnthropicContentBlock.text("First paragraph."), + AnthropicContentBlock.text("Second paragraph."))) + .stopReason("end_turn") + .usage(new AnthropicResponse.Usage(100, 50)) + .build(); + + assertThat(response.getId()).isEqualTo("msg-test-123"); + assertThat(response.getType()).isEqualTo("message"); + assertThat(response.getRole()).isEqualTo("assistant"); + assertThat(response.getModel()).isEqualTo("claude-3-sonnet-20240229"); + assertThat(response.getContent()).hasSize(2); + assertThat(response.getStopReason()).isEqualTo("end_turn"); + assertThat(response.getUsage().getInputTokens()).isEqualTo(100); + assertThat(response.getUsage().getOutputTokens()).isEqualTo(50); + } + + @Test + @DisplayName("AnthropicResponse getSummary should truncate long content") + void testAnthropicResponseGetSummaryTruncation() { + String longText = "A".repeat(200); + AnthropicResponse response = + AnthropicResponse.builder() + .content(List.of(AnthropicContentBlock.text(longText))) + .build(); + + assertThat(response.getSummary()).hasSize(100); + } + + @Test + @DisplayName("AnthropicResponse getSummary should return empty for no content") + void testAnthropicResponseGetSummaryEmpty() { + AnthropicResponse response = AnthropicResponse.builder().content(List.of()).build(); + + assertThat(response.getSummary()).isEmpty(); + } + + @Test + @DisplayName("AnthropicMessage factory methods should work correctly") + void testAnthropicMessageFactory() { + AnthropicMessage user = AnthropicMessage.user("User content"); + assertThat(user.getRole()).isEqualTo("user"); + assertThat(user.getContent()).hasSize(1); + assertThat(user.getContent().get(0).getType()).isEqualTo("text"); + assertThat(user.getContent().get(0).getText()).isEqualTo("User content"); + + AnthropicMessage assistant = AnthropicMessage.assistant("Assistant content"); + assertThat(assistant.getRole()).isEqualTo("assistant"); + assertThat(assistant.getContent().get(0).getText()).isEqualTo("Assistant content"); + } + + @Test + @DisplayName("AnthropicContentBlock text factory should work correctly") + void testAnthropicContentBlock() { + AnthropicContentBlock block = AnthropicContentBlock.text("Test text"); + assertThat(block.getType()).isEqualTo("text"); + assertThat(block.getText()).isEqualTo("Test text"); + } + } + + @Nested + @WireMockTest + @DisplayName("Integration Tests") + class IntegrationTests { + + private AxonFlow axonflow; + private AnthropicInterceptor interceptor; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + interceptor = + AnthropicInterceptor.builder() + .axonflow(axonflow) + .userToken("test-user") + .asyncAudit(false) + .build(); + } + + @Test + @DisplayName("Builder should require AxonFlow") + void testBuilderRequiresAxonFlow() { + assertThatThrownBy(() -> AnthropicInterceptor.builder().build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("wrap should allow request when not blocked") + void testWrapAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock Anthropic call + Function mockCall = + request -> + AnthropicResponse.builder() + .id("msg-123") + .model("claude-3-sonnet-20240229") + .role("assistant") + .content(List.of(AnthropicContentBlock.text("Hello! I'm Claude."))) + .stopReason("end_turn") + .usage(new AnthropicResponse.Usage(10, 20)) + .build(); + + // Create request + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-sonnet-20240229") + .maxTokens(1024) + .addUserMessage("Hello!") + .temperature(0.7) + .build(); + + // Execute wrapped call + AnthropicResponse response = interceptor.wrap(mockCall).apply(request); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("msg-123"); + assertThat(response.getSummary()).isEqualTo("Hello! I'm Claude."); + + // Verify API was called + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrap should throw when blocked by policy") + void testWrapBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: content-filter\"}"))); + + // Create mock Anthropic call (should not be called) + Function mockCall = + request -> { + fail("Anthropic call should not be made when blocked"); + return null; + }; + + // Create request + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-sonnet-20240229") + .maxTokens(1024) + .addUserMessage("Blocked content") + .build(); + + // Execute wrapped call + assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("content-filter"); + } + + @Test + @DisplayName("wrapAsync should allow request when not blocked") + void testWrapAsyncAllowedRequest() throws Exception { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock async Anthropic call + Function> mockCall = + request -> + CompletableFuture.completedFuture( + AnthropicResponse.builder() + .id("msg-456") + .model("claude-3-opus-20240229") + .content(List.of(AnthropicContentBlock.text("Async response"))) + .usage(new AnthropicResponse.Usage(5, 15)) + .build()); + + // Create request + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-opus-20240229") + .maxTokens(1024) + .addUserMessage("Async test") + .build(); + + // Execute wrapped async call + AnthropicResponse response = interceptor.wrapAsync(mockCall).apply(request).get(); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("msg-456"); + assertThat(response.getSummary()).isEqualTo("Async response"); + } + + @Test + @DisplayName("wrapAsync should throw when blocked by policy") + void testWrapAsyncBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation detected\"}"))); + + // Create mock async Anthropic call (should not be called) + Function> mockCall = + request -> { + fail("Anthropic call should not be made when blocked"); + return null; + }; + + // Create request + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-sonnet-20240229") + .maxTokens(1024) + .addUserMessage("Blocked") + .build(); + + // Execute wrapped async call + CompletableFuture future = interceptor.wrapAsync(mockCall).apply(request); + + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(PolicyViolationException.class); } - @Nested - @WireMockTest - @DisplayName("Integration Tests") - class IntegrationTests { - - private AxonFlow axonflow; - private AnthropicInterceptor interceptor; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - interceptor = AnthropicInterceptor.builder() - .axonflow(axonflow) - .userToken("test-user") - .asyncAudit(false) - .build(); - } - - @Test - @DisplayName("Builder should require AxonFlow") - void testBuilderRequiresAxonFlow() { - assertThatThrownBy(() -> AnthropicInterceptor.builder().build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("wrap should allow request when not blocked") - void testWrapAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock Anthropic call - Function mockCall = request -> - AnthropicResponse.builder() - .id("msg-123") - .model("claude-3-sonnet-20240229") - .role("assistant") - .content(List.of(AnthropicContentBlock.text("Hello! I'm Claude."))) - .stopReason("end_turn") - .usage(new AnthropicResponse.Usage(10, 20)) - .build(); - - // Create request - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-sonnet-20240229") - .maxTokens(1024) - .addUserMessage("Hello!") - .temperature(0.7) - .build(); - - // Execute wrapped call - AnthropicResponse response = interceptor.wrap(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("msg-123"); - assertThat(response.getSummary()).isEqualTo("Hello! I'm Claude."); - - // Verify API was called - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrap should throw when blocked by policy") - void testWrapBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: content-filter\"}"))); - - // Create mock Anthropic call (should not be called) - Function mockCall = request -> { - fail("Anthropic call should not be made when blocked"); - return null; - }; - - // Create request - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-sonnet-20240229") - .maxTokens(1024) - .addUserMessage("Blocked content") - .build(); - - // Execute wrapped call - assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("content-filter"); - } - - @Test - @DisplayName("wrapAsync should allow request when not blocked") - void testWrapAsyncAllowedRequest() throws Exception { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock async Anthropic call - Function> mockCall = - request -> CompletableFuture.completedFuture( - AnthropicResponse.builder() - .id("msg-456") - .model("claude-3-opus-20240229") - .content(List.of(AnthropicContentBlock.text("Async response"))) - .usage(new AnthropicResponse.Usage(5, 15)) - .build() - ); - - // Create request - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-opus-20240229") - .maxTokens(1024) - .addUserMessage("Async test") - .build(); - - // Execute wrapped async call - AnthropicResponse response = interceptor.wrapAsync(mockCall) - .apply(request) - .get(); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("msg-456"); - assertThat(response.getSummary()).isEqualTo("Async response"); - } - - @Test - @DisplayName("wrapAsync should throw when blocked by policy") - void testWrapAsyncBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation detected\"}"))); - - // Create mock async Anthropic call (should not be called) - Function> mockCall = - request -> { - fail("Anthropic call should not be made when blocked"); - return null; - }; - - // Create request - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-sonnet-20240229") - .maxTokens(1024) - .addUserMessage("Blocked") - .build(); - - // Execute wrapped async call - CompletableFuture future = interceptor.wrapAsync(mockCall) - .apply(request); - - assertThatThrownBy(future::get) - .isInstanceOf(ExecutionException.class) - .hasCauseInstanceOf(PolicyViolationException.class); - } - - @Test - @DisplayName("static wrapMessage should work") - void testStaticWrapperMethod() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock Anthropic call - Function mockCall = request -> - AnthropicResponse.builder() - .id("msg-789") - .model("claude-3-haiku-20240307") - .build(); - - // Use static wrapper - AnthropicRequest request = AnthropicRequest.builder() - .model("claude-3-haiku-20240307") - .maxTokens(512) - .addUserMessage("Static test") - .build(); - - AnthropicResponse response = AnthropicInterceptor.wrapMessage( - axonflow, "user-token", mockCall - ).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("msg-789"); - } + @Test + @DisplayName("static wrapMessage should work") + void testStaticWrapperMethod() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock Anthropic call + Function mockCall = + request -> + AnthropicResponse.builder().id("msg-789").model("claude-3-haiku-20240307").build(); + + // Use static wrapper + AnthropicRequest request = + AnthropicRequest.builder() + .model("claude-3-haiku-20240307") + .maxTokens(512) + .addUserMessage("Static test") + .build(); + + AnthropicResponse response = + AnthropicInterceptor.wrapMessage(axonflow, "user-token", mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("msg-789"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/interceptors/BedrockInterceptorTest.java b/src/test/java/com/getaxonflow/sdk/interceptors/BedrockInterceptorTest.java index 53e92d2..58abc75 100644 --- a/src/test/java/com/getaxonflow/sdk/interceptors/BedrockInterceptorTest.java +++ b/src/test/java/com/getaxonflow/sdk/interceptors/BedrockInterceptorTest.java @@ -6,511 +6,505 @@ */ package com.getaxonflow.sdk.interceptors; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.AxonFlowConfig; import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.getaxonflow.sdk.interceptors.BedrockInterceptor.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("Bedrock Interceptor") class BedrockInterceptorTest { - @Nested - @DisplayName("Type Tests") - class TypeTests { - - @Test - @DisplayName("Model ID constants should be defined") - void testModelIdConstants() { - assertThat(BedrockInterceptor.CLAUDE_3_OPUS).isEqualTo("anthropic.claude-3-opus-20240229-v1:0"); - assertThat(BedrockInterceptor.CLAUDE_3_SONNET).isEqualTo("anthropic.claude-3-sonnet-20240229-v1:0"); - assertThat(BedrockInterceptor.CLAUDE_3_HAIKU).isEqualTo("anthropic.claude-3-haiku-20240307-v1:0"); - assertThat(BedrockInterceptor.CLAUDE_2).isEqualTo("anthropic.claude-v2:1"); - assertThat(BedrockInterceptor.TITAN_TEXT_EXPRESS).isEqualTo("amazon.titan-text-express-v1"); - assertThat(BedrockInterceptor.TITAN_TEXT_LITE).isEqualTo("amazon.titan-text-lite-v1"); - assertThat(BedrockInterceptor.LLAMA2_70B).isEqualTo("meta.llama2-70b-chat-v1"); - assertThat(BedrockInterceptor.LLAMA3_70B).isEqualTo("meta.llama3-70b-instruct-v1:0"); - } - - @Test - @DisplayName("ClaudeMessage factory methods should work correctly") - void testClaudeMessageFactory() { - ClaudeMessage user = ClaudeMessage.user("User message"); - assertThat(user.getRole()).isEqualTo("user"); - assertThat(user.getContent()).isEqualTo("User message"); - - ClaudeMessage assistant = ClaudeMessage.assistant("Assistant reply"); - assertThat(assistant.getRole()).isEqualTo("assistant"); - assertThat(assistant.getContent()).isEqualTo("Assistant reply"); - } - - @Test - @DisplayName("ClaudeMessage constructor and setters") - void testClaudeMessageConstructorSetters() { - ClaudeMessage message = new ClaudeMessage(); - message.setRole("user"); - message.setContent("Hello"); - - assertThat(message.getRole()).isEqualTo("user"); - assertThat(message.getContent()).isEqualTo("Hello"); - - // Test constructor with role and content - ClaudeMessage message2 = new ClaudeMessage("assistant", "Response"); - assertThat(message2.getRole()).isEqualTo("assistant"); - assertThat(message2.getContent()).isEqualTo("Response"); - } - - @Test - @DisplayName("BedrockInvokeRequest forClaude should work correctly") - void testBedrockInvokeRequestForClaude() { - List messages = List.of( - ClaudeMessage.user("Hello"), - ClaudeMessage.assistant("Hi there!") - ); - - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_3_SONNET, - messages, - 1024 - ); - - assertThat(request.getModelId()).isEqualTo("anthropic.claude-3-sonnet-20240229-v1:0"); - assertThat(request.getMessages()).hasSize(2); - } - - @Test - @DisplayName("BedrockInvokeRequest forTitan should work correctly") - void testBedrockInvokeRequestForTitan() { - BedrockInvokeRequest request = BedrockInvokeRequest.forTitan( - BedrockInterceptor.TITAN_TEXT_EXPRESS, - "Generate some text" - ); - - assertThat(request.getModelId()).isEqualTo("amazon.titan-text-express-v1"); - assertThat(request.getInputText()).isEqualTo("Generate some text"); - } - - @Test - @DisplayName("BedrockInvokeRequest extractPrompt should handle Claude messages") - void testBedrockInvokeRequestExtractPromptClaude() { - List messages = List.of( - ClaudeMessage.user("First message"), - ClaudeMessage.user("Second message") - ); - - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_3_HAIKU, - messages, - 500 - ); - - String prompt = request.extractPrompt(); - assertThat(prompt).contains("First message"); - assertThat(prompt).contains("Second message"); - } - - @Test - @DisplayName("BedrockInvokeRequest extractPrompt should handle Titan inputText") - void testBedrockInvokeRequestExtractPromptTitan() { - BedrockInvokeRequest request = BedrockInvokeRequest.forTitan( - BedrockInterceptor.TITAN_TEXT_LITE, - "Titan prompt" - ); - - assertThat(request.extractPrompt()).isEqualTo("Titan prompt"); - } - - @Test - @DisplayName("BedrockInvokeRequest extractPrompt should handle empty state") - void testBedrockInvokeRequestExtractPromptEmpty() { - BedrockInvokeRequest request = new BedrockInvokeRequest(); - assertThat(request.extractPrompt()).isEmpty(); - } - - @Test - @DisplayName("BedrockInvokeRequest setters should work correctly") - void testBedrockInvokeRequestSetters() { - BedrockInvokeRequest request = new BedrockInvokeRequest(); - request.setModelId("test-model"); - request.setBody("{\"test\":true}"); - request.setContentType("application/json"); - request.setAccept("application/json"); - request.setMessages(List.of(ClaudeMessage.user("Test"))); - request.setInputText("Test input"); - - assertThat(request.getModelId()).isEqualTo("test-model"); - assertThat(request.getBody()).isEqualTo("{\"test\":true}"); - assertThat(request.getContentType()).isEqualTo("application/json"); - assertThat(request.getAccept()).isEqualTo("application/json"); - assertThat(request.getMessages()).hasSize(1); - assertThat(request.getInputText()).isEqualTo("Test input"); - } - - @Test - @DisplayName("BedrockInvokeResponse getSummary should work correctly") - void testBedrockInvokeResponseGetSummary() { + @Nested + @DisplayName("Type Tests") + class TypeTests { + + @Test + @DisplayName("Model ID constants should be defined") + void testModelIdConstants() { + assertThat(BedrockInterceptor.CLAUDE_3_OPUS) + .isEqualTo("anthropic.claude-3-opus-20240229-v1:0"); + assertThat(BedrockInterceptor.CLAUDE_3_SONNET) + .isEqualTo("anthropic.claude-3-sonnet-20240229-v1:0"); + assertThat(BedrockInterceptor.CLAUDE_3_HAIKU) + .isEqualTo("anthropic.claude-3-haiku-20240307-v1:0"); + assertThat(BedrockInterceptor.CLAUDE_2).isEqualTo("anthropic.claude-v2:1"); + assertThat(BedrockInterceptor.TITAN_TEXT_EXPRESS).isEqualTo("amazon.titan-text-express-v1"); + assertThat(BedrockInterceptor.TITAN_TEXT_LITE).isEqualTo("amazon.titan-text-lite-v1"); + assertThat(BedrockInterceptor.LLAMA2_70B).isEqualTo("meta.llama2-70b-chat-v1"); + assertThat(BedrockInterceptor.LLAMA3_70B).isEqualTo("meta.llama3-70b-instruct-v1:0"); + } + + @Test + @DisplayName("ClaudeMessage factory methods should work correctly") + void testClaudeMessageFactory() { + ClaudeMessage user = ClaudeMessage.user("User message"); + assertThat(user.getRole()).isEqualTo("user"); + assertThat(user.getContent()).isEqualTo("User message"); + + ClaudeMessage assistant = ClaudeMessage.assistant("Assistant reply"); + assertThat(assistant.getRole()).isEqualTo("assistant"); + assertThat(assistant.getContent()).isEqualTo("Assistant reply"); + } + + @Test + @DisplayName("ClaudeMessage constructor and setters") + void testClaudeMessageConstructorSetters() { + ClaudeMessage message = new ClaudeMessage(); + message.setRole("user"); + message.setContent("Hello"); + + assertThat(message.getRole()).isEqualTo("user"); + assertThat(message.getContent()).isEqualTo("Hello"); + + // Test constructor with role and content + ClaudeMessage message2 = new ClaudeMessage("assistant", "Response"); + assertThat(message2.getRole()).isEqualTo("assistant"); + assertThat(message2.getContent()).isEqualTo("Response"); + } + + @Test + @DisplayName("BedrockInvokeRequest forClaude should work correctly") + void testBedrockInvokeRequestForClaude() { + List messages = + List.of(ClaudeMessage.user("Hello"), ClaudeMessage.assistant("Hi there!")); + + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude(BedrockInterceptor.CLAUDE_3_SONNET, messages, 1024); + + assertThat(request.getModelId()).isEqualTo("anthropic.claude-3-sonnet-20240229-v1:0"); + assertThat(request.getMessages()).hasSize(2); + } + + @Test + @DisplayName("BedrockInvokeRequest forTitan should work correctly") + void testBedrockInvokeRequestForTitan() { + BedrockInvokeRequest request = + BedrockInvokeRequest.forTitan( + BedrockInterceptor.TITAN_TEXT_EXPRESS, "Generate some text"); + + assertThat(request.getModelId()).isEqualTo("amazon.titan-text-express-v1"); + assertThat(request.getInputText()).isEqualTo("Generate some text"); + } + + @Test + @DisplayName("BedrockInvokeRequest extractPrompt should handle Claude messages") + void testBedrockInvokeRequestExtractPromptClaude() { + List messages = + List.of(ClaudeMessage.user("First message"), ClaudeMessage.user("Second message")); + + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude(BedrockInterceptor.CLAUDE_3_HAIKU, messages, 500); + + String prompt = request.extractPrompt(); + assertThat(prompt).contains("First message"); + assertThat(prompt).contains("Second message"); + } + + @Test + @DisplayName("BedrockInvokeRequest extractPrompt should handle Titan inputText") + void testBedrockInvokeRequestExtractPromptTitan() { + BedrockInvokeRequest request = + BedrockInvokeRequest.forTitan(BedrockInterceptor.TITAN_TEXT_LITE, "Titan prompt"); + + assertThat(request.extractPrompt()).isEqualTo("Titan prompt"); + } + + @Test + @DisplayName("BedrockInvokeRequest extractPrompt should handle empty state") + void testBedrockInvokeRequestExtractPromptEmpty() { + BedrockInvokeRequest request = new BedrockInvokeRequest(); + assertThat(request.extractPrompt()).isEmpty(); + } + + @Test + @DisplayName("BedrockInvokeRequest setters should work correctly") + void testBedrockInvokeRequestSetters() { + BedrockInvokeRequest request = new BedrockInvokeRequest(); + request.setModelId("test-model"); + request.setBody("{\"test\":true}"); + request.setContentType("application/json"); + request.setAccept("application/json"); + request.setMessages(List.of(ClaudeMessage.user("Test"))); + request.setInputText("Test input"); + + assertThat(request.getModelId()).isEqualTo("test-model"); + assertThat(request.getBody()).isEqualTo("{\"test\":true}"); + assertThat(request.getContentType()).isEqualTo("application/json"); + assertThat(request.getAccept()).isEqualTo("application/json"); + assertThat(request.getMessages()).hasSize(1); + assertThat(request.getInputText()).isEqualTo("Test input"); + } + + @Test + @DisplayName("BedrockInvokeResponse getSummary should work correctly") + void testBedrockInvokeResponseGetSummary() { + BedrockInvokeResponse response = new BedrockInvokeResponse(); + response.setResponseText("Short response"); + + assertThat(response.getSummary()).isEqualTo("Short response"); + } + + @Test + @DisplayName("BedrockInvokeResponse getSummary should truncate long text") + void testBedrockInvokeResponseGetSummaryTruncate() { + BedrockInvokeResponse response = new BedrockInvokeResponse(); + response.setResponseText("A".repeat(150)); + + String summary = response.getSummary(); + assertThat(summary).hasSize(103); // 100 + "..." + assertThat(summary).endsWith("..."); + } + + @Test + @DisplayName("BedrockInvokeResponse getSummary should handle empty/null") + void testBedrockInvokeResponseGetSummaryEmpty() { + BedrockInvokeResponse response = new BedrockInvokeResponse(); + assertThat(response.getSummary()).isEmpty(); + + response.setResponseText(""); + assertThat(response.getSummary()).isEmpty(); + } + + @Test + @DisplayName("BedrockInvokeResponse setters should work correctly") + void testBedrockInvokeResponseSetters() { + BedrockInvokeResponse response = new BedrockInvokeResponse(); + response.setBody(new byte[] {1, 2, 3}); + response.setContentType("application/json"); + response.setResponseText("Response text"); + response.setInputTokens(100); + response.setOutputTokens(50); + + assertThat(response.getBody()).hasSize(3); + assertThat(response.getContentType()).isEqualTo("application/json"); + assertThat(response.getResponseText()).isEqualTo("Response text"); + assertThat(response.getInputTokens()).isEqualTo(100); + assertThat(response.getOutputTokens()).isEqualTo(50); + } + } + + @Nested + @WireMockTest + @DisplayName("Integration Tests") + class IntegrationTests { + + private AxonFlow axonflow; + private BedrockInterceptor interceptor; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + interceptor = new BedrockInterceptor(axonflow, "test-user"); + } + + @Test + @DisplayName("Constructor should reject null AxonFlow") + void testConstructorRejectsNullAxonFlow() { + assertThatThrownBy(() -> new BedrockInterceptor(null, "user")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("axonflow cannot be null"); + } + + @Test + @DisplayName("Constructor should reject null userToken") + void testConstructorRejectsNullUserToken() { + assertThatThrownBy(() -> new BedrockInterceptor(axonflow, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("Constructor should reject empty userToken") + void testConstructorRejectsEmptyUserToken() { + assertThatThrownBy(() -> new BedrockInterceptor(axonflow, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("wrap should allow request when not blocked") + void testWrapAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock Bedrock call + Function mockCall = + request -> { BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("Short response"); + response.setResponseText("Hello from Bedrock!"); + response.setInputTokens(10); + response.setOutputTokens(5); + return response; + }; + + // Create request + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude( + BedrockInterceptor.CLAUDE_3_SONNET, List.of(ClaudeMessage.user("Hello!")), 1024); - assertThat(response.getSummary()).isEqualTo("Short response"); - } + // Execute wrapped call + BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - @Test - @DisplayName("BedrockInvokeResponse getSummary should truncate long text") - void testBedrockInvokeResponseGetSummaryTruncate() { + // Verify + assertThat(response).isNotNull(); + assertThat(response.getResponseText()).isEqualTo("Hello from Bedrock!"); + + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrap should throw when blocked by policy") + void testWrapBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation: no-pii\"}"))); + + Function mockCall = + request -> { + fail("Bedrock call should not be made when blocked"); + return null; + }; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forTitan(BedrockInterceptor.TITAN_TEXT_EXPRESS, "Blocked content"); + + assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("no-pii"); + } + + @Test + @DisplayName("wrap should work with Titan model") + void testWrapWithTitanModel() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-titan\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + Function mockCall = + request -> { BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("A".repeat(150)); + response.setResponseText("Titan response"); + return response; + }; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forTitan(BedrockInterceptor.TITAN_TEXT_EXPRESS, "Hello Titan"); + + BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - String summary = response.getSummary(); - assertThat(summary).hasSize(103); // 100 + "..." - assertThat(summary).endsWith("..."); - } + assertThat(response).isNotNull(); + assertThat(response.getResponseText()).isEqualTo("Titan response"); + } - @Test - @DisplayName("BedrockInvokeResponse getSummary should handle empty/null") - void testBedrockInvokeResponseGetSummaryEmpty() { + @Test + @DisplayName("wrapAsync should allow request when not blocked") + void testWrapAsyncAllowedRequest() throws Exception { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-async\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock async Bedrock call + Function> mockCall = + request -> { BedrockInvokeResponse response = new BedrockInvokeResponse(); - assertThat(response.getSummary()).isEmpty(); + response.setResponseText("Async Bedrock response"); + response.setInputTokens(15); + response.setOutputTokens(25); + return CompletableFuture.completedFuture(response); + }; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude( + BedrockInterceptor.CLAUDE_3_HAIKU, List.of(ClaudeMessage.user("Async test")), 512); + + BedrockInvokeResponse response = interceptor.wrapAsync(mockCall).apply(request).get(); + + assertThat(response).isNotNull(); + assertThat(response.getResponseText()).isEqualTo("Async Bedrock response"); + } - response.setResponseText(""); - assertThat(response.getSummary()).isEmpty(); - } + @Test + @DisplayName("wrapAsync should throw when blocked by policy") + void testWrapAsyncBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Async policy violation\"}"))); + + Function> mockCall = + request -> { + fail("Bedrock call should not be made when blocked"); + return null; + }; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude( + BedrockInterceptor.CLAUDE_3_OPUS, List.of(ClaudeMessage.user("Blocked async")), 1024); + + // Execute wrapped async call - should return failed future or throw + try { + CompletableFuture future = + interceptor.wrapAsync(mockCall).apply(request); + + // If we get a future, it should be failed + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(PolicyViolationException.class); + } catch (PolicyViolationException e) { + // Some implementations may throw directly + assertThat(e.getMessage()).contains("Async policy violation"); + } + } + + @Test + @DisplayName("wrap should handle null response") + void testWrapNullResponse() { + // Stub policy check - allowed with no plan_id + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + Function mockCall = request -> null; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forTitan(BedrockInterceptor.TITAN_TEXT_LITE, "Test"); + + BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); + assertThat(response).isNull(); + } - @Test - @DisplayName("BedrockInvokeResponse setters should work correctly") - void testBedrockInvokeResponseSetters() { + @Test + @DisplayName("wrap should handle response with long summary") + void testWrapLongResponseSummary() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + Function mockCall = + request -> { BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setBody(new byte[]{1, 2, 3}); - response.setContentType("application/json"); - response.setResponseText("Response text"); - response.setInputTokens(100); - response.setOutputTokens(50); - - assertThat(response.getBody()).hasSize(3); - assertThat(response.getContentType()).isEqualTo("application/json"); - assertThat(response.getResponseText()).isEqualTo("Response text"); - assertThat(response.getInputTokens()).isEqualTo(100); - assertThat(response.getOutputTokens()).isEqualTo(50); - } + response.setResponseText("X".repeat(200)); + return response; + }; + + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude( + BedrockInterceptor.CLAUDE_2, List.of(ClaudeMessage.user("Test")), 100); + + BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getResponseText()).hasSize(200); + assertThat(response.getSummary()).hasSize(103); // truncated } - @Nested - @WireMockTest - @DisplayName("Integration Tests") - class IntegrationTests { - - private AxonFlow axonflow; - private BedrockInterceptor interceptor; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - interceptor = new BedrockInterceptor(axonflow, "test-user"); - } - - @Test - @DisplayName("Constructor should reject null AxonFlow") - void testConstructorRejectsNullAxonFlow() { - assertThatThrownBy(() -> new BedrockInterceptor(null, "user")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("axonflow cannot be null"); - } - - @Test - @DisplayName("Constructor should reject null userToken") - void testConstructorRejectsNullUserToken() { - assertThatThrownBy(() -> new BedrockInterceptor(axonflow, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("Constructor should reject empty userToken") - void testConstructorRejectsEmptyUserToken() { - assertThatThrownBy(() -> new BedrockInterceptor(axonflow, "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("wrap should allow request when not blocked") - void testWrapAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock Bedrock call - Function mockCall = request -> { - BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("Hello from Bedrock!"); - response.setInputTokens(10); - response.setOutputTokens(5); - return response; - }; - - // Create request - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_3_SONNET, - List.of(ClaudeMessage.user("Hello!")), - 1024 - ); - - // Execute wrapped call - BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getResponseText()).isEqualTo("Hello from Bedrock!"); - - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrap should throw when blocked by policy") - void testWrapBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation: no-pii\"}"))); - - Function mockCall = request -> { - fail("Bedrock call should not be made when blocked"); - return null; - }; - - BedrockInvokeRequest request = BedrockInvokeRequest.forTitan( - BedrockInterceptor.TITAN_TEXT_EXPRESS, - "Blocked content" - ); - - assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("no-pii"); - } - - @Test - @DisplayName("wrap should work with Titan model") - void testWrapWithTitanModel() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-titan\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - Function mockCall = request -> { - BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("Titan response"); - return response; - }; - - BedrockInvokeRequest request = BedrockInvokeRequest.forTitan( - BedrockInterceptor.TITAN_TEXT_EXPRESS, - "Hello Titan" - ); - - BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getResponseText()).isEqualTo("Titan response"); - } - - @Test - @DisplayName("wrapAsync should allow request when not blocked") - void testWrapAsyncAllowedRequest() throws Exception { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-async\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock async Bedrock call - Function> mockCall = request -> { - BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("Async Bedrock response"); - response.setInputTokens(15); - response.setOutputTokens(25); - return CompletableFuture.completedFuture(response); - }; - - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_3_HAIKU, - List.of(ClaudeMessage.user("Async test")), - 512 - ); - - BedrockInvokeResponse response = interceptor.wrapAsync(mockCall) - .apply(request) - .get(); - - assertThat(response).isNotNull(); - assertThat(response.getResponseText()).isEqualTo("Async Bedrock response"); - } - - @Test - @DisplayName("wrapAsync should throw when blocked by policy") - void testWrapAsyncBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Async policy violation\"}"))); - - Function> mockCall = request -> { - fail("Bedrock call should not be made when blocked"); - return null; - }; - - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_3_OPUS, - List.of(ClaudeMessage.user("Blocked async")), - 1024 - ); - - // Execute wrapped async call - should return failed future or throw - try { - CompletableFuture future = interceptor.wrapAsync(mockCall) - .apply(request); - - // If we get a future, it should be failed - assertThatThrownBy(future::get) - .isInstanceOf(ExecutionException.class) - .hasCauseInstanceOf(PolicyViolationException.class); - } catch (PolicyViolationException e) { - // Some implementations may throw directly - assertThat(e.getMessage()).contains("Async policy violation"); - } - } - - @Test - @DisplayName("wrap should handle null response") - void testWrapNullResponse() { - // Stub policy check - allowed with no plan_id - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - Function mockCall = request -> null; - - BedrockInvokeRequest request = BedrockInvokeRequest.forTitan( - BedrockInterceptor.TITAN_TEXT_LITE, - "Test" - ); - - BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - assertThat(response).isNull(); - } - - @Test - @DisplayName("wrap should handle response with long summary") - void testWrapLongResponseSummary() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - Function mockCall = request -> { - BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("X".repeat(200)); - return response; - }; - - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.CLAUDE_2, - List.of(ClaudeMessage.user("Test")), - 100 - ); - - BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getResponseText()).hasSize(200); - assertThat(response.getSummary()).hasSize(103); // truncated - } - - @Test - @DisplayName("wrap should work with Llama models") - void testWrapWithLlamaModel() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-llama\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - Function mockCall = request -> { - BedrockInvokeResponse response = new BedrockInvokeResponse(); - response.setResponseText("Llama response"); - return response; - }; - - // Llama uses same message format as Claude in Bedrock - BedrockInvokeRequest request = BedrockInvokeRequest.forClaude( - BedrockInterceptor.LLAMA3_70B, - List.of(ClaudeMessage.user("Hello Llama")), - 1024 - ); - - BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getResponseText()).isEqualTo("Llama response"); - } + @Test + @DisplayName("wrap should work with Llama models") + void testWrapWithLlamaModel() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-llama\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + Function mockCall = + request -> { + BedrockInvokeResponse response = new BedrockInvokeResponse(); + response.setResponseText("Llama response"); + return response; + }; + + // Llama uses same message format as Claude in Bedrock + BedrockInvokeRequest request = + BedrockInvokeRequest.forClaude( + BedrockInterceptor.LLAMA3_70B, List.of(ClaudeMessage.user("Hello Llama")), 1024); + + BedrockInvokeResponse response = interceptor.wrap(mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getResponseText()).isEqualTo("Llama response"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/interceptors/GeminiInterceptorTest.java b/src/test/java/com/getaxonflow/sdk/interceptors/GeminiInterceptorTest.java index 6ea5844..75f1d26 100644 --- a/src/test/java/com/getaxonflow/sdk/interceptors/GeminiInterceptorTest.java +++ b/src/test/java/com/getaxonflow/sdk/interceptors/GeminiInterceptorTest.java @@ -6,472 +6,486 @@ */ package com.getaxonflow.sdk.interceptors; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.AxonFlowConfig; import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.getaxonflow.sdk.interceptors.GeminiInterceptor.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("Gemini Interceptor") class GeminiInterceptorTest { - @Nested - @DisplayName("Type Tests") - class TypeTests { - - @Test - @DisplayName("GeminiRequest create should work correctly") - void testGeminiRequestCreate() { - GeminiRequest request = GeminiRequest.create("gemini-pro", "Hello, world!"); - - assertThat(request.getModel()).isEqualTo("gemini-pro"); - assertThat(request.getContents()).hasSize(1); - assertThat(request.extractPrompt()).isEqualTo("Hello, world!"); - } - - @Test - @DisplayName("GeminiRequest extractPrompt should handle empty contents") - void testGeminiRequestExtractPromptEmpty() { - GeminiRequest request = new GeminiRequest(); - assertThat(request.extractPrompt()).isEmpty(); - } - - @Test - @DisplayName("GeminiRequest extractPrompt should concatenate multiple parts") - void testGeminiRequestExtractPromptMultipleParts() { - GeminiRequest request = new GeminiRequest(); - request.setModel("gemini-pro"); - - List contents = new ArrayList<>(); - contents.add(Content.text("First part")); - contents.add(Content.text("Second part")); - request.setContents(contents); - - String prompt = request.extractPrompt(); - assertThat(prompt).contains("First part"); - assertThat(prompt).contains("Second part"); - } - - @Test - @DisplayName("GeminiRequest with GenerationConfig") - void testGeminiRequestWithGenerationConfig() { - GeminiRequest request = new GeminiRequest(); - request.setModel("gemini-pro"); - - GenerationConfig config = new GenerationConfig(); - config.setTemperature(0.7); - config.setTopP(0.9); - config.setTopK(40); - config.setMaxOutputTokens(1024); - config.setStopSequences(List.of("END")); - request.setGenerationConfig(config); - - assertThat(request.getGenerationConfig()).isNotNull(); - assertThat(request.getGenerationConfig().getTemperature()).isEqualTo(0.7); - assertThat(request.getGenerationConfig().getTopP()).isEqualTo(0.9); - assertThat(request.getGenerationConfig().getTopK()).isEqualTo(40); - assertThat(request.getGenerationConfig().getMaxOutputTokens()).isEqualTo(1024); - assertThat(request.getGenerationConfig().getStopSequences()).containsExactly("END"); - } - - @Test - @DisplayName("Content text factory method should work correctly") - void testContentTextFactory() { - Content content = Content.text("Test message"); - - assertThat(content.getRole()).isEqualTo("user"); - assertThat(content.getParts()).hasSize(1); - assertThat(content.getParts().get(0).getText()).isEqualTo("Test message"); - } - - @Test - @DisplayName("Content setters should work correctly") - void testContentSetters() { - Content content = new Content(); - content.setRole("assistant"); - - List parts = new ArrayList<>(); - parts.add(Part.text("Response")); - content.setParts(parts); - - assertThat(content.getRole()).isEqualTo("assistant"); - assertThat(content.getParts()).hasSize(1); - } - - @Test - @DisplayName("Part text factory method should work correctly") - void testPartTextFactory() { - Part part = Part.text("Hello"); - assertThat(part.getText()).isEqualTo("Hello"); - assertThat(part.getInlineData()).isNull(); - } - - @Test - @DisplayName("Part with InlineData") - void testPartWithInlineData() { - Part part = new Part(); - InlineData inlineData = new InlineData(); - inlineData.setMimeType("image/png"); - inlineData.setData("base64data"); - part.setInlineData(inlineData); - part.setText(null); - - assertThat(part.getText()).isNull(); - assertThat(part.getInlineData()).isNotNull(); - assertThat(part.getInlineData().getMimeType()).isEqualTo("image/png"); - assertThat(part.getInlineData().getData()).isEqualTo("base64data"); - } - - @Test - @DisplayName("GeminiResponse getText should extract first candidate text") - void testGeminiResponseGetText() { - GeminiResponse response = new GeminiResponse(); + @Nested + @DisplayName("Type Tests") + class TypeTests { - Candidate candidate = new Candidate(); - Content content = new Content(); - List parts = new ArrayList<>(); - parts.add(Part.text("Response text")); - content.setParts(parts); - candidate.setContent(content); - candidate.setFinishReason("STOP"); + @Test + @DisplayName("GeminiRequest create should work correctly") + void testGeminiRequestCreate() { + GeminiRequest request = GeminiRequest.create("gemini-pro", "Hello, world!"); - response.setCandidates(List.of(candidate)); + assertThat(request.getModel()).isEqualTo("gemini-pro"); + assertThat(request.getContents()).hasSize(1); + assertThat(request.extractPrompt()).isEqualTo("Hello, world!"); + } - assertThat(response.getText()).isEqualTo("Response text"); - } + @Test + @DisplayName("GeminiRequest extractPrompt should handle empty contents") + void testGeminiRequestExtractPromptEmpty() { + GeminiRequest request = new GeminiRequest(); + assertThat(request.extractPrompt()).isEmpty(); + } - @Test - @DisplayName("GeminiResponse getText should handle empty candidates") - void testGeminiResponseGetTextEmpty() { - GeminiResponse response = new GeminiResponse(); - assertThat(response.getText()).isEmpty(); + @Test + @DisplayName("GeminiRequest extractPrompt should concatenate multiple parts") + void testGeminiRequestExtractPromptMultipleParts() { + GeminiRequest request = new GeminiRequest(); + request.setModel("gemini-pro"); - response.setCandidates(new ArrayList<>()); - assertThat(response.getText()).isEmpty(); - } + List contents = new ArrayList<>(); + contents.add(Content.text("First part")); + contents.add(Content.text("Second part")); + request.setContents(contents); - @Test - @DisplayName("GeminiResponse getText should handle null content") - void testGeminiResponseGetTextNullContent() { - GeminiResponse response = new GeminiResponse(); - Candidate candidate = new Candidate(); - candidate.setContent(null); - response.setCandidates(List.of(candidate)); + String prompt = request.extractPrompt(); + assertThat(prompt).contains("First part"); + assertThat(prompt).contains("Second part"); + } - assertThat(response.getText()).isEmpty(); - } + @Test + @DisplayName("GeminiRequest with GenerationConfig") + void testGeminiRequestWithGenerationConfig() { + GeminiRequest request = new GeminiRequest(); + request.setModel("gemini-pro"); + + GenerationConfig config = new GenerationConfig(); + config.setTemperature(0.7); + config.setTopP(0.9); + config.setTopK(40); + config.setMaxOutputTokens(1024); + config.setStopSequences(List.of("END")); + request.setGenerationConfig(config); + + assertThat(request.getGenerationConfig()).isNotNull(); + assertThat(request.getGenerationConfig().getTemperature()).isEqualTo(0.7); + assertThat(request.getGenerationConfig().getTopP()).isEqualTo(0.9); + assertThat(request.getGenerationConfig().getTopK()).isEqualTo(40); + assertThat(request.getGenerationConfig().getMaxOutputTokens()).isEqualTo(1024); + assertThat(request.getGenerationConfig().getStopSequences()).containsExactly("END"); + } - @Test - @DisplayName("GeminiResponse getSummary should truncate long text") - void testGeminiResponseGetSummary() { - GeminiResponse response = new GeminiResponse(); + @Test + @DisplayName("Content text factory method should work correctly") + void testContentTextFactory() { + Content content = Content.text("Test message"); - Candidate candidate = new Candidate(); - Content content = new Content(); - List parts = new ArrayList<>(); - parts.add(Part.text("A".repeat(150))); - content.setParts(parts); - candidate.setContent(content); - response.setCandidates(List.of(candidate)); + assertThat(content.getRole()).isEqualTo("user"); + assertThat(content.getParts()).hasSize(1); + assertThat(content.getParts().get(0).getText()).isEqualTo("Test message"); + } + + @Test + @DisplayName("Content setters should work correctly") + void testContentSetters() { + Content content = new Content(); + content.setRole("assistant"); + + List parts = new ArrayList<>(); + parts.add(Part.text("Response")); + content.setParts(parts); + + assertThat(content.getRole()).isEqualTo("assistant"); + assertThat(content.getParts()).hasSize(1); + } + + @Test + @DisplayName("Part text factory method should work correctly") + void testPartTextFactory() { + Part part = Part.text("Hello"); + assertThat(part.getText()).isEqualTo("Hello"); + assertThat(part.getInlineData()).isNull(); + } + + @Test + @DisplayName("Part with InlineData") + void testPartWithInlineData() { + Part part = new Part(); + InlineData inlineData = new InlineData(); + inlineData.setMimeType("image/png"); + inlineData.setData("base64data"); + part.setInlineData(inlineData); + part.setText(null); + + assertThat(part.getText()).isNull(); + assertThat(part.getInlineData()).isNotNull(); + assertThat(part.getInlineData().getMimeType()).isEqualTo("image/png"); + assertThat(part.getInlineData().getData()).isEqualTo("base64data"); + } + + @Test + @DisplayName("GeminiResponse getText should extract first candidate text") + void testGeminiResponseGetText() { + GeminiResponse response = new GeminiResponse(); + + Candidate candidate = new Candidate(); + Content content = new Content(); + List parts = new ArrayList<>(); + parts.add(Part.text("Response text")); + content.setParts(parts); + candidate.setContent(content); + candidate.setFinishReason("STOP"); + + response.setCandidates(List.of(candidate)); - String summary = response.getSummary(); - assertThat(summary).hasSize(103); // 100 + "..." - assertThat(summary).endsWith("..."); - } + assertThat(response.getText()).isEqualTo("Response text"); + } + + @Test + @DisplayName("GeminiResponse getText should handle empty candidates") + void testGeminiResponseGetTextEmpty() { + GeminiResponse response = new GeminiResponse(); + assertThat(response.getText()).isEmpty(); + + response.setCandidates(new ArrayList<>()); + assertThat(response.getText()).isEmpty(); + } + + @Test + @DisplayName("GeminiResponse getText should handle null content") + void testGeminiResponseGetTextNullContent() { + GeminiResponse response = new GeminiResponse(); + Candidate candidate = new Candidate(); + candidate.setContent(null); + response.setCandidates(List.of(candidate)); + + assertThat(response.getText()).isEmpty(); + } + + @Test + @DisplayName("GeminiResponse getSummary should truncate long text") + void testGeminiResponseGetSummary() { + GeminiResponse response = new GeminiResponse(); + + Candidate candidate = new Candidate(); + Content content = new Content(); + List parts = new ArrayList<>(); + parts.add(Part.text("A".repeat(150))); + content.setParts(parts); + candidate.setContent(content); + response.setCandidates(List.of(candidate)); + + String summary = response.getSummary(); + assertThat(summary).hasSize(103); // 100 + "..." + assertThat(summary).endsWith("..."); + } + + @Test + @DisplayName("GeminiResponse with UsageMetadata") + void testGeminiResponseWithUsageMetadata() { + GeminiResponse response = new GeminiResponse(); + + UsageMetadata metadata = new UsageMetadata(); + metadata.setPromptTokenCount(100); + metadata.setCandidatesTokenCount(50); + metadata.setTotalTokenCount(150); + response.setUsageMetadata(metadata); + + assertThat(response.getPromptTokenCount()).isEqualTo(100); + assertThat(response.getCandidatesTokenCount()).isEqualTo(50); + assertThat(response.getTotalTokenCount()).isEqualTo(150); + } + + @Test + @DisplayName("GeminiResponse token counts should be 0 when no metadata") + void testGeminiResponseNoMetadata() { + GeminiResponse response = new GeminiResponse(); + + assertThat(response.getPromptTokenCount()).isZero(); + assertThat(response.getCandidatesTokenCount()).isZero(); + assertThat(response.getTotalTokenCount()).isZero(); + } + + @Test + @DisplayName("Candidate getters and setters") + void testCandidateGettersSetters() { + Candidate candidate = new Candidate(); + Content content = Content.text("Test"); + candidate.setContent(content); + candidate.setFinishReason("STOP"); + + assertThat(candidate.getContent()).isEqualTo(content); + assertThat(candidate.getFinishReason()).isEqualTo("STOP"); + } + } + + @Nested + @WireMockTest + @DisplayName("Integration Tests") + class IntegrationTests { + + private AxonFlow axonflow; + private GeminiInterceptor interceptor; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + interceptor = new GeminiInterceptor(axonflow, "test-user"); + } + + @Test + @DisplayName("Constructor should reject null AxonFlow") + void testConstructorRejectsNullAxonFlow() { + assertThatThrownBy(() -> new GeminiInterceptor(null, "user")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("axonflow cannot be null"); + } - @Test - @DisplayName("GeminiResponse with UsageMetadata") - void testGeminiResponseWithUsageMetadata() { + @Test + @DisplayName("Constructor should reject null userToken") + void testConstructorRejectsNullUserToken() { + assertThatThrownBy(() -> new GeminiInterceptor(axonflow, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("Constructor should reject empty userToken") + void testConstructorRejectsEmptyUserToken() { + assertThatThrownBy(() -> new GeminiInterceptor(axonflow, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("wrap should allow request when not blocked") + void testWrapAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock Gemini call + Function mockCall = + request -> { GeminiResponse response = new GeminiResponse(); + Candidate candidate = new Candidate(); + Content content = Content.text("Hello from Gemini!"); + candidate.setContent(content); + response.setCandidates(List.of(candidate)); UsageMetadata metadata = new UsageMetadata(); - metadata.setPromptTokenCount(100); - metadata.setCandidatesTokenCount(50); - metadata.setTotalTokenCount(150); + metadata.setPromptTokenCount(10); + metadata.setCandidatesTokenCount(5); response.setUsageMetadata(metadata); - assertThat(response.getPromptTokenCount()).isEqualTo(100); - assertThat(response.getCandidatesTokenCount()).isEqualTo(50); - assertThat(response.getTotalTokenCount()).isEqualTo(150); - } + return response; + }; - @Test - @DisplayName("GeminiResponse token counts should be 0 when no metadata") - void testGeminiResponseNoMetadata() { - GeminiResponse response = new GeminiResponse(); + // Create request + GeminiRequest request = GeminiRequest.create("gemini-pro", "Hello!"); + + // Execute wrapped call + GeminiResponse response = interceptor.wrap(mockCall).apply(request); - assertThat(response.getPromptTokenCount()).isZero(); - assertThat(response.getCandidatesTokenCount()).isZero(); - assertThat(response.getTotalTokenCount()).isZero(); - } + // Verify + assertThat(response).isNotNull(); + assertThat(response.getText()).isEqualTo("Hello from Gemini!"); + + // Verify API was called + verify(postRequestedFor(urlEqualTo("/api/request"))); + } - @Test - @DisplayName("Candidate getters and setters") - void testCandidateGettersSetters() { + @Test + @DisplayName("wrap should throw when blocked by policy") + void testWrapBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: no-pii\"}"))); + + // Create mock Gemini call (should not be called) + Function mockCall = + request -> { + fail("Gemini call should not be made when blocked"); + return null; + }; + + // Create request + GeminiRequest request = GeminiRequest.create("gemini-pro", "Tell me about SSN"); + + // Execute wrapped call + assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("no-pii"); + } + + @Test + @DisplayName("wrap should include generation config in context") + void testWrapWithGenerationConfig() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create request with generation config + GeminiRequest request = new GeminiRequest(); + request.setModel("gemini-pro"); + request.setContents(List.of(Content.text("Hello"))); + + GenerationConfig config = new GenerationConfig(); + config.setTemperature(0.5); + config.setMaxOutputTokens(500); + request.setGenerationConfig(config); + + // Create mock call + Function mockCall = req -> new GeminiResponse(); + + // Execute + GeminiResponse response = interceptor.wrap(mockCall).apply(request); + + assertThat(response).isNotNull(); + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrapAsync should allow request when not blocked") + void testWrapAsyncAllowedRequest() throws Exception { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock async Gemini call + Function> mockCall = + request -> { + GeminiResponse response = new GeminiResponse(); Candidate candidate = new Candidate(); - Content content = Content.text("Test"); + Content content = Content.text("Async response"); candidate.setContent(content); - candidate.setFinishReason("STOP"); + response.setCandidates(List.of(candidate)); + return CompletableFuture.completedFuture(response); + }; + + // Create request + GeminiRequest request = GeminiRequest.create("gemini-pro", "Async test"); + + // Execute wrapped async call + GeminiResponse response = interceptor.wrapAsync(mockCall).apply(request).get(); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getText()).isEqualTo("Async response"); + } - assertThat(candidate.getContent()).isEqualTo(content); - assertThat(candidate.getFinishReason()).isEqualTo("STOP"); - } + @Test + @DisplayName("wrapAsync should throw when blocked by policy") + void testWrapAsyncBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Content policy violation\"}"))); + + // Create mock async Gemini call (should not be called) + Function> mockCall = + request -> { + fail("Gemini call should not be made when blocked"); + return null; + }; + + // Create request + GeminiRequest request = GeminiRequest.create("gemini-pro", "Blocked content"); + + // Execute wrapped async call - should return failed future or throw + try { + CompletableFuture future = interceptor.wrapAsync(mockCall).apply(request); + + // If we get a future, it should be failed + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(PolicyViolationException.class); + } catch (PolicyViolationException e) { + // Some implementations may throw directly + assertThat(e.getMessage()).contains("Content policy violation"); + } } - @Nested - @WireMockTest - @DisplayName("Integration Tests") - class IntegrationTests { - - private AxonFlow axonflow; - private GeminiInterceptor interceptor; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - interceptor = new GeminiInterceptor(axonflow, "test-user"); - } - - @Test - @DisplayName("Constructor should reject null AxonFlow") - void testConstructorRejectsNullAxonFlow() { - assertThatThrownBy(() -> new GeminiInterceptor(null, "user")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("axonflow cannot be null"); - } - - @Test - @DisplayName("Constructor should reject null userToken") - void testConstructorRejectsNullUserToken() { - assertThatThrownBy(() -> new GeminiInterceptor(axonflow, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("Constructor should reject empty userToken") - void testConstructorRejectsEmptyUserToken() { - assertThatThrownBy(() -> new GeminiInterceptor(axonflow, "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("wrap should allow request when not blocked") - void testWrapAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock Gemini call - Function mockCall = request -> { - GeminiResponse response = new GeminiResponse(); - Candidate candidate = new Candidate(); - Content content = Content.text("Hello from Gemini!"); - candidate.setContent(content); - response.setCandidates(List.of(candidate)); - - UsageMetadata metadata = new UsageMetadata(); - metadata.setPromptTokenCount(10); - metadata.setCandidatesTokenCount(5); - response.setUsageMetadata(metadata); - - return response; - }; - - // Create request - GeminiRequest request = GeminiRequest.create("gemini-pro", "Hello!"); - - // Execute wrapped call - GeminiResponse response = interceptor.wrap(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getText()).isEqualTo("Hello from Gemini!"); - - // Verify API was called - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrap should throw when blocked by policy") - void testWrapBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: no-pii\"}"))); - - // Create mock Gemini call (should not be called) - Function mockCall = request -> { - fail("Gemini call should not be made when blocked"); - return null; - }; - - // Create request - GeminiRequest request = GeminiRequest.create("gemini-pro", "Tell me about SSN"); - - // Execute wrapped call - assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("no-pii"); - } - - @Test - @DisplayName("wrap should include generation config in context") - void testWrapWithGenerationConfig() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create request with generation config - GeminiRequest request = new GeminiRequest(); - request.setModel("gemini-pro"); - request.setContents(List.of(Content.text("Hello"))); - - GenerationConfig config = new GenerationConfig(); - config.setTemperature(0.5); - config.setMaxOutputTokens(500); - request.setGenerationConfig(config); - - // Create mock call - Function mockCall = req -> new GeminiResponse(); - - // Execute - GeminiResponse response = interceptor.wrap(mockCall).apply(request); - - assertThat(response).isNotNull(); - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrapAsync should allow request when not blocked") - void testWrapAsyncAllowedRequest() throws Exception { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock async Gemini call - Function> mockCall = request -> { - GeminiResponse response = new GeminiResponse(); - Candidate candidate = new Candidate(); - Content content = Content.text("Async response"); - candidate.setContent(content); - response.setCandidates(List.of(candidate)); - return CompletableFuture.completedFuture(response); - }; - - // Create request - GeminiRequest request = GeminiRequest.create("gemini-pro", "Async test"); - - // Execute wrapped async call - GeminiResponse response = interceptor.wrapAsync(mockCall) - .apply(request) - .get(); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getText()).isEqualTo("Async response"); - } - - @Test - @DisplayName("wrapAsync should throw when blocked by policy") - void testWrapAsyncBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Content policy violation\"}"))); - - // Create mock async Gemini call (should not be called) - Function> mockCall = request -> { - fail("Gemini call should not be made when blocked"); - return null; - }; - - // Create request - GeminiRequest request = GeminiRequest.create("gemini-pro", "Blocked content"); - - // Execute wrapped async call - should return failed future or throw - try { - CompletableFuture future = interceptor.wrapAsync(mockCall) - .apply(request); - - // If we get a future, it should be failed - assertThatThrownBy(future::get) - .isInstanceOf(ExecutionException.class) - .hasCauseInstanceOf(PolicyViolationException.class); - } catch (PolicyViolationException e) { - // Some implementations may throw directly - assertThat(e.getMessage()).contains("Content policy violation"); - } - } - - @Test - @DisplayName("wrap should handle null response from LLM") - void testWrapWithNullResponse() { - // Stub policy check - allowed with no plan_id - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - // Create mock call that returns null - Function mockCall = request -> null; - - // Create request - GeminiRequest request = GeminiRequest.create("gemini-pro", "Test"); - - // Execute - should not throw - GeminiResponse response = interceptor.wrap(mockCall).apply(request); - assertThat(response).isNull(); - } + @Test + @DisplayName("wrap should handle null response from LLM") + void testWrapWithNullResponse() { + // Stub policy check - allowed with no plan_id + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + // Create mock call that returns null + Function mockCall = request -> null; + + // Create request + GeminiRequest request = GeminiRequest.create("gemini-pro", "Test"); + + // Execute - should not throw + GeminiResponse response = interceptor.wrap(mockCall).apply(request); + assertThat(response).isNull(); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/interceptors/OllamaInterceptorTest.java b/src/test/java/com/getaxonflow/sdk/interceptors/OllamaInterceptorTest.java index 224198a..766b632 100644 --- a/src/test/java/com/getaxonflow/sdk/interceptors/OllamaInterceptorTest.java +++ b/src/test/java/com/getaxonflow/sdk/interceptors/OllamaInterceptorTest.java @@ -6,566 +6,590 @@ */ package com.getaxonflow.sdk.interceptors; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.AxonFlowConfig; import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.getaxonflow.sdk.interceptors.OllamaInterceptor.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.util.ArrayList; import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("Ollama Interceptor") class OllamaInterceptorTest { - @Nested - @DisplayName("Type Tests") - class TypeTests { - - @Test - @DisplayName("OllamaMessage factory methods should work correctly") - void testOllamaMessageFactory() { - OllamaMessage user = OllamaMessage.user("User message"); - assertThat(user.getRole()).isEqualTo("user"); - assertThat(user.getContent()).isEqualTo("User message"); - - OllamaMessage assistant = OllamaMessage.assistant("Assistant reply"); - assertThat(assistant.getRole()).isEqualTo("assistant"); - assertThat(assistant.getContent()).isEqualTo("Assistant reply"); - - OllamaMessage system = OllamaMessage.system("System prompt"); - assertThat(system.getRole()).isEqualTo("system"); - assertThat(system.getContent()).isEqualTo("System prompt"); - } - - @Test - @DisplayName("OllamaMessage constructor and setters") - void testOllamaMessageConstructorSetters() { - OllamaMessage message = new OllamaMessage(); - message.setRole("user"); - message.setContent("Hello"); - message.setImages(List.of("base64image")); - - assertThat(message.getRole()).isEqualTo("user"); - assertThat(message.getContent()).isEqualTo("Hello"); - assertThat(message.getImages()).containsExactly("base64image"); - - // Test constructor with role and content - OllamaMessage message2 = new OllamaMessage("assistant", "Response"); - assertThat(message2.getRole()).isEqualTo("assistant"); - assertThat(message2.getContent()).isEqualTo("Response"); - } - - @Test - @DisplayName("OllamaChatRequest create should work correctly") - void testOllamaChatRequestCreate() { - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Hello!"); - - assertThat(request.getModel()).isEqualTo("llama2"); - assertThat(request.getMessages()).hasSize(1); - assertThat(request.getMessages().get(0).getRole()).isEqualTo("user"); - assertThat(request.getMessages().get(0).getContent()).isEqualTo("Hello!"); - } - - @Test - @DisplayName("OllamaChatRequest extractPrompt should concatenate messages") - void testOllamaChatRequestExtractPrompt() { - OllamaChatRequest request = new OllamaChatRequest(); - request.setModel("llama2"); - - List messages = new ArrayList<>(); - messages.add(OllamaMessage.system("You are helpful")); - messages.add(OllamaMessage.user("Hello")); - request.setMessages(messages); - - String prompt = request.extractPrompt(); - assertThat(prompt).contains("You are helpful"); - assertThat(prompt).contains("Hello"); - } - - @Test - @DisplayName("OllamaChatRequest extractPrompt should handle empty messages") - void testOllamaChatRequestExtractPromptEmpty() { - OllamaChatRequest request = new OllamaChatRequest(); - assertThat(request.extractPrompt()).isEmpty(); - - request.setMessages(null); - assertThat(request.extractPrompt()).isEmpty(); - } - - @Test - @DisplayName("OllamaChatRequest setters should work correctly") - void testOllamaChatRequestSetters() { - OllamaChatRequest request = new OllamaChatRequest(); - request.setModel("mistral"); - request.setStream(true); - request.setFormat("json"); - - OllamaOptions options = new OllamaOptions(); - options.setTemperature(0.8); - request.setOptions(options); - - assertThat(request.getModel()).isEqualTo("mistral"); - assertThat(request.isStream()).isTrue(); - assertThat(request.getFormat()).isEqualTo("json"); - assertThat(request.getOptions()).isNotNull(); - assertThat(request.getOptions().getTemperature()).isEqualTo(0.8); - } - - @Test - @DisplayName("OllamaOptions setters should work correctly") - void testOllamaOptionsSetters() { - OllamaOptions options = new OllamaOptions(); - options.setTemperature(0.7); - options.setTopP(0.9); - options.setTopK(40); - options.setNumPredict(100); - options.setStop(List.of("END", "STOP")); - - assertThat(options.getTemperature()).isEqualTo(0.7); - assertThat(options.getTopP()).isEqualTo(0.9); - assertThat(options.getTopK()).isEqualTo(40); - assertThat(options.getNumPredict()).isEqualTo(100); - assertThat(options.getStop()).containsExactly("END", "STOP"); - } - - @Test - @DisplayName("OllamaChatResponse setters should work correctly") - void testOllamaChatResponseSetters() { + @Nested + @DisplayName("Type Tests") + class TypeTests { + + @Test + @DisplayName("OllamaMessage factory methods should work correctly") + void testOllamaMessageFactory() { + OllamaMessage user = OllamaMessage.user("User message"); + assertThat(user.getRole()).isEqualTo("user"); + assertThat(user.getContent()).isEqualTo("User message"); + + OllamaMessage assistant = OllamaMessage.assistant("Assistant reply"); + assertThat(assistant.getRole()).isEqualTo("assistant"); + assertThat(assistant.getContent()).isEqualTo("Assistant reply"); + + OllamaMessage system = OllamaMessage.system("System prompt"); + assertThat(system.getRole()).isEqualTo("system"); + assertThat(system.getContent()).isEqualTo("System prompt"); + } + + @Test + @DisplayName("OllamaMessage constructor and setters") + void testOllamaMessageConstructorSetters() { + OllamaMessage message = new OllamaMessage(); + message.setRole("user"); + message.setContent("Hello"); + message.setImages(List.of("base64image")); + + assertThat(message.getRole()).isEqualTo("user"); + assertThat(message.getContent()).isEqualTo("Hello"); + assertThat(message.getImages()).containsExactly("base64image"); + + // Test constructor with role and content + OllamaMessage message2 = new OllamaMessage("assistant", "Response"); + assertThat(message2.getRole()).isEqualTo("assistant"); + assertThat(message2.getContent()).isEqualTo("Response"); + } + + @Test + @DisplayName("OllamaChatRequest create should work correctly") + void testOllamaChatRequestCreate() { + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Hello!"); + + assertThat(request.getModel()).isEqualTo("llama2"); + assertThat(request.getMessages()).hasSize(1); + assertThat(request.getMessages().get(0).getRole()).isEqualTo("user"); + assertThat(request.getMessages().get(0).getContent()).isEqualTo("Hello!"); + } + + @Test + @DisplayName("OllamaChatRequest extractPrompt should concatenate messages") + void testOllamaChatRequestExtractPrompt() { + OllamaChatRequest request = new OllamaChatRequest(); + request.setModel("llama2"); + + List messages = new ArrayList<>(); + messages.add(OllamaMessage.system("You are helpful")); + messages.add(OllamaMessage.user("Hello")); + request.setMessages(messages); + + String prompt = request.extractPrompt(); + assertThat(prompt).contains("You are helpful"); + assertThat(prompt).contains("Hello"); + } + + @Test + @DisplayName("OllamaChatRequest extractPrompt should handle empty messages") + void testOllamaChatRequestExtractPromptEmpty() { + OllamaChatRequest request = new OllamaChatRequest(); + assertThat(request.extractPrompt()).isEmpty(); + + request.setMessages(null); + assertThat(request.extractPrompt()).isEmpty(); + } + + @Test + @DisplayName("OllamaChatRequest setters should work correctly") + void testOllamaChatRequestSetters() { + OllamaChatRequest request = new OllamaChatRequest(); + request.setModel("mistral"); + request.setStream(true); + request.setFormat("json"); + + OllamaOptions options = new OllamaOptions(); + options.setTemperature(0.8); + request.setOptions(options); + + assertThat(request.getModel()).isEqualTo("mistral"); + assertThat(request.isStream()).isTrue(); + assertThat(request.getFormat()).isEqualTo("json"); + assertThat(request.getOptions()).isNotNull(); + assertThat(request.getOptions().getTemperature()).isEqualTo(0.8); + } + + @Test + @DisplayName("OllamaOptions setters should work correctly") + void testOllamaOptionsSetters() { + OllamaOptions options = new OllamaOptions(); + options.setTemperature(0.7); + options.setTopP(0.9); + options.setTopK(40); + options.setNumPredict(100); + options.setStop(List.of("END", "STOP")); + + assertThat(options.getTemperature()).isEqualTo(0.7); + assertThat(options.getTopP()).isEqualTo(0.9); + assertThat(options.getTopK()).isEqualTo(40); + assertThat(options.getNumPredict()).isEqualTo(100); + assertThat(options.getStop()).containsExactly("END", "STOP"); + } + + @Test + @DisplayName("OllamaChatResponse setters should work correctly") + void testOllamaChatResponseSetters() { + OllamaChatResponse response = new OllamaChatResponse(); + response.setModel("llama2"); + response.setCreatedAt("2024-01-15T10:30:00Z"); + response.setMessage(OllamaMessage.assistant("Hello!")); + response.setDone(true); + response.setTotalDuration(1000000000L); + response.setLoadDuration(100000000L); + response.setPromptEvalCount(10); + response.setPromptEvalDuration(50000000L); + response.setEvalCount(20); + response.setEvalDuration(500000000L); + + assertThat(response.getModel()).isEqualTo("llama2"); + assertThat(response.getCreatedAt()).isEqualTo("2024-01-15T10:30:00Z"); + assertThat(response.getMessage().getContent()).isEqualTo("Hello!"); + assertThat(response.isDone()).isTrue(); + assertThat(response.getTotalDuration()).isEqualTo(1000000000L); + assertThat(response.getLoadDuration()).isEqualTo(100000000L); + assertThat(response.getPromptEvalCount()).isEqualTo(10); + assertThat(response.getPromptEvalDuration()).isEqualTo(50000000L); + assertThat(response.getEvalCount()).isEqualTo(20); + assertThat(response.getEvalDuration()).isEqualTo(500000000L); + } + + @Test + @DisplayName("OllamaGenerateRequest create should work correctly") + void testOllamaGenerateRequestCreate() { + OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Tell me a joke"); + + assertThat(request.getModel()).isEqualTo("llama2"); + assertThat(request.getPrompt()).isEqualTo("Tell me a joke"); + } + + @Test + @DisplayName("OllamaGenerateRequest setters should work correctly") + void testOllamaGenerateRequestSetters() { + OllamaGenerateRequest request = new OllamaGenerateRequest(); + request.setModel("codellama"); + request.setPrompt("Write a function"); + request.setStream(false); + request.setFormat("json"); + + OllamaOptions options = new OllamaOptions(); + options.setTemperature(0.2); + request.setOptions(options); + + assertThat(request.getModel()).isEqualTo("codellama"); + assertThat(request.getPrompt()).isEqualTo("Write a function"); + assertThat(request.isStream()).isFalse(); + assertThat(request.getFormat()).isEqualTo("json"); + assertThat(request.getOptions().getTemperature()).isEqualTo(0.2); + } + + @Test + @DisplayName("OllamaGenerateResponse setters should work correctly") + void testOllamaGenerateResponseSetters() { + OllamaGenerateResponse response = new OllamaGenerateResponse(); + response.setModel("llama2"); + response.setCreatedAt("2024-01-15T10:30:00Z"); + response.setResponse("Here is your response"); + response.setDone(true); + response.setTotalDuration(2000000000L); + response.setLoadDuration(200000000L); + response.setPromptEvalCount(15); + response.setPromptEvalDuration(100000000L); + response.setEvalCount(30); + response.setEvalDuration(800000000L); + + assertThat(response.getModel()).isEqualTo("llama2"); + assertThat(response.getCreatedAt()).isEqualTo("2024-01-15T10:30:00Z"); + assertThat(response.getResponse()).isEqualTo("Here is your response"); + assertThat(response.isDone()).isTrue(); + assertThat(response.getTotalDuration()).isEqualTo(2000000000L); + assertThat(response.getLoadDuration()).isEqualTo(200000000L); + assertThat(response.getPromptEvalCount()).isEqualTo(15); + assertThat(response.getPromptEvalDuration()).isEqualTo(100000000L); + assertThat(response.getEvalCount()).isEqualTo(30); + assertThat(response.getEvalDuration()).isEqualTo(800000000L); + } + } + + @Nested + @WireMockTest + @DisplayName("Integration Tests") + class IntegrationTests { + + private AxonFlow axonflow; + private OllamaInterceptor interceptor; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + interceptor = new OllamaInterceptor(axonflow, "test-user"); + } + + @Test + @DisplayName("Constructor should reject null AxonFlow") + void testConstructorRejectsNullAxonFlow() { + assertThatThrownBy(() -> new OllamaInterceptor(null, "user")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("axonflow cannot be null"); + } + + @Test + @DisplayName("Constructor should reject null userToken") + void testConstructorRejectsNullUserToken() { + assertThatThrownBy(() -> new OllamaInterceptor(axonflow, null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("Constructor should reject empty userToken") + void testConstructorRejectsEmptyUserToken() { + assertThatThrownBy(() -> new OllamaInterceptor(axonflow, "")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("userToken cannot be null or empty"); + } + + @Test + @DisplayName("wrapChat should allow request when not blocked") + void testWrapChatAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock Ollama call + Function mockCall = + request -> { OllamaChatResponse response = new OllamaChatResponse(); response.setModel("llama2"); - response.setCreatedAt("2024-01-15T10:30:00Z"); - response.setMessage(OllamaMessage.assistant("Hello!")); + response.setMessage(OllamaMessage.assistant("Hello from Ollama!")); response.setDone(true); - response.setTotalDuration(1000000000L); - response.setLoadDuration(100000000L); response.setPromptEvalCount(10); - response.setPromptEvalDuration(50000000L); - response.setEvalCount(20); - response.setEvalDuration(500000000L); - - assertThat(response.getModel()).isEqualTo("llama2"); - assertThat(response.getCreatedAt()).isEqualTo("2024-01-15T10:30:00Z"); - assertThat(response.getMessage().getContent()).isEqualTo("Hello!"); - assertThat(response.isDone()).isTrue(); - assertThat(response.getTotalDuration()).isEqualTo(1000000000L); - assertThat(response.getLoadDuration()).isEqualTo(100000000L); - assertThat(response.getPromptEvalCount()).isEqualTo(10); - assertThat(response.getPromptEvalDuration()).isEqualTo(50000000L); - assertThat(response.getEvalCount()).isEqualTo(20); - assertThat(response.getEvalDuration()).isEqualTo(500000000L); - } - - @Test - @DisplayName("OllamaGenerateRequest create should work correctly") - void testOllamaGenerateRequestCreate() { - OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Tell me a joke"); - - assertThat(request.getModel()).isEqualTo("llama2"); - assertThat(request.getPrompt()).isEqualTo("Tell me a joke"); - } - - @Test - @DisplayName("OllamaGenerateRequest setters should work correctly") - void testOllamaGenerateRequestSetters() { - OllamaGenerateRequest request = new OllamaGenerateRequest(); - request.setModel("codellama"); - request.setPrompt("Write a function"); - request.setStream(false); - request.setFormat("json"); - - OllamaOptions options = new OllamaOptions(); - options.setTemperature(0.2); - request.setOptions(options); - - assertThat(request.getModel()).isEqualTo("codellama"); - assertThat(request.getPrompt()).isEqualTo("Write a function"); - assertThat(request.isStream()).isFalse(); - assertThat(request.getFormat()).isEqualTo("json"); - assertThat(request.getOptions().getTemperature()).isEqualTo(0.2); - } - - @Test - @DisplayName("OllamaGenerateResponse setters should work correctly") - void testOllamaGenerateResponseSetters() { + response.setEvalCount(15); + return response; + }; + + // Create request + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Hello!"); + + // Execute wrapped call + OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getMessage().getContent()).isEqualTo("Hello from Ollama!"); + + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrapChat should throw when blocked by policy") + void testWrapChatBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation\"}"))); + + Function mockCall = + request -> { + fail("Ollama call should not be made when blocked"); + return null; + }; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Blocked content"); + + assertThatThrownBy(() -> interceptor.wrapChat(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("Policy violation"); + } + + @Test + @DisplayName("wrapGenerate should allow request when not blocked") + void testWrapGenerateAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock Ollama generate call + Function mockCall = + request -> { OllamaGenerateResponse response = new OllamaGenerateResponse(); response.setModel("llama2"); - response.setCreatedAt("2024-01-15T10:30:00Z"); - response.setResponse("Here is your response"); + response.setResponse("Generated text"); response.setDone(true); - response.setTotalDuration(2000000000L); - response.setLoadDuration(200000000L); - response.setPromptEvalCount(15); - response.setPromptEvalDuration(100000000L); - response.setEvalCount(30); - response.setEvalDuration(800000000L); - - assertThat(response.getModel()).isEqualTo("llama2"); - assertThat(response.getCreatedAt()).isEqualTo("2024-01-15T10:30:00Z"); - assertThat(response.getResponse()).isEqualTo("Here is your response"); - assertThat(response.isDone()).isTrue(); - assertThat(response.getTotalDuration()).isEqualTo(2000000000L); - assertThat(response.getLoadDuration()).isEqualTo(200000000L); - assertThat(response.getPromptEvalCount()).isEqualTo(15); - assertThat(response.getPromptEvalDuration()).isEqualTo(100000000L); - assertThat(response.getEvalCount()).isEqualTo(30); - assertThat(response.getEvalDuration()).isEqualTo(800000000L); - } + response.setPromptEvalCount(5); + response.setEvalCount(10); + return response; + }; + + // Create request + OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Generate something"); + + // Execute wrapped call + OllamaGenerateResponse response = interceptor.wrapGenerate(mockCall).apply(request); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getResponse()).isEqualTo("Generated text"); + + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrapGenerate should throw when blocked by policy") + void testWrapGenerateBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Content blocked\"}"))); + + Function mockCall = + request -> { + fail("Ollama call should not be made when blocked"); + return null; + }; + + OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Blocked prompt"); + + assertThatThrownBy(() -> interceptor.wrapGenerate(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("Content blocked"); + } + + @Test + @DisplayName("wrapChatAsync should allow request when not blocked") + void testWrapChatAsyncAllowedRequest() throws Exception { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock async Ollama call + Function> mockCall = + request -> { + OllamaChatResponse response = new OllamaChatResponse(); + response.setModel("llama2"); + response.setMessage(OllamaMessage.assistant("Async response")); + response.setDone(true); + return CompletableFuture.completedFuture(response); + }; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Async test"); + + OllamaChatResponse response = interceptor.wrapChatAsync(mockCall).apply(request).get(); + + assertThat(response).isNotNull(); + assertThat(response.getMessage().getContent()).isEqualTo("Async response"); } - @Nested - @WireMockTest - @DisplayName("Integration Tests") - class IntegrationTests { - - private AxonFlow axonflow; - private OllamaInterceptor interceptor; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - interceptor = new OllamaInterceptor(axonflow, "test-user"); - } - - @Test - @DisplayName("Constructor should reject null AxonFlow") - void testConstructorRejectsNullAxonFlow() { - assertThatThrownBy(() -> new OllamaInterceptor(null, "user")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("axonflow cannot be null"); - } - - @Test - @DisplayName("Constructor should reject null userToken") - void testConstructorRejectsNullUserToken() { - assertThatThrownBy(() -> new OllamaInterceptor(axonflow, null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("Constructor should reject empty userToken") - void testConstructorRejectsEmptyUserToken() { - assertThatThrownBy(() -> new OllamaInterceptor(axonflow, "")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("userToken cannot be null or empty"); - } - - @Test - @DisplayName("wrapChat should allow request when not blocked") - void testWrapChatAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock Ollama call - Function mockCall = request -> { - OllamaChatResponse response = new OllamaChatResponse(); - response.setModel("llama2"); - response.setMessage(OllamaMessage.assistant("Hello from Ollama!")); - response.setDone(true); - response.setPromptEvalCount(10); - response.setEvalCount(15); - return response; - }; - - // Create request - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Hello!"); - - // Execute wrapped call - OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getMessage().getContent()).isEqualTo("Hello from Ollama!"); - - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrapChat should throw when blocked by policy") - void testWrapChatBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Policy violation\"}"))); - - Function mockCall = request -> { - fail("Ollama call should not be made when blocked"); - return null; - }; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Blocked content"); - - assertThatThrownBy(() -> interceptor.wrapChat(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("Policy violation"); - } - - @Test - @DisplayName("wrapGenerate should allow request when not blocked") - void testWrapGenerateAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock Ollama generate call - Function mockCall = request -> { - OllamaGenerateResponse response = new OllamaGenerateResponse(); - response.setModel("llama2"); - response.setResponse("Generated text"); - response.setDone(true); - response.setPromptEvalCount(5); - response.setEvalCount(10); - return response; - }; - - // Create request - OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Generate something"); - - // Execute wrapped call - OllamaGenerateResponse response = interceptor.wrapGenerate(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getResponse()).isEqualTo("Generated text"); - - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrapGenerate should throw when blocked by policy") - void testWrapGenerateBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Content blocked\"}"))); - - Function mockCall = request -> { - fail("Ollama call should not be made when blocked"); - return null; - }; - - OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Blocked prompt"); - - assertThatThrownBy(() -> interceptor.wrapGenerate(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("Content blocked"); - } - - @Test - @DisplayName("wrapChatAsync should allow request when not blocked") - void testWrapChatAsyncAllowedRequest() throws Exception { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock async Ollama call - Function> mockCall = request -> { - OllamaChatResponse response = new OllamaChatResponse(); - response.setModel("llama2"); - response.setMessage(OllamaMessage.assistant("Async response")); - response.setDone(true); - return CompletableFuture.completedFuture(response); - }; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Async test"); - - OllamaChatResponse response = interceptor.wrapChatAsync(mockCall) - .apply(request) - .get(); - - assertThat(response).isNotNull(); - assertThat(response.getMessage().getContent()).isEqualTo("Async response"); - } - - @Test - @DisplayName("wrapChatAsync should throw when blocked by policy") - void testWrapChatAsyncBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Async policy violation\"}"))); - - Function> mockCall = request -> { - fail("Ollama call should not be made when blocked"); - return null; - }; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Blocked async"); - - // Execute wrapped async call - should return failed future or throw - try { - CompletableFuture future = interceptor.wrapChatAsync(mockCall) - .apply(request); - - // If we get a future, it should be failed - assertThatThrownBy(future::get) - .isInstanceOf(ExecutionException.class) - .hasCauseInstanceOf(PolicyViolationException.class); - } catch (PolicyViolationException e) { - // Some implementations may throw directly - assertThat(e.getMessage()).contains("Async policy violation"); - } - } - - @Test - @DisplayName("wrapChat should handle long response summaries") - void testWrapChatLongResponseSummary() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock call with long response - Function mockCall = request -> { - OllamaChatResponse response = new OllamaChatResponse(); - response.setModel("llama2"); - response.setMessage(OllamaMessage.assistant("A".repeat(200))); - return response; - }; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); - - // Execute - summary truncation happens in audit - OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getMessage().getContent()).hasSize(200); - } - - @Test - @DisplayName("wrapGenerate should handle long response summaries") - void testWrapGenerateLongResponseSummary() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long-gen\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - // Create mock call with long response - Function mockCall = request -> { - OllamaGenerateResponse response = new OllamaGenerateResponse(); - response.setModel("llama2"); - response.setResponse("B".repeat(200)); - return response; - }; - - OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Test"); - - OllamaGenerateResponse response = interceptor.wrapGenerate(mockCall).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getResponse()).hasSize(200); - } - - @Test - @DisplayName("wrapChat should handle null response") - void testWrapChatNullResponse() { - // Stub policy check - allowed with no plan_id - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false}"))); - - Function mockCall = request -> null; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); - - OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); - assertThat(response).isNull(); - } - - @Test - @DisplayName("wrapChat should handle null message in response") - void testWrapChatNullMessage() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-null\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withBody("{\"success\":true}"))); - - Function mockCall = request -> { - OllamaChatResponse response = new OllamaChatResponse(); - response.setModel("llama2"); - response.setMessage(null); - return response; - }; - - OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); - - OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); - assertThat(response).isNotNull(); - assertThat(response.getMessage()).isNull(); - } + @Test + @DisplayName("wrapChatAsync should throw when blocked by policy") + void testWrapChatAsyncBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Async policy violation\"}"))); + + Function> mockCall = + request -> { + fail("Ollama call should not be made when blocked"); + return null; + }; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Blocked async"); + + // Execute wrapped async call - should return failed future or throw + try { + CompletableFuture future = + interceptor.wrapChatAsync(mockCall).apply(request); + + // If we get a future, it should be failed + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(PolicyViolationException.class); + } catch (PolicyViolationException e) { + // Some implementations may throw directly + assertThat(e.getMessage()).contains("Async policy violation"); + } + } + + @Test + @DisplayName("wrapChat should handle long response summaries") + void testWrapChatLongResponseSummary() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock call with long response + Function mockCall = + request -> { + OllamaChatResponse response = new OllamaChatResponse(); + response.setModel("llama2"); + response.setMessage(OllamaMessage.assistant("A".repeat(200))); + return response; + }; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); + + // Execute - summary truncation happens in audit + OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getMessage().getContent()).hasSize(200); + } + + @Test + @DisplayName("wrapGenerate should handle long response summaries") + void testWrapGenerateLongResponseSummary() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-long-gen\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + // Create mock call with long response + Function mockCall = + request -> { + OllamaGenerateResponse response = new OllamaGenerateResponse(); + response.setModel("llama2"); + response.setResponse("B".repeat(200)); + return response; + }; + + OllamaGenerateRequest request = OllamaGenerateRequest.create("llama2", "Test"); + + OllamaGenerateResponse response = interceptor.wrapGenerate(mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getResponse()).hasSize(200); + } + + @Test + @DisplayName("wrapChat should handle null response") + void testWrapChatNullResponse() { + // Stub policy check - allowed with no plan_id + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false}"))); + + Function mockCall = request -> null; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); + + OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); + assertThat(response).isNull(); + } + + @Test + @DisplayName("wrapChat should handle null message in response") + void testWrapChatNullMessage() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-null\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn(aResponse().withStatus(200).withBody("{\"success\":true}"))); + + Function mockCall = + request -> { + OllamaChatResponse response = new OllamaChatResponse(); + response.setModel("llama2"); + response.setMessage(null); + return response; + }; + + OllamaChatRequest request = OllamaChatRequest.create("llama2", "Test"); + + OllamaChatResponse response = interceptor.wrapChat(mockCall).apply(request); + assertThat(response).isNotNull(); + assertThat(response.getMessage()).isNull(); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptorTest.java b/src/test/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptorTest.java index d8e36e9..0208f89 100644 --- a/src/test/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptorTest.java +++ b/src/test/java/com/getaxonflow/sdk/interceptors/OpenAIInterceptorTest.java @@ -6,375 +6,379 @@ */ package com.getaxonflow.sdk.interceptors; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.AxonFlowConfig; import com.getaxonflow.sdk.exceptions.PolicyViolationException; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.util.List; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.function.Function; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("OpenAI Interceptor") class OpenAIInterceptorTest { - @Nested - @DisplayName("Type Tests") - class TypeTests { - - @Test - @DisplayName("ChatCompletionRequest builder should work correctly") - void testChatCompletionRequestBuilder() { - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4-turbo") - .addSystemMessage("You are a helpful assistant.") - .addUserMessage("What is 2+2?") - .temperature(0.5) - .maxTokens(100) - .topP(0.9) - .n(1) - .stream(false) - .stop(List.of("\n")) - .build(); - - assertThat(request.getModel()).isEqualTo("gpt-4-turbo"); - assertThat(request.getMessages()).hasSize(2); - assertThat(request.getMessages().get(0).getRole()).isEqualTo("system"); - assertThat(request.getMessages().get(1).getRole()).isEqualTo("user"); - assertThat(request.getTemperature()).isEqualTo(0.5); - assertThat(request.getMaxTokens()).isEqualTo(100); - assertThat(request.getTopP()).isEqualTo(0.9); - assertThat(request.getN()).isEqualTo(1); - assertThat(request.getStream()).isFalse(); - assertThat(request.getStop()).containsExactly("\n"); - } - - @Test - @DisplayName("ChatCompletionRequest extractPrompt should concatenate messages") - void testChatCompletionRequestExtractPrompt() { - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addSystemMessage("System message") - .addUserMessage("User message") - .build(); - - String prompt = request.extractPrompt(); - assertThat(prompt).contains("System message"); - assertThat(prompt).contains("User message"); - } - - @Test - @DisplayName("ChatCompletionRequest should require model") - void testChatCompletionRequestRequiresModel() { - assertThatThrownBy(() -> ChatCompletionRequest.builder() - .addUserMessage("Test") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("ChatCompletionResponse builder should work correctly") - void testChatCompletionResponseBuilder() { - ChatCompletionResponse response = ChatCompletionResponse.builder() - .id("cmpl-123") - .object("chat.completion") - .created(1234567890L) - .model("gpt-4") - .choices(List.of( - new ChatCompletionResponse.Choice( - 0, - ChatMessage.assistant("Test response"), - "stop" - ) - )) - .usage(new ChatCompletionResponse.Usage(10, 5, 15)) - .build(); - - assertThat(response.getId()).isEqualTo("cmpl-123"); - assertThat(response.getObject()).isEqualTo("chat.completion"); - assertThat(response.getCreated()).isEqualTo(1234567890L); - assertThat(response.getModel()).isEqualTo("gpt-4"); - assertThat(response.getChoices()).hasSize(1); - assertThat(response.getContent()).isEqualTo("Test response"); - assertThat(response.getUsage().getPromptTokens()).isEqualTo(10); - assertThat(response.getUsage().getCompletionTokens()).isEqualTo(5); - assertThat(response.getUsage().getTotalTokens()).isEqualTo(15); - } - - @Test - @DisplayName("ChatCompletionResponse getSummary should truncate long content") - void testChatCompletionResponseGetSummary() { - String longContent = "A".repeat(200); - ChatCompletionResponse response = ChatCompletionResponse.builder() - .choices(List.of( - new ChatCompletionResponse.Choice( - 0, - ChatMessage.assistant(longContent), - "stop" - ) - )) - .build(); - - assertThat(response.getSummary()).hasSize(100); - } - - @Test - @DisplayName("ChatMessage factory methods should work correctly") - void testChatMessageFactory() { - ChatMessage system = ChatMessage.system("System prompt"); - assertThat(system.getRole()).isEqualTo("system"); - assertThat(system.getContent()).isEqualTo("System prompt"); - - ChatMessage user = ChatMessage.user("User message"); - assertThat(user.getRole()).isEqualTo("user"); - assertThat(user.getContent()).isEqualTo("User message"); - - ChatMessage assistant = ChatMessage.assistant("Assistant reply"); - assertThat(assistant.getRole()).isEqualTo("assistant"); - assertThat(assistant.getContent()).isEqualTo("Assistant reply"); - } - - @Test - @DisplayName("Usage static factory should calculate total tokens") - void testUsageStaticFactory() { - ChatCompletionResponse.Usage usage = ChatCompletionResponse.Usage.of(100, 50); - assertThat(usage.getPromptTokens()).isEqualTo(100); - assertThat(usage.getCompletionTokens()).isEqualTo(50); - assertThat(usage.getTotalTokens()).isEqualTo(150); - } + @Nested + @DisplayName("Type Tests") + class TypeTests { + + @Test + @DisplayName("ChatCompletionRequest builder should work correctly") + void testChatCompletionRequestBuilder() { + ChatCompletionRequest request = + ChatCompletionRequest.builder() + .model("gpt-4-turbo") + .addSystemMessage("You are a helpful assistant.") + .addUserMessage("What is 2+2?") + .temperature(0.5) + .maxTokens(100) + .topP(0.9) + .n(1) + .stream(false) + .stop(List.of("\n")) + .build(); + + assertThat(request.getModel()).isEqualTo("gpt-4-turbo"); + assertThat(request.getMessages()).hasSize(2); + assertThat(request.getMessages().get(0).getRole()).isEqualTo("system"); + assertThat(request.getMessages().get(1).getRole()).isEqualTo("user"); + assertThat(request.getTemperature()).isEqualTo(0.5); + assertThat(request.getMaxTokens()).isEqualTo(100); + assertThat(request.getTopP()).isEqualTo(0.9); + assertThat(request.getN()).isEqualTo(1); + assertThat(request.getStream()).isFalse(); + assertThat(request.getStop()).containsExactly("\n"); + } + + @Test + @DisplayName("ChatCompletionRequest extractPrompt should concatenate messages") + void testChatCompletionRequestExtractPrompt() { + ChatCompletionRequest request = + ChatCompletionRequest.builder() + .model("gpt-4") + .addSystemMessage("System message") + .addUserMessage("User message") + .build(); + + String prompt = request.extractPrompt(); + assertThat(prompt).contains("System message"); + assertThat(prompt).contains("User message"); + } + + @Test + @DisplayName("ChatCompletionRequest should require model") + void testChatCompletionRequestRequiresModel() { + assertThatThrownBy(() -> ChatCompletionRequest.builder().addUserMessage("Test").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("ChatCompletionResponse builder should work correctly") + void testChatCompletionResponseBuilder() { + ChatCompletionResponse response = + ChatCompletionResponse.builder() + .id("cmpl-123") + .object("chat.completion") + .created(1234567890L) + .model("gpt-4") + .choices( + List.of( + new ChatCompletionResponse.Choice( + 0, ChatMessage.assistant("Test response"), "stop"))) + .usage(new ChatCompletionResponse.Usage(10, 5, 15)) + .build(); + + assertThat(response.getId()).isEqualTo("cmpl-123"); + assertThat(response.getObject()).isEqualTo("chat.completion"); + assertThat(response.getCreated()).isEqualTo(1234567890L); + assertThat(response.getModel()).isEqualTo("gpt-4"); + assertThat(response.getChoices()).hasSize(1); + assertThat(response.getContent()).isEqualTo("Test response"); + assertThat(response.getUsage().getPromptTokens()).isEqualTo(10); + assertThat(response.getUsage().getCompletionTokens()).isEqualTo(5); + assertThat(response.getUsage().getTotalTokens()).isEqualTo(15); + } + + @Test + @DisplayName("ChatCompletionResponse getSummary should truncate long content") + void testChatCompletionResponseGetSummary() { + String longContent = "A".repeat(200); + ChatCompletionResponse response = + ChatCompletionResponse.builder() + .choices( + List.of( + new ChatCompletionResponse.Choice( + 0, ChatMessage.assistant(longContent), "stop"))) + .build(); + + assertThat(response.getSummary()).hasSize(100); + } + + @Test + @DisplayName("ChatMessage factory methods should work correctly") + void testChatMessageFactory() { + ChatMessage system = ChatMessage.system("System prompt"); + assertThat(system.getRole()).isEqualTo("system"); + assertThat(system.getContent()).isEqualTo("System prompt"); + + ChatMessage user = ChatMessage.user("User message"); + assertThat(user.getRole()).isEqualTo("user"); + assertThat(user.getContent()).isEqualTo("User message"); + + ChatMessage assistant = ChatMessage.assistant("Assistant reply"); + assertThat(assistant.getRole()).isEqualTo("assistant"); + assertThat(assistant.getContent()).isEqualTo("Assistant reply"); + } + + @Test + @DisplayName("Usage static factory should calculate total tokens") + void testUsageStaticFactory() { + ChatCompletionResponse.Usage usage = ChatCompletionResponse.Usage.of(100, 50); + assertThat(usage.getPromptTokens()).isEqualTo(100); + assertThat(usage.getCompletionTokens()).isEqualTo(50); + assertThat(usage.getTotalTokens()).isEqualTo(150); + } + } + + @Nested + @WireMockTest + @DisplayName("Integration Tests") + class IntegrationTests { + + private AxonFlow axonflow; + private OpenAIInterceptor interceptor; + + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + axonflow = + AxonFlow.create( + AxonFlowConfig.builder().agentUrl(wmRuntimeInfo.getHttpBaseUrl()).build()); + interceptor = + OpenAIInterceptor.builder() + .axonflow(axonflow) + .userToken("test-user") + .asyncAudit(false) + .build(); + } + + @Test + @DisplayName("Builder should require AxonFlow") + void testBuilderRequiresAxonFlow() { + assertThatThrownBy(() -> OpenAIInterceptor.builder().build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("wrap should allow request when not blocked") + void testWrapAllowedRequest() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock OpenAI call + Function mockCall = + request -> + ChatCompletionResponse.builder() + .id("chatcmpl-123") + .model("gpt-4") + .choices( + List.of( + new ChatCompletionResponse.Choice( + 0, ChatMessage.assistant("Hello! How can I help you?"), "stop"))) + .usage(ChatCompletionResponse.Usage.of(10, 20)) + .build(); + + // Create request + ChatCompletionRequest request = + ChatCompletionRequest.builder() + .model("gpt-4") + .addUserMessage("Hello!") + .temperature(0.7) + .maxTokens(1024) + .build(); + + // Execute wrapped call + ChatCompletionResponse response = interceptor.wrap(mockCall).apply(request); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("chatcmpl-123"); + assertThat(response.getContent()).isEqualTo("Hello! How can I help you?"); + + // Verify API was called + verify(postRequestedFor(urlEqualTo("/api/request"))); + } + + @Test + @DisplayName("wrap should throw when blocked by policy") + void testWrapBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: no-pii\"}"))); + + // Create mock OpenAI call (should not be called) + Function mockCall = + request -> { + fail("OpenAI call should not be made when blocked"); + return null; + }; + + // Create request + ChatCompletionRequest request = + ChatCompletionRequest.builder() + .model("gpt-4") + .addUserMessage("Tell me about John's SSN") + .build(); + + // Execute wrapped call + assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) + .isInstanceOf(PolicyViolationException.class) + .hasMessageContaining("no-pii"); + } + + @Test + @DisplayName("wrapAsync should allow request when not blocked") + void testWrapAsyncAllowedRequest() throws Exception { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock async OpenAI call + Function> mockCall = + request -> + CompletableFuture.completedFuture( + ChatCompletionResponse.builder() + .id("chatcmpl-456") + .model("gpt-4") + .choices( + List.of( + new ChatCompletionResponse.Choice( + 0, ChatMessage.assistant("Async response"), "stop"))) + .usage(ChatCompletionResponse.Usage.of(5, 15)) + .build()); + + // Create request + ChatCompletionRequest request = + ChatCompletionRequest.builder().model("gpt-4").addUserMessage("Async test").build(); + + // Execute wrapped async call + ChatCompletionResponse response = interceptor.wrapAsync(mockCall).apply(request).get(); + + // Verify + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("chatcmpl-456"); + assertThat(response.getContent()).isEqualTo("Async response"); + } + + @Test + @DisplayName("wrapAsync should throw when blocked by policy") + void testWrapAsyncBlockedRequest() { + // Stub policy check - blocked + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{\"success\":false,\"blocked\":true,\"block_reason\":\"Content policy violation\"}"))); + + // Create mock async OpenAI call (should not be called) + Function> mockCall = + request -> { + fail("OpenAI call should not be made when blocked"); + return null; + }; + + // Create request + ChatCompletionRequest request = + ChatCompletionRequest.builder().model("gpt-4").addUserMessage("Blocked content").build(); + + // Execute wrapped async call + CompletableFuture future = + interceptor.wrapAsync(mockCall).apply(request); + + assertThatThrownBy(future::get) + .isInstanceOf(ExecutionException.class) + .hasCauseInstanceOf(PolicyViolationException.class); } - @Nested - @WireMockTest - @DisplayName("Integration Tests") - class IntegrationTests { - - private AxonFlow axonflow; - private OpenAIInterceptor interceptor; - - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - axonflow = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - interceptor = OpenAIInterceptor.builder() - .axonflow(axonflow) - .userToken("test-user") - .asyncAudit(false) - .build(); - } - - @Test - @DisplayName("Builder should require AxonFlow") - void testBuilderRequiresAxonFlow() { - assertThatThrownBy(() -> OpenAIInterceptor.builder().build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("wrap should allow request when not blocked") - void testWrapAllowedRequest() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-123\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock OpenAI call - Function mockCall = request -> - ChatCompletionResponse.builder() - .id("chatcmpl-123") - .model("gpt-4") - .choices(List.of(new ChatCompletionResponse.Choice( - 0, - ChatMessage.assistant("Hello! How can I help you?"), - "stop" - ))) - .usage(ChatCompletionResponse.Usage.of(10, 20)) - .build(); - - // Create request - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addUserMessage("Hello!") - .temperature(0.7) - .maxTokens(1024) - .build(); - - // Execute wrapped call - ChatCompletionResponse response = interceptor.wrap(mockCall).apply(request); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("chatcmpl-123"); - assertThat(response.getContent()).isEqualTo("Hello! How can I help you?"); - - // Verify API was called - verify(postRequestedFor(urlEqualTo("/api/request"))); - } - - @Test - @DisplayName("wrap should throw when blocked by policy") - void testWrapBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Request blocked by policy: no-pii\"}"))); - - // Create mock OpenAI call (should not be called) - Function mockCall = request -> { - fail("OpenAI call should not be made when blocked"); - return null; - }; - - // Create request - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addUserMessage("Tell me about John's SSN") - .build(); - - // Execute wrapped call - assertThatThrownBy(() -> interceptor.wrap(mockCall).apply(request)) - .isInstanceOf(PolicyViolationException.class) - .hasMessageContaining("no-pii"); - } - - @Test - @DisplayName("wrapAsync should allow request when not blocked") - void testWrapAsyncAllowedRequest() throws Exception { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-456\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock async OpenAI call - Function> mockCall = - request -> CompletableFuture.completedFuture( - ChatCompletionResponse.builder() - .id("chatcmpl-456") - .model("gpt-4") - .choices(List.of(new ChatCompletionResponse.Choice( - 0, - ChatMessage.assistant("Async response"), - "stop" - ))) - .usage(ChatCompletionResponse.Usage.of(5, 15)) - .build() - ); - - // Create request - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addUserMessage("Async test") - .build(); - - // Execute wrapped async call - ChatCompletionResponse response = interceptor.wrapAsync(mockCall) - .apply(request) - .get(); - - // Verify - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("chatcmpl-456"); - assertThat(response.getContent()).isEqualTo("Async response"); - } - - @Test - @DisplayName("wrapAsync should throw when blocked by policy") - void testWrapAsyncBlockedRequest() { - // Stub policy check - blocked - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":false,\"blocked\":true,\"block_reason\":\"Content policy violation\"}"))); - - // Create mock async OpenAI call (should not be called) - Function> mockCall = - request -> { - fail("OpenAI call should not be made when blocked"); - return null; - }; - - // Create request - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addUserMessage("Blocked content") - .build(); - - // Execute wrapped async call - CompletableFuture future = interceptor.wrapAsync(mockCall) - .apply(request); - - assertThatThrownBy(future::get) - .isInstanceOf(ExecutionException.class) - .hasCauseInstanceOf(PolicyViolationException.class); - } - - @Test - @DisplayName("static wrapChatCompletion should work") - void testStaticWrapperMethod() { - // Stub policy check - allowed - stubFor(post(urlEqualTo("/api/request")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); - - // Stub audit call - stubFor(post(urlEqualTo("/api/audit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody("{\"success\":true}"))); - - // Create mock OpenAI call - Function mockCall = request -> - ChatCompletionResponse.builder() - .id("chatcmpl-789") - .model("gpt-4") - .build(); - - // Use static wrapper - ChatCompletionRequest request = ChatCompletionRequest.builder() - .model("gpt-4") - .addUserMessage("Static test") - .build(); - - ChatCompletionResponse response = OpenAIInterceptor.wrapChatCompletion( - axonflow, "user-token", mockCall - ).apply(request); - - assertThat(response).isNotNull(); - assertThat(response.getId()).isEqualTo("chatcmpl-789"); - } + @Test + @DisplayName("static wrapChatCompletion should work") + void testStaticWrapperMethod() { + // Stub policy check - allowed + stubFor( + post(urlEqualTo("/api/request")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true,\"blocked\":false,\"plan_id\":\"plan-789\"}"))); + + // Stub audit call + stubFor( + post(urlEqualTo("/api/audit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody("{\"success\":true}"))); + + // Create mock OpenAI call + Function mockCall = + request -> ChatCompletionResponse.builder().id("chatcmpl-789").model("gpt-4").build(); + + // Use static wrapper + ChatCompletionRequest request = + ChatCompletionRequest.builder().model("gpt-4").addUserMessage("Static test").build(); + + ChatCompletionResponse response = + OpenAIInterceptor.wrapChatCompletion(axonflow, "user-token", mockCall).apply(request); + + assertThat(response).isNotNull(); + assertThat(response.getId()).isEqualTo("chatcmpl-789"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java index ece00d6..e9d4c0a 100644 --- a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATClientTest.java @@ -6,470 +6,508 @@ */ package com.getaxonflow.sdk.masfeat; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlow; import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.List; - -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for MAS FEAT client methods. - */ +/** Tests for MAS FEAT client methods. */ @WireMockTest @DisplayName("MAS FEAT Client Tests") class MASFEATClientTest { - private AxonFlow client; + private AxonFlow client; - @BeforeEach - void setUp(WireMockRuntimeInfo wmRuntimeInfo) { - client = AxonFlow.create(AxonFlow.builder() + @BeforeEach + void setUp(WireMockRuntimeInfo wmRuntimeInfo) { + client = + AxonFlow.create( + AxonFlow.builder() .endpoint(wmRuntimeInfo.getHttpBaseUrl()) .clientId("test-client") .clientSecret("test-secret") .build()); + } + + @Nested + @DisplayName("Registry Methods") + class RegistryMethodsTest { + + @Test + @DisplayName("Should register a new AI system") + void testRegisterSystem() { + String responseJson = + "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"credit-model-v1\"," + + "\"system_name\": \"Credit Scoring Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"data-science\"," + + "\"risk_rating_impact\": 3," + + "\"risk_rating_complexity\": 2," + + "\"risk_rating_reliance\": 1," + + "\"materiality\": \"high\"," + + "\"status\": \"draft\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/registry")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RegisterSystemRequest request = + RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .build(); + + AISystemRegistry result = client.masfeat().registerSystem(request); + + assertThat(result.getId()).isEqualTo("sys-123"); + assertThat(result.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(result.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/registry"))); + } + + @Test + @DisplayName("Should get a system by ID") + void testGetSystem() { + String responseJson = + "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"model-v1\"," + + "\"system_name\": \"Test Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"team\"," + + "\"customer_impact\": 3," + + "\"model_complexity\": 2," + + "\"human_reliance\": 1," + + "\"materiality\": \"high\"," + + "\"status\": \"active\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + get(urlEqualTo("/api/v1/masfeat/registry/sys-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + AISystemRegistry result = client.masfeat().getSystem("sys-123"); + + assertThat(result.getId()).isEqualTo("sys-123"); + assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); + } + + @Test + @DisplayName("Should activate a system") + void testActivateSystem() { + String responseJson = + "{" + + "\"id\": \"sys-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"model-v1\"," + + "\"system_name\": \"Test Model\"," + + "\"use_case\": \"credit_scoring\"," + + "\"owner_team\": \"team\"," + + "\"materiality\": \"high\"," + + "\"status\": \"active\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + put(urlEqualTo("/api/v1/masfeat/registry/sys-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + AISystemRegistry result = client.masfeat().activateSystem("sys-123"); + + assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); + + verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); + } + + @Test + @DisplayName("Should get registry summary") + void testGetRegistrySummary() { + String responseJson = + "{" + + "\"total_systems\": 10," + + "\"active_systems\": 8," + + "\"high_materiality_count\": 2," + + "\"medium_materiality_count\": 5," + + "\"low_materiality_count\": 3" + + "}"; + stubFor( + get(urlEqualTo("/api/v1/masfeat/registry/summary")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RegistrySummary result = client.masfeat().getRegistrySummary(); + + assertThat(result.getTotalSystems()).isEqualTo(10); + assertThat(result.getActiveSystems()).isEqualTo(8); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/summary"))); + } + } + + @Nested + @DisplayName("Assessment Methods") + class AssessmentMethodsTest { + + @Test + @DisplayName("Should create a new assessment") + void testCreateAssessment() { + String responseJson = + "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"pending\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/assessments")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + CreateAssessmentRequest request = + CreateAssessmentRequest.builder().systemId("sys-789").assessmentType("annual").build(); + + FEATAssessment result = client.masfeat().createAssessment(request); + + assertThat(result.getId()).isEqualTo("assess-123"); + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.PENDING); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments"))); + } + + @Test + @DisplayName("Should get an assessment by ID") + void testGetAssessment() { + String responseJson = + "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"completed\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"overall_score\": 89," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + get(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + FEATAssessment result = client.masfeat().getAssessment("assess-123"); + + assertThat(result.getId()).isEqualTo("assess-123"); + assertThat(result.getOverallScore()).isEqualTo(89); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); + } + + @Test + @DisplayName("Should update an assessment") + void testUpdateAssessment() { + String responseJson = + "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"in_progress\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"fairness_score\": 85," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + put(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + UpdateAssessmentRequest request = UpdateAssessmentRequest.builder().fairnessScore(85).build(); + + FEATAssessment result = client.masfeat().updateAssessment("assess-123", request); + + assertThat(result.getFairnessScore()).isEqualTo(85); + + verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); + } + + @Test + @DisplayName("Should submit an assessment") + void testSubmitAssessment() { + String responseJson = + "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"completed\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + FEATAssessment result = client.masfeat().submitAssessment("assess-123"); + + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit"))); + } + + @Test + @DisplayName("Should approve an assessment") + void testApproveAssessment() { + String responseJson = + "{" + + "\"id\": \"assess-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"assessment_type\": \"annual\"," + + "\"status\": \"approved\"," + + "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + + "\"approved_by\": \"admin@example.com\"," + + "\"approved_at\": \"2026-01-23T13:00:00Z\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T13:00:00Z\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + ApproveAssessmentRequest request = ApproveAssessmentRequest.builder().build(); + FEATAssessment result = client.masfeat().approveAssessment("assess-123", request); + + assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.APPROVED); + assertThat(result.getApprovedBy()).isEqualTo("admin@example.com"); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve"))); + } + } + + @Nested + @DisplayName("Kill Switch Methods") + class KillSwitchMethodsTest { + + @Test + @DisplayName("Should get kill switch status") + void testGetKillSwitch() { + String responseJson = + "{" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"accuracy_threshold\": 0.95," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + KillSwitch result = client.masfeat().getKillSwitch("sys-789"); + + assertThat(result.getId()).isEqualTo("ks-123"); + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(result.getAccuracyThreshold()).isEqualTo(0.95); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789"))); + } + + @Test + @DisplayName("Should configure kill switch") + void testConfigureKillSwitch() { + String responseJson = + "{" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"accuracy_threshold\": 0.95," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + ConfigureKillSwitchRequest request = + ConfigureKillSwitchRequest.builder() + .accuracyThreshold(0.95) + .autoTriggerEnabled(true) + .build(); + + KillSwitch result = client.masfeat().configureKillSwitch("sys-789", request); + + assertThat(result.isAutoTriggerEnabled()).isTrue(); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure"))); } - @Nested - @DisplayName("Registry Methods") - class RegistryMethodsTest { - - @Test - @DisplayName("Should register a new AI system") - void testRegisterSystem() { - String responseJson = "{" + - "\"id\": \"sys-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"credit-model-v1\"," + - "\"system_name\": \"Credit Scoring Model\"," + - "\"use_case\": \"credit_scoring\"," + - "\"owner_team\": \"data-science\"," + - "\"risk_rating_impact\": 3," + - "\"risk_rating_complexity\": 2," + - "\"risk_rating_reliance\": 1," + - "\"materiality\": \"high\"," + - "\"status\": \"draft\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/registry")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - RegisterSystemRequest request = RegisterSystemRequest.builder() - .systemId("credit-model-v1") - .systemName("Credit Scoring Model") - .useCase(AISystemUseCase.CREDIT_SCORING) - .ownerTeam("data-science") - .customerImpact(3) - .modelComplexity(2) - .humanReliance(1) - .build(); - - AISystemRegistry result = client.masfeat().registerSystem(request); - - assertThat(result.getId()).isEqualTo("sys-123"); - assertThat(result.getSystemName()).isEqualTo("Credit Scoring Model"); - assertThat(result.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/registry"))); - } - - @Test - @DisplayName("Should get a system by ID") - void testGetSystem() { - String responseJson = "{" + - "\"id\": \"sys-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"model-v1\"," + - "\"system_name\": \"Test Model\"," + - "\"use_case\": \"credit_scoring\"," + - "\"owner_team\": \"team\"," + - "\"customer_impact\": 3," + - "\"model_complexity\": 2," + - "\"human_reliance\": 1," + - "\"materiality\": \"high\"," + - "\"status\": \"active\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(get(urlEqualTo("/api/v1/masfeat/registry/sys-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - AISystemRegistry result = client.masfeat().getSystem("sys-123"); - - assertThat(result.getId()).isEqualTo("sys-123"); - assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); - - verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); - } - - @Test - @DisplayName("Should activate a system") - void testActivateSystem() { - String responseJson = "{" + - "\"id\": \"sys-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"model-v1\"," + - "\"system_name\": \"Test Model\"," + - "\"use_case\": \"credit_scoring\"," + - "\"owner_team\": \"team\"," + - "\"materiality\": \"high\"," + - "\"status\": \"active\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(put(urlEqualTo("/api/v1/masfeat/registry/sys-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - AISystemRegistry result = client.masfeat().activateSystem("sys-123"); - - assertThat(result.getStatus()).isEqualTo(SystemStatus.ACTIVE); - - verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/registry/sys-123"))); - } - - @Test - @DisplayName("Should get registry summary") - void testGetRegistrySummary() { - String responseJson = "{" + - "\"total_systems\": 10," + - "\"active_systems\": 8," + - "\"high_materiality_count\": 2," + - "\"medium_materiality_count\": 5," + - "\"low_materiality_count\": 3" + - "}"; - stubFor(get(urlEqualTo("/api/v1/masfeat/registry/summary")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - RegistrySummary result = client.masfeat().getRegistrySummary(); - - assertThat(result.getTotalSystems()).isEqualTo(10); - assertThat(result.getActiveSystems()).isEqualTo(8); - - verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/registry/summary"))); - } + @Test + @DisplayName("Should trigger kill switch") + void testTriggerKillSwitch() { + String responseJson = + "{" + + "\"kill_switch\": {" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"triggered\"," + + "\"auto_trigger_enabled\": true," + + "\"triggered_reason\": \"Manual trigger\"," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}," + + "\"message\": \"Kill switch triggered\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + TriggerKillSwitchRequest request = + TriggerKillSwitchRequest.builder().reason("Manual trigger").build(); + KillSwitch result = client.masfeat().triggerKillSwitch("sys-789", request); + + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); + assertThat(result.getTriggeredReason()).isEqualTo("Manual trigger"); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger"))); } - @Nested - @DisplayName("Assessment Methods") - class AssessmentMethodsTest { - - @Test - @DisplayName("Should create a new assessment") - void testCreateAssessment() { - String responseJson = "{" + - "\"id\": \"assess-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"assessment_type\": \"annual\"," + - "\"status\": \"pending\"," + - "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/assessments")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - CreateAssessmentRequest request = CreateAssessmentRequest.builder() - .systemId("sys-789") - .assessmentType("annual") - .build(); - - FEATAssessment result = client.masfeat().createAssessment(request); - - assertThat(result.getId()).isEqualTo("assess-123"); - assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.PENDING); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments"))); - } - - @Test - @DisplayName("Should get an assessment by ID") - void testGetAssessment() { - String responseJson = "{" + - "\"id\": \"assess-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"assessment_type\": \"annual\"," + - "\"status\": \"completed\"," + - "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + - "\"overall_score\": 89," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(get(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - FEATAssessment result = client.masfeat().getAssessment("assess-123"); - - assertThat(result.getId()).isEqualTo("assess-123"); - assertThat(result.getOverallScore()).isEqualTo(89); - - verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); - } - - @Test - @DisplayName("Should update an assessment") - void testUpdateAssessment() { - String responseJson = "{" + - "\"id\": \"assess-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"assessment_type\": \"annual\"," + - "\"status\": \"in_progress\"," + - "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + - "\"fairness_score\": 85," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(put(urlEqualTo("/api/v1/masfeat/assessments/assess-123")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - UpdateAssessmentRequest request = UpdateAssessmentRequest.builder() - .fairnessScore(85) - .build(); - - FEATAssessment result = client.masfeat().updateAssessment("assess-123", request); - - assertThat(result.getFairnessScore()).isEqualTo(85); - - verify(putRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123"))); - } - - @Test - @DisplayName("Should submit an assessment") - void testSubmitAssessment() { - String responseJson = "{" + - "\"id\": \"assess-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"assessment_type\": \"annual\"," + - "\"status\": \"completed\"," + - "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - FEATAssessment result = client.masfeat().submitAssessment("assess-123"); - - assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/submit"))); - } - - @Test - @DisplayName("Should approve an assessment") - void testApproveAssessment() { - String responseJson = "{" + - "\"id\": \"assess-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"assessment_type\": \"annual\"," + - "\"status\": \"approved\"," + - "\"assessment_date\": \"2026-01-23T12:00:00Z\"," + - "\"approved_by\": \"admin@example.com\"," + - "\"approved_at\": \"2026-01-23T13:00:00Z\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T13:00:00Z\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - ApproveAssessmentRequest request = ApproveAssessmentRequest.builder().build(); - FEATAssessment result = client.masfeat().approveAssessment("assess-123", request); - - assertThat(result.getStatus()).isEqualTo(FEATAssessmentStatus.APPROVED); - assertThat(result.getApprovedBy()).isEqualTo("admin@example.com"); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/assessments/assess-123/approve"))); - } + @Test + @DisplayName("Should restore kill switch") + void testRestoreKillSwitch() { + String responseJson = + "{" + + "\"kill_switch\": {" + + "\"id\": \"ks-123\"," + + "\"org_id\": \"org-456\"," + + "\"system_id\": \"sys-789\"," + + "\"status\": \"enabled\"," + + "\"auto_trigger_enabled\": true," + + "\"created_at\": \"2026-01-23T12:00:00Z\"," + + "\"updated_at\": \"2026-01-23T12:00:00Z\"" + + "}," + + "\"message\": \"Kill switch restored\"" + + "}"; + stubFor( + post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + RestoreKillSwitchRequest request = RestoreKillSwitchRequest.builder().build(); + KillSwitch result = client.masfeat().restoreKillSwitch("sys-789", request); + + assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + + verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore"))); } - @Nested - @DisplayName("Kill Switch Methods") - class KillSwitchMethodsTest { - - @Test - @DisplayName("Should get kill switch status") - void testGetKillSwitch() { - String responseJson = "{" + - "\"id\": \"ks-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"status\": \"enabled\"," + - "\"auto_trigger_enabled\": true," + - "\"accuracy_threshold\": 0.95," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - KillSwitch result = client.masfeat().getKillSwitch("sys-789"); - - assertThat(result.getId()).isEqualTo("ks-123"); - assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); - assertThat(result.getAccuracyThreshold()).isEqualTo(0.95); - - verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789"))); - } - - @Test - @DisplayName("Should configure kill switch") - void testConfigureKillSwitch() { - String responseJson = "{" + - "\"id\": \"ks-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"status\": \"enabled\"," + - "\"auto_trigger_enabled\": true," + - "\"accuracy_threshold\": 0.95," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() - .accuracyThreshold(0.95) - .autoTriggerEnabled(true) - .build(); - - KillSwitch result = client.masfeat().configureKillSwitch("sys-789", request); - - assertThat(result.isAutoTriggerEnabled()).isTrue(); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/configure"))); - } - - @Test - @DisplayName("Should trigger kill switch") - void testTriggerKillSwitch() { - String responseJson = "{" + - "\"kill_switch\": {" + - "\"id\": \"ks-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"status\": \"triggered\"," + - "\"auto_trigger_enabled\": true," + - "\"triggered_reason\": \"Manual trigger\"," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}," + - "\"message\": \"Kill switch triggered\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - TriggerKillSwitchRequest request = TriggerKillSwitchRequest.builder() - .reason("Manual trigger") - .build(); - KillSwitch result = client.masfeat().triggerKillSwitch("sys-789", request); - - assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); - assertThat(result.getTriggeredReason()).isEqualTo("Manual trigger"); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/trigger"))); - } - - @Test - @DisplayName("Should restore kill switch") - void testRestoreKillSwitch() { - String responseJson = "{" + - "\"kill_switch\": {" + - "\"id\": \"ks-123\"," + - "\"org_id\": \"org-456\"," + - "\"system_id\": \"sys-789\"," + - "\"status\": \"enabled\"," + - "\"auto_trigger_enabled\": true," + - "\"created_at\": \"2026-01-23T12:00:00Z\"," + - "\"updated_at\": \"2026-01-23T12:00:00Z\"" + - "}," + - "\"message\": \"Kill switch restored\"" + - "}"; - stubFor(post(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - RestoreKillSwitchRequest request = RestoreKillSwitchRequest.builder().build(); - KillSwitch result = client.masfeat().restoreKillSwitch("sys-789", request); - - assertThat(result.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); - - verify(postRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/restore"))); - } - - @Test - @DisplayName("Should get kill switch history") - void testGetKillSwitchHistory() { - String responseJson = "{" + - "\"history\": [" + - "{\"id\": \"event-1\", \"kill_switch_id\": \"ks-123\", \"action\": \"enabled\", \"performed_by\": \"admin\", \"performed_at\": \"2026-01-23T12:00:00Z\"}," + - "{\"id\": \"event-2\", \"kill_switch_id\": \"ks-123\", \"action\": \"triggered\", \"reason\": \"Bias exceeded\", \"performed_by\": \"system\", \"performed_at\": \"2026-01-23T13:00:00Z\"}" + - "]," + - "\"count\": 2" + - "}"; - stubFor(get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10")) - .willReturn(aResponse() - .withStatus(200) - .withHeader("Content-Type", "application/json") - .withBody(responseJson))); - - List result = client.masfeat().getKillSwitchHistory("sys-789", 10); - - assertThat(result).hasSize(2); - assertThat(result.get(0).getEventType()).isEqualTo("enabled"); - assertThat(result.get(1).getEventType()).isEqualTo("triggered"); - - verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10"))); - } + @Test + @DisplayName("Should get kill switch history") + void testGetKillSwitchHistory() { + String responseJson = + "{" + + "\"history\": [" + + "{\"id\": \"event-1\", \"kill_switch_id\": \"ks-123\", \"action\": \"enabled\", \"performed_by\": \"admin\", \"performed_at\": \"2026-01-23T12:00:00Z\"}," + + "{\"id\": \"event-2\", \"kill_switch_id\": \"ks-123\", \"action\": \"triggered\", \"reason\": \"Bias exceeded\", \"performed_by\": \"system\", \"performed_at\": \"2026-01-23T13:00:00Z\"}" + + "]," + + "\"count\": 2" + + "}"; + stubFor( + get(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10")) + .willReturn( + aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody(responseJson))); + + List result = client.masfeat().getKillSwitchHistory("sys-789", 10); + + assertThat(result).hasSize(2); + assertThat(result.get(0).getEventType()).isEqualTo("enabled"); + assertThat(result.get(1).getEventType()).isEqualTo("triggered"); + + verify(getRequestedFor(urlEqualTo("/api/v1/masfeat/killswitch/sys-789/history?limit=10"))); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java index aa61f72..2b8bb67 100644 --- a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java @@ -6,498 +6,511 @@ */ package com.getaxonflow.sdk.masfeat; -import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; +import com.getaxonflow.sdk.masfeat.MASFEATTypes.*; import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for MAS FEAT compliance types. - */ +/** Tests for MAS FEAT compliance types. */ @DisplayName("MAS FEAT Types Tests") class MASFEATTypesTest { - // ========================================================================= - // Enum Tests - // ========================================================================= - - @Nested - @DisplayName("MaterialityClassification Enum Tests") - class MaterialityClassificationTests { - - @Test - @DisplayName("Should return correct values for all classifications") - void testEnumValues() { - assertThat(MaterialityClassification.HIGH.getValue()).isEqualTo("high"); - assertThat(MaterialityClassification.MEDIUM.getValue()).isEqualTo("medium"); - assertThat(MaterialityClassification.LOW.getValue()).isEqualTo("low"); - } - - @Test - @DisplayName("Should convert from string value") - void testFromValue() { - assertThat(MaterialityClassification.fromValue("high")).isEqualTo(MaterialityClassification.HIGH); - assertThat(MaterialityClassification.fromValue("medium")).isEqualTo(MaterialityClassification.MEDIUM); - assertThat(MaterialityClassification.fromValue("low")).isEqualTo(MaterialityClassification.LOW); - } - - @Test - @DisplayName("Should throw for unknown value") - void testFromValueUnknown() { - assertThatThrownBy(() -> MaterialityClassification.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown materiality"); - } + // ========================================================================= + // Enum Tests + // ========================================================================= + + @Nested + @DisplayName("MaterialityClassification Enum Tests") + class MaterialityClassificationTests { + + @Test + @DisplayName("Should return correct values for all classifications") + void testEnumValues() { + assertThat(MaterialityClassification.HIGH.getValue()).isEqualTo("high"); + assertThat(MaterialityClassification.MEDIUM.getValue()).isEqualTo("medium"); + assertThat(MaterialityClassification.LOW.getValue()).isEqualTo("low"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(MaterialityClassification.fromValue("high")) + .isEqualTo(MaterialityClassification.HIGH); + assertThat(MaterialityClassification.fromValue("medium")) + .isEqualTo(MaterialityClassification.MEDIUM); + assertThat(MaterialityClassification.fromValue("low")) + .isEqualTo(MaterialityClassification.LOW); } - @Nested - @DisplayName("SystemStatus Enum Tests") - class SystemStatusTests { - - @Test - @DisplayName("Should return correct values for all statuses") - void testEnumValues() { - assertThat(SystemStatus.DRAFT.getValue()).isEqualTo("draft"); - assertThat(SystemStatus.ACTIVE.getValue()).isEqualTo("active"); - assertThat(SystemStatus.SUSPENDED.getValue()).isEqualTo("suspended"); - assertThat(SystemStatus.RETIRED.getValue()).isEqualTo("retired"); - } - - @Test - @DisplayName("Should convert from string value") - void testFromValue() { - assertThat(SystemStatus.fromValue("draft")).isEqualTo(SystemStatus.DRAFT); - assertThat(SystemStatus.fromValue("active")).isEqualTo(SystemStatus.ACTIVE); - assertThat(SystemStatus.fromValue("suspended")).isEqualTo(SystemStatus.SUSPENDED); - assertThat(SystemStatus.fromValue("retired")).isEqualTo(SystemStatus.RETIRED); - } - - @Test - @DisplayName("Should throw for unknown value") - void testFromValueUnknown() { - assertThatThrownBy(() -> SystemStatus.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown status"); - } + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> MaterialityClassification.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown materiality"); + } + } + + @Nested + @DisplayName("SystemStatus Enum Tests") + class SystemStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(SystemStatus.DRAFT.getValue()).isEqualTo("draft"); + assertThat(SystemStatus.ACTIVE.getValue()).isEqualTo("active"); + assertThat(SystemStatus.SUSPENDED.getValue()).isEqualTo("suspended"); + assertThat(SystemStatus.RETIRED.getValue()).isEqualTo("retired"); + } + + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(SystemStatus.fromValue("draft")).isEqualTo(SystemStatus.DRAFT); + assertThat(SystemStatus.fromValue("active")).isEqualTo(SystemStatus.ACTIVE); + assertThat(SystemStatus.fromValue("suspended")).isEqualTo(SystemStatus.SUSPENDED); + assertThat(SystemStatus.fromValue("retired")).isEqualTo(SystemStatus.RETIRED); + } + + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> SystemStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown status"); + } + } + + @Nested + @DisplayName("FEATAssessmentStatus Enum Tests") + class FEATAssessmentStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(FEATAssessmentStatus.PENDING.getValue()).isEqualTo("pending"); + assertThat(FEATAssessmentStatus.IN_PROGRESS.getValue()).isEqualTo("in_progress"); + assertThat(FEATAssessmentStatus.COMPLETED.getValue()).isEqualTo("completed"); + assertThat(FEATAssessmentStatus.APPROVED.getValue()).isEqualTo("approved"); + assertThat(FEATAssessmentStatus.REJECTED.getValue()).isEqualTo("rejected"); } - @Nested - @DisplayName("FEATAssessmentStatus Enum Tests") - class FEATAssessmentStatusTests { - - @Test - @DisplayName("Should return correct values for all statuses") - void testEnumValues() { - assertThat(FEATAssessmentStatus.PENDING.getValue()).isEqualTo("pending"); - assertThat(FEATAssessmentStatus.IN_PROGRESS.getValue()).isEqualTo("in_progress"); - assertThat(FEATAssessmentStatus.COMPLETED.getValue()).isEqualTo("completed"); - assertThat(FEATAssessmentStatus.APPROVED.getValue()).isEqualTo("approved"); - assertThat(FEATAssessmentStatus.REJECTED.getValue()).isEqualTo("rejected"); - } - - @Test - @DisplayName("Should convert from string value") - void testFromValue() { - assertThat(FEATAssessmentStatus.fromValue("pending")).isEqualTo(FEATAssessmentStatus.PENDING); - assertThat(FEATAssessmentStatus.fromValue("in_progress")).isEqualTo(FEATAssessmentStatus.IN_PROGRESS); - assertThat(FEATAssessmentStatus.fromValue("completed")).isEqualTo(FEATAssessmentStatus.COMPLETED); - assertThat(FEATAssessmentStatus.fromValue("approved")).isEqualTo(FEATAssessmentStatus.APPROVED); - assertThat(FEATAssessmentStatus.fromValue("rejected")).isEqualTo(FEATAssessmentStatus.REJECTED); - } - - @Test - @DisplayName("Should throw for unknown value") - void testFromValueUnknown() { - assertThatThrownBy(() -> FEATAssessmentStatus.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown assessment status"); - } + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(FEATAssessmentStatus.fromValue("pending")).isEqualTo(FEATAssessmentStatus.PENDING); + assertThat(FEATAssessmentStatus.fromValue("in_progress")) + .isEqualTo(FEATAssessmentStatus.IN_PROGRESS); + assertThat(FEATAssessmentStatus.fromValue("completed")) + .isEqualTo(FEATAssessmentStatus.COMPLETED); + assertThat(FEATAssessmentStatus.fromValue("approved")) + .isEqualTo(FEATAssessmentStatus.APPROVED); + assertThat(FEATAssessmentStatus.fromValue("rejected")) + .isEqualTo(FEATAssessmentStatus.REJECTED); } - @Nested - @DisplayName("KillSwitchStatus Enum Tests") - class KillSwitchStatusTests { - - @Test - @DisplayName("Should return correct values for all statuses") - void testEnumValues() { - assertThat(KillSwitchStatus.ENABLED.getValue()).isEqualTo("enabled"); - assertThat(KillSwitchStatus.DISABLED.getValue()).isEqualTo("disabled"); - assertThat(KillSwitchStatus.TRIGGERED.getValue()).isEqualTo("triggered"); - } - - @Test - @DisplayName("Should convert from string value") - void testFromValue() { - assertThat(KillSwitchStatus.fromValue("enabled")).isEqualTo(KillSwitchStatus.ENABLED); - assertThat(KillSwitchStatus.fromValue("disabled")).isEqualTo(KillSwitchStatus.DISABLED); - assertThat(KillSwitchStatus.fromValue("triggered")).isEqualTo(KillSwitchStatus.TRIGGERED); - } - - @Test - @DisplayName("Should throw for unknown value") - void testFromValueUnknown() { - assertThatThrownBy(() -> KillSwitchStatus.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown kill switch status"); - } + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> FEATAssessmentStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown assessment status"); + } + } + + @Nested + @DisplayName("KillSwitchStatus Enum Tests") + class KillSwitchStatusTests { + + @Test + @DisplayName("Should return correct values for all statuses") + void testEnumValues() { + assertThat(KillSwitchStatus.ENABLED.getValue()).isEqualTo("enabled"); + assertThat(KillSwitchStatus.DISABLED.getValue()).isEqualTo("disabled"); + assertThat(KillSwitchStatus.TRIGGERED.getValue()).isEqualTo("triggered"); } - @Nested - @DisplayName("AISystemUseCase Enum Tests") - class AISystemUseCaseTests { - - @Test - @DisplayName("Should return correct values for all use cases") - void testEnumValues() { - assertThat(AISystemUseCase.CREDIT_SCORING.getValue()).isEqualTo("credit_scoring"); - assertThat(AISystemUseCase.ROBO_ADVISORY.getValue()).isEqualTo("robo_advisory"); - assertThat(AISystemUseCase.INSURANCE_UNDERWRITING.getValue()).isEqualTo("insurance_underwriting"); - assertThat(AISystemUseCase.TRADING_ALGORITHM.getValue()).isEqualTo("trading_algorithm"); - assertThat(AISystemUseCase.AML_CFT.getValue()).isEqualTo("aml_cft"); - assertThat(AISystemUseCase.CUSTOMER_SERVICE.getValue()).isEqualTo("customer_service"); - assertThat(AISystemUseCase.FRAUD_DETECTION.getValue()).isEqualTo("fraud_detection"); - assertThat(AISystemUseCase.OTHER.getValue()).isEqualTo("other"); - } - - @Test - @DisplayName("Should convert from string value") - void testFromValue() { - assertThat(AISystemUseCase.fromValue("credit_scoring")).isEqualTo(AISystemUseCase.CREDIT_SCORING); - assertThat(AISystemUseCase.fromValue("robo_advisory")).isEqualTo(AISystemUseCase.ROBO_ADVISORY); - assertThat(AISystemUseCase.fromValue("fraud_detection")).isEqualTo(AISystemUseCase.FRAUD_DETECTION); - assertThat(AISystemUseCase.fromValue("other")).isEqualTo(AISystemUseCase.OTHER); - } - - @Test - @DisplayName("Should throw for unknown value") - void testFromValueUnknown() { - assertThatThrownBy(() -> AISystemUseCase.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown use case"); - } + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(KillSwitchStatus.fromValue("enabled")).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(KillSwitchStatus.fromValue("disabled")).isEqualTo(KillSwitchStatus.DISABLED); + assertThat(KillSwitchStatus.fromValue("triggered")).isEqualTo(KillSwitchStatus.TRIGGERED); } - // ========================================================================= - // Request Builder Tests - // ========================================================================= - - @Nested - @DisplayName("RegisterSystemRequest Builder Tests") - class RegisterSystemRequestTests { - - @Test - @DisplayName("Should build with required fields") - void testBuilderWithRequiredFields() { - RegisterSystemRequest request = RegisterSystemRequest.builder() - .systemId("credit-model-v1") - .systemName("Credit Scoring Model") - .useCase(AISystemUseCase.CREDIT_SCORING) - .ownerTeam("data-science") - .customerImpact(3) - .modelComplexity(2) - .humanReliance(1) - .build(); - - assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); - assertThat(request.getSystemName()).isEqualTo("Credit Scoring Model"); - assertThat(request.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); - assertThat(request.getOwnerTeam()).isEqualTo("data-science"); - assertThat(request.getCustomerImpact()).isEqualTo(3); - assertThat(request.getModelComplexity()).isEqualTo(2); - assertThat(request.getHumanReliance()).isEqualTo(1); - } - - @Test - @DisplayName("Should build with optional fields") - void testBuilderWithOptionalFields() { - Map metadata = new HashMap<>(); - metadata.put("version", "1.0"); - - RegisterSystemRequest request = RegisterSystemRequest.builder() - .systemId("credit-model-v1") - .systemName("Credit Scoring Model") - .useCase(AISystemUseCase.CREDIT_SCORING) - .ownerTeam("data-science") - .customerImpact(3) - .modelComplexity(2) - .humanReliance(1) - .description("AI model for credit scoring") - .technicalOwner("tech@example.com") - .businessOwner("business@example.com") - .metadata(metadata) - .build(); - - assertThat(request.getDescription()).isEqualTo("AI model for credit scoring"); - assertThat(request.getTechnicalOwner()).isEqualTo("tech@example.com"); - assertThat(request.getBusinessOwner()).isEqualTo("business@example.com"); - assertThat(request.getMetadata()).containsEntry("version", "1.0"); - } + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> KillSwitchStatus.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown kill switch status"); + } + } + + @Nested + @DisplayName("AISystemUseCase Enum Tests") + class AISystemUseCaseTests { + + @Test + @DisplayName("Should return correct values for all use cases") + void testEnumValues() { + assertThat(AISystemUseCase.CREDIT_SCORING.getValue()).isEqualTo("credit_scoring"); + assertThat(AISystemUseCase.ROBO_ADVISORY.getValue()).isEqualTo("robo_advisory"); + assertThat(AISystemUseCase.INSURANCE_UNDERWRITING.getValue()) + .isEqualTo("insurance_underwriting"); + assertThat(AISystemUseCase.TRADING_ALGORITHM.getValue()).isEqualTo("trading_algorithm"); + assertThat(AISystemUseCase.AML_CFT.getValue()).isEqualTo("aml_cft"); + assertThat(AISystemUseCase.CUSTOMER_SERVICE.getValue()).isEqualTo("customer_service"); + assertThat(AISystemUseCase.FRAUD_DETECTION.getValue()).isEqualTo("fraud_detection"); + assertThat(AISystemUseCase.OTHER.getValue()).isEqualTo("other"); } - @Nested - @DisplayName("CreateAssessmentRequest Builder Tests") - class CreateAssessmentRequestTests { - - @Test - @DisplayName("Should build with required fields") - void testBuilderWithRequiredFields() { - CreateAssessmentRequest request = CreateAssessmentRequest.builder() - .systemId("credit-model-v1") - .assessmentType("annual") - .build(); - - assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); - assertThat(request.getAssessmentType()).isEqualTo("annual"); - } - - @Test - @DisplayName("Should build with optional fields") - void testBuilderWithOptionalFields() { - CreateAssessmentRequest request = CreateAssessmentRequest.builder() - .systemId("credit-model-v1") - .assessmentType("annual") - .assessors(List.of("assessor1", "assessor2")) - .build(); - - assertThat(request.getAssessors()).containsExactly("assessor1", "assessor2"); - } + @Test + @DisplayName("Should convert from string value") + void testFromValue() { + assertThat(AISystemUseCase.fromValue("credit_scoring")) + .isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(AISystemUseCase.fromValue("robo_advisory")) + .isEqualTo(AISystemUseCase.ROBO_ADVISORY); + assertThat(AISystemUseCase.fromValue("fraud_detection")) + .isEqualTo(AISystemUseCase.FRAUD_DETECTION); + assertThat(AISystemUseCase.fromValue("other")).isEqualTo(AISystemUseCase.OTHER); } - @Nested - @DisplayName("ConfigureKillSwitchRequest Builder Tests") - class ConfigureKillSwitchRequestTests { - - @Test - @DisplayName("Should build with all thresholds") - void testBuilderWithAllThresholds() { - ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() - .accuracyThreshold(0.95) - .biasThreshold(0.10) - .errorRateThreshold(0.05) - .autoTriggerEnabled(true) - .build(); - - assertThat(request.getAccuracyThreshold()).isEqualTo(0.95); - assertThat(request.getBiasThreshold()).isEqualTo(0.10); - assertThat(request.getErrorRateThreshold()).isEqualTo(0.05); - assertThat(request.getAutoTriggerEnabled()).isTrue(); - } - - @Test - @DisplayName("Should have null autoTriggerEnabled when not set") - void testAutoTriggerDefault() { - ConfigureKillSwitchRequest request = ConfigureKillSwitchRequest.builder() - .accuracyThreshold(0.95) - .build(); - - assertThat(request.getAutoTriggerEnabled()).isNull(); - } + @Test + @DisplayName("Should throw for unknown value") + void testFromValueUnknown() { + assertThatThrownBy(() -> AISystemUseCase.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown use case"); + } + } + + // ========================================================================= + // Request Builder Tests + // ========================================================================= + + @Nested + @DisplayName("RegisterSystemRequest Builder Tests") + class RegisterSystemRequestTests { + + @Test + @DisplayName("Should build with required fields") + void testBuilderWithRequiredFields() { + RegisterSystemRequest request = + RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .build(); + + assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(request.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(request.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(request.getOwnerTeam()).isEqualTo("data-science"); + assertThat(request.getCustomerImpact()).isEqualTo(3); + assertThat(request.getModelComplexity()).isEqualTo(2); + assertThat(request.getHumanReliance()).isEqualTo(1); } - // ========================================================================= - // Response Type Tests (using setters) - // ========================================================================= - - @Nested - @DisplayName("AISystemRegistry Tests") - class AISystemRegistryTests { - - @Test - @DisplayName("Should set and get all fields") - void testSettersAndGetters() { - Instant now = Instant.now(); - Map metadata = Map.of("version", "1.0"); - - AISystemRegistry registry = new AISystemRegistry(); - registry.setId("sys-123"); - registry.setOrgId("org-456"); - registry.setSystemId("credit-model-v1"); - registry.setSystemName("Credit Scoring Model"); - registry.setDescription("AI model for credit scoring"); - registry.setUseCase(AISystemUseCase.CREDIT_SCORING); - registry.setOwnerTeam("data-science"); - registry.setTechnicalOwner("tech@example.com"); - registry.setBusinessOwner("business@example.com"); - registry.setCustomerImpact(3); - registry.setModelComplexity(2); - registry.setHumanReliance(1); - registry.setMateriality(MaterialityClassification.HIGH); - registry.setStatus(SystemStatus.ACTIVE); - registry.setMetadata(metadata); - registry.setCreatedAt(now); - registry.setUpdatedAt(now); - registry.setCreatedBy("admin"); - - assertThat(registry.getId()).isEqualTo("sys-123"); - assertThat(registry.getOrgId()).isEqualTo("org-456"); - assertThat(registry.getSystemId()).isEqualTo("credit-model-v1"); - assertThat(registry.getSystemName()).isEqualTo("Credit Scoring Model"); - assertThat(registry.getDescription()).isEqualTo("AI model for credit scoring"); - assertThat(registry.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); - assertThat(registry.getOwnerTeam()).isEqualTo("data-science"); - assertThat(registry.getTechnicalOwner()).isEqualTo("tech@example.com"); - assertThat(registry.getBusinessOwner()).isEqualTo("business@example.com"); - assertThat(registry.getCustomerImpact()).isEqualTo(3); - assertThat(registry.getModelComplexity()).isEqualTo(2); - assertThat(registry.getHumanReliance()).isEqualTo(1); - assertThat(registry.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); - assertThat(registry.getStatus()).isEqualTo(SystemStatus.ACTIVE); - assertThat(registry.getMetadata()).containsEntry("version", "1.0"); - assertThat(registry.getCreatedAt()).isEqualTo(now); - assertThat(registry.getUpdatedAt()).isEqualTo(now); - assertThat(registry.getCreatedBy()).isEqualTo("admin"); - } + @Test + @DisplayName("Should build with optional fields") + void testBuilderWithOptionalFields() { + Map metadata = new HashMap<>(); + metadata.put("version", "1.0"); + + RegisterSystemRequest request = + RegisterSystemRequest.builder() + .systemId("credit-model-v1") + .systemName("Credit Scoring Model") + .useCase(AISystemUseCase.CREDIT_SCORING) + .ownerTeam("data-science") + .customerImpact(3) + .modelComplexity(2) + .humanReliance(1) + .description("AI model for credit scoring") + .technicalOwner("tech@example.com") + .businessOwner("business@example.com") + .metadata(metadata) + .build(); + + assertThat(request.getDescription()).isEqualTo("AI model for credit scoring"); + assertThat(request.getTechnicalOwner()).isEqualTo("tech@example.com"); + assertThat(request.getBusinessOwner()).isEqualTo("business@example.com"); + assertThat(request.getMetadata()).containsEntry("version", "1.0"); + } + } + + @Nested + @DisplayName("CreateAssessmentRequest Builder Tests") + class CreateAssessmentRequestTests { + + @Test + @DisplayName("Should build with required fields") + void testBuilderWithRequiredFields() { + CreateAssessmentRequest request = + CreateAssessmentRequest.builder() + .systemId("credit-model-v1") + .assessmentType("annual") + .build(); + + assertThat(request.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(request.getAssessmentType()).isEqualTo("annual"); } - @Nested - @DisplayName("RegistrySummary Tests") - class RegistrySummaryTests { - - @Test - @DisplayName("Should set and get all fields") - void testSettersAndGetters() { - Map byUseCase = Map.of( - "credit_scoring", 4, - "fraud_detection", 6 - ); - Map byStatus = Map.of( - "active", 8, - "draft", 2 - ); - - RegistrySummary summary = new RegistrySummary(); - summary.setTotalSystems(10); - summary.setActiveSystems(8); - summary.setHighMaterialityCount(2); - summary.setMediumMaterialityCount(5); - summary.setLowMaterialityCount(3); - summary.setByUseCase(byUseCase); - summary.setByStatus(byStatus); - - assertThat(summary.getTotalSystems()).isEqualTo(10); - assertThat(summary.getActiveSystems()).isEqualTo(8); - assertThat(summary.getHighMaterialityCount()).isEqualTo(2); - assertThat(summary.getMediumMaterialityCount()).isEqualTo(5); - assertThat(summary.getLowMaterialityCount()).isEqualTo(3); - assertThat(summary.getByUseCase()).containsEntry("credit_scoring", 4); - assertThat(summary.getByStatus()).containsEntry("active", 8); - } + @Test + @DisplayName("Should build with optional fields") + void testBuilderWithOptionalFields() { + CreateAssessmentRequest request = + CreateAssessmentRequest.builder() + .systemId("credit-model-v1") + .assessmentType("annual") + .assessors(List.of("assessor1", "assessor2")) + .build(); + + assertThat(request.getAssessors()).containsExactly("assessor1", "assessor2"); } + } + + @Nested + @DisplayName("ConfigureKillSwitchRequest Builder Tests") + class ConfigureKillSwitchRequestTests { + + @Test + @DisplayName("Should build with all thresholds") + void testBuilderWithAllThresholds() { + ConfigureKillSwitchRequest request = + ConfigureKillSwitchRequest.builder() + .accuracyThreshold(0.95) + .biasThreshold(0.10) + .errorRateThreshold(0.05) + .autoTriggerEnabled(true) + .build(); + + assertThat(request.getAccuracyThreshold()).isEqualTo(0.95); + assertThat(request.getBiasThreshold()).isEqualTo(0.10); + assertThat(request.getErrorRateThreshold()).isEqualTo(0.05); + assertThat(request.getAutoTriggerEnabled()).isTrue(); + } + + @Test + @DisplayName("Should have null autoTriggerEnabled when not set") + void testAutoTriggerDefault() { + ConfigureKillSwitchRequest request = + ConfigureKillSwitchRequest.builder().accuracyThreshold(0.95).build(); - @Nested - @DisplayName("FEATAssessment Tests") - class FEATAssessmentTests { - - @Test - @DisplayName("Should set and get all fields") - void testSettersAndGetters() { - Instant now = Instant.now(); - - FEATAssessment assessment = new FEATAssessment(); - assessment.setId("assess-123"); - assessment.setOrgId("org-456"); - assessment.setSystemId("sys-789"); - assessment.setAssessmentType("annual"); - assessment.setStatus(FEATAssessmentStatus.COMPLETED); - assessment.setAssessmentDate(now); - assessment.setValidUntil(now.plusSeconds(86400 * 365)); - assessment.setFairnessScore(85); - assessment.setEthicsScore(90); - assessment.setAccountabilityScore(88); - assessment.setTransparencyScore(92); - assessment.setOverallScore(89); - Finding finding = Finding.builder() - .id("f-1") - .pillar(FEATPillar.FAIRNESS) - .severity(FindingSeverity.MINOR) - .category("test-category") - .description("Finding 1") - .status(FindingStatus.OPEN) - .build(); - assessment.setFindings(List.of(finding)); - assessment.setRecommendations(List.of("Recommendation 1")); - assessment.setAssessors(List.of("assessor1")); - assessment.setApprovedBy("approver@example.com"); - assessment.setApprovedAt(now); - assessment.setCreatedAt(now); - assessment.setUpdatedAt(now); - assessment.setCreatedBy("admin"); - - assertThat(assessment.getId()).isEqualTo("assess-123"); - assertThat(assessment.getOrgId()).isEqualTo("org-456"); - assertThat(assessment.getSystemId()).isEqualTo("sys-789"); - assertThat(assessment.getAssessmentType()).isEqualTo("annual"); - assertThat(assessment.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); - assertThat(assessment.getFairnessScore()).isEqualTo(85); - assertThat(assessment.getEthicsScore()).isEqualTo(90); - assertThat(assessment.getAccountabilityScore()).isEqualTo(88); - assertThat(assessment.getTransparencyScore()).isEqualTo(92); - assertThat(assessment.getOverallScore()).isEqualTo(89); - assertThat(assessment.getFindings()).hasSize(1); - assertThat(assessment.getFindings().get(0).getDescription()).isEqualTo("Finding 1"); - assertThat(assessment.getRecommendations()).containsExactly("Recommendation 1"); - assertThat(assessment.getAssessors()).containsExactly("assessor1"); - assertThat(assessment.getApprovedBy()).isEqualTo("approver@example.com"); - } + assertThat(request.getAutoTriggerEnabled()).isNull(); + } + } + + // ========================================================================= + // Response Type Tests (using setters) + // ========================================================================= + + @Nested + @DisplayName("AISystemRegistry Tests") + class AISystemRegistryTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + Map metadata = Map.of("version", "1.0"); + + AISystemRegistry registry = new AISystemRegistry(); + registry.setId("sys-123"); + registry.setOrgId("org-456"); + registry.setSystemId("credit-model-v1"); + registry.setSystemName("Credit Scoring Model"); + registry.setDescription("AI model for credit scoring"); + registry.setUseCase(AISystemUseCase.CREDIT_SCORING); + registry.setOwnerTeam("data-science"); + registry.setTechnicalOwner("tech@example.com"); + registry.setBusinessOwner("business@example.com"); + registry.setCustomerImpact(3); + registry.setModelComplexity(2); + registry.setHumanReliance(1); + registry.setMateriality(MaterialityClassification.HIGH); + registry.setStatus(SystemStatus.ACTIVE); + registry.setMetadata(metadata); + registry.setCreatedAt(now); + registry.setUpdatedAt(now); + registry.setCreatedBy("admin"); + + assertThat(registry.getId()).isEqualTo("sys-123"); + assertThat(registry.getOrgId()).isEqualTo("org-456"); + assertThat(registry.getSystemId()).isEqualTo("credit-model-v1"); + assertThat(registry.getSystemName()).isEqualTo("Credit Scoring Model"); + assertThat(registry.getDescription()).isEqualTo("AI model for credit scoring"); + assertThat(registry.getUseCase()).isEqualTo(AISystemUseCase.CREDIT_SCORING); + assertThat(registry.getOwnerTeam()).isEqualTo("data-science"); + assertThat(registry.getTechnicalOwner()).isEqualTo("tech@example.com"); + assertThat(registry.getBusinessOwner()).isEqualTo("business@example.com"); + assertThat(registry.getCustomerImpact()).isEqualTo(3); + assertThat(registry.getModelComplexity()).isEqualTo(2); + assertThat(registry.getHumanReliance()).isEqualTo(1); + assertThat(registry.getMaterialityClassification()).isEqualTo(MaterialityClassification.HIGH); + assertThat(registry.getStatus()).isEqualTo(SystemStatus.ACTIVE); + assertThat(registry.getMetadata()).containsEntry("version", "1.0"); + assertThat(registry.getCreatedAt()).isEqualTo(now); + assertThat(registry.getUpdatedAt()).isEqualTo(now); + assertThat(registry.getCreatedBy()).isEqualTo("admin"); + } + } + + @Nested + @DisplayName("RegistrySummary Tests") + class RegistrySummaryTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Map byUseCase = + Map.of( + "credit_scoring", 4, + "fraud_detection", 6); + Map byStatus = + Map.of( + "active", 8, + "draft", 2); + + RegistrySummary summary = new RegistrySummary(); + summary.setTotalSystems(10); + summary.setActiveSystems(8); + summary.setHighMaterialityCount(2); + summary.setMediumMaterialityCount(5); + summary.setLowMaterialityCount(3); + summary.setByUseCase(byUseCase); + summary.setByStatus(byStatus); + + assertThat(summary.getTotalSystems()).isEqualTo(10); + assertThat(summary.getActiveSystems()).isEqualTo(8); + assertThat(summary.getHighMaterialityCount()).isEqualTo(2); + assertThat(summary.getMediumMaterialityCount()).isEqualTo(5); + assertThat(summary.getLowMaterialityCount()).isEqualTo(3); + assertThat(summary.getByUseCase()).containsEntry("credit_scoring", 4); + assertThat(summary.getByStatus()).containsEntry("active", 8); + } + } + + @Nested + @DisplayName("FEATAssessment Tests") + class FEATAssessmentTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + + FEATAssessment assessment = new FEATAssessment(); + assessment.setId("assess-123"); + assessment.setOrgId("org-456"); + assessment.setSystemId("sys-789"); + assessment.setAssessmentType("annual"); + assessment.setStatus(FEATAssessmentStatus.COMPLETED); + assessment.setAssessmentDate(now); + assessment.setValidUntil(now.plusSeconds(86400 * 365)); + assessment.setFairnessScore(85); + assessment.setEthicsScore(90); + assessment.setAccountabilityScore(88); + assessment.setTransparencyScore(92); + assessment.setOverallScore(89); + Finding finding = + Finding.builder() + .id("f-1") + .pillar(FEATPillar.FAIRNESS) + .severity(FindingSeverity.MINOR) + .category("test-category") + .description("Finding 1") + .status(FindingStatus.OPEN) + .build(); + assessment.setFindings(List.of(finding)); + assessment.setRecommendations(List.of("Recommendation 1")); + assessment.setAssessors(List.of("assessor1")); + assessment.setApprovedBy("approver@example.com"); + assessment.setApprovedAt(now); + assessment.setCreatedAt(now); + assessment.setUpdatedAt(now); + assessment.setCreatedBy("admin"); + + assertThat(assessment.getId()).isEqualTo("assess-123"); + assertThat(assessment.getOrgId()).isEqualTo("org-456"); + assertThat(assessment.getSystemId()).isEqualTo("sys-789"); + assertThat(assessment.getAssessmentType()).isEqualTo("annual"); + assertThat(assessment.getStatus()).isEqualTo(FEATAssessmentStatus.COMPLETED); + assertThat(assessment.getFairnessScore()).isEqualTo(85); + assertThat(assessment.getEthicsScore()).isEqualTo(90); + assertThat(assessment.getAccountabilityScore()).isEqualTo(88); + assertThat(assessment.getTransparencyScore()).isEqualTo(92); + assertThat(assessment.getOverallScore()).isEqualTo(89); + assertThat(assessment.getFindings()).hasSize(1); + assertThat(assessment.getFindings().get(0).getDescription()).isEqualTo("Finding 1"); + assertThat(assessment.getRecommendations()).containsExactly("Recommendation 1"); + assertThat(assessment.getAssessors()).containsExactly("assessor1"); + assertThat(assessment.getApprovedBy()).isEqualTo("approver@example.com"); + } + } + + @Nested + @DisplayName("KillSwitch Tests") + class KillSwitchTests { + + @Test + @DisplayName("Should set and get all fields") + void testSettersAndGetters() { + Instant now = Instant.now(); + + KillSwitch ks = new KillSwitch(); + ks.setId("ks-123"); + ks.setOrgId("org-456"); + ks.setSystemId("sys-789"); + ks.setStatus(KillSwitchStatus.ENABLED); + ks.setAccuracyThreshold(0.95); + ks.setBiasThreshold(0.10); + ks.setErrorRateThreshold(0.05); + ks.setAutoTriggerEnabled(true); + ks.setCreatedAt(now); + ks.setUpdatedAt(now); + + assertThat(ks.getId()).isEqualTo("ks-123"); + assertThat(ks.getOrgId()).isEqualTo("org-456"); + assertThat(ks.getSystemId()).isEqualTo("sys-789"); + assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); + assertThat(ks.getAccuracyThreshold()).isEqualTo(0.95); + assertThat(ks.getBiasThreshold()).isEqualTo(0.10); + assertThat(ks.getErrorRateThreshold()).isEqualTo(0.05); + assertThat(ks.isAutoTriggerEnabled()).isTrue(); } - @Nested - @DisplayName("KillSwitch Tests") - class KillSwitchTests { - - @Test - @DisplayName("Should set and get all fields") - void testSettersAndGetters() { - Instant now = Instant.now(); - - KillSwitch ks = new KillSwitch(); - ks.setId("ks-123"); - ks.setOrgId("org-456"); - ks.setSystemId("sys-789"); - ks.setStatus(KillSwitchStatus.ENABLED); - ks.setAccuracyThreshold(0.95); - ks.setBiasThreshold(0.10); - ks.setErrorRateThreshold(0.05); - ks.setAutoTriggerEnabled(true); - ks.setCreatedAt(now); - ks.setUpdatedAt(now); - - assertThat(ks.getId()).isEqualTo("ks-123"); - assertThat(ks.getOrgId()).isEqualTo("org-456"); - assertThat(ks.getSystemId()).isEqualTo("sys-789"); - assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.ENABLED); - assertThat(ks.getAccuracyThreshold()).isEqualTo(0.95); - assertThat(ks.getBiasThreshold()).isEqualTo(0.10); - assertThat(ks.getErrorRateThreshold()).isEqualTo(0.05); - assertThat(ks.isAutoTriggerEnabled()).isTrue(); - } - - @Test - @DisplayName("Should handle triggered state") - void testTriggeredState() { - Instant now = Instant.now(); - - KillSwitch ks = new KillSwitch(); - ks.setId("ks-123"); - ks.setOrgId("org-456"); - ks.setSystemId("sys-789"); - ks.setStatus(KillSwitchStatus.TRIGGERED); - ks.setTriggeredAt(now); - ks.setTriggeredBy("admin"); - ks.setTriggeredReason("Bias threshold exceeded"); - - assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); - assertThat(ks.getTriggeredAt()).isEqualTo(now); - assertThat(ks.getTriggeredBy()).isEqualTo("admin"); - assertThat(ks.getTriggeredReason()).isEqualTo("Bias threshold exceeded"); - } + @Test + @DisplayName("Should handle triggered state") + void testTriggeredState() { + Instant now = Instant.now(); + + KillSwitch ks = new KillSwitch(); + ks.setId("ks-123"); + ks.setOrgId("org-456"); + ks.setSystemId("sys-789"); + ks.setStatus(KillSwitchStatus.TRIGGERED); + ks.setTriggeredAt(now); + ks.setTriggeredBy("admin"); + ks.setTriggeredReason("Bias threshold exceeded"); + + assertThat(ks.getStatus()).isEqualTo(KillSwitchStatus.TRIGGERED); + assertThat(ks.getTriggeredAt()).isEqualTo(now); + assertThat(ks.getTriggeredBy()).isEqualTo("admin"); + assertThat(ks.getTriggeredReason()).isEqualTo("Bias threshold exceeded"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java index b13bc7a..75177ea 100644 --- a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java +++ b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java @@ -15,441 +15,448 @@ */ package com.getaxonflow.sdk.telemetry; +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import com.getaxonflow.sdk.AxonFlowConfig; import com.github.tomakehurst.wiremock.client.WireMock; import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; -import com.getaxonflow.sdk.AxonFlowConfig; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import static com.github.tomakehurst.wiremock.client.WireMock.*; -import static org.assertj.core.api.Assertions.*; - @DisplayName("TelemetryReporter") @WireMockTest class TelemetryReporterTest { - private final ObjectMapper objectMapper = new ObjectMapper(); - - // --- isEnabled tests (using the 5-arg package-private method) --- - - @Test - @DisplayName("should disable telemetry when DO_NOT_TRACK=1") - void testTelemetryDisabledByDoNotTrack() { - assertThat(TelemetryReporter.isEnabled("production", null, true, "1", null)).isFalse(); - assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, "1", null)).isFalse(); - assertThat(TelemetryReporter.isEnabled("sandbox", null, true, "1", null)).isFalse(); - } - - @Test - @DisplayName("should disable telemetry when AXONFLOW_TELEMETRY=off") - void testTelemetryDisabledByAxonflowEnv() { - assertThat(TelemetryReporter.isEnabled("production", null, true, null, "off")).isFalse(); - assertThat(TelemetryReporter.isEnabled("production", null, true, null, "OFF")).isFalse(); - assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, null, "off")).isFalse(); - } - - @Test - @DisplayName("should default telemetry OFF for sandbox mode") - void testTelemetryDefaultOffForSandbox() { - assertThat(TelemetryReporter.isEnabled("sandbox", null, true, null, null)).isFalse(); - } - - @Test - @DisplayName("should default telemetry ON for production mode with credentials") - void testTelemetryDefaultOnForProductionWithCredentials() { - assertThat(TelemetryReporter.isEnabled("production", null, true, null, null)).isTrue(); - } - - @Test - @DisplayName("should default telemetry ON for production mode even without credentials") - void testTelemetryDefaultOnForProductionWithoutCredentials() { - assertThat(TelemetryReporter.isEnabled("production", null, false, null, null)).isTrue(); - } - - @Test - @DisplayName("should default telemetry ON for enterprise mode with credentials") - void testTelemetryDefaultOnForEnterpriseWithCredentials() { - assertThat(TelemetryReporter.isEnabled("enterprise", null, true, null, null)).isTrue(); - } - - @Test - @DisplayName("should allow config override to enable telemetry in sandbox") - void testTelemetryConfigOverrideEnable() { - assertThat(TelemetryReporter.isEnabled("sandbox", Boolean.TRUE, false, null, null)).isTrue(); - } - - @Test - @DisplayName("should allow config override to disable telemetry in production") - void testTelemetryConfigOverrideDisable() { - assertThat(TelemetryReporter.isEnabled("production", Boolean.FALSE, true, null, null)).isFalse(); - } - - @Test - @DisplayName("DO_NOT_TRACK takes precedence over config override") - void testDoNotTrackPrecedence() { - assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, "1", null)).isFalse(); - } - - @Test - @DisplayName("AXONFLOW_TELEMETRY=off takes precedence over config override") - void testAxonflowTelemetryPrecedence() { - assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, null, "off")).isFalse(); - } - - // --- Payload format test --- - - @Test - @DisplayName("should produce correct payload JSON format") - void testPayloadFormat() throws Exception { - String payload = TelemetryReporter.buildPayload("production", null); - JsonNode root = objectMapper.readTree(payload); - - assertThat(root.get("sdk").asText()).isEqualTo("java"); - assertThat(root.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); - assertThat(root.get("platform_version").isNull()).isTrue(); - assertThat(root.get("os").asText()).isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name"))); - assertThat(root.get("arch").asText()).isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch"))); - assertThat(root.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version")); - assertThat(root.get("deployment_mode").asText()).isEqualTo("production"); - assertThat(root.get("features").isArray()).isTrue(); - assertThat(root.get("features").size()).isEqualTo(0); - assertThat(root.get("instance_id").asText()).isNotEmpty(); - // instance_id should be a valid UUID format - assertThat(root.get("instance_id").asText()).matches( - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - } - - @Test - @DisplayName("payload should reflect the given mode") - void testPayloadModeReflection() throws Exception { - String payload = TelemetryReporter.buildPayload("sandbox", null); - JsonNode root = objectMapper.readTree(payload); - assertThat(root.get("deployment_mode").asText()).isEqualTo("sandbox"); - } - - // --- HTTP integration tests --- - - @Test - @DisplayName("should send telemetry ping to custom endpoint") - void testCustomEndpoint(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - // Call sendPing with custom checkpoint URL, no env opt-outs, with credentials - // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - Boolean.TRUE, - false, - true, // hasCredentials - null, // doNotTrack - null, // axonflowTelemetry - customUrl // checkpointUrl - ); - - // Give the async call time to complete - Thread.sleep(2000); - - verify(postRequestedFor(urlEqualTo("/v1/ping")) - .withHeader("Content-Type", containing("application/json"))); - - // Verify the request body has expected fields - var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping"))); - assertThat(requests).hasSize(1); - - JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); - assertThat(body.get("sdk").asText()).isEqualTo("java"); - assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); - assertThat(body.get("deployment_mode").asText()).isEqualTo("production"); - assertThat(body.get("instance_id").asText()).isNotEmpty(); - } - - @Test - @DisplayName("should not send ping when telemetry is disabled") - void testNoRequestWhenDisabled(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - // Disable via DO_NOT_TRACK - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - null, - false, - true, // hasCredentials - "1", // doNotTrack = disabled - null, - customUrl - ); - - Thread.sleep(1000); - - verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("should silently handle connection failure") - void testSilentFailure() { - // Point to a port that is almost certainly not listening - assertThatCode(() -> { - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - null, - false, - true, // hasCredentials - null, - null, - "http://127.0.0.1:1" // port 1 - connection refused - ); - - // Give the async call time to run and fail - Thread.sleep(4000); - }).doesNotThrowAnyException(); - } - - @Test - @DisplayName("should not send ping in sandbox mode without explicit enable") - void testSandboxModeDefaultOff(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - TelemetryReporter.sendPing( - "sandbox", - "http://localhost:8080", - null, // no override - false, - true, // hasCredentials - null, - null, - customUrl - ); - - Thread.sleep(1000); - - verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("should send ping in sandbox mode when explicitly enabled via config") - void testSandboxModeExplicitEnable(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - TelemetryReporter.sendPing( - "sandbox", - "http://localhost:8080", - Boolean.TRUE, // explicit enable - false, - false, // hasCredentials (doesn't matter with explicit override) - null, - null, - customUrl - ); - - Thread.sleep(2000); - - verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("should send ping in production mode even without credentials") - void testProductionModeWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - Boolean.TRUE, - false, - false, // no credentials — no longer affects default - null, - null, - customUrl - ); - - Thread.sleep(2000); - - verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - // --- Additional tests for parity with Python SDK --- - - @Test - @DisplayName("each buildPayload call should generate a unique instance_id") - void testUniqueInstanceId() throws Exception { - String payload1 = TelemetryReporter.buildPayload("production", null); - String payload2 = TelemetryReporter.buildPayload("production", null); - String payload3 = TelemetryReporter.buildPayload("production", null); - - JsonNode root1 = objectMapper.readTree(payload1); - JsonNode root2 = objectMapper.readTree(payload2); - JsonNode root3 = objectMapper.readTree(payload3); - - String id1 = root1.get("instance_id").asText(); - String id2 = root2.get("instance_id").asText(); - String id3 = root3.get("instance_id").asText(); - - // All three should be valid UUIDs - assertThat(id1).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - assertThat(id2).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - assertThat(id3).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - - // All three should be distinct - assertThat(id1).isNotEqualTo(id2); - assertThat(id1).isNotEqualTo(id3); - assertThat(id2).isNotEqualTo(id3); - } - - @Test - @DisplayName("config false in production should skip POST even with credentials") - void testConfigDisableInProduction(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - Boolean.FALSE, // config override disables - false, - true, // hasCredentials (would normally enable) - null, - null, - customUrl - ); - - Thread.sleep(1000); - - verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("should silently handle server timeout without crashing") - void testSilentFailureOnTimeout(WireMockRuntimeInfo wmRuntimeInfo) { - // Delay response for 5 seconds, exceeding the 3s timeout - stubFor(post("/v1/ping").willReturn(ok().withFixedDelay(5000))); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - assertThatCode(() -> { - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - null, - false, - true, // hasCredentials - null, - null, - customUrl - ); - - // Wait long enough for the async call to hit the timeout and fail - Thread.sleep(5000); - }).doesNotThrowAnyException(); - } - - @Test - @DisplayName("should not crash when server returns HTTP 500") - void testNon200ResponseNoCrash(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(post("/v1/ping").willReturn(serverError().withBody("Internal Server Error"))); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) - assertThatCode(() -> { - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - Boolean.TRUE, - false, - true, // hasCredentials - null, - null, - customUrl - ); - - // Give the async call time to complete - Thread.sleep(2000); - }).doesNotThrowAnyException(); - - // Verify the request was still made (the server just returned 500) - verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("AXONFLOW_TELEMETRY=off should skip POST even with credentials in production") - void testAxonflowTelemetrySkipsPost(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - TelemetryReporter.sendPing( - "production", - "http://localhost:8080", - null, - false, - true, // hasCredentials - null, - "off", // AXONFLOW_TELEMETRY=off - customUrl - ); - - Thread.sleep(1000); - - verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); - } - - @Test - @DisplayName("should send correct payload fields in enterprise mode via HTTP") - void testPayloadDeploymentModeEnterprise(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { - stubFor(post("/v1/ping").willReturn(ok())); - - String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; - - // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) - // Use localhost:1 so detectPlatformVersion gets immediate connection-refused - // (localhost:8080 may have a running service that returns a version) - TelemetryReporter.sendPing( - "enterprise", - "http://localhost:1", - Boolean.TRUE, - false, - true, // hasCredentials - null, - null, - customUrl + private final ObjectMapper objectMapper = new ObjectMapper(); + + // --- isEnabled tests (using the 5-arg package-private method) --- + + @Test + @DisplayName("should disable telemetry when DO_NOT_TRACK=1") + void testTelemetryDisabledByDoNotTrack() { + assertThat(TelemetryReporter.isEnabled("production", null, true, "1", null)).isFalse(); + assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, "1", null)).isFalse(); + assertThat(TelemetryReporter.isEnabled("sandbox", null, true, "1", null)).isFalse(); + } + + @Test + @DisplayName("should disable telemetry when AXONFLOW_TELEMETRY=off") + void testTelemetryDisabledByAxonflowEnv() { + assertThat(TelemetryReporter.isEnabled("production", null, true, null, "off")).isFalse(); + assertThat(TelemetryReporter.isEnabled("production", null, true, null, "OFF")).isFalse(); + assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, null, "off")) + .isFalse(); + } + + @Test + @DisplayName("should default telemetry OFF for sandbox mode") + void testTelemetryDefaultOffForSandbox() { + assertThat(TelemetryReporter.isEnabled("sandbox", null, true, null, null)).isFalse(); + } + + @Test + @DisplayName("should default telemetry ON for production mode with credentials") + void testTelemetryDefaultOnForProductionWithCredentials() { + assertThat(TelemetryReporter.isEnabled("production", null, true, null, null)).isTrue(); + } + + @Test + @DisplayName("should default telemetry ON for production mode even without credentials") + void testTelemetryDefaultOnForProductionWithoutCredentials() { + assertThat(TelemetryReporter.isEnabled("production", null, false, null, null)).isTrue(); + } + + @Test + @DisplayName("should default telemetry ON for enterprise mode with credentials") + void testTelemetryDefaultOnForEnterpriseWithCredentials() { + assertThat(TelemetryReporter.isEnabled("enterprise", null, true, null, null)).isTrue(); + } + + @Test + @DisplayName("should allow config override to enable telemetry in sandbox") + void testTelemetryConfigOverrideEnable() { + assertThat(TelemetryReporter.isEnabled("sandbox", Boolean.TRUE, false, null, null)).isTrue(); + } + + @Test + @DisplayName("should allow config override to disable telemetry in production") + void testTelemetryConfigOverrideDisable() { + assertThat(TelemetryReporter.isEnabled("production", Boolean.FALSE, true, null, null)) + .isFalse(); + } + + @Test + @DisplayName("DO_NOT_TRACK takes precedence over config override") + void testDoNotTrackPrecedence() { + assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, "1", null)).isFalse(); + } + + @Test + @DisplayName("AXONFLOW_TELEMETRY=off takes precedence over config override") + void testAxonflowTelemetryPrecedence() { + assertThat(TelemetryReporter.isEnabled("production", Boolean.TRUE, true, null, "off")) + .isFalse(); + } + + // --- Payload format test --- + + @Test + @DisplayName("should produce correct payload JSON format") + void testPayloadFormat() throws Exception { + String payload = TelemetryReporter.buildPayload("production", null); + JsonNode root = objectMapper.readTree(payload); + + assertThat(root.get("sdk").asText()).isEqualTo("java"); + assertThat(root.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); + assertThat(root.get("platform_version").isNull()).isTrue(); + assertThat(root.get("os").asText()) + .isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name"))); + assertThat(root.get("arch").asText()) + .isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch"))); + assertThat(root.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version")); + assertThat(root.get("deployment_mode").asText()).isEqualTo("production"); + assertThat(root.get("features").isArray()).isTrue(); + assertThat(root.get("features").size()).isEqualTo(0); + assertThat(root.get("instance_id").asText()).isNotEmpty(); + // instance_id should be a valid UUID format + assertThat(root.get("instance_id").asText()) + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } + + @Test + @DisplayName("payload should reflect the given mode") + void testPayloadModeReflection() throws Exception { + String payload = TelemetryReporter.buildPayload("sandbox", null); + JsonNode root = objectMapper.readTree(payload); + assertThat(root.get("deployment_mode").asText()).isEqualTo("sandbox"); + } + + // --- HTTP integration tests --- + + @Test + @DisplayName("should send telemetry ping to custom endpoint") + void testCustomEndpoint(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + // Call sendPing with custom checkpoint URL, no env opt-outs, with credentials + // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + Boolean.TRUE, + false, + true, // hasCredentials + null, // doNotTrack + null, // axonflowTelemetry + customUrl // checkpointUrl ); - Thread.sleep(2000); - - verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping")) - .withHeader("Content-Type", containing("application/json"))); - - var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping"))); - assertThat(requests).hasSize(1); - - JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); - assertThat(body.get("sdk").asText()).isEqualTo("java"); - assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); - assertThat(body.get("deployment_mode").asText()).isEqualTo("enterprise"); - assertThat(body.get("os").asText()).isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name"))); - assertThat(body.get("arch").asText()).isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch"))); - assertThat(body.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version")); - assertThat(body.get("platform_version").isNull()).isTrue(); - assertThat(body.get("features").isArray()).isTrue(); - assertThat(body.get("instance_id").asText()).matches( - "[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); - } + // Give the async call time to complete + Thread.sleep(2000); + + verify( + postRequestedFor(urlEqualTo("/v1/ping")) + .withHeader("Content-Type", containing("application/json"))); + + // Verify the request body has expected fields + var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping"))); + assertThat(requests).hasSize(1); + + JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); + assertThat(body.get("sdk").asText()).isEqualTo("java"); + assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); + assertThat(body.get("deployment_mode").asText()).isEqualTo("production"); + assertThat(body.get("instance_id").asText()).isNotEmpty(); + } + + @Test + @DisplayName("should not send ping when telemetry is disabled") + void testNoRequestWhenDisabled(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + // Disable via DO_NOT_TRACK + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + null, + false, + true, // hasCredentials + "1", // doNotTrack = disabled + null, + customUrl); + + Thread.sleep(1000); + + verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("should silently handle connection failure") + void testSilentFailure() { + // Point to a port that is almost certainly not listening + assertThatCode( + () -> { + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + null, + false, + true, // hasCredentials + null, + null, + "http://127.0.0.1:1" // port 1 - connection refused + ); + + // Give the async call time to run and fail + Thread.sleep(4000); + }) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("should not send ping in sandbox mode without explicit enable") + void testSandboxModeDefaultOff(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + TelemetryReporter.sendPing( + "sandbox", + "http://localhost:8080", + null, // no override + false, + true, // hasCredentials + null, + null, + customUrl); + + Thread.sleep(1000); + + verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("should send ping in sandbox mode when explicitly enabled via config") + void testSandboxModeExplicitEnable(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + TelemetryReporter.sendPing( + "sandbox", + "http://localhost:8080", + Boolean.TRUE, // explicit enable + false, + false, // hasCredentials (doesn't matter with explicit override) + null, + null, + customUrl); + + Thread.sleep(2000); + + verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("should send ping in production mode even without credentials") + void testProductionModeWithoutCredentials(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + Boolean.TRUE, + false, + false, // no credentials — no longer affects default + null, + null, + customUrl); + + Thread.sleep(2000); + + verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + // --- Additional tests for parity with Python SDK --- + + @Test + @DisplayName("each buildPayload call should generate a unique instance_id") + void testUniqueInstanceId() throws Exception { + String payload1 = TelemetryReporter.buildPayload("production", null); + String payload2 = TelemetryReporter.buildPayload("production", null); + String payload3 = TelemetryReporter.buildPayload("production", null); + + JsonNode root1 = objectMapper.readTree(payload1); + JsonNode root2 = objectMapper.readTree(payload2); + JsonNode root3 = objectMapper.readTree(payload3); + + String id1 = root1.get("instance_id").asText(); + String id2 = root2.get("instance_id").asText(); + String id3 = root3.get("instance_id").asText(); + + // All three should be valid UUIDs + assertThat(id1).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + assertThat(id2).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + assertThat(id3).matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + + // All three should be distinct + assertThat(id1).isNotEqualTo(id2); + assertThat(id1).isNotEqualTo(id3); + assertThat(id2).isNotEqualTo(id3); + } + + @Test + @DisplayName("config false in production should skip POST even with credentials") + void testConfigDisableInProduction(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + Boolean.FALSE, // config override disables + false, + true, // hasCredentials (would normally enable) + null, + null, + customUrl); + + Thread.sleep(1000); + + verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("should silently handle server timeout without crashing") + void testSilentFailureOnTimeout(WireMockRuntimeInfo wmRuntimeInfo) { + // Delay response for 5 seconds, exceeding the 3s timeout + stubFor(post("/v1/ping").willReturn(ok().withFixedDelay(5000))); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + assertThatCode( + () -> { + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + null, + false, + true, // hasCredentials + null, + null, + customUrl); + + // Wait long enough for the async call to hit the timeout and fail + Thread.sleep(5000); + }) + .doesNotThrowAnyException(); + } + + @Test + @DisplayName("should not crash when server returns HTTP 500") + void testNon200ResponseNoCrash(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(post("/v1/ping").willReturn(serverError().withBody("Internal Server Error"))); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) + assertThatCode( + () -> { + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + Boolean.TRUE, + false, + true, // hasCredentials + null, + null, + customUrl); + + // Give the async call time to complete + Thread.sleep(2000); + }) + .doesNotThrowAnyException(); + + // Verify the request was still made (the server just returned 500) + verify(exactly(1), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("AXONFLOW_TELEMETRY=off should skip POST even with credentials in production") + void testAxonflowTelemetrySkipsPost(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + TelemetryReporter.sendPing( + "production", + "http://localhost:8080", + null, + false, + true, // hasCredentials + null, + "off", // AXONFLOW_TELEMETRY=off + customUrl); + + Thread.sleep(1000); + + verify(exactly(0), postRequestedFor(urlEqualTo("/v1/ping"))); + } + + @Test + @DisplayName("should send correct payload fields in enterprise mode via HTTP") + void testPayloadDeploymentModeEnterprise(WireMockRuntimeInfo wmRuntimeInfo) throws Exception { + stubFor(post("/v1/ping").willReturn(ok())); + + String customUrl = wmRuntimeInfo.getHttpBaseUrl() + "/v1/ping"; + + // telemetryEnabled=true overrides localhost guard (WireMock runs on localhost) + // Use localhost:1 so detectPlatformVersion gets immediate connection-refused + // (localhost:8080 may have a running service that returns a version) + TelemetryReporter.sendPing( + "enterprise", + "http://localhost:1", + Boolean.TRUE, + false, + true, // hasCredentials + null, + null, + customUrl); + + Thread.sleep(2000); + + verify( + exactly(1), + postRequestedFor(urlEqualTo("/v1/ping")) + .withHeader("Content-Type", containing("application/json"))); + + var requests = WireMock.findAll(postRequestedFor(urlEqualTo("/v1/ping"))); + assertThat(requests).hasSize(1); + + JsonNode body = objectMapper.readTree(requests.get(0).getBodyAsString()); + assertThat(body.get("sdk").asText()).isEqualTo("java"); + assertThat(body.get("sdk_version").asText()).isEqualTo(AxonFlowConfig.SDK_VERSION); + assertThat(body.get("deployment_mode").asText()).isEqualTo("enterprise"); + assertThat(body.get("os").asText()) + .isEqualTo(TelemetryReporter.normalizeOS(System.getProperty("os.name"))); + assertThat(body.get("arch").asText()) + .isEqualTo(TelemetryReporter.normalizeArch(System.getProperty("os.arch"))); + assertThat(body.get("runtime_version").asText()).isEqualTo(System.getProperty("java.version")); + assertThat(body.get("platform_version").isNull()).isTrue(); + assertThat(body.get("features").isArray()).isTrue(); + assertThat(body.get("instance_id").asText()) + .matches("[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/AdditionalTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/AdditionalTypesTest.java index 2a22305..0123c4f 100644 --- a/src/test/java/com/getaxonflow/sdk/types/AdditionalTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/AdditionalTypesTest.java @@ -6,773 +6,703 @@ */ package com.getaxonflow.sdk.types; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; import java.time.Instant; import java.util.HashMap; import java.util.List; import java.util.Map; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; @DisplayName("Additional Types Tests") class AdditionalTypesTest { - @Nested - @DisplayName("PolicyInfo Tests") - class PolicyInfoTests { - - @Test - @DisplayName("Should create PolicyInfo with all fields") - void testPolicyInfoCreation() { - PolicyInfo info = new PolicyInfo( - List.of("policy1", "policy2"), - List.of("static-check-1"), - "17.48ms", - "tenant-123", - 0.75, - null - ); - - assertThat(info.getPoliciesEvaluated()).containsExactly("policy1", "policy2"); - assertThat(info.getStaticChecks()).containsExactly("static-check-1"); - assertThat(info.getProcessingTime()).isEqualTo("17.48ms"); - assertThat(info.getTenantId()).isEqualTo("tenant-123"); - assertThat(info.getRiskScore()).isEqualTo(0.75); - } - - @Test - @DisplayName("Should handle null lists") - void testPolicyInfoNullLists() { - PolicyInfo info = new PolicyInfo(null, null, "10ms", "tenant", null, null); - - assertThat(info.getPoliciesEvaluated()).isEmpty(); - assertThat(info.getStaticChecks()).isEmpty(); - } - - @Test - @DisplayName("getProcessingDuration should parse milliseconds") - void testProcessingDurationMilliseconds() { - PolicyInfo info = new PolicyInfo(null, null, "17.48ms", null, null, null); - - Duration duration = info.getProcessingDuration(); - assertThat(duration.toNanos()).isGreaterThan(17_000_000L); - assertThat(duration.toNanos()).isLessThan(18_000_000L); - } - - @Test - @DisplayName("getProcessingDuration should parse seconds") - void testProcessingDurationSeconds() { - PolicyInfo info = new PolicyInfo(null, null, "1.5s", null, null, null); - - Duration duration = info.getProcessingDuration(); - assertThat(duration.toMillis()).isGreaterThanOrEqualTo(1500L); - } - - @Test - @DisplayName("getProcessingDuration should parse microseconds (us)") - void testProcessingDurationMicroseconds() { - // Note: the implementation may not handle 'us' suffix perfectly - PolicyInfo info = new PolicyInfo(null, null, "500µs", null, null, null); - - Duration duration = info.getProcessingDuration(); - // If µs parsing works, we get microseconds; otherwise it falls through - assertThat(duration).isNotNull(); - } - - @Test - @DisplayName("getProcessingDuration should handle ns suffix") - void testProcessingDurationNanoseconds() { - PolicyInfo info = new PolicyInfo(null, null, "1000ns", null, null, null); - - Duration duration = info.getProcessingDuration(); - // Implementation may return ZERO if parsing fails - assertThat(duration).isNotNull(); - } - - @Test - @DisplayName("getProcessingDuration should handle empty/null") - void testProcessingDurationEmpty() { - PolicyInfo info1 = new PolicyInfo(null, null, null, null, null, null); - assertThat(info1.getProcessingDuration()).isEqualTo(Duration.ZERO); - - PolicyInfo info2 = new PolicyInfo(null, null, "", null, null, null); - assertThat(info2.getProcessingDuration()).isEqualTo(Duration.ZERO); - } - - @Test - @DisplayName("getProcessingDuration should handle invalid format") - void testProcessingDurationInvalid() { - PolicyInfo info = new PolicyInfo(null, null, "invalid", null, null, null); - assertThat(info.getProcessingDuration()).isEqualTo(Duration.ZERO); - } - - @Test - @DisplayName("getProcessingDuration should handle raw number as milliseconds") - void testProcessingDurationRawNumber() { - PolicyInfo info = new PolicyInfo(null, null, "100", null, null, null); - - Duration duration = info.getProcessingDuration(); - assertThat(duration.toMillis()).isEqualTo(100L); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testPolicyInfoEqualsHashCode() { - PolicyInfo info1 = new PolicyInfo( - List.of("policy1"), - List.of("check1"), - "10ms", - "tenant1", - 0.5, - null - ); - - PolicyInfo info2 = new PolicyInfo( - List.of("policy1"), - List.of("check1"), - "10ms", - "tenant1", - 0.5, - null - ); - - PolicyInfo info3 = new PolicyInfo( - List.of("policy2"), - List.of("check1"), - "10ms", - "tenant1", - 0.5, - null - ); - - assertThat(info1).isEqualTo(info2); - assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); - assertThat(info1).isNotEqualTo(info3); - assertThat(info1).isNotEqualTo(null); - assertThat(info1).isNotEqualTo("string"); - assertThat(info1).isEqualTo(info1); - } - - @Test - @DisplayName("toString should include all fields") - void testPolicyInfoToString() { - PolicyInfo info = new PolicyInfo( - List.of("policy1"), - List.of("check1"), - "10ms", - "tenant1", - 0.5, - null - ); - - String str = info.toString(); - assertThat(str).contains("policy1"); - assertThat(str).contains("check1"); - assertThat(str).contains("10ms"); - assertThat(str).contains("tenant1"); - assertThat(str).contains("0.5"); - } - } - - @Nested - @DisplayName("HealthStatus Tests") - class HealthStatusTests { - - @Test - @DisplayName("Should create HealthStatus with all fields") - void testHealthStatusCreation() { - Map components = new HashMap<>(); - components.put("database", "healthy"); - components.put("cache", "healthy"); - - HealthStatus status = new HealthStatus( - "healthy", - "1.0.0", - "24h30m", - components, - null, - null - ); - - assertThat(status.getStatus()).isEqualTo("healthy"); - assertThat(status.getVersion()).isEqualTo("1.0.0"); - assertThat(status.getUptime()).isEqualTo("24h30m"); - assertThat(status.getComponents()).containsEntry("database", "healthy"); - } - - @Test - @DisplayName("Should handle null components") - void testHealthStatusNullComponents() { - HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); - - assertThat(status.getComponents()).isEmpty(); - } - - @Test - @DisplayName("isHealthy should return true for healthy status") - void testIsHealthyTrue() { - HealthStatus status1 = new HealthStatus("healthy", null, null, null, null, null); - assertThat(status1.isHealthy()).isTrue(); - - HealthStatus status2 = new HealthStatus("HEALTHY", null, null, null, null, null); - assertThat(status2.isHealthy()).isTrue(); - - HealthStatus status3 = new HealthStatus("ok", null, null, null, null, null); - assertThat(status3.isHealthy()).isTrue(); - - HealthStatus status4 = new HealthStatus("OK", null, null, null, null, null); - assertThat(status4.isHealthy()).isTrue(); - } - - @Test - @DisplayName("isHealthy should return false for unhealthy status") - void testIsHealthyFalse() { - HealthStatus status1 = new HealthStatus("unhealthy", null, null, null, null, null); - assertThat(status1.isHealthy()).isFalse(); - - HealthStatus status2 = new HealthStatus("degraded", null, null, null, null, null); - assertThat(status2.isHealthy()).isFalse(); - - HealthStatus status3 = new HealthStatus(null, null, null, null, null, null); - assertThat(status3.isHealthy()).isFalse(); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testHealthStatusEqualsHashCode() { - HealthStatus status1 = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); - HealthStatus status2 = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); - HealthStatus status3 = new HealthStatus("unhealthy", "1.0.0", "1h", null, null, null); - - assertThat(status1).isEqualTo(status2); - assertThat(status1.hashCode()).isEqualTo(status2.hashCode()); - assertThat(status1).isNotEqualTo(status3); - assertThat(status1).isNotEqualTo(null); - assertThat(status1).isNotEqualTo("string"); - assertThat(status1).isEqualTo(status1); - } - - @Test - @DisplayName("toString should include relevant fields") - void testHealthStatusToString() { - HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); - - String str = status.toString(); - assertThat(str).contains("healthy"); - assertThat(str).contains("1.0.0"); - assertThat(str).contains("1h"); - } - } - - @Nested - @DisplayName("ConnectorResponse Tests") - class ConnectorResponseTests { - - @Test - @DisplayName("Should create ConnectorResponse with all fields") - void testConnectorResponseCreation() { - Map data = new HashMap<>(); - data.put("result", "success"); - - ConnectorResponse response = new ConnectorResponse( - true, - data, - null, - "connector-123", - "query", - "15.5ms" - ); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.getData()).isEqualTo(data); - assertThat(response.getError()).isNull(); - assertThat(response.getConnectorId()).isEqualTo("connector-123"); - assertThat(response.getOperation()).isEqualTo("query"); - assertThat(response.getProcessingTime()).isEqualTo("15.5ms"); - } - - @Test - @DisplayName("Should create error ConnectorResponse") - void testConnectorResponseError() { - ConnectorResponse response = new ConnectorResponse( - false, - null, - "Connection failed", - "connector-456", - "install", - "0ms" - ); - - assertThat(response.isSuccess()).isFalse(); - assertThat(response.getData()).isNull(); - assertThat(response.getError()).isEqualTo("Connection failed"); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testConnectorResponseEqualsHashCode() { - ConnectorResponse response1 = new ConnectorResponse( - true, null, null, "conn1", "query", null - ); - ConnectorResponse response2 = new ConnectorResponse( - true, null, null, "conn1", "query", null - ); - ConnectorResponse response3 = new ConnectorResponse( - false, null, null, "conn1", "query", null - ); - - assertThat(response1).isEqualTo(response2); - assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); - assertThat(response1).isNotEqualTo(response3); - assertThat(response1).isNotEqualTo(null); - assertThat(response1).isNotEqualTo("string"); - assertThat(response1).isEqualTo(response1); - } - - @Test - @DisplayName("toString should include relevant fields") - void testConnectorResponseToString() { - ConnectorResponse response = new ConnectorResponse( - true, null, null, "conn-123", "query", null - ); - - String str = response.toString(); - assertThat(str).contains("success=true"); - assertThat(str).contains("conn-123"); - assertThat(str).contains("query"); - } - } - - @Nested - @DisplayName("TokenUsage Tests") - class TokenUsageTests { - - @Test - @DisplayName("TokenUsage.of should calculate total tokens") - void testTokenUsageOf() { - TokenUsage usage = TokenUsage.of(100, 50); - - assertThat(usage.getPromptTokens()).isEqualTo(100); - assertThat(usage.getCompletionTokens()).isEqualTo(50); - assertThat(usage.getTotalTokens()).isEqualTo(150); - } - - @Test - @DisplayName("TokenUsage equals and hashCode") - void testTokenUsageEqualsHashCode() { - TokenUsage usage1 = TokenUsage.of(100, 50); - TokenUsage usage2 = TokenUsage.of(100, 50); - TokenUsage usage3 = TokenUsage.of(100, 60); - - assertThat(usage1).isEqualTo(usage2); - assertThat(usage1.hashCode()).isEqualTo(usage2.hashCode()); - assertThat(usage1).isNotEqualTo(usage3); - } - } - - @Nested - @DisplayName("Mode Tests") - class ModeTests { - - @Test - @DisplayName("Mode enum values should exist") - void testModeEnumValues() { - assertThat(Mode.values()).contains(Mode.PRODUCTION, Mode.SANDBOX); - } - - @Test - @DisplayName("Mode fromValue should parse valid modes") - void testModeFromValue() { - assertThat(Mode.fromValue("production")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue("PRODUCTION")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue("sandbox")).isEqualTo(Mode.SANDBOX); - assertThat(Mode.fromValue("SANDBOX")).isEqualTo(Mode.SANDBOX); - } - - @Test - @DisplayName("Mode fromValue should return PRODUCTION for invalid/null mode") - void testModeFromValueInvalid() { - assertThat(Mode.fromValue("invalid")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue(null)).isEqualTo(Mode.PRODUCTION); - } - - @Test - @DisplayName("Mode getValue should return lowercase") - void testModeGetValue() { - assertThat(Mode.PRODUCTION.getValue()).isEqualTo("production"); - assertThat(Mode.SANDBOX.getValue()).isEqualTo("sandbox"); - } - } - - @Nested - @DisplayName("RequestType Tests") - class RequestTypeTests { - - @Test - @DisplayName("RequestType enum values should exist") - void testRequestTypeEnumValues() { - assertThat(RequestType.values()).contains( - RequestType.CHAT, - RequestType.SQL, - RequestType.MCP_QUERY, - RequestType.MULTI_AGENT_PLAN - ); - } - - @Test - @DisplayName("RequestType fromValue should parse valid types") - void testRequestTypeFromValue() { - assertThat(RequestType.fromValue("chat")).isEqualTo(RequestType.CHAT); - assertThat(RequestType.fromValue("CHAT")).isEqualTo(RequestType.CHAT); - assertThat(RequestType.fromValue("sql")).isEqualTo(RequestType.SQL); - assertThat(RequestType.fromValue("mcp-query")).isEqualTo(RequestType.MCP_QUERY); - assertThat(RequestType.fromValue("multi-agent-plan")).isEqualTo(RequestType.MULTI_AGENT_PLAN); - } - - @Test - @DisplayName("RequestType fromValue should throw for invalid type") - void testRequestTypeFromValueInvalid() { - assertThatThrownBy(() -> RequestType.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("RequestType fromValue should throw for null") - void testRequestTypeFromValueNull() { - assertThatThrownBy(() -> RequestType.fromValue(null)) - .isInstanceOf(IllegalArgumentException.class); - } - - @Test - @DisplayName("RequestType getValue should return correct value") - void testRequestTypeGetValue() { - assertThat(RequestType.CHAT.getValue()).isEqualTo("chat"); - assertThat(RequestType.SQL.getValue()).isEqualTo("sql"); - assertThat(RequestType.MCP_QUERY.getValue()).isEqualTo("mcp-query"); - assertThat(RequestType.MULTI_AGENT_PLAN.getValue()).isEqualTo("multi-agent-plan"); - } - } - - @Nested - @DisplayName("RateLimitInfo Tests") - class RateLimitInfoTests { - - @Test - @DisplayName("Should create RateLimitInfo correctly") - void testRateLimitInfoCreation() { - Instant resetAt = Instant.now().plusSeconds(60); - RateLimitInfo info = new RateLimitInfo(100, 80, resetAt); - - assertThat(info.getLimit()).isEqualTo(100); - assertThat(info.getRemaining()).isEqualTo(80); - assertThat(info.getResetAt()).isEqualTo(resetAt); - } - - @Test - @DisplayName("isExceeded should return true when remaining is 0") - void testIsExceededTrue() { - RateLimitInfo info = new RateLimitInfo(100, 0, null); - assertThat(info.isExceeded()).isTrue(); - } - - @Test - @DisplayName("isExceeded should return true when remaining is negative") - void testIsExceededNegative() { - RateLimitInfo info = new RateLimitInfo(100, -1, null); - assertThat(info.isExceeded()).isTrue(); - } - - @Test - @DisplayName("isExceeded should return false when remaining is positive") - void testIsExceededFalse() { - RateLimitInfo info = new RateLimitInfo(100, 50, null); - assertThat(info.isExceeded()).isFalse(); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testRateLimitInfoEqualsHashCode() { - Instant resetAt = Instant.now(); - RateLimitInfo info1 = new RateLimitInfo(100, 80, resetAt); - RateLimitInfo info2 = new RateLimitInfo(100, 80, resetAt); - RateLimitInfo info3 = new RateLimitInfo(100, 70, resetAt); - - assertThat(info1).isEqualTo(info2); - assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); - assertThat(info1).isNotEqualTo(info3); - assertThat(info1).isNotEqualTo(null); - assertThat(info1).isNotEqualTo("string"); - assertThat(info1).isEqualTo(info1); - } - - @Test - @DisplayName("toString should include all fields") - void testRateLimitInfoToString() { - Instant resetAt = Instant.parse("2025-01-15T10:30:00Z"); - RateLimitInfo info = new RateLimitInfo(100, 80, resetAt); - - String str = info.toString(); - assertThat(str).contains("100"); - assertThat(str).contains("80"); - } - } - - @Nested - @DisplayName("ConnectorInfo Tests") - class ConnectorInfoTests { - - @Test - @DisplayName("Should create ConnectorInfo correctly") - void testConnectorInfoCreation() { - Map configSchema = new HashMap<>(); - configSchema.put("token", "string"); - - ConnectorInfo info = new ConnectorInfo( - "conn-123", - "GitHub Connector", - "A connector for GitHub", - "github", - "1.0.0", - List.of("read", "write"), - configSchema, - true, - true - ); - - assertThat(info.getId()).isEqualTo("conn-123"); - assertThat(info.getName()).isEqualTo("GitHub Connector"); - assertThat(info.getType()).isEqualTo("github"); - assertThat(info.getDescription()).isEqualTo("A connector for GitHub"); - assertThat(info.getVersion()).isEqualTo("1.0.0"); - assertThat(info.getCapabilities()).containsExactly("read", "write"); - assertThat(info.getConfigSchema()).containsEntry("token", "string"); - assertThat(info.isInstalled()).isTrue(); - assertThat(info.isEnabled()).isTrue(); - } - - @Test - @DisplayName("Should handle null capabilities and configSchema") - void testConnectorInfoNullCollections() { - ConnectorInfo info = new ConnectorInfo( - "id", "name", "desc", "type", "1.0", null, null, null, null - ); - - assertThat(info.getCapabilities()).isEmpty(); - assertThat(info.getConfigSchema()).isEmpty(); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testConnectorInfoEqualsHashCode() { - ConnectorInfo info1 = new ConnectorInfo( - "id", "name", "desc", "type", "1.0", null, null, null, null - ); - ConnectorInfo info2 = new ConnectorInfo( - "id", "name", "desc", "type", "1.0", null, null, null, null - ); - ConnectorInfo info3 = new ConnectorInfo( - "id2", "name", "desc", "type", "1.0", null, null, null, null - ); - - assertThat(info1).isEqualTo(info2); - assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); - assertThat(info1).isNotEqualTo(info3); - assertThat(info1).isNotEqualTo(null); - assertThat(info1).isNotEqualTo("string"); - assertThat(info1).isEqualTo(info1); - } - - @Test - @DisplayName("toString should include relevant fields") - void testConnectorInfoToString() { - ConnectorInfo info = new ConnectorInfo( - "conn-123", "GitHub", "desc", "github", "1.0", null, null, true, false - ); - - String str = info.toString(); - assertThat(str).contains("conn-123"); - assertThat(str).contains("GitHub"); - assertThat(str).contains("github"); - assertThat(str).contains("installed=true"); - assertThat(str).contains("enabled=false"); - } - } - - @Nested - @DisplayName("ConnectorQuery Tests") - class ConnectorQueryTests { - - @Test - @DisplayName("Should build ConnectorQuery correctly") - void testConnectorQueryBuilder() { - Map params = new HashMap<>(); - params.put("limit", 10); - - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("conn-123") - .operation("list") - .parameters(params) - .build(); - - assertThat(query.getConnectorId()).isEqualTo("conn-123"); - assertThat(query.getOperation()).isEqualTo("list"); - assertThat(query.getParameters()).containsEntry("limit", 10); - } - - @Test - @DisplayName("Should build ConnectorQuery with userToken and timeout") - void testConnectorQueryBuilderAllFields() { - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("conn-456") - .operation("execute") - .userToken("user-token-123") - .timeoutMs(5000) - .addParameter("key", "value") - .build(); - - assertThat(query.getConnectorId()).isEqualTo("conn-456"); - assertThat(query.getOperation()).isEqualTo("execute"); - assertThat(query.getUserToken()).isEqualTo("user-token-123"); - assertThat(query.getTimeoutMs()).isEqualTo(5000); - assertThat(query.getParameters()).containsEntry("key", "value"); - } - - @Test - @DisplayName("ConnectorQuery.Builder should require connectorId") - void testConnectorQueryBuilderRequiresConnectorId() { - assertThatThrownBy(() -> ConnectorQuery.builder() - .operation("list") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("ConnectorQuery.Builder should require operation") - void testConnectorQueryBuilderRequiresOperation() { - assertThatThrownBy(() -> ConnectorQuery.builder() - .connectorId("conn-123") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("addParameter should create parameters map if null") - void testConnectorQueryAddParameter() { - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("conn") - .operation("op") - .addParameter("key1", "value1") - .addParameter("key2", "value2") - .build(); - - assertThat(query.getParameters()).containsEntry("key1", "value1"); - assertThat(query.getParameters()).containsEntry("key2", "value2"); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testConnectorQueryEqualsHashCode() { - ConnectorQuery query1 = ConnectorQuery.builder() - .connectorId("conn-1") - .operation("op") - .build(); - ConnectorQuery query2 = ConnectorQuery.builder() - .connectorId("conn-1") - .operation("op") - .build(); - ConnectorQuery query3 = ConnectorQuery.builder() - .connectorId("conn-2") - .operation("op") - .build(); - - assertThat(query1).isEqualTo(query2); - assertThat(query1.hashCode()).isEqualTo(query2.hashCode()); - assertThat(query1).isNotEqualTo(query3); - assertThat(query1).isNotEqualTo(null); - assertThat(query1).isNotEqualTo("string"); - assertThat(query1).isEqualTo(query1); - } - - @Test - @DisplayName("toString should include relevant fields") - void testConnectorQueryToString() { - ConnectorQuery query = ConnectorQuery.builder() - .connectorId("conn-123") - .operation("list") - .userToken("user") - .timeoutMs(3000) - .build(); - - String str = query.toString(); - assertThat(str).contains("conn-123"); - assertThat(str).contains("list"); - assertThat(str).contains("user"); - assertThat(str).contains("3000"); - } - } - - @Nested - @DisplayName("AuditResult Tests") - class AuditResultTests { - - @Test - @DisplayName("Should create AuditResult correctly") - void testAuditResultCreation() { - AuditResult result = new AuditResult( - true, - "audit-123", - "Audit recorded successfully", - null - ); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getAuditId()).isEqualTo("audit-123"); - assertThat(result.getMessage()).isEqualTo("Audit recorded successfully"); - assertThat(result.getError()).isNull(); - } - - @Test - @DisplayName("Should create error AuditResult") - void testAuditResultError() { - AuditResult result = new AuditResult( - false, - null, - null, - "Audit failed" - ); - - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getAuditId()).isNull(); - assertThat(result.getError()).isEqualTo("Audit failed"); - } - - @Test - @DisplayName("equals and hashCode should work correctly") - void testAuditResultEqualsHashCode() { - AuditResult result1 = new AuditResult(true, "id1", "msg", null); - AuditResult result2 = new AuditResult(true, "id1", "msg", null); - AuditResult result3 = new AuditResult(false, "id1", "msg", null); - - assertThat(result1).isEqualTo(result2); - assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); - assertThat(result1).isNotEqualTo(result3); - assertThat(result1).isNotEqualTo(null); - assertThat(result1).isNotEqualTo("string"); - assertThat(result1).isEqualTo(result1); - } - - @Test - @DisplayName("toString should include relevant fields") - void testAuditResultToString() { - AuditResult result = new AuditResult(true, "audit-123", "Success", null); - - String str = result.toString(); - assertThat(str).contains("true"); - assertThat(str).contains("audit-123"); - } + @Nested + @DisplayName("PolicyInfo Tests") + class PolicyInfoTests { + + @Test + @DisplayName("Should create PolicyInfo with all fields") + void testPolicyInfoCreation() { + PolicyInfo info = + new PolicyInfo( + List.of("policy1", "policy2"), + List.of("static-check-1"), + "17.48ms", + "tenant-123", + 0.75, + null); + + assertThat(info.getPoliciesEvaluated()).containsExactly("policy1", "policy2"); + assertThat(info.getStaticChecks()).containsExactly("static-check-1"); + assertThat(info.getProcessingTime()).isEqualTo("17.48ms"); + assertThat(info.getTenantId()).isEqualTo("tenant-123"); + assertThat(info.getRiskScore()).isEqualTo(0.75); + } + + @Test + @DisplayName("Should handle null lists") + void testPolicyInfoNullLists() { + PolicyInfo info = new PolicyInfo(null, null, "10ms", "tenant", null, null); + + assertThat(info.getPoliciesEvaluated()).isEmpty(); + assertThat(info.getStaticChecks()).isEmpty(); + } + + @Test + @DisplayName("getProcessingDuration should parse milliseconds") + void testProcessingDurationMilliseconds() { + PolicyInfo info = new PolicyInfo(null, null, "17.48ms", null, null, null); + + Duration duration = info.getProcessingDuration(); + assertThat(duration.toNanos()).isGreaterThan(17_000_000L); + assertThat(duration.toNanos()).isLessThan(18_000_000L); + } + + @Test + @DisplayName("getProcessingDuration should parse seconds") + void testProcessingDurationSeconds() { + PolicyInfo info = new PolicyInfo(null, null, "1.5s", null, null, null); + + Duration duration = info.getProcessingDuration(); + assertThat(duration.toMillis()).isGreaterThanOrEqualTo(1500L); + } + + @Test + @DisplayName("getProcessingDuration should parse microseconds (us)") + void testProcessingDurationMicroseconds() { + // Note: the implementation may not handle 'us' suffix perfectly + PolicyInfo info = new PolicyInfo(null, null, "500µs", null, null, null); + + Duration duration = info.getProcessingDuration(); + // If µs parsing works, we get microseconds; otherwise it falls through + assertThat(duration).isNotNull(); + } + + @Test + @DisplayName("getProcessingDuration should handle ns suffix") + void testProcessingDurationNanoseconds() { + PolicyInfo info = new PolicyInfo(null, null, "1000ns", null, null, null); + + Duration duration = info.getProcessingDuration(); + // Implementation may return ZERO if parsing fails + assertThat(duration).isNotNull(); + } + + @Test + @DisplayName("getProcessingDuration should handle empty/null") + void testProcessingDurationEmpty() { + PolicyInfo info1 = new PolicyInfo(null, null, null, null, null, null); + assertThat(info1.getProcessingDuration()).isEqualTo(Duration.ZERO); + + PolicyInfo info2 = new PolicyInfo(null, null, "", null, null, null); + assertThat(info2.getProcessingDuration()).isEqualTo(Duration.ZERO); + } + + @Test + @DisplayName("getProcessingDuration should handle invalid format") + void testProcessingDurationInvalid() { + PolicyInfo info = new PolicyInfo(null, null, "invalid", null, null, null); + assertThat(info.getProcessingDuration()).isEqualTo(Duration.ZERO); + } + + @Test + @DisplayName("getProcessingDuration should handle raw number as milliseconds") + void testProcessingDurationRawNumber() { + PolicyInfo info = new PolicyInfo(null, null, "100", null, null, null); + + Duration duration = info.getProcessingDuration(); + assertThat(duration.toMillis()).isEqualTo(100L); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testPolicyInfoEqualsHashCode() { + PolicyInfo info1 = + new PolicyInfo(List.of("policy1"), List.of("check1"), "10ms", "tenant1", 0.5, null); + + PolicyInfo info2 = + new PolicyInfo(List.of("policy1"), List.of("check1"), "10ms", "tenant1", 0.5, null); + + PolicyInfo info3 = + new PolicyInfo(List.of("policy2"), List.of("check1"), "10ms", "tenant1", 0.5, null); + + assertThat(info1).isEqualTo(info2); + assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); + assertThat(info1).isNotEqualTo(info3); + assertThat(info1).isNotEqualTo(null); + assertThat(info1).isNotEqualTo("string"); + assertThat(info1).isEqualTo(info1); + } + + @Test + @DisplayName("toString should include all fields") + void testPolicyInfoToString() { + PolicyInfo info = + new PolicyInfo(List.of("policy1"), List.of("check1"), "10ms", "tenant1", 0.5, null); + + String str = info.toString(); + assertThat(str).contains("policy1"); + assertThat(str).contains("check1"); + assertThat(str).contains("10ms"); + assertThat(str).contains("tenant1"); + assertThat(str).contains("0.5"); + } + } + + @Nested + @DisplayName("HealthStatus Tests") + class HealthStatusTests { + + @Test + @DisplayName("Should create HealthStatus with all fields") + void testHealthStatusCreation() { + Map components = new HashMap<>(); + components.put("database", "healthy"); + components.put("cache", "healthy"); + + HealthStatus status = new HealthStatus("healthy", "1.0.0", "24h30m", components, null, null); + + assertThat(status.getStatus()).isEqualTo("healthy"); + assertThat(status.getVersion()).isEqualTo("1.0.0"); + assertThat(status.getUptime()).isEqualTo("24h30m"); + assertThat(status.getComponents()).containsEntry("database", "healthy"); + } + + @Test + @DisplayName("Should handle null components") + void testHealthStatusNullComponents() { + HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); + + assertThat(status.getComponents()).isEmpty(); + } + + @Test + @DisplayName("isHealthy should return true for healthy status") + void testIsHealthyTrue() { + HealthStatus status1 = new HealthStatus("healthy", null, null, null, null, null); + assertThat(status1.isHealthy()).isTrue(); + + HealthStatus status2 = new HealthStatus("HEALTHY", null, null, null, null, null); + assertThat(status2.isHealthy()).isTrue(); + + HealthStatus status3 = new HealthStatus("ok", null, null, null, null, null); + assertThat(status3.isHealthy()).isTrue(); + + HealthStatus status4 = new HealthStatus("OK", null, null, null, null, null); + assertThat(status4.isHealthy()).isTrue(); + } + + @Test + @DisplayName("isHealthy should return false for unhealthy status") + void testIsHealthyFalse() { + HealthStatus status1 = new HealthStatus("unhealthy", null, null, null, null, null); + assertThat(status1.isHealthy()).isFalse(); + + HealthStatus status2 = new HealthStatus("degraded", null, null, null, null, null); + assertThat(status2.isHealthy()).isFalse(); + + HealthStatus status3 = new HealthStatus(null, null, null, null, null, null); + assertThat(status3.isHealthy()).isFalse(); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testHealthStatusEqualsHashCode() { + HealthStatus status1 = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); + HealthStatus status2 = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); + HealthStatus status3 = new HealthStatus("unhealthy", "1.0.0", "1h", null, null, null); + + assertThat(status1).isEqualTo(status2); + assertThat(status1.hashCode()).isEqualTo(status2.hashCode()); + assertThat(status1).isNotEqualTo(status3); + assertThat(status1).isNotEqualTo(null); + assertThat(status1).isNotEqualTo("string"); + assertThat(status1).isEqualTo(status1); + } + + @Test + @DisplayName("toString should include relevant fields") + void testHealthStatusToString() { + HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); + + String str = status.toString(); + assertThat(str).contains("healthy"); + assertThat(str).contains("1.0.0"); + assertThat(str).contains("1h"); + } + } + + @Nested + @DisplayName("ConnectorResponse Tests") + class ConnectorResponseTests { + + @Test + @DisplayName("Should create ConnectorResponse with all fields") + void testConnectorResponseCreation() { + Map data = new HashMap<>(); + data.put("result", "success"); + + ConnectorResponse response = + new ConnectorResponse(true, data, null, "connector-123", "query", "15.5ms"); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getData()).isEqualTo(data); + assertThat(response.getError()).isNull(); + assertThat(response.getConnectorId()).isEqualTo("connector-123"); + assertThat(response.getOperation()).isEqualTo("query"); + assertThat(response.getProcessingTime()).isEqualTo("15.5ms"); + } + + @Test + @DisplayName("Should create error ConnectorResponse") + void testConnectorResponseError() { + ConnectorResponse response = + new ConnectorResponse( + false, null, "Connection failed", "connector-456", "install", "0ms"); + + assertThat(response.isSuccess()).isFalse(); + assertThat(response.getData()).isNull(); + assertThat(response.getError()).isEqualTo("Connection failed"); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testConnectorResponseEqualsHashCode() { + ConnectorResponse response1 = new ConnectorResponse(true, null, null, "conn1", "query", null); + ConnectorResponse response2 = new ConnectorResponse(true, null, null, "conn1", "query", null); + ConnectorResponse response3 = + new ConnectorResponse(false, null, null, "conn1", "query", null); + + assertThat(response1).isEqualTo(response2); + assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); + assertThat(response1).isNotEqualTo(response3); + assertThat(response1).isNotEqualTo(null); + assertThat(response1).isNotEqualTo("string"); + assertThat(response1).isEqualTo(response1); + } + + @Test + @DisplayName("toString should include relevant fields") + void testConnectorResponseToString() { + ConnectorResponse response = + new ConnectorResponse(true, null, null, "conn-123", "query", null); + + String str = response.toString(); + assertThat(str).contains("success=true"); + assertThat(str).contains("conn-123"); + assertThat(str).contains("query"); + } + } + + @Nested + @DisplayName("TokenUsage Tests") + class TokenUsageTests { + + @Test + @DisplayName("TokenUsage.of should calculate total tokens") + void testTokenUsageOf() { + TokenUsage usage = TokenUsage.of(100, 50); + + assertThat(usage.getPromptTokens()).isEqualTo(100); + assertThat(usage.getCompletionTokens()).isEqualTo(50); + assertThat(usage.getTotalTokens()).isEqualTo(150); + } + + @Test + @DisplayName("TokenUsage equals and hashCode") + void testTokenUsageEqualsHashCode() { + TokenUsage usage1 = TokenUsage.of(100, 50); + TokenUsage usage2 = TokenUsage.of(100, 50); + TokenUsage usage3 = TokenUsage.of(100, 60); + + assertThat(usage1).isEqualTo(usage2); + assertThat(usage1.hashCode()).isEqualTo(usage2.hashCode()); + assertThat(usage1).isNotEqualTo(usage3); + } + } + + @Nested + @DisplayName("Mode Tests") + class ModeTests { + + @Test + @DisplayName("Mode enum values should exist") + void testModeEnumValues() { + assertThat(Mode.values()).contains(Mode.PRODUCTION, Mode.SANDBOX); + } + + @Test + @DisplayName("Mode fromValue should parse valid modes") + void testModeFromValue() { + assertThat(Mode.fromValue("production")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue("PRODUCTION")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue("sandbox")).isEqualTo(Mode.SANDBOX); + assertThat(Mode.fromValue("SANDBOX")).isEqualTo(Mode.SANDBOX); + } + + @Test + @DisplayName("Mode fromValue should return PRODUCTION for invalid/null mode") + void testModeFromValueInvalid() { + assertThat(Mode.fromValue("invalid")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue(null)).isEqualTo(Mode.PRODUCTION); + } + + @Test + @DisplayName("Mode getValue should return lowercase") + void testModeGetValue() { + assertThat(Mode.PRODUCTION.getValue()).isEqualTo("production"); + assertThat(Mode.SANDBOX.getValue()).isEqualTo("sandbox"); + } + } + + @Nested + @DisplayName("RequestType Tests") + class RequestTypeTests { + + @Test + @DisplayName("RequestType enum values should exist") + void testRequestTypeEnumValues() { + assertThat(RequestType.values()) + .contains( + RequestType.CHAT, + RequestType.SQL, + RequestType.MCP_QUERY, + RequestType.MULTI_AGENT_PLAN); + } + + @Test + @DisplayName("RequestType fromValue should parse valid types") + void testRequestTypeFromValue() { + assertThat(RequestType.fromValue("chat")).isEqualTo(RequestType.CHAT); + assertThat(RequestType.fromValue("CHAT")).isEqualTo(RequestType.CHAT); + assertThat(RequestType.fromValue("sql")).isEqualTo(RequestType.SQL); + assertThat(RequestType.fromValue("mcp-query")).isEqualTo(RequestType.MCP_QUERY); + assertThat(RequestType.fromValue("multi-agent-plan")).isEqualTo(RequestType.MULTI_AGENT_PLAN); + } + + @Test + @DisplayName("RequestType fromValue should throw for invalid type") + void testRequestTypeFromValueInvalid() { + assertThatThrownBy(() -> RequestType.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("RequestType fromValue should throw for null") + void testRequestTypeFromValueNull() { + assertThatThrownBy(() -> RequestType.fromValue(null)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + @DisplayName("RequestType getValue should return correct value") + void testRequestTypeGetValue() { + assertThat(RequestType.CHAT.getValue()).isEqualTo("chat"); + assertThat(RequestType.SQL.getValue()).isEqualTo("sql"); + assertThat(RequestType.MCP_QUERY.getValue()).isEqualTo("mcp-query"); + assertThat(RequestType.MULTI_AGENT_PLAN.getValue()).isEqualTo("multi-agent-plan"); + } + } + + @Nested + @DisplayName("RateLimitInfo Tests") + class RateLimitInfoTests { + + @Test + @DisplayName("Should create RateLimitInfo correctly") + void testRateLimitInfoCreation() { + Instant resetAt = Instant.now().plusSeconds(60); + RateLimitInfo info = new RateLimitInfo(100, 80, resetAt); + + assertThat(info.getLimit()).isEqualTo(100); + assertThat(info.getRemaining()).isEqualTo(80); + assertThat(info.getResetAt()).isEqualTo(resetAt); + } + + @Test + @DisplayName("isExceeded should return true when remaining is 0") + void testIsExceededTrue() { + RateLimitInfo info = new RateLimitInfo(100, 0, null); + assertThat(info.isExceeded()).isTrue(); + } + + @Test + @DisplayName("isExceeded should return true when remaining is negative") + void testIsExceededNegative() { + RateLimitInfo info = new RateLimitInfo(100, -1, null); + assertThat(info.isExceeded()).isTrue(); + } + + @Test + @DisplayName("isExceeded should return false when remaining is positive") + void testIsExceededFalse() { + RateLimitInfo info = new RateLimitInfo(100, 50, null); + assertThat(info.isExceeded()).isFalse(); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testRateLimitInfoEqualsHashCode() { + Instant resetAt = Instant.now(); + RateLimitInfo info1 = new RateLimitInfo(100, 80, resetAt); + RateLimitInfo info2 = new RateLimitInfo(100, 80, resetAt); + RateLimitInfo info3 = new RateLimitInfo(100, 70, resetAt); + + assertThat(info1).isEqualTo(info2); + assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); + assertThat(info1).isNotEqualTo(info3); + assertThat(info1).isNotEqualTo(null); + assertThat(info1).isNotEqualTo("string"); + assertThat(info1).isEqualTo(info1); + } + + @Test + @DisplayName("toString should include all fields") + void testRateLimitInfoToString() { + Instant resetAt = Instant.parse("2025-01-15T10:30:00Z"); + RateLimitInfo info = new RateLimitInfo(100, 80, resetAt); + + String str = info.toString(); + assertThat(str).contains("100"); + assertThat(str).contains("80"); + } + } + + @Nested + @DisplayName("ConnectorInfo Tests") + class ConnectorInfoTests { + + @Test + @DisplayName("Should create ConnectorInfo correctly") + void testConnectorInfoCreation() { + Map configSchema = new HashMap<>(); + configSchema.put("token", "string"); + + ConnectorInfo info = + new ConnectorInfo( + "conn-123", + "GitHub Connector", + "A connector for GitHub", + "github", + "1.0.0", + List.of("read", "write"), + configSchema, + true, + true); + + assertThat(info.getId()).isEqualTo("conn-123"); + assertThat(info.getName()).isEqualTo("GitHub Connector"); + assertThat(info.getType()).isEqualTo("github"); + assertThat(info.getDescription()).isEqualTo("A connector for GitHub"); + assertThat(info.getVersion()).isEqualTo("1.0.0"); + assertThat(info.getCapabilities()).containsExactly("read", "write"); + assertThat(info.getConfigSchema()).containsEntry("token", "string"); + assertThat(info.isInstalled()).isTrue(); + assertThat(info.isEnabled()).isTrue(); + } + + @Test + @DisplayName("Should handle null capabilities and configSchema") + void testConnectorInfoNullCollections() { + ConnectorInfo info = + new ConnectorInfo("id", "name", "desc", "type", "1.0", null, null, null, null); + + assertThat(info.getCapabilities()).isEmpty(); + assertThat(info.getConfigSchema()).isEmpty(); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testConnectorInfoEqualsHashCode() { + ConnectorInfo info1 = + new ConnectorInfo("id", "name", "desc", "type", "1.0", null, null, null, null); + ConnectorInfo info2 = + new ConnectorInfo("id", "name", "desc", "type", "1.0", null, null, null, null); + ConnectorInfo info3 = + new ConnectorInfo("id2", "name", "desc", "type", "1.0", null, null, null, null); + + assertThat(info1).isEqualTo(info2); + assertThat(info1.hashCode()).isEqualTo(info2.hashCode()); + assertThat(info1).isNotEqualTo(info3); + assertThat(info1).isNotEqualTo(null); + assertThat(info1).isNotEqualTo("string"); + assertThat(info1).isEqualTo(info1); + } + + @Test + @DisplayName("toString should include relevant fields") + void testConnectorInfoToString() { + ConnectorInfo info = + new ConnectorInfo("conn-123", "GitHub", "desc", "github", "1.0", null, null, true, false); + + String str = info.toString(); + assertThat(str).contains("conn-123"); + assertThat(str).contains("GitHub"); + assertThat(str).contains("github"); + assertThat(str).contains("installed=true"); + assertThat(str).contains("enabled=false"); + } + } + + @Nested + @DisplayName("ConnectorQuery Tests") + class ConnectorQueryTests { + + @Test + @DisplayName("Should build ConnectorQuery correctly") + void testConnectorQueryBuilder() { + Map params = new HashMap<>(); + params.put("limit", 10); + + ConnectorQuery query = + ConnectorQuery.builder() + .connectorId("conn-123") + .operation("list") + .parameters(params) + .build(); + + assertThat(query.getConnectorId()).isEqualTo("conn-123"); + assertThat(query.getOperation()).isEqualTo("list"); + assertThat(query.getParameters()).containsEntry("limit", 10); + } + + @Test + @DisplayName("Should build ConnectorQuery with userToken and timeout") + void testConnectorQueryBuilderAllFields() { + ConnectorQuery query = + ConnectorQuery.builder() + .connectorId("conn-456") + .operation("execute") + .userToken("user-token-123") + .timeoutMs(5000) + .addParameter("key", "value") + .build(); + + assertThat(query.getConnectorId()).isEqualTo("conn-456"); + assertThat(query.getOperation()).isEqualTo("execute"); + assertThat(query.getUserToken()).isEqualTo("user-token-123"); + assertThat(query.getTimeoutMs()).isEqualTo(5000); + assertThat(query.getParameters()).containsEntry("key", "value"); + } + + @Test + @DisplayName("ConnectorQuery.Builder should require connectorId") + void testConnectorQueryBuilderRequiresConnectorId() { + assertThatThrownBy(() -> ConnectorQuery.builder().operation("list").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("ConnectorQuery.Builder should require operation") + void testConnectorQueryBuilderRequiresOperation() { + assertThatThrownBy(() -> ConnectorQuery.builder().connectorId("conn-123").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("addParameter should create parameters map if null") + void testConnectorQueryAddParameter() { + ConnectorQuery query = + ConnectorQuery.builder() + .connectorId("conn") + .operation("op") + .addParameter("key1", "value1") + .addParameter("key2", "value2") + .build(); + + assertThat(query.getParameters()).containsEntry("key1", "value1"); + assertThat(query.getParameters()).containsEntry("key2", "value2"); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testConnectorQueryEqualsHashCode() { + ConnectorQuery query1 = + ConnectorQuery.builder().connectorId("conn-1").operation("op").build(); + ConnectorQuery query2 = + ConnectorQuery.builder().connectorId("conn-1").operation("op").build(); + ConnectorQuery query3 = + ConnectorQuery.builder().connectorId("conn-2").operation("op").build(); + + assertThat(query1).isEqualTo(query2); + assertThat(query1.hashCode()).isEqualTo(query2.hashCode()); + assertThat(query1).isNotEqualTo(query3); + assertThat(query1).isNotEqualTo(null); + assertThat(query1).isNotEqualTo("string"); + assertThat(query1).isEqualTo(query1); + } + + @Test + @DisplayName("toString should include relevant fields") + void testConnectorQueryToString() { + ConnectorQuery query = + ConnectorQuery.builder() + .connectorId("conn-123") + .operation("list") + .userToken("user") + .timeoutMs(3000) + .build(); + + String str = query.toString(); + assertThat(str).contains("conn-123"); + assertThat(str).contains("list"); + assertThat(str).contains("user"); + assertThat(str).contains("3000"); + } + } + + @Nested + @DisplayName("AuditResult Tests") + class AuditResultTests { + + @Test + @DisplayName("Should create AuditResult correctly") + void testAuditResultCreation() { + AuditResult result = new AuditResult(true, "audit-123", "Audit recorded successfully", null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getAuditId()).isEqualTo("audit-123"); + assertThat(result.getMessage()).isEqualTo("Audit recorded successfully"); + assertThat(result.getError()).isNull(); + } + + @Test + @DisplayName("Should create error AuditResult") + void testAuditResultError() { + AuditResult result = new AuditResult(false, null, null, "Audit failed"); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getAuditId()).isNull(); + assertThat(result.getError()).isEqualTo("Audit failed"); + } + + @Test + @DisplayName("equals and hashCode should work correctly") + void testAuditResultEqualsHashCode() { + AuditResult result1 = new AuditResult(true, "id1", "msg", null); + AuditResult result2 = new AuditResult(true, "id1", "msg", null); + AuditResult result3 = new AuditResult(false, "id1", "msg", null); + + assertThat(result1).isEqualTo(result2); + assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); + assertThat(result1).isNotEqualTo(result3); + assertThat(result1).isNotEqualTo(null); + assertThat(result1).isNotEqualTo("string"); + assertThat(result1).isEqualTo(result1); + } + + @Test + @DisplayName("toString should include relevant fields") + void testAuditResultToString() { + AuditResult result = new AuditResult(true, "audit-123", "Success", null); + + String str = result.toString(); + assertThat(str).contains("true"); + assertThat(str).contains("audit-123"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/AuditTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/AuditTypesTest.java index 8554146..e168265 100644 --- a/src/test/java/com/getaxonflow/sdk/types/AuditTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/AuditTypesTest.java @@ -15,80 +15,81 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; @DisplayName("Audit Types") class AuditTypesTest { - private ObjectMapper objectMapper; + private ObjectMapper objectMapper; - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } - @Test - @DisplayName("TokenUsage - should create with factory method") - void tokenUsageShouldCreateWithFactory() { - TokenUsage usage = TokenUsage.of(100, 150); + @Test + @DisplayName("TokenUsage - should create with factory method") + void tokenUsageShouldCreateWithFactory() { + TokenUsage usage = TokenUsage.of(100, 150); - assertThat(usage.getPromptTokens()).isEqualTo(100); - assertThat(usage.getCompletionTokens()).isEqualTo(150); - assertThat(usage.getTotalTokens()).isEqualTo(250); - } + assertThat(usage.getPromptTokens()).isEqualTo(100); + assertThat(usage.getCompletionTokens()).isEqualTo(150); + assertThat(usage.getTotalTokens()).isEqualTo(250); + } - @Test - @DisplayName("TokenUsage - should deserialize from JSON") - void tokenUsageShouldDeserialize() throws Exception { - String json = "{" + @Test + @DisplayName("TokenUsage - should deserialize from JSON") + void tokenUsageShouldDeserialize() throws Exception { + String json = + "{" + "\"prompt_tokens\": 200," + "\"completion_tokens\": 300," + "\"total_tokens\": 500" + "}"; - TokenUsage usage = objectMapper.readValue(json, TokenUsage.class); - - assertThat(usage.getPromptTokens()).isEqualTo(200); - assertThat(usage.getCompletionTokens()).isEqualTo(300); - assertThat(usage.getTotalTokens()).isEqualTo(500); - } - - @Test - @DisplayName("TokenUsage - should serialize to JSON") - void tokenUsageShouldSerialize() throws Exception { - TokenUsage usage = TokenUsage.of(100, 200); - String json = objectMapper.writeValueAsString(usage); - - assertThat(json).contains("\"prompt_tokens\":100"); - assertThat(json).contains("\"completion_tokens\":200"); - assertThat(json).contains("\"total_tokens\":300"); - } - - @Test - @DisplayName("AuditOptions - should build with required fields") - void auditOptionsShouldBuildWithRequired() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx_123").clientId("test-client") - .build(); - - assertThat(options.getContextId()).isEqualTo("ctx_123"); - assertThat(options.getSuccess()).isTrue(); // Default - } - - @Test - @DisplayName("AuditOptions - should build with all fields") - void auditOptionsShouldBuildWithAllFields() { - TokenUsage tokenUsage = TokenUsage.of(100, 150); - - AuditOptions options = AuditOptions.builder() - .contextId("ctx_123").clientId("test-client") + TokenUsage usage = objectMapper.readValue(json, TokenUsage.class); + + assertThat(usage.getPromptTokens()).isEqualTo(200); + assertThat(usage.getCompletionTokens()).isEqualTo(300); + assertThat(usage.getTotalTokens()).isEqualTo(500); + } + + @Test + @DisplayName("TokenUsage - should serialize to JSON") + void tokenUsageShouldSerialize() throws Exception { + TokenUsage usage = TokenUsage.of(100, 200); + String json = objectMapper.writeValueAsString(usage); + + assertThat(json).contains("\"prompt_tokens\":100"); + assertThat(json).contains("\"completion_tokens\":200"); + assertThat(json).contains("\"total_tokens\":300"); + } + + @Test + @DisplayName("AuditOptions - should build with required fields") + void auditOptionsShouldBuildWithRequired() { + AuditOptions options = + AuditOptions.builder().contextId("ctx_123").clientId("test-client").build(); + + assertThat(options.getContextId()).isEqualTo("ctx_123"); + assertThat(options.getSuccess()).isTrue(); // Default + } + + @Test + @DisplayName("AuditOptions - should build with all fields") + void auditOptionsShouldBuildWithAllFields() { + TokenUsage tokenUsage = TokenUsage.of(100, 150); + + AuditOptions options = + AuditOptions.builder() + .contextId("ctx_123") + .clientId("test-client") .responseSummary("Weather information provided") .provider("openai") .model("gpt-4") @@ -98,79 +99,79 @@ void auditOptionsShouldBuildWithAllFields() { .success(true) .build(); - assertThat(options.getContextId()).isEqualTo("ctx_123"); - assertThat(options.getResponseSummary()).isEqualTo("Weather information provided"); - assertThat(options.getProvider()).isEqualTo("openai"); - assertThat(options.getModel()).isEqualTo("gpt-4"); - assertThat(options.getTokenUsage()).isEqualTo(tokenUsage); - assertThat(options.getLatencyMs()).isEqualTo(1234L); - assertThat(options.getMetadata()).containsEntry("source", "api"); - assertThat(options.getSuccess()).isTrue(); - } - - @Test - @DisplayName("AuditOptions - should throw on null context ID") - void auditOptionsShouldThrowOnNullContextId() { - assertThatThrownBy(() -> AuditOptions.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("contextId"); - } - - @Test - @DisplayName("AuditOptions - should add metadata entries") - void auditOptionsShouldAddMetadata() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx_123").clientId("test-client") + assertThat(options.getContextId()).isEqualTo("ctx_123"); + assertThat(options.getResponseSummary()).isEqualTo("Weather information provided"); + assertThat(options.getProvider()).isEqualTo("openai"); + assertThat(options.getModel()).isEqualTo("gpt-4"); + assertThat(options.getTokenUsage()).isEqualTo(tokenUsage); + assertThat(options.getLatencyMs()).isEqualTo(1234L); + assertThat(options.getMetadata()).containsEntry("source", "api"); + assertThat(options.getSuccess()).isTrue(); + } + + @Test + @DisplayName("AuditOptions - should throw on null context ID") + void auditOptionsShouldThrowOnNullContextId() { + assertThatThrownBy(() -> AuditOptions.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("contextId"); + } + + @Test + @DisplayName("AuditOptions - should add metadata entries") + void auditOptionsShouldAddMetadata() { + AuditOptions options = + AuditOptions.builder() + .contextId("ctx_123") + .clientId("test-client") .addMetadata("key1", "value1") .addMetadata("key2", 42) .build(); - assertThat(options.getMetadata()) - .containsEntry("key1", "value1") - .containsEntry("key2", 42); - } + assertThat(options.getMetadata()).containsEntry("key1", "value1").containsEntry("key2", 42); + } - @Test - @DisplayName("AuditOptions - should support error case") - void auditOptionsShouldSupportError() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx_123").clientId("test-client") + @Test + @DisplayName("AuditOptions - should support error case") + void auditOptionsShouldSupportError() { + AuditOptions options = + AuditOptions.builder() + .contextId("ctx_123") + .clientId("test-client") .success(false) .errorMessage("LLM call failed: timeout") .build(); - assertThat(options.getSuccess()).isFalse(); - assertThat(options.getErrorMessage()).isEqualTo("LLM call failed: timeout"); - } + assertThat(options.getSuccess()).isFalse(); + assertThat(options.getErrorMessage()).isEqualTo("LLM call failed: timeout"); + } - @Test - @DisplayName("AuditResult - should deserialize success") - void auditResultShouldDeserializeSuccess() throws Exception { - String json = "{" + @Test + @DisplayName("AuditResult - should deserialize success") + void auditResultShouldDeserializeSuccess() throws Exception { + String json = + "{" + "\"success\": true," + "\"audit_id\": \"audit_abc123\"," + "\"message\": \"Audit recorded\"" + "}"; - AuditResult result = objectMapper.readValue(json, AuditResult.class); + AuditResult result = objectMapper.readValue(json, AuditResult.class); - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getAuditId()).isEqualTo("audit_abc123"); - assertThat(result.getMessage()).isEqualTo("Audit recorded"); - assertThat(result.getError()).isNull(); - } + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getAuditId()).isEqualTo("audit_abc123"); + assertThat(result.getMessage()).isEqualTo("Audit recorded"); + assertThat(result.getError()).isNull(); + } - @Test - @DisplayName("AuditResult - should deserialize failure") - void auditResultShouldDeserializeFailure() throws Exception { - String json = "{" - + "\"success\": false," - + "\"error\": \"Context ID expired\"" - + "}"; + @Test + @DisplayName("AuditResult - should deserialize failure") + void auditResultShouldDeserializeFailure() throws Exception { + String json = "{" + "\"success\": false," + "\"error\": \"Context ID expired\"" + "}"; - AuditResult result = objectMapper.readValue(json, AuditResult.class); + AuditResult result = objectMapper.readValue(json, AuditResult.class); - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getError()).isEqualTo("Context ID expired"); - } + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getError()).isEqualTo("Context ID expired"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/ClientRequestTest.java b/src/test/java/com/getaxonflow/sdk/types/ClientRequestTest.java index 0a5f844..532cd27 100644 --- a/src/test/java/com/getaxonflow/sdk/types/ClientRequestTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/ClientRequestTest.java @@ -15,34 +15,32 @@ */ package com.getaxonflow.sdk.types; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("ClientRequest") class ClientRequestTest { - private final ObjectMapper objectMapper = new ObjectMapper(); + private final ObjectMapper objectMapper = new ObjectMapper(); - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - ClientRequest request = ClientRequest.builder() - .query("What is the weather?") - .build(); + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + ClientRequest request = ClientRequest.builder().query("What is the weather?").build(); - assertThat(request.getQuery()).isEqualTo("What is the weather?"); - assertThat(request.getRequestType()).isEqualTo("chat"); - } + assertThat(request.getQuery()).isEqualTo("What is the weather?"); + assertThat(request.getRequestType()).isEqualTo("chat"); + } - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - ClientRequest request = ClientRequest.builder() + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + ClientRequest request = + ClientRequest.builder() .query("SELECT * FROM users") .userToken("user-123") .clientId("client-456") @@ -52,102 +50,93 @@ void shouldBuildWithAllFields() { .model("gpt-4") .build(); - assertThat(request.getQuery()).isEqualTo("SELECT * FROM users"); - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getClientId()).isEqualTo("client-456"); - assertThat(request.getRequestType()).isEqualTo("sql"); - assertThat(request.getContext()).containsEntry("role", "admin"); - assertThat(request.getLlmProvider()).isEqualTo("openai"); - assertThat(request.getModel()).isEqualTo("gpt-4"); - } - - @Test - @DisplayName("should throw on null query") - void shouldThrowOnNullQuery() { - assertThatThrownBy(() -> ClientRequest.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("query"); - } - - @Test - @DisplayName("should add context entries") - void shouldAddContextEntries() { - ClientRequest request = ClientRequest.builder() + assertThat(request.getQuery()).isEqualTo("SELECT * FROM users"); + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getClientId()).isEqualTo("client-456"); + assertThat(request.getRequestType()).isEqualTo("sql"); + assertThat(request.getContext()).containsEntry("role", "admin"); + assertThat(request.getLlmProvider()).isEqualTo("openai"); + assertThat(request.getModel()).isEqualTo("gpt-4"); + } + + @Test + @DisplayName("should throw on null query") + void shouldThrowOnNullQuery() { + assertThatThrownBy(() -> ClientRequest.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("query"); + } + + @Test + @DisplayName("should add context entries") + void shouldAddContextEntries() { + ClientRequest request = + ClientRequest.builder() .query("test") .addContext("key1", "value1") .addContext("key2", 42) .build(); - assertThat(request.getContext()) - .containsEntry("key1", "value1") - .containsEntry("key2", 42); - } + assertThat(request.getContext()).containsEntry("key1", "value1").containsEntry("key2", 42); + } - @Test - @DisplayName("should return immutable context") - void shouldReturnImmutableContext() { - ClientRequest request = ClientRequest.builder() - .query("test") - .context(Map.of("key", "value")) - .build(); + @Test + @DisplayName("should return immutable context") + void shouldReturnImmutableContext() { + ClientRequest request = + ClientRequest.builder().query("test").context(Map.of("key", "value")).build(); - assertThatThrownBy(() -> request.getContext().put("new", "value")) - .isInstanceOf(UnsupportedOperationException.class); - } + assertThatThrownBy(() -> request.getContext().put("new", "value")) + .isInstanceOf(UnsupportedOperationException.class); + } - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - ClientRequest request = ClientRequest.builder() + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + ClientRequest request = + ClientRequest.builder() .query("What is the weather?") .userToken("user-123") .requestType(RequestType.CHAT) .build(); - String json = objectMapper.writeValueAsString(request); + String json = objectMapper.writeValueAsString(request); - assertThat(json).contains("\"query\":\"What is the weather?\""); - assertThat(json).contains("\"user_token\":\"user-123\""); - assertThat(json).contains("\"request_type\":\"chat\""); - } + assertThat(json).contains("\"query\":\"What is the weather?\""); + assertThat(json).contains("\"user_token\":\"user-123\""); + assertThat(json).contains("\"request_type\":\"chat\""); + } - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ClientRequest request1 = ClientRequest.builder() - .query("test") - .userToken("user-123") - .build(); + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ClientRequest request1 = ClientRequest.builder().query("test").userToken("user-123").build(); - ClientRequest request2 = ClientRequest.builder() - .query("test") - .userToken("user-123") - .build(); + ClientRequest request2 = ClientRequest.builder().query("test").userToken("user-123").build(); - ClientRequest request3 = ClientRequest.builder() - .query("different") - .userToken("user-123") - .build(); + ClientRequest request3 = + ClientRequest.builder().query("different").userToken("user-123").build(); - assertThat(request1).isEqualTo(request2); - assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); - assertThat(request1).isNotEqualTo(request3); - } + assertThat(request1).isEqualTo(request2); + assertThat(request1.hashCode()).isEqualTo(request2.hashCode()); + assertThat(request1).isNotEqualTo(request3); + } - @Test - @DisplayName("should have meaningful toString") - void shouldHaveMeaningfulToString() { - ClientRequest request = ClientRequest.builder() + @Test + @DisplayName("should have meaningful toString") + void shouldHaveMeaningfulToString() { + ClientRequest request = + ClientRequest.builder() .query("What is the weather?") .userToken("user-123") .llmProvider("openai") .model("gpt-4") .build(); - String str = request.toString(); - assertThat(str).contains("What is the weather?"); - assertThat(str).contains("user-123"); - assertThat(str).contains("openai"); - assertThat(str).contains("gpt-4"); - } + String str = request.toString(); + assertThat(str).contains("What is the weather?"); + assertThat(str).contains("user-123"); + assertThat(str).contains("openai"); + assertThat(str).contains("gpt-4"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/ClientResponseTest.java b/src/test/java/com/getaxonflow/sdk/types/ClientResponseTest.java index a4bd58e..594e521 100644 --- a/src/test/java/com/getaxonflow/sdk/types/ClientResponseTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/ClientResponseTest.java @@ -15,33 +15,32 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; -import java.util.List; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("ClientResponse") class ClientResponseTest { - private ObjectMapper objectMapper; + private ObjectMapper objectMapper; - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - } + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } - @Test - @DisplayName("should deserialize success response") - void shouldDeserializeSuccessResponse() throws Exception { - String json = "{" + @Test + @DisplayName("should deserialize success response") + void shouldDeserializeSuccessResponse() throws Exception { + String json = + "{" + "\"success\": true," + "\"data\": {\"message\": \"Hello, world!\"}," + "\"blocked\": false," @@ -51,20 +50,22 @@ void shouldDeserializeSuccessResponse() throws Exception { + "}" + "}"; - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.isBlocked()).isFalse(); - assertThat(response.getData()).isNotNull(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).containsExactly("policy1", "policy2"); - assertThat(response.getPolicyInfo().getProcessingTime()).isEqualTo("5.23ms"); - } - - @Test - @DisplayName("should deserialize blocked response") - void shouldDeserializeBlockedResponse() throws Exception { - String json = "{" + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.isBlocked()).isFalse(); + assertThat(response.getData()).isNotNull(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()) + .containsExactly("policy1", "policy2"); + assertThat(response.getPolicyInfo().getProcessingTime()).isEqualTo("5.23ms"); + } + + @Test + @DisplayName("should deserialize blocked response") + void shouldDeserializeBlockedResponse() throws Exception { + String json = + "{" + "\"success\": false," + "\"blocked\": true," + "\"block_reason\": \"Request blocked by policy: sql_injection_detection\"," @@ -74,106 +75,102 @@ void shouldDeserializeBlockedResponse() throws Exception { + "}" + "}"; - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - - assertThat(response.isSuccess()).isFalse(); - assertThat(response.isBlocked()).isTrue(); - assertThat(response.getBlockReason()).isEqualTo("Request blocked by policy: sql_injection_detection"); - assertThat(response.getBlockingPolicyName()).isEqualTo("sql_injection_detection"); - } - - @ParameterizedTest - @CsvSource({ - "Request blocked by policy: pii_detection,pii_detection", - "Blocked by policy: sql_injection,sql_injection", - "[rate_limit] Too many requests,rate_limit", - "Some other reason,Some other reason" - }) - @DisplayName("should extract policy name from various formats") - void shouldExtractPolicyName(String blockReason, String expectedPolicy) throws Exception { - String json = String.format("{" - + "\"success\": false," - + "\"blocked\": true," - + "\"block_reason\": \"%s\"" - + "}", blockReason); - - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - - assertThat(response.getBlockingPolicyName()).isEqualTo(expectedPolicy); - } - - @Test - @DisplayName("should handle null block reason") - void shouldHandleNullBlockReason() throws Exception { - String json = "{" - + "\"success\": true," - + "\"blocked\": false" - + "}"; - - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - - assertThat(response.getBlockingPolicyName()).isNull(); - } - - @Test - @DisplayName("should deserialize plan response") - void shouldDeserializePlanResponse() throws Exception { - String json = "{" + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + + assertThat(response.isSuccess()).isFalse(); + assertThat(response.isBlocked()).isTrue(); + assertThat(response.getBlockReason()) + .isEqualTo("Request blocked by policy: sql_injection_detection"); + assertThat(response.getBlockingPolicyName()).isEqualTo("sql_injection_detection"); + } + + @ParameterizedTest + @CsvSource({ + "Request blocked by policy: pii_detection,pii_detection", + "Blocked by policy: sql_injection,sql_injection", + "[rate_limit] Too many requests,rate_limit", + "Some other reason,Some other reason" + }) + @DisplayName("should extract policy name from various formats") + void shouldExtractPolicyName(String blockReason, String expectedPolicy) throws Exception { + String json = + String.format( + "{" + "\"success\": false," + "\"blocked\": true," + "\"block_reason\": \"%s\"" + "}", + blockReason); + + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + + assertThat(response.getBlockingPolicyName()).isEqualTo(expectedPolicy); + } + + @Test + @DisplayName("should handle null block reason") + void shouldHandleNullBlockReason() throws Exception { + String json = "{" + "\"success\": true," + "\"blocked\": false" + "}"; + + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + + assertThat(response.getBlockingPolicyName()).isNull(); + } + + @Test + @DisplayName("should deserialize plan response") + void shouldDeserializePlanResponse() throws Exception { + String json = + "{" + "\"success\": true," + "\"blocked\": false," + "\"result\": \"Plan executed successfully\"," + "\"plan_id\": \"plan_123\"" + "}"; - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - assertThat(response.getResult()).isEqualTo("Plan executed successfully"); - assertThat(response.getPlanId()).isEqualTo("plan_123"); - } + assertThat(response.getResult()).isEqualTo("Plan executed successfully"); + assertThat(response.getPlanId()).isEqualTo("plan_123"); + } - @Test - @DisplayName("should handle error response") - void shouldHandleErrorResponse() throws Exception { - String json = "{" + @Test + @DisplayName("should handle error response") + void shouldHandleErrorResponse() throws Exception { + String json = + "{" + "\"success\": false," + "\"blocked\": false," + "\"error\": \"Internal server error\"" + "}"; - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - assertThat(response.isSuccess()).isFalse(); - assertThat(response.getError()).isEqualTo("Internal server error"); - } + assertThat(response.isSuccess()).isFalse(); + assertThat(response.getError()).isEqualTo("Internal server error"); + } - @Test - @DisplayName("should ignore unknown properties") - void shouldIgnoreUnknownProperties() throws Exception { - String json = "{" + @Test + @DisplayName("should ignore unknown properties") + void shouldIgnoreUnknownProperties() throws Exception { + String json = + "{" + "\"success\": true," + "\"blocked\": false," + "\"unknown_field\": \"value\"," + "\"another_unknown\": 123" + "}"; - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - assertThat(response.isSuccess()).isTrue(); - } + assertThat(response.isSuccess()).isTrue(); + } - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() throws Exception { - String json = "{" - + "\"success\": true," - + "\"blocked\": false," - + "\"data\": \"test\"" - + "}"; + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() throws Exception { + String json = "{" + "\"success\": true," + "\"blocked\": false," + "\"data\": \"test\"" + "}"; - ClientResponse response1 = objectMapper.readValue(json, ClientResponse.class); - ClientResponse response2 = objectMapper.readValue(json, ClientResponse.class); + ClientResponse response1 = objectMapper.readValue(json, ClientResponse.class); + ClientResponse response2 = objectMapper.readValue(json, ClientResponse.class); - assertThat(response1).isEqualTo(response2); - assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); - } + assertThat(response1).isEqualTo(response2); + assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/CostControlTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/CostControlTypesTest.java index dae6513..bf03774 100644 --- a/src/test/java/com/getaxonflow/sdk/types/CostControlTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/CostControlTypesTest.java @@ -15,526 +15,535 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.getaxonflow.sdk.types.costcontrols.CostControlTypes.*; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.List; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("Cost Control Types") class CostControlTypesTest { - private static final ObjectMapper MAPPER = new ObjectMapper(); - - @Nested - @DisplayName("BudgetScope") - class BudgetScopeTests { - - @Test - @DisplayName("getValue should return correct string") - void getValueShouldReturnCorrectString() { - assertThat(BudgetScope.ORGANIZATION.getValue()).isEqualTo("organization"); - assertThat(BudgetScope.TEAM.getValue()).isEqualTo("team"); - assertThat(BudgetScope.AGENT.getValue()).isEqualTo("agent"); - assertThat(BudgetScope.WORKFLOW.getValue()).isEqualTo("workflow"); - assertThat(BudgetScope.USER.getValue()).isEqualTo("user"); - } - - @Test - @DisplayName("fromValue should return correct enum") - void fromValueShouldReturnCorrectEnum() { - assertThat(BudgetScope.fromValue("organization")).isEqualTo(BudgetScope.ORGANIZATION); - assertThat(BudgetScope.fromValue("team")).isEqualTo(BudgetScope.TEAM); - assertThat(BudgetScope.fromValue("agent")).isEqualTo(BudgetScope.AGENT); - assertThat(BudgetScope.fromValue("workflow")).isEqualTo(BudgetScope.WORKFLOW); - assertThat(BudgetScope.fromValue("user")).isEqualTo(BudgetScope.USER); - } - - @Test - @DisplayName("fromValue should throw for invalid value") - void fromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> BudgetScope.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget scope"); - } - } + private static final ObjectMapper MAPPER = new ObjectMapper(); - @Nested - @DisplayName("BudgetPeriod") - class BudgetPeriodTests { - - @Test - @DisplayName("getValue should return correct string") - void getValueShouldReturnCorrectString() { - assertThat(BudgetPeriod.DAILY.getValue()).isEqualTo("daily"); - assertThat(BudgetPeriod.WEEKLY.getValue()).isEqualTo("weekly"); - assertThat(BudgetPeriod.MONTHLY.getValue()).isEqualTo("monthly"); - assertThat(BudgetPeriod.QUARTERLY.getValue()).isEqualTo("quarterly"); - assertThat(BudgetPeriod.YEARLY.getValue()).isEqualTo("yearly"); - } - - @Test - @DisplayName("fromValue should return correct enum") - void fromValueShouldReturnCorrectEnum() { - assertThat(BudgetPeriod.fromValue("daily")).isEqualTo(BudgetPeriod.DAILY); - assertThat(BudgetPeriod.fromValue("weekly")).isEqualTo(BudgetPeriod.WEEKLY); - assertThat(BudgetPeriod.fromValue("monthly")).isEqualTo(BudgetPeriod.MONTHLY); - assertThat(BudgetPeriod.fromValue("quarterly")).isEqualTo(BudgetPeriod.QUARTERLY); - assertThat(BudgetPeriod.fromValue("yearly")).isEqualTo(BudgetPeriod.YEARLY); - } - - @Test - @DisplayName("fromValue should throw for invalid value") - void fromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> BudgetPeriod.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget period"); - } - } + @Nested + @DisplayName("BudgetScope") + class BudgetScopeTests { - @Nested - @DisplayName("BudgetOnExceed") - class BudgetOnExceedTests { - - @Test - @DisplayName("getValue should return correct string") - void getValueShouldReturnCorrectString() { - assertThat(BudgetOnExceed.WARN.getValue()).isEqualTo("warn"); - assertThat(BudgetOnExceed.BLOCK.getValue()).isEqualTo("block"); - assertThat(BudgetOnExceed.DOWNGRADE.getValue()).isEqualTo("downgrade"); - } - - @Test - @DisplayName("fromValue should return correct enum") - void fromValueShouldReturnCorrectEnum() { - assertThat(BudgetOnExceed.fromValue("warn")).isEqualTo(BudgetOnExceed.WARN); - assertThat(BudgetOnExceed.fromValue("block")).isEqualTo(BudgetOnExceed.BLOCK); - assertThat(BudgetOnExceed.fromValue("downgrade")).isEqualTo(BudgetOnExceed.DOWNGRADE); - } - - @Test - @DisplayName("fromValue should throw for invalid value") - void fromValueShouldThrowForInvalid() { - assertThatThrownBy(() -> BudgetOnExceed.fromValue("invalid")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown budget on exceed action"); - } + @Test + @DisplayName("getValue should return correct string") + void getValueShouldReturnCorrectString() { + assertThat(BudgetScope.ORGANIZATION.getValue()).isEqualTo("organization"); + assertThat(BudgetScope.TEAM.getValue()).isEqualTo("team"); + assertThat(BudgetScope.AGENT.getValue()).isEqualTo("agent"); + assertThat(BudgetScope.WORKFLOW.getValue()).isEqualTo("workflow"); + assertThat(BudgetScope.USER.getValue()).isEqualTo("user"); } - @Nested - @DisplayName("CreateBudgetRequest") - class CreateBudgetRequestTests { - - @Test - @DisplayName("builder should create request with all fields") - void builderShouldCreateRequest() { - CreateBudgetRequest request = CreateBudgetRequest.builder() - .id("budget-123") - .name("Monthly Budget") - .scope(BudgetScope.ORGANIZATION) - .limitUsd(1000.0) - .period(BudgetPeriod.MONTHLY) - .onExceed(BudgetOnExceed.WARN) - .alertThresholds(List.of(50, 75, 90)) - .scopeId("org-123") - .build(); - - assertThat(request.getId()).isEqualTo("budget-123"); - assertThat(request.getName()).isEqualTo("Monthly Budget"); - assertThat(request.getScope()).isEqualTo(BudgetScope.ORGANIZATION); - assertThat(request.getLimitUsd()).isEqualTo(1000.0); - assertThat(request.getPeriod()).isEqualTo(BudgetPeriod.MONTHLY); - assertThat(request.getOnExceed()).isEqualTo(BudgetOnExceed.WARN); - assertThat(request.getAlertThresholds()).containsExactly(50, 75, 90); - assertThat(request.getScopeId()).isEqualTo("org-123"); - } + @Test + @DisplayName("fromValue should return correct enum") + void fromValueShouldReturnCorrectEnum() { + assertThat(BudgetScope.fromValue("organization")).isEqualTo(BudgetScope.ORGANIZATION); + assertThat(BudgetScope.fromValue("team")).isEqualTo(BudgetScope.TEAM); + assertThat(BudgetScope.fromValue("agent")).isEqualTo(BudgetScope.AGENT); + assertThat(BudgetScope.fromValue("workflow")).isEqualTo(BudgetScope.WORKFLOW); + assertThat(BudgetScope.fromValue("user")).isEqualTo(BudgetScope.USER); } - @Nested - @DisplayName("UpdateBudgetRequest") - class UpdateBudgetRequestTests { - - @Test - @DisplayName("builder should create request with all fields") - void builderShouldCreateRequest() { - UpdateBudgetRequest request = UpdateBudgetRequest.builder() - .name("Updated Budget") - .limitUsd(2000.0) - .onExceed(BudgetOnExceed.BLOCK) - .alertThresholds(List.of(80, 95)) - .build(); - - assertThat(request.getName()).isEqualTo("Updated Budget"); - assertThat(request.getLimitUsd()).isEqualTo(2000.0); - assertThat(request.getOnExceed()).isEqualTo(BudgetOnExceed.BLOCK); - assertThat(request.getAlertThresholds()).containsExactly(80, 95); - } + @Test + @DisplayName("fromValue should throw for invalid value") + void fromValueShouldThrowForInvalid() { + assertThatThrownBy(() -> BudgetScope.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget scope"); } - - @Nested - @DisplayName("ListBudgetsOptions") - class ListBudgetsOptionsTests { - - @Test - @DisplayName("builder should create options with all fields") - void builderShouldCreateOptions() { - ListBudgetsOptions options = ListBudgetsOptions.builder() - .scope(BudgetScope.TEAM) - .limit(10) - .offset(20) - .build(); - - assertThat(options.getScope()).isEqualTo(BudgetScope.TEAM); - assertThat(options.getLimit()).isEqualTo(10); - assertThat(options.getOffset()).isEqualTo(20); - } - - @Test - @DisplayName("builder should create options with default values") - void builderShouldCreateDefaultOptions() { - ListBudgetsOptions options = ListBudgetsOptions.builder().build(); - - assertThat(options.getScope()).isNull(); - assertThat(options.getLimit()).isNull(); - assertThat(options.getOffset()).isNull(); - } + } + + @Nested + @DisplayName("BudgetPeriod") + class BudgetPeriodTests { + + @Test + @DisplayName("getValue should return correct string") + void getValueShouldReturnCorrectString() { + assertThat(BudgetPeriod.DAILY.getValue()).isEqualTo("daily"); + assertThat(BudgetPeriod.WEEKLY.getValue()).isEqualTo("weekly"); + assertThat(BudgetPeriod.MONTHLY.getValue()).isEqualTo("monthly"); + assertThat(BudgetPeriod.QUARTERLY.getValue()).isEqualTo("quarterly"); + assertThat(BudgetPeriod.YEARLY.getValue()).isEqualTo("yearly"); } - @Nested - @DisplayName("BudgetCheckRequest") - class BudgetCheckRequestTests { - - @Test - @DisplayName("builder should create request with all fields") - void builderShouldCreateRequest() { - BudgetCheckRequest request = BudgetCheckRequest.builder() - .orgId("org-123") - .teamId("team-456") - .agentId("agent-789") - .workflowId("wf-101") - .userId("user-202") - .build(); - - assertThat(request.getOrgId()).isEqualTo("org-123"); - assertThat(request.getTeamId()).isEqualTo("team-456"); - assertThat(request.getAgentId()).isEqualTo("agent-789"); - assertThat(request.getWorkflowId()).isEqualTo("wf-101"); - assertThat(request.getUserId()).isEqualTo("user-202"); - } + @Test + @DisplayName("fromValue should return correct enum") + void fromValueShouldReturnCorrectEnum() { + assertThat(BudgetPeriod.fromValue("daily")).isEqualTo(BudgetPeriod.DAILY); + assertThat(BudgetPeriod.fromValue("weekly")).isEqualTo(BudgetPeriod.WEEKLY); + assertThat(BudgetPeriod.fromValue("monthly")).isEqualTo(BudgetPeriod.MONTHLY); + assertThat(BudgetPeriod.fromValue("quarterly")).isEqualTo(BudgetPeriod.QUARTERLY); + assertThat(BudgetPeriod.fromValue("yearly")).isEqualTo(BudgetPeriod.YEARLY); } - @Nested - @DisplayName("ListUsageRecordsOptions") - class ListUsageRecordsOptionsTests { - - @Test - @DisplayName("builder should create options with all fields") - void builderShouldCreateOptions() { - ListUsageRecordsOptions options = ListUsageRecordsOptions.builder() - .limit(50) - .offset(100) - .provider("openai") - .model("gpt-4") - .build(); - - assertThat(options.getLimit()).isEqualTo(50); - assertThat(options.getOffset()).isEqualTo(100); - assertThat(options.getProvider()).isEqualTo("openai"); - assertThat(options.getModel()).isEqualTo("gpt-4"); - } + @Test + @DisplayName("fromValue should throw for invalid value") + void fromValueShouldThrowForInvalid() { + assertThatThrownBy(() -> BudgetPeriod.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget period"); + } + } + + @Nested + @DisplayName("BudgetOnExceed") + class BudgetOnExceedTests { + + @Test + @DisplayName("getValue should return correct string") + void getValueShouldReturnCorrectString() { + assertThat(BudgetOnExceed.WARN.getValue()).isEqualTo("warn"); + assertThat(BudgetOnExceed.BLOCK.getValue()).isEqualTo("block"); + assertThat(BudgetOnExceed.DOWNGRADE.getValue()).isEqualTo("downgrade"); } - @Nested - @DisplayName("Budget") - class BudgetTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"id\":\"budget-123\",\"name\":\"Monthly Budget\",\"scope\":\"organization\"," + - "\"limit_usd\":1000.0,\"period\":\"monthly\",\"on_exceed\":\"warn\"," + - "\"alert_thresholds\":[50,75,90],\"enabled\":true,\"scope_id\":\"org-123\"," + - "\"created_at\":\"2025-01-01T00:00:00Z\",\"updated_at\":\"2025-01-02T00:00:00Z\"}"; - - Budget budget = MAPPER.readValue(json, Budget.class); - - assertThat(budget.getId()).isEqualTo("budget-123"); - assertThat(budget.getName()).isEqualTo("Monthly Budget"); - assertThat(budget.getScope()).isEqualTo("organization"); - assertThat(budget.getLimitUsd()).isEqualTo(1000.0); - assertThat(budget.getPeriod()).isEqualTo("monthly"); - assertThat(budget.getOnExceed()).isEqualTo("warn"); - assertThat(budget.getAlertThresholds()).containsExactly(50, 75, 90); - assertThat(budget.getEnabled()).isTrue(); - assertThat(budget.getScopeId()).isEqualTo("org-123"); - assertThat(budget.getCreatedAt()).isEqualTo("2025-01-01T00:00:00Z"); - assertThat(budget.getUpdatedAt()).isEqualTo("2025-01-02T00:00:00Z"); - } + @Test + @DisplayName("fromValue should return correct enum") + void fromValueShouldReturnCorrectEnum() { + assertThat(BudgetOnExceed.fromValue("warn")).isEqualTo(BudgetOnExceed.WARN); + assertThat(BudgetOnExceed.fromValue("block")).isEqualTo(BudgetOnExceed.BLOCK); + assertThat(BudgetOnExceed.fromValue("downgrade")).isEqualTo(BudgetOnExceed.DOWNGRADE); } - @Nested - @DisplayName("BudgetsResponse") - class BudgetsResponseTests { + @Test + @DisplayName("fromValue should throw for invalid value") + void fromValueShouldThrowForInvalid() { + assertThatThrownBy(() -> BudgetOnExceed.fromValue("invalid")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown budget on exceed action"); + } + } + + @Nested + @DisplayName("CreateBudgetRequest") + class CreateBudgetRequestTests { + + @Test + @DisplayName("builder should create request with all fields") + void builderShouldCreateRequest() { + CreateBudgetRequest request = + CreateBudgetRequest.builder() + .id("budget-123") + .name("Monthly Budget") + .scope(BudgetScope.ORGANIZATION) + .limitUsd(1000.0) + .period(BudgetPeriod.MONTHLY) + .onExceed(BudgetOnExceed.WARN) + .alertThresholds(List.of(50, 75, 90)) + .scopeId("org-123") + .build(); + + assertThat(request.getId()).isEqualTo("budget-123"); + assertThat(request.getName()).isEqualTo("Monthly Budget"); + assertThat(request.getScope()).isEqualTo(BudgetScope.ORGANIZATION); + assertThat(request.getLimitUsd()).isEqualTo(1000.0); + assertThat(request.getPeriod()).isEqualTo(BudgetPeriod.MONTHLY); + assertThat(request.getOnExceed()).isEqualTo(BudgetOnExceed.WARN); + assertThat(request.getAlertThresholds()).containsExactly(50, 75, 90); + assertThat(request.getScopeId()).isEqualTo("org-123"); + } + } + + @Nested + @DisplayName("UpdateBudgetRequest") + class UpdateBudgetRequestTests { + + @Test + @DisplayName("builder should create request with all fields") + void builderShouldCreateRequest() { + UpdateBudgetRequest request = + UpdateBudgetRequest.builder() + .name("Updated Budget") + .limitUsd(2000.0) + .onExceed(BudgetOnExceed.BLOCK) + .alertThresholds(List.of(80, 95)) + .build(); + + assertThat(request.getName()).isEqualTo("Updated Budget"); + assertThat(request.getLimitUsd()).isEqualTo(2000.0); + assertThat(request.getOnExceed()).isEqualTo(BudgetOnExceed.BLOCK); + assertThat(request.getAlertThresholds()).containsExactly(80, 95); + } + } - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"budgets\":[{\"id\":\"budget-1\"},{\"id\":\"budget-2\"}],\"total\":2}"; + @Nested + @DisplayName("ListBudgetsOptions") + class ListBudgetsOptionsTests { - BudgetsResponse response = MAPPER.readValue(json, BudgetsResponse.class); + @Test + @DisplayName("builder should create options with all fields") + void builderShouldCreateOptions() { + ListBudgetsOptions options = + ListBudgetsOptions.builder().scope(BudgetScope.TEAM).limit(10).offset(20).build(); - assertThat(response.getBudgets()).hasSize(2); - assertThat(response.getTotal()).isEqualTo(2); - } + assertThat(options.getScope()).isEqualTo(BudgetScope.TEAM); + assertThat(options.getLimit()).isEqualTo(10); + assertThat(options.getOffset()).isEqualTo(20); } - @Nested - @DisplayName("BudgetStatus") - class BudgetStatusTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"budget\":{\"id\":\"budget-123\"},\"used_usd\":500.0,\"remaining_usd\":500.0," + - "\"percentage\":50.0,\"is_exceeded\":false,\"is_blocked\":false," + - "\"period_start\":\"2025-01-01T00:00:00Z\",\"period_end\":\"2025-01-31T23:59:59Z\"}"; - - BudgetStatus status = MAPPER.readValue(json, BudgetStatus.class); - - assertThat(status.getBudget().getId()).isEqualTo("budget-123"); - assertThat(status.getUsedUsd()).isEqualTo(500.0); - assertThat(status.getRemainingUsd()).isEqualTo(500.0); - assertThat(status.getPercentage()).isEqualTo(50.0); - assertThat(status.isExceeded()).isFalse(); - assertThat(status.isBlocked()).isFalse(); - assertThat(status.getPeriodStart()).isEqualTo("2025-01-01T00:00:00Z"); - assertThat(status.getPeriodEnd()).isEqualTo("2025-01-31T23:59:59Z"); - } - } + @Test + @DisplayName("builder should create options with default values") + void builderShouldCreateDefaultOptions() { + ListBudgetsOptions options = ListBudgetsOptions.builder().build(); - @Nested - @DisplayName("BudgetAlert") - class BudgetAlertTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"id\":\"alert-123\",\"budget_id\":\"budget-456\",\"alert_type\":\"threshold\"," + - "\"threshold\":75,\"percentage_reached\":76.5,\"amount_usd\":765.0," + - "\"message\":\"Budget threshold reached\",\"created_at\":\"2025-01-15T12:00:00Z\"}"; - - BudgetAlert alert = MAPPER.readValue(json, BudgetAlert.class); - - assertThat(alert.getId()).isEqualTo("alert-123"); - assertThat(alert.getBudgetId()).isEqualTo("budget-456"); - assertThat(alert.getAlertType()).isEqualTo("threshold"); - assertThat(alert.getThreshold()).isEqualTo(75); - assertThat(alert.getPercentageReached()).isEqualTo(76.5); - assertThat(alert.getAmountUsd()).isEqualTo(765.0); - assertThat(alert.getMessage()).isEqualTo("Budget threshold reached"); - assertThat(alert.getCreatedAt()).isEqualTo("2025-01-15T12:00:00Z"); - } + assertThat(options.getScope()).isNull(); + assertThat(options.getLimit()).isNull(); + assertThat(options.getOffset()).isNull(); } + } + + @Nested + @DisplayName("BudgetCheckRequest") + class BudgetCheckRequestTests { + + @Test + @DisplayName("builder should create request with all fields") + void builderShouldCreateRequest() { + BudgetCheckRequest request = + BudgetCheckRequest.builder() + .orgId("org-123") + .teamId("team-456") + .agentId("agent-789") + .workflowId("wf-101") + .userId("user-202") + .build(); + + assertThat(request.getOrgId()).isEqualTo("org-123"); + assertThat(request.getTeamId()).isEqualTo("team-456"); + assertThat(request.getAgentId()).isEqualTo("agent-789"); + assertThat(request.getWorkflowId()).isEqualTo("wf-101"); + assertThat(request.getUserId()).isEqualTo("user-202"); + } + } + + @Nested + @DisplayName("ListUsageRecordsOptions") + class ListUsageRecordsOptionsTests { + + @Test + @DisplayName("builder should create options with all fields") + void builderShouldCreateOptions() { + ListUsageRecordsOptions options = + ListUsageRecordsOptions.builder() + .limit(50) + .offset(100) + .provider("openai") + .model("gpt-4") + .build(); + + assertThat(options.getLimit()).isEqualTo(50); + assertThat(options.getOffset()).isEqualTo(100); + assertThat(options.getProvider()).isEqualTo("openai"); + assertThat(options.getModel()).isEqualTo("gpt-4"); + } + } + + @Nested + @DisplayName("Budget") + class BudgetTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"id\":\"budget-123\",\"name\":\"Monthly Budget\",\"scope\":\"organization\"," + + "\"limit_usd\":1000.0,\"period\":\"monthly\",\"on_exceed\":\"warn\"," + + "\"alert_thresholds\":[50,75,90],\"enabled\":true,\"scope_id\":\"org-123\"," + + "\"created_at\":\"2025-01-01T00:00:00Z\",\"updated_at\":\"2025-01-02T00:00:00Z\"}"; + + Budget budget = MAPPER.readValue(json, Budget.class); + + assertThat(budget.getId()).isEqualTo("budget-123"); + assertThat(budget.getName()).isEqualTo("Monthly Budget"); + assertThat(budget.getScope()).isEqualTo("organization"); + assertThat(budget.getLimitUsd()).isEqualTo(1000.0); + assertThat(budget.getPeriod()).isEqualTo("monthly"); + assertThat(budget.getOnExceed()).isEqualTo("warn"); + assertThat(budget.getAlertThresholds()).containsExactly(50, 75, 90); + assertThat(budget.getEnabled()).isTrue(); + assertThat(budget.getScopeId()).isEqualTo("org-123"); + assertThat(budget.getCreatedAt()).isEqualTo("2025-01-01T00:00:00Z"); + assertThat(budget.getUpdatedAt()).isEqualTo("2025-01-02T00:00:00Z"); + } + } - @Nested - @DisplayName("BudgetAlertsResponse") - class BudgetAlertsResponseTests { + @Nested + @DisplayName("BudgetsResponse") + class BudgetsResponseTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"alerts\":[{\"id\":\"alert-1\"},{\"id\":\"alert-2\"}],\"count\":2}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"budgets\":[{\"id\":\"budget-1\"},{\"id\":\"budget-2\"}],\"total\":2}"; - BudgetAlertsResponse response = MAPPER.readValue(json, BudgetAlertsResponse.class); + BudgetsResponse response = MAPPER.readValue(json, BudgetsResponse.class); - assertThat(response.getAlerts()).hasSize(2); - assertThat(response.getCount()).isEqualTo(2); - } + assertThat(response.getBudgets()).hasSize(2); + assertThat(response.getTotal()).isEqualTo(2); } + } + + @Nested + @DisplayName("BudgetStatus") + class BudgetStatusTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"budget\":{\"id\":\"budget-123\"},\"used_usd\":500.0,\"remaining_usd\":500.0," + + "\"percentage\":50.0,\"is_exceeded\":false,\"is_blocked\":false," + + "\"period_start\":\"2025-01-01T00:00:00Z\",\"period_end\":\"2025-01-31T23:59:59Z\"}"; + + BudgetStatus status = MAPPER.readValue(json, BudgetStatus.class); + + assertThat(status.getBudget().getId()).isEqualTo("budget-123"); + assertThat(status.getUsedUsd()).isEqualTo(500.0); + assertThat(status.getRemainingUsd()).isEqualTo(500.0); + assertThat(status.getPercentage()).isEqualTo(50.0); + assertThat(status.isExceeded()).isFalse(); + assertThat(status.isBlocked()).isFalse(); + assertThat(status.getPeriodStart()).isEqualTo("2025-01-01T00:00:00Z"); + assertThat(status.getPeriodEnd()).isEqualTo("2025-01-31T23:59:59Z"); + } + } + + @Nested + @DisplayName("BudgetAlert") + class BudgetAlertTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"id\":\"alert-123\",\"budget_id\":\"budget-456\",\"alert_type\":\"threshold\"," + + "\"threshold\":75,\"percentage_reached\":76.5,\"amount_usd\":765.0," + + "\"message\":\"Budget threshold reached\",\"created_at\":\"2025-01-15T12:00:00Z\"}"; + + BudgetAlert alert = MAPPER.readValue(json, BudgetAlert.class); + + assertThat(alert.getId()).isEqualTo("alert-123"); + assertThat(alert.getBudgetId()).isEqualTo("budget-456"); + assertThat(alert.getAlertType()).isEqualTo("threshold"); + assertThat(alert.getThreshold()).isEqualTo(75); + assertThat(alert.getPercentageReached()).isEqualTo(76.5); + assertThat(alert.getAmountUsd()).isEqualTo(765.0); + assertThat(alert.getMessage()).isEqualTo("Budget threshold reached"); + assertThat(alert.getCreatedAt()).isEqualTo("2025-01-15T12:00:00Z"); + } + } - @Nested - @DisplayName("BudgetDecision") - class BudgetDecisionTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"allowed\":true,\"action\":\"allow\",\"message\":\"Within budget\"," + - "\"budgets\":[{\"id\":\"budget-1\"}]}"; - - BudgetDecision decision = MAPPER.readValue(json, BudgetDecision.class); + @Nested + @DisplayName("BudgetAlertsResponse") + class BudgetAlertsResponseTests { - assertThat(decision.isAllowed()).isTrue(); - assertThat(decision.getAction()).isEqualTo("allow"); - assertThat(decision.getMessage()).isEqualTo("Within budget"); - assertThat(decision.getBudgets()).hasSize(1); - } - } + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"alerts\":[{\"id\":\"alert-1\"},{\"id\":\"alert-2\"}],\"count\":2}"; - @Nested - @DisplayName("UsageSummary") - class UsageSummaryTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"total_cost_usd\":150.75,\"total_requests\":5000,\"total_tokens_in\":1000000," + - "\"total_tokens_out\":500000,\"average_cost_per_request\":0.03,\"period\":\"monthly\"," + - "\"period_start\":\"2025-01-01T00:00:00Z\",\"period_end\":\"2025-01-31T23:59:59Z\"}"; - - UsageSummary summary = MAPPER.readValue(json, UsageSummary.class); - - assertThat(summary.getTotalCostUsd()).isEqualTo(150.75); - assertThat(summary.getTotalRequests()).isEqualTo(5000); - assertThat(summary.getTotalTokensIn()).isEqualTo(1000000); - assertThat(summary.getTotalTokensOut()).isEqualTo(500000); - assertThat(summary.getAverageCostPerRequest()).isEqualTo(0.03); - assertThat(summary.getPeriod()).isEqualTo("monthly"); - } - } + BudgetAlertsResponse response = MAPPER.readValue(json, BudgetAlertsResponse.class); - @Nested - @DisplayName("UsageBreakdownItem") - class UsageBreakdownItemTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"group_value\":\"openai\",\"cost_usd\":100.0,\"percentage\":66.7," + - "\"request_count\":3000,\"tokens_in\":600000,\"tokens_out\":300000}"; - - UsageBreakdownItem item = MAPPER.readValue(json, UsageBreakdownItem.class); - - assertThat(item.getGroupValue()).isEqualTo("openai"); - assertThat(item.getCostUsd()).isEqualTo(100.0); - assertThat(item.getPercentage()).isEqualTo(66.7); - assertThat(item.getRequestCount()).isEqualTo(3000); - assertThat(item.getTokensIn()).isEqualTo(600000); - assertThat(item.getTokensOut()).isEqualTo(300000); - } + assertThat(response.getAlerts()).hasSize(2); + assertThat(response.getCount()).isEqualTo(2); } + } - @Nested - @DisplayName("UsageBreakdown") - class UsageBreakdownTests { + @Nested + @DisplayName("BudgetDecision") + class BudgetDecisionTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"group_by\":\"provider\",\"total_cost_usd\":150.0," + - "\"items\":[{\"group_value\":\"openai\",\"cost_usd\":100.0}],\"period\":\"monthly\"," + - "\"period_start\":\"2025-01-01\",\"period_end\":\"2025-01-31\"}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"allowed\":true,\"action\":\"allow\",\"message\":\"Within budget\"," + + "\"budgets\":[{\"id\":\"budget-1\"}]}"; - UsageBreakdown breakdown = MAPPER.readValue(json, UsageBreakdown.class); + BudgetDecision decision = MAPPER.readValue(json, BudgetDecision.class); - assertThat(breakdown.getGroupBy()).isEqualTo("provider"); - assertThat(breakdown.getTotalCostUsd()).isEqualTo(150.0); - assertThat(breakdown.getItems()).hasSize(1); - assertThat(breakdown.getPeriod()).isEqualTo("monthly"); - } + assertThat(decision.isAllowed()).isTrue(); + assertThat(decision.getAction()).isEqualTo("allow"); + assertThat(decision.getMessage()).isEqualTo("Within budget"); + assertThat(decision.getBudgets()).hasSize(1); } - - @Nested - @DisplayName("UsageRecord") - class UsageRecordTests { - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"id\":\"record-123\",\"provider\":\"openai\",\"model\":\"gpt-4\"," + - "\"tokens_in\":100,\"tokens_out\":50,\"cost_usd\":0.0045," + - "\"request_id\":\"req-456\",\"org_id\":\"org-789\",\"agent_id\":\"agent-101\"," + - "\"timestamp\":\"2025-01-15T12:00:00Z\"}"; - - UsageRecord record = MAPPER.readValue(json, UsageRecord.class); - - assertThat(record.getId()).isEqualTo("record-123"); - assertThat(record.getProvider()).isEqualTo("openai"); - assertThat(record.getModel()).isEqualTo("gpt-4"); - assertThat(record.getTokensIn()).isEqualTo(100); - assertThat(record.getTokensOut()).isEqualTo(50); - assertThat(record.getCostUsd()).isEqualTo(0.0045); - assertThat(record.getRequestId()).isEqualTo("req-456"); - assertThat(record.getOrgId()).isEqualTo("org-789"); - assertThat(record.getAgentId()).isEqualTo("agent-101"); - assertThat(record.getTimestamp()).isEqualTo("2025-01-15T12:00:00Z"); - } + } + + @Nested + @DisplayName("UsageSummary") + class UsageSummaryTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"total_cost_usd\":150.75,\"total_requests\":5000,\"total_tokens_in\":1000000," + + "\"total_tokens_out\":500000,\"average_cost_per_request\":0.03,\"period\":\"monthly\"," + + "\"period_start\":\"2025-01-01T00:00:00Z\",\"period_end\":\"2025-01-31T23:59:59Z\"}"; + + UsageSummary summary = MAPPER.readValue(json, UsageSummary.class); + + assertThat(summary.getTotalCostUsd()).isEqualTo(150.75); + assertThat(summary.getTotalRequests()).isEqualTo(5000); + assertThat(summary.getTotalTokensIn()).isEqualTo(1000000); + assertThat(summary.getTotalTokensOut()).isEqualTo(500000); + assertThat(summary.getAverageCostPerRequest()).isEqualTo(0.03); + assertThat(summary.getPeriod()).isEqualTo("monthly"); + } + } + + @Nested + @DisplayName("UsageBreakdownItem") + class UsageBreakdownItemTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"group_value\":\"openai\",\"cost_usd\":100.0,\"percentage\":66.7," + + "\"request_count\":3000,\"tokens_in\":600000,\"tokens_out\":300000}"; + + UsageBreakdownItem item = MAPPER.readValue(json, UsageBreakdownItem.class); + + assertThat(item.getGroupValue()).isEqualTo("openai"); + assertThat(item.getCostUsd()).isEqualTo(100.0); + assertThat(item.getPercentage()).isEqualTo(66.7); + assertThat(item.getRequestCount()).isEqualTo(3000); + assertThat(item.getTokensIn()).isEqualTo(600000); + assertThat(item.getTokensOut()).isEqualTo(300000); } + } + + @Nested + @DisplayName("UsageBreakdown") + class UsageBreakdownTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"group_by\":\"provider\",\"total_cost_usd\":150.0," + + "\"items\":[{\"group_value\":\"openai\",\"cost_usd\":100.0}],\"period\":\"monthly\"," + + "\"period_start\":\"2025-01-01\",\"period_end\":\"2025-01-31\"}"; + + UsageBreakdown breakdown = MAPPER.readValue(json, UsageBreakdown.class); + + assertThat(breakdown.getGroupBy()).isEqualTo("provider"); + assertThat(breakdown.getTotalCostUsd()).isEqualTo(150.0); + assertThat(breakdown.getItems()).hasSize(1); + assertThat(breakdown.getPeriod()).isEqualTo("monthly"); + } + } + + @Nested + @DisplayName("UsageRecord") + class UsageRecordTests { + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"id\":\"record-123\",\"provider\":\"openai\",\"model\":\"gpt-4\"," + + "\"tokens_in\":100,\"tokens_out\":50,\"cost_usd\":0.0045," + + "\"request_id\":\"req-456\",\"org_id\":\"org-789\",\"agent_id\":\"agent-101\"," + + "\"timestamp\":\"2025-01-15T12:00:00Z\"}"; + + UsageRecord record = MAPPER.readValue(json, UsageRecord.class); + + assertThat(record.getId()).isEqualTo("record-123"); + assertThat(record.getProvider()).isEqualTo("openai"); + assertThat(record.getModel()).isEqualTo("gpt-4"); + assertThat(record.getTokensIn()).isEqualTo(100); + assertThat(record.getTokensOut()).isEqualTo(50); + assertThat(record.getCostUsd()).isEqualTo(0.0045); + assertThat(record.getRequestId()).isEqualTo("req-456"); + assertThat(record.getOrgId()).isEqualTo("org-789"); + assertThat(record.getAgentId()).isEqualTo("agent-101"); + assertThat(record.getTimestamp()).isEqualTo("2025-01-15T12:00:00Z"); + } + } - @Nested - @DisplayName("UsageRecordsResponse") - class UsageRecordsResponseTests { + @Nested + @DisplayName("UsageRecordsResponse") + class UsageRecordsResponseTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"records\":[{\"id\":\"record-1\"},{\"id\":\"record-2\"}],\"total\":2}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"records\":[{\"id\":\"record-1\"},{\"id\":\"record-2\"}],\"total\":2}"; - UsageRecordsResponse response = MAPPER.readValue(json, UsageRecordsResponse.class); + UsageRecordsResponse response = MAPPER.readValue(json, UsageRecordsResponse.class); - assertThat(response.getRecords()).hasSize(2); - assertThat(response.getTotal()).isEqualTo(2); - } + assertThat(response.getRecords()).hasSize(2); + assertThat(response.getTotal()).isEqualTo(2); } + } - @Nested - @DisplayName("ModelPricing") - class ModelPricingTests { + @Nested + @DisplayName("ModelPricing") + class ModelPricingTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"input_per_1k\":0.03,\"output_per_1k\":0.06}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"input_per_1k\":0.03,\"output_per_1k\":0.06}"; - ModelPricing pricing = MAPPER.readValue(json, ModelPricing.class); + ModelPricing pricing = MAPPER.readValue(json, ModelPricing.class); - assertThat(pricing.getInputPer1k()).isEqualTo(0.03); - assertThat(pricing.getOutputPer1k()).isEqualTo(0.06); - } + assertThat(pricing.getInputPer1k()).isEqualTo(0.03); + assertThat(pricing.getOutputPer1k()).isEqualTo(0.06); } + } - @Nested - @DisplayName("PricingInfo") - class PricingInfoTests { + @Nested + @DisplayName("PricingInfo") + class PricingInfoTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"provider\":\"openai\",\"model\":\"gpt-4\"," + - "\"pricing\":{\"input_per_1k\":0.03,\"output_per_1k\":0.06}}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"provider\":\"openai\",\"model\":\"gpt-4\"," + + "\"pricing\":{\"input_per_1k\":0.03,\"output_per_1k\":0.06}}"; - PricingInfo info = MAPPER.readValue(json, PricingInfo.class); + PricingInfo info = MAPPER.readValue(json, PricingInfo.class); - assertThat(info.getProvider()).isEqualTo("openai"); - assertThat(info.getModel()).isEqualTo("gpt-4"); - assertThat(info.getPricing().getInputPer1k()).isEqualTo(0.03); - } + assertThat(info.getProvider()).isEqualTo("openai"); + assertThat(info.getModel()).isEqualTo("gpt-4"); + assertThat(info.getPricing().getInputPer1k()).isEqualTo(0.03); } + } - @Nested - @DisplayName("PricingListResponse") - class PricingListResponseTests { + @Nested + @DisplayName("PricingListResponse") + class PricingListResponseTests { - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"pricing\":[{\"provider\":\"openai\"},{\"provider\":\"anthropic\"}]}"; + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"pricing\":[{\"provider\":\"openai\"},{\"provider\":\"anthropic\"}]}"; - PricingListResponse response = MAPPER.readValue(json, PricingListResponse.class); + PricingListResponse response = MAPPER.readValue(json, PricingListResponse.class); - assertThat(response.getPricing()).hasSize(2); - } + assertThat(response.getPricing()).hasSize(2); + } - @Test - @DisplayName("setPricing should work") - void setPricingShouldWork() { - PricingListResponse response = new PricingListResponse(); - response.setPricing(List.of()); - assertThat(response.getPricing()).isEmpty(); - } + @Test + @DisplayName("setPricing should work") + void setPricingShouldWork() { + PricingListResponse response = new PricingListResponse(); + response.setPricing(List.of()); + assertThat(response.getPricing()).isEmpty(); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/ExecutionReplayTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/ExecutionReplayTypesTest.java index c9df759..642fb0e 100644 --- a/src/test/java/com/getaxonflow/sdk/types/ExecutionReplayTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/ExecutionReplayTypesTest.java @@ -15,252 +15,259 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.getaxonflow.sdk.types.executionreplay.*; import com.getaxonflow.sdk.types.executionreplay.ExecutionReplayTypes.*; +import java.util.Arrays; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.Arrays; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("Execution Replay Types") class ExecutionReplayTypesTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } - - // ======================================================================== - // ExecutionSummary Tests - // ======================================================================== - - @Test - @DisplayName("ExecutionSummary should deserialize from JSON") - void executionSummaryShouldDeserialize() throws Exception { - String json = "{\"request_id\":\"exec-123\",\"workflow_name\":\"test-workflow\"," + - "\"status\":\"completed\",\"total_steps\":3,\"completed_steps\":3," + - "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:05Z\"," + - "\"duration_ms\":5000,\"total_tokens\":150,\"total_cost_usd\":0.01}"; - - ExecutionSummary summary = objectMapper.readValue(json, ExecutionSummary.class); - - assertThat(summary.getRequestId()).isEqualTo("exec-123"); - assertThat(summary.getWorkflowName()).isEqualTo("test-workflow"); - assertThat(summary.getStatus()).isEqualTo("completed"); - assertThat(summary.getTotalSteps()).isEqualTo(3); - assertThat(summary.getCompletedSteps()).isEqualTo(3); - assertThat(summary.getStartedAt()).isEqualTo("2026-01-03T12:00:00Z"); - assertThat(summary.getCompletedAt()).isEqualTo("2026-01-03T12:00:05Z"); - assertThat(summary.getDurationMs()).isEqualTo(5000); - assertThat(summary.getTotalTokens()).isEqualTo(150); - assertThat(summary.getTotalCostUsd()).isEqualTo(0.01); - } - - @Test - @DisplayName("ExecutionSummary setters should work") - void executionSummarySettersShouldWork() { - ExecutionSummary summary = new ExecutionSummary(); - summary.setRequestId("exec-456"); - summary.setWorkflowName("my-workflow"); - summary.setStatus("running"); - summary.setTotalSteps(5); - summary.setCompletedSteps(2); - summary.setStartedAt("2026-01-03T10:00:00Z"); - summary.setTotalTokens(100); - summary.setTotalCostUsd(0.005); - - assertThat(summary.getRequestId()).isEqualTo("exec-456"); - assertThat(summary.getWorkflowName()).isEqualTo("my-workflow"); - assertThat(summary.getStatus()).isEqualTo("running"); - assertThat(summary.getTotalSteps()).isEqualTo(5); - assertThat(summary.getCompletedSteps()).isEqualTo(2); - } - - // ======================================================================== - // ExecutionSnapshot Tests - // ======================================================================== - - @Test - @DisplayName("ExecutionSnapshot should deserialize from JSON") - void executionSnapshotShouldDeserialize() throws Exception { - String json = "{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\"," + - "\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\"," + - "\"completed_at\":\"2026-01-03T12:00:02Z\",\"duration_ms\":2000," + - "\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4\"," + - "\"tokens_in\":20,\"tokens_out\":30,\"cost_usd\":0.002}"; - - ExecutionSnapshot snapshot = objectMapper.readValue(json, ExecutionSnapshot.class); - - assertThat(snapshot.getRequestId()).isEqualTo("exec-123"); - assertThat(snapshot.getStepIndex()).isEqualTo(0); - assertThat(snapshot.getStepName()).isEqualTo("greet"); - assertThat(snapshot.getStatus()).isEqualTo("completed"); - assertThat(snapshot.getProvider()).isEqualTo("anthropic"); - assertThat(snapshot.getModel()).isEqualTo("claude-sonnet-4"); - assertThat(snapshot.getTokensIn()).isEqualTo(20); - assertThat(snapshot.getTokensOut()).isEqualTo(30); - assertThat(snapshot.getCostUsd()).isEqualTo(0.002); - } - - @Test - @DisplayName("ExecutionSnapshot setters should work") - void executionSnapshotSettersShouldWork() { - ExecutionSnapshot snapshot = new ExecutionSnapshot(); - snapshot.setRequestId("exec-789"); - snapshot.setStepIndex(1); - snapshot.setStepName("process"); - snapshot.setStatus("running"); - snapshot.setProvider("openai"); - snapshot.setModel("gpt-4"); - - assertThat(snapshot.getRequestId()).isEqualTo("exec-789"); - assertThat(snapshot.getStepIndex()).isEqualTo(1); - assertThat(snapshot.getStepName()).isEqualTo("process"); - } - - // ======================================================================== - // TimelineEntry Tests - // ======================================================================== - - @Test - @DisplayName("TimelineEntry should deserialize from JSON") - void timelineEntryShouldDeserialize() throws Exception { - String json = "{\"step_index\":0,\"step_name\":\"start\",\"status\":\"completed\"," + - "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:01Z\"," + - "\"duration_ms\":1000,\"has_error\":false,\"has_approval\":true}"; - - TimelineEntry entry = objectMapper.readValue(json, TimelineEntry.class); - - assertThat(entry.getStepIndex()).isEqualTo(0); - assertThat(entry.getStepName()).isEqualTo("start"); - assertThat(entry.getStatus()).isEqualTo("completed"); - assertThat(entry.getDurationMs()).isEqualTo(1000); - assertThat(entry.hasError()).isFalse(); - assertThat(entry.hasApproval()).isTrue(); - } - - @Test - @DisplayName("TimelineEntry with error should deserialize") - void timelineEntryWithErrorShouldDeserialize() throws Exception { - String json = "{\"step_index\":2,\"step_name\":\"failed-step\",\"status\":\"failed\"," + - "\"started_at\":\"2026-01-03T12:00:10Z\",\"has_error\":true,\"has_approval\":false}"; - - TimelineEntry entry = objectMapper.readValue(json, TimelineEntry.class); - - assertThat(entry.getStepName()).isEqualTo("failed-step"); - assertThat(entry.getStatus()).isEqualTo("failed"); - assertThat(entry.hasError()).isTrue(); - assertThat(entry.hasApproval()).isFalse(); - } - - // ======================================================================== - // ExecutionDetail Tests - // ======================================================================== - - @Test - @DisplayName("ExecutionDetail should deserialize from JSON") - void executionDetailShouldDeserialize() throws Exception { - String json = "{\"summary\":{\"request_id\":\"exec-123\",\"workflow_name\":\"test-workflow\"," + - "\"status\":\"completed\",\"total_steps\":2,\"completed_steps\":2," + - "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:05Z\"," + - "\"total_tokens\":100,\"total_cost_usd\":0.005}," + - "\"steps\":[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\"," + - "\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\"," + - "\"tokens_in\":10,\"tokens_out\":20}]}"; - - ExecutionDetail detail = objectMapper.readValue(json, ExecutionDetail.class); - - assertThat(detail.getSummary()).isNotNull(); - assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-123"); - assertThat(detail.getSummary().getStatus()).isEqualTo("completed"); - assertThat(detail.getSteps()).hasSize(1); - assertThat(detail.getSteps().get(0).getStepName()).isEqualTo("greet"); - } - - @Test - @DisplayName("ExecutionDetail setters should work") - void executionDetailSettersShouldWork() { - ExecutionSummary summary = new ExecutionSummary(); - summary.setRequestId("exec-999"); - - ExecutionSnapshot step = new ExecutionSnapshot(); - step.setStepName("test-step"); - - ExecutionDetail detail = new ExecutionDetail(); - detail.setSummary(summary); - detail.setSteps(Arrays.asList(step)); - - assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-999"); - assertThat(detail.getSteps()).hasSize(1); - } - - // ======================================================================== - // ListExecutionsResponse Tests - // ======================================================================== - - @Test - @DisplayName("ListExecutionsResponse should deserialize from JSON") - void listExecutionsResponseShouldDeserialize() throws Exception { - String json = "{\"executions\":[" + - "{\"request_id\":\"exec-1\",\"workflow_name\":\"workflow-1\",\"status\":\"completed\"," + - "\"total_steps\":1,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:00Z\"," + - "\"total_tokens\":50,\"total_cost_usd\":0.001}," + - "{\"request_id\":\"exec-2\",\"workflow_name\":\"workflow-2\",\"status\":\"running\"," + - "\"total_steps\":3,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:10Z\"," + - "\"total_tokens\":25,\"total_cost_usd\":0.0005}]," + - "\"total\":2,\"limit\":50,\"offset\":0}"; - - ListExecutionsResponse response = objectMapper.readValue(json, ListExecutionsResponse.class); - - assertThat(response.getExecutions()).hasSize(2); - assertThat(response.getTotal()).isEqualTo(2); - assertThat(response.getLimit()).isEqualTo(50); - assertThat(response.getOffset()).isEqualTo(0); - assertThat(response.getExecutions().get(0).getRequestId()).isEqualTo("exec-1"); - assertThat(response.getExecutions().get(1).getStatus()).isEqualTo("running"); - } - - // ======================================================================== - // ListExecutionsOptions Tests - // ======================================================================== - - @Test - @DisplayName("ListExecutionsOptions fluent setters should work") - void listExecutionsOptionsFluentSettersShouldWork() { - ListExecutionsOptions options = ListExecutionsOptions.builder() + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + // ======================================================================== + // ExecutionSummary Tests + // ======================================================================== + + @Test + @DisplayName("ExecutionSummary should deserialize from JSON") + void executionSummaryShouldDeserialize() throws Exception { + String json = + "{\"request_id\":\"exec-123\",\"workflow_name\":\"test-workflow\"," + + "\"status\":\"completed\",\"total_steps\":3,\"completed_steps\":3," + + "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:05Z\"," + + "\"duration_ms\":5000,\"total_tokens\":150,\"total_cost_usd\":0.01}"; + + ExecutionSummary summary = objectMapper.readValue(json, ExecutionSummary.class); + + assertThat(summary.getRequestId()).isEqualTo("exec-123"); + assertThat(summary.getWorkflowName()).isEqualTo("test-workflow"); + assertThat(summary.getStatus()).isEqualTo("completed"); + assertThat(summary.getTotalSteps()).isEqualTo(3); + assertThat(summary.getCompletedSteps()).isEqualTo(3); + assertThat(summary.getStartedAt()).isEqualTo("2026-01-03T12:00:00Z"); + assertThat(summary.getCompletedAt()).isEqualTo("2026-01-03T12:00:05Z"); + assertThat(summary.getDurationMs()).isEqualTo(5000); + assertThat(summary.getTotalTokens()).isEqualTo(150); + assertThat(summary.getTotalCostUsd()).isEqualTo(0.01); + } + + @Test + @DisplayName("ExecutionSummary setters should work") + void executionSummarySettersShouldWork() { + ExecutionSummary summary = new ExecutionSummary(); + summary.setRequestId("exec-456"); + summary.setWorkflowName("my-workflow"); + summary.setStatus("running"); + summary.setTotalSteps(5); + summary.setCompletedSteps(2); + summary.setStartedAt("2026-01-03T10:00:00Z"); + summary.setTotalTokens(100); + summary.setTotalCostUsd(0.005); + + assertThat(summary.getRequestId()).isEqualTo("exec-456"); + assertThat(summary.getWorkflowName()).isEqualTo("my-workflow"); + assertThat(summary.getStatus()).isEqualTo("running"); + assertThat(summary.getTotalSteps()).isEqualTo(5); + assertThat(summary.getCompletedSteps()).isEqualTo(2); + } + + // ======================================================================== + // ExecutionSnapshot Tests + // ======================================================================== + + @Test + @DisplayName("ExecutionSnapshot should deserialize from JSON") + void executionSnapshotShouldDeserialize() throws Exception { + String json = + "{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\"," + + "\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\"," + + "\"completed_at\":\"2026-01-03T12:00:02Z\",\"duration_ms\":2000," + + "\"provider\":\"anthropic\",\"model\":\"claude-sonnet-4\"," + + "\"tokens_in\":20,\"tokens_out\":30,\"cost_usd\":0.002}"; + + ExecutionSnapshot snapshot = objectMapper.readValue(json, ExecutionSnapshot.class); + + assertThat(snapshot.getRequestId()).isEqualTo("exec-123"); + assertThat(snapshot.getStepIndex()).isEqualTo(0); + assertThat(snapshot.getStepName()).isEqualTo("greet"); + assertThat(snapshot.getStatus()).isEqualTo("completed"); + assertThat(snapshot.getProvider()).isEqualTo("anthropic"); + assertThat(snapshot.getModel()).isEqualTo("claude-sonnet-4"); + assertThat(snapshot.getTokensIn()).isEqualTo(20); + assertThat(snapshot.getTokensOut()).isEqualTo(30); + assertThat(snapshot.getCostUsd()).isEqualTo(0.002); + } + + @Test + @DisplayName("ExecutionSnapshot setters should work") + void executionSnapshotSettersShouldWork() { + ExecutionSnapshot snapshot = new ExecutionSnapshot(); + snapshot.setRequestId("exec-789"); + snapshot.setStepIndex(1); + snapshot.setStepName("process"); + snapshot.setStatus("running"); + snapshot.setProvider("openai"); + snapshot.setModel("gpt-4"); + + assertThat(snapshot.getRequestId()).isEqualTo("exec-789"); + assertThat(snapshot.getStepIndex()).isEqualTo(1); + assertThat(snapshot.getStepName()).isEqualTo("process"); + } + + // ======================================================================== + // TimelineEntry Tests + // ======================================================================== + + @Test + @DisplayName("TimelineEntry should deserialize from JSON") + void timelineEntryShouldDeserialize() throws Exception { + String json = + "{\"step_index\":0,\"step_name\":\"start\",\"status\":\"completed\"," + + "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:01Z\"," + + "\"duration_ms\":1000,\"has_error\":false,\"has_approval\":true}"; + + TimelineEntry entry = objectMapper.readValue(json, TimelineEntry.class); + + assertThat(entry.getStepIndex()).isEqualTo(0); + assertThat(entry.getStepName()).isEqualTo("start"); + assertThat(entry.getStatus()).isEqualTo("completed"); + assertThat(entry.getDurationMs()).isEqualTo(1000); + assertThat(entry.hasError()).isFalse(); + assertThat(entry.hasApproval()).isTrue(); + } + + @Test + @DisplayName("TimelineEntry with error should deserialize") + void timelineEntryWithErrorShouldDeserialize() throws Exception { + String json = + "{\"step_index\":2,\"step_name\":\"failed-step\",\"status\":\"failed\"," + + "\"started_at\":\"2026-01-03T12:00:10Z\",\"has_error\":true,\"has_approval\":false}"; + + TimelineEntry entry = objectMapper.readValue(json, TimelineEntry.class); + + assertThat(entry.getStepName()).isEqualTo("failed-step"); + assertThat(entry.getStatus()).isEqualTo("failed"); + assertThat(entry.hasError()).isTrue(); + assertThat(entry.hasApproval()).isFalse(); + } + + // ======================================================================== + // ExecutionDetail Tests + // ======================================================================== + + @Test + @DisplayName("ExecutionDetail should deserialize from JSON") + void executionDetailShouldDeserialize() throws Exception { + String json = + "{\"summary\":{\"request_id\":\"exec-123\",\"workflow_name\":\"test-workflow\"," + + "\"status\":\"completed\",\"total_steps\":2,\"completed_steps\":2," + + "\"started_at\":\"2026-01-03T12:00:00Z\",\"completed_at\":\"2026-01-03T12:00:05Z\"," + + "\"total_tokens\":100,\"total_cost_usd\":0.005}," + + "\"steps\":[{\"request_id\":\"exec-123\",\"step_index\":0,\"step_name\":\"greet\"," + + "\"status\":\"completed\",\"started_at\":\"2026-01-03T12:00:00Z\"," + + "\"tokens_in\":10,\"tokens_out\":20}]}"; + + ExecutionDetail detail = objectMapper.readValue(json, ExecutionDetail.class); + + assertThat(detail.getSummary()).isNotNull(); + assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-123"); + assertThat(detail.getSummary().getStatus()).isEqualTo("completed"); + assertThat(detail.getSteps()).hasSize(1); + assertThat(detail.getSteps().get(0).getStepName()).isEqualTo("greet"); + } + + @Test + @DisplayName("ExecutionDetail setters should work") + void executionDetailSettersShouldWork() { + ExecutionSummary summary = new ExecutionSummary(); + summary.setRequestId("exec-999"); + + ExecutionSnapshot step = new ExecutionSnapshot(); + step.setStepName("test-step"); + + ExecutionDetail detail = new ExecutionDetail(); + detail.setSummary(summary); + detail.setSteps(Arrays.asList(step)); + + assertThat(detail.getSummary().getRequestId()).isEqualTo("exec-999"); + assertThat(detail.getSteps()).hasSize(1); + } + + // ======================================================================== + // ListExecutionsResponse Tests + // ======================================================================== + + @Test + @DisplayName("ListExecutionsResponse should deserialize from JSON") + void listExecutionsResponseShouldDeserialize() throws Exception { + String json = + "{\"executions\":[" + + "{\"request_id\":\"exec-1\",\"workflow_name\":\"workflow-1\",\"status\":\"completed\"," + + "\"total_steps\":1,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:00Z\"," + + "\"total_tokens\":50,\"total_cost_usd\":0.001}," + + "{\"request_id\":\"exec-2\",\"workflow_name\":\"workflow-2\",\"status\":\"running\"," + + "\"total_steps\":3,\"completed_steps\":1,\"started_at\":\"2026-01-03T12:00:10Z\"," + + "\"total_tokens\":25,\"total_cost_usd\":0.0005}]," + + "\"total\":2,\"limit\":50,\"offset\":0}"; + + ListExecutionsResponse response = objectMapper.readValue(json, ListExecutionsResponse.class); + + assertThat(response.getExecutions()).hasSize(2); + assertThat(response.getTotal()).isEqualTo(2); + assertThat(response.getLimit()).isEqualTo(50); + assertThat(response.getOffset()).isEqualTo(0); + assertThat(response.getExecutions().get(0).getRequestId()).isEqualTo("exec-1"); + assertThat(response.getExecutions().get(1).getStatus()).isEqualTo("running"); + } + + // ======================================================================== + // ListExecutionsOptions Tests + // ======================================================================== + + @Test + @DisplayName("ListExecutionsOptions fluent setters should work") + void listExecutionsOptionsFluentSettersShouldWork() { + ListExecutionsOptions options = + ListExecutionsOptions.builder() .setLimit(10) .setOffset(20) .setStatus("completed") .setWorkflowId("test-workflow"); - assertThat(options.getLimit()).isEqualTo(10); - assertThat(options.getOffset()).isEqualTo(20); - assertThat(options.getStatus()).isEqualTo("completed"); - assertThat(options.getWorkflowId()).isEqualTo("test-workflow"); - } - - // ======================================================================== - // ExecutionExportOptions Tests - // ======================================================================== - - @Test - @DisplayName("ExecutionExportOptions fluent setters should work") - void executionExportOptionsFluentSettersShouldWork() { - ExecutionExportOptions options = ExecutionExportOptions.builder() + assertThat(options.getLimit()).isEqualTo(10); + assertThat(options.getOffset()).isEqualTo(20); + assertThat(options.getStatus()).isEqualTo("completed"); + assertThat(options.getWorkflowId()).isEqualTo("test-workflow"); + } + + // ======================================================================== + // ExecutionExportOptions Tests + // ======================================================================== + + @Test + @DisplayName("ExecutionExportOptions fluent setters should work") + void executionExportOptionsFluentSettersShouldWork() { + ExecutionExportOptions options = + ExecutionExportOptions.builder() .setFormat("json") .setIncludeInput(true) .setIncludeOutput(true) .setIncludePolicies(false); - assertThat(options.getFormat()).isEqualTo("json"); - assertThat(options.isIncludeInput()).isTrue(); - assertThat(options.isIncludeOutput()).isTrue(); - assertThat(options.isIncludePolicies()).isFalse(); - } + assertThat(options.getFormat()).isEqualTo("json"); + assertThat(options.isIncludeInput()).isTrue(); + assertThat(options.isIncludeOutput()).isTrue(); + assertThat(options.isIncludePolicies()).isFalse(); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/ExecutionTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/ExecutionTypesTest.java index f86c917..afb4aa8 100644 --- a/src/test/java/com/getaxonflow/sdk/types/ExecutionTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/ExecutionTypesTest.java @@ -16,490 +16,455 @@ package com.getaxonflow.sdk.types; -import com.getaxonflow.sdk.types.execution.ExecutionTypes.*; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.EnumSource; +import static org.junit.jupiter.api.Assertions.*; +import com.getaxonflow.sdk.types.execution.ExecutionTypes.*; import java.time.Instant; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests for unified execution types. - */ +/** Tests for unified execution types. */ class ExecutionTypesTest { - @Test - @DisplayName("ExecutionType.fromValue should return correct enum") - void testExecutionTypeFromValue() { - assertEquals(ExecutionType.MAP_PLAN, ExecutionType.fromValue("map_plan")); - assertEquals(ExecutionType.WCP_WORKFLOW, ExecutionType.fromValue("wcp_workflow")); - } - - @Test - @DisplayName("ExecutionType.fromValue should throw for unknown value") - void testExecutionTypeFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> ExecutionType.fromValue("unknown")); - } - - @Test - @DisplayName("ExecutionType.getValue should return correct string") - void testExecutionTypeGetValue() { - assertEquals("map_plan", ExecutionType.MAP_PLAN.getValue()); - assertEquals("wcp_workflow", ExecutionType.WCP_WORKFLOW.getValue()); - } - - @ParameterizedTest - @EnumSource(ExecutionStatusValue.class) - @DisplayName("ExecutionStatusValue should have correct terminal status") - void testExecutionStatusValueIsTerminal(ExecutionStatusValue status) { - boolean expected = status == ExecutionStatusValue.COMPLETED || - status == ExecutionStatusValue.FAILED || - status == ExecutionStatusValue.CANCELLED || - status == ExecutionStatusValue.ABORTED || - status == ExecutionStatusValue.EXPIRED; - assertEquals(expected, status.isTerminal()); - } - - @Test - @DisplayName("ExecutionStatusValue.fromValue should return correct enum") - void testExecutionStatusValueFromValue() { - assertEquals(ExecutionStatusValue.PENDING, ExecutionStatusValue.fromValue("pending")); - assertEquals(ExecutionStatusValue.RUNNING, ExecutionStatusValue.fromValue("running")); - assertEquals(ExecutionStatusValue.COMPLETED, ExecutionStatusValue.fromValue("completed")); - assertEquals(ExecutionStatusValue.FAILED, ExecutionStatusValue.fromValue("failed")); - assertEquals(ExecutionStatusValue.CANCELLED, ExecutionStatusValue.fromValue("cancelled")); - assertEquals(ExecutionStatusValue.ABORTED, ExecutionStatusValue.fromValue("aborted")); - assertEquals(ExecutionStatusValue.EXPIRED, ExecutionStatusValue.fromValue("expired")); - } - - @Test - @DisplayName("ExecutionStatusValue.fromValue should throw for unknown value") - void testExecutionStatusValueFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> ExecutionStatusValue.fromValue("unknown")); - } - - @ParameterizedTest - @EnumSource(StepStatusValue.class) - @DisplayName("StepStatusValue should have correct terminal status") - void testStepStatusValueIsTerminal(StepStatusValue status) { - boolean expected = status == StepStatusValue.COMPLETED || - status == StepStatusValue.FAILED || - status == StepStatusValue.SKIPPED; - assertEquals(expected, status.isTerminal()); - } - - @ParameterizedTest - @EnumSource(StepStatusValue.class) - @DisplayName("StepStatusValue should have correct blocking status") - void testStepStatusValueIsBlocking(StepStatusValue status) { - boolean expected = status == StepStatusValue.BLOCKED || - status == StepStatusValue.APPROVAL; - assertEquals(expected, status.isBlocking()); - } - - @Test - @DisplayName("StepStatusValue.fromValue should return correct enum") - void testStepStatusValueFromValue() { - assertEquals(StepStatusValue.PENDING, StepStatusValue.fromValue("pending")); - assertEquals(StepStatusValue.RUNNING, StepStatusValue.fromValue("running")); - assertEquals(StepStatusValue.COMPLETED, StepStatusValue.fromValue("completed")); - assertEquals(StepStatusValue.FAILED, StepStatusValue.fromValue("failed")); - assertEquals(StepStatusValue.SKIPPED, StepStatusValue.fromValue("skipped")); - assertEquals(StepStatusValue.BLOCKED, StepStatusValue.fromValue("blocked")); - assertEquals(StepStatusValue.APPROVAL, StepStatusValue.fromValue("approval")); - } - - @Test - @DisplayName("StepStatusValue.fromValue should throw for unknown value") - void testStepStatusValueFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> StepStatusValue.fromValue("unknown")); - } - - @Test - @DisplayName("UnifiedStepType.fromValue should return correct enum") - void testUnifiedStepTypeFromValue() { - assertEquals(UnifiedStepType.LLM_CALL, UnifiedStepType.fromValue("llm_call")); - assertEquals(UnifiedStepType.TOOL_CALL, UnifiedStepType.fromValue("tool_call")); - assertEquals(UnifiedStepType.CONNECTOR_CALL, UnifiedStepType.fromValue("connector_call")); - assertEquals(UnifiedStepType.HUMAN_TASK, UnifiedStepType.fromValue("human_task")); - assertEquals(UnifiedStepType.SYNTHESIS, UnifiedStepType.fromValue("synthesis")); - assertEquals(UnifiedStepType.ACTION, UnifiedStepType.fromValue("action")); - assertEquals(UnifiedStepType.GATE, UnifiedStepType.fromValue("gate")); - } - - @Test - @DisplayName("UnifiedStepType.fromValue should throw for unknown value") - void testUnifiedStepTypeFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> UnifiedStepType.fromValue("unknown")); - } - - @Test - @DisplayName("UnifiedGateDecision.fromValue should return correct enum") - void testUnifiedGateDecisionFromValue() { - assertEquals(UnifiedGateDecision.ALLOW, UnifiedGateDecision.fromValue("allow")); - assertEquals(UnifiedGateDecision.BLOCK, UnifiedGateDecision.fromValue("block")); - assertEquals(UnifiedGateDecision.REQUIRE_APPROVAL, UnifiedGateDecision.fromValue("require_approval")); - } - - @Test - @DisplayName("UnifiedGateDecision.fromValue should throw for unknown value") - void testUnifiedGateDecisionFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> UnifiedGateDecision.fromValue("unknown")); - } - - @Test - @DisplayName("UnifiedApprovalStatus.fromValue should return correct enum") - void testUnifiedApprovalStatusFromValue() { - assertEquals(UnifiedApprovalStatus.PENDING, UnifiedApprovalStatus.fromValue("pending")); - assertEquals(UnifiedApprovalStatus.APPROVED, UnifiedApprovalStatus.fromValue("approved")); - assertEquals(UnifiedApprovalStatus.REJECTED, UnifiedApprovalStatus.fromValue("rejected")); - } - - @Test - @DisplayName("UnifiedApprovalStatus.fromValue should throw for unknown value") - void testUnifiedApprovalStatusFromValueThrows() { - assertThrows(IllegalArgumentException.class, () -> UnifiedApprovalStatus.fromValue("unknown")); - } - - @Test - @DisplayName("UnifiedStepStatus builder should create valid object") - void testUnifiedStepStatusBuilder() { - Instant now = Instant.now(); - UnifiedStepStatus step = UnifiedStepStatus.builder() + @Test + @DisplayName("ExecutionType.fromValue should return correct enum") + void testExecutionTypeFromValue() { + assertEquals(ExecutionType.MAP_PLAN, ExecutionType.fromValue("map_plan")); + assertEquals(ExecutionType.WCP_WORKFLOW, ExecutionType.fromValue("wcp_workflow")); + } + + @Test + @DisplayName("ExecutionType.fromValue should throw for unknown value") + void testExecutionTypeFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> ExecutionType.fromValue("unknown")); + } + + @Test + @DisplayName("ExecutionType.getValue should return correct string") + void testExecutionTypeGetValue() { + assertEquals("map_plan", ExecutionType.MAP_PLAN.getValue()); + assertEquals("wcp_workflow", ExecutionType.WCP_WORKFLOW.getValue()); + } + + @ParameterizedTest + @EnumSource(ExecutionStatusValue.class) + @DisplayName("ExecutionStatusValue should have correct terminal status") + void testExecutionStatusValueIsTerminal(ExecutionStatusValue status) { + boolean expected = + status == ExecutionStatusValue.COMPLETED + || status == ExecutionStatusValue.FAILED + || status == ExecutionStatusValue.CANCELLED + || status == ExecutionStatusValue.ABORTED + || status == ExecutionStatusValue.EXPIRED; + assertEquals(expected, status.isTerminal()); + } + + @Test + @DisplayName("ExecutionStatusValue.fromValue should return correct enum") + void testExecutionStatusValueFromValue() { + assertEquals(ExecutionStatusValue.PENDING, ExecutionStatusValue.fromValue("pending")); + assertEquals(ExecutionStatusValue.RUNNING, ExecutionStatusValue.fromValue("running")); + assertEquals(ExecutionStatusValue.COMPLETED, ExecutionStatusValue.fromValue("completed")); + assertEquals(ExecutionStatusValue.FAILED, ExecutionStatusValue.fromValue("failed")); + assertEquals(ExecutionStatusValue.CANCELLED, ExecutionStatusValue.fromValue("cancelled")); + assertEquals(ExecutionStatusValue.ABORTED, ExecutionStatusValue.fromValue("aborted")); + assertEquals(ExecutionStatusValue.EXPIRED, ExecutionStatusValue.fromValue("expired")); + } + + @Test + @DisplayName("ExecutionStatusValue.fromValue should throw for unknown value") + void testExecutionStatusValueFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> ExecutionStatusValue.fromValue("unknown")); + } + + @ParameterizedTest + @EnumSource(StepStatusValue.class) + @DisplayName("StepStatusValue should have correct terminal status") + void testStepStatusValueIsTerminal(StepStatusValue status) { + boolean expected = + status == StepStatusValue.COMPLETED + || status == StepStatusValue.FAILED + || status == StepStatusValue.SKIPPED; + assertEquals(expected, status.isTerminal()); + } + + @ParameterizedTest + @EnumSource(StepStatusValue.class) + @DisplayName("StepStatusValue should have correct blocking status") + void testStepStatusValueIsBlocking(StepStatusValue status) { + boolean expected = status == StepStatusValue.BLOCKED || status == StepStatusValue.APPROVAL; + assertEquals(expected, status.isBlocking()); + } + + @Test + @DisplayName("StepStatusValue.fromValue should return correct enum") + void testStepStatusValueFromValue() { + assertEquals(StepStatusValue.PENDING, StepStatusValue.fromValue("pending")); + assertEquals(StepStatusValue.RUNNING, StepStatusValue.fromValue("running")); + assertEquals(StepStatusValue.COMPLETED, StepStatusValue.fromValue("completed")); + assertEquals(StepStatusValue.FAILED, StepStatusValue.fromValue("failed")); + assertEquals(StepStatusValue.SKIPPED, StepStatusValue.fromValue("skipped")); + assertEquals(StepStatusValue.BLOCKED, StepStatusValue.fromValue("blocked")); + assertEquals(StepStatusValue.APPROVAL, StepStatusValue.fromValue("approval")); + } + + @Test + @DisplayName("StepStatusValue.fromValue should throw for unknown value") + void testStepStatusValueFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> StepStatusValue.fromValue("unknown")); + } + + @Test + @DisplayName("UnifiedStepType.fromValue should return correct enum") + void testUnifiedStepTypeFromValue() { + assertEquals(UnifiedStepType.LLM_CALL, UnifiedStepType.fromValue("llm_call")); + assertEquals(UnifiedStepType.TOOL_CALL, UnifiedStepType.fromValue("tool_call")); + assertEquals(UnifiedStepType.CONNECTOR_CALL, UnifiedStepType.fromValue("connector_call")); + assertEquals(UnifiedStepType.HUMAN_TASK, UnifiedStepType.fromValue("human_task")); + assertEquals(UnifiedStepType.SYNTHESIS, UnifiedStepType.fromValue("synthesis")); + assertEquals(UnifiedStepType.ACTION, UnifiedStepType.fromValue("action")); + assertEquals(UnifiedStepType.GATE, UnifiedStepType.fromValue("gate")); + } + + @Test + @DisplayName("UnifiedStepType.fromValue should throw for unknown value") + void testUnifiedStepTypeFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> UnifiedStepType.fromValue("unknown")); + } + + @Test + @DisplayName("UnifiedGateDecision.fromValue should return correct enum") + void testUnifiedGateDecisionFromValue() { + assertEquals(UnifiedGateDecision.ALLOW, UnifiedGateDecision.fromValue("allow")); + assertEquals(UnifiedGateDecision.BLOCK, UnifiedGateDecision.fromValue("block")); + assertEquals( + UnifiedGateDecision.REQUIRE_APPROVAL, UnifiedGateDecision.fromValue("require_approval")); + } + + @Test + @DisplayName("UnifiedGateDecision.fromValue should throw for unknown value") + void testUnifiedGateDecisionFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> UnifiedGateDecision.fromValue("unknown")); + } + + @Test + @DisplayName("UnifiedApprovalStatus.fromValue should return correct enum") + void testUnifiedApprovalStatusFromValue() { + assertEquals(UnifiedApprovalStatus.PENDING, UnifiedApprovalStatus.fromValue("pending")); + assertEquals(UnifiedApprovalStatus.APPROVED, UnifiedApprovalStatus.fromValue("approved")); + assertEquals(UnifiedApprovalStatus.REJECTED, UnifiedApprovalStatus.fromValue("rejected")); + } + + @Test + @DisplayName("UnifiedApprovalStatus.fromValue should throw for unknown value") + void testUnifiedApprovalStatusFromValueThrows() { + assertThrows(IllegalArgumentException.class, () -> UnifiedApprovalStatus.fromValue("unknown")); + } + + @Test + @DisplayName("UnifiedStepStatus builder should create valid object") + void testUnifiedStepStatusBuilder() { + Instant now = Instant.now(); + UnifiedStepStatus step = + UnifiedStepStatus.builder() + .stepId("step-1") + .stepIndex(0) + .stepName("Test Step") + .stepType(UnifiedStepType.LLM_CALL) + .status(StepStatusValue.COMPLETED) + .startedAt(now) + .endedAt(now.plusSeconds(5)) + .duration("5s") + .decision(UnifiedGateDecision.ALLOW) + .decisionReason("Policy passed") + .policiesMatched(Arrays.asList("policy-1", "policy-2")) + .model("gpt-4") + .provider("openai") + .costUsd(0.05) + .resultSummary("Step completed successfully") + .build(); + + assertEquals("step-1", step.getStepId()); + assertEquals(0, step.getStepIndex()); + assertEquals("Test Step", step.getStepName()); + assertEquals(UnifiedStepType.LLM_CALL, step.getStepType()); + assertEquals(StepStatusValue.COMPLETED, step.getStatus()); + assertEquals(now, step.getStartedAt()); + assertEquals("5s", step.getDuration()); + assertEquals(UnifiedGateDecision.ALLOW, step.getDecision()); + assertEquals("Policy passed", step.getDecisionReason()); + assertEquals(2, step.getPoliciesMatched().size()); + assertEquals("gpt-4", step.getModel()); + assertEquals("openai", step.getProvider()); + assertEquals(0.05, step.getCostUsd()); + assertEquals("Step completed successfully", step.getResultSummary()); + } + + @Test + @DisplayName("UnifiedStepStatus equals and hashCode") + void testUnifiedStepStatusEqualsHashCode() { + UnifiedStepStatus step1 = UnifiedStepStatus.builder().stepId("step-1").stepIndex(0).build(); + UnifiedStepStatus step2 = UnifiedStepStatus.builder().stepId("step-1").stepIndex(0).build(); + UnifiedStepStatus step3 = UnifiedStepStatus.builder().stepId("step-2").stepIndex(1).build(); + + assertEquals(step1, step2); + assertEquals(step1.hashCode(), step2.hashCode()); + assertNotEquals(step1, step3); + } + + @Test + @DisplayName("ExecutionStatus builder should create valid object") + void testExecutionStatusBuilder() { + Instant now = Instant.now(); + List steps = + Arrays.asList( + UnifiedStepStatus.builder() .stepId("step-1") .stepIndex(0) - .stepName("Test Step") - .stepType(UnifiedStepType.LLM_CALL) .status(StepStatusValue.COMPLETED) - .startedAt(now) - .endedAt(now.plusSeconds(5)) - .duration("5s") - .decision(UnifiedGateDecision.ALLOW) - .decisionReason("Policy passed") - .policiesMatched(Arrays.asList("policy-1", "policy-2")) - .model("gpt-4") - .provider("openai") .costUsd(0.05) - .resultSummary("Step completed successfully") - .build(); - - assertEquals("step-1", step.getStepId()); - assertEquals(0, step.getStepIndex()); - assertEquals("Test Step", step.getStepName()); - assertEquals(UnifiedStepType.LLM_CALL, step.getStepType()); - assertEquals(StepStatusValue.COMPLETED, step.getStatus()); - assertEquals(now, step.getStartedAt()); - assertEquals("5s", step.getDuration()); - assertEquals(UnifiedGateDecision.ALLOW, step.getDecision()); - assertEquals("Policy passed", step.getDecisionReason()); - assertEquals(2, step.getPoliciesMatched().size()); - assertEquals("gpt-4", step.getModel()); - assertEquals("openai", step.getProvider()); - assertEquals(0.05, step.getCostUsd()); - assertEquals("Step completed successfully", step.getResultSummary()); - } - - @Test - @DisplayName("UnifiedStepStatus equals and hashCode") - void testUnifiedStepStatusEqualsHashCode() { - UnifiedStepStatus step1 = UnifiedStepStatus.builder() - .stepId("step-1") - .stepIndex(0) - .build(); - UnifiedStepStatus step2 = UnifiedStepStatus.builder() + .build(), + UnifiedStepStatus.builder() + .stepId("step-2") + .stepIndex(1) + .status(StepStatusValue.RUNNING) + .costUsd(0.10) + .build()); + + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + ExecutionStatus status = + ExecutionStatus.builder() + .executionId("exec-1") + .executionType(ExecutionType.MAP_PLAN) + .name("Test Execution") + .source("langchain") + .status(ExecutionStatusValue.RUNNING) + .currentStepIndex(1) + .totalSteps(2) + .progressPercent(50.0) + .startedAt(now) + .duration("30s") + .estimatedCostUsd(0.20) + .steps(steps) + .tenantId("tenant-1") + .orgId("org-1") + .userId("user-1") + .clientId("client-1") + .metadata(metadata) + .createdAt(now) + .updatedAt(now) + .build(); + + assertEquals("exec-1", status.getExecutionId()); + assertEquals(ExecutionType.MAP_PLAN, status.getExecutionType()); + assertEquals("Test Execution", status.getName()); + assertEquals("langchain", status.getSource()); + assertEquals(ExecutionStatusValue.RUNNING, status.getStatus()); + assertEquals(1, status.getCurrentStepIndex()); + assertEquals(2, status.getTotalSteps()); + assertEquals(50.0, status.getProgressPercent()); + assertEquals(now, status.getStartedAt()); + assertEquals("30s", status.getDuration()); + assertEquals(0.20, status.getEstimatedCostUsd()); + assertEquals(2, status.getSteps().size()); + assertEquals("tenant-1", status.getTenantId()); + assertEquals("org-1", status.getOrgId()); + assertEquals("user-1", status.getUserId()); + assertEquals("client-1", status.getClientId()); + assertEquals("value", status.getMetadata().get("key")); + } + + @Test + @DisplayName("ExecutionStatus.isTerminal should delegate to status") + void testExecutionStatusIsTerminal() { + ExecutionStatus running = + ExecutionStatus.builder() + .executionId("exec-1") + .status(ExecutionStatusValue.RUNNING) + .build(); + ExecutionStatus completed = + ExecutionStatus.builder() + .executionId("exec-2") + .status(ExecutionStatusValue.COMPLETED) + .build(); + + assertFalse(running.isTerminal()); + assertTrue(completed.isTerminal()); + } + + @Test + @DisplayName("ExecutionStatus.getCurrentStep should return running step") + void testExecutionStatusGetCurrentStep() { + List steps = + Arrays.asList( + UnifiedStepStatus.builder() .stepId("step-1") .stepIndex(0) - .build(); - UnifiedStepStatus step3 = UnifiedStepStatus.builder() + .status(StepStatusValue.COMPLETED) + .build(), + UnifiedStepStatus.builder() .stepId("step-2") .stepIndex(1) - .build(); - - assertEquals(step1, step2); - assertEquals(step1.hashCode(), step2.hashCode()); - assertNotEquals(step1, step3); - } - - @Test - @DisplayName("ExecutionStatus builder should create valid object") - void testExecutionStatusBuilder() { - Instant now = Instant.now(); - List steps = Arrays.asList( - UnifiedStepStatus.builder() - .stepId("step-1") - .stepIndex(0) - .status(StepStatusValue.COMPLETED) - .costUsd(0.05) - .build(), - UnifiedStepStatus.builder() - .stepId("step-2") - .stepIndex(1) - .status(StepStatusValue.RUNNING) - .costUsd(0.10) - .build() - ); - - Map metadata = new HashMap<>(); - metadata.put("key", "value"); - - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .executionType(ExecutionType.MAP_PLAN) - .name("Test Execution") - .source("langchain") - .status(ExecutionStatusValue.RUNNING) - .currentStepIndex(1) - .totalSteps(2) - .progressPercent(50.0) - .startedAt(now) - .duration("30s") - .estimatedCostUsd(0.20) - .steps(steps) - .tenantId("tenant-1") - .orgId("org-1") - .userId("user-1") - .clientId("client-1") - .metadata(metadata) - .createdAt(now) - .updatedAt(now) - .build(); - - assertEquals("exec-1", status.getExecutionId()); - assertEquals(ExecutionType.MAP_PLAN, status.getExecutionType()); - assertEquals("Test Execution", status.getName()); - assertEquals("langchain", status.getSource()); - assertEquals(ExecutionStatusValue.RUNNING, status.getStatus()); - assertEquals(1, status.getCurrentStepIndex()); - assertEquals(2, status.getTotalSteps()); - assertEquals(50.0, status.getProgressPercent()); - assertEquals(now, status.getStartedAt()); - assertEquals("30s", status.getDuration()); - assertEquals(0.20, status.getEstimatedCostUsd()); - assertEquals(2, status.getSteps().size()); - assertEquals("tenant-1", status.getTenantId()); - assertEquals("org-1", status.getOrgId()); - assertEquals("user-1", status.getUserId()); - assertEquals("client-1", status.getClientId()); - assertEquals("value", status.getMetadata().get("key")); - } - - @Test - @DisplayName("ExecutionStatus.isTerminal should delegate to status") - void testExecutionStatusIsTerminal() { - ExecutionStatus running = ExecutionStatus.builder() - .executionId("exec-1") - .status(ExecutionStatusValue.RUNNING) - .build(); - ExecutionStatus completed = ExecutionStatus.builder() - .executionId("exec-2") - .status(ExecutionStatusValue.COMPLETED) - .build(); - - assertFalse(running.isTerminal()); - assertTrue(completed.isTerminal()); - } - - @Test - @DisplayName("ExecutionStatus.getCurrentStep should return running step") - void testExecutionStatusGetCurrentStep() { - List steps = Arrays.asList( - UnifiedStepStatus.builder() - .stepId("step-1") - .stepIndex(0) - .status(StepStatusValue.COMPLETED) - .build(), - UnifiedStepStatus.builder() - .stepId("step-2") - .stepIndex(1) - .status(StepStatusValue.RUNNING) - .build(), - UnifiedStepStatus.builder() - .stepId("step-3") - .stepIndex(2) - .status(StepStatusValue.PENDING) - .build() - ); - - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .steps(steps) - .build(); - - UnifiedStepStatus current = status.getCurrentStep(); - assertNotNull(current); - assertEquals("step-2", current.getStepId()); - } - - @Test - @DisplayName("ExecutionStatus.getCurrentStep should return null when no running step") - void testExecutionStatusGetCurrentStepNull() { - List steps = Arrays.asList( - UnifiedStepStatus.builder() - .stepId("step-1") - .status(StepStatusValue.COMPLETED) - .build() - ); - - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .steps(steps) - .build(); - - assertNull(status.getCurrentStep()); - } - - @Test - @DisplayName("ExecutionStatus.getCurrentStep should return null when steps is null") - void testExecutionStatusGetCurrentStepNullSteps() { - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .steps(null) - .build(); - - assertNull(status.getCurrentStep()); - } - - @Test - @DisplayName("ExecutionStatus.calculateTotalCost should sum step costs") - void testExecutionStatusCalculateTotalCost() { - List steps = Arrays.asList( - UnifiedStepStatus.builder() - .stepId("step-1") - .costUsd(0.05) - .build(), - UnifiedStepStatus.builder() - .stepId("step-2") - .costUsd(0.10) - .build(), - UnifiedStepStatus.builder() - .stepId("step-3") - .costUsd(null) - .build() - ); - - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .steps(steps) - .build(); - - assertEquals(0.15, status.calculateTotalCost(), 0.001); - } - - @Test - @DisplayName("ExecutionStatus.calculateTotalCost should return 0 for null steps") - void testExecutionStatusCalculateTotalCostNullSteps() { - ExecutionStatus status = ExecutionStatus.builder() - .executionId("exec-1") - .steps(null) - .build(); - - assertEquals(0.0, status.calculateTotalCost()); - } - - @Test - @DisplayName("ExecutionStatus.isMapPlan should return true for MAP_PLAN") - void testExecutionStatusIsMapPlan() { - ExecutionStatus map = ExecutionStatus.builder() - .executionId("exec-1") - .executionType(ExecutionType.MAP_PLAN) - .build(); - ExecutionStatus wcp = ExecutionStatus.builder() - .executionId("exec-2") - .executionType(ExecutionType.WCP_WORKFLOW) - .build(); - - assertTrue(map.isMapPlan()); - assertFalse(wcp.isMapPlan()); - } - - @Test - @DisplayName("ExecutionStatus.isWcpWorkflow should return true for WCP_WORKFLOW") - void testExecutionStatusIsWcpWorkflow() { - ExecutionStatus map = ExecutionStatus.builder() - .executionId("exec-1") - .executionType(ExecutionType.MAP_PLAN) - .build(); - ExecutionStatus wcp = ExecutionStatus.builder() - .executionId("exec-2") - .executionType(ExecutionType.WCP_WORKFLOW) - .build(); - - assertFalse(map.isWcpWorkflow()); - assertTrue(wcp.isWcpWorkflow()); - } - - @Test - @DisplayName("ExecutionStatus equals and hashCode") - void testExecutionStatusEqualsHashCode() { - ExecutionStatus status1 = ExecutionStatus.builder() - .executionId("exec-1") - .build(); - ExecutionStatus status2 = ExecutionStatus.builder() - .executionId("exec-1") - .build(); - ExecutionStatus status3 = ExecutionStatus.builder() - .executionId("exec-2") - .build(); - - assertEquals(status1, status2); - assertEquals(status1.hashCode(), status2.hashCode()); - assertNotEquals(status1, status3); - } - - @Test - @DisplayName("UnifiedListExecutionsRequest builder should create valid object") - void testUnifiedListExecutionsRequestBuilder() { - UnifiedListExecutionsRequest request = UnifiedListExecutionsRequest.builder() - .executionType(ExecutionType.MAP_PLAN) - .status(ExecutionStatusValue.RUNNING) - .tenantId("tenant-1") - .orgId("org-1") - .limit(25) - .offset(10) - .build(); - - assertEquals(ExecutionType.MAP_PLAN, request.getExecutionType()); - assertEquals(ExecutionStatusValue.RUNNING, request.getStatus()); - assertEquals("tenant-1", request.getTenantId()); - assertEquals("org-1", request.getOrgId()); - assertEquals(25, request.getLimit()); - assertEquals(10, request.getOffset()); - } - - @Test - @DisplayName("UnifiedListExecutionsRequest builder should have defaults") - void testUnifiedListExecutionsRequestBuilderDefaults() { - UnifiedListExecutionsRequest request = UnifiedListExecutionsRequest.builder().build(); - - assertNull(request.getExecutionType()); - assertNull(request.getStatus()); - assertEquals(50, request.getLimit()); - assertEquals(0, request.getOffset()); - } - - @Test - @DisplayName("UnifiedListExecutionsResponse should store values correctly") - void testUnifiedListExecutionsResponse() { - List executions = Arrays.asList( - ExecutionStatus.builder().executionId("exec-1").build(), - ExecutionStatus.builder().executionId("exec-2").build() - ); - - UnifiedListExecutionsResponse response = new UnifiedListExecutionsResponse( - executions, 100, 50, 0, true - ); - - assertEquals(2, response.getExecutions().size()); - assertEquals(100, response.getTotal()); - assertEquals(50, response.getLimit()); - assertEquals(0, response.getOffset()); - assertTrue(response.isHasMore()); - } + .status(StepStatusValue.RUNNING) + .build(), + UnifiedStepStatus.builder() + .stepId("step-3") + .stepIndex(2) + .status(StepStatusValue.PENDING) + .build()); + + ExecutionStatus status = ExecutionStatus.builder().executionId("exec-1").steps(steps).build(); + + UnifiedStepStatus current = status.getCurrentStep(); + assertNotNull(current); + assertEquals("step-2", current.getStepId()); + } + + @Test + @DisplayName("ExecutionStatus.getCurrentStep should return null when no running step") + void testExecutionStatusGetCurrentStepNull() { + List steps = + Arrays.asList( + UnifiedStepStatus.builder().stepId("step-1").status(StepStatusValue.COMPLETED).build()); + + ExecutionStatus status = ExecutionStatus.builder().executionId("exec-1").steps(steps).build(); + + assertNull(status.getCurrentStep()); + } + + @Test + @DisplayName("ExecutionStatus.getCurrentStep should return null when steps is null") + void testExecutionStatusGetCurrentStepNullSteps() { + ExecutionStatus status = ExecutionStatus.builder().executionId("exec-1").steps(null).build(); + + assertNull(status.getCurrentStep()); + } + + @Test + @DisplayName("ExecutionStatus.calculateTotalCost should sum step costs") + void testExecutionStatusCalculateTotalCost() { + List steps = + Arrays.asList( + UnifiedStepStatus.builder().stepId("step-1").costUsd(0.05).build(), + UnifiedStepStatus.builder().stepId("step-2").costUsd(0.10).build(), + UnifiedStepStatus.builder().stepId("step-3").costUsd(null).build()); + + ExecutionStatus status = ExecutionStatus.builder().executionId("exec-1").steps(steps).build(); + + assertEquals(0.15, status.calculateTotalCost(), 0.001); + } + + @Test + @DisplayName("ExecutionStatus.calculateTotalCost should return 0 for null steps") + void testExecutionStatusCalculateTotalCostNullSteps() { + ExecutionStatus status = ExecutionStatus.builder().executionId("exec-1").steps(null).build(); + + assertEquals(0.0, status.calculateTotalCost()); + } + + @Test + @DisplayName("ExecutionStatus.isMapPlan should return true for MAP_PLAN") + void testExecutionStatusIsMapPlan() { + ExecutionStatus map = + ExecutionStatus.builder() + .executionId("exec-1") + .executionType(ExecutionType.MAP_PLAN) + .build(); + ExecutionStatus wcp = + ExecutionStatus.builder() + .executionId("exec-2") + .executionType(ExecutionType.WCP_WORKFLOW) + .build(); + + assertTrue(map.isMapPlan()); + assertFalse(wcp.isMapPlan()); + } + + @Test + @DisplayName("ExecutionStatus.isWcpWorkflow should return true for WCP_WORKFLOW") + void testExecutionStatusIsWcpWorkflow() { + ExecutionStatus map = + ExecutionStatus.builder() + .executionId("exec-1") + .executionType(ExecutionType.MAP_PLAN) + .build(); + ExecutionStatus wcp = + ExecutionStatus.builder() + .executionId("exec-2") + .executionType(ExecutionType.WCP_WORKFLOW) + .build(); + + assertFalse(map.isWcpWorkflow()); + assertTrue(wcp.isWcpWorkflow()); + } + + @Test + @DisplayName("ExecutionStatus equals and hashCode") + void testExecutionStatusEqualsHashCode() { + ExecutionStatus status1 = ExecutionStatus.builder().executionId("exec-1").build(); + ExecutionStatus status2 = ExecutionStatus.builder().executionId("exec-1").build(); + ExecutionStatus status3 = ExecutionStatus.builder().executionId("exec-2").build(); + + assertEquals(status1, status2); + assertEquals(status1.hashCode(), status2.hashCode()); + assertNotEquals(status1, status3); + } + + @Test + @DisplayName("UnifiedListExecutionsRequest builder should create valid object") + void testUnifiedListExecutionsRequestBuilder() { + UnifiedListExecutionsRequest request = + UnifiedListExecutionsRequest.builder() + .executionType(ExecutionType.MAP_PLAN) + .status(ExecutionStatusValue.RUNNING) + .tenantId("tenant-1") + .orgId("org-1") + .limit(25) + .offset(10) + .build(); + + assertEquals(ExecutionType.MAP_PLAN, request.getExecutionType()); + assertEquals(ExecutionStatusValue.RUNNING, request.getStatus()); + assertEquals("tenant-1", request.getTenantId()); + assertEquals("org-1", request.getOrgId()); + assertEquals(25, request.getLimit()); + assertEquals(10, request.getOffset()); + } + + @Test + @DisplayName("UnifiedListExecutionsRequest builder should have defaults") + void testUnifiedListExecutionsRequestBuilderDefaults() { + UnifiedListExecutionsRequest request = UnifiedListExecutionsRequest.builder().build(); + + assertNull(request.getExecutionType()); + assertNull(request.getStatus()); + assertEquals(50, request.getLimit()); + assertEquals(0, request.getOffset()); + } + + @Test + @DisplayName("UnifiedListExecutionsResponse should store values correctly") + void testUnifiedListExecutionsResponse() { + List executions = + Arrays.asList( + ExecutionStatus.builder().executionId("exec-1").build(), + ExecutionStatus.builder().executionId("exec-2").build()); + + UnifiedListExecutionsResponse response = + new UnifiedListExecutionsResponse(executions, 100, 50, 0, true); + + assertEquals(2, response.getExecutions().size()); + assertEquals(100, response.getTotal()); + assertEquals(50, response.getLimit()); + assertEquals(0, response.getOffset()); + assertTrue(response.isHasMore()); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java index b02e4ce..8be38d5 100644 --- a/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/MediaGovernanceTypesTest.java @@ -15,510 +15,513 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.getaxonflow.sdk.types.policies.PolicyTypes; import com.getaxonflow.sdk.types.policies.PolicyTypes.PolicyCategory; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.*; - @DisplayName("Media Governance Types") class MediaGovernanceTypesTest { - private final ObjectMapper mapper = new ObjectMapper(); - - // ======================================================================== - // MediaGovernanceConfig - // ======================================================================== - - @Nested - @DisplayName("MediaGovernanceConfig") - class MediaGovernanceConfigTests { - - @Test - @DisplayName("should create with default constructor and set all fields") - void shouldCreateWithDefaultConstructor() { - MediaGovernanceConfig config = new MediaGovernanceConfig(); - config.setTenantId("tenant_001"); - config.setEnabled(true); - config.setAllowedAnalyzers(Arrays.asList("nsfw", "biometric", "ocr")); - config.setUpdatedAt("2026-02-18T10:00:00Z"); - config.setUpdatedBy("admin@example.com"); - - assertThat(config.getTenantId()).isEqualTo("tenant_001"); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); - assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); - assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); - } - - @Test - @DisplayName("should handle disabled state") - void shouldHandleDisabledState() { - MediaGovernanceConfig config = new MediaGovernanceConfig(); - config.setEnabled(false); - config.setAllowedAnalyzers(List.of()); - - assertThat(config.isEnabled()).isFalse(); - assertThat(config.getAllowedAnalyzers()).isEmpty(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"tenant_id\": \"tenant_abc\"," + - "\"enabled\": true," + - "\"allowed_analyzers\": [\"nsfw\", \"document\"]," + - "\"updated_at\": \"2026-02-18T12:00:00Z\"," + - "\"updated_by\": \"user@example.com\"" + - "}"; - - MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); - - assertThat(config.getTenantId()).isEqualTo("tenant_abc"); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "document"); - assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T12:00:00Z"); - assertThat(config.getUpdatedBy()).isEqualTo("user@example.com"); - } - - @Test - @DisplayName("should ignore unknown properties during deserialization") - void shouldIgnoreUnknownProperties() throws Exception { - String json = "{\"tenant_id\": \"t1\", \"enabled\": false, \"future_field\": 42}"; - - MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); - - assertThat(config.getTenantId()).isEqualTo("t1"); - assertThat(config.isEnabled()).isFalse(); - } - - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - MediaGovernanceConfig config = new MediaGovernanceConfig(); - config.setTenantId("tenant_xyz"); - config.setEnabled(true); - config.setAllowedAnalyzers(List.of("nsfw")); - - String json = mapper.writeValueAsString(config); - - assertThat(json).contains("\"tenant_id\":\"tenant_xyz\""); - assertThat(json).contains("\"enabled\":true"); - assertThat(json).contains("\"allowed_analyzers\":[\"nsfw\"]"); - } - - @Test - @DisplayName("equals should be reflexive") - void equalsShouldBeReflexive() { - MediaGovernanceConfig config = new MediaGovernanceConfig(); - config.setTenantId("t1"); - config.setEnabled(true); - - assertThat(config).isEqualTo(config); - } - - @Test - @DisplayName("equals should compare all fields") - void equalsShouldCompareAllFields() { - MediaGovernanceConfig config1 = new MediaGovernanceConfig(); - config1.setTenantId("t1"); - config1.setEnabled(true); - config1.setAllowedAnalyzers(List.of("nsfw")); - config1.setUpdatedAt("2026-02-18T10:00:00Z"); - config1.setUpdatedBy("admin"); - - MediaGovernanceConfig config2 = new MediaGovernanceConfig(); - config2.setTenantId("t1"); - config2.setEnabled(true); - config2.setAllowedAnalyzers(List.of("nsfw")); - config2.setUpdatedAt("2026-02-18T10:00:00Z"); - config2.setUpdatedBy("admin"); - - assertThat(config1).isEqualTo(config2); - assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); - } - - @Test - @DisplayName("equals should detect differences") - void equalsShouldDetectDifferences() { - MediaGovernanceConfig config1 = new MediaGovernanceConfig(); - config1.setTenantId("t1"); - config1.setEnabled(true); - - MediaGovernanceConfig config2 = new MediaGovernanceConfig(); - config2.setTenantId("t2"); - config2.setEnabled(true); - - MediaGovernanceConfig config3 = new MediaGovernanceConfig(); - config3.setTenantId("t1"); - config3.setEnabled(false); - - assertThat(config1).isNotEqualTo(config2); - assertThat(config1).isNotEqualTo(config3); - assertThat(config1).isNotEqualTo(null); - assertThat(config1).isNotEqualTo("string"); - } - - @Test - @DisplayName("toString should include all fields") - void toStringShouldIncludeAllFields() { - MediaGovernanceConfig config = new MediaGovernanceConfig(); - config.setTenantId("t1"); - config.setEnabled(true); - config.setAllowedAnalyzers(List.of("nsfw", "biometric")); - config.setUpdatedAt("2026-02-18T10:00:00Z"); - config.setUpdatedBy("admin"); - - String str = config.toString(); - - assertThat(str).contains("t1"); - assertThat(str).contains("true"); - assertThat(str).contains("nsfw"); - assertThat(str).contains("biometric"); - assertThat(str).contains("2026-02-18T10:00:00Z"); - assertThat(str).contains("admin"); - } - } - - // ======================================================================== - // MediaGovernanceStatus - // ======================================================================== - - @Nested - @DisplayName("MediaGovernanceStatus") - class MediaGovernanceStatusTests { - - @Test - @DisplayName("should create with default constructor and set all fields") - void shouldCreateWithDefaultConstructor() { - MediaGovernanceStatus status = new MediaGovernanceStatus(); - status.setAvailable(true); - status.setEnabledByDefault(false); - status.setPerTenantControl(true); - status.setTier("enterprise"); - - assertThat(status.isAvailable()).isTrue(); - assertThat(status.isEnabledByDefault()).isFalse(); - assertThat(status.isPerTenantControl()).isTrue(); - assertThat(status.getTier()).isEqualTo("enterprise"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"available\": true," + - "\"enabled_by_default\": true," + - "\"per_tenant_control\": false," + - "\"tier\": \"professional\"" + - "}"; - - MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); - - assertThat(status.isAvailable()).isTrue(); - assertThat(status.isEnabledByDefault()).isTrue(); - assertThat(status.isPerTenantControl()).isFalse(); - assertThat(status.getTier()).isEqualTo("professional"); - } - - @Test - @DisplayName("should ignore unknown properties during deserialization") - void shouldIgnoreUnknownProperties() throws Exception { - String json = "{\"available\": false, \"tier\": \"free\", \"unknown_field\": true}"; - - MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); - - assertThat(status.isAvailable()).isFalse(); - assertThat(status.getTier()).isEqualTo("free"); - } - - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - MediaGovernanceStatus status = new MediaGovernanceStatus(); - status.setAvailable(true); - status.setEnabledByDefault(true); - status.setPerTenantControl(true); - status.setTier("enterprise"); - - String json = mapper.writeValueAsString(status); - - assertThat(json).contains("\"available\":true"); - assertThat(json).contains("\"enabled_by_default\":true"); - assertThat(json).contains("\"per_tenant_control\":true"); - assertThat(json).contains("\"tier\":\"enterprise\""); - } - - @Test - @DisplayName("equals should be reflexive") - void equalsShouldBeReflexive() { - MediaGovernanceStatus status = new MediaGovernanceStatus(); - status.setAvailable(true); - - assertThat(status).isEqualTo(status); - } - - @Test - @DisplayName("equals should compare all fields") - void equalsShouldCompareAllFields() { - MediaGovernanceStatus status1 = new MediaGovernanceStatus(); - status1.setAvailable(true); - status1.setEnabledByDefault(false); - status1.setPerTenantControl(true); - status1.setTier("enterprise"); - - MediaGovernanceStatus status2 = new MediaGovernanceStatus(); - status2.setAvailable(true); - status2.setEnabledByDefault(false); - status2.setPerTenantControl(true); - status2.setTier("enterprise"); - - assertThat(status1).isEqualTo(status2); - assertThat(status1.hashCode()).isEqualTo(status2.hashCode()); - } - - @Test - @DisplayName("equals should detect differences") - void equalsShouldDetectDifferences() { - MediaGovernanceStatus status1 = new MediaGovernanceStatus(); - status1.setAvailable(true); - status1.setTier("enterprise"); - - MediaGovernanceStatus status2 = new MediaGovernanceStatus(); - status2.setAvailable(false); - status2.setTier("enterprise"); - - MediaGovernanceStatus status3 = new MediaGovernanceStatus(); - status3.setAvailable(true); - status3.setTier("free"); - - assertThat(status1).isNotEqualTo(status2); - assertThat(status1).isNotEqualTo(status3); - assertThat(status1).isNotEqualTo(null); - assertThat(status1).isNotEqualTo("string"); - } - - @Test - @DisplayName("toString should include all fields") - void toStringShouldIncludeAllFields() { - MediaGovernanceStatus status = new MediaGovernanceStatus(); - status.setAvailable(true); - status.setEnabledByDefault(false); - status.setPerTenantControl(true); - status.setTier("enterprise"); - - String str = status.toString(); - - assertThat(str).contains("true"); - assertThat(str).contains("enterprise"); - } - } - - // ======================================================================== - // UpdateMediaGovernanceConfigRequest - // ======================================================================== - - @Nested - @DisplayName("UpdateMediaGovernanceConfigRequest") - class UpdateMediaGovernanceConfigRequestTests { - - @Test - @DisplayName("should create with default constructor and set fields") - void shouldCreateWithDefaultConstructor() { - UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); - request.setEnabled(true); - request.setAllowedAnalyzers(List.of("nsfw", "ocr")); - - assertThat(request.getEnabled()).isTrue(); - assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "ocr"); - } - - @Test - @DisplayName("should build with builder pattern") - void shouldBuildWithBuilder() { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw", "biometric")) - .build(); - - assertThat(request.getEnabled()).isTrue(); - assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); - } - - @Test - @DisplayName("builder should handle null enabled for partial update") - void builderShouldHandleNullEnabled() { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .allowedAnalyzers(List.of("nsfw")) - .build(); - - assertThat(request.getEnabled()).isNull(); - assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw"); - } - - @Test - @DisplayName("builder should handle null allowedAnalyzers for partial update") - void builderShouldHandleNullAnalyzers() { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(false) - .build(); - - assertThat(request.getEnabled()).isFalse(); - assertThat(request.getAllowedAnalyzers()).isNull(); - } - - @Test - @DisplayName("should serialize omitting null fields") - void shouldSerializeOmittingNulls() throws Exception { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .build(); - - String json = mapper.writeValueAsString(request); - - assertThat(json).contains("\"enabled\":true"); - assertThat(json).doesNotContain("allowed_analyzers"); - } - - @Test - @DisplayName("should serialize with all fields") - void shouldSerializeAllFields() throws Exception { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(false) - .allowedAnalyzers(List.of("ocr")) - .build(); - - String json = mapper.writeValueAsString(request); - - assertThat(json).contains("\"enabled\":false"); - assertThat(json).contains("\"allowed_analyzers\":[\"ocr\"]"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"enabled\": true, \"allowed_analyzers\": [\"nsfw\", \"biometric\"]}"; - - UpdateMediaGovernanceConfigRequest request = - mapper.readValue(json, UpdateMediaGovernanceConfigRequest.class); - - assertThat(request.getEnabled()).isTrue(); - assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); - } - - @Test - @DisplayName("equals should be reflexive") - void equalsShouldBeReflexive() { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .build(); - - assertThat(request).isEqualTo(request); - } - - @Test - @DisplayName("equals should compare all fields") - void equalsShouldCompareAllFields() { - UpdateMediaGovernanceConfigRequest r1 = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw")) - .build(); - - UpdateMediaGovernanceConfigRequest r2 = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw")) - .build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } - - @Test - @DisplayName("equals should detect differences") - void equalsShouldDetectDifferences() { - UpdateMediaGovernanceConfigRequest r1 = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .build(); - - UpdateMediaGovernanceConfigRequest r2 = UpdateMediaGovernanceConfigRequest.builder() - .enabled(false) - .build(); - - assertThat(r1).isNotEqualTo(r2); - assertThat(r1).isNotEqualTo(null); - assertThat(r1).isNotEqualTo("string"); - } - - @Test - @DisplayName("toString should include fields") - void toStringShouldIncludeFields() { - UpdateMediaGovernanceConfigRequest request = UpdateMediaGovernanceConfigRequest.builder() - .enabled(true) - .allowedAnalyzers(List.of("nsfw", "biometric")) - .build(); - - String str = request.toString(); - - assertThat(str).contains("true"); - assertThat(str).contains("nsfw"); - assertThat(str).contains("biometric"); - } - } - - // ======================================================================== - // Media Policy Category Constants & Enum Values - // ======================================================================== - - @Nested - @DisplayName("Media Policy Categories") - class MediaPolicyCategoryTests { - - @Test - @DisplayName("CATEGORY_MEDIA_SAFETY constant should match enum value") - void mediaSafetyConstantShouldMatchEnum() { - assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY).isEqualTo("media-safety"); - assertThat(PolicyCategory.MEDIA_SAFETY.getValue()).isEqualTo("media-safety"); - assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY).isEqualTo(PolicyCategory.MEDIA_SAFETY.getValue()); - } - - @Test - @DisplayName("CATEGORY_MEDIA_BIOMETRIC constant should match enum value") - void mediaBiometricConstantShouldMatchEnum() { - assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC).isEqualTo("media-biometric"); - assertThat(PolicyCategory.MEDIA_BIOMETRIC.getValue()).isEqualTo("media-biometric"); - assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC).isEqualTo(PolicyCategory.MEDIA_BIOMETRIC.getValue()); - } - - @Test - @DisplayName("CATEGORY_MEDIA_DOCUMENT constant should match enum value") - void mediaDocumentConstantShouldMatchEnum() { - assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT).isEqualTo("media-document"); - assertThat(PolicyCategory.MEDIA_DOCUMENT.getValue()).isEqualTo("media-document"); - assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT).isEqualTo(PolicyCategory.MEDIA_DOCUMENT.getValue()); - } - - @Test - @DisplayName("CATEGORY_MEDIA_PII constant should match enum value") - void mediaPiiConstantShouldMatchEnum() { - assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo("media-pii"); - assertThat(PolicyCategory.MEDIA_PII.getValue()).isEqualTo("media-pii"); - assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo(PolicyCategory.MEDIA_PII.getValue()); - } - - @Test - @DisplayName("all media categories should exist in PolicyCategory enum") - void allMediaCategoriesShouldExist() { - assertThat(PolicyCategory.valueOf("MEDIA_SAFETY")).isNotNull(); - assertThat(PolicyCategory.valueOf("MEDIA_BIOMETRIC")).isNotNull(); - assertThat(PolicyCategory.valueOf("MEDIA_DOCUMENT")).isNotNull(); - assertThat(PolicyCategory.valueOf("MEDIA_PII")).isNotNull(); - } + private final ObjectMapper mapper = new ObjectMapper(); + + // ======================================================================== + // MediaGovernanceConfig + // ======================================================================== + + @Nested + @DisplayName("MediaGovernanceConfig") + class MediaGovernanceConfigTests { + + @Test + @DisplayName("should create with default constructor and set all fields") + void shouldCreateWithDefaultConstructor() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("tenant_001"); + config.setEnabled(true); + config.setAllowedAnalyzers(Arrays.asList("nsfw", "biometric", "ocr")); + config.setUpdatedAt("2026-02-18T10:00:00Z"); + config.setUpdatedBy("admin@example.com"); + + assertThat(config.getTenantId()).isEqualTo("tenant_001"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "biometric", "ocr"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T10:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("admin@example.com"); + } + + @Test + @DisplayName("should handle disabled state") + void shouldHandleDisabledState() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setEnabled(false); + config.setAllowedAnalyzers(List.of()); + + assertThat(config.isEnabled()).isFalse(); + assertThat(config.getAllowedAnalyzers()).isEmpty(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"tenant_id\": \"tenant_abc\"," + + "\"enabled\": true," + + "\"allowed_analyzers\": [\"nsfw\", \"document\"]," + + "\"updated_at\": \"2026-02-18T12:00:00Z\"," + + "\"updated_by\": \"user@example.com\"" + + "}"; + + MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); + + assertThat(config.getTenantId()).isEqualTo("tenant_abc"); + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getAllowedAnalyzers()).containsExactly("nsfw", "document"); + assertThat(config.getUpdatedAt()).isEqualTo("2026-02-18T12:00:00Z"); + assertThat(config.getUpdatedBy()).isEqualTo("user@example.com"); + } + + @Test + @DisplayName("should ignore unknown properties during deserialization") + void shouldIgnoreUnknownProperties() throws Exception { + String json = "{\"tenant_id\": \"t1\", \"enabled\": false, \"future_field\": 42}"; + + MediaGovernanceConfig config = mapper.readValue(json, MediaGovernanceConfig.class); + + assertThat(config.getTenantId()).isEqualTo("t1"); + assertThat(config.isEnabled()).isFalse(); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("tenant_xyz"); + config.setEnabled(true); + config.setAllowedAnalyzers(List.of("nsfw")); + + String json = mapper.writeValueAsString(config); + + assertThat(json).contains("\"tenant_id\":\"tenant_xyz\""); + assertThat(json).contains("\"enabled\":true"); + assertThat(json).contains("\"allowed_analyzers\":[\"nsfw\"]"); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("t1"); + config.setEnabled(true); + + assertThat(config).isEqualTo(config); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + MediaGovernanceConfig config1 = new MediaGovernanceConfig(); + config1.setTenantId("t1"); + config1.setEnabled(true); + config1.setAllowedAnalyzers(List.of("nsfw")); + config1.setUpdatedAt("2026-02-18T10:00:00Z"); + config1.setUpdatedBy("admin"); + + MediaGovernanceConfig config2 = new MediaGovernanceConfig(); + config2.setTenantId("t1"); + config2.setEnabled(true); + config2.setAllowedAnalyzers(List.of("nsfw")); + config2.setUpdatedAt("2026-02-18T10:00:00Z"); + config2.setUpdatedBy("admin"); + + assertThat(config1).isEqualTo(config2); + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + MediaGovernanceConfig config1 = new MediaGovernanceConfig(); + config1.setTenantId("t1"); + config1.setEnabled(true); + + MediaGovernanceConfig config2 = new MediaGovernanceConfig(); + config2.setTenantId("t2"); + config2.setEnabled(true); + + MediaGovernanceConfig config3 = new MediaGovernanceConfig(); + config3.setTenantId("t1"); + config3.setEnabled(false); + + assertThat(config1).isNotEqualTo(config2); + assertThat(config1).isNotEqualTo(config3); + assertThat(config1).isNotEqualTo(null); + assertThat(config1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include all fields") + void toStringShouldIncludeAllFields() { + MediaGovernanceConfig config = new MediaGovernanceConfig(); + config.setTenantId("t1"); + config.setEnabled(true); + config.setAllowedAnalyzers(List.of("nsfw", "biometric")); + config.setUpdatedAt("2026-02-18T10:00:00Z"); + config.setUpdatedBy("admin"); + + String str = config.toString(); + + assertThat(str).contains("t1"); + assertThat(str).contains("true"); + assertThat(str).contains("nsfw"); + assertThat(str).contains("biometric"); + assertThat(str).contains("2026-02-18T10:00:00Z"); + assertThat(str).contains("admin"); + } + } + + // ======================================================================== + // MediaGovernanceStatus + // ======================================================================== + + @Nested + @DisplayName("MediaGovernanceStatus") + class MediaGovernanceStatusTests { + + @Test + @DisplayName("should create with default constructor and set all fields") + void shouldCreateWithDefaultConstructor() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(false); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isFalse(); + assertThat(status.isPerTenantControl()).isTrue(); + assertThat(status.getTier()).isEqualTo("enterprise"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"available\": true," + + "\"enabled_by_default\": true," + + "\"per_tenant_control\": false," + + "\"tier\": \"professional\"" + + "}"; + + MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); + + assertThat(status.isAvailable()).isTrue(); + assertThat(status.isEnabledByDefault()).isTrue(); + assertThat(status.isPerTenantControl()).isFalse(); + assertThat(status.getTier()).isEqualTo("professional"); + } + + @Test + @DisplayName("should ignore unknown properties during deserialization") + void shouldIgnoreUnknownProperties() throws Exception { + String json = "{\"available\": false, \"tier\": \"free\", \"unknown_field\": true}"; + + MediaGovernanceStatus status = mapper.readValue(json, MediaGovernanceStatus.class); + + assertThat(status.isAvailable()).isFalse(); + assertThat(status.getTier()).isEqualTo("free"); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(true); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + String json = mapper.writeValueAsString(status); + + assertThat(json).contains("\"available\":true"); + assertThat(json).contains("\"enabled_by_default\":true"); + assertThat(json).contains("\"per_tenant_control\":true"); + assertThat(json).contains("\"tier\":\"enterprise\""); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + + assertThat(status).isEqualTo(status); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + MediaGovernanceStatus status1 = new MediaGovernanceStatus(); + status1.setAvailable(true); + status1.setEnabledByDefault(false); + status1.setPerTenantControl(true); + status1.setTier("enterprise"); + + MediaGovernanceStatus status2 = new MediaGovernanceStatus(); + status2.setAvailable(true); + status2.setEnabledByDefault(false); + status2.setPerTenantControl(true); + status2.setTier("enterprise"); + + assertThat(status1).isEqualTo(status2); + assertThat(status1.hashCode()).isEqualTo(status2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + MediaGovernanceStatus status1 = new MediaGovernanceStatus(); + status1.setAvailable(true); + status1.setTier("enterprise"); + + MediaGovernanceStatus status2 = new MediaGovernanceStatus(); + status2.setAvailable(false); + status2.setTier("enterprise"); + + MediaGovernanceStatus status3 = new MediaGovernanceStatus(); + status3.setAvailable(true); + status3.setTier("free"); + + assertThat(status1).isNotEqualTo(status2); + assertThat(status1).isNotEqualTo(status3); + assertThat(status1).isNotEqualTo(null); + assertThat(status1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include all fields") + void toStringShouldIncludeAllFields() { + MediaGovernanceStatus status = new MediaGovernanceStatus(); + status.setAvailable(true); + status.setEnabledByDefault(false); + status.setPerTenantControl(true); + status.setTier("enterprise"); + + String str = status.toString(); + + assertThat(str).contains("true"); + assertThat(str).contains("enterprise"); + } + } + + // ======================================================================== + // UpdateMediaGovernanceConfigRequest + // ======================================================================== + + @Nested + @DisplayName("UpdateMediaGovernanceConfigRequest") + class UpdateMediaGovernanceConfigRequestTests { + + @Test + @DisplayName("should create with default constructor and set fields") + void shouldCreateWithDefaultConstructor() { + UpdateMediaGovernanceConfigRequest request = new UpdateMediaGovernanceConfigRequest(); + request.setEnabled(true); + request.setAllowedAnalyzers(List.of("nsfw", "ocr")); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "ocr"); + } + + @Test + @DisplayName("should build with builder pattern") + void shouldBuildWithBuilder() { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric")) + .build(); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); + } + + @Test + @DisplayName("builder should handle null enabled for partial update") + void builderShouldHandleNullEnabled() { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().allowedAnalyzers(List.of("nsfw")).build(); + + assertThat(request.getEnabled()).isNull(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw"); + } + + @Test + @DisplayName("builder should handle null allowedAnalyzers for partial update") + void builderShouldHandleNullAnalyzers() { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().enabled(false).build(); + + assertThat(request.getEnabled()).isFalse(); + assertThat(request.getAllowedAnalyzers()).isNull(); + } + + @Test + @DisplayName("should serialize omitting null fields") + void shouldSerializeOmittingNulls() throws Exception { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().enabled(true).build(); + + String json = mapper.writeValueAsString(request); + + assertThat(json).contains("\"enabled\":true"); + assertThat(json).doesNotContain("allowed_analyzers"); + } + + @Test + @DisplayName("should serialize with all fields") + void shouldSerializeAllFields() throws Exception { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(false) + .allowedAnalyzers(List.of("ocr")) + .build(); + + String json = mapper.writeValueAsString(request); + + assertThat(json).contains("\"enabled\":false"); + assertThat(json).contains("\"allowed_analyzers\":[\"ocr\"]"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"enabled\": true, \"allowed_analyzers\": [\"nsfw\", \"biometric\"]}"; + + UpdateMediaGovernanceConfigRequest request = + mapper.readValue(json, UpdateMediaGovernanceConfigRequest.class); + + assertThat(request.getEnabled()).isTrue(); + assertThat(request.getAllowedAnalyzers()).containsExactly("nsfw", "biometric"); + } + + @Test + @DisplayName("equals should be reflexive") + void equalsShouldBeReflexive() { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder().enabled(true).build(); + + assertThat(request).isEqualTo(request); + } + + @Test + @DisplayName("equals should compare all fields") + void equalsShouldCompareAllFields() { + UpdateMediaGovernanceConfigRequest r1 = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + UpdateMediaGovernanceConfigRequest r2 = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw")) + .build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } + + @Test + @DisplayName("equals should detect differences") + void equalsShouldDetectDifferences() { + UpdateMediaGovernanceConfigRequest r1 = + UpdateMediaGovernanceConfigRequest.builder().enabled(true).build(); + + UpdateMediaGovernanceConfigRequest r2 = + UpdateMediaGovernanceConfigRequest.builder().enabled(false).build(); + + assertThat(r1).isNotEqualTo(r2); + assertThat(r1).isNotEqualTo(null); + assertThat(r1).isNotEqualTo("string"); + } + + @Test + @DisplayName("toString should include fields") + void toStringShouldIncludeFields() { + UpdateMediaGovernanceConfigRequest request = + UpdateMediaGovernanceConfigRequest.builder() + .enabled(true) + .allowedAnalyzers(List.of("nsfw", "biometric")) + .build(); + + String str = request.toString(); + + assertThat(str).contains("true"); + assertThat(str).contains("nsfw"); + assertThat(str).contains("biometric"); + } + } + + // ======================================================================== + // Media Policy Category Constants & Enum Values + // ======================================================================== + + @Nested + @DisplayName("Media Policy Categories") + class MediaPolicyCategoryTests { + + @Test + @DisplayName("CATEGORY_MEDIA_SAFETY constant should match enum value") + void mediaSafetyConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY).isEqualTo("media-safety"); + assertThat(PolicyCategory.MEDIA_SAFETY.getValue()).isEqualTo("media-safety"); + assertThat(PolicyTypes.CATEGORY_MEDIA_SAFETY) + .isEqualTo(PolicyCategory.MEDIA_SAFETY.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_BIOMETRIC constant should match enum value") + void mediaBiometricConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC).isEqualTo("media-biometric"); + assertThat(PolicyCategory.MEDIA_BIOMETRIC.getValue()).isEqualTo("media-biometric"); + assertThat(PolicyTypes.CATEGORY_MEDIA_BIOMETRIC) + .isEqualTo(PolicyCategory.MEDIA_BIOMETRIC.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_DOCUMENT constant should match enum value") + void mediaDocumentConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT).isEqualTo("media-document"); + assertThat(PolicyCategory.MEDIA_DOCUMENT.getValue()).isEqualTo("media-document"); + assertThat(PolicyTypes.CATEGORY_MEDIA_DOCUMENT) + .isEqualTo(PolicyCategory.MEDIA_DOCUMENT.getValue()); + } + + @Test + @DisplayName("CATEGORY_MEDIA_PII constant should match enum value") + void mediaPiiConstantShouldMatchEnum() { + assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo("media-pii"); + assertThat(PolicyCategory.MEDIA_PII.getValue()).isEqualTo("media-pii"); + assertThat(PolicyTypes.CATEGORY_MEDIA_PII).isEqualTo(PolicyCategory.MEDIA_PII.getValue()); + } + + @Test + @DisplayName("all media categories should exist in PolicyCategory enum") + void allMediaCategoriesShouldExist() { + assertThat(PolicyCategory.valueOf("MEDIA_SAFETY")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_BIOMETRIC")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_DOCUMENT")).isNotNull(); + assertThat(PolicyCategory.valueOf("MEDIA_PII")).isNotNull(); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java index bc17512..1a6af0f 100644 --- a/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/MoreTypesTest.java @@ -15,1871 +15,1908 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.DisplayName; -import org.junit.jupiter.api.Nested; -import org.junit.jupiter.api.Test; - import java.time.Duration; import java.time.Instant; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Comprehensive tests for SDK types. - */ +/** Comprehensive tests for SDK types. */ @DisplayName("SDK Types") class MoreTypesTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - } - - @Nested - @DisplayName("PortalLoginResponse") - class PortalLoginResponseTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - PortalLoginResponse response = new PortalLoginResponse( - "session-123", - "org-456", - "user@example.com", - "Test User", - "2026-01-04T12:00:00Z" - ); - - assertThat(response.getSessionId()).isEqualTo("session-123"); - assertThat(response.getOrgId()).isEqualTo("org-456"); - assertThat(response.getEmail()).isEqualTo("user@example.com"); - assertThat(response.getName()).isEqualTo("Test User"); - assertThat(response.getExpiresAt()).isEqualTo("2026-01-04T12:00:00Z"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"session_id\":\"sess-abc\"," + - "\"org_id\":\"org-xyz\"," + - "\"email\":\"test@test.com\"," + - "\"name\":\"Test\"," + - "\"expires_at\":\"2026-01-05T00:00:00Z\"" + - "}"; - - PortalLoginResponse response = objectMapper.readValue(json, PortalLoginResponse.class); - - assertThat(response.getSessionId()).isEqualTo("sess-abc"); - assertThat(response.getOrgId()).isEqualTo("org-xyz"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PortalLoginResponse r1 = new PortalLoginResponse("s1", "o1", "e1", "n1", "ex1"); - PortalLoginResponse r2 = new PortalLoginResponse("s1", "o1", "e1", "n1", "ex1"); - PortalLoginResponse r3 = new PortalLoginResponse("s2", "o1", "e1", "n1", "ex1"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PortalLoginResponse response = new PortalLoginResponse("s", "o", "e", "n", "ex"); - assertThat(response.toString()).contains("PortalLoginResponse"); - } - } - - @Nested - @DisplayName("CodeArtifact") - class CodeArtifactTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - List policies = Arrays.asList("policy1", "policy2"); - CodeArtifact artifact = new CodeArtifact( - true, - "python", - "function", - 1024, - 50, - 0, - 1, - policies - ); - - assertThat(artifact.isCodeOutput()).isTrue(); - assertThat(artifact.getLanguage()).isEqualTo("python"); - assertThat(artifact.getCodeType()).isEqualTo("function"); - assertThat(artifact.getSizeBytes()).isEqualTo(1024); - assertThat(artifact.getLineCount()).isEqualTo(50); - assertThat(artifact.getSecretsDetected()).isEqualTo(0); - assertThat(artifact.getUnsafePatterns()).isEqualTo(1); - assertThat(artifact.getPoliciesChecked()).containsExactly("policy1", "policy2"); - } - - @Test - @DisplayName("should handle null values with defaults") - void shouldHandleNullValues() { - CodeArtifact artifact = new CodeArtifact( - false, null, null, 0, 0, 0, 0, null - ); - - assertThat(artifact.getLanguage()).isEmpty(); - assertThat(artifact.getCodeType()).isEmpty(); - assertThat(artifact.getPoliciesChecked()).isEmpty(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"is_code_output\":true," + - "\"language\":\"javascript\"," + - "\"code_type\":\"class\"," + - "\"size_bytes\":2048," + - "\"line_count\":100," + - "\"secrets_detected\":2," + - "\"unsafe_patterns\":3," + - "\"policies_checked\":[\"p1\",\"p2\"]" + - "}"; - - CodeArtifact artifact = objectMapper.readValue(json, CodeArtifact.class); - - assertThat(artifact.isCodeOutput()).isTrue(); - assertThat(artifact.getLanguage()).isEqualTo("javascript"); - assertThat(artifact.getSecretsDetected()).isEqualTo(2); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - CodeArtifact a1 = new CodeArtifact(true, "py", "fn", 100, 10, 0, 0, null); - CodeArtifact a2 = new CodeArtifact(true, "py", "fn", 100, 10, 0, 0, null); - CodeArtifact a3 = new CodeArtifact(false, "py", "fn", 100, 10, 0, 0, null); - - assertThat(a1).isEqualTo(a2); - assertThat(a1.hashCode()).isEqualTo(a2.hashCode()); - assertThat(a1).isNotEqualTo(a3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - CodeArtifact artifact = new CodeArtifact(true, "go", "script", 512, 25, 0, 0, null); - String str = artifact.toString(); - assertThat(str).contains("CodeArtifact"); - assertThat(str).contains("go"); - } - } - - @Nested - @DisplayName("ConnectorHealthStatus") - class ConnectorHealthStatusTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - Map details = new HashMap<>(); - details.put("db", "connected"); - - ConnectorHealthStatus status = new ConnectorHealthStatus( - true, 150L, details, "2026-01-04T10:00:00Z", null - ); - - assertThat(status.isHealthy()).isTrue(); - assertThat(status.getLatency()).isEqualTo(150L); - assertThat(status.getDetails()).containsEntry("db", "connected"); - assertThat(status.getTimestamp()).isEqualTo("2026-01-04T10:00:00Z"); - assertThat(status.getError()).isNull(); - } - - @Test - @DisplayName("should handle null values with defaults") - void shouldHandleNullValues() { - ConnectorHealthStatus status = new ConnectorHealthStatus( - null, null, null, null, "Connection failed" - ); - - assertThat(status.isHealthy()).isFalse(); - assertThat(status.getLatency()).isEqualTo(0L); - assertThat(status.getDetails()).isEmpty(); - assertThat(status.getTimestamp()).isEmpty(); - assertThat(status.getError()).isEqualTo("Connection failed"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"healthy\":false," + - "\"latency\":500," + - "\"timestamp\":\"2026-01-04T12:00:00Z\"," + - "\"error\":\"Timeout\"" + - "}"; - - ConnectorHealthStatus status = objectMapper.readValue(json, ConnectorHealthStatus.class); - - assertThat(status.isHealthy()).isFalse(); - assertThat(status.getLatency()).isEqualTo(500L); - assertThat(status.getError()).isEqualTo("Timeout"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ConnectorHealthStatus s1 = new ConnectorHealthStatus(true, 100L, null, "ts1", null); - ConnectorHealthStatus s2 = new ConnectorHealthStatus(true, 100L, null, "ts1", null); - ConnectorHealthStatus s3 = new ConnectorHealthStatus(false, 100L, null, "ts1", null); - - assertThat(s1).isEqualTo(s2); - assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); - assertThat(s1).isNotEqualTo(s3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ConnectorHealthStatus status = new ConnectorHealthStatus(true, 50L, null, "ts", null); - assertThat(status.toString()).contains("ConnectorHealthStatus"); - } - } - - @Nested - @DisplayName("ConnectorInfo") - class ConnectorInfoTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - List caps = Arrays.asList("read", "write"); - Map schema = new HashMap<>(); - schema.put("host", "string"); - - ConnectorInfo info = new ConnectorInfo( - "conn-1", "PostgreSQL", "Database connector", "database", - "1.0.0", caps, schema, true, true - ); - - assertThat(info.getId()).isEqualTo("conn-1"); - assertThat(info.getName()).isEqualTo("PostgreSQL"); - assertThat(info.getDescription()).isEqualTo("Database connector"); - assertThat(info.getType()).isEqualTo("database"); - assertThat(info.getVersion()).isEqualTo("1.0.0"); - assertThat(info.getCapabilities()).containsExactly("read", "write"); - assertThat(info.getConfigSchema()).containsEntry("host", "string"); - assertThat(info.isInstalled()).isTrue(); - assertThat(info.isEnabled()).isTrue(); - } - - @Test - @DisplayName("should handle null collections") - void shouldHandleNullCollections() { - ConnectorInfo info = new ConnectorInfo( - "id", "name", "desc", "type", "v1", - null, null, false, false - ); - - assertThat(info.getCapabilities()).isEmpty(); - assertThat(info.getConfigSchema()).isEmpty(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"id\":\"mysql-connector\"," + - "\"name\":\"MySQL\"," + - "\"type\":\"database\"," + - "\"version\":\"2.0.0\"," + - "\"installed\":true," + - "\"enabled\":false" + - "}"; - - ConnectorInfo info = objectMapper.readValue(json, ConnectorInfo.class); - - assertThat(info.getId()).isEqualTo("mysql-connector"); - assertThat(info.isInstalled()).isTrue(); - assertThat(info.isEnabled()).isFalse(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ConnectorInfo i1 = new ConnectorInfo("id1", "n", "d", "t", "v", null, null, true, true); - ConnectorInfo i2 = new ConnectorInfo("id1", "n", "d", "t", "v", null, null, true, true); - ConnectorInfo i3 = new ConnectorInfo("id2", "n", "d", "t", "v", null, null, true, true); - - assertThat(i1).isEqualTo(i2); - assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); - assertThat(i1).isNotEqualTo(i3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ConnectorInfo info = new ConnectorInfo("id", "MySQL", "d", "db", "1.0", null, null, true, true); - assertThat(info.toString()).contains("ConnectorInfo").contains("MySQL"); - } - } - - @Nested - @DisplayName("AuditOptions") - class AuditOptionsTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx-123") - .clientId("client-456") - .build(); - - assertThat(options.getContextId()).isEqualTo("ctx-123"); - assertThat(options.getClientId()).isEqualTo("client-456"); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - TokenUsage usage = TokenUsage.of(100, 200); - Map metadata = new HashMap<>(); - metadata.put("key", "value"); - - AuditOptions options = AuditOptions.builder() - .contextId("ctx-123") - .clientId("client-456") - .responseSummary("Summary of response") - .provider("openai") - .model("gpt-4") - .tokenUsage(usage) - .latencyMs(1234) - .metadata(metadata) - .success(true) - .errorMessage(null) - .build(); - - assertThat(options.getContextId()).isEqualTo("ctx-123"); - assertThat(options.getResponseSummary()).isEqualTo("Summary of response"); - assertThat(options.getProvider()).isEqualTo("openai"); - assertThat(options.getModel()).isEqualTo("gpt-4"); - assertThat(options.getTokenUsage()).isEqualTo(usage); - assertThat(options.getLatencyMs()).isEqualTo(1234L); - assertThat(options.getMetadata()).containsEntry("key", "value"); - assertThat(options.getSuccess()).isTrue(); - } - - @Test - @DisplayName("should add metadata incrementally") - void shouldAddMetadataIncrementally() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx") - .clientId("client") - .addMetadata("k1", "v1") - .addMetadata("k2", "v2") - .build(); - - assertThat(options.getMetadata()).hasSize(2); - assertThat(options.getMetadata()).containsEntry("k1", "v1"); - assertThat(options.getMetadata()).containsEntry("k2", "v2"); - } - - @Test - @DisplayName("should fail when contextId is null") - void shouldFailWhenContextIdIsNull() { - assertThatThrownBy(() -> AuditOptions.builder() - .clientId("client") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should allow clientId to be null for smart defaults") - void shouldAllowClientIdToBeNull() { - // clientId can be null - SDK will use smart default "community" - AuditOptions options = AuditOptions.builder() - .contextId("ctx") - .build(); - assertThat(options.getContextId()).isEqualTo("ctx"); - assertThat(options.getClientId()).isNull(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - AuditOptions o1 = AuditOptions.builder().contextId("c1").clientId("cl1").build(); - AuditOptions o2 = AuditOptions.builder().contextId("c1").clientId("cl1").build(); - AuditOptions o3 = AuditOptions.builder().contextId("c2").clientId("cl1").build(); - - assertThat(o1).isEqualTo(o2); - assertThat(o1.hashCode()).isEqualTo(o2.hashCode()); - assertThat(o1).isNotEqualTo(o3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - AuditOptions options = AuditOptions.builder() - .contextId("ctx") - .clientId("client") - .provider("anthropic") - .build(); - assertThat(options.toString()).contains("AuditOptions").contains("anthropic"); - } - } - - @Nested - @DisplayName("AuditResult") - class AuditResultTests { - - @Test - @DisplayName("should create successful result") - void shouldCreateSuccessfulResult() { - AuditResult result = new AuditResult(true, "audit-123", "Recorded", null); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getAuditId()).isEqualTo("audit-123"); - assertThat(result.getMessage()).isEqualTo("Recorded"); - assertThat(result.getError()).isNull(); - } - - @Test - @DisplayName("should create failed result") - void shouldCreateFailedResult() { - AuditResult result = new AuditResult(false, null, null, "Context expired"); - - assertThat(result.isSuccess()).isFalse(); - assertThat(result.getError()).isEqualTo("Context expired"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"success\":true," + - "\"audit_id\":\"aud-456\"," + - "\"message\":\"OK\"" + - "}"; - - AuditResult result = objectMapper.readValue(json, AuditResult.class); - - assertThat(result.isSuccess()).isTrue(); - assertThat(result.getAuditId()).isEqualTo("aud-456"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - AuditResult r1 = new AuditResult(true, "a1", "m1", null); - AuditResult r2 = new AuditResult(true, "a1", "m1", null); - AuditResult r3 = new AuditResult(false, "a1", "m1", null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - AuditResult result = new AuditResult(true, "aud", "msg", null); - assertThat(result.toString()).contains("AuditResult"); - } - } - - @Nested - @DisplayName("PlanStep") - class PlanStepTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - List deps = Arrays.asList("step-1", "step-2"); - Map params = new HashMap<>(); - params.put("query", "SELECT * FROM users"); - - PlanStep step = new PlanStep( - "step-3", "Query Database", "connector-call", "Fetch user data", - deps, "db-agent", params, "2s" - ); - - assertThat(step.getId()).isEqualTo("step-3"); - assertThat(step.getName()).isEqualTo("Query Database"); - assertThat(step.getType()).isEqualTo("connector-call"); - assertThat(step.getDescription()).isEqualTo("Fetch user data"); - assertThat(step.getDependsOn()).containsExactly("step-1", "step-2"); - assertThat(step.getAgent()).isEqualTo("db-agent"); - assertThat(step.getParameters()).containsEntry("query", "SELECT * FROM users"); - assertThat(step.getEstimatedTime()).isEqualTo("2s"); - } - - @Test - @DisplayName("should handle null collections") - void shouldHandleNullCollections() { - PlanStep step = new PlanStep("id", "name", "type", "desc", null, "agent", null, "1s"); - - assertThat(step.getDependsOn()).isEmpty(); - assertThat(step.getParameters()).isEmpty(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"id\":\"s1\"," + - "\"name\":\"Step 1\"," + - "\"type\":\"llm-call\"," + - "\"description\":\"Call LLM\"," + - "\"depends_on\":[\"s0\"]," + - "\"agent\":\"llm-agent\"," + - "\"estimated_time\":\"500ms\"" + - "}"; - - PlanStep step = objectMapper.readValue(json, PlanStep.class); - - assertThat(step.getId()).isEqualTo("s1"); - assertThat(step.getType()).isEqualTo("llm-call"); - assertThat(step.getDependsOn()).containsExactly("s0"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PlanStep s1 = new PlanStep("id1", "n", "t", "d", null, "a", null, "1s"); - PlanStep s2 = new PlanStep("id1", "n", "t", "d", null, "a", null, "1s"); - PlanStep s3 = new PlanStep("id2", "n", "t", "d", null, "a", null, "1s"); - - assertThat(s1).isEqualTo(s2); - assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); - assertThat(s1).isNotEqualTo(s3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PlanStep step = new PlanStep("id", "LLM Call", "llm-call", "desc", null, "agent", null, "1s"); - assertThat(step.toString()).contains("PlanStep").contains("LLM Call"); - } - } - - @Nested - @DisplayName("PolicyApprovalRequest") - class PolicyApprovalRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user-123") - .query("What is the weather?") - .build(); - - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getQuery()).isEqualTo("What is the weather?"); - assertThat(request.getDataSources()).isEmpty(); - assertThat(request.getContext()).isEmpty(); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - List sources = Arrays.asList("db1", "db2"); - Map context = new HashMap<>(); - context.put("env", "production"); - - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user-456") - .query("Query data") - .dataSources(sources) - .context(context) - .clientId("client-789") - .build(); - - assertThat(request.getUserToken()).isEqualTo("user-456"); - assertThat(request.getDataSources()).containsExactly("db1", "db2"); - assertThat(request.getContext()).containsEntry("env", "production"); - assertThat(request.getClientId()).isEqualTo("client-789"); - } - - @Test - @DisplayName("should add context incrementally") - void shouldAddContextIncrementally() { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user") - .query("query") - .addContext("k1", "v1") - .addContext("k2", "v2") - .build(); - - assertThat(request.getContext()).hasSize(2); - } - - @Test - @DisplayName("should fail when userToken is null") - void shouldFailWhenUserTokenIsNull() { - assertThatThrownBy(() -> PolicyApprovalRequest.builder() - .query("query") - .build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PolicyApprovalRequest r1 = PolicyApprovalRequest.builder().userToken("u1").query("q1").build(); - PolicyApprovalRequest r2 = PolicyApprovalRequest.builder().userToken("u1").query("q1").build(); - PolicyApprovalRequest r3 = PolicyApprovalRequest.builder().userToken("u2").query("q1").build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user") - .query("test query") - .build(); - assertThat(request.toString()).contains("PolicyApprovalRequest"); - } - } - - @Nested - @DisplayName("PolicyInfo") - class PolicyInfoTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - List policies = Arrays.asList("policy1", "policy2"); - List checks = Arrays.asList("pii", "sqli"); - CodeArtifact artifact = new CodeArtifact(true, "python", "function", 100, 10, 0, 0, null); - - PolicyInfo info = new PolicyInfo(policies, checks, "17.48ms", "tenant-1", 0.15, artifact); - - assertThat(info.getPoliciesEvaluated()).containsExactly("policy1", "policy2"); - assertThat(info.getStaticChecks()).containsExactly("pii", "sqli"); - assertThat(info.getProcessingTime()).isEqualTo("17.48ms"); - assertThat(info.getTenantId()).isEqualTo("tenant-1"); - assertThat(info.getRiskScore()).isEqualTo(0.15); - assertThat(info.getCodeArtifact()).isNotNull(); - } - - @Test - @DisplayName("should handle null collections") - void shouldHandleNullCollections() { - PolicyInfo info = new PolicyInfo(null, null, null, null, null, null); - - assertThat(info.getPoliciesEvaluated()).isEmpty(); - assertThat(info.getStaticChecks()).isEmpty(); - } - - @Test - @DisplayName("should parse processing time as Duration - milliseconds") - void shouldParseProcessingTimeMs() { - PolicyInfo info = new PolicyInfo(null, null, "17.48ms", null, null, null); - Duration duration = info.getProcessingDuration(); - - assertThat(duration.toMillis()).isEqualTo(17); - } - - @Test - @DisplayName("should parse processing time as Duration - seconds") - void shouldParseProcessingTimeSeconds() { - PolicyInfo info = new PolicyInfo(null, null, "1.5s", null, null, null); - Duration duration = info.getProcessingDuration(); - - assertThat(duration.toMillis()).isEqualTo(1500); - } - - @Test - @DisplayName("should handle plain numeric value as milliseconds") - void shouldHandlePlainNumericValue() { - PolicyInfo info = new PolicyInfo(null, null, "100", null, null, null); - Duration duration = info.getProcessingDuration(); - - assertThat(duration.toMillis()).isEqualTo(100); - } - - @Test - @DisplayName("should return zero duration for null or empty") - void shouldReturnZeroForNullOrEmpty() { - PolicyInfo infoNull = new PolicyInfo(null, null, null, null, null, null); - PolicyInfo infoEmpty = new PolicyInfo(null, null, "", null, null, null); - - assertThat(infoNull.getProcessingDuration()).isEqualTo(Duration.ZERO); - assertThat(infoEmpty.getProcessingDuration()).isEqualTo(Duration.ZERO); - } - - @Test - @DisplayName("should return zero duration for invalid format") - void shouldReturnZeroForInvalidFormat() { - PolicyInfo info = new PolicyInfo(null, null, "invalid", null, null, null); - assertThat(info.getProcessingDuration()).isEqualTo(Duration.ZERO); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"policies_evaluated\":[\"p1\"]," + - "\"static_checks\":[\"c1\"]," + - "\"processing_time\":\"10ms\"," + - "\"tenant_id\":\"t1\"," + - "\"risk_score\":0.5" + - "}"; - - PolicyInfo info = objectMapper.readValue(json, PolicyInfo.class); - - assertThat(info.getPoliciesEvaluated()).containsExactly("p1"); - assertThat(info.getRiskScore()).isEqualTo(0.5); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PolicyInfo i1 = new PolicyInfo(Arrays.asList("p1"), null, "10ms", "t1", null, null); - PolicyInfo i2 = new PolicyInfo(Arrays.asList("p1"), null, "10ms", "t1", null, null); - PolicyInfo i3 = new PolicyInfo(Arrays.asList("p2"), null, "10ms", "t1", null, null); - - assertThat(i1).isEqualTo(i2); - assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); - assertThat(i1).isNotEqualTo(i3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PolicyInfo info = new PolicyInfo(Arrays.asList("pol1"), null, "5ms", "tenant", null, null); - assertThat(info.toString()).contains("PolicyInfo").contains("pol1"); - } - } - - @Nested - @DisplayName("TokenUsage") - class TokenUsageTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - TokenUsage usage = new TokenUsage(100, 200, 300); - - assertThat(usage.getPromptTokens()).isEqualTo(100); - assertThat(usage.getCompletionTokens()).isEqualTo(200); - assertThat(usage.getTotalTokens()).isEqualTo(300); - } - - @Test - @DisplayName("should create using factory method with auto-calculated total") - void shouldCreateUsingFactoryMethod() { - TokenUsage usage = TokenUsage.of(150, 250); - - assertThat(usage.getPromptTokens()).isEqualTo(150); - assertThat(usage.getCompletionTokens()).isEqualTo(250); - assertThat(usage.getTotalTokens()).isEqualTo(400); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"prompt_tokens\":50," + - "\"completion_tokens\":75," + - "\"total_tokens\":125" + - "}"; - - TokenUsage usage = objectMapper.readValue(json, TokenUsage.class); - - assertThat(usage.getPromptTokens()).isEqualTo(50); - assertThat(usage.getCompletionTokens()).isEqualTo(75); - assertThat(usage.getTotalTokens()).isEqualTo(125); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - TokenUsage u1 = TokenUsage.of(100, 200); - TokenUsage u2 = TokenUsage.of(100, 200); - TokenUsage u3 = TokenUsage.of(100, 300); - - assertThat(u1).isEqualTo(u2); - assertThat(u1.hashCode()).isEqualTo(u2.hashCode()); - assertThat(u1).isNotEqualTo(u3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - TokenUsage usage = TokenUsage.of(10, 20); - assertThat(usage.toString()).contains("TokenUsage").contains("10").contains("20"); - } - } - - @Nested - @DisplayName("RateLimitInfo") - class RateLimitInfoTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - Instant resetAt = Instant.parse("2026-01-04T12:00:00Z"); - RateLimitInfo info = new RateLimitInfo(1000, 500, resetAt); - - assertThat(info.getLimit()).isEqualTo(1000); - assertThat(info.getRemaining()).isEqualTo(500); - assertThat(info.getResetAt()).isEqualTo(resetAt); - } - - @Test - @DisplayName("should detect exceeded rate limit") - void shouldDetectExceededRateLimit() { - RateLimitInfo exceeded = new RateLimitInfo(100, 0, null); - RateLimitInfo notExceeded = new RateLimitInfo(100, 50, null); - - assertThat(exceeded.isExceeded()).isTrue(); - assertThat(notExceeded.isExceeded()).isFalse(); - } - - @Test - @DisplayName("should detect exceeded with negative remaining") - void shouldDetectExceededWithNegative() { - RateLimitInfo info = new RateLimitInfo(100, -5, null); - assertThat(info.isExceeded()).isTrue(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - Instant reset = Instant.now(); - RateLimitInfo r1 = new RateLimitInfo(100, 50, reset); - RateLimitInfo r2 = new RateLimitInfo(100, 50, reset); - RateLimitInfo r3 = new RateLimitInfo(100, 25, reset); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - RateLimitInfo info = new RateLimitInfo(100, 75, null); - assertThat(info.toString()).contains("RateLimitInfo").contains("100").contains("75"); - } - } - - @Nested - @DisplayName("Mode") - class ModeTests { - - @Test - @DisplayName("should have correct values") - void shouldHaveCorrectValues() { - assertThat(Mode.PRODUCTION.getValue()).isEqualTo("production"); - assertThat(Mode.SANDBOX.getValue()).isEqualTo("sandbox"); - } - - @Test - @DisplayName("should parse from value") - void shouldParseFromValue() { - assertThat(Mode.fromValue("production")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue("sandbox")).isEqualTo(Mode.SANDBOX); - assertThat(Mode.fromValue("PRODUCTION")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue("SANDBOX")).isEqualTo(Mode.SANDBOX); - } - - @Test - @DisplayName("should return PRODUCTION for unknown values") - void shouldReturnProductionForUnknown() { - assertThat(Mode.fromValue("unknown")).isEqualTo(Mode.PRODUCTION); - assertThat(Mode.fromValue("")).isEqualTo(Mode.PRODUCTION); - } - - @Test - @DisplayName("should return PRODUCTION for null") - void shouldReturnProductionForNull() { - assertThat(Mode.fromValue(null)).isEqualTo(Mode.PRODUCTION); - } - } - - @Nested - @DisplayName("RequestType") - class RequestTypeTests { - - @Test - @DisplayName("should have correct values") - void shouldHaveCorrectValues() { - assertThat(RequestType.CHAT.getValue()).isEqualTo("chat"); - assertThat(RequestType.SQL.getValue()).isEqualTo("sql"); - assertThat(RequestType.MCP_QUERY.getValue()).isEqualTo("mcp-query"); - assertThat(RequestType.MULTI_AGENT_PLAN.getValue()).isEqualTo("multi-agent-plan"); - } - - @Test - @DisplayName("should parse from value") - void shouldParseFromValue() { - assertThat(RequestType.fromValue("chat")).isEqualTo(RequestType.CHAT); - assertThat(RequestType.fromValue("sql")).isEqualTo(RequestType.SQL); - assertThat(RequestType.fromValue("mcp-query")).isEqualTo(RequestType.MCP_QUERY); - assertThat(RequestType.fromValue("multi-agent-plan")).isEqualTo(RequestType.MULTI_AGENT_PLAN); - } - - @Test - @DisplayName("should parse case insensitively") - void shouldParseCaseInsensitively() { - assertThat(RequestType.fromValue("CHAT")).isEqualTo(RequestType.CHAT); - assertThat(RequestType.fromValue("Chat")).isEqualTo(RequestType.CHAT); - } - - @Test - @DisplayName("should throw for unknown value") - void shouldThrowForUnknownValue() { - assertThatThrownBy(() -> RequestType.fromValue("unknown")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown request type"); - } - - @Test - @DisplayName("should throw for null value") - void shouldThrowForNullValue() { - assertThatThrownBy(() -> RequestType.fromValue(null)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("cannot be null"); - } - } - - @Nested - @DisplayName("HealthStatus") - class HealthStatusTests { - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - Map components = new HashMap<>(); - components.put("database", "healthy"); - components.put("cache", "healthy"); - - HealthStatus status = new HealthStatus("healthy", "2.6.0", "24h5m", components, null, null); - - assertThat(status.getStatus()).isEqualTo("healthy"); - assertThat(status.getVersion()).isEqualTo("2.6.0"); - assertThat(status.getUptime()).isEqualTo("24h5m"); - assertThat(status.getComponents()).containsEntry("database", "healthy"); - } - - @Test - @DisplayName("should handle null components") - void shouldHandleNullComponents() { - HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); - assertThat(status.getComponents()).isEmpty(); - } - - @Test - @DisplayName("should detect healthy status") - void shouldDetectHealthyStatus() { - HealthStatus healthy = new HealthStatus("healthy", "1.0", "1h", null, null, null); - HealthStatus ok = new HealthStatus("ok", "1.0", "1h", null, null, null); - HealthStatus degraded = new HealthStatus("degraded", "1.0", "1h", null, null, null); - HealthStatus unhealthy = new HealthStatus("unhealthy", "1.0", "1h", null, null, null); - - assertThat(healthy.isHealthy()).isTrue(); - assertThat(ok.isHealthy()).isTrue(); - assertThat(degraded.isHealthy()).isFalse(); - assertThat(unhealthy.isHealthy()).isFalse(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"status\":\"healthy\"," + - "\"version\":\"2.5.0\"," + - "\"uptime\":\"12h30m\"" + - "}"; - - HealthStatus status = objectMapper.readValue(json, HealthStatus.class); - - assertThat(status.getStatus()).isEqualTo("healthy"); - assertThat(status.getVersion()).isEqualTo("2.5.0"); - assertThat(status.isHealthy()).isTrue(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - HealthStatus s1 = new HealthStatus("healthy", "1.0", "1h", null, null, null); - HealthStatus s2 = new HealthStatus("healthy", "1.0", "1h", null, null, null); - HealthStatus s3 = new HealthStatus("degraded", "1.0", "1h", null, null, null); - - assertThat(s1).isEqualTo(s2); - assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); - assertThat(s1).isNotEqualTo(s3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - HealthStatus status = new HealthStatus("healthy", "2.0.0", "5h", null, null, null); - assertThat(status.toString()).contains("HealthStatus").contains("healthy"); - } - } - - @Nested - @DisplayName("PolicyApprovalResult") - class PolicyApprovalResultTests { - - @Test - @DisplayName("should create approved result") - void shouldCreateApprovedResult() { - Map data = new HashMap<>(); - data.put("sanitized_query", "SELECT * FROM users"); - List policies = Arrays.asList("pii-check", "sqli-check"); - Instant expiresAt = Instant.now().plusSeconds(300); - - PolicyApprovalResult result = new PolicyApprovalResult( - "ctx-123", true, false, data, policies, expiresAt, null, null, "5.2ms" - ); - - assertThat(result.getContextId()).isEqualTo("ctx-123"); - assertThat(result.isApproved()).isTrue(); - assertThat(result.getApprovedData()).containsKey("sanitized_query"); - assertThat(result.getPolicies()).containsExactly("pii-check", "sqli-check"); - assertThat(result.getExpiresAt()).isEqualTo(expiresAt); - assertThat(result.getBlockReason()).isNull(); - assertThat(result.getProcessingTime()).isEqualTo("5.2ms"); - } - - @Test - @DisplayName("should create blocked result") - void shouldCreateBlockedResult() { - PolicyApprovalResult result = new PolicyApprovalResult( - null, false, false, null, null, null, - "Request blocked by policy: pii-detection", null, "3.1ms" - ); - - assertThat(result.isApproved()).isFalse(); - assertThat(result.getBlockReason()).isEqualTo("Request blocked by policy: pii-detection"); - } - - @Test - @DisplayName("should check expiration") - void shouldCheckExpiration() { - Instant future = Instant.now().plusSeconds(3600); - Instant past = Instant.now().minusSeconds(3600); - - PolicyApprovalResult notExpired = new PolicyApprovalResult( - "ctx", true, false, null, null, future, null, null, null - ); - PolicyApprovalResult expired = new PolicyApprovalResult( - "ctx", true, false, null, null, past, null, null, null - ); - PolicyApprovalResult noExpiry = new PolicyApprovalResult( - "ctx", true, false, null, null, null, null, null, null - ); - - assertThat(notExpired.isExpired()).isFalse(); - assertThat(expired.isExpired()).isTrue(); - assertThat(noExpiry.isExpired()).isFalse(); - } - - @Test - @DisplayName("should extract blocking policy name - format 1") - void shouldExtractBlockingPolicyNameFormat1() { - PolicyApprovalResult result = new PolicyApprovalResult( - null, false, false, null, null, null, - "Request blocked by policy: my-policy", null, null - ); - - assertThat(result.getBlockingPolicyName()).isEqualTo("my-policy"); - } - - @Test - @DisplayName("should extract blocking policy name - format 2") - void shouldExtractBlockingPolicyNameFormat2() { - PolicyApprovalResult result = new PolicyApprovalResult( - null, false, false, null, null, null, - "Blocked by policy: another-policy", null, null - ); - - assertThat(result.getBlockingPolicyName()).isEqualTo("another-policy"); - } - - @Test - @DisplayName("should extract blocking policy name - bracket format") - void shouldExtractBlockingPolicyNameBracket() { - PolicyApprovalResult result = new PolicyApprovalResult( - null, false, false, null, null, null, - "[policy-name] Description of violation", null, null - ); - - assertThat(result.getBlockingPolicyName()).isEqualTo("policy-name"); - } - - @Test - @DisplayName("should return full reason when no pattern matches") - void shouldReturnFullReasonWhenNoPattern() { - PolicyApprovalResult result = new PolicyApprovalResult( - null, false, false, null, null, null, - "Generic block reason", null, null - ); - - assertThat(result.getBlockingPolicyName()).isEqualTo("Generic block reason"); - } - - @Test - @DisplayName("should return null for null block reason") - void shouldReturnNullForNullBlockReason() { - PolicyApprovalResult result = new PolicyApprovalResult( - "ctx", true, false, null, null, null, null, null, null - ); - - assertThat(result.getBlockingPolicyName()).isNull(); - } - - @Test - @DisplayName("should handle null collections") - void shouldHandleNullCollections() { - PolicyApprovalResult result = new PolicyApprovalResult( - "ctx", true, false, null, null, null, null, null, null - ); - - assertThat(result.getApprovedData()).isEmpty(); - assertThat(result.getPolicies()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PolicyApprovalResult r1 = new PolicyApprovalResult("c1", true, false, null, null, null, null, null, null); - PolicyApprovalResult r2 = new PolicyApprovalResult("c1", true, false, null, null, null, null, null, null); - PolicyApprovalResult r3 = new PolicyApprovalResult("c2", true, false, null, null, null, null, null, null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PolicyApprovalResult result = new PolicyApprovalResult( - "ctx-abc", true, false, null, Arrays.asList("p1"), null, null, null, "1ms" - ); - assertThat(result.toString()).contains("PolicyApprovalResult").contains("ctx-abc"); - } - } - - @Nested - @DisplayName("PlanRequest") - class PlanRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - PlanRequest request = PlanRequest.builder() - .objective("Analyze sales data") - .build(); - - assertThat(request.getObjective()).isEqualTo("Analyze sales data"); - assertThat(request.getDomain()).isEqualTo("generic"); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - Map context = new HashMap<>(); - context.put("dataset", "sales_2025"); - Map constraints = new HashMap<>(); - constraints.put("max_time", "60s"); - - PlanRequest request = PlanRequest.builder() - .objective("Generate report") - .domain("finance") - .userToken("user-123") - .context(context) - .constraints(constraints) - .maxSteps(10) - .parallel(true) - .build(); - - assertThat(request.getObjective()).isEqualTo("Generate report"); - assertThat(request.getDomain()).isEqualTo("finance"); - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getContext()).containsEntry("dataset", "sales_2025"); - assertThat(request.getConstraints()).containsEntry("max_time", "60s"); - assertThat(request.getMaxSteps()).isEqualTo(10); - assertThat(request.getParallel()).isTrue(); - } - - @Test - @DisplayName("should add context incrementally") - void shouldAddContextIncrementally() { - PlanRequest request = PlanRequest.builder() - .objective("test") - .addContext("k1", "v1") - .addContext("k2", "v2") - .build(); - - assertThat(request.getContext()).hasSize(2); - } - - @Test - @DisplayName("should fail when objective is null") - void shouldFailWhenObjectiveIsNull() { - assertThatThrownBy(() -> PlanRequest.builder().build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PlanRequest r1 = PlanRequest.builder().objective("obj1").build(); - PlanRequest r2 = PlanRequest.builder().objective("obj1").build(); - PlanRequest r3 = PlanRequest.builder().objective("obj2").build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PlanRequest request = PlanRequest.builder() - .objective("My objective") - .domain("healthcare") - .build(); - assertThat(request.toString()).contains("PlanRequest").contains("My objective"); - } - } - - @Nested - @DisplayName("ClientRequest") - class ClientRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - ClientRequest request = ClientRequest.builder() - .query("Hello, world!") - .build(); - - assertThat(request.getQuery()).isEqualTo("Hello, world!"); - assertThat(request.getRequestType()).isEqualTo("chat"); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - Map context = new HashMap<>(); - context.put("session", "sess-123"); - - ClientRequest request = ClientRequest.builder() - .query("What is AI governance?") - .userToken("user-456") - .clientId("client-789") - .requestType(RequestType.CHAT) - .context(context) - .llmProvider("anthropic") - .model("claude-3-opus") - .build(); - - assertThat(request.getQuery()).isEqualTo("What is AI governance?"); - assertThat(request.getUserToken()).isEqualTo("user-456"); - assertThat(request.getClientId()).isEqualTo("client-789"); - assertThat(request.getRequestType()).isEqualTo("chat"); - assertThat(request.getContext()).containsEntry("session", "sess-123"); - assertThat(request.getLlmProvider()).isEqualTo("anthropic"); - assertThat(request.getModel()).isEqualTo("claude-3-opus"); - } - - @Test - @DisplayName("should add context incrementally") - void shouldAddContextIncrementally() { - ClientRequest request = ClientRequest.builder() - .query("test") - .addContext("k1", "v1") - .addContext("k2", "v2") - .build(); - - assertThat(request.getContext()).hasSize(2); - } - - @Test - @DisplayName("should use different request types") - void shouldUseDifferentRequestTypes() { - ClientRequest chat = ClientRequest.builder().query("q").requestType(RequestType.CHAT).build(); - ClientRequest sql = ClientRequest.builder().query("q").requestType(RequestType.SQL).build(); - ClientRequest mcp = ClientRequest.builder().query("q").requestType(RequestType.MCP_QUERY).build(); - ClientRequest plan = ClientRequest.builder().query("q").requestType(RequestType.MULTI_AGENT_PLAN).build(); - - assertThat(chat.getRequestType()).isEqualTo("chat"); - assertThat(sql.getRequestType()).isEqualTo("sql"); - assertThat(mcp.getRequestType()).isEqualTo("mcp-query"); - assertThat(plan.getRequestType()).isEqualTo("multi-agent-plan"); - } - - @Test - @DisplayName("should fail when query is null") - void shouldFailWhenQueryIsNull() { - assertThatThrownBy(() -> ClientRequest.builder().build()) - .isInstanceOf(NullPointerException.class); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ClientRequest r1 = ClientRequest.builder().query("q1").build(); - ClientRequest r2 = ClientRequest.builder().query("q1").build(); - ClientRequest r3 = ClientRequest.builder().query("q2").build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ClientRequest request = ClientRequest.builder() - .query("test query") - .llmProvider("openai") - .build(); - assertThat(request.toString()).contains("ClientRequest").contains("openai"); - } - } - - @Nested - @DisplayName("ClientResponse") - class ClientResponseTests { - - @Test - @DisplayName("should create successful response") - void shouldCreateSuccessfulResponse() { - PolicyInfo policyInfo = new PolicyInfo( - Arrays.asList("policy1"), null, "5ms", "tenant1", null, null - ); - - ClientResponse response = new ClientResponse( - true, "Response data", "result text", "plan-123", - false, null, policyInfo, null, null, null - ); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.getData()).isEqualTo("Response data"); - assertThat(response.getResult()).isEqualTo("result text"); - assertThat(response.getPlanId()).isEqualTo("plan-123"); - assertThat(response.isBlocked()).isFalse(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getError()).isNull(); - } - - @Test - @DisplayName("should create blocked response") - void shouldCreateBlockedResponse() { - ClientResponse response = new ClientResponse( - false, null, null, null, - true, "Request blocked by policy: pii-check", null, null, null, null - ); - - assertThat(response.isSuccess()).isFalse(); - assertThat(response.isBlocked()).isTrue(); - assertThat(response.getBlockReason()).isEqualTo("Request blocked by policy: pii-check"); - } - - @Test - @DisplayName("should create error response") - void shouldCreateErrorResponse() { - ClientResponse response = new ClientResponse( - false, null, null, null, - false, null, null, "Internal server error", null, null - ); - - assertThat(response.isSuccess()).isFalse(); - assertThat(response.getError()).isEqualTo("Internal server error"); - } - - @Test - @DisplayName("should extract blocking policy name - format 1") - void shouldExtractBlockingPolicyNameFormat1() { - ClientResponse response = new ClientResponse( - false, null, null, null, - true, "Request blocked by policy: my-policy", null, null, null, null - ); - - assertThat(response.getBlockingPolicyName()).isEqualTo("my-policy"); - } - - @Test - @DisplayName("should extract blocking policy name - format 2") - void shouldExtractBlockingPolicyNameFormat2() { - ClientResponse response = new ClientResponse( - false, null, null, null, - true, "Blocked by policy: another-policy", null, null, null, null - ); - - assertThat(response.getBlockingPolicyName()).isEqualTo("another-policy"); - } - - @Test - @DisplayName("should extract blocking policy name - bracket format") - void shouldExtractBlockingPolicyNameBracket() { - ClientResponse response = new ClientResponse( - false, null, null, null, - true, "[policy-name] Detailed description", null, null, null, null - ); - - assertThat(response.getBlockingPolicyName()).isEqualTo("policy-name"); - } - - @Test - @DisplayName("should return full reason when no pattern matches") - void shouldReturnFullReasonWhenNoPattern() { - ClientResponse response = new ClientResponse( - false, null, null, null, - true, "Custom block reason", null, null, null, null - ); - - assertThat(response.getBlockingPolicyName()).isEqualTo("Custom block reason"); - } - - @Test - @DisplayName("should return null for null or empty block reason") - void shouldReturnNullForNullOrEmpty() { - ClientResponse nullReason = new ClientResponse( - true, null, null, null, false, null, null, null, null, null - ); - ClientResponse emptyReason = new ClientResponse( - true, null, null, null, false, "", null, null, null, null - ); - - assertThat(nullReason.getBlockingPolicyName()).isNull(); - assertThat(emptyReason.getBlockingPolicyName()).isNull(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"success\":true," + - "\"data\":{\"key\":\"value\"}," + - "\"blocked\":false," + - "\"policy_info\":{\"policies_evaluated\":[\"p1\"],\"processing_time\":\"2ms\"}" + - "}"; - - ClientResponse response = objectMapper.readValue(json, ClientResponse.class); - - assertThat(response.isSuccess()).isTrue(); - assertThat(response.isBlocked()).isFalse(); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).containsExactly("p1"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ClientResponse r1 = new ClientResponse(true, "data", null, null, false, null, null, null, null, null); - ClientResponse r2 = new ClientResponse(true, "data", null, null, false, null, null, null, null, null); - ClientResponse r3 = new ClientResponse(false, "data", null, null, false, null, null, null, null, null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ClientResponse response = new ClientResponse( - true, null, null, null, false, null, null, null, null, null - ); - assertThat(response.toString()).contains("ClientResponse"); - } - } - - @Nested - @DisplayName("MCPCheckInputRequest") - class MCPCheckInputRequestTests { - - @Test - @DisplayName("should create instance with connector type and statement only") - void shouldCreateWithBasicFields() { - MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT * FROM users"); - - assertThat(request.getConnectorType()).isEqualTo("postgres"); - assertThat(request.getStatement()).isEqualTo("SELECT * FROM users"); - assertThat(request.getOperation()).isEqualTo("execute"); - assertThat(request.getParameters()).isNull(); - } - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - Map params = Map.of("limit", 100); - MCPCheckInputRequest request = new MCPCheckInputRequest( - "postgres", "UPDATE users SET name = $1", params, "execute" - ); - - assertThat(request.getConnectorType()).isEqualTo("postgres"); - assertThat(request.getStatement()).isEqualTo("UPDATE users SET name = $1"); - assertThat(request.getOperation()).isEqualTo("execute"); - assertThat(request.getParameters()).containsEntry("limit", 100); - } - - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - MCPCheckInputRequest request = new MCPCheckInputRequest( - "postgres", "SELECT 1", Map.of("timeout", 30), "query" - ); - - String json = objectMapper.writeValueAsString(request); - - assertThat(json).contains("\"connector_type\":\"postgres\""); - assertThat(json).contains("\"statement\":\"SELECT 1\""); - assertThat(json).contains("\"operation\":\"query\""); - assertThat(json).contains("\"parameters\""); - } - - @Test - @DisplayName("should omit null parameters in JSON") - void shouldOmitNullParametersInJson() throws Exception { - MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT 1"); - - String json = objectMapper.writeValueAsString(request); - - assertThat(json).doesNotContain("\"parameters\""); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - MCPCheckInputRequest r1 = new MCPCheckInputRequest("postgres", "SELECT 1"); - MCPCheckInputRequest r2 = new MCPCheckInputRequest("postgres", "SELECT 1"); - MCPCheckInputRequest r3 = new MCPCheckInputRequest("mysql", "SELECT 1"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT 1"); - assertThat(request.toString()).contains("MCPCheckInputRequest"); - assertThat(request.toString()).contains("postgres"); - } - } - - @Nested - @DisplayName("MCPCheckInputResponse") - class MCPCheckInputResponseTests { - - @Test - @DisplayName("should create allowed response") - void shouldCreateAllowedResponse() { - MCPCheckInputResponse response = new MCPCheckInputResponse(true, null, 3, null); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getBlockReason()).isNull(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(3); - assertThat(response.getPolicyInfo()).isNull(); - } - - @Test - @DisplayName("should create blocked response") - void shouldCreateBlockedResponse() { - ConnectorPolicyInfo policyInfo = new ConnectorPolicyInfo( - 3, true, "SQL injection detected", 0, 1, null - ); - MCPCheckInputResponse response = new MCPCheckInputResponse( - false, "SQL injection detected", 3, policyInfo - ); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("SQL injection detected"); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().isBlocked()).isTrue(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"allowed\":true," + - "\"policies_evaluated\":5," + - "\"policy_info\":{\"policies_evaluated\":5,\"blocked\":false," + - "\"redactions_applied\":0,\"processing_time_ms\":2}" + - "}"; - - MCPCheckInputResponse response = objectMapper.readValue(json, MCPCheckInputResponse.class); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(5); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(5); - } - - @Test - @DisplayName("should deserialize blocked response from JSON") - void shouldDeserializeBlockedResponseFromJson() throws Exception { - String json = "{" + - "\"allowed\":false," + - "\"block_reason\":\"DROP TABLE not allowed\"," + - "\"policies_evaluated\":3," + - "\"policy_info\":{\"policies_evaluated\":3,\"blocked\":true," + - "\"block_reason\":\"DROP TABLE not allowed\"," + - "\"redactions_applied\":0,\"processing_time_ms\":1}" + - "}"; - - MCPCheckInputResponse response = objectMapper.readValue(json, MCPCheckInputResponse.class); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("DROP TABLE not allowed"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - MCPCheckInputResponse r1 = new MCPCheckInputResponse(true, null, 3, null); - MCPCheckInputResponse r2 = new MCPCheckInputResponse(true, null, 3, null); - MCPCheckInputResponse r3 = new MCPCheckInputResponse(false, "blocked", 3, null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - MCPCheckInputResponse response = new MCPCheckInputResponse(true, null, 3, null); - assertThat(response.toString()).contains("MCPCheckInputResponse"); - } - } - - @Nested - @DisplayName("MCPCheckOutputRequest") - class MCPCheckOutputRequestTests { - - @Test - @DisplayName("should create instance with connector type and response data only") - void shouldCreateWithBasicFields() { - List> data = List.of(Map.of("id", 1, "name", "Alice")); - MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); - - assertThat(request.getConnectorType()).isEqualTo("postgres"); - assertThat(request.getResponseData()).hasSize(1); - assertThat(request.getMessage()).isNull(); - assertThat(request.getMetadata()).isNull(); - assertThat(request.getRowCount()).isEqualTo(0); - } - - @Test - @DisplayName("should create instance with all fields") - void shouldCreateWithAllFields() { - List> data = List.of( - Map.of("id", 1, "name", "Alice"), - Map.of("id", 2, "name", "Bob") - ); - Map metadata = Map.of("source", "analytics"); - MCPCheckOutputRequest request = new MCPCheckOutputRequest( - "postgres", data, "Query completed", metadata, 2 - ); - - assertThat(request.getConnectorType()).isEqualTo("postgres"); - assertThat(request.getResponseData()).hasSize(2); - assertThat(request.getMessage()).isEqualTo("Query completed"); - assertThat(request.getMetadata()).containsEntry("source", "analytics"); - assertThat(request.getRowCount()).isEqualTo(2); - } - - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - List> data = List.of(Map.of("id", 1)); - MCPCheckOutputRequest request = new MCPCheckOutputRequest( - "postgres", data, "done", Map.of("key", "val"), 1 - ); - - String json = objectMapper.writeValueAsString(request); - - assertThat(json).contains("\"connector_type\":\"postgres\""); - assertThat(json).contains("\"response_data\""); - assertThat(json).contains("\"message\":\"done\""); - assertThat(json).contains("\"row_count\":1"); - } - - @Test - @DisplayName("should omit null fields in JSON") - void shouldOmitNullFieldsInJson() throws Exception { - List> data = List.of(Map.of("id", 1)); - MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); - - String json = objectMapper.writeValueAsString(request); - - assertThat(json).doesNotContain("\"message\""); - assertThat(json).doesNotContain("\"metadata\""); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - List> data = List.of(Map.of("id", 1)); - MCPCheckOutputRequest r1 = new MCPCheckOutputRequest("postgres", data); - MCPCheckOutputRequest r2 = new MCPCheckOutputRequest("postgres", data); - MCPCheckOutputRequest r3 = new MCPCheckOutputRequest("mysql", data); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - List> data = List.of(Map.of("id", 1)); - MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); - assertThat(request.toString()).contains("MCPCheckOutputRequest"); - assertThat(request.toString()).contains("postgres"); - } - } - - @Nested - @DisplayName("MCPCheckOutputResponse") - class MCPCheckOutputResponseTests { - - @Test - @DisplayName("should create allowed response") - void shouldCreateAllowedResponse() { - MCPCheckOutputResponse response = new MCPCheckOutputResponse( - true, null, null, 4, null, null - ); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getBlockReason()).isNull(); - assertThat(response.getRedactedData()).isNull(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(4); - assertThat(response.getExfiltrationInfo()).isNull(); - assertThat(response.getPolicyInfo()).isNull(); - } - - @Test - @DisplayName("should create blocked response with redacted data") - void shouldCreateBlockedResponseWithRedactedData() { - ConnectorPolicyInfo policyInfo = new ConnectorPolicyInfo( - 4, true, "PII detected", 1, 5, null - ); - List> redacted = List.of( - Map.of("id", 1, "ssn", "***REDACTED***") - ); - MCPCheckOutputResponse response = new MCPCheckOutputResponse( - false, "PII detected", redacted, 4, null, policyInfo - ); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("PII detected"); - assertThat(response.getRedactedData()).isNotNull(); - assertThat(response.getPolicyInfo().getRedactionsApplied()).isEqualTo(1); - } - - @Test - @DisplayName("should create response with exfiltration info") - void shouldCreateResponseWithExfiltrationInfo() { - ExfiltrationCheckInfo exfilInfo = new ExfiltrationCheckInfo( - 10, 1000, 2048, 1048576, true - ); - MCPCheckOutputResponse response = new MCPCheckOutputResponse( - true, null, null, 3, exfilInfo, null - ); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getExfiltrationInfo()).isNotNull(); - assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(10); - assertThat(response.getExfiltrationInfo().getRowLimit()).isEqualTo(1000); - assertThat(response.getExfiltrationInfo().isWithinLimits()).isTrue(); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"allowed\":true," + - "\"policies_evaluated\":3," + - "\"exfiltration_info\":{\"rows_returned\":5,\"row_limit\":500," + - "\"bytes_returned\":1024,\"byte_limit\":524288,\"within_limits\":true}," + - "\"policy_info\":{\"policies_evaluated\":3,\"blocked\":false," + - "\"redactions_applied\":0,\"processing_time_ms\":2}" + - "}"; - - MCPCheckOutputResponse response = objectMapper.readValue(json, MCPCheckOutputResponse.class); - - assertThat(response.isAllowed()).isTrue(); - assertThat(response.getPoliciesEvaluated()).isEqualTo(3); - assertThat(response.getExfiltrationInfo()).isNotNull(); - assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(5); - assertThat(response.getPolicyInfo()).isNotNull(); - } - - @Test - @DisplayName("should deserialize blocked response with redacted data from JSON") - void shouldDeserializeBlockedResponseFromJson() throws Exception { - String json = "{" + - "\"allowed\":false," + - "\"block_reason\":\"PII content detected\"," + - "\"redacted_data\":[{\"id\":1,\"ssn\":\"***REDACTED***\"}]," + - "\"policies_evaluated\":4," + - "\"policy_info\":{\"policies_evaluated\":4,\"blocked\":true," + - "\"block_reason\":\"PII content detected\"," + - "\"redactions_applied\":1,\"processing_time_ms\":3}" + - "}"; - - MCPCheckOutputResponse response = objectMapper.readValue(json, MCPCheckOutputResponse.class); - - assertThat(response.isAllowed()).isFalse(); - assertThat(response.getBlockReason()).isEqualTo("PII content detected"); - assertThat(response.getRedactedData()).isNotNull(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - MCPCheckOutputResponse r1 = new MCPCheckOutputResponse(true, null, null, 3, null, null); - MCPCheckOutputResponse r2 = new MCPCheckOutputResponse(true, null, null, 3, null, null); - MCPCheckOutputResponse r3 = new MCPCheckOutputResponse(false, "blocked", null, 3, null, null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - MCPCheckOutputResponse response = new MCPCheckOutputResponse( - true, null, null, 3, null, null - ); - assertThat(response.toString()).contains("MCPCheckOutputResponse"); - } + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } + + @Nested + @DisplayName("PortalLoginResponse") + class PortalLoginResponseTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + PortalLoginResponse response = + new PortalLoginResponse( + "session-123", "org-456", "user@example.com", "Test User", "2026-01-04T12:00:00Z"); + + assertThat(response.getSessionId()).isEqualTo("session-123"); + assertThat(response.getOrgId()).isEqualTo("org-456"); + assertThat(response.getEmail()).isEqualTo("user@example.com"); + assertThat(response.getName()).isEqualTo("Test User"); + assertThat(response.getExpiresAt()).isEqualTo("2026-01-04T12:00:00Z"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"session_id\":\"sess-abc\"," + + "\"org_id\":\"org-xyz\"," + + "\"email\":\"test@test.com\"," + + "\"name\":\"Test\"," + + "\"expires_at\":\"2026-01-05T00:00:00Z\"" + + "}"; + + PortalLoginResponse response = objectMapper.readValue(json, PortalLoginResponse.class); + + assertThat(response.getSessionId()).isEqualTo("sess-abc"); + assertThat(response.getOrgId()).isEqualTo("org-xyz"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PortalLoginResponse r1 = new PortalLoginResponse("s1", "o1", "e1", "n1", "ex1"); + PortalLoginResponse r2 = new PortalLoginResponse("s1", "o1", "e1", "n1", "ex1"); + PortalLoginResponse r3 = new PortalLoginResponse("s2", "o1", "e1", "n1", "ex1"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PortalLoginResponse response = new PortalLoginResponse("s", "o", "e", "n", "ex"); + assertThat(response.toString()).contains("PortalLoginResponse"); + } + } + + @Nested + @DisplayName("CodeArtifact") + class CodeArtifactTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + List policies = Arrays.asList("policy1", "policy2"); + CodeArtifact artifact = + new CodeArtifact(true, "python", "function", 1024, 50, 0, 1, policies); + + assertThat(artifact.isCodeOutput()).isTrue(); + assertThat(artifact.getLanguage()).isEqualTo("python"); + assertThat(artifact.getCodeType()).isEqualTo("function"); + assertThat(artifact.getSizeBytes()).isEqualTo(1024); + assertThat(artifact.getLineCount()).isEqualTo(50); + assertThat(artifact.getSecretsDetected()).isEqualTo(0); + assertThat(artifact.getUnsafePatterns()).isEqualTo(1); + assertThat(artifact.getPoliciesChecked()).containsExactly("policy1", "policy2"); + } + + @Test + @DisplayName("should handle null values with defaults") + void shouldHandleNullValues() { + CodeArtifact artifact = new CodeArtifact(false, null, null, 0, 0, 0, 0, null); + + assertThat(artifact.getLanguage()).isEmpty(); + assertThat(artifact.getCodeType()).isEmpty(); + assertThat(artifact.getPoliciesChecked()).isEmpty(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"is_code_output\":true," + + "\"language\":\"javascript\"," + + "\"code_type\":\"class\"," + + "\"size_bytes\":2048," + + "\"line_count\":100," + + "\"secrets_detected\":2," + + "\"unsafe_patterns\":3," + + "\"policies_checked\":[\"p1\",\"p2\"]" + + "}"; + + CodeArtifact artifact = objectMapper.readValue(json, CodeArtifact.class); + + assertThat(artifact.isCodeOutput()).isTrue(); + assertThat(artifact.getLanguage()).isEqualTo("javascript"); + assertThat(artifact.getSecretsDetected()).isEqualTo(2); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + CodeArtifact a1 = new CodeArtifact(true, "py", "fn", 100, 10, 0, 0, null); + CodeArtifact a2 = new CodeArtifact(true, "py", "fn", 100, 10, 0, 0, null); + CodeArtifact a3 = new CodeArtifact(false, "py", "fn", 100, 10, 0, 0, null); + + assertThat(a1).isEqualTo(a2); + assertThat(a1.hashCode()).isEqualTo(a2.hashCode()); + assertThat(a1).isNotEqualTo(a3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + CodeArtifact artifact = new CodeArtifact(true, "go", "script", 512, 25, 0, 0, null); + String str = artifact.toString(); + assertThat(str).contains("CodeArtifact"); + assertThat(str).contains("go"); + } + } + + @Nested + @DisplayName("ConnectorHealthStatus") + class ConnectorHealthStatusTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + Map details = new HashMap<>(); + details.put("db", "connected"); + + ConnectorHealthStatus status = + new ConnectorHealthStatus(true, 150L, details, "2026-01-04T10:00:00Z", null); + + assertThat(status.isHealthy()).isTrue(); + assertThat(status.getLatency()).isEqualTo(150L); + assertThat(status.getDetails()).containsEntry("db", "connected"); + assertThat(status.getTimestamp()).isEqualTo("2026-01-04T10:00:00Z"); + assertThat(status.getError()).isNull(); + } + + @Test + @DisplayName("should handle null values with defaults") + void shouldHandleNullValues() { + ConnectorHealthStatus status = + new ConnectorHealthStatus(null, null, null, null, "Connection failed"); + + assertThat(status.isHealthy()).isFalse(); + assertThat(status.getLatency()).isEqualTo(0L); + assertThat(status.getDetails()).isEmpty(); + assertThat(status.getTimestamp()).isEmpty(); + assertThat(status.getError()).isEqualTo("Connection failed"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"healthy\":false," + + "\"latency\":500," + + "\"timestamp\":\"2026-01-04T12:00:00Z\"," + + "\"error\":\"Timeout\"" + + "}"; + + ConnectorHealthStatus status = objectMapper.readValue(json, ConnectorHealthStatus.class); + + assertThat(status.isHealthy()).isFalse(); + assertThat(status.getLatency()).isEqualTo(500L); + assertThat(status.getError()).isEqualTo("Timeout"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ConnectorHealthStatus s1 = new ConnectorHealthStatus(true, 100L, null, "ts1", null); + ConnectorHealthStatus s2 = new ConnectorHealthStatus(true, 100L, null, "ts1", null); + ConnectorHealthStatus s3 = new ConnectorHealthStatus(false, 100L, null, "ts1", null); + + assertThat(s1).isEqualTo(s2); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isNotEqualTo(s3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ConnectorHealthStatus status = new ConnectorHealthStatus(true, 50L, null, "ts", null); + assertThat(status.toString()).contains("ConnectorHealthStatus"); + } + } + + @Nested + @DisplayName("ConnectorInfo") + class ConnectorInfoTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + List caps = Arrays.asList("read", "write"); + Map schema = new HashMap<>(); + schema.put("host", "string"); + + ConnectorInfo info = + new ConnectorInfo( + "conn-1", + "PostgreSQL", + "Database connector", + "database", + "1.0.0", + caps, + schema, + true, + true); + + assertThat(info.getId()).isEqualTo("conn-1"); + assertThat(info.getName()).isEqualTo("PostgreSQL"); + assertThat(info.getDescription()).isEqualTo("Database connector"); + assertThat(info.getType()).isEqualTo("database"); + assertThat(info.getVersion()).isEqualTo("1.0.0"); + assertThat(info.getCapabilities()).containsExactly("read", "write"); + assertThat(info.getConfigSchema()).containsEntry("host", "string"); + assertThat(info.isInstalled()).isTrue(); + assertThat(info.isEnabled()).isTrue(); + } + + @Test + @DisplayName("should handle null collections") + void shouldHandleNullCollections() { + ConnectorInfo info = + new ConnectorInfo("id", "name", "desc", "type", "v1", null, null, false, false); + + assertThat(info.getCapabilities()).isEmpty(); + assertThat(info.getConfigSchema()).isEmpty(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"id\":\"mysql-connector\"," + + "\"name\":\"MySQL\"," + + "\"type\":\"database\"," + + "\"version\":\"2.0.0\"," + + "\"installed\":true," + + "\"enabled\":false" + + "}"; + + ConnectorInfo info = objectMapper.readValue(json, ConnectorInfo.class); + + assertThat(info.getId()).isEqualTo("mysql-connector"); + assertThat(info.isInstalled()).isTrue(); + assertThat(info.isEnabled()).isFalse(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ConnectorInfo i1 = new ConnectorInfo("id1", "n", "d", "t", "v", null, null, true, true); + ConnectorInfo i2 = new ConnectorInfo("id1", "n", "d", "t", "v", null, null, true, true); + ConnectorInfo i3 = new ConnectorInfo("id2", "n", "d", "t", "v", null, null, true, true); + + assertThat(i1).isEqualTo(i2); + assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); + assertThat(i1).isNotEqualTo(i3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ConnectorInfo info = + new ConnectorInfo("id", "MySQL", "d", "db", "1.0", null, null, true, true); + assertThat(info.toString()).contains("ConnectorInfo").contains("MySQL"); + } + } + + @Nested + @DisplayName("AuditOptions") + class AuditOptionsTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + AuditOptions options = + AuditOptions.builder().contextId("ctx-123").clientId("client-456").build(); + + assertThat(options.getContextId()).isEqualTo("ctx-123"); + assertThat(options.getClientId()).isEqualTo("client-456"); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + TokenUsage usage = TokenUsage.of(100, 200); + Map metadata = new HashMap<>(); + metadata.put("key", "value"); + + AuditOptions options = + AuditOptions.builder() + .contextId("ctx-123") + .clientId("client-456") + .responseSummary("Summary of response") + .provider("openai") + .model("gpt-4") + .tokenUsage(usage) + .latencyMs(1234) + .metadata(metadata) + .success(true) + .errorMessage(null) + .build(); + + assertThat(options.getContextId()).isEqualTo("ctx-123"); + assertThat(options.getResponseSummary()).isEqualTo("Summary of response"); + assertThat(options.getProvider()).isEqualTo("openai"); + assertThat(options.getModel()).isEqualTo("gpt-4"); + assertThat(options.getTokenUsage()).isEqualTo(usage); + assertThat(options.getLatencyMs()).isEqualTo(1234L); + assertThat(options.getMetadata()).containsEntry("key", "value"); + assertThat(options.getSuccess()).isTrue(); + } + + @Test + @DisplayName("should add metadata incrementally") + void shouldAddMetadataIncrementally() { + AuditOptions options = + AuditOptions.builder() + .contextId("ctx") + .clientId("client") + .addMetadata("k1", "v1") + .addMetadata("k2", "v2") + .build(); + + assertThat(options.getMetadata()).hasSize(2); + assertThat(options.getMetadata()).containsEntry("k1", "v1"); + assertThat(options.getMetadata()).containsEntry("k2", "v2"); + } + + @Test + @DisplayName("should fail when contextId is null") + void shouldFailWhenContextIdIsNull() { + assertThatThrownBy(() -> AuditOptions.builder().clientId("client").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should allow clientId to be null for smart defaults") + void shouldAllowClientIdToBeNull() { + // clientId can be null - SDK will use smart default "community" + AuditOptions options = AuditOptions.builder().contextId("ctx").build(); + assertThat(options.getContextId()).isEqualTo("ctx"); + assertThat(options.getClientId()).isNull(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + AuditOptions o1 = AuditOptions.builder().contextId("c1").clientId("cl1").build(); + AuditOptions o2 = AuditOptions.builder().contextId("c1").clientId("cl1").build(); + AuditOptions o3 = AuditOptions.builder().contextId("c2").clientId("cl1").build(); + + assertThat(o1).isEqualTo(o2); + assertThat(o1.hashCode()).isEqualTo(o2.hashCode()); + assertThat(o1).isNotEqualTo(o3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + AuditOptions options = + AuditOptions.builder().contextId("ctx").clientId("client").provider("anthropic").build(); + assertThat(options.toString()).contains("AuditOptions").contains("anthropic"); + } + } + + @Nested + @DisplayName("AuditResult") + class AuditResultTests { + + @Test + @DisplayName("should create successful result") + void shouldCreateSuccessfulResult() { + AuditResult result = new AuditResult(true, "audit-123", "Recorded", null); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getAuditId()).isEqualTo("audit-123"); + assertThat(result.getMessage()).isEqualTo("Recorded"); + assertThat(result.getError()).isNull(); + } + + @Test + @DisplayName("should create failed result") + void shouldCreateFailedResult() { + AuditResult result = new AuditResult(false, null, null, "Context expired"); + + assertThat(result.isSuccess()).isFalse(); + assertThat(result.getError()).isEqualTo("Context expired"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + "\"success\":true," + "\"audit_id\":\"aud-456\"," + "\"message\":\"OK\"" + "}"; + + AuditResult result = objectMapper.readValue(json, AuditResult.class); + + assertThat(result.isSuccess()).isTrue(); + assertThat(result.getAuditId()).isEqualTo("aud-456"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + AuditResult r1 = new AuditResult(true, "a1", "m1", null); + AuditResult r2 = new AuditResult(true, "a1", "m1", null); + AuditResult r3 = new AuditResult(false, "a1", "m1", null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + AuditResult result = new AuditResult(true, "aud", "msg", null); + assertThat(result.toString()).contains("AuditResult"); + } + } + + @Nested + @DisplayName("PlanStep") + class PlanStepTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + List deps = Arrays.asList("step-1", "step-2"); + Map params = new HashMap<>(); + params.put("query", "SELECT * FROM users"); + + PlanStep step = + new PlanStep( + "step-3", + "Query Database", + "connector-call", + "Fetch user data", + deps, + "db-agent", + params, + "2s"); + + assertThat(step.getId()).isEqualTo("step-3"); + assertThat(step.getName()).isEqualTo("Query Database"); + assertThat(step.getType()).isEqualTo("connector-call"); + assertThat(step.getDescription()).isEqualTo("Fetch user data"); + assertThat(step.getDependsOn()).containsExactly("step-1", "step-2"); + assertThat(step.getAgent()).isEqualTo("db-agent"); + assertThat(step.getParameters()).containsEntry("query", "SELECT * FROM users"); + assertThat(step.getEstimatedTime()).isEqualTo("2s"); + } + + @Test + @DisplayName("should handle null collections") + void shouldHandleNullCollections() { + PlanStep step = new PlanStep("id", "name", "type", "desc", null, "agent", null, "1s"); + + assertThat(step.getDependsOn()).isEmpty(); + assertThat(step.getParameters()).isEmpty(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"id\":\"s1\"," + + "\"name\":\"Step 1\"," + + "\"type\":\"llm-call\"," + + "\"description\":\"Call LLM\"," + + "\"depends_on\":[\"s0\"]," + + "\"agent\":\"llm-agent\"," + + "\"estimated_time\":\"500ms\"" + + "}"; + + PlanStep step = objectMapper.readValue(json, PlanStep.class); + + assertThat(step.getId()).isEqualTo("s1"); + assertThat(step.getType()).isEqualTo("llm-call"); + assertThat(step.getDependsOn()).containsExactly("s0"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PlanStep s1 = new PlanStep("id1", "n", "t", "d", null, "a", null, "1s"); + PlanStep s2 = new PlanStep("id1", "n", "t", "d", null, "a", null, "1s"); + PlanStep s3 = new PlanStep("id2", "n", "t", "d", null, "a", null, "1s"); + + assertThat(s1).isEqualTo(s2); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isNotEqualTo(s3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PlanStep step = new PlanStep("id", "LLM Call", "llm-call", "desc", null, "agent", null, "1s"); + assertThat(step.toString()).contains("PlanStep").contains("LLM Call"); + } + } + + @Nested + @DisplayName("PolicyApprovalRequest") + class PolicyApprovalRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() + .userToken("user-123") + .query("What is the weather?") + .build(); + + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getQuery()).isEqualTo("What is the weather?"); + assertThat(request.getDataSources()).isEmpty(); + assertThat(request.getContext()).isEmpty(); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + List sources = Arrays.asList("db1", "db2"); + Map context = new HashMap<>(); + context.put("env", "production"); + + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() + .userToken("user-456") + .query("Query data") + .dataSources(sources) + .context(context) + .clientId("client-789") + .build(); + + assertThat(request.getUserToken()).isEqualTo("user-456"); + assertThat(request.getDataSources()).containsExactly("db1", "db2"); + assertThat(request.getContext()).containsEntry("env", "production"); + assertThat(request.getClientId()).isEqualTo("client-789"); + } + + @Test + @DisplayName("should add context incrementally") + void shouldAddContextIncrementally() { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() + .userToken("user") + .query("query") + .addContext("k1", "v1") + .addContext("k2", "v2") + .build(); + + assertThat(request.getContext()).hasSize(2); + } + + @Test + @DisplayName("should fail when userToken is null") + void shouldFailWhenUserTokenIsNull() { + assertThatThrownBy(() -> PolicyApprovalRequest.builder().query("query").build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PolicyApprovalRequest r1 = + PolicyApprovalRequest.builder().userToken("u1").query("q1").build(); + PolicyApprovalRequest r2 = + PolicyApprovalRequest.builder().userToken("u1").query("q1").build(); + PolicyApprovalRequest r3 = + PolicyApprovalRequest.builder().userToken("u2").query("q1").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder().userToken("user").query("test query").build(); + assertThat(request.toString()).contains("PolicyApprovalRequest"); + } + } + + @Nested + @DisplayName("PolicyInfo") + class PolicyInfoTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + List policies = Arrays.asList("policy1", "policy2"); + List checks = Arrays.asList("pii", "sqli"); + CodeArtifact artifact = new CodeArtifact(true, "python", "function", 100, 10, 0, 0, null); + + PolicyInfo info = new PolicyInfo(policies, checks, "17.48ms", "tenant-1", 0.15, artifact); + + assertThat(info.getPoliciesEvaluated()).containsExactly("policy1", "policy2"); + assertThat(info.getStaticChecks()).containsExactly("pii", "sqli"); + assertThat(info.getProcessingTime()).isEqualTo("17.48ms"); + assertThat(info.getTenantId()).isEqualTo("tenant-1"); + assertThat(info.getRiskScore()).isEqualTo(0.15); + assertThat(info.getCodeArtifact()).isNotNull(); + } + + @Test + @DisplayName("should handle null collections") + void shouldHandleNullCollections() { + PolicyInfo info = new PolicyInfo(null, null, null, null, null, null); + + assertThat(info.getPoliciesEvaluated()).isEmpty(); + assertThat(info.getStaticChecks()).isEmpty(); + } + + @Test + @DisplayName("should parse processing time as Duration - milliseconds") + void shouldParseProcessingTimeMs() { + PolicyInfo info = new PolicyInfo(null, null, "17.48ms", null, null, null); + Duration duration = info.getProcessingDuration(); + + assertThat(duration.toMillis()).isEqualTo(17); + } + + @Test + @DisplayName("should parse processing time as Duration - seconds") + void shouldParseProcessingTimeSeconds() { + PolicyInfo info = new PolicyInfo(null, null, "1.5s", null, null, null); + Duration duration = info.getProcessingDuration(); + + assertThat(duration.toMillis()).isEqualTo(1500); + } + + @Test + @DisplayName("should handle plain numeric value as milliseconds") + void shouldHandlePlainNumericValue() { + PolicyInfo info = new PolicyInfo(null, null, "100", null, null, null); + Duration duration = info.getProcessingDuration(); + + assertThat(duration.toMillis()).isEqualTo(100); + } + + @Test + @DisplayName("should return zero duration for null or empty") + void shouldReturnZeroForNullOrEmpty() { + PolicyInfo infoNull = new PolicyInfo(null, null, null, null, null, null); + PolicyInfo infoEmpty = new PolicyInfo(null, null, "", null, null, null); + + assertThat(infoNull.getProcessingDuration()).isEqualTo(Duration.ZERO); + assertThat(infoEmpty.getProcessingDuration()).isEqualTo(Duration.ZERO); + } + + @Test + @DisplayName("should return zero duration for invalid format") + void shouldReturnZeroForInvalidFormat() { + PolicyInfo info = new PolicyInfo(null, null, "invalid", null, null, null); + assertThat(info.getProcessingDuration()).isEqualTo(Duration.ZERO); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"policies_evaluated\":[\"p1\"]," + + "\"static_checks\":[\"c1\"]," + + "\"processing_time\":\"10ms\"," + + "\"tenant_id\":\"t1\"," + + "\"risk_score\":0.5" + + "}"; + + PolicyInfo info = objectMapper.readValue(json, PolicyInfo.class); + + assertThat(info.getPoliciesEvaluated()).containsExactly("p1"); + assertThat(info.getRiskScore()).isEqualTo(0.5); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PolicyInfo i1 = new PolicyInfo(Arrays.asList("p1"), null, "10ms", "t1", null, null); + PolicyInfo i2 = new PolicyInfo(Arrays.asList("p1"), null, "10ms", "t1", null, null); + PolicyInfo i3 = new PolicyInfo(Arrays.asList("p2"), null, "10ms", "t1", null, null); + + assertThat(i1).isEqualTo(i2); + assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); + assertThat(i1).isNotEqualTo(i3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PolicyInfo info = new PolicyInfo(Arrays.asList("pol1"), null, "5ms", "tenant", null, null); + assertThat(info.toString()).contains("PolicyInfo").contains("pol1"); + } + } + + @Nested + @DisplayName("TokenUsage") + class TokenUsageTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + TokenUsage usage = new TokenUsage(100, 200, 300); + + assertThat(usage.getPromptTokens()).isEqualTo(100); + assertThat(usage.getCompletionTokens()).isEqualTo(200); + assertThat(usage.getTotalTokens()).isEqualTo(300); + } + + @Test + @DisplayName("should create using factory method with auto-calculated total") + void shouldCreateUsingFactoryMethod() { + TokenUsage usage = TokenUsage.of(150, 250); + + assertThat(usage.getPromptTokens()).isEqualTo(150); + assertThat(usage.getCompletionTokens()).isEqualTo(250); + assertThat(usage.getTotalTokens()).isEqualTo(400); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"prompt_tokens\":50," + + "\"completion_tokens\":75," + + "\"total_tokens\":125" + + "}"; + + TokenUsage usage = objectMapper.readValue(json, TokenUsage.class); + + assertThat(usage.getPromptTokens()).isEqualTo(50); + assertThat(usage.getCompletionTokens()).isEqualTo(75); + assertThat(usage.getTotalTokens()).isEqualTo(125); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + TokenUsage u1 = TokenUsage.of(100, 200); + TokenUsage u2 = TokenUsage.of(100, 200); + TokenUsage u3 = TokenUsage.of(100, 300); + + assertThat(u1).isEqualTo(u2); + assertThat(u1.hashCode()).isEqualTo(u2.hashCode()); + assertThat(u1).isNotEqualTo(u3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + TokenUsage usage = TokenUsage.of(10, 20); + assertThat(usage.toString()).contains("TokenUsage").contains("10").contains("20"); + } + } + + @Nested + @DisplayName("RateLimitInfo") + class RateLimitInfoTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + Instant resetAt = Instant.parse("2026-01-04T12:00:00Z"); + RateLimitInfo info = new RateLimitInfo(1000, 500, resetAt); + + assertThat(info.getLimit()).isEqualTo(1000); + assertThat(info.getRemaining()).isEqualTo(500); + assertThat(info.getResetAt()).isEqualTo(resetAt); + } + + @Test + @DisplayName("should detect exceeded rate limit") + void shouldDetectExceededRateLimit() { + RateLimitInfo exceeded = new RateLimitInfo(100, 0, null); + RateLimitInfo notExceeded = new RateLimitInfo(100, 50, null); + + assertThat(exceeded.isExceeded()).isTrue(); + assertThat(notExceeded.isExceeded()).isFalse(); + } + + @Test + @DisplayName("should detect exceeded with negative remaining") + void shouldDetectExceededWithNegative() { + RateLimitInfo info = new RateLimitInfo(100, -5, null); + assertThat(info.isExceeded()).isTrue(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + Instant reset = Instant.now(); + RateLimitInfo r1 = new RateLimitInfo(100, 50, reset); + RateLimitInfo r2 = new RateLimitInfo(100, 50, reset); + RateLimitInfo r3 = new RateLimitInfo(100, 25, reset); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + RateLimitInfo info = new RateLimitInfo(100, 75, null); + assertThat(info.toString()).contains("RateLimitInfo").contains("100").contains("75"); + } + } + + @Nested + @DisplayName("Mode") + class ModeTests { + + @Test + @DisplayName("should have correct values") + void shouldHaveCorrectValues() { + assertThat(Mode.PRODUCTION.getValue()).isEqualTo("production"); + assertThat(Mode.SANDBOX.getValue()).isEqualTo("sandbox"); + } + + @Test + @DisplayName("should parse from value") + void shouldParseFromValue() { + assertThat(Mode.fromValue("production")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue("sandbox")).isEqualTo(Mode.SANDBOX); + assertThat(Mode.fromValue("PRODUCTION")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue("SANDBOX")).isEqualTo(Mode.SANDBOX); + } + + @Test + @DisplayName("should return PRODUCTION for unknown values") + void shouldReturnProductionForUnknown() { + assertThat(Mode.fromValue("unknown")).isEqualTo(Mode.PRODUCTION); + assertThat(Mode.fromValue("")).isEqualTo(Mode.PRODUCTION); + } + + @Test + @DisplayName("should return PRODUCTION for null") + void shouldReturnProductionForNull() { + assertThat(Mode.fromValue(null)).isEqualTo(Mode.PRODUCTION); + } + } + + @Nested + @DisplayName("RequestType") + class RequestTypeTests { + + @Test + @DisplayName("should have correct values") + void shouldHaveCorrectValues() { + assertThat(RequestType.CHAT.getValue()).isEqualTo("chat"); + assertThat(RequestType.SQL.getValue()).isEqualTo("sql"); + assertThat(RequestType.MCP_QUERY.getValue()).isEqualTo("mcp-query"); + assertThat(RequestType.MULTI_AGENT_PLAN.getValue()).isEqualTo("multi-agent-plan"); + } + + @Test + @DisplayName("should parse from value") + void shouldParseFromValue() { + assertThat(RequestType.fromValue("chat")).isEqualTo(RequestType.CHAT); + assertThat(RequestType.fromValue("sql")).isEqualTo(RequestType.SQL); + assertThat(RequestType.fromValue("mcp-query")).isEqualTo(RequestType.MCP_QUERY); + assertThat(RequestType.fromValue("multi-agent-plan")).isEqualTo(RequestType.MULTI_AGENT_PLAN); + } + + @Test + @DisplayName("should parse case insensitively") + void shouldParseCaseInsensitively() { + assertThat(RequestType.fromValue("CHAT")).isEqualTo(RequestType.CHAT); + assertThat(RequestType.fromValue("Chat")).isEqualTo(RequestType.CHAT); + } + + @Test + @DisplayName("should throw for unknown value") + void shouldThrowForUnknownValue() { + assertThatThrownBy(() -> RequestType.fromValue("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown request type"); + } + + @Test + @DisplayName("should throw for null value") + void shouldThrowForNullValue() { + assertThatThrownBy(() -> RequestType.fromValue(null)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("cannot be null"); + } + } + + @Nested + @DisplayName("HealthStatus") + class HealthStatusTests { + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + Map components = new HashMap<>(); + components.put("database", "healthy"); + components.put("cache", "healthy"); + + HealthStatus status = new HealthStatus("healthy", "2.6.0", "24h5m", components, null, null); + + assertThat(status.getStatus()).isEqualTo("healthy"); + assertThat(status.getVersion()).isEqualTo("2.6.0"); + assertThat(status.getUptime()).isEqualTo("24h5m"); + assertThat(status.getComponents()).containsEntry("database", "healthy"); + } + + @Test + @DisplayName("should handle null components") + void shouldHandleNullComponents() { + HealthStatus status = new HealthStatus("healthy", "1.0.0", "1h", null, null, null); + assertThat(status.getComponents()).isEmpty(); + } + + @Test + @DisplayName("should detect healthy status") + void shouldDetectHealthyStatus() { + HealthStatus healthy = new HealthStatus("healthy", "1.0", "1h", null, null, null); + HealthStatus ok = new HealthStatus("ok", "1.0", "1h", null, null, null); + HealthStatus degraded = new HealthStatus("degraded", "1.0", "1h", null, null, null); + HealthStatus unhealthy = new HealthStatus("unhealthy", "1.0", "1h", null, null, null); + + assertThat(healthy.isHealthy()).isTrue(); + assertThat(ok.isHealthy()).isTrue(); + assertThat(degraded.isHealthy()).isFalse(); + assertThat(unhealthy.isHealthy()).isFalse(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"status\":\"healthy\"," + + "\"version\":\"2.5.0\"," + + "\"uptime\":\"12h30m\"" + + "}"; + + HealthStatus status = objectMapper.readValue(json, HealthStatus.class); + + assertThat(status.getStatus()).isEqualTo("healthy"); + assertThat(status.getVersion()).isEqualTo("2.5.0"); + assertThat(status.isHealthy()).isTrue(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + HealthStatus s1 = new HealthStatus("healthy", "1.0", "1h", null, null, null); + HealthStatus s2 = new HealthStatus("healthy", "1.0", "1h", null, null, null); + HealthStatus s3 = new HealthStatus("degraded", "1.0", "1h", null, null, null); + + assertThat(s1).isEqualTo(s2); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isNotEqualTo(s3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + HealthStatus status = new HealthStatus("healthy", "2.0.0", "5h", null, null, null); + assertThat(status.toString()).contains("HealthStatus").contains("healthy"); + } + } + + @Nested + @DisplayName("PolicyApprovalResult") + class PolicyApprovalResultTests { + + @Test + @DisplayName("should create approved result") + void shouldCreateApprovedResult() { + Map data = new HashMap<>(); + data.put("sanitized_query", "SELECT * FROM users"); + List policies = Arrays.asList("pii-check", "sqli-check"); + Instant expiresAt = Instant.now().plusSeconds(300); + + PolicyApprovalResult result = + new PolicyApprovalResult( + "ctx-123", true, false, data, policies, expiresAt, null, null, "5.2ms"); + + assertThat(result.getContextId()).isEqualTo("ctx-123"); + assertThat(result.isApproved()).isTrue(); + assertThat(result.getApprovedData()).containsKey("sanitized_query"); + assertThat(result.getPolicies()).containsExactly("pii-check", "sqli-check"); + assertThat(result.getExpiresAt()).isEqualTo(expiresAt); + assertThat(result.getBlockReason()).isNull(); + assertThat(result.getProcessingTime()).isEqualTo("5.2ms"); + } + + @Test + @DisplayName("should create blocked result") + void shouldCreateBlockedResult() { + PolicyApprovalResult result = + new PolicyApprovalResult( + null, + false, + false, + null, + null, + null, + "Request blocked by policy: pii-detection", + null, + "3.1ms"); + + assertThat(result.isApproved()).isFalse(); + assertThat(result.getBlockReason()).isEqualTo("Request blocked by policy: pii-detection"); + } + + @Test + @DisplayName("should check expiration") + void shouldCheckExpiration() { + Instant future = Instant.now().plusSeconds(3600); + Instant past = Instant.now().minusSeconds(3600); + + PolicyApprovalResult notExpired = + new PolicyApprovalResult("ctx", true, false, null, null, future, null, null, null); + PolicyApprovalResult expired = + new PolicyApprovalResult("ctx", true, false, null, null, past, null, null, null); + PolicyApprovalResult noExpiry = + new PolicyApprovalResult("ctx", true, false, null, null, null, null, null, null); + + assertThat(notExpired.isExpired()).isFalse(); + assertThat(expired.isExpired()).isTrue(); + assertThat(noExpiry.isExpired()).isFalse(); + } + + @Test + @DisplayName("should extract blocking policy name - format 1") + void shouldExtractBlockingPolicyNameFormat1() { + PolicyApprovalResult result = + new PolicyApprovalResult( + null, + false, + false, + null, + null, + null, + "Request blocked by policy: my-policy", + null, + null); + + assertThat(result.getBlockingPolicyName()).isEqualTo("my-policy"); + } + + @Test + @DisplayName("should extract blocking policy name - format 2") + void shouldExtractBlockingPolicyNameFormat2() { + PolicyApprovalResult result = + new PolicyApprovalResult( + null, + false, + false, + null, + null, + null, + "Blocked by policy: another-policy", + null, + null); + + assertThat(result.getBlockingPolicyName()).isEqualTo("another-policy"); + } + + @Test + @DisplayName("should extract blocking policy name - bracket format") + void shouldExtractBlockingPolicyNameBracket() { + PolicyApprovalResult result = + new PolicyApprovalResult( + null, + false, + false, + null, + null, + null, + "[policy-name] Description of violation", + null, + null); + + assertThat(result.getBlockingPolicyName()).isEqualTo("policy-name"); + } + + @Test + @DisplayName("should return full reason when no pattern matches") + void shouldReturnFullReasonWhenNoPattern() { + PolicyApprovalResult result = + new PolicyApprovalResult( + null, false, false, null, null, null, "Generic block reason", null, null); + + assertThat(result.getBlockingPolicyName()).isEqualTo("Generic block reason"); + } + + @Test + @DisplayName("should return null for null block reason") + void shouldReturnNullForNullBlockReason() { + PolicyApprovalResult result = + new PolicyApprovalResult("ctx", true, false, null, null, null, null, null, null); + + assertThat(result.getBlockingPolicyName()).isNull(); + } + + @Test + @DisplayName("should handle null collections") + void shouldHandleNullCollections() { + PolicyApprovalResult result = + new PolicyApprovalResult("ctx", true, false, null, null, null, null, null, null); + + assertThat(result.getApprovedData()).isEmpty(); + assertThat(result.getPolicies()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PolicyApprovalResult r1 = + new PolicyApprovalResult("c1", true, false, null, null, null, null, null, null); + PolicyApprovalResult r2 = + new PolicyApprovalResult("c1", true, false, null, null, null, null, null, null); + PolicyApprovalResult r3 = + new PolicyApprovalResult("c2", true, false, null, null, null, null, null, null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PolicyApprovalResult result = + new PolicyApprovalResult( + "ctx-abc", true, false, null, Arrays.asList("p1"), null, null, null, "1ms"); + assertThat(result.toString()).contains("PolicyApprovalResult").contains("ctx-abc"); + } + } + + @Nested + @DisplayName("PlanRequest") + class PlanRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + PlanRequest request = PlanRequest.builder().objective("Analyze sales data").build(); + + assertThat(request.getObjective()).isEqualTo("Analyze sales data"); + assertThat(request.getDomain()).isEqualTo("generic"); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + Map context = new HashMap<>(); + context.put("dataset", "sales_2025"); + Map constraints = new HashMap<>(); + constraints.put("max_time", "60s"); + + PlanRequest request = + PlanRequest.builder() + .objective("Generate report") + .domain("finance") + .userToken("user-123") + .context(context) + .constraints(constraints) + .maxSteps(10) + .parallel(true) + .build(); + + assertThat(request.getObjective()).isEqualTo("Generate report"); + assertThat(request.getDomain()).isEqualTo("finance"); + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getContext()).containsEntry("dataset", "sales_2025"); + assertThat(request.getConstraints()).containsEntry("max_time", "60s"); + assertThat(request.getMaxSteps()).isEqualTo(10); + assertThat(request.getParallel()).isTrue(); + } + + @Test + @DisplayName("should add context incrementally") + void shouldAddContextIncrementally() { + PlanRequest request = + PlanRequest.builder() + .objective("test") + .addContext("k1", "v1") + .addContext("k2", "v2") + .build(); + + assertThat(request.getContext()).hasSize(2); + } + + @Test + @DisplayName("should fail when objective is null") + void shouldFailWhenObjectiveIsNull() { + assertThatThrownBy(() -> PlanRequest.builder().build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PlanRequest r1 = PlanRequest.builder().objective("obj1").build(); + PlanRequest r2 = PlanRequest.builder().objective("obj1").build(); + PlanRequest r3 = PlanRequest.builder().objective("obj2").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PlanRequest request = + PlanRequest.builder().objective("My objective").domain("healthcare").build(); + assertThat(request.toString()).contains("PlanRequest").contains("My objective"); + } + } + + @Nested + @DisplayName("ClientRequest") + class ClientRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + ClientRequest request = ClientRequest.builder().query("Hello, world!").build(); + + assertThat(request.getQuery()).isEqualTo("Hello, world!"); + assertThat(request.getRequestType()).isEqualTo("chat"); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + Map context = new HashMap<>(); + context.put("session", "sess-123"); + + ClientRequest request = + ClientRequest.builder() + .query("What is AI governance?") + .userToken("user-456") + .clientId("client-789") + .requestType(RequestType.CHAT) + .context(context) + .llmProvider("anthropic") + .model("claude-3-opus") + .build(); + + assertThat(request.getQuery()).isEqualTo("What is AI governance?"); + assertThat(request.getUserToken()).isEqualTo("user-456"); + assertThat(request.getClientId()).isEqualTo("client-789"); + assertThat(request.getRequestType()).isEqualTo("chat"); + assertThat(request.getContext()).containsEntry("session", "sess-123"); + assertThat(request.getLlmProvider()).isEqualTo("anthropic"); + assertThat(request.getModel()).isEqualTo("claude-3-opus"); + } + + @Test + @DisplayName("should add context incrementally") + void shouldAddContextIncrementally() { + ClientRequest request = + ClientRequest.builder() + .query("test") + .addContext("k1", "v1") + .addContext("k2", "v2") + .build(); + + assertThat(request.getContext()).hasSize(2); + } + + @Test + @DisplayName("should use different request types") + void shouldUseDifferentRequestTypes() { + ClientRequest chat = ClientRequest.builder().query("q").requestType(RequestType.CHAT).build(); + ClientRequest sql = ClientRequest.builder().query("q").requestType(RequestType.SQL).build(); + ClientRequest mcp = + ClientRequest.builder().query("q").requestType(RequestType.MCP_QUERY).build(); + ClientRequest plan = + ClientRequest.builder().query("q").requestType(RequestType.MULTI_AGENT_PLAN).build(); + + assertThat(chat.getRequestType()).isEqualTo("chat"); + assertThat(sql.getRequestType()).isEqualTo("sql"); + assertThat(mcp.getRequestType()).isEqualTo("mcp-query"); + assertThat(plan.getRequestType()).isEqualTo("multi-agent-plan"); + } + + @Test + @DisplayName("should fail when query is null") + void shouldFailWhenQueryIsNull() { + assertThatThrownBy(() -> ClientRequest.builder().build()) + .isInstanceOf(NullPointerException.class); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ClientRequest r1 = ClientRequest.builder().query("q1").build(); + ClientRequest r2 = ClientRequest.builder().query("q1").build(); + ClientRequest r3 = ClientRequest.builder().query("q2").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ClientRequest request = + ClientRequest.builder().query("test query").llmProvider("openai").build(); + assertThat(request.toString()).contains("ClientRequest").contains("openai"); + } + } + + @Nested + @DisplayName("ClientResponse") + class ClientResponseTests { + + @Test + @DisplayName("should create successful response") + void shouldCreateSuccessfulResponse() { + PolicyInfo policyInfo = + new PolicyInfo(Arrays.asList("policy1"), null, "5ms", "tenant1", null, null); + + ClientResponse response = + new ClientResponse( + true, + "Response data", + "result text", + "plan-123", + false, + null, + policyInfo, + null, + null, + null); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.getData()).isEqualTo("Response data"); + assertThat(response.getResult()).isEqualTo("result text"); + assertThat(response.getPlanId()).isEqualTo("plan-123"); + assertThat(response.isBlocked()).isFalse(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getError()).isNull(); + } + + @Test + @DisplayName("should create blocked response") + void shouldCreateBlockedResponse() { + ClientResponse response = + new ClientResponse( + false, + null, + null, + null, + true, + "Request blocked by policy: pii-check", + null, + null, + null, + null); + + assertThat(response.isSuccess()).isFalse(); + assertThat(response.isBlocked()).isTrue(); + assertThat(response.getBlockReason()).isEqualTo("Request blocked by policy: pii-check"); + } + + @Test + @DisplayName("should create error response") + void shouldCreateErrorResponse() { + ClientResponse response = + new ClientResponse( + false, null, null, null, false, null, null, "Internal server error", null, null); + + assertThat(response.isSuccess()).isFalse(); + assertThat(response.getError()).isEqualTo("Internal server error"); + } + + @Test + @DisplayName("should extract blocking policy name - format 1") + void shouldExtractBlockingPolicyNameFormat1() { + ClientResponse response = + new ClientResponse( + false, + null, + null, + null, + true, + "Request blocked by policy: my-policy", + null, + null, + null, + null); + + assertThat(response.getBlockingPolicyName()).isEqualTo("my-policy"); + } + + @Test + @DisplayName("should extract blocking policy name - format 2") + void shouldExtractBlockingPolicyNameFormat2() { + ClientResponse response = + new ClientResponse( + false, + null, + null, + null, + true, + "Blocked by policy: another-policy", + null, + null, + null, + null); + + assertThat(response.getBlockingPolicyName()).isEqualTo("another-policy"); + } + + @Test + @DisplayName("should extract blocking policy name - bracket format") + void shouldExtractBlockingPolicyNameBracket() { + ClientResponse response = + new ClientResponse( + false, + null, + null, + null, + true, + "[policy-name] Detailed description", + null, + null, + null, + null); + + assertThat(response.getBlockingPolicyName()).isEqualTo("policy-name"); + } + + @Test + @DisplayName("should return full reason when no pattern matches") + void shouldReturnFullReasonWhenNoPattern() { + ClientResponse response = + new ClientResponse( + false, null, null, null, true, "Custom block reason", null, null, null, null); + + assertThat(response.getBlockingPolicyName()).isEqualTo("Custom block reason"); + } + + @Test + @DisplayName("should return null for null or empty block reason") + void shouldReturnNullForNullOrEmpty() { + ClientResponse nullReason = + new ClientResponse(true, null, null, null, false, null, null, null, null, null); + ClientResponse emptyReason = + new ClientResponse(true, null, null, null, false, "", null, null, null, null); + + assertThat(nullReason.getBlockingPolicyName()).isNull(); + assertThat(emptyReason.getBlockingPolicyName()).isNull(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"success\":true," + + "\"data\":{\"key\":\"value\"}," + + "\"blocked\":false," + + "\"policy_info\":{\"policies_evaluated\":[\"p1\"],\"processing_time\":\"2ms\"}" + + "}"; + + ClientResponse response = objectMapper.readValue(json, ClientResponse.class); + + assertThat(response.isSuccess()).isTrue(); + assertThat(response.isBlocked()).isFalse(); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()).containsExactly("p1"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ClientResponse r1 = + new ClientResponse(true, "data", null, null, false, null, null, null, null, null); + ClientResponse r2 = + new ClientResponse(true, "data", null, null, false, null, null, null, null, null); + ClientResponse r3 = + new ClientResponse(false, "data", null, null, false, null, null, null, null, null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ClientResponse response = + new ClientResponse(true, null, null, null, false, null, null, null, null, null); + assertThat(response.toString()).contains("ClientResponse"); + } + } + + @Nested + @DisplayName("MCPCheckInputRequest") + class MCPCheckInputRequestTests { + + @Test + @DisplayName("should create instance with connector type and statement only") + void shouldCreateWithBasicFields() { + MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT * FROM users"); + + assertThat(request.getConnectorType()).isEqualTo("postgres"); + assertThat(request.getStatement()).isEqualTo("SELECT * FROM users"); + assertThat(request.getOperation()).isEqualTo("execute"); + assertThat(request.getParameters()).isNull(); + } + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + Map params = Map.of("limit", 100); + MCPCheckInputRequest request = + new MCPCheckInputRequest("postgres", "UPDATE users SET name = $1", params, "execute"); + + assertThat(request.getConnectorType()).isEqualTo("postgres"); + assertThat(request.getStatement()).isEqualTo("UPDATE users SET name = $1"); + assertThat(request.getOperation()).isEqualTo("execute"); + assertThat(request.getParameters()).containsEntry("limit", 100); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + MCPCheckInputRequest request = + new MCPCheckInputRequest("postgres", "SELECT 1", Map.of("timeout", 30), "query"); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"connector_type\":\"postgres\""); + assertThat(json).contains("\"statement\":\"SELECT 1\""); + assertThat(json).contains("\"operation\":\"query\""); + assertThat(json).contains("\"parameters\""); + } + + @Test + @DisplayName("should omit null parameters in JSON") + void shouldOmitNullParametersInJson() throws Exception { + MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT 1"); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).doesNotContain("\"parameters\""); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + MCPCheckInputRequest r1 = new MCPCheckInputRequest("postgres", "SELECT 1"); + MCPCheckInputRequest r2 = new MCPCheckInputRequest("postgres", "SELECT 1"); + MCPCheckInputRequest r3 = new MCPCheckInputRequest("mysql", "SELECT 1"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + MCPCheckInputRequest request = new MCPCheckInputRequest("postgres", "SELECT 1"); + assertThat(request.toString()).contains("MCPCheckInputRequest"); + assertThat(request.toString()).contains("postgres"); + } + } + + @Nested + @DisplayName("MCPCheckInputResponse") + class MCPCheckInputResponseTests { + + @Test + @DisplayName("should create allowed response") + void shouldCreateAllowedResponse() { + MCPCheckInputResponse response = new MCPCheckInputResponse(true, null, 3, null); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getBlockReason()).isNull(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(3); + assertThat(response.getPolicyInfo()).isNull(); + } + + @Test + @DisplayName("should create blocked response") + void shouldCreateBlockedResponse() { + ConnectorPolicyInfo policyInfo = + new ConnectorPolicyInfo(3, true, "SQL injection detected", 0, 1, null); + MCPCheckInputResponse response = + new MCPCheckInputResponse(false, "SQL injection detected", 3, policyInfo); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("SQL injection detected"); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().isBlocked()).isTrue(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"allowed\":true," + + "\"policies_evaluated\":5," + + "\"policy_info\":{\"policies_evaluated\":5,\"blocked\":false," + + "\"redactions_applied\":0,\"processing_time_ms\":2}" + + "}"; + + MCPCheckInputResponse response = objectMapper.readValue(json, MCPCheckInputResponse.class); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(5); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().getPoliciesEvaluated()).isEqualTo(5); + } + + @Test + @DisplayName("should deserialize blocked response from JSON") + void shouldDeserializeBlockedResponseFromJson() throws Exception { + String json = + "{" + + "\"allowed\":false," + + "\"block_reason\":\"DROP TABLE not allowed\"," + + "\"policies_evaluated\":3," + + "\"policy_info\":{\"policies_evaluated\":3,\"blocked\":true," + + "\"block_reason\":\"DROP TABLE not allowed\"," + + "\"redactions_applied\":0,\"processing_time_ms\":1}" + + "}"; + + MCPCheckInputResponse response = objectMapper.readValue(json, MCPCheckInputResponse.class); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("DROP TABLE not allowed"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + MCPCheckInputResponse r1 = new MCPCheckInputResponse(true, null, 3, null); + MCPCheckInputResponse r2 = new MCPCheckInputResponse(true, null, 3, null); + MCPCheckInputResponse r3 = new MCPCheckInputResponse(false, "blocked", 3, null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + MCPCheckInputResponse response = new MCPCheckInputResponse(true, null, 3, null); + assertThat(response.toString()).contains("MCPCheckInputResponse"); + } + } + + @Nested + @DisplayName("MCPCheckOutputRequest") + class MCPCheckOutputRequestTests { + + @Test + @DisplayName("should create instance with connector type and response data only") + void shouldCreateWithBasicFields() { + List> data = List.of(Map.of("id", 1, "name", "Alice")); + MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); + + assertThat(request.getConnectorType()).isEqualTo("postgres"); + assertThat(request.getResponseData()).hasSize(1); + assertThat(request.getMessage()).isNull(); + assertThat(request.getMetadata()).isNull(); + assertThat(request.getRowCount()).isEqualTo(0); + } + + @Test + @DisplayName("should create instance with all fields") + void shouldCreateWithAllFields() { + List> data = + List.of(Map.of("id", 1, "name", "Alice"), Map.of("id", 2, "name", "Bob")); + Map metadata = Map.of("source", "analytics"); + MCPCheckOutputRequest request = + new MCPCheckOutputRequest("postgres", data, "Query completed", metadata, 2); + + assertThat(request.getConnectorType()).isEqualTo("postgres"); + assertThat(request.getResponseData()).hasSize(2); + assertThat(request.getMessage()).isEqualTo("Query completed"); + assertThat(request.getMetadata()).containsEntry("source", "analytics"); + assertThat(request.getRowCount()).isEqualTo(2); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = + new MCPCheckOutputRequest("postgres", data, "done", Map.of("key", "val"), 1); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"connector_type\":\"postgres\""); + assertThat(json).contains("\"response_data\""); + assertThat(json).contains("\"message\":\"done\""); + assertThat(json).contains("\"row_count\":1"); + } + + @Test + @DisplayName("should omit null fields in JSON") + void shouldOmitNullFieldsInJson() throws Exception { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).doesNotContain("\"message\""); + assertThat(json).doesNotContain("\"metadata\""); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest r1 = new MCPCheckOutputRequest("postgres", data); + MCPCheckOutputRequest r2 = new MCPCheckOutputRequest("postgres", data); + MCPCheckOutputRequest r3 = new MCPCheckOutputRequest("mysql", data); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + List> data = List.of(Map.of("id", 1)); + MCPCheckOutputRequest request = new MCPCheckOutputRequest("postgres", data); + assertThat(request.toString()).contains("MCPCheckOutputRequest"); + assertThat(request.toString()).contains("postgres"); + } + } + + @Nested + @DisplayName("MCPCheckOutputResponse") + class MCPCheckOutputResponseTests { + + @Test + @DisplayName("should create allowed response") + void shouldCreateAllowedResponse() { + MCPCheckOutputResponse response = new MCPCheckOutputResponse(true, null, null, 4, null, null); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getBlockReason()).isNull(); + assertThat(response.getRedactedData()).isNull(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(4); + assertThat(response.getExfiltrationInfo()).isNull(); + assertThat(response.getPolicyInfo()).isNull(); + } + + @Test + @DisplayName("should create blocked response with redacted data") + void shouldCreateBlockedResponseWithRedactedData() { + ConnectorPolicyInfo policyInfo = new ConnectorPolicyInfo(4, true, "PII detected", 1, 5, null); + List> redacted = List.of(Map.of("id", 1, "ssn", "***REDACTED***")); + MCPCheckOutputResponse response = + new MCPCheckOutputResponse(false, "PII detected", redacted, 4, null, policyInfo); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("PII detected"); + assertThat(response.getRedactedData()).isNotNull(); + assertThat(response.getPolicyInfo().getRedactionsApplied()).isEqualTo(1); + } + + @Test + @DisplayName("should create response with exfiltration info") + void shouldCreateResponseWithExfiltrationInfo() { + ExfiltrationCheckInfo exfilInfo = new ExfiltrationCheckInfo(10, 1000, 2048, 1048576, true); + MCPCheckOutputResponse response = + new MCPCheckOutputResponse(true, null, null, 3, exfilInfo, null); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getExfiltrationInfo()).isNotNull(); + assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(10); + assertThat(response.getExfiltrationInfo().getRowLimit()).isEqualTo(1000); + assertThat(response.getExfiltrationInfo().isWithinLimits()).isTrue(); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"allowed\":true," + + "\"policies_evaluated\":3," + + "\"exfiltration_info\":{\"rows_returned\":5,\"row_limit\":500," + + "\"bytes_returned\":1024,\"byte_limit\":524288,\"within_limits\":true}," + + "\"policy_info\":{\"policies_evaluated\":3,\"blocked\":false," + + "\"redactions_applied\":0,\"processing_time_ms\":2}" + + "}"; + + MCPCheckOutputResponse response = objectMapper.readValue(json, MCPCheckOutputResponse.class); + + assertThat(response.isAllowed()).isTrue(); + assertThat(response.getPoliciesEvaluated()).isEqualTo(3); + assertThat(response.getExfiltrationInfo()).isNotNull(); + assertThat(response.getExfiltrationInfo().getRowsReturned()).isEqualTo(5); + assertThat(response.getPolicyInfo()).isNotNull(); + } + + @Test + @DisplayName("should deserialize blocked response with redacted data from JSON") + void shouldDeserializeBlockedResponseFromJson() throws Exception { + String json = + "{" + + "\"allowed\":false," + + "\"block_reason\":\"PII content detected\"," + + "\"redacted_data\":[{\"id\":1,\"ssn\":\"***REDACTED***\"}]," + + "\"policies_evaluated\":4," + + "\"policy_info\":{\"policies_evaluated\":4,\"blocked\":true," + + "\"block_reason\":\"PII content detected\"," + + "\"redactions_applied\":1,\"processing_time_ms\":3}" + + "}"; + + MCPCheckOutputResponse response = objectMapper.readValue(json, MCPCheckOutputResponse.class); + + assertThat(response.isAllowed()).isFalse(); + assertThat(response.getBlockReason()).isEqualTo("PII content detected"); + assertThat(response.getRedactedData()).isNotNull(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + MCPCheckOutputResponse r1 = new MCPCheckOutputResponse(true, null, null, 3, null, null); + MCPCheckOutputResponse r2 = new MCPCheckOutputResponse(true, null, null, 3, null, null); + MCPCheckOutputResponse r3 = new MCPCheckOutputResponse(false, "blocked", null, 3, null, null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + MCPCheckOutputResponse response = new MCPCheckOutputResponse(true, null, null, 3, null, null); + assertThat(response.toString()).contains("MCPCheckOutputResponse"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/PlanTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/PlanTypesTest.java index 41b8681..e575812 100644 --- a/src/test/java/com/getaxonflow/sdk/types/PlanTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/PlanTypesTest.java @@ -15,40 +15,38 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; - -import java.util.Map; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; @DisplayName("Plan Types") class PlanTypesTest { - private ObjectMapper objectMapper; + private ObjectMapper objectMapper; - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } - @Test - @DisplayName("PlanRequest - should build with required fields") - void planRequestShouldBuildWithRequired() { - PlanRequest request = PlanRequest.builder() - .objective("Research AI governance") - .build(); + @Test + @DisplayName("PlanRequest - should build with required fields") + void planRequestShouldBuildWithRequired() { + PlanRequest request = PlanRequest.builder().objective("Research AI governance").build(); - assertThat(request.getObjective()).isEqualTo("Research AI governance"); - assertThat(request.getDomain()).isEqualTo("generic"); - } + assertThat(request.getObjective()).isEqualTo("Research AI governance"); + assertThat(request.getDomain()).isEqualTo("generic"); + } - @Test - @DisplayName("PlanRequest - should build with all fields") - void planRequestShouldBuildWithAllFields() { - PlanRequest request = PlanRequest.builder() + @Test + @DisplayName("PlanRequest - should build with all fields") + void planRequestShouldBuildWithAllFields() { + PlanRequest request = + PlanRequest.builder() .objective("Book a flight to Paris") .domain("travel") .userToken("user-123") @@ -58,27 +56,28 @@ void planRequestShouldBuildWithAllFields() { .parallel(true) .build(); - assertThat(request.getObjective()).isEqualTo("Book a flight to Paris"); - assertThat(request.getDomain()).isEqualTo("travel"); - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getContext()).containsEntry("budget", 1000); - assertThat(request.getConstraints()).containsEntry("maxStops", 1); - assertThat(request.getMaxSteps()).isEqualTo(5); - assertThat(request.getParallel()).isTrue(); - } - - @Test - @DisplayName("PlanRequest - should throw on null objective") - void planRequestShouldThrowOnNullObjective() { - assertThatThrownBy(() -> PlanRequest.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("objective"); - } - - @Test - @DisplayName("PlanStep - should deserialize from JSON") - void planStepShouldDeserialize() throws Exception { - String json = "{" + assertThat(request.getObjective()).isEqualTo("Book a flight to Paris"); + assertThat(request.getDomain()).isEqualTo("travel"); + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getContext()).containsEntry("budget", 1000); + assertThat(request.getConstraints()).containsEntry("maxStops", 1); + assertThat(request.getMaxSteps()).isEqualTo(5); + assertThat(request.getParallel()).isTrue(); + } + + @Test + @DisplayName("PlanRequest - should throw on null objective") + void planRequestShouldThrowOnNullObjective() { + assertThatThrownBy(() -> PlanRequest.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("objective"); + } + + @Test + @DisplayName("PlanStep - should deserialize from JSON") + void planStepShouldDeserialize() throws Exception { + String json = + "{" + "\"id\": \"step_001\"," + "\"name\": \"research-benefits\"," + "\"type\": \"llm-call\"," @@ -89,37 +88,39 @@ void planStepShouldDeserialize() throws Exception { + "\"estimated_time\": \"2s\"" + "}"; - PlanStep step = objectMapper.readValue(json, PlanStep.class); - - assertThat(step.getId()).isEqualTo("step_001"); - assertThat(step.getName()).isEqualTo("research-benefits"); - assertThat(step.getType()).isEqualTo("llm-call"); - assertThat(step.getDescription()).isEqualTo("Research the benefits of AI governance"); - assertThat(step.getDependsOn()).isEmpty(); - assertThat(step.getAgent()).isEqualTo("researcher"); - assertThat(step.getParameters()).containsEntry("topic", "governance"); - assertThat(step.getEstimatedTime()).isEqualTo("2s"); - } - - @Test - @DisplayName("PlanStep - should handle dependencies") - void planStepShouldHandleDependencies() throws Exception { - String json = "{" + PlanStep step = objectMapper.readValue(json, PlanStep.class); + + assertThat(step.getId()).isEqualTo("step_001"); + assertThat(step.getName()).isEqualTo("research-benefits"); + assertThat(step.getType()).isEqualTo("llm-call"); + assertThat(step.getDescription()).isEqualTo("Research the benefits of AI governance"); + assertThat(step.getDependsOn()).isEmpty(); + assertThat(step.getAgent()).isEqualTo("researcher"); + assertThat(step.getParameters()).containsEntry("topic", "governance"); + assertThat(step.getEstimatedTime()).isEqualTo("2s"); + } + + @Test + @DisplayName("PlanStep - should handle dependencies") + void planStepShouldHandleDependencies() throws Exception { + String json = + "{" + "\"id\": \"step_002\"," + "\"name\": \"summarize\"," + "\"type\": \"llm-call\"," + "\"depends_on\": [\"step_001\"]" + "}"; - PlanStep step = objectMapper.readValue(json, PlanStep.class); + PlanStep step = objectMapper.readValue(json, PlanStep.class); - assertThat(step.getDependsOn()).containsExactly("step_001"); - } + assertThat(step.getDependsOn()).containsExactly("step_001"); + } - @Test - @DisplayName("PlanResponse - should deserialize complete plan") - void planResponseShouldDeserialize() throws Exception { - String json = "{" + @Test + @DisplayName("PlanResponse - should deserialize complete plan") + void planResponseShouldDeserialize() throws Exception { + String json = + "{" + "\"plan_id\": \"plan_abc123\"," + "\"steps\": [" + "{" @@ -142,47 +143,41 @@ void planResponseShouldDeserialize() throws Exception { + "\"result\": \"Plan executed successfully\"" + "}"; - PlanResponse response = objectMapper.readValue(json, PlanResponse.class); - - assertThat(response.getPlanId()).isEqualTo("plan_abc123"); - assertThat(response.getSteps()).hasSize(2); - assertThat(response.getStepCount()).isEqualTo(2); - assertThat(response.getDomain()).isEqualTo("generic"); - assertThat(response.getComplexity()).isEqualTo(3); - assertThat(response.isParallel()).isTrue(); - assertThat(response.getEstimatedDuration()).isEqualTo("10s"); - assertThat(response.getStatus()).isEqualTo("completed"); - assertThat(response.getResult()).isEqualTo("Plan executed successfully"); - assertThat(response.isCompleted()).isTrue(); - assertThat(response.isFailed()).isFalse(); - } - - @Test - @DisplayName("PlanResponse - should detect failed status") - void planResponseShouldDetectFailed() throws Exception { - String json = "{" - + "\"plan_id\": \"plan_abc123\"," - + "\"steps\": []," - + "\"status\": \"failed\"" - + "}"; - - PlanResponse response = objectMapper.readValue(json, PlanResponse.class); - - assertThat(response.isFailed()).isTrue(); - assertThat(response.isCompleted()).isFalse(); - } - - @Test - @DisplayName("PlanResponse - should handle empty steps") - void planResponseShouldHandleEmptySteps() throws Exception { - String json = "{" - + "\"plan_id\": \"plan_abc123\"," - + "\"status\": \"pending\"" - + "}"; - - PlanResponse response = objectMapper.readValue(json, PlanResponse.class); - - assertThat(response.getSteps()).isEmpty(); - assertThat(response.getStepCount()).isEqualTo(0); - } + PlanResponse response = objectMapper.readValue(json, PlanResponse.class); + + assertThat(response.getPlanId()).isEqualTo("plan_abc123"); + assertThat(response.getSteps()).hasSize(2); + assertThat(response.getStepCount()).isEqualTo(2); + assertThat(response.getDomain()).isEqualTo("generic"); + assertThat(response.getComplexity()).isEqualTo(3); + assertThat(response.isParallel()).isTrue(); + assertThat(response.getEstimatedDuration()).isEqualTo("10s"); + assertThat(response.getStatus()).isEqualTo("completed"); + assertThat(response.getResult()).isEqualTo("Plan executed successfully"); + assertThat(response.isCompleted()).isTrue(); + assertThat(response.isFailed()).isFalse(); + } + + @Test + @DisplayName("PlanResponse - should detect failed status") + void planResponseShouldDetectFailed() throws Exception { + String json = + "{" + "\"plan_id\": \"plan_abc123\"," + "\"steps\": []," + "\"status\": \"failed\"" + "}"; + + PlanResponse response = objectMapper.readValue(json, PlanResponse.class); + + assertThat(response.isFailed()).isTrue(); + assertThat(response.isCompleted()).isFalse(); + } + + @Test + @DisplayName("PlanResponse - should handle empty steps") + void planResponseShouldHandleEmptySteps() throws Exception { + String json = "{" + "\"plan_id\": \"plan_abc123\"," + "\"status\": \"pending\"" + "}"; + + PlanResponse response = objectMapper.readValue(json, PlanResponse.class); + + assertThat(response.getSteps()).isEmpty(); + assertThat(response.getStepCount()).isEqualTo(0); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/PolicyApprovalTest.java b/src/test/java/com/getaxonflow/sdk/types/PolicyApprovalTest.java index dd25f12..96918b6 100644 --- a/src/test/java/com/getaxonflow/sdk/types/PolicyApprovalTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/PolicyApprovalTest.java @@ -15,49 +15,47 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.time.Instant; import java.util.List; import java.util.Map; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("PolicyApproval Types") class PolicyApprovalTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); - } - - @Test - @DisplayName("PolicyApprovalRequest - should build with required fields") - void requestShouldBuildWithRequiredFields() { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() - .userToken("user-123") - .query("What is the weather?") - .build(); - - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getQuery()).isEqualTo("What is the weather?"); - assertThat(request.getDataSources()).isEmpty(); - assertThat(request.getContext()).isEmpty(); - } - - @Test - @DisplayName("PolicyApprovalRequest - should build with all fields") - void requestShouldBuildWithAllFields() { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false); + } + + @Test + @DisplayName("PolicyApprovalRequest - should build with required fields") + void requestShouldBuildWithRequiredFields() { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder().userToken("user-123").query("What is the weather?").build(); + + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getQuery()).isEqualTo("What is the weather?"); + assertThat(request.getDataSources()).isEmpty(); + assertThat(request.getContext()).isEmpty(); + } + + @Test + @DisplayName("PolicyApprovalRequest - should build with all fields") + void requestShouldBuildWithAllFields() { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() .userToken("user-123") .query("Analyze customer data") .dataSources(List.of("crm", "analytics")) @@ -65,53 +63,51 @@ void requestShouldBuildWithAllFields() { .clientId("client-456") .build(); - assertThat(request.getUserToken()).isEqualTo("user-123"); - assertThat(request.getQuery()).isEqualTo("Analyze customer data"); - assertThat(request.getDataSources()).containsExactly("crm", "analytics"); - assertThat(request.getContext()).containsEntry("department", "sales"); - assertThat(request.getClientId()).isEqualTo("client-456"); - } - - @Test - @DisplayName("PolicyApprovalRequest - should throw on null user token") - void requestShouldThrowOnNullUserToken() { - assertThatThrownBy(() -> PolicyApprovalRequest.builder() - .query("test") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("userToken"); - } - - @Test - @DisplayName("PolicyApprovalRequest - should throw on null query") - void requestShouldThrowOnNullQuery() { - assertThatThrownBy(() -> PolicyApprovalRequest.builder() - .userToken("user-123") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("query"); - } - - @Test - @DisplayName("PolicyApprovalRequest - should serialize to JSON") - void requestShouldSerializeToJson() throws Exception { - PolicyApprovalRequest request = PolicyApprovalRequest.builder() + assertThat(request.getUserToken()).isEqualTo("user-123"); + assertThat(request.getQuery()).isEqualTo("Analyze customer data"); + assertThat(request.getDataSources()).containsExactly("crm", "analytics"); + assertThat(request.getContext()).containsEntry("department", "sales"); + assertThat(request.getClientId()).isEqualTo("client-456"); + } + + @Test + @DisplayName("PolicyApprovalRequest - should throw on null user token") + void requestShouldThrowOnNullUserToken() { + assertThatThrownBy(() -> PolicyApprovalRequest.builder().query("test").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("userToken"); + } + + @Test + @DisplayName("PolicyApprovalRequest - should throw on null query") + void requestShouldThrowOnNullQuery() { + assertThatThrownBy(() -> PolicyApprovalRequest.builder().userToken("user-123").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("query"); + } + + @Test + @DisplayName("PolicyApprovalRequest - should serialize to JSON") + void requestShouldSerializeToJson() throws Exception { + PolicyApprovalRequest request = + PolicyApprovalRequest.builder() .userToken("user-123") .query("test query") .dataSources(List.of("source1")) .build(); - String json = objectMapper.writeValueAsString(request); + String json = objectMapper.writeValueAsString(request); - assertThat(json).contains("\"user_token\":\"user-123\""); - assertThat(json).contains("\"query\":\"test query\""); - assertThat(json).contains("\"data_sources\":[\"source1\"]"); - } + assertThat(json).contains("\"user_token\":\"user-123\""); + assertThat(json).contains("\"query\":\"test query\""); + assertThat(json).contains("\"data_sources\":[\"source1\"]"); + } - @Test - @DisplayName("PolicyApprovalResult - should deserialize approved response") - void resultShouldDeserializeApproved() throws Exception { - String json = "{" + @Test + @DisplayName("PolicyApprovalResult - should deserialize approved response") + void resultShouldDeserializeApproved() throws Exception { + String json = + "{" + "\"context_id\": \"ctx_abc123\"," + "\"approved\": true," + "\"approved_data\": {\"filtered\": \"data\"}," @@ -120,67 +116,73 @@ void resultShouldDeserializeApproved() throws Exception { + "\"processing_time\": \"3.14ms\"" + "}"; - PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); - - assertThat(result.getContextId()).isEqualTo("ctx_abc123"); - assertThat(result.isApproved()).isTrue(); - assertThat(result.getApprovedData()).containsEntry("filtered", "data"); - assertThat(result.getPolicies()).containsExactly("policy1", "policy2"); - assertThat(result.getExpiresAt()).isNotNull(); - assertThat(result.getProcessingTime()).isEqualTo("3.14ms"); - assertThat(result.getBlockReason()).isNull(); - } - - @Test - @DisplayName("PolicyApprovalResult - should deserialize blocked response") - void resultShouldDeserializeBlocked() throws Exception { - String json = "{" + PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); + + assertThat(result.getContextId()).isEqualTo("ctx_abc123"); + assertThat(result.isApproved()).isTrue(); + assertThat(result.getApprovedData()).containsEntry("filtered", "data"); + assertThat(result.getPolicies()).containsExactly("policy1", "policy2"); + assertThat(result.getExpiresAt()).isNotNull(); + assertThat(result.getProcessingTime()).isEqualTo("3.14ms"); + assertThat(result.getBlockReason()).isNull(); + } + + @Test + @DisplayName("PolicyApprovalResult - should deserialize blocked response") + void resultShouldDeserializeBlocked() throws Exception { + String json = + "{" + "\"context_id\": \"ctx_abc123\"," + "\"approved\": false," + "\"block_reason\": \"Request blocked by policy: pii_detection\"," + "\"policies\": [\"pii_detection\"]" + "}"; - PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); + PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); - assertThat(result.isApproved()).isFalse(); - assertThat(result.getBlockReason()).isEqualTo("Request blocked by policy: pii_detection"); - assertThat(result.getBlockingPolicyName()).isEqualTo("pii_detection"); - } + assertThat(result.isApproved()).isFalse(); + assertThat(result.getBlockReason()).isEqualTo("Request blocked by policy: pii_detection"); + assertThat(result.getBlockingPolicyName()).isEqualTo("pii_detection"); + } - @Test - @DisplayName("PolicyApprovalResult - should detect expired approval") - void resultShouldDetectExpired() throws Exception { - String json = "{" + @Test + @DisplayName("PolicyApprovalResult - should detect expired approval") + void resultShouldDetectExpired() throws Exception { + String json = + "{" + "\"context_id\": \"ctx_abc123\"," + "\"approved\": true," + "\"expires_at\": \"2020-01-01T00:00:00Z\"" + "}"; - PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); - - assertThat(result.isExpired()).isTrue(); - } - - @Test - @DisplayName("PolicyApprovalResult - should detect non-expired approval") - void resultShouldDetectNonExpired() throws Exception { - Instant futureTime = Instant.now().plusSeconds(300); - String json = String.format("{" - + "\"context_id\": \"ctx_abc123\"," - + "\"approved\": true," - + "\"expires_at\": \"%s\"" - + "}", futureTime.toString()); - - PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); - - assertThat(result.isExpired()).isFalse(); - } - - @Test - @DisplayName("PolicyApprovalResult - should handle rate limit info") - void resultShouldHandleRateLimitInfo() throws Exception { - String json = "{" + PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); + + assertThat(result.isExpired()).isTrue(); + } + + @Test + @DisplayName("PolicyApprovalResult - should detect non-expired approval") + void resultShouldDetectNonExpired() throws Exception { + Instant futureTime = Instant.now().plusSeconds(300); + String json = + String.format( + "{" + + "\"context_id\": \"ctx_abc123\"," + + "\"approved\": true," + + "\"expires_at\": \"%s\"" + + "}", + futureTime.toString()); + + PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); + + assertThat(result.isExpired()).isFalse(); + } + + @Test + @DisplayName("PolicyApprovalResult - should handle rate limit info") + void resultShouldHandleRateLimitInfo() throws Exception { + String json = + "{" + "\"context_id\": \"ctx_abc123\"," + "\"approved\": true," + "\"rate_limit_info\": {" @@ -190,10 +192,10 @@ void resultShouldHandleRateLimitInfo() throws Exception { + "}" + "}"; - PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); + PolicyApprovalResult result = objectMapper.readValue(json, PolicyApprovalResult.class); - assertThat(result.getRateLimitInfo()).isNotNull(); - assertThat(result.getRateLimitInfo().getLimit()).isEqualTo(100); - assertThat(result.getRateLimitInfo().getRemaining()).isEqualTo(95); - } + assertThat(result.getRateLimitInfo()).isNotNull(); + assertThat(result.getRateLimitInfo().getLimit()).isEqualTo(100); + assertThat(result.getRateLimitInfo().getRemaining()).isEqualTo(95); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/RollbackPlanResponseTest.java b/src/test/java/com/getaxonflow/sdk/types/RollbackPlanResponseTest.java index 470b922..b486e79 100644 --- a/src/test/java/com/getaxonflow/sdk/types/RollbackPlanResponseTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/RollbackPlanResponseTest.java @@ -15,92 +15,94 @@ */ package com.getaxonflow.sdk.types; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; @DisplayName("RollbackPlanResponse") class RollbackPlanResponseTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } - - @Test - @DisplayName("should construct with all fields") - void shouldConstructWithAllFields() { - RollbackPlanResponse response = new RollbackPlanResponse("plan-123", 2, 3, "rolled_back"); - - assertThat(response.getPlanId()).isEqualTo("plan-123"); - assertThat(response.getVersion()).isEqualTo(2); - assertThat(response.getPreviousVersion()).isEqualTo(3); - assertThat(response.getStatus()).isEqualTo("rolled_back"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"plan_id\":\"plan-456\",\"version\":1,\"previous_version\":3,\"status\":\"rolled_back\"}"; - - RollbackPlanResponse response = objectMapper.readValue(json, RollbackPlanResponse.class); - - assertThat(response.getPlanId()).isEqualTo("plan-456"); - assertThat(response.getVersion()).isEqualTo(1); - assertThat(response.getPreviousVersion()).isEqualTo(3); - assertThat(response.getStatus()).isEqualTo("rolled_back"); - } - - @Test - @DisplayName("should serialize to JSON") - void shouldSerializeToJson() throws Exception { - RollbackPlanResponse response = new RollbackPlanResponse("plan-789", 2, 4, "rolled_back"); - - String json = objectMapper.writeValueAsString(response); - - assertThat(json).contains("\"plan_id\":\"plan-789\""); - assertThat(json).contains("\"version\":2"); - assertThat(json).contains("\"previous_version\":4"); - assertThat(json).contains("\"status\":\"rolled_back\""); - } - - @Test - @DisplayName("should ignore unknown properties") - void shouldIgnoreUnknownProperties() throws Exception { - String json = "{\"plan_id\":\"plan-1\",\"version\":1,\"previous_version\":2," + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + @Test + @DisplayName("should construct with all fields") + void shouldConstructWithAllFields() { + RollbackPlanResponse response = new RollbackPlanResponse("plan-123", 2, 3, "rolled_back"); + + assertThat(response.getPlanId()).isEqualTo("plan-123"); + assertThat(response.getVersion()).isEqualTo(2); + assertThat(response.getPreviousVersion()).isEqualTo(3); + assertThat(response.getStatus()).isEqualTo("rolled_back"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{\"plan_id\":\"plan-456\",\"version\":1,\"previous_version\":3,\"status\":\"rolled_back\"}"; + + RollbackPlanResponse response = objectMapper.readValue(json, RollbackPlanResponse.class); + + assertThat(response.getPlanId()).isEqualTo("plan-456"); + assertThat(response.getVersion()).isEqualTo(1); + assertThat(response.getPreviousVersion()).isEqualTo(3); + assertThat(response.getStatus()).isEqualTo("rolled_back"); + } + + @Test + @DisplayName("should serialize to JSON") + void shouldSerializeToJson() throws Exception { + RollbackPlanResponse response = new RollbackPlanResponse("plan-789", 2, 4, "rolled_back"); + + String json = objectMapper.writeValueAsString(response); + + assertThat(json).contains("\"plan_id\":\"plan-789\""); + assertThat(json).contains("\"version\":2"); + assertThat(json).contains("\"previous_version\":4"); + assertThat(json).contains("\"status\":\"rolled_back\""); + } + + @Test + @DisplayName("should ignore unknown properties") + void shouldIgnoreUnknownProperties() throws Exception { + String json = + "{\"plan_id\":\"plan-1\",\"version\":1,\"previous_version\":2," + "\"status\":\"rolled_back\",\"unknown_field\":\"value\"}"; - RollbackPlanResponse response = objectMapper.readValue(json, RollbackPlanResponse.class); - - assertThat(response.getPlanId()).isEqualTo("plan-1"); - } - - @Test - @DisplayName("equals and hashCode") - void equalsAndHashCode() { - RollbackPlanResponse r1 = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); - RollbackPlanResponse r2 = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); - RollbackPlanResponse r3 = new RollbackPlanResponse("plan-2", 2, 3, "rolled_back"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("toString contains all fields") - void toStringShouldContainAllFields() { - RollbackPlanResponse response = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); - String str = response.toString(); - - assertThat(str).contains("plan-1"); - assertThat(str).contains("2"); - assertThat(str).contains("3"); - assertThat(str).contains("rolled_back"); - } + RollbackPlanResponse response = objectMapper.readValue(json, RollbackPlanResponse.class); + + assertThat(response.getPlanId()).isEqualTo("plan-1"); + } + + @Test + @DisplayName("equals and hashCode") + void equalsAndHashCode() { + RollbackPlanResponse r1 = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); + RollbackPlanResponse r2 = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); + RollbackPlanResponse r3 = new RollbackPlanResponse("plan-2", 2, 3, "rolled_back"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("toString contains all fields") + void toStringShouldContainAllFields() { + RollbackPlanResponse response = new RollbackPlanResponse("plan-1", 2, 3, "rolled_back"); + String str = response.toString(); + + assertThat(str).contains("plan-1"); + assertThat(str).contains("2"); + assertThat(str).contains("3"); + assertThat(str).contains("rolled_back"); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceTypesTest.java index 45508c2..fef750a 100644 --- a/src/test/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/codegovernance/CodeGovernanceTypesTest.java @@ -15,946 +15,930 @@ */ package com.getaxonflow.sdk.types.codegovernance; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import java.time.Instant; +import java.util.Arrays; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import java.time.Instant; -import java.util.Arrays; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Comprehensive tests for code governance types. - */ +/** Comprehensive tests for code governance types. */ @DisplayName("Code Governance Types") class CodeGovernanceTypesTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - objectMapper.registerModule(new JavaTimeModule()); - } - - @Nested - @DisplayName("FileAction") - class FileActionTests { - - @Test - @DisplayName("should have correct values") - void shouldHaveCorrectValues() { - assertThat(FileAction.CREATE.getValue()).isEqualTo("create"); - assertThat(FileAction.UPDATE.getValue()).isEqualTo("update"); - assertThat(FileAction.DELETE.getValue()).isEqualTo("delete"); - } - - @Test - @DisplayName("should parse from value") - void shouldParseFromValue() { - assertThat(FileAction.fromValue("create")).isEqualTo(FileAction.CREATE); - assertThat(FileAction.fromValue("update")).isEqualTo(FileAction.UPDATE); - assertThat(FileAction.fromValue("delete")).isEqualTo(FileAction.DELETE); - } - - @Test - @DisplayName("should parse case insensitively") - void shouldParseCaseInsensitively() { - assertThat(FileAction.fromValue("CREATE")).isEqualTo(FileAction.CREATE); - assertThat(FileAction.fromValue("Update")).isEqualTo(FileAction.UPDATE); - } - - @Test - @DisplayName("should throw for unknown value") - void shouldThrowForUnknownValue() { - assertThatThrownBy(() -> FileAction.fromValue("unknown")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown file action"); - } - } - - @Nested - @DisplayName("GitProviderType") - class GitProviderTypeTests { - - @Test - @DisplayName("should have correct values") - void shouldHaveCorrectValues() { - assertThat(GitProviderType.GITHUB.getValue()).isEqualTo("github"); - assertThat(GitProviderType.GITLAB.getValue()).isEqualTo("gitlab"); - assertThat(GitProviderType.BITBUCKET.getValue()).isEqualTo("bitbucket"); - } - - @Test - @DisplayName("should parse from value") - void shouldParseFromValue() { - assertThat(GitProviderType.fromValue("github")).isEqualTo(GitProviderType.GITHUB); - assertThat(GitProviderType.fromValue("gitlab")).isEqualTo(GitProviderType.GITLAB); - assertThat(GitProviderType.fromValue("bitbucket")).isEqualTo(GitProviderType.BITBUCKET); - } - - @Test - @DisplayName("should parse case insensitively") - void shouldParseCaseInsensitively() { - assertThat(GitProviderType.fromValue("GITHUB")).isEqualTo(GitProviderType.GITHUB); - assertThat(GitProviderType.fromValue("GitLab")).isEqualTo(GitProviderType.GITLAB); - } - - @Test - @DisplayName("should throw for unknown value") - void shouldThrowForUnknownValue() { - assertThatThrownBy(() -> GitProviderType.fromValue("unknown")) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("Unknown Git provider type"); - } - } - - @Nested - @DisplayName("CodeFile") - class CodeFileTests { - - @Test - @DisplayName("should create with all fields") - void shouldCreateWithAllFields() { - CodeFile file = new CodeFile( - "src/main/java/Test.java", - "public class Test {}", - "java", - FileAction.CREATE - ); - - assertThat(file.getPath()).isEqualTo("src/main/java/Test.java"); - assertThat(file.getContent()).isEqualTo("public class Test {}"); - assertThat(file.getLanguage()).isEqualTo("java"); - assertThat(file.getAction()).isEqualTo(FileAction.CREATE); - } - - @Test - @DisplayName("should build using builder") - void shouldBuildUsingBuilder() { - CodeFile file = CodeFile.builder() - .path("src/test.py") - .content("print('hello')") - .language("python") - .action(FileAction.UPDATE) - .build(); - - assertThat(file.getPath()).isEqualTo("src/test.py"); - assertThat(file.getContent()).isEqualTo("print('hello')"); - assertThat(file.getLanguage()).isEqualTo("python"); - assertThat(file.getAction()).isEqualTo(FileAction.UPDATE); - } - - @Test - @DisplayName("should fail when path is null") - void shouldFailWhenPathIsNull() { - assertThatThrownBy(() -> new CodeFile(null, "content", "java", FileAction.CREATE)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("path is required"); - } - - @Test - @DisplayName("should fail when content is null") - void shouldFailWhenContentIsNull() { - assertThatThrownBy(() -> new CodeFile("path", null, "java", FileAction.CREATE)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("content is required"); - } - - @Test - @DisplayName("should fail when action is null") - void shouldFailWhenActionIsNull() { - assertThatThrownBy(() -> new CodeFile("path", "content", "java", null)) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("action is required"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"path\":\"test.go\"," + - "\"content\":\"package main\"," + - "\"language\":\"go\"," + - "\"action\":\"create\"" + - "}"; - - CodeFile file = objectMapper.readValue(json, CodeFile.class); - - assertThat(file.getPath()).isEqualTo("test.go"); - assertThat(file.getLanguage()).isEqualTo("go"); - assertThat(file.getAction()).isEqualTo(FileAction.CREATE); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - CodeFile f1 = new CodeFile("p", "c", "l", FileAction.CREATE); - CodeFile f2 = new CodeFile("p", "c", "l", FileAction.CREATE); - CodeFile f3 = new CodeFile("p2", "c", "l", FileAction.CREATE); - - assertThat(f1).isEqualTo(f2); - assertThat(f1.hashCode()).isEqualTo(f2.hashCode()); - assertThat(f1).isNotEqualTo(f3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - CodeFile file = new CodeFile("test.java", "content", "java", FileAction.CREATE); - assertThat(file.toString()).contains("CodeFile").contains("test.java"); - } - } - - @Nested - @DisplayName("GitProviderInfo") - class GitProviderInfoTests { - - @Test - @DisplayName("should create with type") - void shouldCreateWithType() { - GitProviderInfo info = new GitProviderInfo(GitProviderType.GITHUB); - assertThat(info.getType()).isEqualTo(GitProviderType.GITHUB); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{\"type\":\"gitlab\"}"; - GitProviderInfo info = objectMapper.readValue(json, GitProviderInfo.class); - assertThat(info.getType()).isEqualTo(GitProviderType.GITLAB); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - GitProviderInfo i1 = new GitProviderInfo(GitProviderType.GITHUB); - GitProviderInfo i2 = new GitProviderInfo(GitProviderType.GITHUB); - GitProviderInfo i3 = new GitProviderInfo(GitProviderType.GITLAB); - - assertThat(i1).isEqualTo(i2); - assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); - assertThat(i1).isNotEqualTo(i3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - GitProviderInfo info = new GitProviderInfo(GitProviderType.BITBUCKET); - assertThat(info.toString()).contains("GitProviderInfo").contains("BITBUCKET"); - } - } - - @Nested - @DisplayName("ListPRsOptions") - class ListPRsOptionsTests { - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - ListPRsOptions options = ListPRsOptions.builder() - .limit(10) - .offset(20) - .state("open") - .build(); - - assertThat(options.getLimit()).isEqualTo(10); - assertThat(options.getOffset()).isEqualTo(20); - assertThat(options.getState()).isEqualTo("open"); - } - - @Test - @DisplayName("should build with partial fields") - void shouldBuildWithPartialFields() { - ListPRsOptions options = ListPRsOptions.builder() - .limit(5) - .build(); - - assertThat(options.getLimit()).isEqualTo(5); - assertThat(options.getOffset()).isNull(); - assertThat(options.getState()).isNull(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ListPRsOptions o1 = ListPRsOptions.builder().limit(10).state("open").build(); - ListPRsOptions o2 = ListPRsOptions.builder().limit(10).state("open").build(); - ListPRsOptions o3 = ListPRsOptions.builder().limit(20).state("open").build(); - - assertThat(o1).isEqualTo(o2); - assertThat(o1.hashCode()).isEqualTo(o2.hashCode()); - assertThat(o1).isNotEqualTo(o3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ListPRsOptions options = ListPRsOptions.builder().limit(50).build(); - assertThat(options.toString()).contains("ListPRsOptions").contains("50"); - } - } - - @Nested - @DisplayName("PRRecord") - class PRRecordTests { - - @Test - @DisplayName("should create with all fields") - void shouldCreateWithAllFields() { - Instant now = Instant.now(); - PRRecord record = new PRRecord( - "pr-123", 42, "https://github.com/owner/repo/pull/42", - "Add feature", "open", "owner", "repo", - "feature-branch", "main", 5, 0, 1, - now, null, "user@test.com", "github" - ); - - assertThat(record.getId()).isEqualTo("pr-123"); - assertThat(record.getPrNumber()).isEqualTo(42); - assertThat(record.getPrUrl()).isEqualTo("https://github.com/owner/repo/pull/42"); - assertThat(record.getTitle()).isEqualTo("Add feature"); - assertThat(record.getState()).isEqualTo("open"); - assertThat(record.getOwner()).isEqualTo("owner"); - assertThat(record.getRepo()).isEqualTo("repo"); - assertThat(record.getHeadBranch()).isEqualTo("feature-branch"); - assertThat(record.getBaseBranch()).isEqualTo("main"); - assertThat(record.getFilesCount()).isEqualTo(5); - assertThat(record.getSecretsDetected()).isEqualTo(0); - assertThat(record.getUnsafePatterns()).isEqualTo(1); - assertThat(record.getCreatedAt()).isEqualTo(now); - assertThat(record.getClosedAt()).isNull(); - assertThat(record.getCreatedBy()).isEqualTo("user@test.com"); - assertThat(record.getProviderType()).isEqualTo("github"); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"id\":\"pr-456\"," + - "\"pr_number\":123," + - "\"pr_url\":\"https://gitlab.com/owner/repo/-/merge_requests/123\"," + - "\"title\":\"Fix bug\"," + - "\"state\":\"merged\"," + - "\"owner\":\"myorg\"," + - "\"repo\":\"myrepo\"," + - "\"head_branch\":\"fix/bug\"," + - "\"base_branch\":\"develop\"," + - "\"files_count\":3," + - "\"secrets_detected\":0," + - "\"unsafe_patterns\":0," + - "\"provider_type\":\"gitlab\"" + - "}"; - - PRRecord record = objectMapper.readValue(json, PRRecord.class); - - assertThat(record.getId()).isEqualTo("pr-456"); - assertThat(record.getPrNumber()).isEqualTo(123); - assertThat(record.getTitle()).isEqualTo("Fix bug"); - assertThat(record.getState()).isEqualTo("merged"); - assertThat(record.getProviderType()).isEqualTo("gitlab"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - PRRecord r1 = new PRRecord("id1", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); - PRRecord r2 = new PRRecord("id1", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); - PRRecord r3 = new PRRecord("id2", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - PRRecord record = new PRRecord("id", 99, "url", "My PR", "open", "owner", "repo", "h", "b", 2, 0, 0, null, null, "u", "g"); - String str = record.toString(); - assertThat(str).contains("PRRecord"); - assertThat(str).contains("My PR"); - assertThat(str).contains("99"); - } - } - - @Nested - @DisplayName("CreatePRRequest") - class CreatePRRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - CreatePRRequest request = CreatePRRequest.builder() - .owner("owner") - .repo("repo") - .title("My PR") - .build(); - - assertThat(request.getOwner()).isEqualTo("owner"); - assertThat(request.getRepo()).isEqualTo("repo"); - assertThat(request.getTitle()).isEqualTo("My PR"); - assertThat(request.getFiles()).isEmpty(); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - List files = Arrays.asList( - new CodeFile("test.java", "content", "java", FileAction.CREATE) - ); - List policies = Arrays.asList("security", "style"); - - CreatePRRequest request = CreatePRRequest.builder() - .owner("myorg") - .repo("myrepo") - .title("Feature: Add login") - .description("Adds login functionality") - .baseBranch("main") - .branchName("feature/login") - .draft(true) - .files(files) - .agentRequestId("agent-123") - .model("gpt-4") - .policiesChecked(policies) - .secretsDetected(0) - .unsafePatterns(0) - .build(); - - assertThat(request.getOwner()).isEqualTo("myorg"); - assertThat(request.getRepo()).isEqualTo("myrepo"); - assertThat(request.getTitle()).isEqualTo("Feature: Add login"); - assertThat(request.getDescription()).isEqualTo("Adds login functionality"); - assertThat(request.getBaseBranch()).isEqualTo("main"); - assertThat(request.getBranchName()).isEqualTo("feature/login"); - assertThat(request.isDraft()).isTrue(); - assertThat(request.getFiles()).hasSize(1); - assertThat(request.getAgentRequestId()).isEqualTo("agent-123"); - assertThat(request.getModel()).isEqualTo("gpt-4"); - assertThat(request.getPoliciesChecked()).containsExactly("security", "style"); - assertThat(request.getSecretsDetected()).isEqualTo(0); - assertThat(request.getUnsafePatterns()).isEqualTo(0); - } - - @Test - @DisplayName("should fail when owner is null") - void shouldFailWhenOwnerIsNull() { - assertThatThrownBy(() -> CreatePRRequest.builder() - .repo("repo") - .title("title") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("owner is required"); - } - - @Test - @DisplayName("should fail when repo is null") - void shouldFailWhenRepoIsNull() { - assertThatThrownBy(() -> CreatePRRequest.builder() - .owner("owner") - .title("title") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("repo is required"); - } - - @Test - @DisplayName("should fail when title is null") - void shouldFailWhenTitleIsNull() { - assertThatThrownBy(() -> CreatePRRequest.builder() - .owner("owner") - .repo("repo") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("title is required"); - } - - @Test - @DisplayName("should handle null files list") - void shouldHandleNullFilesList() { - CreatePRRequest request = CreatePRRequest.builder() - .owner("o") - .repo("r") - .title("t") - .files(null) - .build(); - - assertThat(request.getFiles()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - CreatePRRequest r1 = CreatePRRequest.builder().owner("o").repo("r").title("t").build(); - CreatePRRequest r2 = CreatePRRequest.builder().owner("o").repo("r").title("t").build(); - CreatePRRequest r3 = CreatePRRequest.builder().owner("o2").repo("r").title("t").build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - CreatePRRequest request = CreatePRRequest.builder() - .owner("myowner") - .repo("myrepo") - .title("My title") - .draft(true) - .build(); - String str = request.toString(); - assertThat(str).contains("CreatePRRequest"); - assertThat(str).contains("myowner"); - assertThat(str).contains("myrepo"); - } - } - - @Nested - @DisplayName("CreatePRResponse") - class CreatePRResponseTests { - - @Test - @DisplayName("should create with all fields") - void shouldCreateWithAllFields() { - Instant now = Instant.now(); - CreatePRResponse response = new CreatePRResponse( - "pr-id-123", 99, "https://github.com/o/r/pull/99", - "open", "feature-branch", now - ); - - assertThat(response.getPrId()).isEqualTo("pr-id-123"); - assertThat(response.getPrNumber()).isEqualTo(99); - assertThat(response.getPrUrl()).isEqualTo("https://github.com/o/r/pull/99"); - assertThat(response.getState()).isEqualTo("open"); - assertThat(response.getHeadBranch()).isEqualTo("feature-branch"); - assertThat(response.getCreatedAt()).isEqualTo(now); - } - - @Test - @DisplayName("should deserialize from JSON") - void shouldDeserializeFromJson() throws Exception { - String json = "{" + - "\"pr_id\":\"abc123\"," + - "\"pr_number\":42," + - "\"pr_url\":\"https://gitlab.com/merge/42\"," + - "\"state\":\"opened\"," + - "\"head_branch\":\"my-branch\"" + - "}"; - - CreatePRResponse response = objectMapper.readValue(json, CreatePRResponse.class); - - assertThat(response.getPrId()).isEqualTo("abc123"); - assertThat(response.getPrNumber()).isEqualTo(42); - assertThat(response.getState()).isEqualTo("opened"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - CreatePRResponse r1 = new CreatePRResponse("id1", 1, "url", "open", "b", null); - CreatePRResponse r2 = new CreatePRResponse("id1", 1, "url", "open", "b", null); - CreatePRResponse r3 = new CreatePRResponse("id2", 1, "url", "open", "b", null); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - CreatePRResponse response = new CreatePRResponse("id", 77, "url", "merged", "branch", null); - String str = response.toString(); - assertThat(str).contains("CreatePRResponse"); - assertThat(str).contains("77"); - } - } - - @Nested - @DisplayName("ConfigureGitProviderRequest") - class ConfigureGitProviderRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - ConfigureGitProviderRequest request = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - ConfigureGitProviderRequest request = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("ghp_token123") - .baseUrl("https://github.example.com") - .appId(12345) - .installationId(67890) - .privateKey("-----BEGIN PRIVATE KEY-----") - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); - assertThat(request.getToken()).isEqualTo("ghp_token123"); - assertThat(request.getBaseUrl()).isEqualTo("https://github.example.com"); - assertThat(request.getAppId()).isEqualTo(12345); - assertThat(request.getInstallationId()).isEqualTo(67890); - assertThat(request.getPrivateKey()).isEqualTo("-----BEGIN PRIVATE KEY-----"); - } - - @Test - @DisplayName("should fail when type is null") - void shouldFailWhenTypeIsNull() { - assertThatThrownBy(() -> ConfigureGitProviderRequest.builder() - .token("token") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("type is required"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ConfigureGitProviderRequest r1 = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("t") - .build(); - ConfigureGitProviderRequest r2 = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("t") - .build(); - ConfigureGitProviderRequest r3 = ConfigureGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .token("t") - .build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ConfigureGitProviderRequest request = ConfigureGitProviderRequest.builder() - .type(GitProviderType.BITBUCKET) - .baseUrl("https://bitbucket.org") - .build(); - String str = request.toString(); - assertThat(str).contains("ConfigureGitProviderRequest"); - assertThat(str).contains("BITBUCKET"); - } - } - - @Nested - @DisplayName("ValidateGitProviderRequest") - class ValidateGitProviderRequestTests { - - @Test - @DisplayName("should build with required fields") - void shouldBuildWithRequiredFields() { - ValidateGitProviderRequest request = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITLAB); - } - - @Test - @DisplayName("should build with all fields") - void shouldBuildWithAllFields() { - ValidateGitProviderRequest request = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .token("glpat-xxx") - .baseUrl("https://gitlab.example.com") - .appId(111) - .installationId(222) - .privateKey("key") - .build(); - - assertThat(request.getType()).isEqualTo(GitProviderType.GITLAB); - assertThat(request.getToken()).isEqualTo("glpat-xxx"); - assertThat(request.getBaseUrl()).isEqualTo("https://gitlab.example.com"); - assertThat(request.getAppId()).isEqualTo(111); - assertThat(request.getInstallationId()).isEqualTo(222); - assertThat(request.getPrivateKey()).isEqualTo("key"); - } - - @Test - @DisplayName("should fail when type is null") - void shouldFailWhenTypeIsNull() { - assertThatThrownBy(() -> ValidateGitProviderRequest.builder() - .token("token") - .build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("type is required"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ValidateGitProviderRequest r1 = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .token("t") - .build(); - ValidateGitProviderRequest r2 = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .token("t") - .build(); - ValidateGitProviderRequest r3 = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITHUB) - .token("t") - .build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ValidateGitProviderRequest request = ValidateGitProviderRequest.builder() - .type(GitProviderType.GITLAB) - .baseUrl("https://gitlab.com") - .build(); - String str = request.toString(); - assertThat(str).contains("ValidateGitProviderRequest"); - assertThat(str).contains("GITLAB"); - } - } - - @Nested - @DisplayName("ExportOptions") - class ExportOptionsTests { - - @Test - @DisplayName("should create with defaults") - void shouldCreateWithDefaults() { - ExportOptions options = new ExportOptions(); - - assertThat(options.getFormat()).isEqualTo("json"); - assertThat(options.getStartDate()).isNull(); - assertThat(options.getEndDate()).isNull(); - assertThat(options.getState()).isNull(); - } - - @Test - @DisplayName("should set and get all fields") - void shouldSetAndGetAllFields() { - Instant start = Instant.parse("2026-01-01T00:00:00Z"); - Instant end = Instant.parse("2026-01-31T23:59:59Z"); - - ExportOptions options = new ExportOptions() - .setFormat("csv") - .setStartDate(start) - .setEndDate(end) - .setState("merged"); - - assertThat(options.getFormat()).isEqualTo("csv"); - assertThat(options.getStartDate()).isEqualTo(start); - assertThat(options.getEndDate()).isEqualTo(end); - assertThat(options.getState()).isEqualTo("merged"); - } - - @Test - @DisplayName("should support fluent API") - void shouldSupportFluentApi() { - ExportOptions options = new ExportOptions() - .setFormat("json") - .setState("open"); - - assertThat(options.getFormat()).isEqualTo("json"); - assertThat(options.getState()).isEqualTo("open"); - } - } - - @Nested - @DisplayName("ListPRsResponse") - class ListPRsResponseTests { - - @Test - @DisplayName("should create with prs and count") - void shouldCreateWithPrsAndCount() { - PRRecord pr = new PRRecord("id", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); - List prs = Arrays.asList(pr); - - ListPRsResponse response = new ListPRsResponse(prs, 1); - - assertThat(response.getPrs()).hasSize(1); - assertThat(response.getCount()).isEqualTo(1); - } - - @Test - @DisplayName("should handle null prs list") - void shouldHandleNullPrsList() { - ListPRsResponse response = new ListPRsResponse(null, 0); - assertThat(response.getPrs()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ListPRsResponse r1 = new ListPRsResponse(null, 5); - ListPRsResponse r2 = new ListPRsResponse(null, 5); - ListPRsResponse r3 = new ListPRsResponse(null, 10); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ListPRsResponse response = new ListPRsResponse(null, 3); - assertThat(response.toString()).contains("ListPRsResponse").contains("3"); - } - } - - @Nested - @DisplayName("ListGitProvidersResponse") - class ListGitProvidersResponseTests { - - @Test - @DisplayName("should create with providers and count") - void shouldCreateWithProvidersAndCount() { - GitProviderInfo info = new GitProviderInfo(GitProviderType.GITHUB); - List providers = Arrays.asList(info); - - ListGitProvidersResponse response = new ListGitProvidersResponse(providers, 1); - - assertThat(response.getProviders()).hasSize(1); - assertThat(response.getCount()).isEqualTo(1); - } - - @Test - @DisplayName("should handle null providers list") - void shouldHandleNullProvidersList() { - ListGitProvidersResponse response = new ListGitProvidersResponse(null, 0); - assertThat(response.getProviders()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ListGitProvidersResponse r1 = new ListGitProvidersResponse(null, 2); - ListGitProvidersResponse r2 = new ListGitProvidersResponse(null, 2); - ListGitProvidersResponse r3 = new ListGitProvidersResponse(null, 4); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ListGitProvidersResponse response = new ListGitProvidersResponse(null, 1); - assertThat(response.toString()).contains("ListGitProvidersResponse"); - } - } - - @Nested - @DisplayName("ConfigureGitProviderResponse") - class ConfigureGitProviderResponseTests { - - @Test - @DisplayName("should create with all fields") - void shouldCreateWithAllFields() { - ConfigureGitProviderResponse response = new ConfigureGitProviderResponse( - "Provider configured successfully", "github" - ); - - assertThat(response.getMessage()).isEqualTo("Provider configured successfully"); - assertThat(response.getType()).isEqualTo("github"); - } - - @Test - @DisplayName("should handle null values with defaults") - void shouldHandleNullValues() { - ConfigureGitProviderResponse response = new ConfigureGitProviderResponse(null, null); - - assertThat(response.getMessage()).isEmpty(); - assertThat(response.getType()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ConfigureGitProviderResponse r1 = new ConfigureGitProviderResponse("msg", "type"); - ConfigureGitProviderResponse r2 = new ConfigureGitProviderResponse("msg", "type"); - ConfigureGitProviderResponse r3 = new ConfigureGitProviderResponse("msg2", "type"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ConfigureGitProviderResponse response = new ConfigureGitProviderResponse("OK", "gitlab"); - assertThat(response.toString()).contains("ConfigureGitProviderResponse"); - } - } - - @Nested - @DisplayName("ValidateGitProviderResponse") - class ValidateGitProviderResponseTests { - - @Test - @DisplayName("should create valid response") - void shouldCreateValidResponse() { - ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, "Validation successful"); - - assertThat(response.isValid()).isTrue(); - assertThat(response.getMessage()).isEqualTo("Validation successful"); - } - - @Test - @DisplayName("should create invalid response") - void shouldCreateInvalidResponse() { - ValidateGitProviderResponse response = new ValidateGitProviderResponse(false, "Invalid token"); - - assertThat(response.isValid()).isFalse(); - assertThat(response.getMessage()).isEqualTo("Invalid token"); - } - - @Test - @DisplayName("should handle null message") - void shouldHandleNullMessage() { - ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, null); - assertThat(response.getMessage()).isEmpty(); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - ValidateGitProviderResponse r1 = new ValidateGitProviderResponse(true, "msg"); - ValidateGitProviderResponse r2 = new ValidateGitProviderResponse(true, "msg"); - ValidateGitProviderResponse r3 = new ValidateGitProviderResponse(false, "msg"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("should have toString") - void shouldHaveToString() { - ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, "OK"); - assertThat(response.toString()).contains("ValidateGitProviderResponse").contains("true"); - } + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + objectMapper.registerModule(new JavaTimeModule()); + } + + @Nested + @DisplayName("FileAction") + class FileActionTests { + + @Test + @DisplayName("should have correct values") + void shouldHaveCorrectValues() { + assertThat(FileAction.CREATE.getValue()).isEqualTo("create"); + assertThat(FileAction.UPDATE.getValue()).isEqualTo("update"); + assertThat(FileAction.DELETE.getValue()).isEqualTo("delete"); + } + + @Test + @DisplayName("should parse from value") + void shouldParseFromValue() { + assertThat(FileAction.fromValue("create")).isEqualTo(FileAction.CREATE); + assertThat(FileAction.fromValue("update")).isEqualTo(FileAction.UPDATE); + assertThat(FileAction.fromValue("delete")).isEqualTo(FileAction.DELETE); + } + + @Test + @DisplayName("should parse case insensitively") + void shouldParseCaseInsensitively() { + assertThat(FileAction.fromValue("CREATE")).isEqualTo(FileAction.CREATE); + assertThat(FileAction.fromValue("Update")).isEqualTo(FileAction.UPDATE); + } + + @Test + @DisplayName("should throw for unknown value") + void shouldThrowForUnknownValue() { + assertThatThrownBy(() -> FileAction.fromValue("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown file action"); + } + } + + @Nested + @DisplayName("GitProviderType") + class GitProviderTypeTests { + + @Test + @DisplayName("should have correct values") + void shouldHaveCorrectValues() { + assertThat(GitProviderType.GITHUB.getValue()).isEqualTo("github"); + assertThat(GitProviderType.GITLAB.getValue()).isEqualTo("gitlab"); + assertThat(GitProviderType.BITBUCKET.getValue()).isEqualTo("bitbucket"); + } + + @Test + @DisplayName("should parse from value") + void shouldParseFromValue() { + assertThat(GitProviderType.fromValue("github")).isEqualTo(GitProviderType.GITHUB); + assertThat(GitProviderType.fromValue("gitlab")).isEqualTo(GitProviderType.GITLAB); + assertThat(GitProviderType.fromValue("bitbucket")).isEqualTo(GitProviderType.BITBUCKET); + } + + @Test + @DisplayName("should parse case insensitively") + void shouldParseCaseInsensitively() { + assertThat(GitProviderType.fromValue("GITHUB")).isEqualTo(GitProviderType.GITHUB); + assertThat(GitProviderType.fromValue("GitLab")).isEqualTo(GitProviderType.GITLAB); + } + + @Test + @DisplayName("should throw for unknown value") + void shouldThrowForUnknownValue() { + assertThatThrownBy(() -> GitProviderType.fromValue("unknown")) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("Unknown Git provider type"); + } + } + + @Nested + @DisplayName("CodeFile") + class CodeFileTests { + + @Test + @DisplayName("should create with all fields") + void shouldCreateWithAllFields() { + CodeFile file = + new CodeFile( + "src/main/java/Test.java", "public class Test {}", "java", FileAction.CREATE); + + assertThat(file.getPath()).isEqualTo("src/main/java/Test.java"); + assertThat(file.getContent()).isEqualTo("public class Test {}"); + assertThat(file.getLanguage()).isEqualTo("java"); + assertThat(file.getAction()).isEqualTo(FileAction.CREATE); + } + + @Test + @DisplayName("should build using builder") + void shouldBuildUsingBuilder() { + CodeFile file = + CodeFile.builder() + .path("src/test.py") + .content("print('hello')") + .language("python") + .action(FileAction.UPDATE) + .build(); + + assertThat(file.getPath()).isEqualTo("src/test.py"); + assertThat(file.getContent()).isEqualTo("print('hello')"); + assertThat(file.getLanguage()).isEqualTo("python"); + assertThat(file.getAction()).isEqualTo(FileAction.UPDATE); + } + + @Test + @DisplayName("should fail when path is null") + void shouldFailWhenPathIsNull() { + assertThatThrownBy(() -> new CodeFile(null, "content", "java", FileAction.CREATE)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("path is required"); + } + + @Test + @DisplayName("should fail when content is null") + void shouldFailWhenContentIsNull() { + assertThatThrownBy(() -> new CodeFile("path", null, "java", FileAction.CREATE)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("content is required"); + } + + @Test + @DisplayName("should fail when action is null") + void shouldFailWhenActionIsNull() { + assertThatThrownBy(() -> new CodeFile("path", "content", "java", null)) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("action is required"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"path\":\"test.go\"," + + "\"content\":\"package main\"," + + "\"language\":\"go\"," + + "\"action\":\"create\"" + + "}"; + + CodeFile file = objectMapper.readValue(json, CodeFile.class); + + assertThat(file.getPath()).isEqualTo("test.go"); + assertThat(file.getLanguage()).isEqualTo("go"); + assertThat(file.getAction()).isEqualTo(FileAction.CREATE); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + CodeFile f1 = new CodeFile("p", "c", "l", FileAction.CREATE); + CodeFile f2 = new CodeFile("p", "c", "l", FileAction.CREATE); + CodeFile f3 = new CodeFile("p2", "c", "l", FileAction.CREATE); + + assertThat(f1).isEqualTo(f2); + assertThat(f1.hashCode()).isEqualTo(f2.hashCode()); + assertThat(f1).isNotEqualTo(f3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + CodeFile file = new CodeFile("test.java", "content", "java", FileAction.CREATE); + assertThat(file.toString()).contains("CodeFile").contains("test.java"); + } + } + + @Nested + @DisplayName("GitProviderInfo") + class GitProviderInfoTests { + + @Test + @DisplayName("should create with type") + void shouldCreateWithType() { + GitProviderInfo info = new GitProviderInfo(GitProviderType.GITHUB); + assertThat(info.getType()).isEqualTo(GitProviderType.GITHUB); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = "{\"type\":\"gitlab\"}"; + GitProviderInfo info = objectMapper.readValue(json, GitProviderInfo.class); + assertThat(info.getType()).isEqualTo(GitProviderType.GITLAB); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + GitProviderInfo i1 = new GitProviderInfo(GitProviderType.GITHUB); + GitProviderInfo i2 = new GitProviderInfo(GitProviderType.GITHUB); + GitProviderInfo i3 = new GitProviderInfo(GitProviderType.GITLAB); + + assertThat(i1).isEqualTo(i2); + assertThat(i1.hashCode()).isEqualTo(i2.hashCode()); + assertThat(i1).isNotEqualTo(i3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + GitProviderInfo info = new GitProviderInfo(GitProviderType.BITBUCKET); + assertThat(info.toString()).contains("GitProviderInfo").contains("BITBUCKET"); + } + } + + @Nested + @DisplayName("ListPRsOptions") + class ListPRsOptionsTests { + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + ListPRsOptions options = ListPRsOptions.builder().limit(10).offset(20).state("open").build(); + + assertThat(options.getLimit()).isEqualTo(10); + assertThat(options.getOffset()).isEqualTo(20); + assertThat(options.getState()).isEqualTo("open"); + } + + @Test + @DisplayName("should build with partial fields") + void shouldBuildWithPartialFields() { + ListPRsOptions options = ListPRsOptions.builder().limit(5).build(); + + assertThat(options.getLimit()).isEqualTo(5); + assertThat(options.getOffset()).isNull(); + assertThat(options.getState()).isNull(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ListPRsOptions o1 = ListPRsOptions.builder().limit(10).state("open").build(); + ListPRsOptions o2 = ListPRsOptions.builder().limit(10).state("open").build(); + ListPRsOptions o3 = ListPRsOptions.builder().limit(20).state("open").build(); + + assertThat(o1).isEqualTo(o2); + assertThat(o1.hashCode()).isEqualTo(o2.hashCode()); + assertThat(o1).isNotEqualTo(o3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ListPRsOptions options = ListPRsOptions.builder().limit(50).build(); + assertThat(options.toString()).contains("ListPRsOptions").contains("50"); + } + } + + @Nested + @DisplayName("PRRecord") + class PRRecordTests { + + @Test + @DisplayName("should create with all fields") + void shouldCreateWithAllFields() { + Instant now = Instant.now(); + PRRecord record = + new PRRecord( + "pr-123", + 42, + "https://github.com/owner/repo/pull/42", + "Add feature", + "open", + "owner", + "repo", + "feature-branch", + "main", + 5, + 0, + 1, + now, + null, + "user@test.com", + "github"); + + assertThat(record.getId()).isEqualTo("pr-123"); + assertThat(record.getPrNumber()).isEqualTo(42); + assertThat(record.getPrUrl()).isEqualTo("https://github.com/owner/repo/pull/42"); + assertThat(record.getTitle()).isEqualTo("Add feature"); + assertThat(record.getState()).isEqualTo("open"); + assertThat(record.getOwner()).isEqualTo("owner"); + assertThat(record.getRepo()).isEqualTo("repo"); + assertThat(record.getHeadBranch()).isEqualTo("feature-branch"); + assertThat(record.getBaseBranch()).isEqualTo("main"); + assertThat(record.getFilesCount()).isEqualTo(5); + assertThat(record.getSecretsDetected()).isEqualTo(0); + assertThat(record.getUnsafePatterns()).isEqualTo(1); + assertThat(record.getCreatedAt()).isEqualTo(now); + assertThat(record.getClosedAt()).isNull(); + assertThat(record.getCreatedBy()).isEqualTo("user@test.com"); + assertThat(record.getProviderType()).isEqualTo("github"); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"id\":\"pr-456\"," + + "\"pr_number\":123," + + "\"pr_url\":\"https://gitlab.com/owner/repo/-/merge_requests/123\"," + + "\"title\":\"Fix bug\"," + + "\"state\":\"merged\"," + + "\"owner\":\"myorg\"," + + "\"repo\":\"myrepo\"," + + "\"head_branch\":\"fix/bug\"," + + "\"base_branch\":\"develop\"," + + "\"files_count\":3," + + "\"secrets_detected\":0," + + "\"unsafe_patterns\":0," + + "\"provider_type\":\"gitlab\"" + + "}"; + + PRRecord record = objectMapper.readValue(json, PRRecord.class); + + assertThat(record.getId()).isEqualTo("pr-456"); + assertThat(record.getPrNumber()).isEqualTo(123); + assertThat(record.getTitle()).isEqualTo("Fix bug"); + assertThat(record.getState()).isEqualTo("merged"); + assertThat(record.getProviderType()).isEqualTo("gitlab"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + PRRecord r1 = + new PRRecord( + "id1", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); + PRRecord r2 = + new PRRecord( + "id1", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); + PRRecord r3 = + new PRRecord( + "id2", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + PRRecord record = + new PRRecord( + "id", 99, "url", "My PR", "open", "owner", "repo", "h", "b", 2, 0, 0, null, null, "u", + "g"); + String str = record.toString(); + assertThat(str).contains("PRRecord"); + assertThat(str).contains("My PR"); + assertThat(str).contains("99"); + } + } + + @Nested + @DisplayName("CreatePRRequest") + class CreatePRRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + CreatePRRequest request = + CreatePRRequest.builder().owner("owner").repo("repo").title("My PR").build(); + + assertThat(request.getOwner()).isEqualTo("owner"); + assertThat(request.getRepo()).isEqualTo("repo"); + assertThat(request.getTitle()).isEqualTo("My PR"); + assertThat(request.getFiles()).isEmpty(); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + List files = + Arrays.asList(new CodeFile("test.java", "content", "java", FileAction.CREATE)); + List policies = Arrays.asList("security", "style"); + + CreatePRRequest request = + CreatePRRequest.builder() + .owner("myorg") + .repo("myrepo") + .title("Feature: Add login") + .description("Adds login functionality") + .baseBranch("main") + .branchName("feature/login") + .draft(true) + .files(files) + .agentRequestId("agent-123") + .model("gpt-4") + .policiesChecked(policies) + .secretsDetected(0) + .unsafePatterns(0) + .build(); + + assertThat(request.getOwner()).isEqualTo("myorg"); + assertThat(request.getRepo()).isEqualTo("myrepo"); + assertThat(request.getTitle()).isEqualTo("Feature: Add login"); + assertThat(request.getDescription()).isEqualTo("Adds login functionality"); + assertThat(request.getBaseBranch()).isEqualTo("main"); + assertThat(request.getBranchName()).isEqualTo("feature/login"); + assertThat(request.isDraft()).isTrue(); + assertThat(request.getFiles()).hasSize(1); + assertThat(request.getAgentRequestId()).isEqualTo("agent-123"); + assertThat(request.getModel()).isEqualTo("gpt-4"); + assertThat(request.getPoliciesChecked()).containsExactly("security", "style"); + assertThat(request.getSecretsDetected()).isEqualTo(0); + assertThat(request.getUnsafePatterns()).isEqualTo(0); + } + + @Test + @DisplayName("should fail when owner is null") + void shouldFailWhenOwnerIsNull() { + assertThatThrownBy(() -> CreatePRRequest.builder().repo("repo").title("title").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("owner is required"); + } + + @Test + @DisplayName("should fail when repo is null") + void shouldFailWhenRepoIsNull() { + assertThatThrownBy(() -> CreatePRRequest.builder().owner("owner").title("title").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("repo is required"); + } + + @Test + @DisplayName("should fail when title is null") + void shouldFailWhenTitleIsNull() { + assertThatThrownBy(() -> CreatePRRequest.builder().owner("owner").repo("repo").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("title is required"); + } + + @Test + @DisplayName("should handle null files list") + void shouldHandleNullFilesList() { + CreatePRRequest request = + CreatePRRequest.builder().owner("o").repo("r").title("t").files(null).build(); + + assertThat(request.getFiles()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + CreatePRRequest r1 = CreatePRRequest.builder().owner("o").repo("r").title("t").build(); + CreatePRRequest r2 = CreatePRRequest.builder().owner("o").repo("r").title("t").build(); + CreatePRRequest r3 = CreatePRRequest.builder().owner("o2").repo("r").title("t").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + CreatePRRequest request = + CreatePRRequest.builder() + .owner("myowner") + .repo("myrepo") + .title("My title") + .draft(true) + .build(); + String str = request.toString(); + assertThat(str).contains("CreatePRRequest"); + assertThat(str).contains("myowner"); + assertThat(str).contains("myrepo"); + } + } + + @Nested + @DisplayName("CreatePRResponse") + class CreatePRResponseTests { + + @Test + @DisplayName("should create with all fields") + void shouldCreateWithAllFields() { + Instant now = Instant.now(); + CreatePRResponse response = + new CreatePRResponse( + "pr-id-123", 99, "https://github.com/o/r/pull/99", "open", "feature-branch", now); + + assertThat(response.getPrId()).isEqualTo("pr-id-123"); + assertThat(response.getPrNumber()).isEqualTo(99); + assertThat(response.getPrUrl()).isEqualTo("https://github.com/o/r/pull/99"); + assertThat(response.getState()).isEqualTo("open"); + assertThat(response.getHeadBranch()).isEqualTo("feature-branch"); + assertThat(response.getCreatedAt()).isEqualTo(now); + } + + @Test + @DisplayName("should deserialize from JSON") + void shouldDeserializeFromJson() throws Exception { + String json = + "{" + + "\"pr_id\":\"abc123\"," + + "\"pr_number\":42," + + "\"pr_url\":\"https://gitlab.com/merge/42\"," + + "\"state\":\"opened\"," + + "\"head_branch\":\"my-branch\"" + + "}"; + + CreatePRResponse response = objectMapper.readValue(json, CreatePRResponse.class); + + assertThat(response.getPrId()).isEqualTo("abc123"); + assertThat(response.getPrNumber()).isEqualTo(42); + assertThat(response.getState()).isEqualTo("opened"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + CreatePRResponse r1 = new CreatePRResponse("id1", 1, "url", "open", "b", null); + CreatePRResponse r2 = new CreatePRResponse("id1", 1, "url", "open", "b", null); + CreatePRResponse r3 = new CreatePRResponse("id2", 1, "url", "open", "b", null); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + CreatePRResponse response = new CreatePRResponse("id", 77, "url", "merged", "branch", null); + String str = response.toString(); + assertThat(str).contains("CreatePRResponse"); + assertThat(str).contains("77"); + } + } + + @Nested + @DisplayName("ConfigureGitProviderRequest") + class ConfigureGitProviderRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + ConfigureGitProviderRequest request = + ConfigureGitProviderRequest.builder().type(GitProviderType.GITHUB).build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + ConfigureGitProviderRequest request = + ConfigureGitProviderRequest.builder() + .type(GitProviderType.GITHUB) + .token("ghp_token123") + .baseUrl("https://github.example.com") + .appId(12345) + .installationId(67890) + .privateKey("-----BEGIN PRIVATE KEY-----") + .build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITHUB); + assertThat(request.getToken()).isEqualTo("ghp_token123"); + assertThat(request.getBaseUrl()).isEqualTo("https://github.example.com"); + assertThat(request.getAppId()).isEqualTo(12345); + assertThat(request.getInstallationId()).isEqualTo(67890); + assertThat(request.getPrivateKey()).isEqualTo("-----BEGIN PRIVATE KEY-----"); + } + + @Test + @DisplayName("should fail when type is null") + void shouldFailWhenTypeIsNull() { + assertThatThrownBy(() -> ConfigureGitProviderRequest.builder().token("token").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("type is required"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ConfigureGitProviderRequest r1 = + ConfigureGitProviderRequest.builder().type(GitProviderType.GITHUB).token("t").build(); + ConfigureGitProviderRequest r2 = + ConfigureGitProviderRequest.builder().type(GitProviderType.GITHUB).token("t").build(); + ConfigureGitProviderRequest r3 = + ConfigureGitProviderRequest.builder().type(GitProviderType.GITLAB).token("t").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ConfigureGitProviderRequest request = + ConfigureGitProviderRequest.builder() + .type(GitProviderType.BITBUCKET) + .baseUrl("https://bitbucket.org") + .build(); + String str = request.toString(); + assertThat(str).contains("ConfigureGitProviderRequest"); + assertThat(str).contains("BITBUCKET"); + } + } + + @Nested + @DisplayName("ValidateGitProviderRequest") + class ValidateGitProviderRequestTests { + + @Test + @DisplayName("should build with required fields") + void shouldBuildWithRequiredFields() { + ValidateGitProviderRequest request = + ValidateGitProviderRequest.builder().type(GitProviderType.GITLAB).build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITLAB); + } + + @Test + @DisplayName("should build with all fields") + void shouldBuildWithAllFields() { + ValidateGitProviderRequest request = + ValidateGitProviderRequest.builder() + .type(GitProviderType.GITLAB) + .token("glpat-xxx") + .baseUrl("https://gitlab.example.com") + .appId(111) + .installationId(222) + .privateKey("key") + .build(); + + assertThat(request.getType()).isEqualTo(GitProviderType.GITLAB); + assertThat(request.getToken()).isEqualTo("glpat-xxx"); + assertThat(request.getBaseUrl()).isEqualTo("https://gitlab.example.com"); + assertThat(request.getAppId()).isEqualTo(111); + assertThat(request.getInstallationId()).isEqualTo(222); + assertThat(request.getPrivateKey()).isEqualTo("key"); + } + + @Test + @DisplayName("should fail when type is null") + void shouldFailWhenTypeIsNull() { + assertThatThrownBy(() -> ValidateGitProviderRequest.builder().token("token").build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("type is required"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ValidateGitProviderRequest r1 = + ValidateGitProviderRequest.builder().type(GitProviderType.GITLAB).token("t").build(); + ValidateGitProviderRequest r2 = + ValidateGitProviderRequest.builder().type(GitProviderType.GITLAB).token("t").build(); + ValidateGitProviderRequest r3 = + ValidateGitProviderRequest.builder().type(GitProviderType.GITHUB).token("t").build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ValidateGitProviderRequest request = + ValidateGitProviderRequest.builder() + .type(GitProviderType.GITLAB) + .baseUrl("https://gitlab.com") + .build(); + String str = request.toString(); + assertThat(str).contains("ValidateGitProviderRequest"); + assertThat(str).contains("GITLAB"); + } + } + + @Nested + @DisplayName("ExportOptions") + class ExportOptionsTests { + + @Test + @DisplayName("should create with defaults") + void shouldCreateWithDefaults() { + ExportOptions options = new ExportOptions(); + + assertThat(options.getFormat()).isEqualTo("json"); + assertThat(options.getStartDate()).isNull(); + assertThat(options.getEndDate()).isNull(); + assertThat(options.getState()).isNull(); + } + + @Test + @DisplayName("should set and get all fields") + void shouldSetAndGetAllFields() { + Instant start = Instant.parse("2026-01-01T00:00:00Z"); + Instant end = Instant.parse("2026-01-31T23:59:59Z"); + + ExportOptions options = + new ExportOptions() + .setFormat("csv") + .setStartDate(start) + .setEndDate(end) + .setState("merged"); + + assertThat(options.getFormat()).isEqualTo("csv"); + assertThat(options.getStartDate()).isEqualTo(start); + assertThat(options.getEndDate()).isEqualTo(end); + assertThat(options.getState()).isEqualTo("merged"); + } + + @Test + @DisplayName("should support fluent API") + void shouldSupportFluentApi() { + ExportOptions options = new ExportOptions().setFormat("json").setState("open"); + + assertThat(options.getFormat()).isEqualTo("json"); + assertThat(options.getState()).isEqualTo("open"); + } + } + + @Nested + @DisplayName("ListPRsResponse") + class ListPRsResponseTests { + + @Test + @DisplayName("should create with prs and count") + void shouldCreateWithPrsAndCount() { + PRRecord pr = + new PRRecord("id", 1, "url", "t", "s", "o", "r", "h", "b", 1, 0, 0, null, null, "u", "g"); + List prs = Arrays.asList(pr); + + ListPRsResponse response = new ListPRsResponse(prs, 1); + + assertThat(response.getPrs()).hasSize(1); + assertThat(response.getCount()).isEqualTo(1); + } + + @Test + @DisplayName("should handle null prs list") + void shouldHandleNullPrsList() { + ListPRsResponse response = new ListPRsResponse(null, 0); + assertThat(response.getPrs()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ListPRsResponse r1 = new ListPRsResponse(null, 5); + ListPRsResponse r2 = new ListPRsResponse(null, 5); + ListPRsResponse r3 = new ListPRsResponse(null, 10); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ListPRsResponse response = new ListPRsResponse(null, 3); + assertThat(response.toString()).contains("ListPRsResponse").contains("3"); + } + } + + @Nested + @DisplayName("ListGitProvidersResponse") + class ListGitProvidersResponseTests { + + @Test + @DisplayName("should create with providers and count") + void shouldCreateWithProvidersAndCount() { + GitProviderInfo info = new GitProviderInfo(GitProviderType.GITHUB); + List providers = Arrays.asList(info); + + ListGitProvidersResponse response = new ListGitProvidersResponse(providers, 1); + + assertThat(response.getProviders()).hasSize(1); + assertThat(response.getCount()).isEqualTo(1); + } + + @Test + @DisplayName("should handle null providers list") + void shouldHandleNullProvidersList() { + ListGitProvidersResponse response = new ListGitProvidersResponse(null, 0); + assertThat(response.getProviders()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ListGitProvidersResponse r1 = new ListGitProvidersResponse(null, 2); + ListGitProvidersResponse r2 = new ListGitProvidersResponse(null, 2); + ListGitProvidersResponse r3 = new ListGitProvidersResponse(null, 4); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ListGitProvidersResponse response = new ListGitProvidersResponse(null, 1); + assertThat(response.toString()).contains("ListGitProvidersResponse"); + } + } + + @Nested + @DisplayName("ConfigureGitProviderResponse") + class ConfigureGitProviderResponseTests { + + @Test + @DisplayName("should create with all fields") + void shouldCreateWithAllFields() { + ConfigureGitProviderResponse response = + new ConfigureGitProviderResponse("Provider configured successfully", "github"); + + assertThat(response.getMessage()).isEqualTo("Provider configured successfully"); + assertThat(response.getType()).isEqualTo("github"); + } + + @Test + @DisplayName("should handle null values with defaults") + void shouldHandleNullValues() { + ConfigureGitProviderResponse response = new ConfigureGitProviderResponse(null, null); + + assertThat(response.getMessage()).isEmpty(); + assertThat(response.getType()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ConfigureGitProviderResponse r1 = new ConfigureGitProviderResponse("msg", "type"); + ConfigureGitProviderResponse r2 = new ConfigureGitProviderResponse("msg", "type"); + ConfigureGitProviderResponse r3 = new ConfigureGitProviderResponse("msg2", "type"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ConfigureGitProviderResponse response = new ConfigureGitProviderResponse("OK", "gitlab"); + assertThat(response.toString()).contains("ConfigureGitProviderResponse"); + } + } + + @Nested + @DisplayName("ValidateGitProviderResponse") + class ValidateGitProviderResponseTests { + + @Test + @DisplayName("should create valid response") + void shouldCreateValidResponse() { + ValidateGitProviderResponse response = + new ValidateGitProviderResponse(true, "Validation successful"); + + assertThat(response.isValid()).isTrue(); + assertThat(response.getMessage()).isEqualTo("Validation successful"); + } + + @Test + @DisplayName("should create invalid response") + void shouldCreateInvalidResponse() { + ValidateGitProviderResponse response = + new ValidateGitProviderResponse(false, "Invalid token"); + + assertThat(response.isValid()).isFalse(); + assertThat(response.getMessage()).isEqualTo("Invalid token"); + } + + @Test + @DisplayName("should handle null message") + void shouldHandleNullMessage() { + ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, null); + assertThat(response.getMessage()).isEmpty(); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + ValidateGitProviderResponse r1 = new ValidateGitProviderResponse(true, "msg"); + ValidateGitProviderResponse r2 = new ValidateGitProviderResponse(true, "msg"); + ValidateGitProviderResponse r3 = new ValidateGitProviderResponse(false, "msg"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("should have toString") + void shouldHaveToString() { + ValidateGitProviderResponse response = new ValidateGitProviderResponse(true, "OK"); + assertThat(response.toString()).contains("ValidateGitProviderResponse").contains("true"); } + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/webhook/WebhookTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/webhook/WebhookTypesTest.java index 8caa2a2..1399dcb 100644 --- a/src/test/java/com/getaxonflow/sdk/types/webhook/WebhookTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/webhook/WebhookTypesTest.java @@ -15,179 +15,198 @@ */ package com.getaxonflow.sdk.types.webhook; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.getaxonflow.sdk.types.webhook.WebhookTypes.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.util.Arrays; import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for Webhook types (Feature 7). - */ +/** Tests for Webhook types (Feature 7). */ @DisplayName("Webhook Types") class WebhookTypesTest { - private ObjectMapper objectMapper; + private ObjectMapper objectMapper; - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } - // ======================================================================== - // CreateWebhookRequest - // ======================================================================== + // ======================================================================== + // CreateWebhookRequest + // ======================================================================== - @Test - @DisplayName("CreateWebhookRequest - should build with builder") - void createWebhookRequestShouldBuildWithBuilder() { - CreateWebhookRequest request = CreateWebhookRequest.builder() + @Test + @DisplayName("CreateWebhookRequest - should build with builder") + void createWebhookRequestShouldBuildWithBuilder() { + CreateWebhookRequest request = + CreateWebhookRequest.builder() .url("https://example.com/webhook") .events(Arrays.asList("workflow.completed", "step.blocked")) .secret("my-secret-key") .active(true) .build(); - assertThat(request.getUrl()).isEqualTo("https://example.com/webhook"); - assertThat(request.getEvents()).containsExactly("workflow.completed", "step.blocked"); - assertThat(request.getSecret()).isEqualTo("my-secret-key"); - assertThat(request.isActive()).isTrue(); - } - - @Test - @DisplayName("CreateWebhookRequest - should default active to true") - void createWebhookRequestShouldDefaultActiveToTrue() { - CreateWebhookRequest request = CreateWebhookRequest.builder() - .url("https://example.com/webhook") - .build(); - - assertThat(request.isActive()).isTrue(); - } - - @Test - @DisplayName("CreateWebhookRequest - should require url") - void createWebhookRequestShouldRequireUrl() { - assertThatThrownBy(() -> CreateWebhookRequest.builder().build()) - .isInstanceOf(NullPointerException.class) - .hasMessageContaining("url"); - } - - @Test - @DisplayName("CreateWebhookRequest - should deserialize from JSON") - void createWebhookRequestShouldDeserialize() throws Exception { - String json = "{\"url\":\"https://example.com/hook\",\"events\":[\"step.blocked\"]," + assertThat(request.getUrl()).isEqualTo("https://example.com/webhook"); + assertThat(request.getEvents()).containsExactly("workflow.completed", "step.blocked"); + assertThat(request.getSecret()).isEqualTo("my-secret-key"); + assertThat(request.isActive()).isTrue(); + } + + @Test + @DisplayName("CreateWebhookRequest - should default active to true") + void createWebhookRequestShouldDefaultActiveToTrue() { + CreateWebhookRequest request = + CreateWebhookRequest.builder().url("https://example.com/webhook").build(); + + assertThat(request.isActive()).isTrue(); + } + + @Test + @DisplayName("CreateWebhookRequest - should require url") + void createWebhookRequestShouldRequireUrl() { + assertThatThrownBy(() -> CreateWebhookRequest.builder().build()) + .isInstanceOf(NullPointerException.class) + .hasMessageContaining("url"); + } + + @Test + @DisplayName("CreateWebhookRequest - should deserialize from JSON") + void createWebhookRequestShouldDeserialize() throws Exception { + String json = + "{\"url\":\"https://example.com/hook\",\"events\":[\"step.blocked\"]," + "\"secret\":\"s3cret\",\"active\":true}"; - CreateWebhookRequest request = objectMapper.readValue(json, CreateWebhookRequest.class); + CreateWebhookRequest request = objectMapper.readValue(json, CreateWebhookRequest.class); - assertThat(request.getUrl()).isEqualTo("https://example.com/hook"); - assertThat(request.getEvents()).containsExactly("step.blocked"); - assertThat(request.getSecret()).isEqualTo("s3cret"); - assertThat(request.isActive()).isTrue(); - } + assertThat(request.getUrl()).isEqualTo("https://example.com/hook"); + assertThat(request.getEvents()).containsExactly("step.blocked"); + assertThat(request.getSecret()).isEqualTo("s3cret"); + assertThat(request.isActive()).isTrue(); + } - @Test - @DisplayName("CreateWebhookRequest - should serialize to JSON") - void createWebhookRequestShouldSerialize() throws Exception { - CreateWebhookRequest request = CreateWebhookRequest.builder() + @Test + @DisplayName("CreateWebhookRequest - should serialize to JSON") + void createWebhookRequestShouldSerialize() throws Exception { + CreateWebhookRequest request = + CreateWebhookRequest.builder() .url("https://example.com/webhook") .events(Arrays.asList("workflow.completed")) .secret("key") .active(true) .build(); - String json = objectMapper.writeValueAsString(request); + String json = objectMapper.writeValueAsString(request); - assertThat(json).contains("\"url\":\"https://example.com/webhook\""); - assertThat(json).contains("\"events\":[\"workflow.completed\"]"); - assertThat(json).contains("\"active\":true"); - } + assertThat(json).contains("\"url\":\"https://example.com/webhook\""); + assertThat(json).contains("\"events\":[\"workflow.completed\"]"); + assertThat(json).contains("\"active\":true"); + } - @Test - @DisplayName("CreateWebhookRequest - should handle null events as empty list") - void createWebhookRequestShouldHandleNullEvents() { - CreateWebhookRequest request = CreateWebhookRequest.builder() - .url("https://example.com/webhook") - .events(null) - .build(); + @Test + @DisplayName("CreateWebhookRequest - should handle null events as empty list") + void createWebhookRequestShouldHandleNullEvents() { + CreateWebhookRequest request = + CreateWebhookRequest.builder().url("https://example.com/webhook").events(null).build(); - assertThat(request.getEvents()).isEmpty(); - } + assertThat(request.getEvents()).isEmpty(); + } - @Test - @DisplayName("CreateWebhookRequest - events should be immutable") - void createWebhookRequestEventsShouldBeImmutable() { - CreateWebhookRequest request = CreateWebhookRequest.builder() + @Test + @DisplayName("CreateWebhookRequest - events should be immutable") + void createWebhookRequestEventsShouldBeImmutable() { + CreateWebhookRequest request = + CreateWebhookRequest.builder() .url("https://example.com/webhook") .events(Arrays.asList("event-1")) .build(); - assertThatThrownBy(() -> request.getEvents().add("event-2")) - .isInstanceOf(UnsupportedOperationException.class); - } - - @Test - @DisplayName("CreateWebhookRequest - equals and hashCode") - void createWebhookRequestEqualsAndHashCode() { - CreateWebhookRequest r1 = CreateWebhookRequest.builder() - .url("https://example.com").events(Arrays.asList("e1")).secret("s").active(true).build(); - CreateWebhookRequest r2 = CreateWebhookRequest.builder() - .url("https://example.com").events(Arrays.asList("e1")).secret("s").active(true).build(); - CreateWebhookRequest r3 = CreateWebhookRequest.builder() - .url("https://other.com").events(Arrays.asList("e1")).secret("s").active(true).build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("CreateWebhookRequest - toString should not contain secret") - void createWebhookRequestToStringShouldNotContainSecret() { - CreateWebhookRequest request = CreateWebhookRequest.builder() + assertThatThrownBy(() -> request.getEvents().add("event-2")) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + @DisplayName("CreateWebhookRequest - equals and hashCode") + void createWebhookRequestEqualsAndHashCode() { + CreateWebhookRequest r1 = + CreateWebhookRequest.builder() + .url("https://example.com") + .events(Arrays.asList("e1")) + .secret("s") + .active(true) + .build(); + CreateWebhookRequest r2 = + CreateWebhookRequest.builder() + .url("https://example.com") + .events(Arrays.asList("e1")) + .secret("s") + .active(true) + .build(); + CreateWebhookRequest r3 = + CreateWebhookRequest.builder() + .url("https://other.com") + .events(Arrays.asList("e1")) + .secret("s") + .active(true) + .build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("CreateWebhookRequest - toString should not contain secret") + void createWebhookRequestToStringShouldNotContainSecret() { + CreateWebhookRequest request = + CreateWebhookRequest.builder() .url("https://example.com/webhook") .events(Arrays.asList("event-1")) .secret("super-secret-key") .active(true) .build(); - String str = request.toString(); - - assertThat(str).contains("https://example.com/webhook"); - assertThat(str).contains("event-1"); - // Secret is excluded from toString for security - assertThat(str).doesNotContain("super-secret-key"); - } - - // ======================================================================== - // WebhookSubscription - // ======================================================================== - - @Test - @DisplayName("WebhookSubscription - should construct with all fields") - void webhookSubscriptionShouldConstructWithAllFields() { - WebhookSubscription subscription = new WebhookSubscription( - "wh-123", "https://example.com/hook", - Arrays.asList("step.blocked"), true, - "2026-02-07T10:00:00Z", "2026-02-07T11:00:00Z"); - - assertThat(subscription.getId()).isEqualTo("wh-123"); - assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); - assertThat(subscription.getEvents()).containsExactly("step.blocked"); - assertThat(subscription.isActive()).isTrue(); - assertThat(subscription.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); - assertThat(subscription.getUpdatedAt()).isEqualTo("2026-02-07T11:00:00Z"); - } - - @Test - @DisplayName("WebhookSubscription - should deserialize from JSON") - void webhookSubscriptionShouldDeserialize() throws Exception { - String json = "{" + String str = request.toString(); + + assertThat(str).contains("https://example.com/webhook"); + assertThat(str).contains("event-1"); + // Secret is excluded from toString for security + assertThat(str).doesNotContain("super-secret-key"); + } + + // ======================================================================== + // WebhookSubscription + // ======================================================================== + + @Test + @DisplayName("WebhookSubscription - should construct with all fields") + void webhookSubscriptionShouldConstructWithAllFields() { + WebhookSubscription subscription = + new WebhookSubscription( + "wh-123", + "https://example.com/hook", + Arrays.asList("step.blocked"), + true, + "2026-02-07T10:00:00Z", + "2026-02-07T11:00:00Z"); + + assertThat(subscription.getId()).isEqualTo("wh-123"); + assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); + assertThat(subscription.getEvents()).containsExactly("step.blocked"); + assertThat(subscription.isActive()).isTrue(); + assertThat(subscription.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); + assertThat(subscription.getUpdatedAt()).isEqualTo("2026-02-07T11:00:00Z"); + } + + @Test + @DisplayName("WebhookSubscription - should deserialize from JSON") + void webhookSubscriptionShouldDeserialize() throws Exception { + String json = + "{" + "\"id\":\"wh-456\"," + "\"url\":\"https://example.com/hook\"," + "\"events\":[\"workflow.completed\",\"step.blocked\"]," @@ -196,197 +215,205 @@ void webhookSubscriptionShouldDeserialize() throws Exception { + "\"updated_at\":\"2026-02-07T11:00:00Z\"" + "}"; - WebhookSubscription subscription = objectMapper.readValue(json, WebhookSubscription.class); - - assertThat(subscription.getId()).isEqualTo("wh-456"); - assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); - assertThat(subscription.getEvents()).containsExactly("workflow.completed", "step.blocked"); - assertThat(subscription.isActive()).isTrue(); - } - - @Test - @DisplayName("WebhookSubscription - should serialize to JSON") - void webhookSubscriptionShouldSerialize() throws Exception { - WebhookSubscription subscription = new WebhookSubscription( - "wh-789", "https://example.com/hook", - Arrays.asList("step.blocked"), true, - "2026-02-07T10:00:00Z", "2026-02-07T11:00:00Z"); - - String json = objectMapper.writeValueAsString(subscription); - - assertThat(json).contains("\"id\":\"wh-789\""); - assertThat(json).contains("\"url\":\"https://example.com/hook\""); - assertThat(json).contains("\"active\":true"); - } - - @Test - @DisplayName("WebhookSubscription - should handle null events as empty list") - void webhookSubscriptionShouldHandleNullEvents() { - WebhookSubscription subscription = new WebhookSubscription( - "wh-1", "https://example.com", null, true, null, null); - - assertThat(subscription.getEvents()).isEmpty(); - } - - @Test - @DisplayName("WebhookSubscription - events should be immutable") - void webhookSubscriptionEventsShouldBeImmutable() { - WebhookSubscription subscription = new WebhookSubscription( + WebhookSubscription subscription = objectMapper.readValue(json, WebhookSubscription.class); + + assertThat(subscription.getId()).isEqualTo("wh-456"); + assertThat(subscription.getUrl()).isEqualTo("https://example.com/hook"); + assertThat(subscription.getEvents()).containsExactly("workflow.completed", "step.blocked"); + assertThat(subscription.isActive()).isTrue(); + } + + @Test + @DisplayName("WebhookSubscription - should serialize to JSON") + void webhookSubscriptionShouldSerialize() throws Exception { + WebhookSubscription subscription = + new WebhookSubscription( + "wh-789", + "https://example.com/hook", + Arrays.asList("step.blocked"), + true, + "2026-02-07T10:00:00Z", + "2026-02-07T11:00:00Z"); + + String json = objectMapper.writeValueAsString(subscription); + + assertThat(json).contains("\"id\":\"wh-789\""); + assertThat(json).contains("\"url\":\"https://example.com/hook\""); + assertThat(json).contains("\"active\":true"); + } + + @Test + @DisplayName("WebhookSubscription - should handle null events as empty list") + void webhookSubscriptionShouldHandleNullEvents() { + WebhookSubscription subscription = + new WebhookSubscription("wh-1", "https://example.com", null, true, null, null); + + assertThat(subscription.getEvents()).isEmpty(); + } + + @Test + @DisplayName("WebhookSubscription - events should be immutable") + void webhookSubscriptionEventsShouldBeImmutable() { + WebhookSubscription subscription = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, null, null); - assertThatThrownBy(() -> subscription.getEvents().add("e2")) - .isInstanceOf(UnsupportedOperationException.class); - } + assertThatThrownBy(() -> subscription.getEvents().add("e2")) + .isInstanceOf(UnsupportedOperationException.class); + } - @Test - @DisplayName("WebhookSubscription - equals and hashCode") - void webhookSubscriptionEqualsAndHashCode() { - WebhookSubscription s1 = new WebhookSubscription( + @Test + @DisplayName("WebhookSubscription - equals and hashCode") + void webhookSubscriptionEqualsAndHashCode() { + WebhookSubscription s1 = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, "c1", "u1"); - WebhookSubscription s2 = new WebhookSubscription( + WebhookSubscription s2 = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, "c1", "u1"); - WebhookSubscription s3 = new WebhookSubscription( + WebhookSubscription s3 = + new WebhookSubscription( "wh-2", "https://example.com", Arrays.asList("e1"), true, "c1", "u1"); - assertThat(s1).isEqualTo(s2); - assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); - assertThat(s1).isNotEqualTo(s3); - } + assertThat(s1).isEqualTo(s2); + assertThat(s1.hashCode()).isEqualTo(s2.hashCode()); + assertThat(s1).isNotEqualTo(s3); + } - @Test - @DisplayName("WebhookSubscription - toString contains key info") - void webhookSubscriptionToStringShouldContainInfo() { - WebhookSubscription subscription = new WebhookSubscription( + @Test + @DisplayName("WebhookSubscription - toString contains key info") + void webhookSubscriptionToStringShouldContainInfo() { + WebhookSubscription subscription = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, null, null); - String str = subscription.toString(); - - assertThat(str).contains("wh-1"); - assertThat(str).contains("https://example.com"); - assertThat(str).contains("active=true"); - } - - @Test - @DisplayName("WebhookSubscription - should ignore unknown properties") - void webhookSubscriptionShouldIgnoreUnknownProperties() throws Exception { - String json = "{\"id\":\"wh-1\",\"url\":\"https://example.com\"," + String str = subscription.toString(); + + assertThat(str).contains("wh-1"); + assertThat(str).contains("https://example.com"); + assertThat(str).contains("active=true"); + } + + @Test + @DisplayName("WebhookSubscription - should ignore unknown properties") + void webhookSubscriptionShouldIgnoreUnknownProperties() throws Exception { + String json = + "{\"id\":\"wh-1\",\"url\":\"https://example.com\"," + "\"events\":[],\"active\":true,\"extra\":\"field\"}"; - WebhookSubscription subscription = objectMapper.readValue(json, WebhookSubscription.class); + WebhookSubscription subscription = objectMapper.readValue(json, WebhookSubscription.class); - assertThat(subscription.getId()).isEqualTo("wh-1"); - } + assertThat(subscription.getId()).isEqualTo("wh-1"); + } - // ======================================================================== - // UpdateWebhookRequest - // ======================================================================== + // ======================================================================== + // UpdateWebhookRequest + // ======================================================================== - @Test - @DisplayName("UpdateWebhookRequest - should build with builder") - void updateWebhookRequestShouldBuildWithBuilder() { - UpdateWebhookRequest request = UpdateWebhookRequest.builder() + @Test + @DisplayName("UpdateWebhookRequest - should build with builder") + void updateWebhookRequestShouldBuildWithBuilder() { + UpdateWebhookRequest request = + UpdateWebhookRequest.builder() .url("https://new-url.com/hook") .events(Arrays.asList("step.approved")) .active(false) .build(); - assertThat(request.getUrl()).isEqualTo("https://new-url.com/hook"); - assertThat(request.getEvents()).containsExactly("step.approved"); - assertThat(request.getActive()).isFalse(); - } - - @Test - @DisplayName("UpdateWebhookRequest - should allow partial updates (null fields)") - void updateWebhookRequestShouldAllowPartialUpdates() { - UpdateWebhookRequest request = UpdateWebhookRequest.builder() - .active(false) - .build(); - - assertThat(request.getUrl()).isNull(); - assertThat(request.getEvents()).isNull(); - assertThat(request.getActive()).isFalse(); - } - - @Test - @DisplayName("UpdateWebhookRequest - should deserialize from JSON") - void updateWebhookRequestShouldDeserialize() throws Exception { - String json = "{\"url\":\"https://new.com\",\"events\":[\"e1\"],\"active\":false}"; - - UpdateWebhookRequest request = objectMapper.readValue(json, UpdateWebhookRequest.class); - - assertThat(request.getUrl()).isEqualTo("https://new.com"); - assertThat(request.getEvents()).containsExactly("e1"); - assertThat(request.getActive()).isFalse(); - } - - @Test - @DisplayName("UpdateWebhookRequest - should serialize to JSON") - void updateWebhookRequestShouldSerialize() throws Exception { - UpdateWebhookRequest request = UpdateWebhookRequest.builder() - .url("https://new.com") - .active(true) - .build(); - - String json = objectMapper.writeValueAsString(request); - - assertThat(json).contains("\"url\":\"https://new.com\""); - assertThat(json).contains("\"active\":true"); - } - - @Test - @DisplayName("UpdateWebhookRequest - equals and hashCode") - void updateWebhookRequestEqualsAndHashCode() { - UpdateWebhookRequest r1 = UpdateWebhookRequest.builder() - .url("https://a.com").active(true).build(); - UpdateWebhookRequest r2 = UpdateWebhookRequest.builder() - .url("https://a.com").active(true).build(); - UpdateWebhookRequest r3 = UpdateWebhookRequest.builder() - .url("https://b.com").active(true).build(); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("UpdateWebhookRequest - toString contains fields") - void updateWebhookRequestToStringShouldContainFields() { - UpdateWebhookRequest request = UpdateWebhookRequest.builder() - .url("https://example.com").active(true).build(); - String str = request.toString(); - - assertThat(str).contains("https://example.com"); - assertThat(str).contains("true"); - } - - // ======================================================================== - // ListWebhooksResponse - // ======================================================================== - - @Test - @DisplayName("ListWebhooksResponse - should construct with all fields") - void listWebhooksResponseShouldConstructWithAllFields() { - WebhookSubscription sub = new WebhookSubscription( + assertThat(request.getUrl()).isEqualTo("https://new-url.com/hook"); + assertThat(request.getEvents()).containsExactly("step.approved"); + assertThat(request.getActive()).isFalse(); + } + + @Test + @DisplayName("UpdateWebhookRequest - should allow partial updates (null fields)") + void updateWebhookRequestShouldAllowPartialUpdates() { + UpdateWebhookRequest request = UpdateWebhookRequest.builder().active(false).build(); + + assertThat(request.getUrl()).isNull(); + assertThat(request.getEvents()).isNull(); + assertThat(request.getActive()).isFalse(); + } + + @Test + @DisplayName("UpdateWebhookRequest - should deserialize from JSON") + void updateWebhookRequestShouldDeserialize() throws Exception { + String json = "{\"url\":\"https://new.com\",\"events\":[\"e1\"],\"active\":false}"; + + UpdateWebhookRequest request = objectMapper.readValue(json, UpdateWebhookRequest.class); + + assertThat(request.getUrl()).isEqualTo("https://new.com"); + assertThat(request.getEvents()).containsExactly("e1"); + assertThat(request.getActive()).isFalse(); + } + + @Test + @DisplayName("UpdateWebhookRequest - should serialize to JSON") + void updateWebhookRequestShouldSerialize() throws Exception { + UpdateWebhookRequest request = + UpdateWebhookRequest.builder().url("https://new.com").active(true).build(); + + String json = objectMapper.writeValueAsString(request); + + assertThat(json).contains("\"url\":\"https://new.com\""); + assertThat(json).contains("\"active\":true"); + } + + @Test + @DisplayName("UpdateWebhookRequest - equals and hashCode") + void updateWebhookRequestEqualsAndHashCode() { + UpdateWebhookRequest r1 = + UpdateWebhookRequest.builder().url("https://a.com").active(true).build(); + UpdateWebhookRequest r2 = + UpdateWebhookRequest.builder().url("https://a.com").active(true).build(); + UpdateWebhookRequest r3 = + UpdateWebhookRequest.builder().url("https://b.com").active(true).build(); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("UpdateWebhookRequest - toString contains fields") + void updateWebhookRequestToStringShouldContainFields() { + UpdateWebhookRequest request = + UpdateWebhookRequest.builder().url("https://example.com").active(true).build(); + String str = request.toString(); + + assertThat(str).contains("https://example.com"); + assertThat(str).contains("true"); + } + + // ======================================================================== + // ListWebhooksResponse + // ======================================================================== + + @Test + @DisplayName("ListWebhooksResponse - should construct with all fields") + void listWebhooksResponseShouldConstructWithAllFields() { + WebhookSubscription sub = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, null, null); - ListWebhooksResponse response = new ListWebhooksResponse( - Collections.singletonList(sub), 1); - - assertThat(response.getWebhooks()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(1); - } - - @Test - @DisplayName("ListWebhooksResponse - should handle null webhooks list") - void listWebhooksResponseShouldHandleNullList() { - ListWebhooksResponse response = new ListWebhooksResponse(null, 0); - - assertThat(response.getWebhooks()).isEmpty(); - assertThat(response.getTotal()).isEqualTo(0); - } - - @Test - @DisplayName("ListWebhooksResponse - should deserialize from JSON") - void listWebhooksResponseShouldDeserialize() throws Exception { - String json = "{" + ListWebhooksResponse response = new ListWebhooksResponse(Collections.singletonList(sub), 1); + + assertThat(response.getWebhooks()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(1); + } + + @Test + @DisplayName("ListWebhooksResponse - should handle null webhooks list") + void listWebhooksResponseShouldHandleNullList() { + ListWebhooksResponse response = new ListWebhooksResponse(null, 0); + + assertThat(response.getWebhooks()).isEmpty(); + assertThat(response.getTotal()).isEqualTo(0); + } + + @Test + @DisplayName("ListWebhooksResponse - should deserialize from JSON") + void listWebhooksResponseShouldDeserialize() throws Exception { + String json = + "{" + "\"webhooks\":[" + " {\"id\":\"wh-1\",\"url\":\"https://example.com\"," + " \"events\":[\"e1\"],\"active\":true}" @@ -394,45 +421,49 @@ void listWebhooksResponseShouldDeserialize() throws Exception { + "\"total\":1" + "}"; - ListWebhooksResponse response = objectMapper.readValue(json, ListWebhooksResponse.class); + ListWebhooksResponse response = objectMapper.readValue(json, ListWebhooksResponse.class); - assertThat(response.getWebhooks()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(1); - assertThat(response.getWebhooks().get(0).getId()).isEqualTo("wh-1"); - } + assertThat(response.getWebhooks()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(1); + assertThat(response.getWebhooks().get(0).getId()).isEqualTo("wh-1"); + } - @Test - @DisplayName("ListWebhooksResponse - webhooks list should be immutable") - void listWebhooksResponseListShouldBeImmutable() { - WebhookSubscription sub = new WebhookSubscription( + @Test + @DisplayName("ListWebhooksResponse - webhooks list should be immutable") + void listWebhooksResponseListShouldBeImmutable() { + WebhookSubscription sub = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, null, null); - ListWebhooksResponse response = new ListWebhooksResponse( - Arrays.asList(sub), 1); - - assertThatThrownBy(() -> response.getWebhooks().add( - new WebhookSubscription("wh-2", "url", null, true, null, null))) - .isInstanceOf(UnsupportedOperationException.class); - } - - @Test - @DisplayName("ListWebhooksResponse - equals and hashCode") - void listWebhooksResponseEqualsAndHashCode() { - WebhookSubscription sub = new WebhookSubscription( + ListWebhooksResponse response = new ListWebhooksResponse(Arrays.asList(sub), 1); + + assertThatThrownBy( + () -> + response + .getWebhooks() + .add(new WebhookSubscription("wh-2", "url", null, true, null, null))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + @DisplayName("ListWebhooksResponse - equals and hashCode") + void listWebhooksResponseEqualsAndHashCode() { + WebhookSubscription sub = + new WebhookSubscription( "wh-1", "https://example.com", Arrays.asList("e1"), true, null, null); - ListWebhooksResponse r1 = new ListWebhooksResponse(Collections.singletonList(sub), 1); - ListWebhooksResponse r2 = new ListWebhooksResponse(Collections.singletonList(sub), 1); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } - - @Test - @DisplayName("ListWebhooksResponse - toString contains key info") - void listWebhooksResponseToStringShouldContainInfo() { - ListWebhooksResponse response = new ListWebhooksResponse(Collections.emptyList(), 0); - String str = response.toString(); - - assertThat(str).contains("total=0"); - assertThat(str).contains("webhooks="); - } + ListWebhooksResponse r1 = new ListWebhooksResponse(Collections.singletonList(sub), 1); + ListWebhooksResponse r2 = new ListWebhooksResponse(Collections.singletonList(sub), 1); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } + + @Test + @DisplayName("ListWebhooksResponse - toString contains key info") + void listWebhooksResponseToStringShouldContainInfo() { + ListWebhooksResponse response = new ListWebhooksResponse(Collections.emptyList(), 0); + String str = response.toString(); + + assertThat(str).contains("total=0"); + assertThat(str).contains("webhooks="); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/workflow/WCPApprovalTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/workflow/WCPApprovalTypesTest.java index b6211d5..1b2f13a 100644 --- a/src/test/java/com/getaxonflow/sdk/types/workflow/WCPApprovalTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/workflow/WCPApprovalTypesTest.java @@ -15,184 +15,184 @@ */ package com.getaxonflow.sdk.types.workflow; +import static org.assertj.core.api.Assertions.*; + import com.fasterxml.jackson.databind.ObjectMapper; import com.getaxonflow.sdk.types.workflow.WorkflowTypes.*; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; - import java.util.Arrays; import java.util.Collections; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for WCP Approval types (Feature 5). - */ +/** Tests for WCP Approval types (Feature 5). */ @DisplayName("WCP Approval Types") class WCPApprovalTypesTest { - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } - - // ======================================================================== - // ApproveStepResponse - // ======================================================================== - - @Test - @DisplayName("ApproveStepResponse - should construct with all fields") - void approveStepResponseShouldConstructWithAllFields() { - ApproveStepResponse response = new ApproveStepResponse("wf-123", "step-1", "approved"); - - assertThat(response.getWorkflowId()).isEqualTo("wf-123"); - assertThat(response.getStepId()).isEqualTo("step-1"); - assertThat(response.getStatus()).isEqualTo("approved"); - } - - @Test - @DisplayName("ApproveStepResponse - should deserialize from JSON") - void approveStepResponseShouldDeserialize() throws Exception { - String json = "{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"approved\"}"; - - ApproveStepResponse response = objectMapper.readValue(json, ApproveStepResponse.class); - - assertThat(response.getWorkflowId()).isEqualTo("wf-456"); - assertThat(response.getStepId()).isEqualTo("step-2"); - assertThat(response.getStatus()).isEqualTo("approved"); - } - - @Test - @DisplayName("ApproveStepResponse - should serialize to JSON") - void approveStepResponseShouldSerialize() throws Exception { - ApproveStepResponse response = new ApproveStepResponse("wf-789", "step-3", "approved"); - - String json = objectMapper.writeValueAsString(response); - - assertThat(json).contains("\"workflow_id\":\"wf-789\""); - assertThat(json).contains("\"step_id\":\"step-3\""); - assertThat(json).contains("\"status\":\"approved\""); - } - - @Test - @DisplayName("ApproveStepResponse - equals and hashCode") - void approveStepResponseEqualsAndHashCode() { - ApproveStepResponse r1 = new ApproveStepResponse("wf-1", "step-1", "approved"); - ApproveStepResponse r2 = new ApproveStepResponse("wf-1", "step-1", "approved"); - ApproveStepResponse r3 = new ApproveStepResponse("wf-2", "step-1", "approved"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("ApproveStepResponse - toString contains all fields") - void approveStepResponseToStringShouldContainAllFields() { - ApproveStepResponse response = new ApproveStepResponse("wf-1", "step-1", "approved"); - String str = response.toString(); - - assertThat(str).contains("wf-1"); - assertThat(str).contains("step-1"); - assertThat(str).contains("approved"); - } - - @Test - @DisplayName("ApproveStepResponse - should ignore unknown properties") - void approveStepResponseShouldIgnoreUnknownProperties() throws Exception { - String json = "{\"workflow_id\":\"wf-1\",\"step_id\":\"s-1\",\"status\":\"approved\",\"extra\":\"field\"}"; - - ApproveStepResponse response = objectMapper.readValue(json, ApproveStepResponse.class); - - assertThat(response.getWorkflowId()).isEqualTo("wf-1"); - } - - // ======================================================================== - // RejectStepResponse - // ======================================================================== - - @Test - @DisplayName("RejectStepResponse - should construct with all fields") - void rejectStepResponseShouldConstructWithAllFields() { - RejectStepResponse response = new RejectStepResponse("wf-123", "step-1", "rejected"); - - assertThat(response.getWorkflowId()).isEqualTo("wf-123"); - assertThat(response.getStepId()).isEqualTo("step-1"); - assertThat(response.getStatus()).isEqualTo("rejected"); - } - - @Test - @DisplayName("RejectStepResponse - should deserialize from JSON") - void rejectStepResponseShouldDeserialize() throws Exception { - String json = "{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"rejected\"}"; - - RejectStepResponse response = objectMapper.readValue(json, RejectStepResponse.class); - - assertThat(response.getWorkflowId()).isEqualTo("wf-456"); - assertThat(response.getStepId()).isEqualTo("step-2"); - assertThat(response.getStatus()).isEqualTo("rejected"); - } - - @Test - @DisplayName("RejectStepResponse - should serialize to JSON") - void rejectStepResponseShouldSerialize() throws Exception { - RejectStepResponse response = new RejectStepResponse("wf-789", "step-3", "rejected"); - - String json = objectMapper.writeValueAsString(response); - - assertThat(json).contains("\"workflow_id\":\"wf-789\""); - assertThat(json).contains("\"step_id\":\"step-3\""); - assertThat(json).contains("\"status\":\"rejected\""); - } - - @Test - @DisplayName("RejectStepResponse - equals and hashCode") - void rejectStepResponseEqualsAndHashCode() { - RejectStepResponse r1 = new RejectStepResponse("wf-1", "step-1", "rejected"); - RejectStepResponse r2 = new RejectStepResponse("wf-1", "step-1", "rejected"); - RejectStepResponse r3 = new RejectStepResponse("wf-2", "step-1", "rejected"); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - assertThat(r1).isNotEqualTo(r3); - } - - @Test - @DisplayName("RejectStepResponse - toString contains all fields") - void rejectStepResponseToStringShouldContainAllFields() { - RejectStepResponse response = new RejectStepResponse("wf-1", "step-1", "rejected"); - String str = response.toString(); - - assertThat(str).contains("wf-1"); - assertThat(str).contains("step-1"); - assertThat(str).contains("rejected"); - } - - // ======================================================================== - // PendingApproval - // ======================================================================== - - @Test - @DisplayName("PendingApproval - should construct with all fields") - void pendingApprovalShouldConstructWithAllFields() { - PendingApproval approval = new PendingApproval( + private ObjectMapper objectMapper; + + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } + + // ======================================================================== + // ApproveStepResponse + // ======================================================================== + + @Test + @DisplayName("ApproveStepResponse - should construct with all fields") + void approveStepResponseShouldConstructWithAllFields() { + ApproveStepResponse response = new ApproveStepResponse("wf-123", "step-1", "approved"); + + assertThat(response.getWorkflowId()).isEqualTo("wf-123"); + assertThat(response.getStepId()).isEqualTo("step-1"); + assertThat(response.getStatus()).isEqualTo("approved"); + } + + @Test + @DisplayName("ApproveStepResponse - should deserialize from JSON") + void approveStepResponseShouldDeserialize() throws Exception { + String json = "{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"approved\"}"; + + ApproveStepResponse response = objectMapper.readValue(json, ApproveStepResponse.class); + + assertThat(response.getWorkflowId()).isEqualTo("wf-456"); + assertThat(response.getStepId()).isEqualTo("step-2"); + assertThat(response.getStatus()).isEqualTo("approved"); + } + + @Test + @DisplayName("ApproveStepResponse - should serialize to JSON") + void approveStepResponseShouldSerialize() throws Exception { + ApproveStepResponse response = new ApproveStepResponse("wf-789", "step-3", "approved"); + + String json = objectMapper.writeValueAsString(response); + + assertThat(json).contains("\"workflow_id\":\"wf-789\""); + assertThat(json).contains("\"step_id\":\"step-3\""); + assertThat(json).contains("\"status\":\"approved\""); + } + + @Test + @DisplayName("ApproveStepResponse - equals and hashCode") + void approveStepResponseEqualsAndHashCode() { + ApproveStepResponse r1 = new ApproveStepResponse("wf-1", "step-1", "approved"); + ApproveStepResponse r2 = new ApproveStepResponse("wf-1", "step-1", "approved"); + ApproveStepResponse r3 = new ApproveStepResponse("wf-2", "step-1", "approved"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("ApproveStepResponse - toString contains all fields") + void approveStepResponseToStringShouldContainAllFields() { + ApproveStepResponse response = new ApproveStepResponse("wf-1", "step-1", "approved"); + String str = response.toString(); + + assertThat(str).contains("wf-1"); + assertThat(str).contains("step-1"); + assertThat(str).contains("approved"); + } + + @Test + @DisplayName("ApproveStepResponse - should ignore unknown properties") + void approveStepResponseShouldIgnoreUnknownProperties() throws Exception { + String json = + "{\"workflow_id\":\"wf-1\",\"step_id\":\"s-1\",\"status\":\"approved\",\"extra\":\"field\"}"; + + ApproveStepResponse response = objectMapper.readValue(json, ApproveStepResponse.class); + + assertThat(response.getWorkflowId()).isEqualTo("wf-1"); + } + + // ======================================================================== + // RejectStepResponse + // ======================================================================== + + @Test + @DisplayName("RejectStepResponse - should construct with all fields") + void rejectStepResponseShouldConstructWithAllFields() { + RejectStepResponse response = new RejectStepResponse("wf-123", "step-1", "rejected"); + + assertThat(response.getWorkflowId()).isEqualTo("wf-123"); + assertThat(response.getStepId()).isEqualTo("step-1"); + assertThat(response.getStatus()).isEqualTo("rejected"); + } + + @Test + @DisplayName("RejectStepResponse - should deserialize from JSON") + void rejectStepResponseShouldDeserialize() throws Exception { + String json = "{\"workflow_id\":\"wf-456\",\"step_id\":\"step-2\",\"status\":\"rejected\"}"; + + RejectStepResponse response = objectMapper.readValue(json, RejectStepResponse.class); + + assertThat(response.getWorkflowId()).isEqualTo("wf-456"); + assertThat(response.getStepId()).isEqualTo("step-2"); + assertThat(response.getStatus()).isEqualTo("rejected"); + } + + @Test + @DisplayName("RejectStepResponse - should serialize to JSON") + void rejectStepResponseShouldSerialize() throws Exception { + RejectStepResponse response = new RejectStepResponse("wf-789", "step-3", "rejected"); + + String json = objectMapper.writeValueAsString(response); + + assertThat(json).contains("\"workflow_id\":\"wf-789\""); + assertThat(json).contains("\"step_id\":\"step-3\""); + assertThat(json).contains("\"status\":\"rejected\""); + } + + @Test + @DisplayName("RejectStepResponse - equals and hashCode") + void rejectStepResponseEqualsAndHashCode() { + RejectStepResponse r1 = new RejectStepResponse("wf-1", "step-1", "rejected"); + RejectStepResponse r2 = new RejectStepResponse("wf-1", "step-1", "rejected"); + RejectStepResponse r3 = new RejectStepResponse("wf-2", "step-1", "rejected"); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + assertThat(r1).isNotEqualTo(r3); + } + + @Test + @DisplayName("RejectStepResponse - toString contains all fields") + void rejectStepResponseToStringShouldContainAllFields() { + RejectStepResponse response = new RejectStepResponse("wf-1", "step-1", "rejected"); + String str = response.toString(); + + assertThat(str).contains("wf-1"); + assertThat(str).contains("step-1"); + assertThat(str).contains("rejected"); + } + + // ======================================================================== + // PendingApproval + // ======================================================================== + + @Test + @DisplayName("PendingApproval - should construct with all fields") + void pendingApprovalShouldConstructWithAllFields() { + PendingApproval approval = + new PendingApproval( "wf-1", "Code Review", "step-1", "Generate Code", "llm_call", "2026-02-07T10:00:00Z"); - assertThat(approval.getWorkflowId()).isEqualTo("wf-1"); - assertThat(approval.getWorkflowName()).isEqualTo("Code Review"); - assertThat(approval.getStepId()).isEqualTo("step-1"); - assertThat(approval.getStepName()).isEqualTo("Generate Code"); - assertThat(approval.getStepType()).isEqualTo("llm_call"); - assertThat(approval.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); - } - - @Test - @DisplayName("PendingApproval - should deserialize from JSON") - void pendingApprovalShouldDeserialize() throws Exception { - String json = "{" + assertThat(approval.getWorkflowId()).isEqualTo("wf-1"); + assertThat(approval.getWorkflowName()).isEqualTo("Code Review"); + assertThat(approval.getStepId()).isEqualTo("step-1"); + assertThat(approval.getStepName()).isEqualTo("Generate Code"); + assertThat(approval.getStepType()).isEqualTo("llm_call"); + assertThat(approval.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); + } + + @Test + @DisplayName("PendingApproval - should deserialize from JSON") + void pendingApprovalShouldDeserialize() throws Exception { + String json = + "{" + "\"workflow_id\":\"wf-1\"," + "\"workflow_name\":\"Code Review\"," + "\"step_id\":\"step-1\"," @@ -201,87 +201,93 @@ void pendingApprovalShouldDeserialize() throws Exception { + "\"created_at\":\"2026-02-07T10:00:00Z\"" + "}"; - PendingApproval approval = objectMapper.readValue(json, PendingApproval.class); - - assertThat(approval.getWorkflowId()).isEqualTo("wf-1"); - assertThat(approval.getWorkflowName()).isEqualTo("Code Review"); - assertThat(approval.getStepId()).isEqualTo("step-1"); - assertThat(approval.getStepName()).isEqualTo("Generate Code"); - assertThat(approval.getStepType()).isEqualTo("llm_call"); - assertThat(approval.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); - } - - @Test - @DisplayName("PendingApproval - should serialize to JSON") - void pendingApprovalShouldSerialize() throws Exception { - PendingApproval approval = new PendingApproval( + PendingApproval approval = objectMapper.readValue(json, PendingApproval.class); + + assertThat(approval.getWorkflowId()).isEqualTo("wf-1"); + assertThat(approval.getWorkflowName()).isEqualTo("Code Review"); + assertThat(approval.getStepId()).isEqualTo("step-1"); + assertThat(approval.getStepName()).isEqualTo("Generate Code"); + assertThat(approval.getStepType()).isEqualTo("llm_call"); + assertThat(approval.getCreatedAt()).isEqualTo("2026-02-07T10:00:00Z"); + } + + @Test + @DisplayName("PendingApproval - should serialize to JSON") + void pendingApprovalShouldSerialize() throws Exception { + PendingApproval approval = + new PendingApproval( "wf-1", "Code Review", "step-1", "Generate Code", "llm_call", "2026-02-07T10:00:00Z"); - String json = objectMapper.writeValueAsString(approval); - - assertThat(json).contains("\"workflow_id\":\"wf-1\""); - assertThat(json).contains("\"workflow_name\":\"Code Review\""); - assertThat(json).contains("\"step_id\":\"step-1\""); - assertThat(json).contains("\"step_name\":\"Generate Code\""); - assertThat(json).contains("\"step_type\":\"llm_call\""); - } - - @Test - @DisplayName("PendingApproval - equals and hashCode") - void pendingApprovalEqualsAndHashCode() { - PendingApproval a1 = new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - PendingApproval a2 = new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - PendingApproval a3 = new PendingApproval("wf-2", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - - assertThat(a1).isEqualTo(a2); - assertThat(a1.hashCode()).isEqualTo(a2.hashCode()); - assertThat(a1).isNotEqualTo(a3); - } - - @Test - @DisplayName("PendingApproval - toString contains all fields") - void pendingApprovalToStringShouldContainAllFields() { - PendingApproval approval = new PendingApproval( + String json = objectMapper.writeValueAsString(approval); + + assertThat(json).contains("\"workflow_id\":\"wf-1\""); + assertThat(json).contains("\"workflow_name\":\"Code Review\""); + assertThat(json).contains("\"step_id\":\"step-1\""); + assertThat(json).contains("\"step_name\":\"Generate Code\""); + assertThat(json).contains("\"step_type\":\"llm_call\""); + } + + @Test + @DisplayName("PendingApproval - equals and hashCode") + void pendingApprovalEqualsAndHashCode() { + PendingApproval a1 = + new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + PendingApproval a2 = + new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + PendingApproval a3 = + new PendingApproval("wf-2", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + + assertThat(a1).isEqualTo(a2); + assertThat(a1.hashCode()).isEqualTo(a2.hashCode()); + assertThat(a1).isNotEqualTo(a3); + } + + @Test + @DisplayName("PendingApproval - toString contains all fields") + void pendingApprovalToStringShouldContainAllFields() { + PendingApproval approval = + new PendingApproval( "wf-1", "Code Review", "step-1", "Generate Code", "llm_call", "2026-02-07T10:00:00Z"); - String str = approval.toString(); - - assertThat(str).contains("wf-1"); - assertThat(str).contains("Code Review"); - assertThat(str).contains("step-1"); - assertThat(str).contains("Generate Code"); - assertThat(str).contains("llm_call"); - } - - // ======================================================================== - // PendingApprovalsResponse - // ======================================================================== - - @Test - @DisplayName("PendingApprovalsResponse - should construct with all fields") - void pendingApprovalsResponseShouldConstructWithAllFields() { - PendingApproval approval = new PendingApproval( - "wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - PendingApprovalsResponse response = new PendingApprovalsResponse( - Collections.singletonList(approval), 1); - - assertThat(response.getApprovals()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(1); - assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); - } - - @Test - @DisplayName("PendingApprovalsResponse - should handle null approvals list") - void pendingApprovalsResponseShouldHandleNullList() { - PendingApprovalsResponse response = new PendingApprovalsResponse(null, 0); - - assertThat(response.getApprovals()).isEmpty(); - assertThat(response.getTotal()).isEqualTo(0); - } - - @Test - @DisplayName("PendingApprovalsResponse - should deserialize from JSON") - void pendingApprovalsResponseShouldDeserialize() throws Exception { - String json = "{" + String str = approval.toString(); + + assertThat(str).contains("wf-1"); + assertThat(str).contains("Code Review"); + assertThat(str).contains("step-1"); + assertThat(str).contains("Generate Code"); + assertThat(str).contains("llm_call"); + } + + // ======================================================================== + // PendingApprovalsResponse + // ======================================================================== + + @Test + @DisplayName("PendingApprovalsResponse - should construct with all fields") + void pendingApprovalsResponseShouldConstructWithAllFields() { + PendingApproval approval = + new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + PendingApprovalsResponse response = + new PendingApprovalsResponse(Collections.singletonList(approval), 1); + + assertThat(response.getApprovals()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(1); + assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); + } + + @Test + @DisplayName("PendingApprovalsResponse - should handle null approvals list") + void pendingApprovalsResponseShouldHandleNullList() { + PendingApprovalsResponse response = new PendingApprovalsResponse(null, 0); + + assertThat(response.getApprovals()).isEmpty(); + assertThat(response.getTotal()).isEqualTo(0); + } + + @Test + @DisplayName("PendingApprovalsResponse - should deserialize from JSON") + void pendingApprovalsResponseShouldDeserialize() throws Exception { + String json = + "{" + "\"approvals\":[" + " {\"workflow_id\":\"wf-1\",\"workflow_name\":\"Review\"," + " \"step_id\":\"s-1\",\"step_name\":\"Generate\"," @@ -290,45 +296,52 @@ void pendingApprovalsResponseShouldDeserialize() throws Exception { + "\"total\":1" + "}"; - PendingApprovalsResponse response = objectMapper.readValue(json, PendingApprovalsResponse.class); - - assertThat(response.getApprovals()).hasSize(1); - assertThat(response.getTotal()).isEqualTo(1); - assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); - } - - @Test - @DisplayName("PendingApprovalsResponse - approvals list should be immutable") - void pendingApprovalsResponseListShouldBeImmutable() { - PendingApproval approval = new PendingApproval( - "wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - PendingApprovalsResponse response = new PendingApprovalsResponse( - Arrays.asList(approval), 1); - - assertThatThrownBy(() -> response.getApprovals().add( - new PendingApproval("wf-2", "N", "s-2", "S", "tool_call", "2026-02-07T11:00:00Z"))) - .isInstanceOf(UnsupportedOperationException.class); - } - - @Test - @DisplayName("PendingApprovalsResponse - equals and hashCode") - void pendingApprovalsResponseEqualsAndHashCode() { - PendingApproval approval = new PendingApproval( - "wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); - PendingApprovalsResponse r1 = new PendingApprovalsResponse(Collections.singletonList(approval), 1); - PendingApprovalsResponse r2 = new PendingApprovalsResponse(Collections.singletonList(approval), 1); - - assertThat(r1).isEqualTo(r2); - assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); - } - - @Test - @DisplayName("PendingApprovalsResponse - toString contains key info") - void pendingApprovalsResponseToStringShouldContainInfo() { - PendingApprovalsResponse response = new PendingApprovalsResponse(Collections.emptyList(), 0); - String str = response.toString(); - - assertThat(str).contains("total=0"); - assertThat(str).contains("approvals="); - } + PendingApprovalsResponse response = + objectMapper.readValue(json, PendingApprovalsResponse.class); + + assertThat(response.getApprovals()).hasSize(1); + assertThat(response.getTotal()).isEqualTo(1); + assertThat(response.getApprovals().get(0).getWorkflowId()).isEqualTo("wf-1"); + } + + @Test + @DisplayName("PendingApprovalsResponse - approvals list should be immutable") + void pendingApprovalsResponseListShouldBeImmutable() { + PendingApproval approval = + new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + PendingApprovalsResponse response = new PendingApprovalsResponse(Arrays.asList(approval), 1); + + assertThatThrownBy( + () -> + response + .getApprovals() + .add( + new PendingApproval( + "wf-2", "N", "s-2", "S", "tool_call", "2026-02-07T11:00:00Z"))) + .isInstanceOf(UnsupportedOperationException.class); + } + + @Test + @DisplayName("PendingApprovalsResponse - equals and hashCode") + void pendingApprovalsResponseEqualsAndHashCode() { + PendingApproval approval = + new PendingApproval("wf-1", "Name", "s-1", "Step", "llm_call", "2026-02-07T10:00:00Z"); + PendingApprovalsResponse r1 = + new PendingApprovalsResponse(Collections.singletonList(approval), 1); + PendingApprovalsResponse r2 = + new PendingApprovalsResponse(Collections.singletonList(approval), 1); + + assertThat(r1).isEqualTo(r2); + assertThat(r1.hashCode()).isEqualTo(r2.hashCode()); + } + + @Test + @DisplayName("PendingApprovalsResponse - toString contains key info") + void pendingApprovalsResponseToStringShouldContainInfo() { + PendingApprovalsResponse response = new PendingApprovalsResponse(Collections.emptyList(), 0); + String str = response.toString(); + + assertThat(str).contains("total=0"); + assertThat(str).contains("approvals="); + } } diff --git a/src/test/java/com/getaxonflow/sdk/types/workflow/WorkflowPolicyTypesTest.java b/src/test/java/com/getaxonflow/sdk/types/workflow/WorkflowPolicyTypesTest.java index a47047e..5360948 100644 --- a/src/test/java/com/getaxonflow/sdk/types/workflow/WorkflowPolicyTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/types/workflow/WorkflowPolicyTypesTest.java @@ -15,250 +15,241 @@ */ package com.getaxonflow.sdk.types.workflow; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; +import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Arrays; import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; -import static org.assertj.core.api.Assertions.*; - -/** - * Tests for workflow policy types (Issues #1019, #1020, #1021). - */ +/** Tests for workflow policy types (Issues #1019, #1020, #1021). */ @DisplayName("Workflow Policy Types") class WorkflowPolicyTypesTest { - private ObjectMapper objectMapper; + private ObjectMapper objectMapper; - @BeforeEach - void setUp() { - objectMapper = new ObjectMapper(); - } + @BeforeEach + void setUp() { + objectMapper = new ObjectMapper(); + } - // PolicyMatch tests + // PolicyMatch tests - @Test - @DisplayName("PolicyMatch - should build with all fields") - void policyMatchShouldBuildWithAllFields() { - PolicyMatch match = PolicyMatch.builder() + @Test + @DisplayName("PolicyMatch - should build with all fields") + void policyMatchShouldBuildWithAllFields() { + PolicyMatch match = + PolicyMatch.builder() .policyId("policy-123") .policyName("block-gpt4") .action("block") .reason("GPT-4 not allowed in production") .build(); - assertThat(match.getPolicyId()).isEqualTo("policy-123"); - assertThat(match.getPolicyName()).isEqualTo("block-gpt4"); - assertThat(match.getAction()).isEqualTo("block"); - assertThat(match.getReason()).isEqualTo("GPT-4 not allowed in production"); - } - - @Test - @DisplayName("PolicyMatch - isBlocking returns true for block action") - void policyMatchIsBlockingShouldReturnTrueForBlockAction() { - PolicyMatch match = PolicyMatch.builder() - .policyId("policy-123") - .action("block") - .build(); - - assertThat(match.isBlocking()).isTrue(); - } - - @Test - @DisplayName("PolicyMatch - isBlocking returns false for allow action") - void policyMatchIsBlockingShouldReturnFalseForAllowAction() { - PolicyMatch match = PolicyMatch.builder() - .policyId("policy-123") - .action("allow") - .build(); - - assertThat(match.isBlocking()).isFalse(); - } - - @Test - @DisplayName("PolicyMatch - requiresApproval returns true for require_approval action") - void policyMatchRequiresApprovalShouldReturnTrue() { - PolicyMatch match = PolicyMatch.builder() - .policyId("policy-123") - .action("require_approval") - .build(); - - assertThat(match.requiresApproval()).isTrue(); - } - - @Test - @DisplayName("PolicyMatch - should deserialize from JSON") - void policyMatchShouldDeserialize() throws Exception { - String json = "{" + assertThat(match.getPolicyId()).isEqualTo("policy-123"); + assertThat(match.getPolicyName()).isEqualTo("block-gpt4"); + assertThat(match.getAction()).isEqualTo("block"); + assertThat(match.getReason()).isEqualTo("GPT-4 not allowed in production"); + } + + @Test + @DisplayName("PolicyMatch - isBlocking returns true for block action") + void policyMatchIsBlockingShouldReturnTrueForBlockAction() { + PolicyMatch match = PolicyMatch.builder().policyId("policy-123").action("block").build(); + + assertThat(match.isBlocking()).isTrue(); + } + + @Test + @DisplayName("PolicyMatch - isBlocking returns false for allow action") + void policyMatchIsBlockingShouldReturnFalseForAllowAction() { + PolicyMatch match = PolicyMatch.builder().policyId("policy-123").action("allow").build(); + + assertThat(match.isBlocking()).isFalse(); + } + + @Test + @DisplayName("PolicyMatch - requiresApproval returns true for require_approval action") + void policyMatchRequiresApprovalShouldReturnTrue() { + PolicyMatch match = + PolicyMatch.builder().policyId("policy-123").action("require_approval").build(); + + assertThat(match.requiresApproval()).isTrue(); + } + + @Test + @DisplayName("PolicyMatch - should deserialize from JSON") + void policyMatchShouldDeserialize() throws Exception { + String json = + "{" + "\"policy_id\": \"policy-456\"," + "\"policy_name\": \"pii-detection\"," + "\"action\": \"redact\"," + "\"reason\": \"PII detected in input\"" + "}"; - PolicyMatch match = objectMapper.readValue(json, PolicyMatch.class); + PolicyMatch match = objectMapper.readValue(json, PolicyMatch.class); - assertThat(match.getPolicyId()).isEqualTo("policy-456"); - assertThat(match.getPolicyName()).isEqualTo("pii-detection"); - assertThat(match.getAction()).isEqualTo("redact"); - assertThat(match.getReason()).isEqualTo("PII detected in input"); - } + assertThat(match.getPolicyId()).isEqualTo("policy-456"); + assertThat(match.getPolicyName()).isEqualTo("pii-detection"); + assertThat(match.getAction()).isEqualTo("redact"); + assertThat(match.getReason()).isEqualTo("PII detected in input"); + } - @Test - @DisplayName("PolicyMatch - should serialize to JSON") - void policyMatchShouldSerialize() throws Exception { - PolicyMatch match = PolicyMatch.builder() + @Test + @DisplayName("PolicyMatch - should serialize to JSON") + void policyMatchShouldSerialize() throws Exception { + PolicyMatch match = + PolicyMatch.builder() .policyId("policy-789") .policyName("cost-limit") .action("allow") .reason("Within budget") .build(); - String json = objectMapper.writeValueAsString(match); + String json = objectMapper.writeValueAsString(match); - assertThat(json).contains("\"policy_id\":\"policy-789\""); - assertThat(json).contains("\"policy_name\":\"cost-limit\""); - assertThat(json).contains("\"action\":\"allow\""); - } + assertThat(json).contains("\"policy_id\":\"policy-789\""); + assertThat(json).contains("\"policy_name\":\"cost-limit\""); + assertThat(json).contains("\"action\":\"allow\""); + } - @Test - @DisplayName("PolicyMatch - equals and hashCode") - void policyMatchEqualsAndHashCode() { - PolicyMatch match1 = PolicyMatch.builder() - .policyId("policy-123") - .policyName("test") - .action("allow") - .build(); + @Test + @DisplayName("PolicyMatch - equals and hashCode") + void policyMatchEqualsAndHashCode() { + PolicyMatch match1 = + PolicyMatch.builder().policyId("policy-123").policyName("test").action("allow").build(); - PolicyMatch match2 = PolicyMatch.builder() - .policyId("policy-123") - .policyName("test") - .action("allow") - .build(); + PolicyMatch match2 = + PolicyMatch.builder().policyId("policy-123").policyName("test").action("allow").build(); - PolicyMatch match3 = PolicyMatch.builder() - .policyId("policy-456") - .policyName("other") - .action("block") - .build(); + PolicyMatch match3 = + PolicyMatch.builder().policyId("policy-456").policyName("other").action("block").build(); - assertThat(match1).isEqualTo(match2); - assertThat(match1.hashCode()).isEqualTo(match2.hashCode()); - assertThat(match1).isNotEqualTo(match3); - } + assertThat(match1).isEqualTo(match2); + assertThat(match1.hashCode()).isEqualTo(match2.hashCode()); + assertThat(match1).isNotEqualTo(match3); + } - @Test - @DisplayName("PolicyMatch - toString contains all fields") - void policyMatchToStringShouldContainAllFields() { - PolicyMatch match = PolicyMatch.builder() + @Test + @DisplayName("PolicyMatch - toString contains all fields") + void policyMatchToStringShouldContainAllFields() { + PolicyMatch match = + PolicyMatch.builder() .policyId("policy-123") .policyName("test-policy") .action("block") .reason("test reason") .build(); - String str = match.toString(); + String str = match.toString(); - assertThat(str).contains("policy-123"); - assertThat(str).contains("test-policy"); - assertThat(str).contains("block"); - assertThat(str).contains("test reason"); - } + assertThat(str).contains("policy-123"); + assertThat(str).contains("test-policy"); + assertThat(str).contains("block"); + assertThat(str).contains("test reason"); + } - // PolicyEvaluationResult tests + // PolicyEvaluationResult tests - @Test - @DisplayName("PolicyEvaluationResult - should build with all fields") - void policyEvaluationResultShouldBuildWithAllFields() { - List policies = Arrays.asList("cost-limit", "model-restriction"); - PolicyEvaluationResult result = PolicyEvaluationResult.builder() + @Test + @DisplayName("PolicyEvaluationResult - should build with all fields") + void policyEvaluationResultShouldBuildWithAllFields() { + List policies = Arrays.asList("cost-limit", "model-restriction"); + PolicyEvaluationResult result = + PolicyEvaluationResult.builder() .allowed(true) .appliedPolicies(policies) .riskScore(0.2) .build(); - assertThat(result.isAllowed()).isTrue(); - assertThat(result.getAppliedPolicies()).containsExactly("cost-limit", "model-restriction"); - assertThat(result.getRiskScore()).isEqualTo(0.2); - } + assertThat(result.isAllowed()).isTrue(); + assertThat(result.getAppliedPolicies()).containsExactly("cost-limit", "model-restriction"); + assertThat(result.getRiskScore()).isEqualTo(0.2); + } - @Test - @DisplayName("PolicyEvaluationResult - should deserialize from JSON") - void policyEvaluationResultShouldDeserialize() throws Exception { - String json = "{" + @Test + @DisplayName("PolicyEvaluationResult - should deserialize from JSON") + void policyEvaluationResultShouldDeserialize() throws Exception { + String json = + "{" + "\"allowed\": false," + "\"applied_policies\": [\"high-risk-block\"]," + "\"risk_score\": 0.85" + "}"; - PolicyEvaluationResult result = objectMapper.readValue(json, PolicyEvaluationResult.class); + PolicyEvaluationResult result = objectMapper.readValue(json, PolicyEvaluationResult.class); - assertThat(result.isAllowed()).isFalse(); - assertThat(result.getAppliedPolicies()).containsExactly("high-risk-block"); - assertThat(result.getRiskScore()).isEqualTo(0.85); - } + assertThat(result.isAllowed()).isFalse(); + assertThat(result.getAppliedPolicies()).containsExactly("high-risk-block"); + assertThat(result.getRiskScore()).isEqualTo(0.85); + } - @Test - @DisplayName("PolicyEvaluationResult - should serialize to JSON") - void policyEvaluationResultShouldSerialize() throws Exception { - PolicyEvaluationResult result = PolicyEvaluationResult.builder() + @Test + @DisplayName("PolicyEvaluationResult - should serialize to JSON") + void policyEvaluationResultShouldSerialize() throws Exception { + PolicyEvaluationResult result = + PolicyEvaluationResult.builder() .allowed(true) .appliedPolicies(Arrays.asList("policy-1", "policy-2")) .riskScore(0.1) .build(); - String json = objectMapper.writeValueAsString(result); + String json = objectMapper.writeValueAsString(result); - assertThat(json).contains("\"allowed\":true"); - assertThat(json).contains("\"applied_policies\""); - assertThat(json).contains("\"risk_score\":0.1"); - } + assertThat(json).contains("\"allowed\":true"); + assertThat(json).contains("\"applied_policies\""); + assertThat(json).contains("\"risk_score\":0.1"); + } - @Test - @DisplayName("PolicyEvaluationResult - equals and hashCode") - void policyEvaluationResultEqualsAndHashCode() { - List policies = Arrays.asList("policy-1"); - PolicyEvaluationResult result1 = PolicyEvaluationResult.builder() + @Test + @DisplayName("PolicyEvaluationResult - equals and hashCode") + void policyEvaluationResultEqualsAndHashCode() { + List policies = Arrays.asList("policy-1"); + PolicyEvaluationResult result1 = + PolicyEvaluationResult.builder() .allowed(true) .appliedPolicies(policies) .riskScore(0.5) .build(); - PolicyEvaluationResult result2 = PolicyEvaluationResult.builder() + PolicyEvaluationResult result2 = + PolicyEvaluationResult.builder() .allowed(true) .appliedPolicies(policies) .riskScore(0.5) .build(); - assertThat(result1).isEqualTo(result2); - assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); - } + assertThat(result1).isEqualTo(result2); + assertThat(result1.hashCode()).isEqualTo(result2.hashCode()); + } - @Test - @DisplayName("PolicyEvaluationResult - toString contains all fields") - void policyEvaluationResultToStringShouldContainAllFields() { - PolicyEvaluationResult result = PolicyEvaluationResult.builder() + @Test + @DisplayName("PolicyEvaluationResult - toString contains all fields") + void policyEvaluationResultToStringShouldContainAllFields() { + PolicyEvaluationResult result = + PolicyEvaluationResult.builder() .allowed(false) .appliedPolicies(Arrays.asList("test-policy")) .riskScore(0.75) .build(); - String str = result.toString(); + String str = result.toString(); - assertThat(str).contains("allowed=false"); - assertThat(str).contains("test-policy"); - assertThat(str).contains("0.75"); - } + assertThat(str).contains("allowed=false"); + assertThat(str).contains("test-policy"); + assertThat(str).contains("0.75"); + } - // PlanExecutionResponse tests + // PlanExecutionResponse tests - @Test - @DisplayName("PlanExecutionResponse - should deserialize from JSON") - void planExecutionResponseShouldDeserialize() throws Exception { - String json = "{" + @Test + @DisplayName("PlanExecutionResponse - should deserialize from JSON") + void planExecutionResponseShouldDeserialize() throws Exception { + String json = + "{" + "\"plan_id\": \"plan-123\"," + "\"status\": \"completed\"," + "\"result\": \"Plan executed successfully\"," @@ -271,20 +262,21 @@ void planExecutionResponseShouldDeserialize() throws Exception { + "}" + "}"; - PlanExecutionResponse response = objectMapper.readValue(json, PlanExecutionResponse.class); - - assertThat(response.isCompleted()).isTrue(); - assertThat(response.getPlanId()).isEqualTo("plan-123"); - assertThat(response.getResult()).isEqualTo("Plan executed successfully"); - assertThat(response.getPolicyInfo()).isNotNull(); - assertThat(response.getPolicyInfo().isAllowed()).isTrue(); - assertThat(response.getPolicyInfo().getAppliedPolicies()).containsExactly("cost-limit"); - } - - @Test - @DisplayName("PlanExecutionResponse - should handle blocked response") - void planExecutionResponseShouldHandleBlockedResponse() throws Exception { - String json = "{" + PlanExecutionResponse response = objectMapper.readValue(json, PlanExecutionResponse.class); + + assertThat(response.isCompleted()).isTrue(); + assertThat(response.getPlanId()).isEqualTo("plan-123"); + assertThat(response.getResult()).isEqualTo("Plan executed successfully"); + assertThat(response.getPolicyInfo()).isNotNull(); + assertThat(response.getPolicyInfo().isAllowed()).isTrue(); + assertThat(response.getPolicyInfo().getAppliedPolicies()).containsExactly("cost-limit"); + } + + @Test + @DisplayName("PlanExecutionResponse - should handle blocked response") + void planExecutionResponseShouldHandleBlockedResponse() throws Exception { + String json = + "{" + "\"plan_id\": \"plan-456\"," + "\"status\": \"blocked\"," + "\"result\": \"Plan execution blocked by policy\"," @@ -297,123 +289,102 @@ void planExecutionResponseShouldHandleBlockedResponse() throws Exception { + "}" + "}"; - PlanExecutionResponse response = objectMapper.readValue(json, PlanExecutionResponse.class); - - assertThat(response.isBlocked()).isTrue(); - assertThat(response.isCompleted()).isFalse(); - assertThat(response.getResult()).isEqualTo("Plan execution blocked by policy"); - assertThat(response.getPolicyInfo().isAllowed()).isFalse(); - } - - @Test - @DisplayName("PlanExecutionResponse - should construct with all fields") - void planExecutionResponseShouldConstructWithAllFields() { - PolicyEvaluationResult policyInfo = PolicyEvaluationResult.builder() - .allowed(true) - .riskScore(0.1) - .build(); - - PlanExecutionResponse response = new PlanExecutionResponse( - "plan-789", - "completed", - "done", - 3, - 3, - null, - null, - null, - policyInfo, - null - ); - - assertThat(response.isCompleted()).isTrue(); - assertThat(response.getPlanId()).isEqualTo("plan-789"); - assertThat(response.getResult()).isEqualTo("done"); - assertThat(response.getPolicyInfo()).isEqualTo(policyInfo); - } - - @Test - @DisplayName("PlanExecutionResponse - equals and hashCode") - void planExecutionResponseEqualsAndHashCode() { - PolicyEvaluationResult policyInfo = PolicyEvaluationResult.builder() - .allowed(true) - .riskScore(0.5) - .build(); - - PlanExecutionResponse response1 = new PlanExecutionResponse( - "plan-123", "completed", "done", 2, 2, null, null, null, policyInfo, null - ); - - PlanExecutionResponse response2 = new PlanExecutionResponse( - "plan-123", "completed", "done", 2, 2, null, null, null, policyInfo, null - ); - - assertThat(response1).isEqualTo(response2); - assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); - } - - @Test - @DisplayName("PlanExecutionResponse - toString contains all fields") - void planExecutionResponseToStringShouldContainAllFields() { - PlanExecutionResponse response = new PlanExecutionResponse( - "plan-test", "completed", "test-result", 1, 2, null, null, null, null, null - ); - - String str = response.toString(); - - assertThat(str).contains("plan-test"); - assertThat(str).contains("completed"); - } - - @Test - @DisplayName("PlanExecutionResponse - status helper methods") - void planExecutionResponseStatusHelperMethods() { - PlanExecutionResponse completed = new PlanExecutionResponse( - "p1", "completed", null, 3, 3, null, null, null, null, null - ); - PlanExecutionResponse failed = new PlanExecutionResponse( - "p2", "failed", null, 1, 3, null, null, null, null, null - ); - PlanExecutionResponse blocked = new PlanExecutionResponse( - "p3", "blocked", null, 0, 3, null, null, null, null, null - ); - PlanExecutionResponse inProgress = new PlanExecutionResponse( - "p4", "in_progress", null, 1, 3, null, null, null, null, null - ); - - assertThat(completed.isCompleted()).isTrue(); - assertThat(completed.isFailed()).isFalse(); - assertThat(completed.isBlocked()).isFalse(); - - assertThat(failed.isFailed()).isTrue(); - assertThat(failed.isCompleted()).isFalse(); - - assertThat(blocked.isBlocked()).isTrue(); - assertThat(blocked.isCompleted()).isFalse(); - - assertThat(inProgress.isInProgress()).isTrue(); - assertThat(inProgress.isCompleted()).isFalse(); - } - - @Test - @DisplayName("PlanExecutionResponse - progress calculation") - void planExecutionResponseProgressCalculation() { - PlanExecutionResponse halfDone = new PlanExecutionResponse( - "p1", "in_progress", null, 2, 4, null, null, null, null, null - ); - PlanExecutionResponse allDone = new PlanExecutionResponse( - "p2", "completed", null, 3, 3, null, null, null, null, null - ); - PlanExecutionResponse notStarted = new PlanExecutionResponse( - "p3", "pending", null, 0, 5, null, null, null, null, null - ); - PlanExecutionResponse zeroSteps = new PlanExecutionResponse( - "p4", "pending", null, 0, 0, null, null, null, null, null - ); - - assertThat(halfDone.getProgress()).isEqualTo(0.5); - assertThat(allDone.getProgress()).isEqualTo(1.0); - assertThat(notStarted.getProgress()).isEqualTo(0.0); - assertThat(zeroSteps.getProgress()).isEqualTo(0.0); - } + PlanExecutionResponse response = objectMapper.readValue(json, PlanExecutionResponse.class); + + assertThat(response.isBlocked()).isTrue(); + assertThat(response.isCompleted()).isFalse(); + assertThat(response.getResult()).isEqualTo("Plan execution blocked by policy"); + assertThat(response.getPolicyInfo().isAllowed()).isFalse(); + } + + @Test + @DisplayName("PlanExecutionResponse - should construct with all fields") + void planExecutionResponseShouldConstructWithAllFields() { + PolicyEvaluationResult policyInfo = + PolicyEvaluationResult.builder().allowed(true).riskScore(0.1).build(); + + PlanExecutionResponse response = + new PlanExecutionResponse( + "plan-789", "completed", "done", 3, 3, null, null, null, policyInfo, null); + + assertThat(response.isCompleted()).isTrue(); + assertThat(response.getPlanId()).isEqualTo("plan-789"); + assertThat(response.getResult()).isEqualTo("done"); + assertThat(response.getPolicyInfo()).isEqualTo(policyInfo); + } + + @Test + @DisplayName("PlanExecutionResponse - equals and hashCode") + void planExecutionResponseEqualsAndHashCode() { + PolicyEvaluationResult policyInfo = + PolicyEvaluationResult.builder().allowed(true).riskScore(0.5).build(); + + PlanExecutionResponse response1 = + new PlanExecutionResponse( + "plan-123", "completed", "done", 2, 2, null, null, null, policyInfo, null); + + PlanExecutionResponse response2 = + new PlanExecutionResponse( + "plan-123", "completed", "done", 2, 2, null, null, null, policyInfo, null); + + assertThat(response1).isEqualTo(response2); + assertThat(response1.hashCode()).isEqualTo(response2.hashCode()); + } + + @Test + @DisplayName("PlanExecutionResponse - toString contains all fields") + void planExecutionResponseToStringShouldContainAllFields() { + PlanExecutionResponse response = + new PlanExecutionResponse( + "plan-test", "completed", "test-result", 1, 2, null, null, null, null, null); + + String str = response.toString(); + + assertThat(str).contains("plan-test"); + assertThat(str).contains("completed"); + } + + @Test + @DisplayName("PlanExecutionResponse - status helper methods") + void planExecutionResponseStatusHelperMethods() { + PlanExecutionResponse completed = + new PlanExecutionResponse("p1", "completed", null, 3, 3, null, null, null, null, null); + PlanExecutionResponse failed = + new PlanExecutionResponse("p2", "failed", null, 1, 3, null, null, null, null, null); + PlanExecutionResponse blocked = + new PlanExecutionResponse("p3", "blocked", null, 0, 3, null, null, null, null, null); + PlanExecutionResponse inProgress = + new PlanExecutionResponse("p4", "in_progress", null, 1, 3, null, null, null, null, null); + + assertThat(completed.isCompleted()).isTrue(); + assertThat(completed.isFailed()).isFalse(); + assertThat(completed.isBlocked()).isFalse(); + + assertThat(failed.isFailed()).isTrue(); + assertThat(failed.isCompleted()).isFalse(); + + assertThat(blocked.isBlocked()).isTrue(); + assertThat(blocked.isCompleted()).isFalse(); + + assertThat(inProgress.isInProgress()).isTrue(); + assertThat(inProgress.isCompleted()).isFalse(); + } + + @Test + @DisplayName("PlanExecutionResponse - progress calculation") + void planExecutionResponseProgressCalculation() { + PlanExecutionResponse halfDone = + new PlanExecutionResponse("p1", "in_progress", null, 2, 4, null, null, null, null, null); + PlanExecutionResponse allDone = + new PlanExecutionResponse("p2", "completed", null, 3, 3, null, null, null, null, null); + PlanExecutionResponse notStarted = + new PlanExecutionResponse("p3", "pending", null, 0, 5, null, null, null, null, null); + PlanExecutionResponse zeroSteps = + new PlanExecutionResponse("p4", "pending", null, 0, 0, null, null, null, null, null); + + assertThat(halfDone.getProgress()).isEqualTo(0.5); + assertThat(allDone.getProgress()).isEqualTo(1.0); + assertThat(notStarted.getProgress()).isEqualTo(0.0); + assertThat(zeroSteps.getProgress()).isEqualTo(0.0); + } } diff --git a/src/test/java/com/getaxonflow/sdk/util/CacheConfigTest.java b/src/test/java/com/getaxonflow/sdk/util/CacheConfigTest.java index dd5f650..dc3b302 100644 --- a/src/test/java/com/getaxonflow/sdk/util/CacheConfigTest.java +++ b/src/test/java/com/getaxonflow/sdk/util/CacheConfigTest.java @@ -15,77 +15,73 @@ */ package com.getaxonflow.sdk.util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("CacheConfig") class CacheConfigTest { - @Test - @DisplayName("should create defaults") - void shouldCreateDefaults() { - CacheConfig config = CacheConfig.defaults(); + @Test + @DisplayName("should create defaults") + void shouldCreateDefaults() { + CacheConfig config = CacheConfig.defaults(); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getTtl()).isEqualTo(Duration.ofSeconds(60)); - assertThat(config.getMaxSize()).isEqualTo(1000); - } + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getTtl()).isEqualTo(Duration.ofSeconds(60)); + assertThat(config.getMaxSize()).isEqualTo(1000); + } - @Test - @DisplayName("should create disabled config") - void shouldCreateDisabled() { - CacheConfig config = CacheConfig.disabled(); + @Test + @DisplayName("should create disabled config") + void shouldCreateDisabled() { + CacheConfig config = CacheConfig.disabled(); - assertThat(config.isEnabled()).isFalse(); - } + assertThat(config.isEnabled()).isFalse(); + } - @Test - @DisplayName("should build with custom values") - void shouldBuildWithCustomValues() { - CacheConfig config = CacheConfig.builder() - .enabled(true) - .ttl(Duration.ofMinutes(5)) - .maxSize(500) - .build(); + @Test + @DisplayName("should build with custom values") + void shouldBuildWithCustomValues() { + CacheConfig config = + CacheConfig.builder().enabled(true).ttl(Duration.ofMinutes(5)).maxSize(500).build(); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getTtl()).isEqualTo(Duration.ofMinutes(5)); - assertThat(config.getMaxSize()).isEqualTo(500); - } + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getTtl()).isEqualTo(Duration.ofMinutes(5)); + assertThat(config.getMaxSize()).isEqualTo(500); + } - @Test - @DisplayName("should validate TTL") - void shouldValidateTtl() { - assertThatThrownBy(() -> CacheConfig.builder().ttl(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ttl"); + @Test + @DisplayName("should validate TTL") + void shouldValidateTtl() { + assertThatThrownBy(() -> CacheConfig.builder().ttl(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ttl"); - assertThatThrownBy(() -> CacheConfig.builder().ttl(Duration.ofSeconds(-1)).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("ttl"); - } + assertThatThrownBy(() -> CacheConfig.builder().ttl(Duration.ofSeconds(-1)).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("ttl"); + } - @Test - @DisplayName("should validate max size") - void shouldValidateMaxSize() { - assertThatThrownBy(() -> CacheConfig.builder().maxSize(0).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("maxSize"); - } + @Test + @DisplayName("should validate max size") + void shouldValidateMaxSize() { + assertThatThrownBy(() -> CacheConfig.builder().maxSize(0).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("maxSize"); + } - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - CacheConfig config1 = CacheConfig.builder().maxSize(100).build(); - CacheConfig config2 = CacheConfig.builder().maxSize(100).build(); - CacheConfig config3 = CacheConfig.builder().maxSize(200).build(); + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + CacheConfig config1 = CacheConfig.builder().maxSize(100).build(); + CacheConfig config2 = CacheConfig.builder().maxSize(100).build(); + CacheConfig config3 = CacheConfig.builder().maxSize(200).build(); - assertThat(config1).isEqualTo(config2); - assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); - assertThat(config1).isNotEqualTo(config3); - } + assertThat(config1).isEqualTo(config2); + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); + assertThat(config1).isNotEqualTo(config3); + } } diff --git a/src/test/java/com/getaxonflow/sdk/util/HttpClientFactoryTest.java b/src/test/java/com/getaxonflow/sdk/util/HttpClientFactoryTest.java index ee98b9c..aa7e6e3 100644 --- a/src/test/java/com/getaxonflow/sdk/util/HttpClientFactoryTest.java +++ b/src/test/java/com/getaxonflow/sdk/util/HttpClientFactoryTest.java @@ -15,76 +15,70 @@ */ package com.getaxonflow.sdk.util; +import static org.assertj.core.api.Assertions.*; + import com.getaxonflow.sdk.AxonFlowConfig; +import java.time.Duration; import okhttp3.OkHttpClient; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.DisplayName; - -import java.time.Duration; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.Test; @DisplayName("HttpClientFactory") class HttpClientFactoryTest { - @Test - @DisplayName("should create client with default config") - void shouldCreateClientWithDefaultConfig() { - AxonFlowConfig config = AxonFlowConfig.builder() - .agentUrl("http://localhost:8080") - .build(); - - OkHttpClient client = HttpClientFactory.create(config); - - assertThat(client).isNotNull(); - // Default timeout is 60 seconds - assertThat(client.connectTimeoutMillis()).isEqualTo(60000); - assertThat(client.readTimeoutMillis()).isEqualTo(60000); - assertThat(client.writeTimeoutMillis()).isEqualTo(60000); - } - - @Test - @DisplayName("should create client with custom timeout") - void shouldCreateClientWithCustomTimeout() { - AxonFlowConfig config = AxonFlowConfig.builder() + @Test + @DisplayName("should create client with default config") + void shouldCreateClientWithDefaultConfig() { + AxonFlowConfig config = AxonFlowConfig.builder().agentUrl("http://localhost:8080").build(); + + OkHttpClient client = HttpClientFactory.create(config); + + assertThat(client).isNotNull(); + // Default timeout is 60 seconds + assertThat(client.connectTimeoutMillis()).isEqualTo(60000); + assertThat(client.readTimeoutMillis()).isEqualTo(60000); + assertThat(client.writeTimeoutMillis()).isEqualTo(60000); + } + + @Test + @DisplayName("should create client with custom timeout") + void shouldCreateClientWithCustomTimeout() { + AxonFlowConfig config = + AxonFlowConfig.builder() .agentUrl("http://localhost:8080") .timeout(Duration.ofSeconds(10)) .build(); - OkHttpClient client = HttpClientFactory.create(config); + OkHttpClient client = HttpClientFactory.create(config); - assertThat(client.connectTimeoutMillis()).isEqualTo(10000); - assertThat(client.readTimeoutMillis()).isEqualTo(10000); - assertThat(client.writeTimeoutMillis()).isEqualTo(10000); - } + assertThat(client.connectTimeoutMillis()).isEqualTo(10000); + assertThat(client.readTimeoutMillis()).isEqualTo(10000); + assertThat(client.writeTimeoutMillis()).isEqualTo(10000); + } - @Test - @DisplayName("should create client with debug mode") - void shouldCreateClientWithDebugMode() { - AxonFlowConfig config = AxonFlowConfig.builder() - .agentUrl("http://localhost:8080") - .debug(true) - .build(); + @Test + @DisplayName("should create client with debug mode") + void shouldCreateClientWithDebugMode() { + AxonFlowConfig config = + AxonFlowConfig.builder().agentUrl("http://localhost:8080").debug(true).build(); - OkHttpClient client = HttpClientFactory.create(config); + OkHttpClient client = HttpClientFactory.create(config); - assertThat(client).isNotNull(); - // Debug mode adds an interceptor - assertThat(client.interceptors()).hasSize(1); - } + assertThat(client).isNotNull(); + // Debug mode adds an interceptor + assertThat(client.interceptors()).hasSize(1); + } - @Test - @DisplayName("should create client with insecure skip verify") - void shouldCreateClientWithInsecureSkipVerify() { - AxonFlowConfig config = AxonFlowConfig.builder() - .agentUrl("http://localhost:8080") - .insecureSkipVerify(true) - .build(); + @Test + @DisplayName("should create client with insecure skip verify") + void shouldCreateClientWithInsecureSkipVerify() { + AxonFlowConfig config = + AxonFlowConfig.builder().agentUrl("http://localhost:8080").insecureSkipVerify(true).build(); - OkHttpClient client = HttpClientFactory.create(config); + OkHttpClient client = HttpClientFactory.create(config); - assertThat(client).isNotNull(); - // Should have a custom hostname verifier that accepts all hosts - assertThat(client.hostnameVerifier()).isNotNull(); - } + assertThat(client).isNotNull(); + // Should have a custom hostname verifier that accepts all hosts + assertThat(client.hostnameVerifier()).isNotNull(); + } } diff --git a/src/test/java/com/getaxonflow/sdk/util/ResponseCacheTest.java b/src/test/java/com/getaxonflow/sdk/util/ResponseCacheTest.java index dbcf6e3..ba5d0c9 100644 --- a/src/test/java/com/getaxonflow/sdk/util/ResponseCacheTest.java +++ b/src/test/java/com/getaxonflow/sdk/util/ResponseCacheTest.java @@ -15,142 +15,140 @@ */ package com.getaxonflow.sdk.util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; -import java.time.Duration; import java.util.Optional; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("ResponseCache") class ResponseCacheTest { - @Test - @DisplayName("should store and retrieve values") - void shouldStoreAndRetrieveValues() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should store and retrieve values") + void shouldStoreAndRetrieveValues() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - String key = "test-key"; - String value = "test-value"; + String key = "test-key"; + String value = "test-value"; - cache.put(key, value); - Optional retrieved = cache.get(key, String.class); + cache.put(key, value); + Optional retrieved = cache.get(key, String.class); - assertThat(retrieved).isPresent().contains(value); - } + assertThat(retrieved).isPresent().contains(value); + } - @Test - @DisplayName("should return empty for missing keys") - void shouldReturnEmptyForMissingKeys() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should return empty for missing keys") + void shouldReturnEmptyForMissingKeys() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - Optional result = cache.get("nonexistent", String.class); + Optional result = cache.get("nonexistent", String.class); - assertThat(result).isEmpty(); - } + assertThat(result).isEmpty(); + } - @Test - @DisplayName("should return empty for wrong type") - void shouldReturnEmptyForWrongType() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should return empty for wrong type") + void shouldReturnEmptyForWrongType() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - cache.put("key", "string-value"); - Optional result = cache.get("key", Integer.class); + cache.put("key", "string-value"); + Optional result = cache.get("key", Integer.class); - assertThat(result).isEmpty(); - } + assertThat(result).isEmpty(); + } - @Test - @DisplayName("should not cache when disabled") - void shouldNotCacheWhenDisabled() { - ResponseCache cache = new ResponseCache(CacheConfig.disabled()); + @Test + @DisplayName("should not cache when disabled") + void shouldNotCacheWhenDisabled() { + ResponseCache cache = new ResponseCache(CacheConfig.disabled()); - cache.put("key", "value"); - Optional result = cache.get("key", String.class); + cache.put("key", "value"); + Optional result = cache.get("key", String.class); - assertThat(result).isEmpty(); - } + assertThat(result).isEmpty(); + } - @Test - @DisplayName("should invalidate specific key") - void shouldInvalidateSpecificKey() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should invalidate specific key") + void shouldInvalidateSpecificKey() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - cache.put("key1", "value1"); - cache.put("key2", "value2"); + cache.put("key1", "value1"); + cache.put("key2", "value2"); - cache.invalidate("key1"); + cache.invalidate("key1"); - assertThat(cache.get("key1", String.class)).isEmpty(); - assertThat(cache.get("key2", String.class)).isPresent(); - } + assertThat(cache.get("key1", String.class)).isEmpty(); + assertThat(cache.get("key2", String.class)).isPresent(); + } - @Test - @DisplayName("should clear all entries") - void shouldClearAllEntries() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should clear all entries") + void shouldClearAllEntries() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - cache.put("key1", "value1"); - cache.put("key2", "value2"); + cache.put("key1", "value1"); + cache.put("key2", "value2"); - cache.clear(); + cache.clear(); - assertThat(cache.get("key1", String.class)).isEmpty(); - assertThat(cache.get("key2", String.class)).isEmpty(); - } + assertThat(cache.get("key1", String.class)).isEmpty(); + assertThat(cache.get("key2", String.class)).isEmpty(); + } - @Test - @DisplayName("should generate consistent cache keys") - void shouldGenerateConsistentKeys() { - String key1 = ResponseCache.generateKey("chat", "hello", "user-123"); - String key2 = ResponseCache.generateKey("chat", "hello", "user-123"); - String key3 = ResponseCache.generateKey("chat", "hello", "user-456"); + @Test + @DisplayName("should generate consistent cache keys") + void shouldGenerateConsistentKeys() { + String key1 = ResponseCache.generateKey("chat", "hello", "user-123"); + String key2 = ResponseCache.generateKey("chat", "hello", "user-123"); + String key3 = ResponseCache.generateKey("chat", "hello", "user-456"); - assertThat(key1).isEqualTo(key2); - assertThat(key1).isNotEqualTo(key3); - } + assertThat(key1).isEqualTo(key2); + assertThat(key1).isNotEqualTo(key3); + } - @Test - @DisplayName("should handle null values in key generation") - void shouldHandleNullsInKeyGeneration() { - String key1 = ResponseCache.generateKey(null, "query", "user"); - String key2 = ResponseCache.generateKey("", "query", "user"); - String key3 = ResponseCache.generateKey("type", null, null); + @Test + @DisplayName("should handle null values in key generation") + void shouldHandleNullsInKeyGeneration() { + String key1 = ResponseCache.generateKey(null, "query", "user"); + String key2 = ResponseCache.generateKey("", "query", "user"); + String key3 = ResponseCache.generateKey("type", null, null); - assertThat(key1).isNotEmpty(); - assertThat(key2).isNotEmpty(); - assertThat(key3).isNotEmpty(); - } + assertThat(key1).isNotEmpty(); + assertThat(key2).isNotEmpty(); + assertThat(key3).isNotEmpty(); + } - @Test - @DisplayName("should provide stats") - void shouldProvideStats() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should provide stats") + void shouldProvideStats() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - String stats = cache.getStats(); + String stats = cache.getStats(); - assertThat(stats).isNotEmpty(); - } + assertThat(stats).isNotEmpty(); + } - @Test - @DisplayName("should provide stats for disabled cache") - void shouldProvideStatsForDisabledCache() { - ResponseCache cache = new ResponseCache(CacheConfig.disabled()); + @Test + @DisplayName("should provide stats for disabled cache") + void shouldProvideStatsForDisabledCache() { + ResponseCache cache = new ResponseCache(CacheConfig.disabled()); - String stats = cache.getStats(); + String stats = cache.getStats(); - assertThat(stats).isEqualTo("Cache disabled"); - } + assertThat(stats).isEqualTo("Cache disabled"); + } - @Test - @DisplayName("should not cache null values") - void shouldNotCacheNullValues() { - ResponseCache cache = new ResponseCache(CacheConfig.defaults()); + @Test + @DisplayName("should not cache null values") + void shouldNotCacheNullValues() { + ResponseCache cache = new ResponseCache(CacheConfig.defaults()); - cache.put("key", null); - Optional result = cache.get("key", String.class); + cache.put("key", null); + Optional result = cache.get("key", String.class); - assertThat(result).isEmpty(); - } + assertThat(result).isEmpty(); + } } diff --git a/src/test/java/com/getaxonflow/sdk/util/RetryConfigTest.java b/src/test/java/com/getaxonflow/sdk/util/RetryConfigTest.java index 55a7dff..1d1003e 100644 --- a/src/test/java/com/getaxonflow/sdk/util/RetryConfigTest.java +++ b/src/test/java/com/getaxonflow/sdk/util/RetryConfigTest.java @@ -15,40 +15,40 @@ */ package com.getaxonflow.sdk.util; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; import java.time.Duration; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("RetryConfig") class RetryConfigTest { - @Test - @DisplayName("should create defaults") - void shouldCreateDefaults() { - RetryConfig config = RetryConfig.defaults(); - - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getMaxAttempts()).isEqualTo(3); - assertThat(config.getInitialDelay()).isEqualTo(Duration.ofSeconds(1)); - assertThat(config.getMaxDelay()).isEqualTo(Duration.ofSeconds(30)); - assertThat(config.getMultiplier()).isEqualTo(2.0); - } - - @Test - @DisplayName("should create disabled config") - void shouldCreateDisabled() { - RetryConfig config = RetryConfig.disabled(); - - assertThat(config.isEnabled()).isFalse(); - } - - @Test - @DisplayName("should build with custom values") - void shouldBuildWithCustomValues() { - RetryConfig config = RetryConfig.builder() + @Test + @DisplayName("should create defaults") + void shouldCreateDefaults() { + RetryConfig config = RetryConfig.defaults(); + + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getMaxAttempts()).isEqualTo(3); + assertThat(config.getInitialDelay()).isEqualTo(Duration.ofSeconds(1)); + assertThat(config.getMaxDelay()).isEqualTo(Duration.ofSeconds(30)); + assertThat(config.getMultiplier()).isEqualTo(2.0); + } + + @Test + @DisplayName("should create disabled config") + void shouldCreateDisabled() { + RetryConfig config = RetryConfig.disabled(); + + assertThat(config.isEnabled()).isFalse(); + } + + @Test + @DisplayName("should build with custom values") + void shouldBuildWithCustomValues() { + RetryConfig config = + RetryConfig.builder() .enabled(true) .maxAttempts(5) .initialDelay(Duration.ofMillis(500)) @@ -56,83 +56,85 @@ void shouldBuildWithCustomValues() { .multiplier(1.5) .build(); - assertThat(config.isEnabled()).isTrue(); - assertThat(config.getMaxAttempts()).isEqualTo(5); - assertThat(config.getInitialDelay()).isEqualTo(Duration.ofMillis(500)); - assertThat(config.getMaxDelay()).isEqualTo(Duration.ofSeconds(10)); - assertThat(config.getMultiplier()).isEqualTo(1.5); - } - - @Test - @DisplayName("should calculate delay for attempts") - void shouldCalculateDelayForAttempts() { - RetryConfig config = RetryConfig.builder() + assertThat(config.isEnabled()).isTrue(); + assertThat(config.getMaxAttempts()).isEqualTo(5); + assertThat(config.getInitialDelay()).isEqualTo(Duration.ofMillis(500)); + assertThat(config.getMaxDelay()).isEqualTo(Duration.ofSeconds(10)); + assertThat(config.getMultiplier()).isEqualTo(1.5); + } + + @Test + @DisplayName("should calculate delay for attempts") + void shouldCalculateDelayForAttempts() { + RetryConfig config = + RetryConfig.builder() .initialDelay(Duration.ofSeconds(1)) .multiplier(2.0) .maxDelay(Duration.ofSeconds(30)) .build(); - assertThat(config.getDelayForAttempt(1)).isEqualTo(Duration.ofSeconds(1)); - assertThat(config.getDelayForAttempt(2)).isEqualTo(Duration.ofSeconds(2)); - assertThat(config.getDelayForAttempt(3)).isEqualTo(Duration.ofSeconds(4)); - assertThat(config.getDelayForAttempt(4)).isEqualTo(Duration.ofSeconds(8)); - } - - @Test - @DisplayName("should cap delay at max") - void shouldCapDelayAtMax() { - RetryConfig config = RetryConfig.builder() + assertThat(config.getDelayForAttempt(1)).isEqualTo(Duration.ofSeconds(1)); + assertThat(config.getDelayForAttempt(2)).isEqualTo(Duration.ofSeconds(2)); + assertThat(config.getDelayForAttempt(3)).isEqualTo(Duration.ofSeconds(4)); + assertThat(config.getDelayForAttempt(4)).isEqualTo(Duration.ofSeconds(8)); + } + + @Test + @DisplayName("should cap delay at max") + void shouldCapDelayAtMax() { + RetryConfig config = + RetryConfig.builder() .initialDelay(Duration.ofSeconds(10)) .multiplier(2.0) .maxDelay(Duration.ofSeconds(15)) .build(); - assertThat(config.getDelayForAttempt(1)).isEqualTo(Duration.ofSeconds(10)); - assertThat(config.getDelayForAttempt(2)).isEqualTo(Duration.ofSeconds(15)); // Capped - assertThat(config.getDelayForAttempt(3)).isEqualTo(Duration.ofSeconds(15)); // Capped - } - - @Test - @DisplayName("should validate max attempts range") - void shouldValidateMaxAttempts() { - assertThatThrownBy(() -> RetryConfig.builder().maxAttempts(0).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("maxAttempts"); - - assertThatThrownBy(() -> RetryConfig.builder().maxAttempts(11).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("maxAttempts"); - } - - @Test - @DisplayName("should validate initial delay") - void shouldValidateInitialDelay() { - assertThatThrownBy(() -> RetryConfig.builder().initialDelay(null).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("initialDelay"); - - assertThatThrownBy(() -> RetryConfig.builder().initialDelay(Duration.ofSeconds(-1)).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("initialDelay"); - } - - @Test - @DisplayName("should validate multiplier") - void shouldValidateMultiplier() { - assertThatThrownBy(() -> RetryConfig.builder().multiplier(0.5).build()) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("multiplier"); - } - - @Test - @DisplayName("should implement equals and hashCode") - void shouldImplementEqualsAndHashCode() { - RetryConfig config1 = RetryConfig.builder().maxAttempts(3).build(); - RetryConfig config2 = RetryConfig.builder().maxAttempts(3).build(); - RetryConfig config3 = RetryConfig.builder().maxAttempts(5).build(); - - assertThat(config1).isEqualTo(config2); - assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); - assertThat(config1).isNotEqualTo(config3); - } + assertThat(config.getDelayForAttempt(1)).isEqualTo(Duration.ofSeconds(10)); + assertThat(config.getDelayForAttempt(2)).isEqualTo(Duration.ofSeconds(15)); // Capped + assertThat(config.getDelayForAttempt(3)).isEqualTo(Duration.ofSeconds(15)); // Capped + } + + @Test + @DisplayName("should validate max attempts range") + void shouldValidateMaxAttempts() { + assertThatThrownBy(() -> RetryConfig.builder().maxAttempts(0).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("maxAttempts"); + + assertThatThrownBy(() -> RetryConfig.builder().maxAttempts(11).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("maxAttempts"); + } + + @Test + @DisplayName("should validate initial delay") + void shouldValidateInitialDelay() { + assertThatThrownBy(() -> RetryConfig.builder().initialDelay(null).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("initialDelay"); + + assertThatThrownBy(() -> RetryConfig.builder().initialDelay(Duration.ofSeconds(-1)).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("initialDelay"); + } + + @Test + @DisplayName("should validate multiplier") + void shouldValidateMultiplier() { + assertThatThrownBy(() -> RetryConfig.builder().multiplier(0.5).build()) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("multiplier"); + } + + @Test + @DisplayName("should implement equals and hashCode") + void shouldImplementEqualsAndHashCode() { + RetryConfig config1 = RetryConfig.builder().maxAttempts(3).build(); + RetryConfig config2 = RetryConfig.builder().maxAttempts(3).build(); + RetryConfig config3 = RetryConfig.builder().maxAttempts(5).build(); + + assertThat(config1).isEqualTo(config2); + assertThat(config1.hashCode()).isEqualTo(config2.hashCode()); + assertThat(config1).isNotEqualTo(config3); + } } diff --git a/src/test/java/com/getaxonflow/sdk/util/RetryExecutorTest.java b/src/test/java/com/getaxonflow/sdk/util/RetryExecutorTest.java index 5fbb6d8..0d3ca81 100644 --- a/src/test/java/com/getaxonflow/sdk/util/RetryExecutorTest.java +++ b/src/test/java/com/getaxonflow/sdk/util/RetryExecutorTest.java @@ -15,189 +15,208 @@ */ package com.getaxonflow.sdk.util; -import com.getaxonflow.sdk.exceptions.*; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.DisplayName; +import static org.assertj.core.api.Assertions.*; +import com.getaxonflow.sdk.exceptions.*; import java.io.IOException; import java.net.SocketTimeoutException; import java.time.Duration; import java.util.concurrent.atomic.AtomicInteger; - -import static org.assertj.core.api.Assertions.*; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; @DisplayName("RetryExecutor") class RetryExecutorTest { - @Test - @DisplayName("should execute without retry on success") - void shouldExecuteWithoutRetryOnSuccess() { - RetryExecutor executor = new RetryExecutor(RetryConfig.defaults()); - AtomicInteger attempts = new AtomicInteger(0); - - String result = executor.execute(() -> { - attempts.incrementAndGet(); - return "success"; - }, "test"); - - assertThat(result).isEqualTo("success"); - assertThat(attempts.get()).isEqualTo(1); - } - - @Test - @DisplayName("should retry on IOException") - void shouldRetryOnIOException() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(3) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - String result = executor.execute(() -> { - if (attempts.incrementAndGet() < 3) { + @Test + @DisplayName("should execute without retry on success") + void shouldExecuteWithoutRetryOnSuccess() { + RetryExecutor executor = new RetryExecutor(RetryConfig.defaults()); + AtomicInteger attempts = new AtomicInteger(0); + + String result = + executor.execute( + () -> { + attempts.incrementAndGet(); + return "success"; + }, + "test"); + + assertThat(result).isEqualTo("success"); + assertThat(attempts.get()).isEqualTo(1); + } + + @Test + @DisplayName("should retry on IOException") + void shouldRetryOnIOException() { + RetryConfig config = + RetryConfig.builder().maxAttempts(3).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + String result = + executor.execute( + () -> { + if (attempts.incrementAndGet() < 3) { throw new IOException("Connection failed"); - } - return "success"; - }, "test"); - - assertThat(result).isEqualTo("success"); - assertThat(attempts.get()).isEqualTo(3); - } - - @Test - @DisplayName("should retry on SocketTimeoutException") - void shouldRetryOnSocketTimeout() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(2) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - String result = executor.execute(() -> { - if (attempts.incrementAndGet() < 2) { + } + return "success"; + }, + "test"); + + assertThat(result).isEqualTo("success"); + assertThat(attempts.get()).isEqualTo(3); + } + + @Test + @DisplayName("should retry on SocketTimeoutException") + void shouldRetryOnSocketTimeout() { + RetryConfig config = + RetryConfig.builder().maxAttempts(2).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + String result = + executor.execute( + () -> { + if (attempts.incrementAndGet() < 2) { throw new SocketTimeoutException("Read timed out"); - } - return "success"; - }, "test"); - - assertThat(result).isEqualTo("success"); - assertThat(attempts.get()).isEqualTo(2); - } - - @Test - @DisplayName("should not retry on AuthenticationException") - void shouldNotRetryOnAuthenticationException() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(3) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - assertThatThrownBy(() -> executor.execute(() -> { - attempts.incrementAndGet(); - throw new AuthenticationException("Invalid credentials"); - }, "test")) - .isInstanceOf(AuthenticationException.class); - - assertThat(attempts.get()).isEqualTo(1); // No retry - } - - @Test - @DisplayName("should not retry on PolicyViolationException") - void shouldNotRetryOnPolicyViolationException() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(3) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - assertThatThrownBy(() -> executor.execute(() -> { - attempts.incrementAndGet(); - throw new PolicyViolationException("Blocked by policy"); - }, "test")) - .isInstanceOf(PolicyViolationException.class); - - assertThat(attempts.get()).isEqualTo(1); // No retry - } - - @Test - @DisplayName("should throw after max attempts") - void shouldThrowAfterMaxAttempts() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(3) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - assertThatThrownBy(() -> executor.execute(() -> { - attempts.incrementAndGet(); - throw new IOException("Always fails"); - }, "test")) - .isInstanceOf(ConnectionException.class); - - assertThat(attempts.get()).isEqualTo(3); - } - - @Test - @DisplayName("should not retry when disabled") - void shouldNotRetryWhenDisabled() { - RetryExecutor executor = new RetryExecutor(RetryConfig.disabled()); - AtomicInteger attempts = new AtomicInteger(0); - - assertThatThrownBy(() -> executor.execute(() -> { - attempts.incrementAndGet(); - throw new IOException("Connection failed"); - }, "test")) - .isInstanceOf(ConnectionException.class); - - assertThat(attempts.get()).isEqualTo(1); - } - - @Test - @DisplayName("should retry on RateLimitException") - void shouldRetryOnRateLimitException() { - RetryConfig config = RetryConfig.builder() - .maxAttempts(2) - .initialDelay(Duration.ofMillis(10)) - .build(); - RetryExecutor executor = new RetryExecutor(config); - AtomicInteger attempts = new AtomicInteger(0); - - String result = executor.execute(() -> { - if (attempts.incrementAndGet() < 2) { + } + return "success"; + }, + "test"); + + assertThat(result).isEqualTo("success"); + assertThat(attempts.get()).isEqualTo(2); + } + + @Test + @DisplayName("should not retry on AuthenticationException") + void shouldNotRetryOnAuthenticationException() { + RetryConfig config = + RetryConfig.builder().maxAttempts(3).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + assertThatThrownBy( + () -> + executor.execute( + () -> { + attempts.incrementAndGet(); + throw new AuthenticationException("Invalid credentials"); + }, + "test")) + .isInstanceOf(AuthenticationException.class); + + assertThat(attempts.get()).isEqualTo(1); // No retry + } + + @Test + @DisplayName("should not retry on PolicyViolationException") + void shouldNotRetryOnPolicyViolationException() { + RetryConfig config = + RetryConfig.builder().maxAttempts(3).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + assertThatThrownBy( + () -> + executor.execute( + () -> { + attempts.incrementAndGet(); + throw new PolicyViolationException("Blocked by policy"); + }, + "test")) + .isInstanceOf(PolicyViolationException.class); + + assertThat(attempts.get()).isEqualTo(1); // No retry + } + + @Test + @DisplayName("should throw after max attempts") + void shouldThrowAfterMaxAttempts() { + RetryConfig config = + RetryConfig.builder().maxAttempts(3).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + assertThatThrownBy( + () -> + executor.execute( + () -> { + attempts.incrementAndGet(); + throw new IOException("Always fails"); + }, + "test")) + .isInstanceOf(ConnectionException.class); + + assertThat(attempts.get()).isEqualTo(3); + } + + @Test + @DisplayName("should not retry when disabled") + void shouldNotRetryWhenDisabled() { + RetryExecutor executor = new RetryExecutor(RetryConfig.disabled()); + AtomicInteger attempts = new AtomicInteger(0); + + assertThatThrownBy( + () -> + executor.execute( + () -> { + attempts.incrementAndGet(); + throw new IOException("Connection failed"); + }, + "test")) + .isInstanceOf(ConnectionException.class); + + assertThat(attempts.get()).isEqualTo(1); + } + + @Test + @DisplayName("should retry on RateLimitException") + void shouldRetryOnRateLimitException() { + RetryConfig config = + RetryConfig.builder().maxAttempts(2).initialDelay(Duration.ofMillis(10)).build(); + RetryExecutor executor = new RetryExecutor(config); + AtomicInteger attempts = new AtomicInteger(0); + + String result = + executor.execute( + () -> { + if (attempts.incrementAndGet() < 2) { throw new RateLimitException("Rate limit exceeded"); - } - return "success"; - }, "test"); - - assertThat(result).isEqualTo("success"); - assertThat(attempts.get()).isEqualTo(2); - } - - @Test - @DisplayName("should wrap generic exceptions") - void shouldWrapGenericExceptions() { - RetryExecutor executor = new RetryExecutor(RetryConfig.disabled()); - - assertThatThrownBy(() -> executor.execute(() -> { - throw new RuntimeException("Unexpected error"); - }, "test")) - .isInstanceOf(AxonFlowException.class) - .hasMessageContaining("test"); - } - - @Test - @DisplayName("should handle null config") - void shouldHandleNullConfig() { - RetryExecutor executor = new RetryExecutor(null); - - String result = executor.execute(() -> "success", "test"); - - assertThat(result).isEqualTo("success"); - } + } + return "success"; + }, + "test"); + + assertThat(result).isEqualTo("success"); + assertThat(attempts.get()).isEqualTo(2); + } + + @Test + @DisplayName("should wrap generic exceptions") + void shouldWrapGenericExceptions() { + RetryExecutor executor = new RetryExecutor(RetryConfig.disabled()); + + assertThatThrownBy( + () -> + executor.execute( + () -> { + throw new RuntimeException("Unexpected error"); + }, + "test")) + .isInstanceOf(AxonFlowException.class) + .hasMessageContaining("test"); + } + + @Test + @DisplayName("should handle null config") + void shouldHandleNullConfig() { + RetryExecutor executor = new RetryExecutor(null); + + String result = executor.execute(() -> "success", "test"); + + assertThat(result).isEqualTo("success"); + } } From 585ada4f8b72762563ff499f6841d0814010d00f Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 17:53:27 +0200 Subject: [PATCH 09/11] chore: bump version to 5.0.0, set changelog release date --- CHANGELOG.md | 2 +- pom.xml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7dfe0f2..125b1d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,7 @@ All notable changes to the AxonFlow Java SDK 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). -## [Unreleased] +## [5.0.0] - 2026-04-04 ### BREAKING CHANGES diff --git a/pom.xml b/pom.xml index ae8bdc3..3250a22 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ com.getaxonflow axonflow-sdk - 4.3.0 + 5.0.0 jar AxonFlow Java SDK From 3deea993f83a5e85d56b44878eb9af1e8a4918e7 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 18:02:17 +0200 Subject: [PATCH 10/11] fix: update setMateriality call to setMaterialityClassification in AxonFlow.java --- src/main/java/com/getaxonflow/sdk/AxonFlow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 973b77d..34a3ea9 100644 --- a/src/main/java/com/getaxonflow/sdk/AxonFlow.java +++ b/src/main/java/com/getaxonflow/sdk/AxonFlow.java @@ -6175,7 +6175,7 @@ private AISystemRegistry parseSystemResponse(Response response) throws IOExcepti } if (materiality != null) { try { - system.setMateriality(MaterialityClassification.fromValue(materiality)); + system.setMaterialityClassification(MaterialityClassification.fromValue(materiality)); } catch (IllegalArgumentException e) { logger.warn("Unknown materiality: {}", materiality); } From 93ededcab1b53c96fdb336fef47e323de45d3859 Mon Sep 17 00:00:00 2001 From: Saurabh Jain Date: Sat, 4 Apr 2026 18:07:07 +0200 Subject: [PATCH 11/11] fix: update setMateriality to setMaterialityClassification in test --- src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java index 2b8bb67..9f126d8 100644 --- a/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java +++ b/src/test/java/com/getaxonflow/sdk/masfeat/MASFEATTypesTest.java @@ -341,7 +341,7 @@ void testSettersAndGetters() { registry.setCustomerImpact(3); registry.setModelComplexity(2); registry.setHumanReliance(1); - registry.setMateriality(MaterialityClassification.HIGH); + registry.setMaterialityClassification(MaterialityClassification.HIGH); registry.setStatus(SystemStatus.ACTIVE); registry.setMetadata(metadata); registry.setCreatedAt(now);