fix(site-runtime): step-log review — trace-flag-gated ES step scope, shared-script Log, formatter guard
Task 5 code-review follow-ups: - I1: ExternalSystemHelper gates its TraceStepScope on a new isTraced ctor arg (default false for direct construction), not on `_traceSink is not null` — a traced run without an inline collector still writes durable ScriptLog rows that must name their step. - I2 (design §5): an inline shared script now SHARES its caller's ScriptLogger via the new `sharedLog` ctor param — one line cap, one anchor, one closing row. Consequence documented on the Log property: a shared script's lines carry the PARENT run's ExecutionId and land on the parent's ScriptRun node. - M3: TraceCapturingLogger swallows a throwing formatter — most of what it captures is best-effort "audit write failed" warnings logged from inside a catch block, and a formatting exception must not escape that path. - Tests: ApiCall step attribution, traced-without-collector, shared-script logger sharing, nested batch/wait scope attribution + unwind, throwing formatter; test (c) now also pins the anti-recursion property (EmitLog must log through the undecorated logger). CompileSurfaceParityTests stays red until Task 7 mirrors Log on ScriptCompileSurface.
This commit is contained in:
+125
@@ -621,6 +621,12 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
var collector = new FlowTraceCollector();
|
||||
// The throwing writer makes EmitTrace log its best-effort "write failed" warning while the
|
||||
// AttributeWrite step scope is open — exactly the runtime line D6 exists to attribute.
|
||||
//
|
||||
// It also PINS THE ANTI-RECURSION PROPERTY: EmitLog's own write fails against this writer
|
||||
// too, so its "log-line write failed" warning must go to the UNDECORATED logger. Routed
|
||||
// through the capturing decorator instead, each warning would emit another row whose write
|
||||
// fails again — an unbounded loop, and this test would hang or blow the row count rather
|
||||
// than see exactly one ScriptLog row.
|
||||
var ctx = CreateContext(probe.Ref, new ThrowingAuditWriter(), traced: true, collector);
|
||||
|
||||
var call = ctx.SetAttribute("Setpoint", "42");
|
||||
@@ -697,4 +703,123 @@ public class ScriptRuntimeContextTraceEmissionTests : TestKit
|
||||
Assert.Equal("runtime", extra.LogSource);
|
||||
Assert.Contains("1 line(s) dropped", closing.RequestSummary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review I1: the sync <c>ExternalSystem.Call</c> pre-mints its <c>ApiCall</c> row id and
|
||||
/// publishes it as the ambient step, so a warning logged while the call is in flight names
|
||||
/// that node. Provoked the same way as the write case — a writer that throws makes the
|
||||
/// helper's own best-effort "audit write threw" warning fire inside the scope.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task RuntimeWarning_InsideATracedExternalCall_IsAttributedToTheApiCallStep()
|
||||
{
|
||||
var collector = new FlowTraceCollector();
|
||||
var ctx = CreateContext(CreateTestProbe().Ref, new ThrowingAuditWriter(), traced: true, collector,
|
||||
externalSystemClient: SyncClient(new ExternalCallResult(true, "{}", null)).Object);
|
||||
|
||||
var result = await ctx.ExternalSystem.Call("ERP", "GetOrder");
|
||||
|
||||
Assert.True(result.Success);
|
||||
var rows = collector.Snapshot();
|
||||
var call = rows.Single(e => Row(e).Kind == AuditKind.ApiCall);
|
||||
var log = rows.Single(e => Row(e).Kind == AuditKind.ScriptLog);
|
||||
var extra = ScriptTraceExtra.TryParse(Row(log).Extra)!;
|
||||
Assert.Equal("runtime", extra.LogSource);
|
||||
Assert.Equal(call.EventId, extra.StepEventId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review I1: the step scope is gated on the run's TRACE FLAG, not on the presence of an
|
||||
/// inline collector — a traced run without one still writes durable <c>ScriptLog</c> rows that
|
||||
/// have to name their step.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task TracedExternalCall_WithoutACollector_StillNamesTheApiCallStep()
|
||||
{
|
||||
var writer = new CapturingAuditWriter();
|
||||
var ctx = CreateContext(CreateTestProbe().Ref, writer, traced: true, collector: null,
|
||||
externalSystemClient: SyncClient(new ExternalCallResult(true, "{}", null)).Object);
|
||||
|
||||
await ctx.ExternalSystem.Call("ERP", "GetOrder");
|
||||
ctx.Log.Info("after the call, outside any scope");
|
||||
|
||||
var call = writer.Events.Single(e => Row(e).Kind == AuditKind.ApiCall);
|
||||
// The line above is outside the scope, so it falls back to the anchor; what this pins is
|
||||
// that the call's row id was minted and published at all on a collector-less traced run.
|
||||
Assert.NotEqual(Guid.Empty, call.EventId);
|
||||
var log = writer.Events.Single(e => Row(e).Kind == AuditKind.ScriptLog);
|
||||
Assert.Null(ScriptTraceExtra.TryParse(Row(log).Extra)!.StepEventId);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review I2 (design §5): an inline shared script SHARES its caller's <see cref="ScriptLogger"/>
|
||||
/// — one counter, one anchor. Its lines therefore carry the PARENT run's <c>ExecutionId</c> and
|
||||
/// land on the parent's <c>ScriptRun</c> node, even though its own step rows chain under its own
|
||||
/// execution.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void SharedScriptChild_SharesTheCallersLogger_ExecutionIdAndCap()
|
||||
{
|
||||
var writer = new CapturingAuditWriter();
|
||||
var collector = new FlowTraceCollector();
|
||||
var parent = CreateContext(CreateTestProbe().Ref, writer, traced: true, collector, maxLogLinesPerRun: 2);
|
||||
var child = parent.CreateChildContextForSharedScript(1);
|
||||
|
||||
Assert.Same(parent.Log, child.Log);
|
||||
Assert.NotEqual(parent.ExecutionId, child.ExecutionId);
|
||||
|
||||
parent.Log.Info("from the caller");
|
||||
child.Log.Info("shared");
|
||||
// Third line from either side is over the SHARED cap of 2.
|
||||
child.Log.Info("dropped");
|
||||
parent.Log.Info("also dropped");
|
||||
|
||||
var rows = collector.Snapshot();
|
||||
Assert.Equal(2, rows.Count);
|
||||
Assert.All(rows, e => Assert.Equal(parent.ExecutionId, Row(e).ExecutionId));
|
||||
Assert.Equal(["from the caller", "shared"], rows.Select(e => Row(e).RequestSummary));
|
||||
|
||||
parent.Log.FlushDropped();
|
||||
Assert.Contains("2 line(s) dropped", Row(collector.Snapshot()[^1]).RequestSummary);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Review item 4: step scopes NEST — the batch leg's lines name the batch row, the wait leg's
|
||||
/// name the wait row, and both unwind so a later line falls back to the run anchor. The
|
||||
/// throwing writer is again what provokes a runtime line inside each leg.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task NestedStepScopes_AttributeEachLegSeparately_AndUnwind()
|
||||
{
|
||||
var probe = CreateTestProbe();
|
||||
var collector = new FlowTraceCollector();
|
||||
var ctx = CreateContext(probe.Ref, new ThrowingAuditWriter(), traced: true, collector);
|
||||
|
||||
var call = ctx.WriteBatchAndWait(
|
||||
new Dictionary<string, string?> { ["Recipe"] = "R-17" },
|
||||
"Trigger", "1", "Ack", "1", TimeSpan.FromSeconds(3));
|
||||
|
||||
var batchReq = probe.ExpectMsg<WriteAttributeBatchRequest>(TimeSpan.FromSeconds(5));
|
||||
probe.Reply(new WriteAttributeBatchResponse(batchReq.CorrelationId, true, null));
|
||||
var waitReq = probe.ExpectMsg<WaitForAttributeRequest>(TimeSpan.FromSeconds(5));
|
||||
probe.Reply(new WaitForAttributeResponse(waitReq.CorrelationId, Matched: true, "1", "Good", TimedOut: false));
|
||||
Assert.True(await call);
|
||||
|
||||
// Both scopes have unwound by the time the run continues.
|
||||
ctx.Log.Info("after both legs");
|
||||
|
||||
var rows = collector.Snapshot();
|
||||
var write = rows.Single(e => Row(e).Kind == AuditKind.AttributeWrite);
|
||||
var wait = rows.Single(e => Row(e).Kind == AuditKind.AttributeWait);
|
||||
Assert.NotEqual(write.EventId, wait.EventId);
|
||||
|
||||
var logs = rows.Where(e => Row(e).Kind == AuditKind.ScriptLog)
|
||||
.Select(e => ScriptTraceExtra.TryParse(Row(e).Extra)!)
|
||||
.ToList();
|
||||
Assert.Equal(3, logs.Count);
|
||||
Assert.Equal(write.EventId, logs[0].StepEventId);
|
||||
Assert.Equal(wait.EventId, logs[1].StepEventId);
|
||||
Assert.Null(logs[2].StepEventId);
|
||||
Assert.Equal(["runtime", "runtime", "script"], logs.Select(l => l.LogSource));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user