fix(audit): trace/outbound fault continuations run on the default scheduler and central emitters log (arch-review F8)

Claude-Session: https://claude.ai/code/session_01NcMG6xXdsJbmD2wYAgkK29
This commit is contained in:
Joseph Doherty
2026-09-06 14:28:44 -04:00
parent b8752cc855
commit 262de38cee
6 changed files with 93 additions and 18 deletions
@@ -1,6 +1,7 @@
using System.Data.Common;
using Microsoft.Data.Sqlite;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using NSubstitute;
using ZB.MOM.WW.Audit;
@@ -58,6 +59,26 @@ public class InboundDatabaseHelperAuditTests
=> throw new NotImplementedException();
}
/// <summary>
/// Arch-review F8: a minimal <see cref="ILogger"/> double that records every
/// Warning-level message, mirroring the <c>CountingLogger&lt;T&gt;</c> pattern in
/// <c>InboundScriptExecutorTests</c>.
/// </summary>
private sealed class RecordingLogger : ILogger
{
public List<(LogLevel Level, string Message, Exception? Exception)> Entries { get; } = new();
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
public bool IsEnabled(LogLevel logLevel) => true;
public void Log<TState>(
LogLevel logLevel, EventId eventId, TState state, Exception? exception,
Func<TState, Exception?, string> formatter)
{
Entries.Add((logLevel, formatter(state, exception), exception));
}
}
private static SqliteGateway SeededGateway() => new();
private readonly List<AuditEvent> _written = new();
@@ -166,6 +187,32 @@ public class InboundDatabaseHelperAuditTests
Assert.Equal(1, n2);
}
[Fact]
public async Task AuditWriterFaulting_LogsWarning_AndCallStillSucceeds()
{
// Arch-review F8: the best-effort continuation used to be `static _ => { }` — a
// faulting write vanished with no trace anywhere. It now logs a Warning (and runs
// on TaskScheduler.Default rather than riding whatever context completed the
// audit-writer task), while the call itself still succeeds either way.
var faultedTask = Substitute.For<ICentralAuditWriter>();
faultedTask.WriteAsync(Arg.Any<AuditEvent>(), Arg.Any<CancellationToken>())
.Returns(Task.FromException(new InvalidOperationException("audit down")));
var logger = new RecordingLogger();
var helper = new InboundDatabaseHelper(
SeededGateway(), CancellationToken.None, TimeSpan.Zero,
auditWriter: faultedTask, nodeIdentity: _node,
executionId: _exec, parentExecutionId: _parent, methodName: "movein",
traceSink: null, logger: logger);
var n = await helper.ExecuteAsync("MesDb", "UPDATE Machine SET Code=Code", null);
Assert.Equal(1, n);
var entry = Assert.Single(logger.Entries);
Assert.Equal(LogLevel.Warning, entry.Level);
Assert.NotNull(entry.Exception);
Assert.Contains("MesDb", entry.Message);
}
[Fact]
public async Task TraceSink_ReceivesTheSameInstance_BeforeTheWriter()
{