fix(site-runtime): trace sink is guarded — a throwing observer can neither fail a call nor skip the audit write
Review of Task 3: the sink invoke sat before WriteAsync/ForwardAsync inside the best-effort try, so a throwing sink would have silently dropped the durable row. GuardTraceSink wraps the delegate in every helper ctor (and Collect guards EmitTrace); pinned by a throwing-sink test. ScriptRunLauncher no longer allocates a collector for a traced run with no reply target (Nobody) — the list would never be read. FlowTraceCollector.Complete pinned at unit level; Snapshot documented as a non-closing peek. Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
@@ -64,4 +64,34 @@ public class FlowTraceCollectorTests
|
||||
Assert.Equal(500, FlowTraceCollector.DefaultMaxEvents);
|
||||
Assert.Throws<ArgumentOutOfRangeException>(() => new FlowTraceCollector(0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Complete_ClosesTheCollector_LaterAddsAreIgnored_AndNeverTruncate()
|
||||
{
|
||||
var c = new FlowTraceCollector(maxEvents: 2);
|
||||
var first = NewEvent();
|
||||
c.Add(first);
|
||||
Assert.False(c.Completed);
|
||||
|
||||
var snapshot = c.Complete();
|
||||
|
||||
Assert.True(c.Completed);
|
||||
Assert.Same(first, Assert.Single(snapshot));
|
||||
c.Add(NewEvent());
|
||||
c.Add(NewEvent());
|
||||
c.Add(NewEvent());
|
||||
Assert.Equal(1, c.Count);
|
||||
Assert.False(c.Truncated);
|
||||
Assert.Equal(snapshot, c.Complete()); // idempotent
|
||||
}
|
||||
|
||||
private static AuditEvent NewEvent() => new()
|
||||
{
|
||||
EventId = Guid.NewGuid(),
|
||||
OccurredAtUtc = DateTimeOffset.UtcNow,
|
||||
Actor = "t",
|
||||
Action = "ScriptTrace.ScriptRun",
|
||||
Outcome = AuditOutcome.Success,
|
||||
DetailsJson = "{}",
|
||||
};
|
||||
}
|
||||
|
||||
+16
@@ -507,4 +507,20 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
writer.Events.Select(e => Row(e).Kind));
|
||||
Assert.Equal(AuditKind.CachedSubmit, Row(Assert.Single(forwarder.Telemetry).Audit).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ThrowingTraceSink_NeverFailsTheCall_AndTheWriterStillGetsTheRow()
|
||||
{
|
||||
var writer = new CapturingAuditWriter();
|
||||
var helper = new ScriptRuntimeContext.ExternalSystemHelper(
|
||||
SyncClient(new ExternalCallResult(true, "{}", null)).Object, InstanceName, NullLogger.Instance, Guid.NewGuid(),
|
||||
auditWriter: writer, siteId: "site-a", sourceScript: "S",
|
||||
traceSink: _ => throw new InvalidOperationException("observer broke"));
|
||||
|
||||
var result = await helper.Call("ERP", "GetOrder");
|
||||
|
||||
Assert.True(result.Success);
|
||||
var written = Assert.Single(writer.Events);
|
||||
Assert.Equal(AuditKind.ApiCall, Row(written).Kind);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user