using Dapper; using KArtSell.BuildingBlocks.Data; using KArtSell.BuildingBlocks.Time; using Npgsql; using Xunit; namespace KArtSell.Integration.Tests; /// /// Gate 3: Shadow Run Validation Tests (252+ trading-day execution) /// Covers: End-to-end shadow run workflow, validation gates, approval auto-population /// Following AGENTS.md v16.0: Complete validation pipeline, evidence preservation /// public sealed class ShadowRunGate3Tests : IAsyncLifetime { private readonly string _connectionString; private readonly NpgsqlDataSource _dataSource; private readonly IDbConnectionFactory _connectionFactory; private readonly IClock _clock = new SystemClock(); public ShadowRunGate3Tests() { _connectionString = Environment.GetEnvironmentVariable("KARTSELL_POSTGRES") ?? "Host=localhost;Port=5432;Database=kartselldb;Username=kartsell;Password=kartsell4321@!"; _dataSource = new NpgsqlDataSourceBuilder(_connectionString).Build(); _connectionFactory = new NpgsqlConnectionFactory(_dataSource); } public async Task InitializeAsync() { await using var connection = await _dataSource.OpenConnectionAsync(); await using var cmd = connection.CreateCommand(); cmd.CommandText = "SELECT 1"; await cmd.ExecuteScalarAsync(); } public async Task DisposeAsync() { await _dataSource.DisposeAsync(); } /// /// Gate 3.1: Shadow Run Completion - EvaluationComplete status recorded /// [Fact] public async Task ShadowRun_ExecutionComplete_RecordsMetricsAndValidationGates() { // Arrange: Create test shadow run with complete validation gates var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var now = _clock.UtcNow.DateTime; var validationGates = """ { "pbo": 0.15, "pbo_under_20": true, "dsr": 0.96, "dsr_above_95": true, "cost_2x_positive": true, "all_gates_passed": true, "sharpe": 1.45, "calmar": 0.82, "max_drawdown": 0.18, "returns": 0.28 } """; var phaseAnalysis = """ { "bull": {"sharpe": 1.8, "return": 0.35, "days": 85}, "bear": {"sharpe": 0.9, "return": 0.15, "days": 45}, "sideways": {"sharpe": 1.2, "return": 0.22, "days": 122} } """; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, published_at, validation_gates_json, phase_analysis_json) VALUES (@RunId, @ModelId, @Start, @End, @Status, @Now, CAST(@Gates AS jsonb), CAST(@Phase AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Status = "EvaluationComplete", Now = now, Gates = validationGates, Phase = phaseAnalysis }); // Act: Query validation gates var gates = await connection.QuerySingleAsync<(bool AllPassed, decimal Pbo, decimal Dsr, bool Cost2xPositive)>(""" SELECT CAST(validation_gates_json->>'all_gates_passed' AS bool) as AllPassed, CAST(validation_gates_json->>'pbo' AS decimal) as Pbo, CAST(validation_gates_json->>'dsr' AS decimal) as Dsr, CAST(validation_gates_json->>'cost_2x_positive' AS bool) as Cost2xPositive FROM model_operations.shadow_run WHERE run_id = @RunId """, new { RunId = runId }); // Assert: All validation gates passed Assert.True(gates.AllPassed); Assert.True(gates.Pbo <= 0.20m, "PBO should be ≤ 20%"); Assert.True(gates.Dsr >= 0.95m, "DSR should be ≥ 95th percentile"); Assert.True(gates.Cost2xPositive); } /// /// Gate 3.2: Validation Gates - PBO ≤ 20% check /// [Fact] public async Task ShadowRun_ValidationGate_PboUnder20Percent() { // Arrange: Create shadow run with PBO near threshold var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var now = _clock.UtcNow.DateTime; var validationGates = """{"pbo": 0.18, "pbo_under_20": true}"""; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, published_at, validation_gates_json) VALUES (@RunId, @ModelId, @Start, @End, @Status, @Now, CAST(@Gates AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Status = "EvaluationComplete", Now = now, Gates = validationGates }); // Act: Check PBO gate var pboGate = await connection.QuerySingleAsync<(decimal Pbo, bool Pass)>(""" SELECT CAST(validation_gates_json->>'pbo' AS decimal) as Pbo, CAST(validation_gates_json->>'pbo_under_20' AS bool) as Pass FROM model_operations.shadow_run WHERE run_id = @RunId """, new { RunId = runId }); // Assert: PBO under 20% threshold Assert.True(pboGate.Pass); Assert.True(pboGate.Pbo <= 0.20m); } /// /// Gate 3.3: Approval Queue Auto-Population - Shadow run completion triggers approval /// [Fact] public async Task ShadowRun_CompleteWithAllGatesPassed_AutoPopulatesApprovalQueue() { // Arrange: Create shadow run that passes all gates var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var now = _clock.UtcNow.DateTime; var validationGates = """ { "all_gates_passed": true, "pbo_under_20": true, "dsr_above_95": true, "cost_2x_positive": true } """; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, published_at, validation_gates_json) VALUES (@RunId, @ModelId, @Start, @End, @Status, @Now, CAST(@Gates AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Status = "EvaluationComplete", Now = now, Gates = validationGates }); // Simulate approval queue auto-population (what downstream job would do) await connection.ExecuteAsync(""" INSERT INTO model_operations.approval_queue (run_id, model_id, status) VALUES (@RunId, @ModelId, 'Pending') """, new { RunId = runId, ModelId = modelId }); // Act: Query approval status var approval = await connection.QuerySingleAsync<(Guid RunId, string Status, DateTime RequestedAt)>(""" SELECT run_id as RunId, status as Status, requested_at as RequestedAt FROM model_operations.approval_queue WHERE run_id = @RunId """, new { RunId = runId }); // Assert: Approval auto-created in Pending status Assert.Equal("Pending", approval.Status); Assert.True(approval.RequestedAt <= now.AddSeconds(1)); } /// /// Gate 3.4: Audit Trail - CorrelationId present in all events /// [Fact] public async Task ShadowRun_AuditTrail_CorrelationIdPreservedInOutbox() { // Arrange: Create shadow run with correlation tracking var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var correlationId = Guid.NewGuid().ToString(); var now = _clock.UtcNow.DateTime; var validationGates = """{"all_gates_passed": true}"""; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); // Insert shadow run await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, published_at, validation_gates_json) VALUES (@RunId, @ModelId, @Start, @End, @Status, @Now, CAST(@Gates AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Status = "EvaluationComplete", Now = now, Gates = validationGates }); // Emit event to outbox with correlation ID var eventId = Guid.NewGuid(); await connection.ExecuteAsync(""" INSERT INTO building_blocks.outbox_message (message_id, event_type, schema_version, payload_json, correlation_id, occurred_at, payload_hash, published_at) VALUES (@EventId, @EventType, 1, @Payload, @CorrelationId, @Now, @Hash, @Now) """, new { EventId = eventId, EventType = "ShadowRunCompleted", Payload = $$$"""{{"runId":"{runId}","modelId":"{modelId}"}}""", CorrelationId = correlationId, Now = now, Hash = "hash123" }); // Act: Verify correlation ID is preserved var audit = await connection.QuerySingleAsync<(string CorrelationId, string EventType)>(""" SELECT correlation_id as CorrelationId, event_type as EventType FROM building_blocks.outbox_message WHERE message_id = @EventId """, new { EventId = eventId }); // Assert: Correlation ID tracked end-to-end Assert.Equal(correlationId, audit.CorrelationId); Assert.Equal("ShadowRunCompleted", audit.EventType); } /// /// Gate 3.5: Phase Analysis - Bull/Bear/Sideways metrics non-zero /// [Fact] public async Task ShadowRun_PhaseSegmentation_AllPhaseMetricsNonZero() { // Arrange: Create shadow run with phase analysis var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var now = _clock.UtcNow.DateTime; var phaseAnalysis = """ { "bull": {"sharpe": 1.8, "return": 0.35, "days": 85}, "bear": {"sharpe": 0.9, "return": 0.15, "days": 45}, "sideways": {"sharpe": 1.2, "return": 0.22, "days": 122} } """; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, phase_analysis_json) VALUES (@RunId, @ModelId, @Start, @End, @Status, CAST(@Phase AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Status = "EvaluationComplete", Phase = phaseAnalysis }); // Act: Query phase metrics var phaseJson = await connection.QuerySingleAsync(""" SELECT phase_analysis_json::text FROM model_operations.shadow_run WHERE run_id = @RunId """, new { RunId = runId }); // Assert: Phase analysis captured (structure validation in real execution) Assert.NotNull(phaseJson); Assert.Contains("bull", phaseJson); Assert.Contains("bear", phaseJson); Assert.Contains("sideways", phaseJson); } /// /// Gate 3.6: End-to-End Flow - Shadow run → Approval → Activation /// [Fact] public async Task ShadowRun_EndToEndFlow_CompletionTriggersApprovalWorkflow() { // Arrange: Complete shadow run flow var runId = Guid.NewGuid(); var modelId = Guid.NewGuid(); var approverId = Guid.NewGuid(); var now = _clock.UtcNow.DateTime; var validationGates = """ { "all_gates_passed": true, "pbo": 0.15, "dsr": 0.96, "cost_2x_positive": true } """; await using var connection = await _connectionFactory.OpenAsync(CancellationToken.None); // Step 1: Shadow run completes await connection.ExecuteAsync(""" INSERT INTO model_operations.shadow_run (run_id, model_id, window_start, window_end, status, published_at, validation_gates_json) VALUES (@RunId, @ModelId, @Start, @End, 'EvaluationComplete', @Now, CAST(@Gates AS jsonb)) """, new { RunId = runId, ModelId = modelId, Start = new DateOnly(2024, 1, 2), End = new DateOnly(2024, 8, 31), Now = now, Gates = validationGates }); // Step 2: Approval auto-populated await connection.ExecuteAsync(""" INSERT INTO model_operations.approval_queue (run_id, model_id, status) VALUES (@RunId, @ModelId, 'Pending') """, new { RunId = runId, ModelId = modelId }); // Step 3: Maker-checker approves await connection.ExecuteAsync(""" UPDATE model_operations.approval_queue SET status = 'Approved', approved_by = @ApproverId, approval_reason = @Reason WHERE run_id = @RunId """, new { RunId = runId, ApproverId = approverId, Reason = "All gates passed. Ready for activation." }); // Act: Verify complete flow var shadowRun = await connection.QuerySingleAsync<(string Status, bool AllGatesPassed)>(""" SELECT status as Status, CAST(validation_gates_json->>'all_gates_passed' AS bool) as AllGatesPassed FROM model_operations.shadow_run WHERE run_id = @RunId """, new { RunId = runId }); var approval = await connection.QuerySingleAsync<(string Status, Guid ApprovedBy)>(""" SELECT status as Status, approved_by as ApprovedBy FROM model_operations.approval_queue WHERE run_id = @RunId """, new { RunId = runId }); // Assert: Complete flow successful Assert.Equal("EvaluationComplete", shadowRun.Status); Assert.True(shadowRun.AllGatesPassed); Assert.Equal("Approved", approval.Status); Assert.Equal(approverId, approval.ApprovedBy); } // ========== Helper Class ========== private sealed class NpgsqlConnectionFactory : IDbConnectionFactory { private readonly NpgsqlDataSource _dataSource; public NpgsqlConnectionFactory(NpgsqlDataSource dataSource) { _dataSource = dataSource; } public async ValueTask OpenAsync(CancellationToken cancellationToken) => await _dataSource.OpenConnectionAsync(cancellationToken); } }