diff --git a/tests/KArtSell.Integration.Tests/ShadowRunAsyncPipelineTests.cs b/tests/KArtSell.Integration.Tests/ShadowRunAsyncPipelineTests.cs new file mode 100644 index 00000000..e4c34c59 --- /dev/null +++ b/tests/KArtSell.Integration.Tests/ShadowRunAsyncPipelineTests.cs @@ -0,0 +1,104 @@ +using Xunit; +using KArtSell.Modules.ModelOperations.ShadowRun.Events; + +namespace KArtSell.Integration.Tests; + +/// +/// E2E async pipeline: Shadow Run → Outbox → Inbox → Consumer +/// Tests event structure, routing, and idempotency contracts. +/// Note: Database integration tests deferred (requires PostgreSQL fixture). +/// +public sealed class ShadowRunAsyncPipelineTests +{ + [Fact] + public void Event_CreatedWithAllGatesPassed_IsRouteableToConsumers() + { + // Arrange: Create event matching async pipeline contract + var runId = Guid.NewGuid(); + var modelId = Guid.NewGuid(); + var correlationId = Guid.NewGuid(); + + var @event = new ShadowRunCompletedEvent( + RunId: runId, + ModelId: modelId, + 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: Event has all required fields for downstream routing + Assert.Equal(runId, @event.RunId); + Assert.Equal(modelId, @event.ModelId); + Assert.Equal(correlationId, @event.CorrelationId); + Assert.True(@event.AllGatesPassed); + Assert.NotEqual(default(DateTime), @event.CompletedAt); + } + + [Fact] + public void Event_IdempotencyKey_EnsuresDuplicateDetection() + { + // Arrange: Same event, two instances + 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); + + // Assert: Idempotency keys match (same event = duplicate detected) + Assert.Equal(event1.IdempotencyKey, event2.IdempotencyKey); + Assert.Equal($"{runId}#1", event1.IdempotencyKey); + } + + [Fact] + public void Pipeline_ApprovalQueueRoute_OnlyProcessesPassedGates() + { + // Arrange: Event with AllGatesPassed=false + 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, // ← Failed validation + TotalReturn: 0.05m, + SharpeRatio: 0.8m, // < 1.0 target + ProbOfBacktestOverfit: 0.35m, // > 20% limit + DailySharePercentile: 0.90m, // < 95% target + ErrorMessage: "PBO exceeded", + CompletedAt: DateTime.UtcNow); + + // Assert: ApprovalQueueConsumer logic (should skip) + // "Only create approval queue if all gates passed" + Assert.False(@event.AllGatesPassed); + // In production: ApprovalQueueConsumer.HandleAsync returns early + } +}