91d14ebdee
Notify.Send mints NotificationId as Guid "N" (no dashes) and the outbox parses it back into the audit row's CorrelationId, so the Flow Debugger's Notify node linked and relayed a "D" string the Notifications table never matches (found live: the report opened empty). The link and RetryNotificationRequest now use "N", and the Notification report normalises any Guid-shaped ?correlationId= to "N" so Audit Log and Flow Debugger callers both land. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
186 lines
8.9 KiB
C#
186 lines
8.9 KiB
C#
using Bunit;
|
|
using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Deployment;
|
|
using ZB.MOM.WW.ScadaBridge.Commons.Types.Debugging;
|
|
|
|
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. 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, 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, correlationId);
|
|
|
|
private static IReadOnlyList<FlowLifecycleRow> CachedLifecycle(string terminalStatus) =>
|
|
[
|
|
new(Guid.NewGuid(), "CachedSubmit", "Submitted", T0, "node-a", null, null),
|
|
new(Guid.NewGuid(), "ApiCallCached", "Attempted", T0.AddSeconds(3), "node-a", "503 Service Unavailable", 40),
|
|
new(Guid.NewGuid(), "CachedResolve", terminalStatus, T0.AddMilliseconds(4100), "node-b", null, null),
|
|
];
|
|
|
|
[Fact]
|
|
public void LifecycleTab_RendersRowsInTimeOrder_AndDeliveredBy()
|
|
{
|
|
var step = Step(FlowStepKind.ApiCall, FlowStepOutcome.Ok, "Delmia.AckRecipe", CachedLifecycle("Delivered"));
|
|
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
|
|
|
cut.Find("[data-test=flow-step-tab-lifecycle]").Click();
|
|
|
|
var rows = cut.FindAll("[data-test=flow-step-lifecycle] tbody tr");
|
|
Assert.Equal(3, rows.Count);
|
|
Assert.Contains("CachedSubmit", rows[0].TextContent);
|
|
Assert.Contains("Submitted", rows[0].TextContent);
|
|
Assert.Contains("503 Service Unavailable", rows[1].TextContent);
|
|
Assert.Contains("CachedResolve", rows[2].TextContent);
|
|
Assert.Contains("node-b", rows[2].TextContent);
|
|
Assert.Contains("12:00:04.100", rows[2].TextContent);
|
|
|
|
var where = cut.Find("[data-test=flow-step-where]").TextContent;
|
|
Assert.Contains("System", where);
|
|
Assert.Contains("Delmia.AckRecipe", where);
|
|
Assert.Contains("Delivered by", where);
|
|
Assert.Contains("node-b", where);
|
|
Assert.Empty(cut.FindAll("[data-test=flow-step-parked-hint]"));
|
|
}
|
|
|
|
[Fact]
|
|
public void NoLifecycle_HidesTheTab()
|
|
{
|
|
var step = Step(FlowStepKind.AttributeWrite, FlowStepOutcome.Ok, "A") with { Lane = FlowLane.Tags };
|
|
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
|
Assert.Empty(cut.FindAll("[data-test=flow-step-tab-lifecycle]"));
|
|
Assert.Equal("Input", cut.Find("[data-test=flow-step-tab-input]").TextContent.Trim());
|
|
}
|
|
|
|
[Fact]
|
|
public void ParkedNotify_ShowsHint_AndListLabel_AndParkedBadge()
|
|
{
|
|
var lifecycle = new List<FlowLifecycleRow>
|
|
{
|
|
new(Guid.NewGuid(), "NotifySend", "Submitted", T0, "node-a", null, null),
|
|
new(Guid.NewGuid(), "NotifyDeliver", "Parked", T0.AddSeconds(9), "central-a", "retries exhausted", null),
|
|
};
|
|
var step = Step(FlowStepKind.Notify, FlowStepOutcome.Parked, "Planners", lifecycle);
|
|
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
|
|
|
Assert.Equal("Parked", cut.Find("[data-test=flow-step-outcome]").TextContent.Trim());
|
|
Assert.Contains("warning", cut.Find("[data-test=flow-step-outcome]").ClassName);
|
|
Assert.Equal("Subject + body", cut.Find("[data-test=flow-step-tab-input]").TextContent.Trim());
|
|
var where = cut.Find("[data-test=flow-step-where]").TextContent;
|
|
Assert.Contains("List", where);
|
|
Assert.Contains("central-a", where);
|
|
|
|
cut.Find("[data-test=flow-step-tab-lifecycle]").Click();
|
|
Assert.Contains("Site Calls / Notification", cut.Find("[data-test=flow-step-parked-hint]").TextContent);
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData(FlowStepKind.ApiCall, "Request", "System")]
|
|
[InlineData(FlowStepKind.DbCall, "SQL + parameters", "Connection")]
|
|
[InlineData(FlowStepKind.Notify, "Subject + body", "List")]
|
|
public void OutboundKinds_LabelInputTabAndTarget(FlowStepKind kind, string inputLabel, string targetLabel)
|
|
{
|
|
var step = Step(kind, FlowStepOutcome.Pending, "T");
|
|
var cut = Render<FlowStepDetail>(p => p.Add(x => x.Step, step));
|
|
Assert.Equal(inputLabel, cut.Find("[data-test=flow-step-tab-input]").TextContent.Trim());
|
|
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:N}", 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:N}", 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]"));
|
|
}
|
|
}
|