Harden polymorphic event narrowing to prevent over-linking transitions.

Fail closed when enum predicates or polymorphicEvents cannot be resolved, infer candidates from configured transitions, and validate exports that link more transitions than the machine defines.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-12 16:22:58 +02:00
parent 77bf840465
commit 83008a6898
6 changed files with 417 additions and 51 deletions

View File

@@ -101,11 +101,22 @@ class StrictFqnMatchingEngineTest {
}
@Test
void shouldMatchWildcards() {
void shouldNotMatchWildcardAgainstEnumTransitionWithoutPolymorphicEvents() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder().event("event").build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldNotMatchDynamicMethodCallWithoutResolvedPolymorphicEvents() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("event.getPayload()")
.eventTypeFqn("com.example.OrderEvents")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
@@ -187,13 +198,25 @@ class StrictFqnMatchingEngineTest {
}
@Test
void shouldMatchDynamicVariableWithMatchingTypeFqn() {
void shouldNotMatchDynamicVariableWithoutResolvedPolymorphicEvents() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("myCustomEvt") // Not "event", "e", etc., but a custom variable name
.event("myCustomEvt")
.eventTypeFqn("com.example.OrderEvents")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldMatchDynamicVariableWhenPolymorphicEventsAreResolved() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("myCustomEvt")
.eventTypeFqn("com.example.OrderEvents")
.polymorphicEvents(List.of("com.example.OrderEvents.PAY"))
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
@@ -209,11 +232,22 @@ class StrictFqnMatchingEngineTest {
}
@Test
void shouldMatchDynamicVariableWithUnknownTypeFqn() {
void shouldNotMatchDynamicVariableWithUnknownTypeFqnWhenSmEventIsEnum() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("myCustomEvt")
.eventTypeFqn(null) // Unknown type, fallback to true
.eventTypeFqn(null)
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldMatchDynamicVariableWithUnknownTypeFqnForStringEvents() {
Event smEvent = Event.of("PAY", "PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("myCustomEvt")
.eventTypeFqn(null)
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
@@ -250,15 +284,4 @@ class StrictFqnMatchingEngineTest {
assertThat(engine.matches(smEvent, triggerPoint)).isFalse();
}
@Test
void shouldMatchDynamicMethodCallAsVariable() {
Event smEvent = Event.of("PAY", "com.example.OrderEvents.PAY");
TriggerPoint triggerPoint = TriggerPoint.builder()
.event("event.getPayload()") // Dynamic method call
.eventTypeFqn("com.example.OrderEvents")
.build();
assertThat(engine.matches(smEvent, triggerPoint)).isTrue();
}
}

View File

@@ -214,6 +214,172 @@ class EnumMemberPredicatePolymorphicInferenceTest {
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
}
@Test
void shouldNarrowEqualSizePolyListWithNonTransitionConstantsWhenConstraintIsNull(@TempDir Path tempDir) throws Exception {
CodebaseContext context = scanEnumProject(tempDir, """
package com.example.order;
public enum OrderCommand {
PAY,
SHIP,
META;
}
""");
TriggerPoint trigger = TriggerPoint.builder()
.event("event")
.polymorphicEvents(List.of(
"com.example.order.OrderCommand.PAY",
"com.example.order.OrderCommand.SHIP",
"com.example.order.OrderCommand.META"))
.constraint(null)
.ambiguous(true)
.build();
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
List<Transition> transitions = List.of(
transition("OrderCommand.PAY", "NEW", "PAID"),
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
trigger, types, context, transitions, true);
assertThat(enriched.getPolymorphicEvents())
.containsExactlyInAnyOrder(
"com.example.order.OrderCommand.PAY",
"com.example.order.OrderCommand.SHIP");
assertThat(enriched.getPolymorphicEvents())
.doesNotContain("com.example.order.OrderCommand.META");
}
@Test
void shouldFallBackToTransitionEventsWhenPredicateEvaluationIsInconclusive(@TempDir Path tempDir) throws Exception {
CodebaseContext context = scanEnumProject(tempDir, """
package com.example.order;
public enum OrderCommand {
PAY,
SHIP,
META;
}
""");
TriggerPoint trigger = TriggerPoint.builder()
.event("OrderCommand.valueOf(name)")
.constraint("eventType.isEvent()")
.build();
MachineTypes types = new MachineTypes(null, "com.example.order.OrderCommand");
List<Transition> transitions = List.of(
transition("OrderCommand.PAY", "NEW", "PAID"),
transition("OrderCommand.SHIP", "PAID", "SHIPPED"));
TriggerPoint enriched = MachineEnumCanonicalizer.ensureCallChainPolymorphicEvents(
trigger, types, context, transitions, true);
assertThat(enriched.getPolymorphicEvents())
.containsExactlyInAnyOrder(
"com.example.order.OrderCommand.PAY",
"com.example.order.OrderCommand.SHIP");
}
@Test
void shouldExcludeConstantsWhenPredicateMethodCannotBeEvaluated(@TempDir Path tempDir) throws Exception {
CodebaseContext context = scanEnumProject(tempDir, """
package com.example.order;
public enum OrderCommand {
PAY,
META;
}
""");
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
List.of(
"com.example.order.OrderCommand.PAY",
"com.example.order.OrderCommand.META"),
"eventType.isEvent()",
"com.example.order.OrderCommand",
context);
assertThat(filtered).isEmpty();
}
@Test
void shouldEvaluateInterfaceDefaultPredicateDelegation(@TempDir Path tempDir) throws Exception {
Path javaRoot = tempDir.resolve("src/main/java/com/example");
Files.createDirectories(javaRoot.resolve("core"));
Files.createDirectories(javaRoot.resolve("order"));
Files.writeString(tempDir.resolve("build.gradle"), "plugins { id 'java' }");
Files.writeString(javaRoot.resolve("core/TriggerClassifier.java"), """
package com.example.core;
public interface TriggerClassifier {
default boolean isEvent() { return isTrigger(); }
boolean isTrigger();
}
""");
Files.writeString(javaRoot.resolve("order/OrderEvent.java"), """
package com.example.order;
import com.example.core.TriggerClassifier;
public enum OrderEvent implements TriggerClassifier {
PAY(true),
LOG(false);
private final boolean trigger;
OrderEvent(boolean trigger) { this.trigger = trigger; }
@Override
public boolean isTrigger() { return this.trigger; }
}
""");
CodebaseContext context = new CodebaseContext();
context.setSourcepath(List.of(tempDir.resolve("src/main/java").toString()));
context.scan(tempDir);
List<String> filtered = EnumMemberPredicateEvaluator.filterEnumConstants(
List.of("com.example.order.OrderEvent.PAY", "com.example.order.OrderEvent.LOG"),
"eventType.isEvent()",
"com.example.order.OrderEvent",
context);
assertThat(filtered).containsExactly("com.example.order.OrderEvent.PAY");
}
@Test
void shouldInferPolymorphicEventsForNullPolyExternalTriggerWithConstraint(@TempDir Path tempDir) throws Exception {
CodebaseContext context = scanEnumProject(tempDir, """
package com.example.order;
public enum OrderCommand {
PAY(true),
SHIP(true),
META(false);
private final boolean event;
OrderCommand(boolean event) { this.event = event; }
public boolean isEvent() { return event; }
}
""");
CallChain chain = CallChain.builder()
.triggerPoint(TriggerPoint.builder()
.event("eventType")
.constraint("eventType.isEvent()")
.external(true)
.ambiguous(true)
.build())
.build();
Transition pay = transition("OrderCommand.PAY", "NEW", "PAID");
Transition ship = transition("OrderCommand.SHIP", "PAID", "SHIPPED");
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.eventTypeFqn("com.example.order.OrderCommand")
.transitions(List.of(pay, ship))
.metadata(CodebaseMetadata.builder().callChains(List.of(chain)).build())
.build();
new TransitionLinkerEnricher().enrich(result, context, null);
TriggerPoint tp = result.getMetadata().getCallChains().get(0).getTriggerPoint();
assertThat(tp.getPolymorphicEvents())
.containsExactlyInAnyOrder(
"com.example.order.OrderCommand.PAY",
"com.example.order.OrderCommand.SHIP");
assertThat(result.getMetadata().getCallChains().get(0).getMatchedTransitions()).hasSize(2);
}
@Test
void shouldNotMatchNonEventConstantsWhenPolyListWasOverBroad(@TempDir Path tempDir) throws Exception {
CodebaseContext context = scanEnumProject(tempDir, """