From 2d478d577937a19a39385e74ad6d45bbb09529fd Mon Sep 17 00:00:00 2001 From: Kamil Patryk Kozakowski Date: Tue, 14 Jul 2026 17:50:59 +0200 Subject: [PATCH] Add multi-module path binding and enterprise flow linkKey regressions. Verify sibling-module scan resolves cross-module EventNormalizer helpers for path expansion and assert HTML export embeds linkKey on structured business-flow steps. Co-authored-by: Cursor --- .../MultiModulePathBindingExpanderTest.java | 97 +++++++++++++++++++ .../EnterpriseDedicatedEndpointHtmlTest.java | 3 + 2 files changed, 100 insertions(+) create mode 100644 state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java new file mode 100644 index 0000000..04c015c --- /dev/null +++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/MultiModulePathBindingExpanderTest.java @@ -0,0 +1,97 @@ +package click.kamil.springstatemachineexporter.analysis.service; + +import click.kamil.springstatemachineexporter.analysis.model.EntryPoint; +import click.kamil.springstatemachineexporter.analysis.resolver.ProjectModuleGraph; +import click.kamil.springstatemachineexporter.analysis.resolver.SiblingDependencyResolver; +import click.kamil.springstatemachineexporter.ast.common.CodebaseContext; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.Collections; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Path binding expansion must resolve helpers declared in sibling Gradle modules without executing the build. + */ +class MultiModulePathBindingExpanderTest { + + @Test + void shouldExpandEventUsingHelperFromSiblingModule(@TempDir Path tempDir) throws Exception { + Path root = tempDir.resolve("order-system"); + Files.createDirectories(root); + Files.writeString(root.resolve("settings.gradle"), "include 'api-module', 'core-module'"); + + Path apiModule = root.resolve("api-module"); + Files.createDirectories(apiModule.resolve("src/main/java/com/example/util")); + Files.writeString(apiModule.resolve("build.gradle"), ""); + Files.writeString(apiModule.resolve("src/main/java/com/example/util/EventNormalizer.java"), """ + package com.example.util; + public class EventNormalizer { + public static String normalize(String raw) { + return raw.toUpperCase(); + } + } + """); + + Path coreModule = root.resolve("core-module"); + Files.createDirectories(coreModule.resolve("src/main/java/com/example")); + Files.writeString(coreModule.resolve("build.gradle"), + "dependencies { implementation project(':api-module') }"); + Files.writeString(coreModule.resolve("src/main/java/com/example/OrderGateway.java"), """ + package com.example; + import com.example.util.EventNormalizer; + public class OrderController { + OrderGateway gateway; + public void transition(String event) { + gateway.trigger(event); + } + } + class OrderGateway { + void trigger(String event) { + OrderEvent.valueOf(EventNormalizer.normalize(event)); + } + } + enum OrderEvent { PAY, SHIP } + """); + + SiblingDependencyResolver siblingResolver = new SiblingDependencyResolver(); + ProjectModuleGraph graph = siblingResolver.analyzeProject(coreModule); + Set scanPaths = graph.resolveScanPaths(coreModule, false); + + CodebaseContext context = new CodebaseContext(); + context.setResolveBindings(true); + context.scan(scanPaths, Collections.emptySet()); + + assertThat(context.getTypeDeclaration("com.example.util.EventNormalizer")).isNotNull(); + + HeuristicCallGraphEngine engine = new HeuristicCallGraphEngine(context); + Map> callGraph = + engine.buildCallGraph(); + + EntryPoint entryPoint = EntryPoint.builder() + .className("com.example.OrderController") + .methodName("transition") + .name("POST /api/order/{event}") + .parameters(List.of(EntryPoint.Parameter.builder() + .name("event") + .type("String") + .annotations(List.of("PathVariable")) + .build())) + .build(); + + List> variants = + EntryPointBindingExpander.expandPathVariableBindings(entryPoint, context, callGraph); + + assertThat(variants).extracting(map -> map.get("event")) + .containsExactlyInAnyOrder("PAY", "SHIP"); + assertThat(EntryPointBindingExpander.withResolvedPath( + entryPoint, Map.of("event", "PAY")).getName()) + .isEqualTo("POST /api/order/PAY"); + } +} diff --git a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java index 88b1d59..0564ec0 100644 --- a/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java +++ b/state_machine_exporter_html/src/test/java/click/kamil/springstatemachineexporter/html/EnterpriseDedicatedEndpointHtmlTest.java @@ -46,6 +46,9 @@ class EnterpriseDedicatedEndpointHtmlTest { assertThat(html).contains("\"source\" : \"click.kamil.enterprise.machines.order.OrderState.NEW\""); assertThat(html).contains("\"source\" : \"click.kamil.enterprise.machines.order.OrderState.PENDING\""); assertThat(html).contains("function highlightEvents(steps)"); + assertThat(html).contains("\"name\" : \"Order fulfillment\""); + assertThat(html).containsPattern( + "\"steps\"\\s*:\\s*\\[\\s*\\{[^\\]]*\"linkKey\"\\s*:\\s*\"OrderState_NEW__OrderEvent_PAY\""); } private static Path findOrderMachineHtml(Path outputDir) throws Exception {