fix(site-runtime): TraceCapturingLogger rejects double-wrapping; ScriptLogger clip keeps surrogate pairs (Task 4 review)

This commit is contained in:
Joseph Doherty
2026-09-09 11:42:40 -04:00
parent 14fb8c889b
commit aaec993b84
4 changed files with 125 additions and 11 deletions
@@ -112,6 +112,32 @@ public class ScriptLoggerTests
Assert.Null(rows[1].StepEventId);
}
[Fact]
public void NestedStepScopes_Unwind_ToTheEnclosingStep()
{
// A helper calling another helper: the inner step owns its own lines, and the outer step
// gets its attribution back the moment the inner scope closes.
var (logger, _, rows) = MakeTraced();
var outer = Guid.NewGuid();
var inner = Guid.NewGuid();
using (TraceStepScope.Enter(outer))
{
using (TraceStepScope.Enter(inner))
{
Assert.Equal(inner, TraceStepScope.Current);
logger.Info("inner");
}
Assert.Equal(outer, TraceStepScope.Current);
logger.Info("outer");
}
Assert.Null(TraceStepScope.Current);
Assert.Equal(inner, rows[0].StepEventId);
Assert.Equal(outer, rows[1].StepEventId);
}
[Fact]
public void PastThePerRunCap_LinesAreDropped_AndFlushDroppedNamesTheCount_Once()
{
@@ -164,6 +190,33 @@ public class ScriptLoggerTests
Assert.EndsWith("…", message);
}
[Fact]
public void AMessageExactlyAtTheCap_PassesThroughUnclipped()
{
var (logger, _, rows) = MakeTraced();
var exact = new string('x', ScriptLogger.MaxMessageChars);
logger.Info(exact);
Assert.Equal(exact, Assert.Single(rows).Message);
}
[Fact]
public void ASurrogatePairStraddlingTheCap_IsDroppedWhole()
{
// Cutting between the halves would leave a lone surrogate, which breaks JSON serialization
// of the audit row further down the pipe.
var (logger, _, rows) = MakeTraced();
var straddling = new string('x', ScriptLogger.MaxMessageChars - 1) + "🚀" + new string('y', 10);
logger.Info(straddling);
var message = Assert.Single(rows).Message;
Assert.Equal(ScriptLogger.MaxMessageChars, message.Length);
Assert.EndsWith("…", message);
Assert.DoesNotContain(message, static c => char.IsSurrogate(c));
}
[Fact]
public void AThrowingEmitter_NeverReachesTheScript_AndTheNodeLoggerStillGotTheLine()
{
@@ -135,6 +135,27 @@ public class TraceCapturingLoggerTests
Assert.Equal(stepId, Assert.Single(rows).StepEventId);
}
[Fact]
public void Ctor_RejectsAnAlreadyDecoratedLogger()
{
// Wrapping twice would forward each script line through two capture stages and emit two
// runtime rows per line — and would burn the run's line cap at double rate.
var (already, _, _) = Make();
var sink = new ScriptLogger(new RecordingLogger(), emit: null, maxLines: 200, "i", "s", null);
var ex = Assert.Throws<ArgumentException>(() => new TraceCapturingLogger(already, sink));
Assert.Equal("inner", ex.ParamName);
}
[Fact]
public void Ctor_RequiresBothCollaborators()
{
var sink = new ScriptLogger(new RecordingLogger(), emit: null, maxLines: 200, "i", "s", null);
Assert.Throws<ArgumentNullException>(() => new TraceCapturingLogger(null!, sink));
Assert.Throws<ArgumentNullException>(() => new TraceCapturingLogger(new RecordingLogger(), null!));
}
[Fact]
public void BeginScope_DelegatesToTheInnerLogger()
{