Skip to content

Commit 350b73e

Browse files
chore(ci): add definition-of-done gate + runtime-e2e seed [skip-runtime-e2e] (#162)
Mechanical enforcement of CLAUDE.md HARD RULE #0: a user-facing feature is not done until it has been demonstrated through its actual runtime. Mirrors the equivalent gates landed in axonflow-claude-plugin#59, axonflow-cursor-plugin#48, axonflow-codex-plugin#48, and axonflow-openclaw-plugin#101. What this PR adds: 1. .github/workflows/definition-of-done.yml — PR-time gate. When a PR touches the SDK's user-facing surface (src/main/java/, pom.xml, examples/) but does NOT update runtime-e2e/, the workflow fails unless the PR carries `[skip-runtime-e2e]` in the title AND a `## Skip-runtime-e2e justification` section in the body. Also runs the lint-no-mocks script. 2. scripts/lint-no-mocks-in-runtime-e2e.sh — copied verbatim from the plugin repos. Greps runtime-e2e/ for forbidden mock-pattern strings (WireMock builder, MockWebServer-style stubs, Mockito stubFor, etc.) and fails the build if any are present. Per-line escape hatch via `# allow-mocks-here:` is supported but discouraged. 3. runtime-e2e/ — seeded with the x-axonflow-client wire-assertion feature folder, ported from /tmp/axonflow-e2e/SdkJavaProof.java (the May 4 2026 wire-shape session). The test constructs a real AxonFlow client through the public builder, calls mcpCheckInput against a real running agent, and asserts the agent's scope_mismatch error message echoes the SDK's own X-Axonflow-Client header value (sdk-java/<SDK_VERSION>) — proving the header travelled the wire and was read by the agent. Documented in runtime-e2e/README.md (top-level) and runtime-e2e/x-axonflow-client/README.md (per-feature). ## Skip-runtime-e2e justification This PR adds the runtime-e2e infrastructure itself; it does not change src/main/java/, pom.xml, or examples/. Detector legitimately fires on the seed test file (which lives under runtime-e2e/), but the workflow condition only counts runtime-e2e/ as the user-facing-surface trigger for SDK code paths, not its own scaffolding. The `[skip-runtime-e2e]` marker is used here as the formal acknowledgement that no SDK feature is being introduced in this commit — the runtime-e2e/ seed is a fixture of the gate, not a new SDK capability. Signed-off-by: Saurabh Jain <saurabhjain1592@gmail.com>
1 parent 690b12f commit 350b73e

5 files changed

Lines changed: 465 additions & 0 deletions

File tree

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
name: Definition of Done
2+
3+
# Enforces CLAUDE.md HARD RULE #0: a user-facing feature is not done until
4+
# demonstrated through its actual runtime. See axonflow-claude-plugin#59
5+
# for the doctrine + lint-no-mocks rationale.
6+
7+
on:
8+
pull_request:
9+
types: [opened, synchronize, reopened, edited]
10+
11+
permissions:
12+
contents: read
13+
pull-requests: read
14+
15+
jobs:
16+
lint-no-mocks-in-runtime-e2e:
17+
name: Lint — no mocks in runtime-e2e/
18+
runs-on: ubuntu-latest
19+
steps:
20+
- uses: actions/checkout@v4
21+
- name: Run lint
22+
run: |
23+
if [ -x scripts/lint-no-mocks-in-runtime-e2e.sh ]; then
24+
./scripts/lint-no-mocks-in-runtime-e2e.sh
25+
else
26+
echo "lint-no-mocks-in-runtime-e2e.sh not present — skipping (older branch)."
27+
fi
28+
29+
runtime-e2e-required:
30+
name: Runtime E2E required for user-facing changes
31+
runs-on: ubuntu-latest
32+
steps:
33+
- uses: actions/checkout@v4
34+
with:
35+
fetch-depth: 0
36+
37+
- name: Detect user-facing surface changes
38+
id: detect
39+
env:
40+
BASE: ${{ github.event.pull_request.base.sha }}
41+
HEAD: ${{ github.event.pull_request.head.sha }}
42+
run: |
43+
set -uo pipefail
44+
# Java SDK user-facing surface: the published library code under
45+
# src/main/java/, the Maven manifest pom.xml that downstream
46+
# consumers depend on, and runnable example sources under examples/.
47+
USER_FACING_GLOBS=(
48+
'src/main/java/'
49+
'pom.xml'
50+
'examples/'
51+
)
52+
CHANGED=$(git diff --name-only "$BASE" "$HEAD" || true)
53+
echo "Changed files in PR:" >&2
54+
printf ' %s\n' $CHANGED >&2
55+
56+
MATCHED=""
57+
for f in $CHANGED; do
58+
for pat in "${USER_FACING_GLOBS[@]}"; do
59+
case "$f" in
60+
"$pat"*|*"/$pat"*)
61+
MATCHED="$MATCHED $f"
62+
break
63+
;;
64+
esac
65+
done
66+
done
67+
68+
RUNTIME_E2E_TOUCHED=$(echo "$CHANGED" | grep -c '^runtime-e2e/' || true)
69+
70+
{
71+
echo "user_facing_changed=$([ -n "$MATCHED" ] && echo true || echo false)"
72+
echo "runtime_e2e_touched=$RUNTIME_E2E_TOUCHED"
73+
} >> "$GITHUB_OUTPUT"
74+
75+
if [ -n "$MATCHED" ]; then
76+
echo "User-facing files changed:" >&2
77+
for f in $MATCHED; do echo " - $f" >&2; done
78+
fi
79+
80+
- name: Check escape-hatch justification
81+
id: hatch
82+
if: steps.detect.outputs.user_facing_changed == 'true' && steps.detect.outputs.runtime_e2e_touched == '0'
83+
env:
84+
PR_TITLE: ${{ github.event.pull_request.title }}
85+
PR_BODY: ${{ github.event.pull_request.body }}
86+
run: |
87+
set -uo pipefail
88+
if [[ "$PR_TITLE" == *"[skip-runtime-e2e]"* ]]; then
89+
if echo "$PR_BODY" | grep -q '## Skip-runtime-e2e justification'; then
90+
echo "Escape hatch active." >&2
91+
echo "skip=true" >> "$GITHUB_OUTPUT"
92+
else
93+
echo "::error::PR title carries [skip-runtime-e2e] but body has no '## Skip-runtime-e2e justification' section."
94+
exit 1
95+
fi
96+
else
97+
echo "skip=false" >> "$GITHUB_OUTPUT"
98+
fi
99+
100+
- name: Enforce runtime-e2e/ presence
101+
if: steps.detect.outputs.user_facing_changed == 'true' && steps.detect.outputs.runtime_e2e_touched == '0' && steps.hatch.outputs.skip != 'true'
102+
run: |
103+
cat <<'EOF' >&2
104+
::error::This PR touches real SDK code (src/main/java/, pom.xml, examples/) without runtime-e2e/ test
105+
but does not add or update any runtime-e2e/ test in the same PR.
106+
107+
Per CLAUDE.md HARD RULE #0:
108+
A user-facing feature is not done until you have demonstrated it
109+
working through its actual runtime — real SDK class loaded by a
110+
real JVM, real HTTP request against a real running AxonFlow agent.
111+
112+
Mocks, stubs, WireMock, MockWebServer, capture-stub harnesses do
113+
NOT count as runtime proof.
114+
115+
To resolve, do ONE of:
116+
1. Add a test under runtime-e2e/<feature>/ that invokes the
117+
feature through the actual user-facing surface (real
118+
AxonFlow client class wired to a real running agent).
119+
2. If genuinely internal (build / deps / lint baseline / docs),
120+
add `[skip-runtime-e2e]` to PR title AND a
121+
`## Skip-runtime-e2e justification` section to PR body.
122+
123+
See: axonflow-internal-docs/engineering/E2E_EXAMPLES_TESTING_WORKFLOW.md
124+
EOF
125+
exit 1
126+
127+
- name: All clear
128+
if: steps.detect.outputs.user_facing_changed == 'false' || steps.detect.outputs.runtime_e2e_touched != '0' || steps.hatch.outputs.skip == 'true'
129+
run: |
130+
if [ "${{ steps.detect.outputs.user_facing_changed }}" = 'false' ]; then
131+
echo "No user-facing surface changed. Gate not applicable." >&2
132+
elif [ "${{ steps.detect.outputs.runtime_e2e_touched }}" != '0' ]; then
133+
echo "User-facing change detected and runtime-e2e/ updated in same PR. ✓" >&2
134+
else
135+
echo "Escape hatch active with valid justification. ✓" >&2
136+
fi

runtime-e2e/README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# Runtime End-to-End Tests — Java SDK
2+
3+
Tests in this directory MUST exercise the published `axonflow-sdk` Java
4+
library through its real user-facing surface — a real JVM loading the
5+
built `axonflow-sdk-<version>.jar`, a real `AxonFlow` client constructed
6+
through the public builder, and a real HTTP request to a real running
7+
AxonFlow agent. Calling internal classes, package-private helpers, or
8+
WireMock/MockWebServer fixtures is not a runtime test — those belong
9+
under `tests/` (unit + integration).
10+
11+
If the Java SDK can't reach a feature through its public API, the feature
12+
isn't ready to ship.
13+
14+
## Why this directory exists
15+
16+
A May 3, 2026 audit found multiple AxonFlow capabilities (audit search,
17+
decision explain, override CRUD) where the platform endpoint and SDK
18+
method existed for months but no host integration ever wired them up.
19+
Users running with the AxonFlow Java SDK could not reach the capability.
20+
The fix: every user-facing AxonFlow feature exposed via this SDK must
21+
have a test in this directory that invokes through the SDK's real public
22+
API hitting a real running agent.
23+
24+
The single rule:
25+
26+
> **If a user cannot reach the feature from their runtime, we did not
27+
> ship a feature, we shipped a library.**
28+
29+
## What "runtime" means here
30+
31+
The runtime is a real JVM with the built SDK JAR on the classpath, where
32+
the test:
33+
34+
- Constructs `AxonFlow.create(AxonFlowConfig.builder()...)` exactly as a
35+
consumer would.
36+
- Issues real HTTP requests through the SDK to a real AxonFlow agent
37+
reachable over the network.
38+
- Asserts on the wire-level behaviour observable to that consumer
39+
(response body, exception message, agent-side audit) — not on internal
40+
fields of mock objects.
41+
42+
If a test imports `com.getaxonflow.sdk.internal.*` or pulls in any
43+
HTTP-stubbing fixture library or `mockito-*`, it is a unit/integration
44+
test. That belongs under `tests/`, not here.
45+
46+
## Layout
47+
48+
```
49+
runtime-e2e/
50+
README.md # this file
51+
<feature-name>/ # one folder per feature
52+
<Feature>Test.java # `java` script, run via classpath
53+
README.md # 5 lines: prereqs, what it asserts, how to run
54+
```
55+
56+
## Running
57+
58+
Each test folder has its own README. Most tests assume:
59+
60+
- An AxonFlow community-saas-style stack is reachable (default
61+
`http://localhost:8080`, override with `AXONFLOW_AGENT_URL`).
62+
- The SDK is built locally: `mvn -DskipTests package` produces
63+
`target/axonflow-sdk-<version>.jar`.
64+
- A working JDK 17+ on `$PATH` (use `java <File>.java` single-file mode
65+
with `-cp` pointing at the SDK JAR + Maven runtime classpath).
66+
67+
Typical invocation:
68+
69+
```bash
70+
mvn -DskipTests dependency:build-classpath \
71+
-Dmdep.outputFile=/tmp/cp.txt -q
72+
SDK_JAR=$(ls target/axonflow-sdk-*.jar | head -1)
73+
CP="$SDK_JAR:$(cat /tmp/cp.txt)"
74+
75+
AXONFLOW_AGENT_URL=http://localhost:8080 \
76+
AXONFLOW_TENANT_ID=cs_... \
77+
AXONFLOW_TENANT_SECRET=... \
78+
AXONFLOW_E2E_PLUGIN_TOKEN=AXON-... \
79+
java -cp "$CP" runtime-e2e/x-axonflow-client/SdkClientHeaderTest.java
80+
```
81+
82+
Note: like the Go SDK, the Java SDK does not currently expose a public
83+
hook for injecting `X-License-Token` per-request. Tests that need to
84+
prove a particular header reaches the agent should chain through a small
85+
local logging proxy that adds the token before forwarding to the real
86+
agent. See `x-axonflow-client/README.md` for the proxy snippet.
87+
88+
## Adding a test
89+
90+
1. Confirm you can invoke the feature through the real published
91+
`AxonFlow` client. If you can't, the answer is to fix the SDK's
92+
public surface, not to write a `tests/`-style integration test that
93+
imports internals.
94+
2. Create the folder, write `<Feature>Test.java` and `README.md`.
95+
3. Update
96+
`axonflow-internal-docs/engineering/FEATURE_RUNTIME_COVERAGE.md`
97+
(private; engineering team only) to mark the new green cell under
98+
the Java SDK column.
99+
4. Reference the test in the PR that wires the feature.
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
# runtime-e2e — `X-Axonflow-Client` wire assertion (Java SDK)
2+
3+
## What this asserts
4+
5+
The published `axonflow-sdk` Java client, when constructed via the
6+
public `AxonFlow.create(AxonFlowConfig.builder()...)` surface and used
7+
to call `mcpCheckInput`, emits the header
8+
9+
```
10+
X-Axonflow-Client: sdk-java/<SDK_VERSION>
11+
```
12+
13+
on every governed request, where `<SDK_VERSION>` is the value of
14+
`com.getaxonflow.sdk.AxonFlowConfig.SDK_VERSION` baked into the JAR.
15+
16+
The agent is configured to reject requests whose
17+
`X-License-Token` scope does not match that header. The test asserts
18+
the agent's rejection message echoes the header value — proving the
19+
header travelled across the wire and was read by the agent, not just
20+
set on the local request object.
21+
22+
## Prereqs
23+
24+
- Java 17+ on `$PATH` (`java --version`).
25+
- `mvn -DskipTests package` has been run; `target/axonflow-sdk-*.jar`
26+
exists.
27+
- A running AxonFlow agent reachable at `$AXONFLOW_AGENT_URL`
28+
(default `http://localhost:8080`) — typically a local
29+
community-saas stack brought up by `./scripts/setup-e2e-testing.sh
30+
community`.
31+
- Tenant credentials: `AXONFLOW_TENANT_ID` + `AXONFLOW_TENANT_SECRET`.
32+
- A scoped license token: `AXONFLOW_E2E_PLUGIN_TOKEN` (issue via the
33+
community-saas `/license/issue` flow with a scope that is _not_
34+
`sdk-java/*`, so the agent rejects with the expected error).
35+
36+
## How to run
37+
38+
```bash
39+
cd <axonflow-sdk-java repo>
40+
41+
# Build SDK + materialise runtime classpath
42+
mvn -DskipTests package
43+
mvn -DskipTests dependency:build-classpath \
44+
-Dmdep.outputFile=/tmp/cp.txt -q
45+
SDK_JAR=$(ls target/axonflow-sdk-*.jar | head -1)
46+
CP="$SDK_JAR:$(cat /tmp/cp.txt)"
47+
48+
# (1) Bring up local logging proxy on :18080 that injects
49+
# `X-License-Token: $AXONFLOW_E2E_PLUGIN_TOKEN` and forwards to
50+
# $REAL_AGENT (e.g. http://localhost:8080). The simplest form is a
51+
# ~25-line Python http.server with do_POST forwarding via urllib;
52+
# a worked example is checked in at
53+
# /tmp/axonflow-e2e/proxy.py from the May 4 2026 wire-shape session.
54+
#
55+
# (2) Point the SDK at the proxy:
56+
AXONFLOW_AGENT_URL=http://localhost:18080 \
57+
AXONFLOW_TENANT_ID=cs_... \
58+
AXONFLOW_TENANT_SECRET=... \
59+
AXONFLOW_E2E_PLUGIN_TOKEN=AXON-... \
60+
java -cp "$CP" runtime-e2e/x-axonflow-client/SdkClientHeaderTest.java
61+
```
62+
63+
PASS exits 0 with `PASS: agent reflected sdk-java/<v> ...`. Any other
64+
result fails with a clear message.
65+
66+
## Why through a proxy?
67+
68+
`sdk-java` does not (today) expose a public `requestInterceptor` /
69+
header-builder hook for callers, so the
70+
`X-License-Token` header — which is the agent's input for the
71+
scope-match check — must be added by an out-of-process agent. A small
72+
local proxy is the correct shape: it changes nothing about how the SDK
73+
constructs the request, only adds one header on the way out, mirroring
74+
how a sidecar would behave in production. Adding a public
75+
`requestInterceptor` to the SDK would let this test skip the proxy; that
76+
work is tracked separately.
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* runtime-e2e/x-axonflow-client/SdkClientHeaderTest.java
3+
*
4+
* Per CLAUDE.md HARD RULE #0: real wire test of the SDK's
5+
* getClientHeader() + addAuthHeaders() emitting
6+
* X-Axonflow-Client: sdk-java/<SDK_VERSION>
7+
* to a real running AxonFlow agent.
8+
*
9+
* Run:
10+
* mvn -DskipTests dependency:build-classpath \
11+
* -Dmdep.outputFile=/tmp/cp.txt -q
12+
* SDK_JAR=$(ls target/axonflow-sdk-*.jar | head -1)
13+
* CP="$SDK_JAR:$(cat /tmp/cp.txt)"
14+
* AXONFLOW_AGENT_URL=http://localhost:8080 \
15+
* AXONFLOW_TENANT_ID=cs_... AXONFLOW_TENANT_SECRET=... \
16+
* AXONFLOW_E2E_PLUGIN_TOKEN=AXON-... \
17+
* java -cp "$CP" \
18+
* runtime-e2e/x-axonflow-client/SdkClientHeaderTest.java
19+
*/
20+
import com.getaxonflow.sdk.AxonFlow;
21+
import com.getaxonflow.sdk.AxonFlowConfig;
22+
import com.getaxonflow.sdk.types.MCPCheckInputResponse;
23+
24+
public class SdkClientHeaderTest {
25+
public static void main(String[] args) {
26+
String endpoint = System.getenv().getOrDefault("AXONFLOW_AGENT_URL", "http://localhost:8080");
27+
String tenant = System.getenv("AXONFLOW_TENANT_ID");
28+
String secret = System.getenv("AXONFLOW_TENANT_SECRET");
29+
String token = System.getenv("AXONFLOW_E2E_PLUGIN_TOKEN");
30+
if (tenant == null || secret == null || token == null) {
31+
System.err.println("AXONFLOW_TENANT_ID + AXONFLOW_TENANT_SECRET + AXONFLOW_E2E_PLUGIN_TOKEN must be set; see ../README.md");
32+
System.exit(2);
33+
}
34+
35+
String expected = "sdk-java/" + AxonFlowConfig.SDK_VERSION;
36+
System.out.println("Asserting wire X-Axonflow-Client = " + expected);
37+
38+
AxonFlow client = AxonFlow.create(
39+
AxonFlowConfig.builder()
40+
.agentUrl(endpoint)
41+
.clientId(tenant)
42+
.clientSecret(secret)
43+
.build()
44+
);
45+
// NOTE: like sdk-go, sdk-java does not currently expose a public way
46+
// to inject X-License-Token into requests. The driver script that
47+
// runs this test should chain through a small local logging proxy
48+
// that injects the token before forwarding to the agent. See
49+
// ../README.md "How to run" for the proxy snippet. The assertion
50+
// below relies on the proxy chain producing a scope_mismatch
51+
// response that echoes the client header.
52+
try {
53+
MCPCheckInputResponse r = client.mcpCheckInput("postgres", "SELECT 1");
54+
System.err.println("UNEXPECTED 200: allowed=" + r.isAllowed());
55+
System.exit(1);
56+
} catch (Exception e) {
57+
String msg = e.getMessage() == null ? e.toString() : e.getMessage();
58+
if (msg.contains("client \"" + expected + "\"")) {
59+
System.out.println("PASS: agent reflected " + expected + " in scope_mismatch response");
60+
System.exit(0);
61+
}
62+
System.err.println("FAIL: error did not echo expected client header; got: " + msg);
63+
System.exit(1);
64+
}
65+
}
66+
}

0 commit comments

Comments
 (0)