From 688136de856e11649b3adb6030642cc2086ee26e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Sun, 6 Sep 2026 14:10:44 -0400 Subject: [PATCH] fix(central-ui): Flow Debugger prediction overlay matches by kind and title (arch-review F10) Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29 --- .../Components/Shared/FlowGraphView.razor.cs | 8 +++++-- .../Shared/FlowGraphViewTests.cs | 24 +++++++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs index 4b566d42..33bfe56f 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/FlowGraphView.razor.cs @@ -410,11 +410,15 @@ public partial class FlowGraphView : ComponentBase if (hasPredicted) { - // Overlay: match predicted nodes to real steps by kind, in order; the leftovers are ghosts. + // Overlay: match predicted nodes to real steps by (kind, title) first, falling back to kind + // alone; the leftovers are ghosts. Kind-only matching pairs steps to predicted nodes in + // declaration order, which mismatches a method with multiple same-kind calls when only some + // of them actually ran (arch-review F10). var unmatched = predicted!.Nodes.ToList(); foreach (var s in graph.Steps) { - var m = unmatched.FirstOrDefault(p => p.Kind == s.Kind); + var m = unmatched.FirstOrDefault(p => p.Kind == s.Kind && string.Equals(p.Title, s.Title, StringComparison.OrdinalIgnoreCase)) + ?? unmatched.FirstOrDefault(p => p.Kind == s.Kind); if (m != null) { unmatched.Remove(m); diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/FlowGraphViewTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/FlowGraphViewTests.cs index 69ce06e8..33e06002 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/FlowGraphViewTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Shared/FlowGraphViewTests.cs @@ -193,6 +193,30 @@ public class FlowGraphViewTests : BunitContext Assert.True(int.Parse(ghost.GetAttribute("data-column")!) > realMax); } + [Fact] + public void Overlay_TwoPredictedRouteCalls_RealSecondCallMatchesByTitle_FirstIsTheGhost() + { + // The run only ever made the second of two predicted route calls (a branch skipped the first). + // Matching by kind alone would pair the real "B.Y" step with the first predicted node ("A.X") + // and leave "B.Y" as the ghost — wrong: "A.X" is the one that never ran. + var step = new FlowStep(Guid.NewGuid(), null, null, FlowStepKind.RouteCall, FlowLane.Central, "B.Y", null, + T0, 5, FlowStepOutcome.Ok, null, null, null, null, null, null, "B.Y", null); + var graph = new FlowGraph([step], [], false, false); + var predicted = new PredictedFlow( + [ + new("n1", FlowStepKind.RouteCall, FlowLane.Central, "A.X", false, null), + new("n2", FlowStepKind.RouteCall, FlowLane.Central, "B.Y", false, null), + ], []); + + var layout = FlowGraphView.ComputeLayout(graph, predicted); + + var real = Assert.Single(layout.Nodes, n => n.Step != null); + Assert.Equal("B.Y", real.Predicted?.Title); + var ghost = Assert.Single(layout.Nodes, n => n.Ghost); + Assert.Equal("p:n1", ghost.Key); + Assert.Equal("A.X", ghost.Predicted?.Title); + } + [Fact] public void Render_UnderGermanCulture_CoordinatesStayInvariant() {