Match HTML sidebar chains by entry point name when paths differ.

Expanded REST endpoints share the same controller method, so class/method matching grouped unrelated call chains; prefer entry point name when present and cover with enterprise HTML tests.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-14 17:39:58 +02:00
parent e3f3350398
commit 2e7afd6d42
3 changed files with 95 additions and 10 deletions

View File

@@ -394,10 +394,20 @@
return isLifecycleChain(c) || isHighlightableChain(c) || isUnresolvedExternalChain(c); return isLifecycleChain(c) || isHighlightableChain(c) || isUnresolvedExternalChain(c);
} }
function entryPointsMatch(chainEntryPoint, sidebarEntryPoint) {
if (!chainEntryPoint || !sidebarEntryPoint) {
return false;
}
if (chainEntryPoint.name && sidebarEntryPoint.name) {
return chainEntryPoint.name === sidebarEntryPoint.name;
}
return chainEntryPoint.className === sidebarEntryPoint.className
&& chainEntryPoint.methodName === sidebarEntryPoint.methodName;
}
function chainsForEntryPoint(ep) { function chainsForEntryPoint(ep) {
return (metadata.metadata.callChains || []).filter(c => return (metadata.metadata.callChains || []).filter(c =>
c.entryPoint.className === ep.className && entryPointsMatch(c.entryPoint, ep) &&
c.entryPoint.methodName === ep.methodName &&
isSidebarChain(c) isSidebarChain(c)
); );
} }

View File

@@ -13,16 +13,16 @@ import java.util.List;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Enterprise dedicated REST endpoints must produce HTML explorer link keys end-to-end; * Enterprise dedicated and expanded generic REST endpoints must produce HTML explorer link keys end-to-end.
* generic {@code {event}} dispatchers remain unresolved external.
*/ */
class EnterpriseDedicatedEndpointHtmlTest { class EnterpriseDedicatedEndpointHtmlTest {
private static final String PAY_ENDPOINT = "POST /api/machine/order/pay"; private static final String PAY_ENDPOINT = "POST /api/machine/order/pay";
private static final String GENERIC_ENDPOINT = "POST /api/machine/{machineType}/transition/{event}"; private static final String ORDER_PAY = "POST /api/machine/ORDER/transition/PAY";
private static final String ORDER_SHIP = "POST /api/machine/ORDER/transition/SHIP";
@Test @Test
void htmlShouldExposeLinkKeysForDedicatedPayEndpoint(@TempDir Path tempDir) throws Exception { void htmlShouldExposeLinkKeysForDedicatedAndExpandedOrderEndpoints(@TempDir Path tempDir) throws Exception {
Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise"); Path enterprise = findProjectRoot().resolve("state_machines/state_machine_enterprise");
ExportService exportService = new ExportService(List.of(new HtmlExporter(), new JsonExporter())); ExportService exportService = new ExportService(List.of(new HtmlExporter(), new JsonExporter()));
exportService.runExporter(enterprise, tempDir, List.of("html"), true, List.of(), null, null, exportService.runExporter(enterprise, tempDir, List.of("html"), true, List.of(), null, null,
@@ -33,11 +33,14 @@ class EnterpriseDedicatedEndpointHtmlTest {
String html = Files.readString(htmlFile); String html = Files.readString(htmlFile);
assertThat(html).contains(PAY_ENDPOINT); assertThat(html).contains(PAY_ENDPOINT);
assertThat(html).contains(ORDER_PAY);
assertThat(html).contains(ORDER_SHIP);
assertThat(html).contains("function entryPointsMatch(chainEntryPoint, sidebarEntryPoint)");
assertThat(html).contains("\"linkResolution\" : \"RESOLVED\""); assertThat(html).contains("\"linkResolution\" : \"RESOLVED\"");
assertThat(html).contains("#link_"); assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
assertThat(html).contains("\"linkKey\""); assertThat(html).contains("#link_OrderState_PENDING__OrderEvent_SHIP");
assertThat(html).contains(GENERIC_ENDPOINT); assertThat(html).contains("\"linkKey\" : \"OrderState_NEW__OrderEvent_PAY\"");
assertThat(html).contains("linkResolution === 'UNRESOLVED_EXTERNAL'"); assertThat(html).contains("\"linkKey\" : \"OrderState_PENDING__OrderEvent_SHIP\"");
} }
private static Path findOrderMachineHtml(Path outputDir) throws Exception { private static Path findOrderMachineHtml(Path outputDir) throws Exception {

View File

@@ -410,6 +410,78 @@ class HtmlTransitionLinkConsistencyTest {
assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY"); assertThat(html).contains("#link_OrderState_NEW__OrderEvent_PAY");
} }
@Test
void htmlShouldMatchCallChainsByEntryPointNameWhenControllerMethodIsShared() {
EntryPoint payTransition = EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name("POST /api/machine/ORDER/transition/PAY")
.className("com.example.web.StateMachineController")
.methodName("transition")
.build();
EntryPoint shipTransition = EntryPoint.builder()
.type(EntryPoint.Type.REST)
.name("POST /api/machine/ORDER/transition/SHIP")
.className("com.example.web.StateMachineController")
.methodName("transition")
.build();
CallChain payChain = CallChain.builder()
.entryPoint(payTransition)
.linkResolution(LinkResolution.RESOLVED)
.triggerPoint(TriggerPoint.builder()
.event("com.example.order.OrderEvent.PAY")
.build())
.matchedTransitions(List.of(MatchedTransition.builder()
.sourceState("com.example.order.OrderState.NEW")
.targetState("com.example.order.OrderState.PAID")
.event("com.example.order.OrderEvent.PAY")
.linkKey("OrderState_NEW__OrderEvent_PAY")
.build()))
.build();
CallChain shipChain = CallChain.builder()
.entryPoint(shipTransition)
.linkResolution(LinkResolution.RESOLVED)
.triggerPoint(TriggerPoint.builder()
.event("com.example.order.OrderEvent.SHIP")
.build())
.matchedTransitions(List.of(MatchedTransition.builder()
.sourceState("com.example.order.OrderState.PAID")
.targetState("com.example.order.OrderState.SHIPPED")
.event("com.example.order.OrderEvent.SHIP")
.linkKey("OrderState_PAID__OrderEvent_SHIP")
.build()))
.build();
AnalysisResult result = AnalysisResult.builder()
.name("com.example.config.OrderStateMachineConfiguration")
.stateTypeFqn("com.example.order.OrderState")
.eventTypeFqn("com.example.order.OrderEvent")
.transitions(List.of(
shortTransition(
"com.example.order.OrderEvent.PAY",
"com.example.order.OrderState.NEW",
"com.example.order.OrderState.PAID"),
shortTransition(
"com.example.order.OrderEvent.SHIP",
"com.example.order.OrderState.PAID",
"com.example.order.OrderState.SHIPPED")))
.startStates(Set.of("com.example.order.OrderState.NEW"))
.endStates(Set.of("com.example.order.OrderState.SHIPPED"))
.metadata(CodebaseMetadata.builder()
.entryPoints(List.of(payTransition, shipTransition))
.callChains(List.of(payChain, shipChain))
.build())
.build();
String html = new HtmlExporter().export(result, ExportOptions.builder().includeMetadataPane(true).build());
assertThat(html).contains("function entryPointsMatch(chainEntryPoint, sidebarEntryPoint)");
assertThat(html).contains("POST /api/machine/ORDER/transition/PAY");
assertThat(html).contains("POST /api/machine/ORDER/transition/SHIP");
assertThat(html).contains("\"linkKey\" : \"OrderState_NEW__OrderEvent_PAY\"");
assertThat(html).contains("\"linkKey\" : \"OrderState_PAID__OrderEvent_SHIP\"");
}
@Test @Test
void ambiguousWidenHtmlShouldExposeMatchedTransitionLinkKeys() { void ambiguousWidenHtmlShouldExposeMatchedTransitionLinkKeys() {
EntryPoint ambiguousEp = EntryPoint.builder() EntryPoint ambiguousEp = EntryPoint.builder()