Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
102 changes: 89 additions & 13 deletions .github/workflows/integration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,43 @@ on:
pull_request:
branches: [main]
workflow_dispatch: {}
# Weekly drift catch
# Weekly drift catch — Tuesday 06:00 UTC so failures land in EU/IN
# working hours, not weekend handover.
schedule:
- cron: '0 6 * * 1' # Monday 06:00 UTC
- cron: '0 6 * * 2'

permissions:
contents: read

# Avoid spawning parallel docker-compose stacks for back-to-back pushes;
# also cancels stale PR runs when a new commit lands.
concurrency:
group: integration-${{ github.ref }}
cancel-in-progress: true

env:
DO_NOT_TRACK: '1'

jobs:
# WireMock-based integration tests run on every PR + push. No live stack
# needed — these are contract-style tests over the SDK + agent wire shape.
# Matrixed across the same JDKs as the unit-test suite in ci.yml.
contract-integration:
name: Contract Integration (WireMock)
name: Contract Integration (WireMock, JDK ${{ matrix.java-version }})
runs-on: ubuntu-latest
timeout-minutes: 15
strategy:
fail-fast: false
matrix:
java-version: [11, 17, 21]
steps:
- name: Checkout SDK
uses: actions/checkout@v4

- name: Set up JDK 17
- name: Set up JDK ${{ matrix.java-version }}
uses: actions/setup-java@v4
with:
java-version: '17'
java-version: ${{ matrix.java-version }}
distribution: 'temurin'
cache: 'maven'

Expand All @@ -51,8 +63,23 @@ jobs:
</settings>
EOF

# `-DskipUnitTests=true` is now a real toggle (bound to surefire's
# <skipTests> via pom.xml); previously it was a no-op flag and unit
# tests were silently re-running here.
#
# `-Djacoco.skip=true` because the jacoco:check goal (bound to verify)
# expects coverage data from the unit tests we just skipped; coverage
# gating is the unit-test job's responsibility (ci.yml `build (17)`).
- name: Run integration tests (WireMock)
run: mvn verify -DskipUnitTests -B -U
run: mvn verify -DskipUnitTests=true -Djacoco.skip=true -B -U

- name: Upload failsafe reports on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: failsafe-reports-jdk${{ matrix.java-version }}
path: target/failsafe-reports/
if-no-files-found: ignore

# Live integration runs against a real community stack via docker compose.
# Mirrors axonflow-sdk-go/.github/workflows/integration.yml — clones the
Expand Down Expand Up @@ -92,17 +119,53 @@ jobs:
</settings>
EOF

# 3-attempt retry for transient Maven Central flakes — same pattern
# ci.yml's `Build with Maven` and `Run unit tests` use.
- name: Install SDK to local Maven repo
run: mvn install -DskipTests -B -U
run: |
for i in 1 2 3; do
echo "Attempt $i: mvn install"
if mvn install -DskipTests -B -U; then break; fi
if [ $i -eq 3 ]; then exit 1; fi
sleep 30
done

# Pin the basic example's SDK dep to whatever we just installed
# locally, so the example resolves the freshly-built artifact and
# not whatever version is published to Central. Without this, when
# the parent pom bumps from 6.1.0 → 6.2.0 the example silently
# keeps testing the OLD 6.1.0 from Central.
- name: Sync example SDK version with parent
run: |
PARENT_VERSION=$(mvn -B -q -DforceStdout help:evaluate -Dexpression=project.version)
echo "Parent SDK version: ${PARENT_VERSION}"
# Replace the axonflow-sdk dependency version in examples/basic/pom.xml.
# Anchored on the artifactId on the previous line to avoid touching
# other deps.
python3 - <<PY
import re, pathlib
p = pathlib.Path("examples/basic/pom.xml")
s = p.read_text()
new = re.sub(
r"(<artifactId>axonflow-sdk</artifactId>\s*<version>)[^<]+(</version>)",
rf"\g<1>${PARENT_VERSION}\g<2>",
s,
)
assert new != s, "Failed to rewrite example pom version"
p.write_text(new)
PY
grep -A 1 "axonflow-sdk" examples/basic/pom.xml | head -4

- name: Clone community stack
run: git clone --depth 1 https://github.com/getaxonflow/axonflow.git ../axonflow

- name: Start community stack
run: |
cd ../axonflow
docker compose up -d
docker compose up -d --wait --wait-timeout 120

# Belt-and-suspenders: also poll /health since not every compose
# service has a healthcheck wired.
echo "Waiting for agent to be healthy..."
timeout 120 bash -c 'until curl -sf http://localhost:8080/health; do sleep 2; done'
echo "Agent is healthy"
Expand All @@ -119,18 +182,31 @@ jobs:
working-directory: examples/basic
run: timeout 90 mvn -q compile exec:java

- name: Stop community stack
if: always()
# Logs MUST be captured before `Stop community stack` runs — `compose
# down` destroys the containers and `compose logs` then returns
# nothing.
- name: Show docker logs on failure
if: failure()
run: |
if [ -d "../axonflow" ]; then
cd ../axonflow
docker compose down --volumes --remove-orphans || true
docker compose logs --tail=200 || true
fi

- name: Show docker logs on failure
- name: Upload docker logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: docker-compose-logs
path: ../axonflow/docker-compose-logs.txt
if-no-files-found: ignore

- name: Stop community stack
if: always()
run: |
if [ -d "../axonflow" ]; then
cd ../axonflow
docker compose logs --tail=200 || true
# Persist logs to disk so the upload step can grab them even after teardown.
docker compose logs --tail=500 > docker-compose-logs.txt 2>/dev/null || true
docker compose down --volumes --remove-orphans || true
fi
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- **`axonflow.listLLMProviders()`** + `listLLMProviders(String type, Boolean enabled)` — list configured LLM providers and their per-provider health snapshot. Calls `GET /api/v1/llm-providers`. New `LLMProvider` and `LLMProviderHealth` types in `com.getaxonflow.sdk.types`. Async variant `listLLMProvidersAsync()`. Closes the parity gap with the Python SDK's `list_providers()` and the Go SDK's `ListProviders()`.
- **`examples/basic/`** — minimal smoke example exercising `healthCheck()`, `proxyLLMCall()`, and `listConnectors()` against a running AxonFlow agent. Uses try-with-resources so OkHttp's dispatcher + connection pool are cleaned up at exit. Run via `mvn -q compile exec:java` after `mvn install -DskipTests` at the SDK root.

### Fixed

- **`pom.xml`** — `mvn verify -DskipUnitTests=true` now actually skips surefire (unit tests). Previously the property was unbound — `-DskipUnitTests` was a no-op flag and unit tests ran redundantly during integration-test invocations. The flag now binds to the surefire `<skipTests>` config; default remains `false`.

## [6.1.0] - 2026-04-25 — Plugin Batch 1 explainability fields on MCP responses

Expand Down
83 changes: 70 additions & 13 deletions examples/basic/src/main/java/com/getaxonflow/examples/Basic.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,39 +7,66 @@
// - Client initialization from environment
// - Health check against the agent
// - A protected proxyLLMCall round-trip
// - Listing connectors (read-only sanity check)
//
// Run from this directory after `mvn install -DskipTests` at the SDK root:
//
// export AXONFLOW_AGENT_URL=http://localhost:8080
// export AXONFLOW_CLIENT_ID=demo-client
// export AXONFLOW_CLIENT_SECRET=demo-secret
// export AXONFLOW_CLIENT_ID=your-client-id
// export AXONFLOW_CLIENT_SECRET=your-client-secret
// mvn -q compile exec:java
package com.getaxonflow.examples;

import com.getaxonflow.sdk.AxonFlow;
import com.getaxonflow.sdk.AxonFlowConfig;
import com.getaxonflow.sdk.exceptions.AuthenticationException;
import com.getaxonflow.sdk.exceptions.ConnectionException;
import com.getaxonflow.sdk.exceptions.PolicyViolationException;
import com.getaxonflow.sdk.types.ClientRequest;
import com.getaxonflow.sdk.types.ClientResponse;
import com.getaxonflow.sdk.types.ConnectorInfo;
import com.getaxonflow.sdk.types.HealthStatus;
import com.getaxonflow.sdk.types.RequestType;

import java.util.List;

public class Basic {

public static void main(String[] args) {
String endpoint = envOrDefault("AXONFLOW_AGENT_URL", "http://localhost:8080");
String clientId = envOrDefault("AXONFLOW_CLIENT_ID", "demo-client");
String clientSecret = envOrDefault("AXONFLOW_CLIENT_SECRET", "demo-secret");
String clientId = System.getenv("AXONFLOW_CLIENT_ID");
String clientSecret = System.getenv("AXONFLOW_CLIENT_SECRET");

if (clientId == null || clientId.isEmpty()
|| clientSecret == null || clientSecret.isEmpty()) {
System.err.println(
"AXONFLOW_CLIENT_ID and AXONFLOW_CLIENT_SECRET must be set");
System.exit(1);
}

System.out.println("Initializing AxonFlow client...");
AxonFlow client =
// try-with-resources so OkHttp's dispatcher + connection pool are
// cleaned up promptly. Without this, non-daemon threads keep the JVM
// alive ~60s after main() returns and the smoke timeout starts to
// bite.
try (AxonFlow client =
AxonFlow.create(
AxonFlowConfig.builder()
.agentUrl(endpoint)
.clientId(clientId)
.clientSecret(clientSecret)
.debug(true)
.build());
.build())) {

healthCheck(client);
proxyLLMCallStep(client, clientId);
listConnectorsStep(client);
}

System.out.println("\nBasic example complete.");
}

private static void healthCheck(AxonFlow client) {
System.out.println("\n============================================================");
System.out.println("Step 1: Health Check");
System.out.println("============================================================");
Expand All @@ -53,30 +80,60 @@ public static void main(String[] args) {
System.err.println("Health check failed: " + e.getMessage());
System.exit(1);
}
}

private static void proxyLLMCallStep(AxonFlow client, String clientId) {
System.out.println("\n============================================================");
System.out.println("Step 2: Protected proxyLLMCall");
System.out.println("============================================================");
try {
// Don't set userToken — the SDK auto-populates it from clientId
// when omitted. Sending a literal "demo-user" string is rejected
// by the agent's JWT middleware on stacks with token validation.
ClientRequest request =
ClientRequest.builder()
.query("What is the capital of France?")
.userToken("demo-user")
.clientId(clientId)
.requestType(RequestType.CHAT)
.build();

ClientResponse response = client.proxyLLMCall(request);
System.out.printf(" Success: %s%n", response.isSuccess());
System.out.printf(" Blocked: %s%n", response.isBlocked());
} catch (Exception e) {
// Community stack often runs without an LLM provider configured;
// a fail-open or 503 is normal here. Don't fail the smoke for it.
System.out.println(" (proxyLLMCall returned non-success — expected on community without LLM): "
+ e.getMessage());
} catch (PolicyViolationException e) {
// Policy block is a valid outcome — community policies can match
// the demo query depending on configuration.
System.out.printf(" Blocked by policy: %s%n", e.getMessage());
} catch (AuthenticationException | ConnectionException e) {
// These are real failures: bad creds or stack down. Fail loud.
System.err.println("proxyLLMCall failed: " + e.getMessage());
System.exit(1);
} catch (RuntimeException e) {
// Other runtime failures (e.g. agent returns non-2xx because no
// LLM provider is configured) — log and continue. Tightening
// this further requires capability detection from /health,
// tracked in axonflow-sdk-java#146.
System.out.println(" proxyLLMCall non-success: " + e.getMessage());
}
}

System.out.println("\nBasic example complete.");
private static void listConnectorsStep(AxonFlow client) {
System.out.println("\n============================================================");
System.out.println("Step 3: List Connectors");
System.out.println("============================================================");
try {
List<ConnectorInfo> connectors = client.listConnectors();
System.out.printf(" Found %d connectors%n", connectors.size());
for (ConnectorInfo c : connectors) {
System.out.printf(" - %s (%s) installed=%s%n",
c.getName(), c.getType(), c.isInstalled());
}
} catch (AuthenticationException | ConnectionException e) {
System.err.println("listConnectors failed: " + e.getMessage());
System.exit(1);
} catch (RuntimeException e) {
System.out.println(" listConnectors non-success: " + e.getMessage());
}
}

private static String envOrDefault(String key, String fallback) {
Expand Down
6 changes: 6 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@

<!-- Coverage Threshold (lowered from 0.75 for Workflow Control Plane types) -->
<jacoco.minimum.coverage>0.73</jacoco.minimum.coverage>

<!-- Set to true to skip surefire (unit tests) without affecting failsafe
(integration tests). Used by the contract-integration CI job so
`mvn verify -DskipUnitTests=true` runs only failsafe. -->
<skipUnitTests>false</skipUnitTests>
</properties>

<dependencies>
Expand Down Expand Up @@ -179,6 +184,7 @@
<version>${maven-surefire-plugin.version}</version>
<configuration>
<argLine>@{argLine} -Dnet.bytebuddy.experimental=true</argLine>
<skipTests>${skipUnitTests}</skipTests>
<includes>
<include>**/*Test.java</include>
</includes>
Expand Down
Loading