Merge branch 'worktree-agent-adf34e265d2dcae96' into arch-review-remediation

This commit is contained in:
Joseph Doherty
2026-08-14 23:47:00 -04:00
30 changed files with 1042 additions and 126 deletions
@@ -322,6 +322,47 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
Assert.Equal(2, counter.Count);
}
/// <summary>
/// The per-row fallback must run on its OWN cancellation token, never the
/// batch's. Sharing it meant that when the batch failed BECAUSE the 20 s
/// ingest budget expired, every fallback insert was handed an
/// already-cancelled token: N instant failures, N counter bumps, zero rows
/// accepted — the fallback's entire purpose (land the good rows) defeated at
/// the exact moment it was needed.
/// </summary>
/// <remarks>
/// Pinned by token IDENTITY rather than by expiring a real budget: the budget
/// is a fixed 20 s and a test that waited for it would cost 20 s of wall clock
/// to assert something the token comparison establishes outright. Two
/// <see cref="CancellationToken"/>s are equal iff they come from the same
/// source, so "not equal" IS "a fresh CTS", and a fresh CTS cannot be
/// pre-cancelled by the batch.
/// </remarks>
[Fact]
public async Task Receive_WhenBatchFails_PerRowFallbackUsesAFreshToken()
{
var repository = new TokenRecordingRepository();
var actor = CreateActor(repository);
var events = Enumerable.Range(0, 3).Select(_ => NewEvent(NewSiteId())).ToList();
actor.Tell(new IngestAuditEventsCommand(events), TestActor);
var reply = ExpectMsg<IngestAuditEventsReply>(TimeSpan.FromSeconds(10));
// The fallback landed every row.
Assert.Equal(3, reply.AcceptedEventIds.Count);
Assert.NotNull(repository.BatchToken);
Assert.Equal(3, repository.RowTokens.Count);
Assert.All(repository.RowTokens, rowToken =>
{
Assert.NotEqual(repository.BatchToken!.Value, rowToken);
Assert.False(rowToken.IsCancellationRequested);
});
await Task.CompletedTask;
}
/// <summary>Counts how many times the guard's catch surfaced a write failure.</summary>
private sealed class CountingFailureCounter : ICentralAuditWriteFailureCounter
{
@@ -329,6 +370,62 @@ public class AuditLogIngestActorTests : TestKit, IClassFixture<MsSqlMigrationFix
public void Increment() => Count++;
}
/// <summary>
/// Fails the set-based insert (as an expired budget would) and records the
/// token handed to each write so the fallback's token can be compared with
/// the batch's.
/// </summary>
private sealed class TokenRecordingRepository : IAuditLogRepository
{
public CancellationToken? BatchToken { get; private set; }
public List<CancellationToken> RowTokens { get; } = new();
public Task<int> InsertManyIfNotExistsAsync(
IReadOnlyList<AuditEvent> events, TimeSpan? commandTimeout = null, CancellationToken ct = default)
{
BatchToken = ct;
throw new OperationCanceledException("simulated ingest-budget expiry", ct);
}
public Task InsertIfNotExistsAsync(AuditEvent evt, CancellationToken ct = default)
{
RowTokens.Add(ct);
return Task.CompletedTask;
}
public Task<IReadOnlyList<AuditEvent>> QueryAsync(
AuditLogQueryFilter filter, AuditLogPaging paging, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<long> SwitchOutPartitionAsync(
DateTime monthBoundary, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<long> PurgeChannelOlderThanAsync(
string channel, DateTime threshold, int batchSize, TimeSpan? commandTimeout = null, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<long> BackfillSourceNodeAsync(
string sentinel, DateTime before, int batchSize, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<IReadOnlyList<DateTime>> GetPartitionBoundariesOlderThanAsync(
DateTime threshold, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<ZB.MOM.WW.ScadaBridge.Commons.Types.AuditLogKpiSnapshot> GetKpiSnapshotAsync(
TimeSpan window, DateTime? nowUtc = null, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<IReadOnlyList<ExecutionTreeNode>> GetExecutionTreeAsync(
Guid executionId, CancellationToken ct = default) =>
throw new NotSupportedException();
public Task<IReadOnlyList<string>> GetDistinctSourceNodesAsync(CancellationToken ct = default) =>
throw new NotSupportedException();
}
/// <summary>
/// Tiny test double that delegates to a real repository but throws on a
/// specified EventId. Used to verify per-row failure isolation: one bad