chore(data-layer): low-severity cleanups — SQLite param chunking, KPI metric catalog, computed-column predicate guard

This commit is contained in:
Joseph Doherty
2026-07-09 09:32:04 -04:00
parent b58d810dbe
commit acd8a6b8b5
6 changed files with 138 additions and 41 deletions
@@ -590,6 +590,44 @@ public class SqliteAuditWriterWriteTests
}
}
/// <summary>
/// P8 (arch-review 04): a drain batch larger than SQLite's bound-parameter ceiling
/// (999 on older builds) must still flip every row. The writer chunks the id list
/// into ≤500-id UPDATEs inside one transaction, so 1200 ids in a single
/// <see cref="SqliteAuditWriter.MarkForwardedAsync"/> call flip atomically — and
/// <see cref="SqliteAuditWriter.MarkReconciledAsync"/> chunks the same way.
/// </summary>
[Fact]
public async Task MarkForwardedThenReconciled_LargeBatch_ChunksParameters_AllFlip()
{
var (writer, dataSource) = CreateWriter(nameof(MarkForwardedThenReconciled_LargeBatch_ChunksParameters_AllFlip));
await using var _ = writer;
const int count = 1200; // > 999 and > the 500 chunk size, so multiple chunks fire
var ids = new List<Guid>(count);
for (int i = 0; i < count; i++)
{
var id = Guid.NewGuid();
ids.Add(id);
await writer.WriteAsync(NewEvent(id));
}
// One MarkForwardedAsync call over all 1200 ids — must not throw on the
// parameter ceiling and must flip every sidecar row to Forwarded.
await writer.MarkForwardedAsync(ids);
var afterForward = ForwardStateCounts(dataSource);
Assert.Equal(count, afterForward[AuditForwardState.Forwarded.ToString()]);
Assert.False(afterForward.ContainsKey(AuditForwardState.Pending.ToString()));
// Same for reconciliation over the full batch.
await writer.MarkReconciledAsync(ids);
var afterReconcile = ForwardStateCounts(dataSource);
Assert.Equal(count, afterReconcile[AuditForwardState.Reconciled.ToString()]);
Assert.False(afterReconcile.ContainsKey(AuditForwardState.Forwarded.ToString()));
}
// ----- ExecutionId (rides DetailsJson, recomposed via AsRow) ----- //
[Fact]
@@ -33,6 +33,20 @@ public class NotificationOutboxKpiSampleSourceTests
Assert.Equal(KpiSources.NotificationOutbox, source.Source);
}
// C4 (arch-review 04): the two metric names that were inline string literals now
// live in the shared KpiMetrics catalog. Their VALUES are persisted in the KpiSample
// store and rendered by the trend UI, so a rename would silently orphan history —
// pin the exact historical strings here.
[Fact]
public void KpiMetricCatalog_NotificationOutbox_PreservesHistoricalStringValues()
{
Assert.Equal("queueDepth", KpiMetrics.NotificationOutbox.QueueDepth);
Assert.Equal("stuckCount", KpiMetrics.NotificationOutbox.StuckCount);
Assert.Equal("parkedCount", KpiMetrics.NotificationOutbox.ParkedCount);
Assert.Equal("deliveredLastInterval", KpiMetrics.NotificationOutbox.DeliveredLastInterval);
Assert.Equal("oldestPendingAgeSeconds", KpiMetrics.NotificationOutbox.OldestPendingAgeSeconds);
}
[Fact]
public async Task CollectAsync_PassesCutoffsAnchoredOnCapturedAt()
{