feat(audit): GetTraceEventsAsync returns the ScriptTrace chain for Replay

Flow Debugger (design 2026-09-05 §7.1): IAuditLogRepository.GetTraceEventsAsync walks any
entry id to its root and down the Edges/Chain CTE exactly as GetExecutionTreeAsync, then
returns the chain's ScriptTrace rows plus the ApiInbound.InboundRequest row via the canonical
row mapper, ordered (OccurredAtUtc, EventId). Test doubles implement the new member with a
NotSupported stub.

Claude-Session: https://claude.ai/code/session_01GSLkkHKz9JrAtmJjmQK3cP
This commit is contained in:
Joseph Doherty
2026-09-05 23:59:08 -04:00
parent 981deed0fd
commit 9300bc1a4f
9 changed files with 246 additions and 0 deletions
@@ -430,6 +430,9 @@ public class AuditLogIngestActorCombinedTelemetryTests : TestKit, IClassFixture<
/// </summary>
private sealed class BatchFailingAuditRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly IAuditLogRepository _inner;
public BatchFailingAuditRepo(IAuditLogRepository inner) { _inner = inner; }
@@ -382,6 +382,9 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
/// </summary>
private sealed class TokenRecordingRepository : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public CancellationToken? BatchToken { get; private set; }
public List<CancellationToken> RowTokens { get; } = new();
@@ -438,6 +441,9 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
/// </summary>
private sealed class ThrowingRepository : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly IAuditLogRepository _inner;
private readonly Guid _poisonId;
@@ -45,6 +45,9 @@ public class AuditLogPurgeActorTests : TestKit, IClassFixture<MsSqlMigrationFixt
/// </summary>
private sealed class RecordingRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public List<DateTime> ThresholdQueries { get; } = new();
public List<DateTime> SwitchedBoundaries { get; } = new();
public Func<DateTime, long> RowsPerBoundary { get; set; } = _ => 0L;
@@ -37,6 +37,9 @@ public class CentralAuditWriteFailuresTests : TestKit
/// </summary>
private sealed class ThrowingRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
throw new InvalidOperationException("simulated repo failure");
public Task<IReadOnlyList<AuditEvent>> QueryAsync(
@@ -68,6 +68,9 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
/// </summary>
private sealed class RecordingRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public List<AuditEvent> Inserted { get; } = new();
private readonly HashSet<Guid> _seen = new();
public int InsertCallCount { get; private set; }
@@ -121,6 +124,9 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
/// </summary>
private sealed class PoisonInsertRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
private readonly Guid _poisonId;
private readonly Exception _fault;
public List<AuditEvent> Inserted { get; } = new();
@@ -192,6 +198,9 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
/// </summary>
private sealed class AlwaysFailUnlessSyntheticRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public List<AuditEvent> Inserted { get; } = new();
private readonly HashSet<Guid> _seen = new();
@@ -603,6 +612,9 @@ public class SiteAuditReconciliationActorTests : TestKit, IClassFixture<MsSqlMig
/// <summary>Repository whose every insert throws, so the retry hold-back path is taken.</summary>
private sealed class AlwaysThrowingRepo : IAuditLogRepository
{
public Task<IReadOnlyList<AuditEvent>> GetTraceEventsAsync(Guid executionId, CancellationToken ct = default)
=> throw new NotSupportedException("GetTraceEventsAsync is not exercised by this test double.");
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default) =>
throw new InvalidOperationException("central insert failed");