---
title: "Test Java workflows and agents"
description: "Use deterministic business and agent tests, then verify real server execution and packaged applications separately."
category: "Java SDK"
---

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

| Layer | What it proves | What it does not prove |
| --- | --- | --- |
| Business-stage unit test | Validation, result classification and context changes | Server scheduling, leases or delivery |
| Agent harness | Scripted decisions through production agent handlers and native tools | Real model quality, queues, distributed retries or persistence |
| API/transport fixture | Request/response contracts and error handling | A particular deployed server's credentials and network |
| Real server run | Bootstrap, dispatch, execution, state and selected transport | Every possible business failure or provider behavior |
| Packaged consumer | Dependencies and public APIs work outside the SDK reactor | External service availability |

## Run deterministic agent scenarios

[Download the agent demo](/downloads/pipelogiq-java-agent-demo.zip), install `sdk-runtime,sdk-agent,sdk-testing -am` from the matching SDK source, and build the demo as described in [agents](/docs/java/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](/docs/java/getting-started), [workflows](/docs/java/workflows), [schedules](/docs/java/schedules), [Spring](/docs/java/spring-boot) and [agents](/docs/java/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.
