feat(management): Flow Debugger inbound row carries exception detail; LogsTruncated on the run result

FlowDebugRunner.RunMethodAsync now passes InboundScriptResult.ErrorDetail through to the
synthesized inbound audit row's errorDetail, so a SCRIPT_ERROR run's exception ToString() lands
on AuditDetails.ErrorDetail for the Flow Debugger's inbound node to read (never surfaced to the
HTTP caller). FlowDebugRunResult grows an additive LogsTruncated (default false, last positional
member) reporting the inline FlowTraceCollector's low-priority (ScriptLog) truncation separately
from Truncated (step-row fidelity); RunMethodAsync/RunScriptAsync thread collector.LogsTruncated
through on their two collector-backed results.
This commit is contained in:
Joseph Doherty
2026-09-09 12:23:38 -04:00
parent 8ba1a0e2f8
commit ba7f8ee7a9
3 changed files with 72 additions and 3 deletions
@@ -152,6 +152,30 @@ public class FlowDebugRunnerTests
Assert.Same(siteRun, result.Events[2]);
}
[Fact]
public async Task RunMethod_ScriptThrows_InboundRowCarriesErrorDetail()
{
// Step-logs D3: InboundScriptExecutor turns an unhandled script exception into a
// SCRIPT_ERROR InboundScriptResult whose ErrorDetail is the exception's own ToString() —
// never surfaced to the HTTP caller, but the Flow Debugger's inbound node reads it off
// the written audit row.
var method = new ApiMethod("M", "x") { Id = 21, TimeoutSeconds = 5 };
_apiRepo.GetApiMethodByIdAsync(21, Arg.Any<CancellationToken>()).Returns(method);
_executor.RegisterHandler("M", _ => throw new InvalidOperationException("boom"));
var result = await Runner().RunMethodAsync(_services.BuildServiceProvider(),
new FlowDebugRunMethodCommand(21, null), Deployer, CancellationToken.None);
Assert.False(result.Completed);
Assert.Equal("SCRIPT_ERROR", result.ErrorCode);
var inbound = Assert.Single(_central, e => e.AsRow().Kind == AuditKind.InboundRequest).AsRow();
Assert.NotNull(inbound.ErrorDetail);
Assert.StartsWith("System.InvalidOperationException: boom", inbound.ErrorDetail);
// The returned copy carries the same detail (redacted, same as every other field).
var returnedInbound = result.Events[0].AsRow();
Assert.StartsWith("System.InvalidOperationException: boom", returnedInbound.ErrorDetail);
}
[Fact]
public async Task RunMethod_UnknownMethod_IsNotFound()
{
@@ -389,6 +413,12 @@ public class FlowDebugRunnerTests
executionId: Guid.NewGuid(), target: "work") with
{ DetailsJson = detailsJson };
/// <summary>A <c>ScriptLog</c> row — the low-priority lane <see cref="FlowStepProjector.IsLogLine"/> recognizes.</summary>
private static AuditEvent LogRow() =>
ScadaBridgeAuditEventFactory.Create(
AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered,
executionId: Guid.NewGuid(), target: "work");
[Fact]
public async Task RunMethod_SinkEvents_AreRedacted_WhenARedactorIsRegistered()
{
@@ -454,6 +484,37 @@ public class FlowDebugRunnerTests
Assert.True(result.Truncated);
Assert.Equal(501, result.Events.Count); // inbound row + the 500 kept
Assert.False(result.LogsTruncated); // these are full-priority step rows, not log lines
}
[Fact]
public async Task RunMethod_NoLogRows_LogsTruncatedIsFalse()
{
WireSinkPushingMethod(33, [TraceRow("{}")]);
var result = await Runner().RunMethodAsync(_services.BuildServiceProvider(),
new FlowDebugRunMethodCommand(33, null), Deployer, CancellationToken.None);
Assert.True(result.Completed, result.Error);
Assert.False(result.LogsTruncated);
Assert.False(result.Truncated);
}
[Fact]
public async Task RunMethod_OverLowPriorityReserve_ReportsLogsTruncated_ButNotTruncated()
{
// FlowTraceCollector reserves the last LowPriorityReserveEvents (50) of DefaultMaxEvents
// (500) for full-priority step rows — a low-priority (ScriptLog) Add stops at 450 and
// sets LogsTruncated, leaving Truncated (the step-row fidelity signal) untouched.
WireSinkPushingMethod(34, Enumerable.Range(0, 451).Select(_ => LogRow()).ToList());
var result = await Runner().RunMethodAsync(_services.BuildServiceProvider(),
new FlowDebugRunMethodCommand(34, null), Deployer, CancellationToken.None);
Assert.True(result.Completed, result.Error);
Assert.True(result.LogsTruncated);
Assert.False(result.Truncated);
Assert.Equal(451, result.Events.Count); // inbound row + the 450 log rows kept
}
// --- Flow Debugger script entry (design 2026-09-08) ---