feat(worker): route Serilog Warn/Error to footer + buffer recent logs for overlay
This commit is contained in:
107
tests/ClaudeDo.Worker.Tests/Logging/BroadcastLogSinkTests.cs
Normal file
107
tests/ClaudeDo.Worker.Tests/Logging/BroadcastLogSinkTests.cs
Normal file
@@ -0,0 +1,107 @@
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Worker.Logging;
|
||||
using Serilog.Events;
|
||||
using Serilog.Parsing;
|
||||
|
||||
namespace ClaudeDo.Worker.Tests.Logging;
|
||||
|
||||
public class BroadcastLogSinkTests
|
||||
{
|
||||
private static readonly DateTimeOffset EvtTime = new(2026, 6, 23, 8, 0, 0, TimeSpan.Zero);
|
||||
private static readonly MessageTemplateParser Parser = new();
|
||||
|
||||
private static LogEvent Evt(LogEventLevel level, string template, Exception? ex = null, string? sourceContext = null)
|
||||
{
|
||||
var props = new List<LogEventProperty>();
|
||||
if (sourceContext is not null)
|
||||
props.Add(new LogEventProperty("SourceContext", new ScalarValue(sourceContext)));
|
||||
return new LogEvent(EvtTime, level, ex, Parser.Parse(template), props);
|
||||
}
|
||||
|
||||
private static (BroadcastLogSink sink, LogRingBuffer buffer, List<(string msg, WorkerLogLevel lvl)> calls)
|
||||
NewSink(Func<DateTime> clock)
|
||||
{
|
||||
var buffer = new LogRingBuffer(TimeSpan.FromHours(1), utcNow: () => new DateTime(2026, 6, 23, 8, 0, 0, DateTimeKind.Utc));
|
||||
var sink = new BroadcastLogSink(buffer, clock);
|
||||
var calls = new List<(string, WorkerLogLevel)>();
|
||||
sink.Attach((m, l, _) => { calls.Add((m, l)); return Task.CompletedTask; });
|
||||
return (sink, buffer, calls);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Buffers_all_levels()
|
||||
{
|
||||
var (sink, buffer, _) = NewSink(() => EvtTime.UtcDateTime);
|
||||
sink.Emit(Evt(LogEventLevel.Information, "info"));
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "warn"));
|
||||
sink.Emit(Evt(LogEventLevel.Error, "err"));
|
||||
|
||||
Assert.Equal(3, buffer.Snapshot().Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Broadcasts_only_warn_and_error()
|
||||
{
|
||||
var (sink, _, calls) = NewSink(() => EvtTime.UtcDateTime);
|
||||
sink.Emit(Evt(LogEventLevel.Information, "info"));
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "warn"));
|
||||
sink.Emit(Evt(LogEventLevel.Error, "err"));
|
||||
|
||||
Assert.Equal(new[] { WorkerLogLevel.Warn, WorkerLogLevel.Error }, calls.Select(c => c.lvl));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Dedupes_repeat_within_window_but_still_buffers()
|
||||
{
|
||||
var now = EvtTime.UtcDateTime;
|
||||
var (sink, buffer, calls) = NewSink(() => now);
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "same"));
|
||||
now = now.AddSeconds(30);
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "same"));
|
||||
|
||||
Assert.Single(calls); // second broadcast suppressed
|
||||
Assert.Equal(2, buffer.Snapshot().Count); // both buffered
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Allows_repeat_after_window()
|
||||
{
|
||||
var now = EvtTime.UtcDateTime;
|
||||
var (sink, _, calls) = NewSink(() => now);
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "same"));
|
||||
now = now.AddSeconds(121);
|
||||
sink.Emit(Evt(LogEventLevel.Warning, "same"));
|
||||
|
||||
Assert.Equal(2, calls.Count);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Appends_exception_to_message()
|
||||
{
|
||||
var (sink, _, calls) = NewSink(() => EvtTime.UtcDateTime);
|
||||
sink.Emit(Evt(LogEventLevel.Error, "boom", new InvalidOperationException("bad dir")));
|
||||
|
||||
Assert.Single(calls);
|
||||
Assert.Equal("boom: InvalidOperationException: bad dir", calls[0].msg);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Plumbing_source_buffered_but_not_broadcast()
|
||||
{
|
||||
var (sink, buffer, calls) = NewSink(() => EvtTime.UtcDateTime);
|
||||
sink.Emit(Evt(LogEventLevel.Error, "transport hiccup", sourceContext: "Microsoft.AspNetCore.SignalR.HubConnectionHandler"));
|
||||
|
||||
Assert.Empty(calls);
|
||||
Assert.Single(buffer.Snapshot());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Does_not_throw_when_detached()
|
||||
{
|
||||
var buffer = new LogRingBuffer(TimeSpan.FromHours(1));
|
||||
var sink = new BroadcastLogSink(buffer);
|
||||
sink.Emit(Evt(LogEventLevel.Error, "no subscriber"));
|
||||
|
||||
Assert.Single(buffer.Snapshot());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user