feat(central-ui): Flow Debugger step detail links to the report page and exposes Retry/Discard on a parked node

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 11:01:08 -04:00
parent 95773aa9fd
commit 9c31f70105
2 changed files with 153 additions and 5 deletions
@@ -6,15 +6,16 @@ namespace ZB.MOM.WW.ScadaBridge.CentralUI.Tests.Deployment;
/// <summary>
/// Flow Debugger outbound-calls plan (2026-09-06) Task 8: the step detail panel's Lifecycle tab,
/// outbound "Where this ran" fields and kind-specific Input tab labels.
/// outbound "Where this ran" fields and kind-specific Input tab labels. Follow-ups plan (2026-09-06)
/// Task 6: report deep links from a correlated outbound step and Retry/Discard on a parked one (D2/D5).
/// </summary>
public class FlowStepDetailTests : BunitContext
{
private static readonly DateTimeOffset T0 = new(2026, 9, 6, 12, 0, 0, TimeSpan.Zero);
private static FlowStep Step(FlowStepKind kind, FlowStepOutcome outcome, string target, IReadOnlyList<FlowLifecycleRow>? lifecycle = null)
private static FlowStep Step(FlowStepKind kind, FlowStepOutcome outcome, string target, IReadOnlyList<FlowLifecycleRow>? lifecycle = null, Guid? correlationId = null)
=> new(Guid.NewGuid(), Guid.NewGuid(), null, kind, FlowLane.External, target, "Delivered · cached", T0, 4100, outcome,
"{\"id\":1}", "{\"ok\":true}", outcome == FlowStepOutcome.Failed ? "boom" : null, "site-a", "node-a", "M1", target, null, lifecycle);
"{\"id\":1}", "{\"ok\":true}", outcome == FlowStepOutcome.Failed ? "boom" : null, "site-a", "node-a", "M1", target, null, lifecycle, correlationId);
private static IReadOnlyList<FlowLifecycleRow> CachedLifecycle(string terminalStatus) =>
[
@@ -91,4 +92,94 @@ public class FlowStepDetailTests : BunitContext
Assert.Contains(targetLabel, cut.Find("[data-test=flow-step-where]").TextContent);
Assert.Equal("Pending", cut.Find("[data-test=flow-step-outcome]").TextContent.Trim());
}
[Fact]
public void OutboundStepWithCorrelation_LinksToTheReportPage()
{
var c = Guid.NewGuid();
var api = Step(FlowStepKind.ApiCall, FlowStepOutcome.Ok, "Delmia.AckRecipe", CachedLifecycle("Delivered"), c);
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, api));
var link = cut.Find("a[data-test=open-report]");
Assert.Equal($"/site-calls/report?correlationId={c:D}", link.GetAttribute("href"));
Assert.Equal("Open in Site Calls", link.TextContent.Trim());
var notify = Step(FlowStepKind.Notify, FlowStepOutcome.Ok, "Planners", CachedLifecycle("Delivered"), c);
cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, notify));
link = cut.Find("a[data-test=open-report]");
Assert.Equal($"/notifications/report?correlationId={c:D}", link.GetAttribute("href"));
Assert.Equal("Open in Notification report", link.TextContent.Trim());
var sync = Step(FlowStepKind.ApiCall, FlowStepOutcome.Ok, "Delmia.AckRecipe");
cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, sync));
Assert.Empty(cut.FindAll("[data-test=open-report]"));
}
[Fact]
public void ParkedStep_WithCallbacks_ShowsRetryAndDiscard()
{
var c = Guid.NewGuid();
var step = Step(FlowStepKind.ApiCall, FlowStepOutcome.Parked, "Delmia.AckRecipe", CachedLifecycle("Parked"), c);
FlowStep? retried = null;
FlowStep? discarded = null;
var cut = Render<FlowStepDetail>(p => p
.Add(x => x.Step, step)
.Add(x => x.OnRetry, s => retried = s)
.Add(x => x.OnDiscard, s => discarded = s));
var retry = cut.Find("button[data-test=flow-step-retry]");
var discard = cut.Find("button[data-test=flow-step-discard]");
Assert.Equal("Retry", retry.TextContent.Trim());
Assert.Equal("Discard", discard.TextContent.Trim());
Assert.Contains("btn-outline-primary", retry.ClassName);
Assert.Contains("btn-outline-danger", discard.ClassName);
Assert.False(retry.HasAttribute("disabled"));
Assert.False(discard.HasAttribute("disabled"));
retry.Click();
Assert.Same(step, retried);
Assert.Null(discarded);
discard.Click();
Assert.Same(step, discarded);
cut.Render(p => p
.Add(x => x.Step, step)
.Add(x => x.OnRetry, s => retried = s)
.Add(x => x.OnDiscard, s => discarded = s)
.Add(x => x.ActionInProgress, true));
Assert.True(cut.Find("button[data-test=flow-step-retry]").HasAttribute("disabled"));
Assert.True(cut.Find("button[data-test=flow-step-discard]").HasAttribute("disabled"));
}
[Fact]
public void ParkedStep_WithoutCallbacks_ShowsOnlyTheHintLink()
{
var c = Guid.NewGuid();
var step = Step(FlowStepKind.Notify, FlowStepOutcome.Parked, "Planners", CachedLifecycle("Parked"), c);
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
Assert.Empty(cut.FindAll("[data-test=flow-step-retry]"));
Assert.Empty(cut.FindAll("[data-test=flow-step-discard]"));
cut.Find("[data-test=flow-step-tab-lifecycle]").Click();
var hint = cut.Find("[data-test=flow-step-parked-hint]");
Assert.Contains("Parked", hint.TextContent);
var hintLink = hint.QuerySelector("a");
Assert.NotNull(hintLink);
Assert.Equal($"/notifications/report?correlationId={c:D}", hintLink!.GetAttribute("href"));
}
[Fact]
public void OkStep_NeverShowsActions()
{
var step = Step(FlowStepKind.ApiCall, FlowStepOutcome.Ok, "Delmia.AckRecipe", CachedLifecycle("Delivered"), Guid.NewGuid());
var cut = Render<FlowStepDetail>(p => p
.Add(x => x.Step, step)
.Add(x => x.OnRetry, _ => { })
.Add(x => x.OnDiscard, _ => { }));
Assert.Empty(cut.FindAll("[data-test=flow-step-retry]"));
Assert.Empty(cut.FindAll("[data-test=flow-step-discard]"));
}
}