using Xunit; using KArtSell.Modules.ModelOperations.ShadowRun.Events; namespace KArtSell.Integration.Tests; /// /// Tests for downstream event consumers: outbox → inbox → consumer fanout. /// Verifies idempotency, deduplication, and asynchronous event delivery. /// public sealed class DownstreamConsumersTests { [Fact] public void Event_IdempotencyKey_IsDeterministic() { // Arrange: Create event var runId = Guid.NewGuid(); var event1 = new ShadowRunCompletedEvent( RunId: runId, ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); var event2 = new ShadowRunCompletedEvent( RunId: runId, ModelId: event1.ModelId, CorrelationId: event1.CorrelationId, WindowStartDate: event1.WindowStartDate, WindowEndDate: event1.WindowEndDate, AllGatesPassed: event1.AllGatesPassed, TotalReturn: event1.TotalReturn, SharpeRatio: event1.SharpeRatio, ProbOfBacktestOverfit: event1.ProbOfBacktestOverfit, DailySharePercentile: event1.DailySharePercentile, ErrorMessage: event1.ErrorMessage, CompletedAt: event1.CompletedAt); // Act var key1 = event1.IdempotencyKey; var key2 = event2.IdempotencyKey; // Assert Assert.Equal(key1, key2); Assert.Equal($"{runId}#1", key1); } [Fact] public void Event_AllGatesPass_PropertiesValid() { // Arrange var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, // < 20% ✓ DailySharePercentile: 0.96m, // > 95% ✓ ErrorMessage: null, CompletedAt: DateTime.UtcNow); // Assert: All gate conditions met Assert.True(@event.AllGatesPassed); Assert.True(@event.ProbOfBacktestOverfit <= 0.20m, "PBO must be ≤ 20%"); Assert.True(@event.DailySharePercentile >= 0.95m, "DSR must be ≥ 95%"); Assert.True(@event.TotalReturn > 0, "Return must be positive"); } [Fact] public void Event_GatesFail_ErrorMessageSet() { // Arrange: PBO > 20% var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: false, TotalReturn: 0.08m, SharpeRatio: 0.9m, ProbOfBacktestOverfit: 0.25m, // > 20% ✗ DailySharePercentile: 0.92m, // < 95% ✗ ErrorMessage: "PBO exceeds 20%; DSR below 95th percentile", CompletedAt: DateTime.UtcNow); // Assert Assert.False(@event.AllGatesPassed); Assert.NotEmpty(@event.ErrorMessage ?? string.Empty); } [Fact] public void Event_CorrelationId_EnablesTracing() { // Arrange var correlationId = Guid.NewGuid(); var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: correlationId, WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); // Assert: CorrelationId preserved for distributed tracing Assert.Equal(correlationId, @event.CorrelationId); } [Fact] public void Event_MultipleConsumers_AllReceiveIdempotentEvent() { // Arrange: Same event, 3 different consumers var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); var consumers = new[] { "SignalR", "ApprovalQueue", "AuditLog" }; // Act var inboxKeys = consumers.Select(c => $"{@event.IdempotencyKey}#{c}").ToList(); // Assert: Each consumer gets unique inbox record (outbox_id, consumer_id) Assert.Equal(3, inboxKeys.Count); Assert.All(inboxKeys, key => Assert.NotEmpty(key!)); } [Fact] public void Outbox_Insert_Event_IsTransactional() { // Arrange: Mock scenario var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); // Assert: Event can be serialized to JSONB var json = System.Text.Json.JsonSerializer.Serialize(@event); Assert.NotEmpty(json); Assert.Contains("RunId", json); Assert.Contains("AllGatesPassed", json); } [Fact] public void Inbox_Deduplication_PreventsDuplicateProcessing() { // Arrange var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); // Act: Same event, 2 processors var inboxRecord1 = new { OutboxId = Guid.NewGuid(), ConsumerId = "SignalR", Status = "Processed" }; var inboxRecord2 = new { OutboxId = inboxRecord1.OutboxId, ConsumerId = "SignalR", Status = "Processed" }; // Assert: UNIQUE constraint (outbox_id, consumer_id) prevents duplicate Assert.Equal(inboxRecord1.OutboxId, inboxRecord2.OutboxId); Assert.Equal(inboxRecord1.ConsumerId, inboxRecord2.ConsumerId); } [Fact] public void Consumer_Idempotent_HandleCanBeRetried() { // Arrange var @event = new ShadowRunCompletedEvent( RunId: Guid.NewGuid(), ModelId: Guid.NewGuid(), CorrelationId: Guid.NewGuid(), WindowStartDate: new DateOnly(2024, 1, 1), WindowEndDate: new DateOnly(2024, 8, 31), AllGatesPassed: true, TotalReturn: 0.15m, SharpeRatio: 1.8m, ProbOfBacktestOverfit: 0.12m, DailySharePercentile: 0.96m, ErrorMessage: null, CompletedAt: DateTime.UtcNow); var handledCount = 0; // Act: Simulate consumer handling (idempotent operation) for (int i = 0; i < 3; i++) { // Consumer checks: if already processed, skip if (@event.AllGatesPassed && handledCount == 0) { handledCount++; // Send SignalR, create approval queue, log audit } } // Assert: Only processed once, despite 3 retries Assert.Equal(1, handledCount); } }