feat(flow-debugger): carry site-side trace truncation on the route reply (ScadaBridge#54)

ScriptCallResult and RouteToCallResponse gain trailing Truncated/LogsTruncated,
filled from the site FlowTraceCollector after Complete(); RouteToCallResponseDto
fields 8/9 carry them (sitecommand regen). FlowDebugRunner ORs them into
FlowDebugRunResult on the script entry, and on the method entry through a new
optional truncation sink on RouteHelper.WithTraceSink (carried through every
With* clone). Previously a site run whose collector dropped rows came back as a
silently short list. Older sites send false.

Claude-Session: https://claude.ai/code/session_01GT67AEHd2Acv1YPwGfXEoV
This commit is contained in:
Joseph Doherty
2026-11-09 12:37:57 -05:00
parent 5310fd0e26
commit 6d5bb6189f
15 changed files with 484 additions and 155 deletions
@@ -46,9 +46,9 @@ public class DeploymentManagerRouteTraceTests : TestKit, IDisposable
TestLocalDb.DeleteFiles(path);
}
private IActorRef CreateDeploymentManager() =>
private IActorRef CreateDeploymentManager(SiteRuntimeOptions? options = null) =>
ActorOf(Props.Create(() => new DeploymentManagerActor(
_storage, _compilationService, _sharedScriptLibrary, null, new SiteRuntimeOptions(),
_storage, _compilationService, _sharedScriptLibrary, null, options ?? new SiteRuntimeOptions(),
NullLogger<DeploymentManagerActor>.Instance,
dclManager: null, healthCollector: null, serviceProvider: null, loggerFactory: null, configFetcher: null)));
@@ -56,12 +56,17 @@ public class DeploymentManagerRouteTraceTests : TestKit, IDisposable
{
InstanceUniqueName = instance,
Attributes = [new ResolvedAttribute { CanonicalName = "Mode", Value = "Auto", DataType = "String" }],
Scripts = [new ResolvedScript { CanonicalName = "DoWork", Code = "await Instance.SetAttribute(\"Mode\", \"Manual\"); return 7;" }],
Scripts =
[
new ResolvedScript { CanonicalName = "DoWork", Code = "await Instance.SetAttribute(\"Mode\", \"Manual\"); return 7;" },
// Gitea #54: more log lines than the inline collector's low-priority share (450).
new ResolvedScript { CanonicalName = "Chatty", Code = "for (var i = 0; i < 600; i++) Log.Info(\"line \" + i); return 1;" },
],
});
private async Task<IActorRef> DeployedManager(string instance)
private async Task<IActorRef> DeployedManager(string instance, SiteRuntimeOptions? options = null)
{
var actor = CreateDeploymentManager();
var actor = CreateDeploymentManager(options);
await Task.Delay(500);
actor.Tell(new DeployInstanceCommand("dep", instance, "sha256:x", ConfigJson(instance), "admin", DateTimeOffset.UtcNow));
ExpectMsg<DeploymentStatusResponse>(TimeSpan.FromSeconds(5));
@@ -96,6 +101,30 @@ public class DeploymentManagerRouteTraceTests : TestKit, IDisposable
Assert.All(rows, r => Assert.Equal(response.ChildExecutionId, r.ExecutionId));
}
/// <summary>
/// Gitea #54: a traced routed run whose inline collector dropped log rows carries
/// <c>LogsTruncated</c> on the <see cref="RouteToCallResponse"/>, so central can label the short
/// site lane; a run that fit reports neither flag.
/// </summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
public async Task RouteCall_WithTrace_CarriesTheSiteCollectorsTruncationFlags()
{
var actor = await DeployedManager("ChattyPump", new SiteRuntimeOptions { MaxLogLinesPerRun = 1000 });
actor.Tell(new RouteToCallRequest("rc-t", "ChattyPump", "Chatty", null, DateTimeOffset.UtcNow, Trace: true));
var chatty = ExpectMsg<RouteToCallResponse>(TimeSpan.FromSeconds(10));
Assert.True(chatty.Success, chatty.ErrorMessage);
Assert.True(chatty.LogsTruncated);
Assert.False(chatty.Truncated);
actor.Tell(new RouteToCallRequest("rc-q", "ChattyPump", "DoWork", null, DateTimeOffset.UtcNow, Trace: true));
var quiet = ExpectMsg<RouteToCallResponse>(TimeSpan.FromSeconds(10));
Assert.True(quiet.Success, quiet.ErrorMessage);
Assert.False(quiet.LogsTruncated);
Assert.False(quiet.Truncated);
}
/// <summary>Verifies that an untraced route call returns the execution id but no trace events.</summary>
/// <returns>A task that represents the asynchronous operation.</returns>
[Fact]
@@ -161,4 +161,38 @@ public class ScriptRunLauncherTraceTests : TestKit, IDisposable
Assert.All(logs, e => Assert.True(rows.ToList().IndexOf(e) < anchorIndex));
Assert.Equal(logs.Count, anchorIndex);
}
// ── (d) the reply reports the site collector's truncation (Gitea #54) ────────
/// <summary>
/// Verifies that a traced run whose inline collector dropped log-line rows says so on the
/// reply (<c>LogsTruncated</c>), instead of handing the caller a silently short list.
/// </summary>
[Fact]
public void TracedRun_WhoseCollectorDroppedLogRows_ReportsLogsTruncated()
{
// 600 lines is under the per-run line cap set here (1000) but over the collector's
// low-priority event share (DefaultMaxEvents - LowPriorityReserveEvents = 450).
var result = RunTraced(
"for (var i = 0; i < 600; i++) Log.Info(\"line \" + i); return null;",
Options(maxLogLines: 1000));
Assert.True(result.Success);
Assert.True(result.LogsTruncated);
Assert.False(result.Truncated);
Assert.True(result.TraceEvents!.Count < 600);
// Step rows still fit: the ScriptRun anchor is present.
ScriptRunRow(result);
}
/// <summary>Positive control: a small traced run reports neither truncation flag.</summary>
[Fact]
public void TracedRun_WithinTheCollectorCaps_ReportsNoTruncation()
{
var result = RunTraced("Log.Info(\"one\"); return null;", Options());
Assert.True(result.Success);
Assert.False(result.LogsTruncated);
Assert.False(result.Truncated);
}
}