fix(store-and-forward): resolve StoreAndForward-006,007,008,009 — transactional parked reads, PipeTo, fault-isolated activity events; 002/011/012 deferred

This commit is contained in:
Joseph Doherty
2026-05-16 22:32:30 -04:00
parent dd7626da63
commit 9e2416b34c
6 changed files with 255 additions and 52 deletions

View File

@@ -372,6 +372,50 @@ public class StoreAndForwardServiceTests : IAsyncLifetime, IDisposable
Assert.Contains("Queued", activities);
}
// ── StoreAndForward-009: faulting activity subscriber must not corrupt delivery ──
[Fact]
public async Task EnqueueAsync_ImmediateDeliverySuccess_FaultingActivitySubscriber_StillReportsDelivered()
{
// StoreAndForward-009: a throwing OnActivity subscriber (e.g. the site event
// log) must not be misclassified as a transient delivery failure. Pre-fix the
// subscriber's exception escaped RaiseActivity, was caught by EnqueueAsync's
// transient-failure handler, and a successfully delivered message was buffered.
_service.OnActivity += (_, _, _) => throw new InvalidOperationException("logging blew up");
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem,
_ => Task.FromResult(true));
var result = await _service.EnqueueAsync(
StoreAndForwardCategory.ExternalSystem, "api", """{}""");
Assert.True(result.Accepted);
Assert.False(result.WasBuffered); // delivered, NOT buffered
var msg = await _storage.GetMessageByIdAsync(result.MessageId);
Assert.Null(msg); // nothing left in the buffer
}
[Fact]
public async Task RetryMessageAsync_FaultingActivitySubscriber_DoesNotIncrementRetryCount()
{
// StoreAndForward-009: a throwing subscriber raised after a successful retry
// delivery must not be caught by the retry-failure handler and counted as a
// transient failure.
var result = await _service.EnqueueAsync(
StoreAndForwardCategory.ExternalSystem, "api", """{}""",
attemptImmediateDelivery: false, maxRetries: 5);
_service.RegisterDeliveryHandler(StoreAndForwardCategory.ExternalSystem,
_ => Task.FromResult(true));
_service.OnActivity += (_, _, _) => throw new InvalidOperationException("logging blew up");
await _service.RetryPendingMessagesAsync();
// The retry succeeded; the message must be gone, not re-buffered with a bumped count.
var msg = await _storage.GetMessageByIdAsync(result.MessageId);
Assert.Null(msg);
}
// ── WP-10: Per-source-entity retry settings ──
[Fact]

View File

@@ -250,6 +250,39 @@ public class StoreAndForwardStorageTests : IAsyncLifetime, IDisposable
Assert.Equal(2, page2.Count);
}
[Fact]
public async Task GetParkedMessagesAsync_TransactionedReads_CountMatchesFullResultSet()
{
// StoreAndForward-006: the COUNT(*) and paged SELECT now run inside one
// transaction so they share a consistent snapshot. This functional check
// guards the fix — it verifies the transaction wiring did not break paging:
// the reported TotalCount and the rows assembled across all pages agree, and
// a page wide enough to hold every parked row contains exactly TotalCount rows.
for (int i = 0; i < 25; i++)
{
var m = CreateMessage($"txn-{i}", StoreAndForwardCategory.ExternalSystem);
m.Status = StoreAndForwardMessageStatus.Parked;
await _storage.EnqueueAsync(m);
await _storage.UpdateMessageAsync(m);
}
var (wholePage, wholeTotal) = await _storage.GetParkedMessagesAsync(pageNumber: 1, pageSize: 1000);
Assert.Equal(25, wholeTotal);
Assert.Equal(wholeTotal, wholePage.Count);
var collected = new List<string>();
int reportedTotal = -1;
for (int page = 1; ; page++)
{
var (rows, total) = await _storage.GetParkedMessagesAsync(pageNumber: page, pageSize: 7);
reportedTotal = total;
collected.AddRange(rows.Select(r => r.Id));
if (rows.Count < 7) break;
}
Assert.Equal(reportedTotal, collected.Count);
Assert.Equal(25, collected.Distinct().Count());
}
[Fact]
public async Task GetMessageCountByStatusAsync_ReturnsAccurateCount()
{