Skip to content

feat(examples): end-to-end JamJet Cloud SDK example (Java)#8

Open
sunilp wants to merge 6 commits into
mainfrom
feat/java-cloud-sdk-example
Open

feat(examples): end-to-end JamJet Cloud SDK example (Java)#8
sunilp wants to merge 6 commits into
mainfrom
feat/java-cloud-sdk-example

Conversation

@sunilp
Copy link
Copy Markdown
Member

@sunilp sunilp commented Jun 4, 2026

Summary

Adds examples/jamjet-cloud-sdk-demo, the missing runnable example of a Java app using the JamJet Cloud SDK end to end. Two flavours:

  • Spring Boot: the cloud starter auto-wires span observability; an ActionReceiptAdvisor + a FileActionReceiptEmitter add AgentBoundary receipts for @Tool calls. Spans are zero-config; receipts are one advisor + emitter bean.
  • Plain Java: the raw JamjetCloud / Span / ActionReceipt API.

It is a reactor module so it builds against the in-repo cloud SDK (not yet published). Hermetic tests stub JamJet Cloud with WireMock and use a deterministic model, so they need no API keys. The README documents live mode (real key + model -> spans in the dashboard, receipts in a JSONL file) and is explicit about what the Java Cloud SDK does not yet do (local policy enforcement, PII redaction, approval UX, cache injection).

Tests

5 hermetic tests: FileActionReceiptEmitterTest, PlainJavaCloudDemoTest (span + receipt), SpringCloudDemoTest (context wiring + a governed call + a span-emission probe). The plain-Java test proves the SDK span path reaches /v1/events/ingest via WireMock and the receipt is schema-valid.

Notes / follow-ups

  • Fixes the root README cloud-starter version drift (0.2.0 -> 0.3.1).
  • Discovered: the example must add langchain4j-core (compile) because jamjet-cloud-spring-boot-starter's JamjetCloudAutoConfiguration references a langchain4j type as a @Bean return type, so the method-level @ConditionalOnClass does not protect a non-langchain4j Spring user from NoClassDefFoundError at startup. Worth a starter follow-up (move the langchain4j beans into a class-level @ConditionalOnClass nested config). The example's dependency + comment is the stopgap.
  • Spring AI observability does not emit a span for a hand-rolled stub ChatModel, so the Spring test documents that and relies on the plain-Java test for the SDK span path; live mode (a real model) exercises the full flow.

Test plan

  • mvn -pl examples/jamjet-cloud-sdk-demo -am test-compile
  • mvn -pl examples/jamjet-cloud-sdk-demo test (5 green)
  • Live: set JJ_API_KEY + OPENAI_API_KEY, mvn -pl examples/jamjet-cloud-sdk-demo spring-boot:run -> span in the dashboard, receipt in ~/.jamjet/audit/cloud-sdk-demo.jsonl

Summary by CodeRabbit

Release Notes

  • Documentation

    • Updated Maven dependency versions to 0.3.1 with new example reference in README
  • New Features

    • Added a complete, runnable end-to-end example demonstrating the JamJet Cloud SDK with both Spring Boot and plain Java implementations
    • Includes local test setup with mocked API calls and live mode for real API integration

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jun 4, 2026

Review Change Stack

📝 Walkthrough

Walkthrough

This PR introduces examples/jamjet-cloud-sdk-demo, a runnable Maven module demonstrating JamJet Cloud SDK integration in both Spring Boot and plain Java variants. It updates documentation and version references, adds receipt file persistence infrastructure, configures two application implementations with governance integration, and provides comprehensive WireMock-based hermetic tests.

Changes

JamJet Cloud SDK Demo Example Module

Layer / File(s) Summary
Project setup and documentation
pom.xml, examples/jamjet-cloud-sdk-demo/pom.xml, README.md, examples/jamjet-cloud-sdk-demo/README.md, examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/package-info.java
Parent pom registers new demo module; demo pom configures Maven properties, BOM imports, and dependencies (JamJet Cloud SDK, Spring AI, WireMock). Documentation updated with version bumps (0.2.0→0.3.1) and pointer to the runnable example.
Receipt file handling infrastructure
examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/FileActionReceiptEmitter.java, examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/FileActionReceiptEmitterTest.java
FileActionReceiptEmitter writes ActionReceipt objects as JSONL lines with atomic file appending under lock and Jackson serialization. Test verifies schema-valid output and receipt hashing via ActionReceiptValidator.
Spring Boot demo application with governance
examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ChatConfig.java, examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/CloudDemoApplication.java, examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ReceiptConfig.java, examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/LookupTool.java, examples/jamjet-cloud-sdk-demo/src/main/resources/application.yml, examples/jamjet-cloud-sdk-demo/src/main/resources/application-test.yml, examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java, examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/StubChatModelConfig.java
ChatConfig beans ChatClient with ActionReceiptAdvisor and LookupTool; CloudDemoApplication provides entry point and CommandLineRunner gated by non-test profile; ReceiptConfig wires file-backed receipt emission; LookupTool exposes example tool method; application.yml configures OpenAI API key and JamJet Cloud credentials; SpringCloudDemoTest verifies Spring context wiring, governed chat execution, and WireMock span ingestion polling.
Plain Java demo application
examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/PlainJavaCloudDemo.java, examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/PlainJavaCloudDemoTest.java
PlainJavaCloudDemo.run() configures SDK, opens/finishes span with token metadata, constructs and validates ActionReceipt with computed hashes, and emits via provided emitter; main() reads environment variables and orchestrates demo. PlainJavaCloudDemoTest starts WireMock, configures SDK to target it, runs demo, polls for ingest POST, and validates emitted receipt against schema.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐰 A new example hops into view,
Spring Boot and plain Java too!
Receipts now file with locksmith care,
WireMock stubs make tests fair.
From config to span, the demo shows the way!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 8.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately and concisely summarizes the main change: adding a runnable end-to-end example demonstrating the JamJet Cloud SDK in Java, with both Spring Boot and plain Java implementations.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/java-cloud-sdk-example

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

🧹 Nitpick comments (1)
examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java (1)

64-89: ⚡ Quick win

This test currently passes even when no span is emitted.

spanReachesCloudAfterGovernedCall falls back to non-null bean assertions, so a span regression won’t fail the test. Consider renaming to a probe-style test or asserting/aborting explicitly (e.g., Assumptions.assumeTrue) to keep intent clear.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java`
around lines 64 - 89, The test spanReachesCloudAfterGovernedCall currently masks
span regressions by falling back to non-null bean assertions; change it to
either assert/abort explicitly when no span is observed or convert it to a
probe-style (assumption) test: inside spanReachesCloudAfterGovernedCall replace
the current if/else fallback with either a failing assertion like
org.junit.jupiter.api.Assertions.assertTrue(gotSpan, "expected span POST to
/v1/events/ingest") so the test fails on missing span, or use
org.junit.jupiter.api.Assumptions.assumeTrue(gotSpan, "no span emitted; skipping
probe") to mark it as skipped; update uses of chatClient and emitter only as
context (do not keep the non-span assertions that hide failures).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/jamjet-cloud-sdk-demo/pom.xml`:
- Around line 62-65: The pom currently hard-pins langchain4j-core version
"0.36.2" in the examples/jamjet-cloud-sdk-demo POM; remove the literal
<version>0.36.2</version> from the dependency for artifactId langchain4j-core
and instead reference a centralized property (e.g. use
<version>${langchain4j.version}</version>) or rely on a parent/shared
dependencyManagement entry; ensure the examples POM still declares the
dependency for langchain4j-core (since jamjet-cloud-spring-boot-starter marks it
optional) but uses the centralized property or dependencyManagement entry so the
version is managed in one place.

In `@examples/jamjet-cloud-sdk-demo/README.md`:
- Around line 30-32: Replace the non-runnable example Java launch line in
README.md for PlainJavaCloudDemo with a concrete, copy-pasteable run approach:
either show running the demo via Maven’s exec plugin (calling main class
dev.jamjet.example.cloud.PlainJavaCloudDemo) or document how to generate an
explicit classpath (e.g., build target/classes plus resolved dependency JARs)
and use that concrete classpath in the java -cp invocation; update the README’s
example so the placeholder “...” is removed and the user can run the demo
directly.

In
`@examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ReceiptConfig.java`:
- Around line 16-18: The actionReceiptEmitter() currently hardcodes
Path.of(System.getProperty("user.home"), ".jamjet", "audit",
"cloud-sdk-demo.jsonl"), which breaks test isolation and leaks demo data; change
it to read a configurable property (e.g., receipt.file.path or similar) with a
safe default that matches the current home-based path, and pass that Path into
new FileActionReceiptEmitter(...) so tests can override it via
application-test.yml; locate and update the actionReceiptEmitter() factory
method (and any config class) to inject the property (using `@Value` or
ConfigurationProperties/Environment) and ensure the default is preserved if the
property is not supplied.

In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/FileActionReceiptEmitterTest.java`:
- Around line 50-54: The test only validates the first JSONL line but should
validate every emitted line; update the assertions to iterate over the lines
list, for each entry call MAPPER.readValue(...) to parse into a Map and then
call new ActionReceiptValidator().validate(...) on that Map (using the same
suppressed cast pattern), and assertThat(errors).isEmpty() for each parsed line
to ensure both lines are validated; reference variables/methods: lines, MAPPER,
ActionReceiptValidator.validate().

In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java`:
- Around line 26-41: The DynamicPropertySource supplier may run before JUnit's
`@BeforeAll`, causing wireMock to be null; ensure WireMock is initialized and
started before props() runs by moving the startup and stub registration into a
static initializer (e.g., a static { ... } block) or other early hook so
wireMock = new WireMockServer(0); wireMock.start(); and wireMock.stubFor(...)
execute before props() is called; keep stopWireMock() for teardown but remove
reliance on `@BeforeAll` startWireMock() for providing the baseUrl in props().

In `@README.md`:
- Around line 144-145: Replace the plain text reference "See
examples/jamjet-cloud-sdk-demo for a runnable end-to-end example." with a
Markdown link so it becomes clickable in rendered docs; update the line that
contains "See examples/jamjet-cloud-sdk-demo" to use the link text
[examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/) (including the
trailing slash) so readers can navigate directly to the demo folder.

---

Nitpick comments:
In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java`:
- Around line 64-89: The test spanReachesCloudAfterGovernedCall currently masks
span regressions by falling back to non-null bean assertions; change it to
either assert/abort explicitly when no span is observed or convert it to a
probe-style (assumption) test: inside spanReachesCloudAfterGovernedCall replace
the current if/else fallback with either a failing assertion like
org.junit.jupiter.api.Assertions.assertTrue(gotSpan, "expected span POST to
/v1/events/ingest") so the test fails on missing span, or use
org.junit.jupiter.api.Assumptions.assumeTrue(gotSpan, "no span emitted; skipping
probe") to mark it as skipped; update uses of chatClient and emitter only as
context (do not keep the non-span assertions that hide failures).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 513516de-b0cb-499b-a499-42a7978e56a3

📥 Commits

Reviewing files that changed from the base of the PR and between 3b5fd86 and 2b9ac11.

📒 Files selected for processing (17)
  • README.md
  • examples/jamjet-cloud-sdk-demo/README.md
  • examples/jamjet-cloud-sdk-demo/pom.xml
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ChatConfig.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/CloudDemoApplication.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/FileActionReceiptEmitter.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/LookupTool.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/PlainJavaCloudDemo.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ReceiptConfig.java
  • examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/package-info.java
  • examples/jamjet-cloud-sdk-demo/src/main/resources/application-test.yml
  • examples/jamjet-cloud-sdk-demo/src/main/resources/application.yml
  • examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/FileActionReceiptEmitterTest.java
  • examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/PlainJavaCloudDemoTest.java
  • examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java
  • examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/StubChatModelConfig.java
  • pom.xml

Comment on lines +62 to +65
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-core</artifactId>
<version>0.36.2</version>
</dependency>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Demo module langchain4j-core declaration =="
rg -n -C2 '<artifactId>langchain4j-core</artifactId>|<version>0\.36\.2</version>' examples/jamjet-cloud-sdk-demo/pom.xml

echo
echo "== Starter module references to langchain4j (if any) =="
fd -i 'pom.xml' | xargs rg -n -C2 'langchain4j|JamjetChatModelListener|ConditionalOnClass' || true

echo
echo "== Parent/version properties that might manage langchain4j =="
rg -n -C2 'langchain4j|spring-ai.version|spring-boot.version' pom.xml examples/jamjet-cloud-sdk-demo/pom.xml

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 6368


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
nl -ba examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,140p'

echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
nl -ba jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'

echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
nl -ba pom.xml | sed -n '1,220p'

echo
echo "== any dependencyManagement for langchain4j-core in repo =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* | head -n 50

echo
echo "== any centralized property for langchain4j-core version =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* | head -n 50

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 207


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== examples/jamjet-cloud-sdk-demo/pom.xml (around langchain4j-core) =="
cat -n examples/jamjet-cloud-sdk-demo/pom.xml | sed -n '1,120p'
echo
echo "== jamjet-cloud-spring-boot-starter/pom.xml (around langchain4j-core) =="
cat -n jamjet-cloud-spring-boot-starter/pom.xml | sed -n '1,140p'
echo
echo "== repository root pom.xml (dependencyManagement relevant bits) =="
cat -n pom.xml | sed -n '1,240p'

echo
echo "== dependencyManagement blocks mentioning langchain4j-core =="
rg -n --glob 'pom.xml' 'dependencyManagement' pom.xml examples jamjet-cloud-* || true

echo
echo "== centralized properties for langchain4j version (if any) =="
rg -n --glob 'pom.xml' 'langchain4j.*version' pom.xml examples jamjet-cloud-* || true

echo
echo "== any direct langchain4j-core pinning =="
rg -n --glob 'pom.xml' '<artifactId>langchain4j-core</artifactId>' pom.xml examples jamjet-cloud-* | head -n 50

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 19231


Avoid duplicating/hard-pinning langchain4j-core in examples/jamjet-cloud-sdk-demo/pom.xml.

langchain4j-core is explicitly pinned to 0.36.2 at line 64, and the repo parent doesn’t centrally manage a langchain4j version. Since jamjet-cloud-spring-boot-starter also pins langchain4j-core 0.36.2 as <optional>true</optional>, the demo must keep an explicit dependency—but the version should be centralized/shared (e.g., parent property or shared dependencyManagement) to avoid updating both poms in lockstep later.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/jamjet-cloud-sdk-demo/pom.xml` around lines 62 - 65, The pom
currently hard-pins langchain4j-core version "0.36.2" in the
examples/jamjet-cloud-sdk-demo POM; remove the literal <version>0.36.2</version>
from the dependency for artifactId langchain4j-core and instead reference a
centralized property (e.g. use <version>${langchain4j.version}</version>) or
rely on a parent/shared dependencyManagement entry; ensure the examples POM
still declares the dependency for langchain4j-core (since
jamjet-cloud-spring-boot-starter marks it optional) but uses the centralized
property or dependencyManagement entry so the version is managed in one place.

Comment on lines +30 to +32
mvn -pl examples/jamjet-cloud-sdk-demo -am package -DskipTests
java -cp examples/jamjet-cloud-sdk-demo/target/classes:... dev.jamjet.example.cloud.PlainJavaCloudDemo
```
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Plain-Java run command is not copy-paste runnable.

At Line 31, -cp ... contains an unresolved placeholder, so users can’t run the example as written. Provide a concrete command (e.g., via exec-maven-plugin or documented classpath generation) to keep the live-mode path executable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/jamjet-cloud-sdk-demo/README.md` around lines 30 - 32, Replace the
non-runnable example Java launch line in README.md for PlainJavaCloudDemo with a
concrete, copy-pasteable run approach: either show running the demo via Maven’s
exec plugin (calling main class dev.jamjet.example.cloud.PlainJavaCloudDemo) or
document how to generate an explicit classpath (e.g., build target/classes plus
resolved dependency JARs) and use that concrete classpath in the java -cp
invocation; update the README’s example so the placeholder “...” is removed and
the user can run the demo directly.

Comment on lines +16 to +18
public ActionReceiptEmitter actionReceiptEmitter() {
Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
return new FileActionReceiptEmitter(file);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make receipt file path configurable instead of hardcoding ${user.home}.

Line 17 persists receipts to a fixed user-home path, which can leak unredacted demo/test data and breaks hermetic test isolation. Prefer a property-backed path with a safe default, then override it in application-test.yml.

Proposed diff
+import org.springframework.beans.factory.annotation.Value;
...
     `@Bean`
-    public ActionReceiptEmitter actionReceiptEmitter() {
-        Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
-        return new FileActionReceiptEmitter(file);
+    public ActionReceiptEmitter actionReceiptEmitter(
+            `@Value`("${jamjet.cloud.receipt-file:${user.home}/.jamjet/audit/cloud-sdk-demo.jsonl}") String receiptFile) {
+        return new FileActionReceiptEmitter(Path.of(receiptFile));
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public ActionReceiptEmitter actionReceiptEmitter() {
Path file = Path.of(System.getProperty("user.home"), ".jamjet", "audit", "cloud-sdk-demo.jsonl");
return new FileActionReceiptEmitter(file);
import org.springframework.beans.factory.annotation.Value;
...
`@Bean`
public ActionReceiptEmitter actionReceiptEmitter(
`@Value`("${jamjet.cloud.receipt-file:${user.home}/.jamjet/audit/cloud-sdk-demo.jsonl}") String receiptFile) {
return new FileActionReceiptEmitter(Path.of(receiptFile));
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/jamjet-cloud-sdk-demo/src/main/java/dev/jamjet/example/cloud/ReceiptConfig.java`
around lines 16 - 18, The actionReceiptEmitter() currently hardcodes
Path.of(System.getProperty("user.home"), ".jamjet", "audit",
"cloud-sdk-demo.jsonl"), which breaks test isolation and leaks demo data; change
it to read a configurable property (e.g., receipt.file.path or similar) with a
safe default that matches the current home-based path, and pass that Path into
new FileActionReceiptEmitter(...) so tests can override it via
application-test.yml; locate and update the actionReceiptEmitter() factory
method (and any config class) to inject the property (using `@Value` or
ConfigurationProperties/Environment) and ensure the default is preserved if the
property is not supplied.

Comment on lines +50 to +54
// each line is valid JSON and the receipt validates against the v0.1 schema
var parsed = MAPPER.readValue(lines.get(0), java.util.Map.class);
@SuppressWarnings("unchecked")
List<String> errors = new ActionReceiptValidator().validate((java.util.Map<String, Object>) parsed);
assertThat(errors).isEmpty();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Validate both emitted JSONL lines, not just the first.

Line 50 says “each line is valid,” but Lines 51–54 validate only lines.get(0). This can miss corruption in the appended line. Iterate over both lines and validate each parsed receipt.

Suggested patch
-        // each line is valid JSON and the receipt validates against the v0.1 schema
-        var parsed = MAPPER.readValue(lines.get(0), java.util.Map.class);
-        `@SuppressWarnings`("unchecked")
-        List<String> errors = new ActionReceiptValidator().validate((java.util.Map<String, Object>) parsed);
-        assertThat(errors).isEmpty();
+        // each line is valid JSON and validates against the v0.1 schema
+        ActionReceiptValidator validator = new ActionReceiptValidator();
+        for (String line : lines) {
+            var parsed = MAPPER.readValue(line, java.util.Map.class);
+            `@SuppressWarnings`("unchecked")
+            List<String> errors = validator.validate((java.util.Map<String, Object>) parsed);
+            assertThat(errors).isEmpty();
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// each line is valid JSON and the receipt validates against the v0.1 schema
var parsed = MAPPER.readValue(lines.get(0), java.util.Map.class);
@SuppressWarnings("unchecked")
List<String> errors = new ActionReceiptValidator().validate((java.util.Map<String, Object>) parsed);
assertThat(errors).isEmpty();
// each line is valid JSON and validates against the v0.1 schema
ActionReceiptValidator validator = new ActionReceiptValidator();
for (String line : lines) {
var parsed = MAPPER.readValue(line, java.util.Map.class);
`@SuppressWarnings`("unchecked")
List<String> errors = validator.validate((java.util.Map<String, Object>) parsed);
assertThat(errors).isEmpty();
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/FileActionReceiptEmitterTest.java`
around lines 50 - 54, The test only validates the first JSONL line but should
validate every emitted line; update the assertions to iterate over the lines
list, for each entry call MAPPER.readValue(...) to parse into a Map and then
call new ActionReceiptValidator().validate(...) on that Map (using the same
suppressed cast pattern), and assertThat(errors).isEmpty() for each parsed line
to ensure both lines are validated; reference variables/methods: lines, MAPPER,
ActionReceiptValidator.validate().

Comment on lines +26 to +41
@BeforeAll
static void startWireMock() {
wireMock = new WireMockServer(0);
wireMock.start();
wireMock.stubFor(post(urlEqualTo("/v1/events/ingest")).willReturn(ok()));
}

@AfterAll
static void stopWireMock() {
if (wireMock != null) wireMock.stop();
}

@DynamicPropertySource
static void props(DynamicPropertyRegistry r) {
r.add("jamjet.cloud.api-url", () -> wireMock.baseUrl());
r.add("jamjet.cloud.batch.size", () -> "1");
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java"
if [ ! -f "$FILE" ]; then
  echo "Missing file: $FILE"
  exit 1
fi

echo "== File (head 120 lines) =="
sed -n '1,120p' "$FILE" | cat -n

echo
echo "== Locate where wireMock is declared =="
rg -n "wireMock" "$FILE"

echo
echo "== Determine test framework versions (pom/gradle) =="
# Look for Maven pom first
if [ -f "examples/jamjet-cloud-sdk-demo/pom.xml" ]; then
  echo "--- examples/jamjet-cloud-sdk-demo/pom.xml ---"
  rg -n "spring-boot|junit-jupiter|junit|junit-platform|junit-jupiter-engine" examples/jamjet-cloud-sdk-demo/pom.xml || true
elif [ -f "pom.xml" ]; then
  echo "--- root pom.xml ---"
  rg -n "spring-boot|junit-jupiter|junit|junit-platform|junit-jupiter-engine" pom.xml || true
fi

# Look for Gradle build
if [ -f "examples/jamjet-cloud-sdk-demo/build.gradle" ] || [ -f "examples/jamjet-cloud-sdk-demo/build.gradle.kts" ]; then
  echo "--- examples/jamjet-cloud-sdk-demo/build.gradle* ---"
  ls -la examples/jamjet-cloud-sdk-demo/build.gradle* || true
  rg -n "spring-boot|junit-jupiter|junit|junit-platform" examples/jamjet-cloud-sdk-demo/build.gradle* || true
elif [ -f "build.gradle" ] || [ -f "build.gradle.kts" ]; then
  echo "--- root build.gradle* ---"
  ls -la build.gradle* || true
  rg -n "spring-boot|junit-jupiter|junit|junit-platform" build.gradle* || true
fi

Repository: jamjet-labs/jamjet-runtime-java

Length of output: 5216


🌐 Web query:

Spring @DynamicPropertySourcelifecycle when invoked relative to JUnit5@BeforeAll called before or after

💡 Result:

In Spring integration tests, the @DynamicPropertySource annotation is processed as part of the ApplicationContext loading process [1][2]. Consequently, its invocation relative to JUnit 5's @BeforeAll lifecycle method depends on when that context is initialized [3][4]. Under standard conditions, where the ApplicationContext is loaded for a test class, @DynamicPropertySource methods are typically invoked before @BeforeAll methods [3][5][4]. This occurs because the Spring TestContext Framework must load the context—and therefore resolve dynamic properties—before the test class execution proceeds to user-defined lifecycle methods [4]. However, this order can be sensitive to specific configurations: 1. Lifecycle Semantic Influences: When using @TestInstance(Lifecycle.PER_CLASS), the SpringExtension may trigger ApplicationContext loading during the TestInstancePostProcessor phase [6]. This can result in Spring loading the context and executing @DynamicPropertySource logic before JUnit's @BeforeAll has an opportunity to run [6]. 2. Configuration-Specific Behavior: Certain test configurations, such as the use of @Sql with the execution phase set to BEFORE_TEST_CLASS, force an eager loading of the ApplicationContext [3][5]. In such cases, it is explicitly documented that @DynamicPropertySource methods will be invoked before @BeforeAll methods [3][4]. Because @DynamicPropertySource relies on the context loading lifecycle, it is generally considered unavailable for tasks that must strictly precede the initialization of the Spring container itself (e.g., if you need to start an external resource before Spring touches the property system). For such requirements, developers often use static initialization blocks or alternative manual lifecycle management [7][4].

Citations:


Initialize WireMock before @DynamicPropertySource runs.
@DynamicPropertySource is evaluated during Spring ApplicationContext bootstrap and can execute before JUnit 5 @BeforeAll, so r.add("jamjet.cloud.api-url", () -> wireMock.baseUrl()) (line 40) may dereference wireMock while it’s still null. Start WireMock in a static initializer (or a JUnit extension hook that runs early enough) so the supplier never sees a null instance.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@examples/jamjet-cloud-sdk-demo/src/test/java/dev/jamjet/example/cloud/SpringCloudDemoTest.java`
around lines 26 - 41, The DynamicPropertySource supplier may run before JUnit's
`@BeforeAll`, causing wireMock to be null; ensure WireMock is initialized and
started before props() runs by moving the startup and stub registration into a
static initializer (e.g., a static { ... } block) or other early hook so
wireMock = new WireMockServer(0); wireMock.start(); and wireMock.stubFor(...)
execute before props() is called; keep stopWireMock() for teardown but remove
reliance on `@BeforeAll` startWireMock() for providing the baseUrl in props().

Comment thread README.md
Comment on lines +144 to +145
See examples/jamjet-cloud-sdk-demo for a runnable end-to-end example.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Make the demo pointer a clickable Markdown link.

At Line 144, use a link ([examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/)) so readers can navigate directly from rendered docs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` around lines 144 - 145, Replace the plain text reference "See
examples/jamjet-cloud-sdk-demo for a runnable end-to-end example." with a
Markdown link so it becomes clickable in rendered docs; update the line that
contains "See examples/jamjet-cloud-sdk-demo" to use the link text
[examples/jamjet-cloud-sdk-demo](examples/jamjet-cloud-sdk-demo/) (including the
trailing slash) so readers can navigate directly to the demo folder.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant