88fbc9e351
SiteAuditReconciliationActor minted a fresh random EventId for the synthetic ReconciliationAbandoned row every time a permanently-abandoned row was re-abandoned. Because the reconciliation cursor is held at `since` by ANY co-batch unresolved row (see PullSiteAsync's hasUnresolvedFailure gate), a poisoned row's per-EventId attempt counter restarts every time it reappears in a stuck batch, so it gets re-abandoned roughly every MaxPermanentInsertAttempts ticks — minting an unbounded number of duplicate central audit rows for the SAME lost event. Fix: DeriveAbandonmentEventId(Guid lostEventId) derives the synthetic EventId deterministically (SHA-256 over a fixed namespace GUID + the lost row's bytes, RFC4122-v5-shaped). Re-abandonment of the same lost row now always produces the same synthetic EventId, so IAuditLogRepository.InsertIfNotExistsAsync's existing idempotency collapses every repeat into the first insert. Also adds an in-memory _abandonedEventIds set as a CHEAP SHORT-CIRCUIT ONLY (skips the wasted ~25 min retry-then-abandon cycle for a row already known abandoned this process's lifetime) — it is not the correctness mechanism; it resets on singleton restart/failover, at which point the deterministic id alone still keeps re-abandonment idempotent. Tests (SiteAuditReconciliationActorTests): - ReAbandonment_SameLostEventId_YieldsExactlyOneCentralRow: end-to-end repro of the defect trigger (poison row + a rotating-EventId co-batch row that never itself reaches the threshold, holding the cursor forever) across two full abandon cycles. Proof of failure: run against the unfixed source (git stash of the source file only, pure-function tests temporarily #if false'd to keep the file compiling), the test failed with exactly the predicted defect shape — 2 distinct synthetic rows for one lost EventId. Passes post-fix (1 row). - DeriveAbandonmentEventId_DifferentLostRows_YieldDistinctSyntheticIds and DeriveAbandonmentEventId_IsPureAndStableAcrossIndependentInvocations: pure-function coverage of the derivation directly, including a pinned golden-value regression guard proving it is a function of the input GUID's value alone (no clock/randomness/process state).