Merge branch 'worktree-agent-a349413889618d94c' into fix/flow-debugger-arch-review

This commit is contained in:
Joseph Doherty
2026-09-06 14:13:56 -04:00
8 changed files with 266 additions and 27 deletions
@@ -119,6 +119,73 @@ public class FlowDebuggerPageTests : BunitContext
Assert.NotNull(cut.Find("[data-test='flow-graph-empty']"));
Assert.Equal("Not run", cut.Find("[data-test='flow-state']").TextContent.Trim());
Assert.True(cut.Find("button[data-test='flow-run']").HasAttribute("disabled"));
// arch-review F3: a Deployer gets the whole surface — entry point, both modes, Run.
Assert.NotNull(cut.Find("button[data-test='flow-entry-method']"));
Assert.NotNull(cut.Find("button[data-test='flow-mode-live']"));
Assert.NotNull(cut.Find("button[data-test='flow-mode-replay']"));
}
// ---- F3: Replay opens to the OperationalAudit roles; Live stays Deployer-only ----------------
[Fact]
public void Renders_ForViewer_ReplayOnly_WithNoLiveControls()
{
// A Viewer holds OperationalAudit but not Deployer: the page opens (it is the
// Audit Log / Execution Chain drill-in target) pinned to Replay, with no way to
// start a Live run.
RegisterServices(role: "Viewer");
var cut = Render<FlowDebuggerPage>();
Assert.NotNull(cut.Find("[data-test='flow-toolbar']"));
Assert.NotNull(cut.Find("input[data-test='flow-execution-id']"));
Assert.NotNull(cut.Find("button[data-test='flow-load']"));
Assert.NotNull(cut.Find("[data-test='flow-replay-only']"));
Assert.Empty(cut.FindAll("button[data-test='flow-run']"));
Assert.Empty(cut.FindAll("button[data-test='flow-mode-live']"));
Assert.Empty(cut.FindAll("button[data-test='flow-entry-method']"));
Assert.Empty(cut.FindAll("button[data-test='flow-entry-value-change']"));
Assert.Empty(cut.FindAll("select[data-test='flow-method-select']"));
}
[Fact]
public void Viewer_LoadsAReplay_ButGetsNoRetryOrDiscardOnAParkedNode()
{
// Retry / Discard relay a command to the site — a Deployer act. The auditor sees
// the Parked node and its detail, but not the two action buttons.
RegisterServices(role: "Viewer");
var root = Guid.NewGuid();
var (parked, _, _) = ParkedThenResolved(root);
_flow.ReplayAsync(root, Arg.Any<CancellationToken>()).Returns(parked);
var nav = Services.GetRequiredService<NavigationManager>();
nav.NavigateTo($"/deployment/flow-debugger?executionId={root}");
var cut = Render<FlowDebuggerPage>();
cut.Instance.Delay = (_, _) => Task.CompletedTask;
cut.WaitForAssertion(() => Assert.Equal("Completed", cut.Find("[data-test='flow-state']").TextContent.Trim()));
cut.Find("g[data-test='flow-node'][data-kind='ApiCall']").Click();
Assert.Empty(cut.FindAll("button[data-test='flow-step-retry']"));
Assert.Empty(cut.FindAll("button[data-test='flow-step-discard']"));
_flow.Received(1).ReplayAsync(root, Arg.Any<CancellationToken>());
}
[Fact]
public void Viewer_DrillInWithAMethodId_DoesNotOpenTheLiveRunSurface()
{
// ?methodId= is a Live-run drill-in. For a non-Deployer it must not put the page
// into Live mode or surface the Run button.
RegisterServices(role: "Viewer");
var nav = Services.GetRequiredService<NavigationManager>();
nav.NavigateTo("/deployment/flow-debugger?methodId=4");
var cut = Render<FlowDebuggerPage>();
Assert.Empty(cut.FindAll("button[data-test='flow-run']"));
Assert.Empty(cut.FindAll("select[data-test='flow-method-select']"));
Assert.NotNull(cut.Find("input[data-test='flow-execution-id']"));
}
[Fact]