Resolve all 46 low-severity architecture review findings

This commit is contained in:
Joseph Doherty
2026-09-09 22:56:36 -04:00
parent fef3e7675f
commit 9ee402bbcd
139 changed files with 2621 additions and 1082 deletions
@@ -60,6 +60,166 @@ public class FlowDebugRunnerTests
private static JsonElement Json(string s) => JsonDocument.Parse(s).RootElement.Clone();
[Theory]
[InlineData(0)]
[InlineData(-3600)]
public async Task RunValueChange_FindsLateCompletionBehindLogCursorDespiteSiteClockSkew(int skewSeconds)
{
var (_, _, puller) = WireValueChange();
var child = Guid.NewGuid();
var pollStarted = _clock;
var siteStarted = DateTime.UtcNow.AddSeconds(skewSeconds);
Guid root = default;
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
root = ci.Arg<RouteToSetAttributesRequest>().ParentExecutionId!.Value;
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
});
var logId = Guid.NewGuid();
var runId = Guid.NewGuid();
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(ci =>
{
var rows = new List<AuditEvent>
{
ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptLog,
AuditStatus.Delivered, eventId: logId, executionId: child, parentExecutionId: root,
occurredAtUtc: siteStarted.AddSeconds(1)),
};
// Runtime stamps its closing ScriptRun row with the START time, after live log rows were already polled.
if (_clock - pollStarted >= TimeSpan.FromSeconds(2))
rows.Add(ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun,
AuditStatus.Delivered, eventId: runId, executionId: child, parentExecutionId: root,
occurredAtUtc: siteStarted));
var since = ci.Arg<DateTime>();
var after = ci.ArgAt<string?>(5);
var ids = ci.Arg<IReadOnlyCollection<Guid>>();
return (IReadOnlyList<AuditEvent>)rows.Where(e => (ids.Contains(root) || ids.Contains(child))
&& (e.OccurredAtUtc.UtcDateTime > since || e.OccurredAtUtc.UtcDateTime == since
&& (after is null || string.CompareOrdinal(e.EventId.ToString("D"), after) > 0)))
.OrderBy(e => e.OccurredAtUtc).ThenBy(e => e.EventId.ToString("D"), StringComparer.Ordinal).ToArray();
});
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 5), Deployer, CancellationToken.None);
Assert.True(result.Completed);
Assert.Contains(result.Events, e => e.EventId == runId);
Assert.Contains(result.Events, e => e.EventId == logId);
Assert.Equal(3, result.Events.Count);
}
[Fact]
public async Task RunValueChange_PagesTiesAndQueriesOlderDescendantsBeyond64Ids()
{
var (_, _, puller) = WireValueChange();
var children = Enumerable.Range(0, 70).Select(_ => Guid.NewGuid()).ToArray();
var grandchildren = children.ToDictionary(id => id, _ => Guid.NewGuid());
var rows = new List<AuditEvent>();
Guid root = default;
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
.Returns(ci =>
{
root = ci.Arg<RouteToSetAttributesRequest>().ParentExecutionId!.Value;
var t = DateTime.UtcNow;
rows.AddRange(Enumerable.Range(1, 2001).Select(n => ScadaBridgeAuditEventFactory.Create(
AuditChannel.ScriptTrace, AuditKind.ScriptLog, AuditStatus.Delivered,
eventId: Guid.Parse($"00000000-0000-0000-0000-{n:D12}"), occurredAtUtc: t, executionId: root)));
rows.AddRange(children.Select((id, n) => ScadaBridgeAuditEventFactory.Create(
AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
eventId: Guid.Parse($"ffffffff-0000-0000-0000-{n:D12}"), occurredAtUtc: t,
executionId: id, parentExecutionId: root)));
// These are older than the root's page cursor and only visible once child IDs are queried.
rows.AddRange(children.Select(id => ScadaBridgeAuditEventFactory.Create(
AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered,
occurredAtUtc: t.AddSeconds(-1), executionId: grandchildren[id], parentExecutionId: id)));
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
});
var queried = new HashSet<Guid>();
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(ci =>
{
var ids = ci.Arg<IReadOnlyCollection<Guid>>();
Assert.InRange(ids.Count, 1, 64);
queried.UnionWith(ids);
var since = ci.Arg<DateTime>();
var after = ci.ArgAt<string?>(5);
return (IReadOnlyList<AuditEvent>)rows.Where(e =>
{
var d = AuditDetailsCodec.Deserialize(e.DetailsJson);
return (ids.Contains(d.ExecutionId ?? Guid.Empty) || ids.Contains(d.ParentExecutionId ?? Guid.Empty))
&& (e.OccurredAtUtc.UtcDateTime > since || e.OccurredAtUtc.UtcDateTime == since
&& (after is null || string.CompareOrdinal(e.EventId.ToString("D"), after) > 0));
}).OrderBy(e => e.OccurredAtUtc).ThenBy(e => e.EventId.ToString("D"), StringComparer.Ordinal).Take(ci.Arg<int>()).ToArray();
});
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 4), Deployer, CancellationToken.None);
Assert.True(result.Completed);
Assert.True(result.LogsTruncated);
Assert.All(children, id => Assert.Contains(id, queried));
Assert.All(grandchildren.Values, id => Assert.Contains(id, queried));
}
[Fact]
public async Task Replay_UsesChainAnchorEvenWhenChildClockPrecedesRoot()
{
var repo = Substitute.For<IAuditLogRepository>();
var root = Guid.NewGuid();
var child = Guid.NewGuid();
var childRow = ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun,
AuditStatus.Delivered, executionId: child, parentExecutionId: root);
repo.GetTraceEventsAsync(child, Arg.Any<CancellationToken>()).Returns(new TraceEventBatch(root, [childRow], false));
_services.AddSingleton(repo);
var result = await Runner().ReplayAsync(_services.BuildServiceProvider(), new FlowDebugReplayCommand(child), CancellationToken.None, Deployer);
Assert.Equal(root, result.RootExecutionId);
}
[Fact]
public async Task RunValueChange_TotalDeadlineIncludesInitialWrite()
{
var (_, _, puller) = WireValueChange();
_services.Configure<ManagementServiceOptions>(o => o.LongRunningCommandTimeout = TimeSpan.FromSeconds(6));
_router.RouteToSetAttributesAsync("site-a", Arg.Any<RouteToSetAttributesRequest>(), Arg.Any<CancellationToken>())
.Returns(async ci =>
{
await Task.Delay(Timeout.InfiniteTimeSpan, ci.Arg<CancellationToken>());
return new RouteToSetAttributesResponse("x", true, null, DateTimeOffset.UtcNow);
});
var task = Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 300), Deployer, CancellationToken.None);
var ex = await Assert.ThrowsAsync<ManagementCommandException>(() => task.WaitAsync(TimeSpan.FromSeconds(3)));
Assert.Equal(FlowDebugRunner.TimeoutCode, ex.Code);
Assert.Empty(puller.ReceivedCalls());
}
[Fact]
public async Task RunValueChange_TotalDeadlineIncludesAuditWrite()
{
var (_, _, puller) = WireValueChange();
_services.Configure<ManagementServiceOptions>(o => o.LongRunningCommandTimeout = TimeSpan.FromSeconds(6));
var writer = Substitute.For<ICentralAuditWriter>();
writer.WriteAsync(Arg.Any<AuditEvent>(), Arg.Any<CancellationToken>())
.Returns(ci => Task.Delay(Timeout.InfiniteTimeSpan, ci.Arg<CancellationToken>()));
_services.AddSingleton(writer);
var ex = await Assert.ThrowsAsync<ManagementCommandException>(() => Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 300), Deployer, CancellationToken.None)
.WaitAsync(TimeSpan.FromSeconds(3)));
Assert.Equal(FlowDebugRunner.TimeoutCode, ex.Code);
Assert.Empty(puller.ReceivedCalls());
}
[Fact]
public async Task RunValueChange_CapsReturnedEventsAndReportsTruncation()
{
var (_, _, puller) = WireValueChange();
var events = Enumerable.Range(0, 700).Select(_ => ScadaBridgeAuditEventFactory.Create(
AuditChannel.ScriptTrace, AuditKind.AttributeWrite, AuditStatus.Delivered)).ToArray();
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(events);
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 1), Deployer, CancellationToken.None);
Assert.True(result.Truncated);
Assert.InRange(result.Events.Count, 1, FlowTraceCollector.DefaultMaxEvents);
}
[Fact]
public async Task RunMethod_OnStandby_RefusesWithStandbyCode_BeforeTouchingTheMethod()
{
@@ -207,7 +367,7 @@ public class FlowDebugRunnerTests
_router.RouteToSetAttributesAsync("site-a", Arg.Do<RouteToSetAttributesRequest>(r => write = r), Arg.Any<CancellationToken>())
.Returns(ci => new RouteToSetAttributesResponse(((RouteToSetAttributesRequest)ci[1]).CorrelationId, true, null, DateTimeOffset.UtcNow));
var calls = 0;
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(ci =>
{
calls++;
@@ -240,7 +400,7 @@ public class FlowDebugRunnerTests
public async Task RunValueChange_NoSpawnWithinWindow_ReportsNoTriggerCode()
{
var (_, _, puller) = WireValueChange();
puller.PullAsync(default!, default!, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
new FlowDebugRunValueChangeCommand(5, "Status", "Running", WaitSeconds: 3), Deployer, CancellationToken.None);
@@ -260,7 +420,7 @@ public class FlowDebugRunnerTests
// the site's own ceiling (SiteStreamGrpcServer.MaxTraceEventsPerPull) instead.
var (_, _, puller) = WireValueChange();
var maxima = new List<int>();
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Do<int>(maxima.Add), Arg.Any<CancellationToken>())
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Do<int>(maxima.Add), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(Array.Empty<AuditEvent>());
await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
@@ -476,7 +636,7 @@ public class FlowDebugRunnerTests
public async Task RunValueChange_SynthesizedRootRow_IsRedacted_WhenARedactorIsRegistered()
{
var (_, _, puller) = WireValueChange();
puller.PullAsync(default!, default!, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
puller.PullAsync(default!, default!, default, default, default, default).ReturnsForAnyArgs(Array.Empty<AuditEvent>());
_services.AddSingleton<IAuditRedactor>(new UpperRedactor());
var result = await Runner().RunValueChangeAsync(_services.BuildServiceProvider(),
@@ -587,7 +747,7 @@ public class FlowDebugRunnerTests
// poll, then the SAME row (fixed event id) from the second on — a fresh id per poll would
// re-arm the settle window forever and the loop would only ever end on the deadline.
var calls = 0;
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(ci =>
{
calls++;
@@ -662,7 +822,7 @@ public class FlowDebugRunnerTests
// The write may fire several scripts. A sibling's run is still COLLECTED (the graph draws
// it) but must not report the script the operator chose as having run.
var (_, _, puller, _) = WireScript(WriteScript("OnLevel", "ValueChange", "Level"));
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>())
puller.PullAsync("site-a", Arg.Any<IReadOnlyCollection<Guid>>(), Arg.Any<DateTime>(), Arg.Any<int>(), Arg.Any<CancellationToken>(), Arg.Any<string?>())
.Returns(ci => (IReadOnlyList<AuditEvent>)[ScadaBridgeAuditEventFactory.Create(AuditChannel.ScriptTrace, AuditKind.ScriptRun, AuditStatus.Delivered,
eventId: Guid.Parse("cccccccc-0000-0000-0000-000000000001"),
executionId: Guid.NewGuid(), parentExecutionId: ((IReadOnlyCollection<Guid>)ci[1]).First(), target: "OtherScript")]);