diff --git a/CHANGELOG.md b/CHANGELOG.md index 5706f90..125b1d2 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). +## [5.0.0] - 2026-04-04 + +### 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 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 diff --git a/src/main/java/com/getaxonflow/sdk/AxonFlow.java b/src/main/java/com/getaxonflow/sdk/AxonFlow.java index 6644f55..34a3ea9 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 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,6028 +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.setMaterialityClassification(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); - addTenantIdHeader(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 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()); - } - - // 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) { - // 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(); - 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 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); - - 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); - addTenantIdHeader(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 70aa3ca..7a0908c 100644
--- a/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java
+++ b/src/main/java/com/getaxonflow/sdk/masfeat/MASFEATTypes.java
@@ -7,937 +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; - private MaterialityClassification materiality; - 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 getMateriality() { return materiality; } - public void setMateriality(MaterialityClassification materiality) { this.materiality = materiality; } - 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 1a4401a..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,505 +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 not send auth headers when no credentials configured") - void shouldNotSendAuthHeadersWithoutCredentials(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 - AxonFlow client = AxonFlow.create(AxonFlowConfig.builder() - .agentUrl(wmRuntimeInfo.getHttpBaseUrl()) - .build()); - - client.proxyLLMCall( - ClientRequest.builder() - .userToken("") - .query("Test query") - .build() - ); - - // Verify no auth headers were sent when no credentials configured - verify(postRequestedFor(urlEqualTo("/api/request")) - .withoutHeader("X-License-Key") - .withoutHeader("Authorization")); - - System.out.println("✅ Auth headers not sent in community mode (no credentials)"); - } - - @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 4e7fe17..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.getMateriality()).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 cb3cb03..9f126d8 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.getMateriality()).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.setMaterialityClassification(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 0109af3..75177ea 100644 --- a/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java +++ b/src/test/java/com/getaxonflow/sdk/telemetry/TelemetryReporterTest.java @@ -15,439 +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) - TelemetryReporter.sendPing( - "enterprise", - "http://localhost:8080", - 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"); + } }