v0.5.0
Java SDK

Test Java workflows and agents

Use deterministic business and agent tests, then verify real server execution and packaged applications separately.

Test business rules and agent decisions without a network, then test the server boundaries separately. A local harness should give a reproducible result; a live run should prove the transport and durable execution behavior you actually deploy.

Choose the test boundary

LayerWhat it provesWhat it does not prove
Business-stage unit testValidation, result classification and context changesServer scheduling, leases or delivery
Agent harnessScripted decisions through production agent handlers and native toolsReal model quality, queues, distributed retries or persistence
API/transport fixtureRequest/response contracts and error handlingA particular deployed server's credentials and network
Real server runBootstrap, dispatch, execution, state and selected transportEvery possible business failure or provider behavior
Packaged consumerDependencies and public APIs work outside the SDK reactorExternal service availability

Run deterministic agent scenarios

Download the agent demo, install sdk-runtime,sdk-agent,sdk-testing -am from the matching SDK source, and build the demo as described in agents. Then run:

sh
java -cp 'target/classes:target/dependency/*' example.AgentHarnessExample

No server, broker, application key or model key is needed. The complete example is:

java
package example;

import io.pipelogiq.sdk.agent.tools.NativeTool;
import io.pipelogiq.sdk.agent.tools.ToolDefinition;
import io.pipelogiq.sdk.testing.AgentTestHarness;
import io.pipelogiq.sdk.testing.AgentTestResult;
import java.util.Map;

/** Deterministic real-handler exercise: no server, broker, API keys or model-provider calls. */
public final class AgentHarnessExample {
  public static void main(String[] args) {
    var result = runScenario();
    System.out.println("Success: " + result.isSuccess());
    System.out.println("Tools: " + result.toolCallsExecuted());
    System.out.println("Answer: " + result.finalResponse());
    System.out.println("Executed stages: " + result.stageResults().size());
    if (!result.isSuccess()) throw new IllegalStateException(result.errorMessage());
  }

  public static AgentTestResult runScenario() {
    return AgentTestHarness.create()
        .withNativeTool(new ToolDefinition("service_status", "Read demo service status").mutating(false),
            (parameters, context) -> NativeTool.Output.success(Map.of("status", "ready")))
        .withScenario(script -> script.strict().thenCallTool("service_status")
            .thenFinishWith("The service is ready."))
        .run("Check service status", "deterministic-example");
  }
}

Expected output:

text
Success: true
Tools: [service_status]
Answer: The service is ready.
Executed stages: <count>

The strict script chooses one tool and a final answer. The tool and actual agent handlers execute, with input serialization, context patches and dynamically appended stages. This example exercises a read tool; approval behavior requires a separate scenario.

Check waiting, approval and rejection

Create src/main/java/example/AgentApprovalChecks.java in the agent demo:

java
package example;

import io.pipelogiq.sdk.agent.model.ToolCall;
import io.pipelogiq.sdk.agent.tools.NativeTool;
import io.pipelogiq.sdk.agent.tools.ToolDefinition;
import io.pipelogiq.sdk.testing.AgentTestHarness;
import java.util.List;
import java.util.Map;

public final class AgentApprovalChecks {
  private static AgentTestHarness scenario() {
    return AgentTestHarness.create(options -> {
      options.useReActMode = false;
      options.requireConfirmationForMutations = true;
    }).withNativeTool(
        new ToolDefinition("reserve_stock", "Simulate a reservation").mutating(true),
        (parameters, context) -> NativeTool.Output.success(Map.of("reserved", true)))
      .withScenario(script -> script
          .withPlan(new ToolCall("reserve_stock", Map.of(), "reservation"))
          .withSynthesizedResponse("Reservation handled"));
  }

  private static void check(boolean value, String message) {
    if (!value) throw new AssertionError(message);
  }

  public static void main(String[] args) {
    var waiting = scenario().run("Reserve stock");
    check(waiting.isWaitingForApproval(), "Must wait for a decision");
    check(waiting.toolCallsExecuted().isEmpty(), "No mutation before approval");

    var approved = scenario().withApprovalDecision(true).run("Reserve stock");
    check(approved.isSuccess(), approved.errorMessage());
    check(approved.toolCallsExecuted().equals(List.of("reserve_stock")),
        "Approval should execute one mutation");

    var rejected = scenario().withApprovalDecision(false, "Customer cancelled")
        .run("Reserve stock");
    check(rejected.toolCallsExecuted().isEmpty(), "Rejected mutation must not run");
    System.out.println("Approval checks passed: waiting, approved, rejected");
  }
}

Build and run:

sh
"$PIPELOGIQ_SDK_DIR/mvnw" -q compile dependency:copy-dependencies
java -cp 'target/classes:target/dependency/*' example.AgentApprovalChecks

The harness supplies a test notification route and requires explicit decisions. In production, configure a real notification route and an authorized approval surface; a successful offline scenario does not prove delivery to Telegram or another external channel.

Other harness extension points include withToolPolicy, withCritic, withLifecycleObserver, withSessionStore, withMemoryStore and withPlanner. Inspect stageResults, contextSnapshot, notifications, toolCallsExecuted and finalResponse, or use getContextValue(key, type). A harness can be reused sequentially, but should not be run concurrently.

Test business stages through the execution boundary

For business unit tests, construct the stage and test its business dependencies directly. When you also want to verify SDK input conversion, error classification and context serialization, use HandlerRegistry with a small dispatch fixture. Protocol JSON belongs in this boundary test, not in production pipeline creation.

Example src/test/java/example/OrderStageTest.java, using the getting-started business classes:

java
package example;

import io.pipelogiq.sdk.core.Json;
import io.pipelogiq.sdk.core.execution.HandlerRegistry;
import io.pipelogiq.sdk.core.execution.StageContext;
import java.math.BigDecimal;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class OrderStageTest {
  @Test
  void aValidOrderSurvivesSerializedContextAndIsRecorded() {
    var handlers = new HandlerRegistry().stages(ValidateOrder.class, RecordOrder.class);
    var input = Json.object().put("stageHandlerName", "ValidateOrder")
        .put("input", Json.write(new Order("test-1", new BigDecimal("12.00"))));
    var validated = handlers.execute(input);
    assertTrue(validated.path("isSuccess").asBoolean());

    var next = Json.object().put("stageHandlerName", "RecordOrder");
    next.set("contextItems", validated.path("contextItems"));
    var recorded = handlers.execute(next);
    assertTrue(recorded.path("isSuccess").asBoolean());
    var context = new StageContext(
        Json.object().set("contextItems", recorded.path("contextItems")));
    assertEquals(true, context.require("recorded", Boolean.class));
  }

  @Test
  void negativeAmountIsTerminalValidationFailure() {
    var handlers = new HandlerRegistry().register(ValidateOrder.class);
    var input = Json.object().put("stageHandlerName", "ValidateOrder")
        .put("input", Json.write(new Order("invalid", new BigDecimal("-1.00"))));
    var result = handlers.execute(input);
    assertFalse(result.path("isSuccess").asBoolean());
    assertEquals("VALIDATION_ERROR", result.path("errorCode").asText());
    assertFalse(result.path("retryable").asBoolean());
  }
}

Add JUnit to your existing demo POM:

xml
<dependency>
  <groupId>org.junit.jupiter</groupId>
  <artifactId>junit-jupiter</artifactId>
  <version>5.14.4</version>
  <scope>test</scope>
</dependency>

Add this plugin alongside the compiler/dependency plugins, then run "$PIPELOGIQ_SDK_DIR/mvnw" test:

xml
<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-surefire-plugin</artifactId>
  <version>3.6.0</version>
</plugin>

The same approach can assert a handler's retryable result or returned dynamic-stage descriptor. Only a real server run proves that the server schedules the retry or applies the append at the durable stage boundary.

Use the testing module in an application test suite

For JUnit tests, add the harness with test scope:

xml
<dependency>
  <groupId>com.pipelogiq</groupId>
  <artifactId>pipelogiq-sdk-testing</artifactId>
  <version>0.5.0</version>
  <scope>test</scope>
</dependency>

Convert the assertions in the standalone approval example into JUnit tests under src/test/java. The downloaded agent demo deliberately keeps the dependency at normal scope for its runnable offline main; do not change it to test scope while that main still resides under src/main/java.

Run the live recipes

Use fresh business IDs so an old idempotent result does not masquerade as a new run:

  1. Start OrdersExample; submit, wait, inspect validation failure and test idempotency.
  2. Run the same commands with PIPELOGIQ_TRANSPORT=amqp when your server's broker is reachable.
  3. Start WorkflowExample worker; submit a fresh order and observe the retry plus fifth, dynamically appended stage.
  4. Create cron/interval/one-time schedules, manually trigger one, inspect history and archive recurring examples.
  5. Start the Spring demo and submit through the same producer contract.
  6. Run a provider-backed agent only when you intend to make real provider calls.

Exact commands and expected outputs are in getting started, workflows, schedules, Spring and agents.

Verify the SDK from source

From the extracted full source root, run builds sequentially in the same checkout:

sh
./mvnw -B clean verify
./mvnw -B install
python3 -m unittest discover -s scripts -p 'test_*.py' -v
python3 scripts/verify-packages.py

The package check consumes locally built artifacts from an isolated consumer project, including the minimal umbrella dependency path. This does not publish to Central. The full source also includes the examples and their deterministic tests:

sh
./mvnw -B -pl examples -am test

Live SDK Lab and cross-language store checks require the adjacent Lab/.NET setup described in the source repository guides; they are not created by the standalone demo ZIP. Live suites are gated, and skipped tests must be reported as skipped. Provider and notification fixtures do not make paid calls or send real messages.

Download this page as MarkdownMatches the 0.5.0 source release

Start here