diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrower.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrower.java
new file mode 100644
index 0000000..010058f
--- /dev/null
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrower.java
@@ -0,0 +1,422 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.enricher.path.SymbolicPathValueEstimator;
+import click.kamil.springstatemachineexporter.analysis.model.CallChain;
+import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.analysis.resolver.MachineEnumCanonicalizer;
+import click.kamil.springstatemachineexporter.analysis.resolver.StateMachineTypeResolver;
+import click.kamil.springstatemachineexporter.ast.common.CodebaseContext;
+import click.kamil.springstatemachineexporter.model.Transition;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Narrows wide call-graph polymorphic widens to endpoint-specific enum constants.
+ *
+ *
Uses multiple independent evidence sources (REST path, command keys, constraints, static trigger
+ * events, method-chain literals, camelCase method names) and only narrows when evidence converges.
+ */
+public final class CallChainPolyNarrower {
+
+ private static final int STRONG = 3;
+ private static final int MEDIUM = 2;
+ private static final int WEAK = 1;
+
+ private static final Pattern QUOTED_EQUALS_PARAM = Pattern.compile(
+ "\"([^\"]+)\"\\.equalsIgnoreCase\\((\\w+)\\)");
+ private static final Pattern ENUM_EQ = Pattern.compile(
+ "([A-Za-z_][\\w.]*)\\s*==\\s*([A-Za-z_][\\w.]*)");
+ private static final Pattern ROUTE_LITERAL = Pattern.compile(
+ "route\\(\\s*\"([^\"]+)\"\\s*,\\s*\"([^\"]+)\"\\s*\\)");
+
+ private CallChainPolyNarrower() {
+ }
+
+ public static TriggerPoint narrowForCallChain(
+ TriggerPoint trigger,
+ CallChain chain,
+ StateMachineTypeResolver.MachineTypes machineTypes,
+ List machineTransitions,
+ CodebaseContext context) {
+ if (trigger == null || machineTypes == null || machineTypes.eventTypeFqn() == null) {
+ return trigger;
+ }
+ List poly = trigger.getPolymorphicEvents();
+ if (poly == null || poly.size() <= 1) {
+ return trigger;
+ }
+
+ String eventTypeFqn = machineTypes.eventTypeFqn();
+ Set configuredConstants = configuredEventConstants(machineTransitions, eventTypeFqn, context);
+ if (configuredConstants.isEmpty()) {
+ return trigger;
+ }
+
+ HintVotes votes = new HintVotes();
+ collectHints(trigger, chain, configuredConstants, context, votes);
+
+ String chosen = votes.choose(configuredConstants);
+ if (chosen == null) {
+ return trigger;
+ }
+
+ List narrowed = filterPolyToConstant(poly, chosen, eventTypeFqn, context);
+ if (narrowed.size() == 1) {
+ return trigger.toBuilder()
+ .polymorphicEvents(narrowed)
+ .ambiguous(false)
+ .build();
+ }
+
+ return trigger.toBuilder()
+ .polymorphicEvents(List.of(eventTypeFqn + "." + chosen))
+ .ambiguous(false)
+ .build();
+ }
+
+ private static void collectHints(
+ TriggerPoint trigger,
+ CallChain chain,
+ Set configuredConstants,
+ CodebaseContext context,
+ HintVotes votes) {
+ collectFromStaticTriggerEvent(trigger, configuredConstants, votes);
+ collectFromEntryPoint(chain != null ? chain.getEntryPoint() : null, configuredConstants, votes);
+ collectFromConstraint(trigger.getConstraint(), configuredConstants, votes);
+ if (chain != null) {
+ collectFromMethodChain(chain.getMethodChain(), configuredConstants, votes);
+ collectFromPathEstimator(chain, configuredConstants, context, votes);
+ }
+ }
+
+ private static void collectFromStaticTriggerEvent(
+ TriggerPoint trigger,
+ Set configuredConstants,
+ HintVotes votes) {
+ String event = trigger.getEvent();
+ if (event == null || event.isBlank()) {
+ return;
+ }
+ if (MachineEnumCanonicalizer.isDynamicTriggerExpression(event)) {
+ return;
+ }
+ addEnumReference(event, configuredConstants, votes, STRONG);
+ }
+
+ private static void collectFromEntryPoint(
+ EntryPoint entryPoint,
+ Set configuredConstants,
+ HintVotes votes) {
+ if (entryPoint == null) {
+ return;
+ }
+ if (entryPoint.getName() != null) {
+ collectFromPath(entryPoint.getName(), configuredConstants, votes);
+ }
+ if (entryPoint.getMetadata() != null) {
+ Object path = entryPoint.getMetadata().get("path");
+ if (path instanceof String pathValue) {
+ collectFromPath(pathValue, configuredConstants, votes);
+ }
+ }
+ if (entryPoint.getMethodName() != null) {
+ addUniqueIdentifierMatch(entryPoint.getMethodName(), configuredConstants, votes, WEAK);
+ }
+ }
+
+ private static void collectFromPath(String rawPath, Set configuredConstants, HintVotes votes) {
+ if (rawPath == null || rawPath.isBlank()) {
+ return;
+ }
+ int space = rawPath.indexOf(' ');
+ String path = space >= 0 ? rawPath.substring(space + 1).trim() : rawPath.trim();
+ if (path.isBlank()) {
+ return;
+ }
+ for (String segment : path.split("/")) {
+ if (segment.isBlank() || segment.contains("{")) {
+ continue;
+ }
+ addPathToken(segment, configuredConstants, votes, STRONG);
+ }
+ }
+
+ private static void collectFromConstraint(
+ String constraint,
+ Set configuredConstants,
+ HintVotes votes) {
+ if (constraint == null || constraint.isBlank()) {
+ return;
+ }
+ Matcher quoted = QUOTED_EQUALS_PARAM.matcher(constraint);
+ while (quoted.find()) {
+ String literal = quoted.group(1);
+ String param = quoted.group(2);
+ if ("event".equals(param) || "eventString".equals(param) || "actionKey".equals(param)
+ || "action".equals(param)) {
+ addPathToken(literal, configuredConstants, votes, MEDIUM);
+ }
+ if ("commandKey".equals(param) || literal.contains(".")) {
+ addPathToken(literal, configuredConstants, votes, MEDIUM);
+ }
+ }
+ Matcher route = ROUTE_LITERAL.matcher(constraint);
+ while (route.find()) {
+ addPathToken(route.group(2), configuredConstants, votes, MEDIUM);
+ }
+ Matcher enumEq = ENUM_EQ.matcher(constraint);
+ while (enumEq.find()) {
+ addEnumReference(enumEq.group(1), configuredConstants, votes, MEDIUM);
+ addEnumReference(enumEq.group(2), configuredConstants, votes, MEDIUM);
+ }
+ }
+
+ private static void collectFromMethodChain(
+ List methodChain,
+ Set configuredConstants,
+ HintVotes votes) {
+ if (methodChain == null) {
+ return;
+ }
+ for (String methodFqn : methodChain) {
+ if (methodFqn == null || !methodFqn.contains(".")) {
+ continue;
+ }
+ String methodName = methodFqn.substring(methodFqn.lastIndexOf('.') + 1);
+ addUniqueIdentifierMatch(methodName, configuredConstants, votes, WEAK);
+ for (String token : splitCamelCase(methodName)) {
+ addPathToken(token, configuredConstants, votes, WEAK);
+ }
+ }
+ }
+
+ private static void collectFromPathEstimator(
+ CallChain chain,
+ Set configuredConstants,
+ CodebaseContext context,
+ HintVotes votes) {
+ if (context == null) {
+ return;
+ }
+ List estimated = new SymbolicPathValueEstimator().estimatePossibleValues(chain, context);
+ if (estimated == null || estimated.isEmpty()) {
+ return;
+ }
+ int weight = estimated.size() == 1 ? MEDIUM : WEAK;
+ for (String value : estimated) {
+ addEnumReference(value, configuredConstants, votes, weight);
+ }
+ }
+
+ private static void addPathToken(
+ String token,
+ Set configuredConstants,
+ HintVotes votes,
+ int weight) {
+ if (token == null || token.isBlank()) {
+ return;
+ }
+ addPathTokenVariants(token, configuredConstants, votes, weight);
+ if (token.contains(".")) {
+ addPathTokenVariants(token.substring(token.lastIndexOf('.') + 1), configuredConstants, votes, weight);
+ }
+ }
+
+ private static void addPathTokenVariants(
+ String token,
+ Set configuredConstants,
+ HintVotes votes,
+ int weight) {
+ maybeVoteConstant(votes, normalizeToken(token), configuredConstants, weight);
+ for (String part : token.split("[-_.]")) {
+ if (!part.isBlank()) {
+ maybeVoteConstant(votes, normalizeToken(part), configuredConstants, weight);
+ }
+ }
+ if (token.toUpperCase(Locale.ROOT).contains("_")) {
+ String tail = token.substring(token.lastIndexOf('_') + 1);
+ maybeVoteConstant(votes, normalizeToken(tail), configuredConstants, weight);
+ }
+ }
+
+ private static void addEnumReference(
+ String reference,
+ Set configuredConstants,
+ HintVotes votes,
+ int weight) {
+ if (reference == null || reference.isBlank()) {
+ return;
+ }
+ maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
+ if (reference.contains(".")) {
+ maybeVoteConstant(votes, constantName(reference), configuredConstants, weight);
+ }
+ String upper = reference.toUpperCase(Locale.ROOT);
+ if (upper.contains("_")) {
+ maybeVoteConstant(votes, upper.substring(upper.lastIndexOf('_') + 1), configuredConstants, weight);
+ }
+ }
+
+ private static void addUniqueIdentifierMatch(
+ String identifier,
+ Set configuredConstants,
+ HintVotes votes,
+ int weight) {
+ if (identifier == null || identifier.isBlank()) {
+ return;
+ }
+ String upper = identifier.toUpperCase(Locale.ROOT);
+ List matched = new ArrayList<>();
+ for (String constant : configuredConstants) {
+ if (upper.contains(constant)) {
+ matched.add(constant);
+ }
+ }
+ if (matched.size() == 1) {
+ votes.add(matched.get(0), weight);
+ }
+ }
+
+ private static void maybeVoteConstant(
+ HintVotes votes,
+ String candidate,
+ Set configuredConstants,
+ int weight) {
+ if (candidate == null || candidate.isBlank()) {
+ return;
+ }
+ String normalized = candidate.toUpperCase(Locale.ROOT);
+ if (configuredConstants.contains(normalized)) {
+ votes.add(normalized, weight);
+ }
+ }
+
+ private static List filterPolyToConstant(
+ List poly,
+ String constant,
+ String eventTypeFqn,
+ CodebaseContext context) {
+ List narrowed = new ArrayList<>();
+ for (String candidate : poly) {
+ if (candidate == null || candidate.startsWith(" splitCamelCase(String identifier) {
+ if (identifier == null || identifier.isBlank()) {
+ return List.of();
+ }
+ List tokens = new ArrayList<>();
+ StringBuilder current = new StringBuilder();
+ for (int i = 0; i < identifier.length(); i++) {
+ char ch = identifier.charAt(i);
+ if (Character.isUpperCase(ch) && !current.isEmpty()) {
+ tokens.add(current.toString());
+ current.setLength(0);
+ }
+ current.append(ch);
+ }
+ if (!current.isEmpty()) {
+ tokens.add(current.toString());
+ }
+ return tokens;
+ }
+
+ private static Set configuredEventConstants(
+ List transitions,
+ String eventTypeFqn,
+ CodebaseContext context) {
+ Set constants = new LinkedHashSet<>();
+ for (String event : MachineEnumCanonicalizer.polymorphicEventsFromTransitions(
+ transitions, eventTypeFqn, context)) {
+ constants.add(constantName(event).toUpperCase(Locale.ROOT));
+ }
+ return constants;
+ }
+
+ private static String constantName(String ref) {
+ int dot = ref.lastIndexOf('.');
+ return (dot >= 0 ? ref.substring(dot + 1) : ref).toUpperCase(Locale.ROOT);
+ }
+
+ private static final class HintVotes {
+ private final Map scores = new HashMap<>();
+ private final Map maxWeight = new HashMap<>();
+
+ void add(String constant, int weight) {
+ if (constant == null || constant.isBlank()) {
+ return;
+ }
+ String key = constant.toUpperCase(Locale.ROOT);
+ scores.merge(key, weight, Integer::sum);
+ maxWeight.merge(key, weight, Math::max);
+ }
+
+ String choose(Set configured) {
+ String strongUnique = uniqueAboveWeight(configured, STRONG);
+ if (strongUnique != null) {
+ return strongUnique;
+ }
+ String mediumUnique = uniqueAboveWeight(configured, MEDIUM);
+ if (mediumUnique != null) {
+ return mediumUnique;
+ }
+ String best = null;
+ int bestScore = 0;
+ for (String constant : configured) {
+ int score = scores.getOrDefault(constant, 0);
+ if (score > bestScore) {
+ bestScore = score;
+ best = constant;
+ }
+ }
+ if (best == null || bestScore < MEDIUM) {
+ return null;
+ }
+ final int winningScore = bestScore;
+ long contenders = scores.entrySet().stream()
+ .filter(e -> configured.contains(e.getKey()))
+ .filter(e -> e.getValue() == winningScore)
+ .count();
+ return contenders == 1 ? best : null;
+ }
+
+ private String uniqueAboveWeight(Set configured, int minWeight) {
+ String chosen = null;
+ for (String constant : configured) {
+ if (maxWeight.getOrDefault(constant, 0) >= minWeight) {
+ if (chosen != null) {
+ return null;
+ }
+ chosen = constant;
+ }
+ }
+ return chosen;
+ }
+ }
+}
diff --git a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java
index f6590a8..1b7290e 100644
--- a/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java
+++ b/state_machine_exporter/src/main/java/click/kamil/springstatemachineexporter/analysis/enricher/TransitionLinkerEnricher.java
@@ -61,6 +61,8 @@ public class TransitionLinkerEnricher implements AnalysisEnricher {
tp, machineTypes, context, chain.getEntryPoint());
tp = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
tp, machineTypes, context, stateMachineTransitions, true);
+ tp = CallChainPolyNarrower.narrowForCallChain(
+ tp, chain, machineTypes, stateMachineTransitions, context);
tp = MachineEnumCanonicalizer.canonicalizeTriggerLabelsForLinking(tp, machineTypes, context);
tp = CallChainLinkPolicy.applyRestExternalPolicy(tp, chain, context);
if (machineTypes != null) {
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrowerTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrowerTest.java
new file mode 100644
index 0000000..726168c
--- /dev/null
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/enricher/CallChainPolyNarrowerTest.java
@@ -0,0 +1,198 @@
+package click.kamil.springstatemachineexporter.analysis.enricher;
+
+import click.kamil.springstatemachineexporter.analysis.model.AnalysisResult;
+import click.kamil.springstatemachineexporter.analysis.model.CallChain;
+import click.kamil.springstatemachineexporter.analysis.model.CodebaseMetadata;
+import click.kamil.springstatemachineexporter.analysis.model.EntryPoint;
+import click.kamil.springstatemachineexporter.analysis.model.LinkResolution;
+import click.kamil.springstatemachineexporter.analysis.model.TriggerPoint;
+import click.kamil.springstatemachineexporter.model.Event;
+import click.kamil.springstatemachineexporter.model.State;
+import click.kamil.springstatemachineexporter.model.Transition;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class CallChainPolyNarrowerTest {
+
+ private final TransitionLinkerEnricher enricher = new TransitionLinkerEnricher();
+
+ @Test
+ void shouldNarrowWidePolyUsingExpandedRestPathSegment() {
+ Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
+ Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
+
+ CallChain chain = CallChain.builder()
+ .entryPoint(EntryPoint.builder()
+ .name("POST /api/machine/ORDER/transition/PAY")
+ .className("com.example.MachineController")
+ .methodName("transition")
+ .build())
+ .methodChain(List.of(
+ "com.example.MachineController.transition",
+ "com.example.StateMachineDispatcher.fireOrder",
+ "com.example.StateMachine.send"))
+ .triggerPoint(TriggerPoint.builder()
+ .ambiguous(true)
+ .event("OrderEvent.valueOf(eventStr)")
+ .polymorphicEvents(List.of(
+ "com.example.OrderEvent.PAY",
+ "com.example.OrderEvent.SHIP",
+ "com.example.OrderEvent.CANCEL"))
+ .build())
+ .build();
+
+ AnalysisResult result = AnalysisResult.builder()
+ .name("com.example.config.OrderStateMachineConfig")
+ .eventTypeFqn("com.example.OrderEvent")
+ .stateTypeFqn("com.example.OrderState")
+ .transitions(List.of(pay, ship))
+ .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
+ .build();
+
+ enricher.enrich(result, null, null);
+
+ CallChain linked = result.getMetadata().getCallChains().get(0);
+ assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
+ assertThat(linked.getMatchedTransitions()).hasSize(1);
+ assertThat(linked.getMatchedTransitions().get(0).getEvent())
+ .isEqualTo("com.example.OrderEvent.PAY");
+ assertThat(linked.getTriggerPoint().getPolymorphicEvents())
+ .containsExactly("com.example.OrderEvent.PAY");
+ }
+
+ @Test
+ void shouldNarrowWidePolyUsingRichDispatcherMethodName() {
+ Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
+ Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
+
+ CallChain chain = CallChain.builder()
+ .entryPoint(EntryPoint.builder()
+ .name("POST /api/orders/rich/pay")
+ .className("com.example.RichOrderController")
+ .methodName("payViaRichEvent")
+ .build())
+ .methodChain(List.of(
+ "com.example.RichOrderController.payViaRichEvent",
+ "com.example.CommandGateway.payOrderViaRichEvent",
+ "com.example.CentralEventDispatcher.orderPayViaRichEvent",
+ "com.example.CentralEventDispatcher.sendOrderEvent"))
+ .triggerPoint(TriggerPoint.builder()
+ .ambiguous(true)
+ .event("richEvent.getType()")
+ .polymorphicEvents(List.of(
+ "com.example.OrderEvent.PAY",
+ "com.example.OrderEvent.SHIP",
+ "com.example.OrderEvent.CANCEL"))
+ .build())
+ .build();
+
+ AnalysisResult result = AnalysisResult.builder()
+ .name("com.example.config.OrderStateMachineConfig")
+ .eventTypeFqn("com.example.OrderEvent")
+ .stateTypeFqn("com.example.OrderState")
+ .transitions(List.of(pay, ship))
+ .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
+ .build();
+
+ enricher.enrich(result, null, null);
+
+ CallChain linked = result.getMetadata().getCallChains().get(0);
+ assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
+ assertThat(linked.getMatchedTransitions()).hasSize(1);
+ assertThat(linked.getMatchedTransitions().get(0).getEvent())
+ .isEqualTo("com.example.OrderEvent.PAY");
+ }
+
+ @Test
+ void shouldNarrowUsingDottedCommandKeyPath() {
+ Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
+ Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
+
+ CallChain chain = CallChain.builder()
+ .entryPoint(EntryPoint.builder()
+ .name("POST /api/commands/order.pay")
+ .className("com.example.GenericCommandController")
+ .methodName("execute")
+ .build())
+ .methodChain(List.of(
+ "com.example.GenericCommandController.execute",
+ "com.example.CommandGateway.executeViaMapper",
+ "com.example.CentralEventDispatcher.orderPay",
+ "com.example.CentralEventDispatcher.sendOrderEvent"))
+ .triggerPoint(TriggerPoint.builder()
+ .ambiguous(true)
+ .event("OrderEvent.valueOf(actionKey)")
+ .constraint("\"order.pay\".equalsIgnoreCase(commandKey) && command == ORDER_PAY")
+ .polymorphicEvents(List.of(
+ "com.example.OrderEvent.PAY",
+ "com.example.OrderEvent.SHIP",
+ "com.example.OrderEvent.CANCEL"))
+ .build())
+ .build();
+
+ AnalysisResult result = AnalysisResult.builder()
+ .name("com.example.config.OrderStateMachineConfig")
+ .eventTypeFqn("com.example.OrderEvent")
+ .stateTypeFqn("com.example.OrderState")
+ .transitions(List.of(pay, ship))
+ .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
+ .build();
+
+ enricher.enrich(result, null, null);
+
+ CallChain linked = result.getMetadata().getCallChains().get(0);
+ assertThat(linked.getLinkResolution()).isEqualTo(LinkResolution.RESOLVED);
+ assertThat(linked.getMatchedTransitions()).hasSize(1);
+ assertThat(linked.getMatchedTransitions().get(0).getEvent())
+ .isEqualTo("com.example.OrderEvent.PAY");
+ }
+
+ @Test
+ void shouldFailClosedWhenWidePolyHasNoEndpointSpecificHint() {
+ Transition pay = transition("NEW", "PAID", "com.example.OrderEvent.PAY");
+ Transition ship = transition("PAID", "SHIPPED", "com.example.OrderEvent.SHIP");
+
+ CallChain chain = CallChain.builder()
+ .entryPoint(EntryPoint.builder()
+ .name("POST /api/machine/{machineType}/transition/{event}")
+ .className("com.example.MachineController")
+ .methodName("transition")
+ .build())
+ .triggerPoint(TriggerPoint.builder()
+ .ambiguous(true)
+ .external(true)
+ .event("OrderEvent.valueOf(eventStr)")
+ .polymorphicEvents(List.of(
+ "com.example.OrderEvent.PAY",
+ "com.example.OrderEvent.SHIP"))
+ .build())
+ .build();
+
+ AnalysisResult result = AnalysisResult.builder()
+ .name("com.example.config.OrderStateMachineConfig")
+ .eventTypeFqn("com.example.OrderEvent")
+ .stateTypeFqn("com.example.OrderState")
+ .transitions(List.of(pay, ship))
+ .metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
+ .build();
+
+ enricher.enrich(result, null, null);
+
+ CallChain linked = result.getMetadata().getCallChains().get(0);
+ assertThat(linked.getMatchedTransitions()).isNullOrEmpty();
+ assertThat(linked.getLinkResolution()).isIn(
+ LinkResolution.AMBIGUOUS_WIDEN,
+ LinkResolution.UNRESOLVED_EXTERNAL);
+ }
+
+ private static Transition transition(String source, String target, String eventFqn) {
+ Transition transition = new Transition();
+ transition.setSourceStates(List.of(State.of(source, "com.example.OrderState." + source)));
+ transition.setTargetStates(List.of(State.of(target, "com.example.OrderState." + target)));
+ transition.setEvent(Event.of(eventFqn.substring(eventFqn.lastIndexOf('.') + 1), eventFqn));
+ return transition;
+ }
+}
diff --git a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DispatcherPolyCeilingPipelineTest.java b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DispatcherPolyCeilingPipelineTest.java
index 31cc9f8..2e8f13d 100644
--- a/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DispatcherPolyCeilingPipelineTest.java
+++ b/state_machine_exporter/src/test/java/click/kamil/springstatemachineexporter/analysis/service/DispatcherPolyCeilingPipelineTest.java
@@ -67,14 +67,12 @@ class DispatcherPolyCeilingPipelineTest {
CallChain linked = linkChain(
context, bloatedChain, MACHINE_CONFIG, EVENT_TYPE, STATE_TYPE, pay, ship);
- assertPolyWithinTransitions(linked, 2);
- assertPolyCappedToTransitionEvents(linked, EVENT_TYPE, pay, ship);
assertThat(linked.getTriggerPoint().getPolymorphicEvents())
- .containsExactlyInAnyOrder(EVENT_TYPE + ".PAY", EVENT_TYPE + ".SHIP");
- assertMatchedWithinTransitions(linked, 2);
+ .containsExactly(EVENT_TYPE + ".PAY");
+ assertMatchedWithinTransitions(linked, 1);
assertThat(linked.getMatchedTransitions())
.extracting(MatchedTransition::getEvent)
- .containsExactlyInAnyOrder(EVENT_TYPE + ".PAY", EVENT_TYPE + ".SHIP");
+ .containsExactly(EVENT_TYPE + ".PAY");
}
@Test