Reclazz: Test Guide for SAP Commerce (Hybris)
Prerequisites
- SAP Commerce (Hybris) 2211 or later
- IntelliJ IDEA 2023.3+ (Ultimate or Community)
- JDK 17 or 21, any vendor. Structural hot-reload works on standard JVMs (SapMachine, OpenJDK, Temurin, Corretto…) via Reclazz's companion-class reloader, and on JBR/DCEVM via enhanced redefinition.
- Project configured and
ant allcompleted successfully
1. Plugin Installation
- Build the plugin:
./gradlew clean buildPlugin - In IntelliJ: Settings → Plugins → ⚙️ → Install Plugin from Disk
- Select
build/distributions/reclazz-*.zip - Restart IDE
Verify
- Settings → Tools → Reclazz section exists
- Status bar shows
Reclazz: Idle
2. Hybris Project Detection
- Open your SAP Commerce project in IntelliJ
- Wait for project indexing to complete
Verify
- Notification balloon: "SAP Commerce project detected"
- View → Tool Windows → Reclazz is available
- Check IDE log (
Help → Show Log in Explorer) for:textReclazz: Detected Hybris home at /path/to/hybris Reclazz: JDK detection, version=17, isJBR=true/false
3. JDK Detection
Test with Standard OpenJDK (including SapMachine)
- Set project SDK to OpenJDK / SapMachine / Temurin / Corretto 17 or 21
- Check Reclazz tool window log
Expected: Detected JDK 21 (SapMachine), structural hot-reload enabled (companion-class mode)
The notification also mentions the one caveat of companion mode:
reflective caches (e.g. Hybris ModelService) won't see newly added
members until server restart, because Class.getMethod(...) on the
original class can't reach the hidden companion nestmate.
Test with JetBrains Runtime
- Set project SDK to JBR 17 or 21
- Check Reclazz tool window log
Expected: Detected JDK 21 (JetBrains Runtime), structural hot-reload enabled (enhanced redefinition)
Enhanced redefinition does not have the reflective-visibility caveat;
new members are applied to the original Class object directly.
4. Agent Injection
- Enable Reclazz: Settings → Tools → Reclazz → Enable
- Create or edit a Run Configuration for Hybris server
- Run the server
Verify
- In the console output, look for:text
[Reclazz] Agent initialized [Reclazz] Hybris home: /path/to/hybris [Reclazz] Watching N directories for changes [Reclazz] Status server listening on port XXXXX - Check
.idea/reclazz/agent.portfile exists - Run Configuration VM options should contain
-javaagent:.../reclazz-agent.jar=...
Verify JVM flags
- OpenJDK:
--add-opens=java.base/java.lang=ALL-UNNAMEDpresent - JBR: above +
-XX:+AllowEnhancedClassRedefinitionpresent
5. Plugin-Agent Connection
- After server is fully started (Spring context loaded)
- Check status bar widget
Verify
- Status bar shows
Reclazz: Connected(may take a few seconds after server starts) - Tool window shows "Connected to Reclazz agent"
6. Hot-Reload: Method Body Change
This tests the most common scenario, changing method logic.
- Find a simple service class in your custom extension, e.g.:java
public class MyService { public String getMessage() { return "Hello"; } } - Change the return value:java
return "Hello World"; - Compile the project: Build → Build Project (Ctrl+F9)
- Wait for the
.classfile to be picked up by the file watcher
Verify
- Tool window shows:
Hot-swapped: com.example.MyService (XXms) - Status bar reload count increments:
Reclazz: 1 reloads - Calling the service in HAC or via API returns the new value
7. Hot-Reload: Spring Bean Refresh
- Modify a Spring-managed
@Serviceor bean defined in XML - Change a method body and compile
Verify
- Tool window shows:text
Reloaded com.example.MyService (XXms): bean myService re-created - New singleton instance is created with updated logic
- Note: existing injected references still point to old instance
8. Hot-Reload: Structural Change (any JDK 17+)
Works on any JDK 17+, standard or enhanced-redefinition.
- Add a new method or field to an existing class:java
private String newField; public String getNewMethod() { return "new"; } - Compile
Verify (any JDK 17+)
- Tool window shows:
Structural reload: com.example.MyClass (XXms) - New method/field is callable/readable from other hot-compiled code that
calls it directly (via
invokedynamicrouted through the companion nestmate)
Reflective-visibility caveat (standard JVMs only)
On standard JVMs (companion-class mode), the new members live on a
hidden nestmate rather than on the original Class object. This means:
Class.getMethod("getNewMethod")on the original class still throwsNoSuchMethodExceptionuntil server restart- Reflective caches (Hybris
ModelService, Jackson, Gson, etc.) built from the original class at boot time also won't see the new members - Jalo-layer property access and flexible search (which go through the
type dictionary rather than Java reflection) do see the new members
after a HAC
updatesystemfor Hybris items.xml attributes
On JBR/DCEVM with -XX:+AllowEnhancedClassRedefinition, the original
Class object itself gains the new members, so reflection and
reflective caches work without restart.
9. Interceptor Reload
- Modify an interceptor class (implements
Interceptor<T>) - Compile
Verify
- Run the integration
InterceptorReloadTest: it POSTs a unique nonce to/reclazztest/v2/test/interceptor-save, which callsmodelService.saveon a test product and always rolls back its transaction. - Require HTTP 200 and
validated-v2:reclazz-probe-<nonce>|calls=1. A reload event, stale value,none, duplicate invocation or HTTP failure is not evidence of success. - The fixture requires its own transaction and a test-only Commerce environment.
A portable SDK registry check is also available via
scripts/test-sap-sdk.py; it does not replace the live model-save test.
Isolated OCC field mapping acceptance
With an installed SAP Commerce SDK, run:
./gradlew :agent:compileJava
python3 scripts/test-sap-occ-sdk.py /path/to/hybris
The runner compiles synthetic DTOs against the local SDK, uses real XML reload,
DefaultDataMapper and a Spring cache proxy, and checks BASIC/DEFAULT/FULL field
additions/removals, repeated saves, parent/child visibility, merged declarations,
unrelated contexts/caches and custom-helper restart reporting. It never starts SAP,
contacts a database or writes the SDK. The SDK jars are not distributed.
Add --disable-refresh for a negative control in a temporary compiled copy; this
must fail on stale mapping output. This is an isolated SDK check, not live OCC HTTP
acceptance. Use a JDK compatible with the installed SDK.
Isolated SAP list-directive diagnostics
After ./gradlew :agent:compileJava, run
python3 scripts/test-sap-directives-sdk.py /path/to/hybris with an installed SDK
containing compiled platformservices/classes. The synthetic fixture uses real SDK
processors and converter output to verify restart diagnostics, unchanged live
definitions/lists, repeated saves, inherited/subclass/lazy directives, shared targets,
and unrelated XML property updates. Add --disable-guard to remove the guard in a
temporary compiled copy: the restart assertion must fail. The runner neither starts
SAP nor writes the SDK; it is not live application acceptance.
Isolated OCC request mapping acceptance
After ./gradlew :agent:compileJava, run:
python3 scripts/test-sap-mappings-sdk.py /path/to/hybris
The SDK must contain compiled commercewebservices web classes, commerceservices
classes and its Spring test/servlet jars. The runner uses the project's cached ASM
9.10.1 and a JDK compatible with the SDK; no new dependency is installed. Synthetic
controllers are redefined in a temporary instrumented JVM around the genuine SDK
CommerceHandlerMapping. Real Spring request lookup is compared with fresh SDK
initialization after mapping removal/move, override addition/removal, lower priority,
base fallback and API-version changes, including repeated saves and unrelated routes.
It also checks registration-failure rollback, missing-lock/inspection refusal,
generated-adapter preservation, sticky added-method refusal across saves, independent
context diagnostics, lookup locking during rebuild and ordinary Spring MVC behavior.
Add --disable-refresh to compile a temporary copy without the SDK rescan hook;
the removed-winner fallback assertion must fail. SDK jars and sources are not copied
into the project, and the runner does not start or write a SAP application.
The fixture substitutes only configuration-service priority lookup with numeric synthetic keys. It proves priority selection and registry refresh on Spring 5.3.43 and 6.2.12, not property-file reload, live SAP HTTP acceptance, or custom/programmatic mapping discovery.
Isolated dynamic-attribute and CronJob reload acceptance
Run python3 scripts/test_sap_flows_proof.py for the portable acceptance predicate.
After ./gradlew :agent:shadowJar, run:
python3 scripts/test-sap-flows-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar
Use the jar version built by your checkout and a JDK compatible with the SDK. The
runner reads the SDK Spring version instead of upgrading the project's existing
Spring lanes. Genuine SDK dynamic get/set dispatch and job performable results run
under the real agent, with v1 → v2 → added-helper v3 → v2 edits. Each step checks
fresh HTTP behavior on retained and current instances, exact-byte VERIFY receipts,
stable agent session/JVM, and unchanged unrelated behavior. The negative control
--withhold-edit must fail; a compiled class never published to the watcher must
not be certified as applied.
Persistence/type resolution is isolated in memory and jobs are invoked directly. This does not test CronJobService scheduling, stored models, item-type metadata changes, real tenant startup or cluster behavior. Report a live SAP lane separately as unrun unless it was actually executed in an authorized test environment. Cross-check the existing OCC/list-directive/request-mapping runners above, keeping their support boundaries unchanged. No proprietary jars or private source belong in the fixture. See integration acceptance.
SAP Solr provider/resolver reload guidance
A successful code reload for a class implementing SAP Commerce
FieldValueProvider, ValueResolver or TypeValueResolver emits a visible INFO advisory:
Reclazz did not reindex stored documents. Validate a subsequent indexing operation
and explicitly reindex affected documents if needed. The code receipt/VERIFY
still describes class bytes only; it is not an indexing receipt. This advice
neither requests a JVM restart nor triggers an index operation.
Recognition follows implemented interfaces through superclass/interface inheritance.
It does not infer index names, document IDs or downstream dependencies of ordinary
helper classes. A class name containing Solr or Provider is insufficient.
Failure to resolve the loaded hierarchy produces no indexing claim. Absence of a
advisory is not evidence that no index can be affected.
./gradlew :agent:unitTest --tests '*SapIndexingGuidanceTest' --rerun
./gradlew :agent:e2eTest --tests '*SapIndexingGuidanceTest' --rerun
./gradlew :agent:shadowJar
python3 scripts/test-sap-indexing-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar
# Negative control: must exit nonzero because edited class bytes are withheld.
python3 scripts/test-sap-indexing-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar --withhold-edit
The portable tests use synthetic interface identities and a snapshot. The optional
SDK acceptance instead compiles synthetic application classes against installed
SAP interfaces, invokes genuine FieldValue / DefaultSolrInputDocument paths
with the real agent, and stores immutable document snapshots locally. It checks
method-body edits, a newly added helper method, a revert, same-session exact-byte
receipts, changed newly built documents, unchanged older snapshots until explicit
reindex, and ordinary-class exclusion. The installed SDK stays outside the repo.
This is isolated document-building acceptance, not live Solr or full SAP indexer acceptance. It starts no tenant, indexer CronJob or Solr server, and writes no live index. Schema changes, index configuration, cluster distribution, commit/query visibility and selecting affected documents/indexes remain unverified here.
Isolated custom-cache characterization
./gradlew :agent:shadowJar
python3 scripts/test-sap-caches-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar
# Desired-contract probe: currently exits nonzero after proving helper code is live.
python3 scripts/test-sap-caches-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar --require-fresh-helper
# Negative control: unpublished edited bytes must fail the receipt check.
python3 scripts/test-sap-caches-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar --withhold-edit
The optional runner uses installed SDK platform jars and synthetic Spring singleton beans with a genuine Guava cache and an AtomicReference/TTL cache. Observed on Spring 5.3.43 / Guava 33.1.0-jre and Spring 6.2.12 / Guava 33.4.8-jre:
| Method-body reload | Guava owner | Custom TTL owner | Unrelated caches |
|---|---|---|---|
| Registered, autowired bean dependency | Fresh result through recreated owner | Fresh result through recreated owner | Warm entries retained |
| Cache owner itself | Fresh result when its owner reloads | Fresh result when its owner reloads | Warm entries retained |
| Plain static helper, not a Spring bean | Old cached result retained | Old cached result retained | Warm entries retained |
| Subsequent explicit owner edit | Fresh result | Fresh result | Warm entries retained |
The helper limitation is deliberate characterization, not a freshness pass.
Default success means this entire observed matrix was reproduced; the stricter
--require-fresh-helper mode exposes the unsupported case as a failure. Each reload
requires an applied receipt with the exact edited class hash in the same JVM/session.
The helper changes from 1 to 2 and uncached results become 122/222, while warmed
cached results remain 121/221. Receipt success alone does not establish cache freshness.
Both clocks stay fixed through all reloads, before their 60-second expiry; repeated reads and computation counts establish actual cache hits. An explicit 61-second clock advance finally proves expiry in both target and unrelated caches. Constructor counts, managed consumer references and unrelated singleton identity are checked. No cache hooks or application runtime dependencies were added. Existing Spring bean refresh covers the tested owner/registered-dependency paths; custom caches on plain helper dependencies require separate treatment if immediate freshness is needed. No automatic general invalidation is claimed.
This is isolated Spring/Guava acceptance using SDK libraries. It starts no SAP tenant and contacts no external service or cluster. Structural edits, external references to destroyed beans, static/shared caches, FactoryBeans, scoped/prototype owners and concurrent TTL computation are outside this experiment. The simple AtomicReference fixture publishes immutable entries under serial test operations; it is not a concurrency algorithm recommendation.
SAP process definition diagnostics
Changes to local XML referenced by an existing ProcessDefinitionResource
singleton in a captured active Spring context produce a WARN: not applied;
restart required. This is resource-change guidance, not a successful code reload
or a VERIFY receipt. No process definition is refreshed, and no process is started,
migrated or replayed. Inspect/validate the changed definition before restarting;
restart guidance does not certify that edited XML is valid.
Recognition uses the exact registered local path, including arbitrary XML names.
The watcher polls registered files outside native roots, subject to existing file
and module exclusions. An unregistered *-process.xml is not enough evidence.
Closed contexts, remote/packed resources, lazy registrations and FactoryBean
products are not claimed. Existing dedicated kinds such as items/backoffice/logging
configuration keep their meaning; a registered process with a Spring XML suffix
receives process diagnostics instead of being parsed as bean XML.
./gradlew :agent:unitTest --tests '*SapProcessResourceRecognitionTest' --rerun
./gradlew :agent:e2eTest --tests '*SapProcessDefinitionDiagnosticTest' --rerun
python3 scripts/test-sap-process-sdk.py /path/to/hybris
The portable tests use a synthetic API fixture and a real agent/watcher JVM. The
SDK runner independently checks genuine resource registrations against the
installed Spring/SDK jars after :agent:compileJava. It does not start a tenant,
parse a business process or exercise a scheduler. These checks are not live SAP
acceptance. Automatic definition refresh requires a separate design: the SDK
factory/cache APIs alone do not establish isolation to newly started processes.
Isolated SAP event-listener acceptance
./gradlew :agent:shadowJar
python3 scripts/test-sap-events-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar
# Negative control: an unpublished edit must fail the receipt/behavior check.
python3 scripts/test-sap-events-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar --withhold-edit
The runner compiles a synthetic com.example subclass of the genuine SDK
AbstractEventListener plus a AbstractEvent and registers it with a real Spring
context under the built agent. Events are published through Spring's multicaster, so
the SDK's final onApplicationEvent cluster and tenant guard runs before onEvent;
local stub ClusterService/TenantService and a locally scoped event make that guard
pass honestly. Editing the listener body and a registered dependency, then reverting,
each requires an applied VERIFY receipt with the exact class hash in one JVM/session,
and is checked for changed event output, exactly one delivery per publish, preserved
listener identity and an unaffected unrelated bean.
This confirms the existing hot-swap already covers SAP event-listener reload; no new
production hook was needed. It is isolated acceptance, not a live run: it starts no
tenant or cluster, sends no external events, and the afterPropertiesSet tenant-scope
initialization path is unverified here because it needs live SAP infrastructure.
Isolated ConstraintValidator acceptance
./gradlew :agent:shadowJar
python3 scripts/test-sap-validators-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar
# Negative control: an unpublished edit must fail the receipt/behavior check.
python3 scripts/test-sap-validators-sdk.py /path/to/hybris --agent agent/build/libs/agent-1.4.0.jar --withhold-edit
The runner builds a genuine Hibernate Validator (the installed provider, jakarta
namespace) with a synthetic constraint, a custom ConstraintValidator and an injected
dependency, and validates a bean through the real provider under the built agent.
Editing the validator's isValid body and the injected dependency, then reverting, each
requires an applied VERIFY receipt with the exact class hash in one JVM/session and is
checked for the changed violation message, exactly one validation per request, and an
initialize count that stays at one. A stable init count proves the provider keeps a
single cached validator instance whose method bodies the agent hot-swaps in place, so
the existing SpringValidatorReloader metadata flush is not needed for a body or
dependency edit. No new production hook was required.
This is isolated acceptance, not a live run. It distinguishes a validator-instance body edit (covered) from constraint-metadata changes (handled separately by the metadata flush) and from structural annotation edits, which stay unverified here along with provider lifecycle beyond instance reuse.
10. Extension Watching Scope
Verify only custom extensions are watched
- Check tool window startup log for watched directory count
- Should only include your custom extensions, NOT platform extensions like:
core,catalog,europe1,platformservices, etc.
Verify with settings
- In Settings → Tools → Reclazz → Watch Extensions, enter specific extension names
- Restart server
- Only the specified extensions should be watched
11. Reconnection
- With server running and plugin connected, stop the server
- Status bar should show
Reclazz: Disconnected - Start the server again
- Wait for agent to initialize
Verify
- Plugin reconnects automatically (with backoff: 1s, 2s, 4s, up to 10s)
- Status bar returns to
Reclazz: Connected - Tool window logs the reconnection
12. Error Scenarios
Server not started
- Status bar:
Reclazz: IdleorReclazz: Disconnected - No errors in IDE log
Agent JAR missing
- Check IDE log for warning about agent JAR not found
- Run configuration should NOT have
-javaagentflag
Wrong JDK version
- JDK < 17: Agent may not load, check for error in console
Non-Hybris project
- Plugin should be invisible, no tool window, no status widget activity
- Settings section still available but disabled state
13. Settings Persistence
- Change settings in Settings → Tools → Reclazz
- Close and reopen the project
- Verify settings are preserved
Settings to test
| Setting | Default | Test Value |
|---|---|---|
| Enable | false | true |
| Auto Compile | false | true |
| Auto ImpEx | false | true |
| Watch Extensions | (empty) | "myext1,myext2" |
| Debounce (ms) | 500 | 1000 |
| Verbose Logging | false | true |
| Auto-detect JDK | true | false |
14. Performance
File watcher overhead
- Monitor CPU usage after server starts
- File watcher should be idle when no changes occur
- With 30-second startup delay, server boot should not be impacted
Status socket
- Agent StatusServer binds to loopback (127.0.0.1) only
- Max 5 concurrent client connections
- JSON line events are lightweight (< 200 bytes each)
Troubleshooting
| Symptom | Likely Cause | Fix |
|---|---|---|
| Server hangs on startup | Too many watched dirs | Check custom extension filter |
| No console logs after agent | SLF4J conflict | Ensure agent JAR has no slf4j-simple |
Reclazz: Disconnected persists |
Port mismatch | Delete .idea/reclazz/agent.port, restart |
| Hot-swap fails | Schema change on OpenJDK | Use JetBrains Runtime |
| Spring bean not refreshed | Non-singleton bean | Expected, prototypes recreate automatically |
| Agent not injected | Plugin disabled or non-Hybris project | Check Settings → Tools → Reclazz |
Synthetic constructor ownership on installed SDK Spring graphs
scripts/test-sap-constructor-sdk.py runs agentless fresh startup and cached-constructor controls, then real class-file saves through a held HTTP caller. It uses the installed exact SAP SDK Spring5.3.43/6.2.12 binaries in isolated classpaths and never starts a tenant or writes the SDK tree. The current runner targets local SapMachine17/21 installations on macOS; other JVM/OS layouts need a separately reviewed runner configuration.
python3 scripts/test-sap-constructor-sdk.py --sdk-search-root /path/to/local/sdks --agent agent/build/libs/agent-1.4.0.jar --output /tmp/reclazz-constructor-results
The search root must contain the owner's installed SDK directories. The exact measured spring-beans binary hashes are checked; a missing or different binary is a setup failure, not a behavioral failure. No proprietary JAR is downloaded or bundled. The runner builds only synthetic fixtures in temporary directories, sanitizes SDK paths from logs, closes owned JVMs, and writes per-profile/JDK observations and raw logs to the requested output directory. Use a fresh output directory per run; a lock prevents duplicate drivers writing one result set.
Fresh native controls use10/20/30/40. Watcher cases cover direct Owner saves followed by helper-only saves, parent-child helper-only cascade, same-factory recursive destruction, and a withheld-save negative control. Accepted save receipts alone do not pass: exact session/input hashes, current constructor value, current held receiver, native dependency edges and unrelated identity are asserted. The HTTP observer never looks up or recreates a bean to repair the fixture. Exit0 requires all desired/control cases to pass; baseline desired failures remain exit1.
The fixture uses @Component to select actual ordinary Spring recreation. A plain programmatic registration without a stereotype can use the agent's helper-body route, so a stale result from that different route is not constructor-transaction causal proof. SDK-derived native graph acceptance must not be labelled live SAP application/tenant acceptance. Ordinary sibling/shadow/refusal/failure/batch controls also live in the inherited ConstructorDependencyContract; the standard Spring5 and isolated Spring6 test wrappers remain their normal Gradle entry points. SDK execution uses existing test-only JUnit artifacts and keeps them out of runtime distributions.
Canonical source
This full page is generated from docs/test-guide.md. Edit the repository source and rebuild; do not edit generated HTML.