fix(audit): ScadaBridge C4 review — enable PRAGMA foreign_keys + MarkForwarded state guard (no Reconciled demotion) + test (Task 2.5)
This commit is contained in:
@@ -524,6 +524,72 @@ public class SqliteAuditWriterWriteTests
|
||||
// Completes without throwing.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fix 2 / M1 state guard: <see cref="SqliteAuditWriter.MarkForwardedAsync"/>
|
||||
/// must NOT demote a <see cref="AuditForwardState.Reconciled"/> row back to
|
||||
/// <see cref="AuditForwardState.Forwarded"/>. When a batch contains both a
|
||||
/// Pending ID and an already-Reconciled ID:
|
||||
/// <list type="bullet">
|
||||
/// <item>the Pending row transitions to Forwarded (normal path)</item>
|
||||
/// <item>the Reconciled row stays Reconciled (AttemptCount unchanged)</item>
|
||||
/// </list>
|
||||
/// This mirrors the idempotency guard already present on
|
||||
/// <see cref="SqliteAuditWriter.MarkReconciledAsync"/>.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public async Task MarkForwardedAsync_DoesNotDemoteReconciledRow_WhilePendingStillTransitions()
|
||||
{
|
||||
var (writer, dataSource) = CreateWriter(
|
||||
nameof(MarkForwardedAsync_DoesNotDemoteReconciledRow_WhilePendingStillTransitions));
|
||||
await using var _ = writer;
|
||||
|
||||
var pending = NewEvent();
|
||||
var reconciled = NewEvent();
|
||||
|
||||
await writer.WriteAsync(pending);
|
||||
await writer.WriteAsync(reconciled);
|
||||
|
||||
// Advance reconciled through Forwarded → Reconciled so its AttemptCount = 1.
|
||||
await writer.MarkForwardedAsync(new[] { reconciled.EventId });
|
||||
await writer.MarkReconciledAsync(new[] { reconciled.EventId });
|
||||
|
||||
// Verify the reconciled row's AttemptCount is 1 before the test call.
|
||||
using var conn = OpenVerifierConnection(dataSource);
|
||||
long reconciledAttemptBefore;
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT AttemptCount FROM audit_forward_state WHERE EventId = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", reconciled.EventId.ToString());
|
||||
reconciledAttemptBefore = Convert.ToInt64(cmd.ExecuteScalar());
|
||||
}
|
||||
Assert.Equal(1L, reconciledAttemptBefore);
|
||||
|
||||
// Now call MarkForwardedAsync with BOTH IDs in the same batch.
|
||||
await writer.MarkForwardedAsync(new[] { pending.EventId, reconciled.EventId });
|
||||
|
||||
// The Pending row must have transitioned to Forwarded.
|
||||
Assert.Equal(
|
||||
AuditForwardState.Forwarded.ToString(),
|
||||
ReadForwardState(dataSource, pending.EventId));
|
||||
|
||||
// The Reconciled row must remain Reconciled — the state guard must have
|
||||
// excluded it from the UPDATE.
|
||||
Assert.Equal(
|
||||
AuditForwardState.Reconciled.ToString(),
|
||||
ReadForwardState(dataSource, reconciled.EventId));
|
||||
|
||||
// AttemptCount on the Reconciled row must be unchanged (still 1, not 2).
|
||||
using (var cmd = conn.CreateCommand())
|
||||
{
|
||||
cmd.CommandText =
|
||||
"SELECT AttemptCount FROM audit_forward_state WHERE EventId = $id;";
|
||||
cmd.Parameters.AddWithValue("$id", reconciled.EventId.ToString());
|
||||
var attemptAfter = Convert.ToInt64(cmd.ExecuteScalar());
|
||||
Assert.Equal(reconciledAttemptBefore, attemptAfter);
|
||||
}
|
||||
}
|
||||
|
||||
// ----- ExecutionId (rides DetailsJson, recomposed via AsRow) ----- //
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user