using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using StackExchange.Redis; [Collection("Integration")] public class SepsisBundleTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _encounterId; private Guid _patientId; public SepsisBundleTests(ApiFixture fixture) => _fixture = fixture; public async Task InitializeAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); var patient = new Patient { Id = Guid.NewGuid(), Mrn = "MRN-BUNDLE-001", FirstName = "Bundle", LastName = "Test", DateOfBirth = new DateOnly(1965, 3, 15), Gender = "F", CreatedAt = DateTimeOffset.UtcNow }; var encounter = new Encounter { Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient, Status = EncounterStatus.Active, Department = Department.Icu, AttendingPhysician = "Dr. Bundle", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); await db.SaveChangesAsync(); _patientId = patient.Id; _encounterId = encounter.Id; var redis = scope.ServiceProvider.GetRequiredService(); var cache = redis.GetDatabase(); foreach (var key in QsofaCalculator.AllCriterionKeys(_encounterId)) await cache.KeyDeleteAsync(key); } public Task DisposeAsync() => Task.CompletedTask; private async Task CreateSofaSepsisBundleAsync(IServiceScope scope) { var handler = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); var alertId = Guid.NewGuid(); db.ClinicalAlerts.Add(new ClinicalAlert { Id = alertId, EncounterId = _encounterId, PatientId = _patientId, AlertType = AlertType.SofaSepsis, Severity = AlertSeverity.Critical, Details = "SOFA delta +2", Status = AlertStatus.Open, TriggeredAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(); await handler.OnSepsisAlertCreatedAsync( _encounterId, alertId, AlertType.SofaSepsis, CancellationToken.None); } [Fact] public async Task SofaSepsisAlert_CreatesBundleWithFourOrders() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var bundle = await db.SepsisBundles .Include(b => b.Elements) .SingleAsync(b => b.EncounterId == _encounterId); bundle.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.InProgress); bundle.TriggeringAlertType.Should().Be("SOFA_SEPSIS"); bundle.DeadlineAt.Should().BeCloseTo(bundle.RecognizedAt.AddHours(1), TimeSpan.FromSeconds(5)); bundle.Elements.Should().HaveCount(4); var orders = await db.Orders .Where(o => o.EncounterId == _encounterId) .ToListAsync(); orders.Should().HaveCount(4); orders.Should().AllSatisfy(o => { o.Status.Should().Be(OrderStatus.Pending); o.Description.Should().StartWith("SEP-1:"); o.OrderedBy.Should().Be("sepsis-bundle-engine"); }); var bundleOutbox = await db.OutboxEvents .Where(e => e.Topic == "sepsis.bundle.created") .SingleAsync(); bundleOutbox.PartitionKey.Should().Be(_encounterId.ToString()); } [Fact] public async Task QsofaScreen_DoesNotTriggerBundle() { using var scope = _fixture.Services.CreateScope(); var detector = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m); await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m); (await db.SepsisBundles.CountAsync()).Should().Be(0); } [Fact] public async Task SecondAlert_IdempotentBundle() { using var scope = _fixture.Services.CreateScope(); var handler = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var bundleCountAfterFirst = await db.SepsisBundles.CountAsync(b => b.EncounterId == _encounterId); bundleCountAfterFirst.Should().Be(1); var secondAlertId = Guid.NewGuid(); db.ClinicalAlerts.Add(new ClinicalAlert { Id = secondAlertId, EncounterId = _encounterId, PatientId = _patientId, AlertType = AlertType.SofaSepsis, Severity = AlertSeverity.Critical, Details = "SOFA delta +3", Status = AlertStatus.Open, TriggeredAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(); await handler.OnSepsisAlertCreatedAsync( _encounterId, secondAlertId, AlertType.SofaSepsis, CancellationToken.None); var bundleCount = await db.SepsisBundles.CountAsync(b => b.EncounterId == _encounterId); bundleCount.Should().Be(1, "a second bundle must not be created while one is IN_PROGRESS"); var orderCount = await db.Orders.CountAsync(o => o.EncounterId == _encounterId); orderCount.Should().Be(4, "no additional orders should be created for the duplicate bundle attempt"); } [Fact] public async Task OrderResulted_CompletesElement() { using var scope = _fixture.Services.CreateScope(); var bundleService = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var lactateElement = await db.SepsisBundleElements .Include(e => e.Order) .SingleAsync(e => e.ElementType == SepsisBundleElementType.SerumLactate); await bundleService.OnOrderResultedAsync(lactateElement.OrderId!.Value); var updated = await db.SepsisBundleElements .AsNoTracking() .SingleAsync(e => e.Id == lactateElement.Id); updated.Status.Should().Be(SepsisBundleElementStatus.Completed); updated.CompletedAt.Should().NotBeNull(); var bundle = await db.SepsisBundles.AsNoTracking().SingleAsync(); bundle.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.InProgress, "bundle should remain in progress until all 4 elements are completed"); } [Fact] public async Task AllElementsResulted_BundleCompliant() { using var scope = _fixture.Services.CreateScope(); var bundleService = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var elements = await db.SepsisBundleElements.ToListAsync(); foreach (var element in elements) await bundleService.OnOrderResultedAsync(element.OrderId!.Value); var bundle = await db.SepsisBundles.AsNoTracking().SingleAsync(); bundle.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.Compliant); bundle.CompletedAt.Should().NotBeNull(); bundle.CompletedAt.Should().BeBefore(bundle.DeadlineAt, "all elements completed within the 1-hour window"); var outboxEvents = await db.OutboxEvents .Where(e => e.Topic == "sepsis.bundle.updated") .ToListAsync(); outboxEvents.Should().HaveCount(4, "one outbox event per element completion"); } [Fact] public async Task DeadlinePassed_CompletionMarksNonCompliant() { using var scope = _fixture.Services.CreateScope(); var bundleService = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var bundle = await db.SepsisBundles.SingleAsync(); bundle.DeadlineAt = DateTimeOffset.UtcNow.AddHours(-1); await db.SaveChangesAsync(); var elements = await db.SepsisBundleElements.ToListAsync(); foreach (var element in elements) await bundleService.OnOrderResultedAsync(element.OrderId!.Value); var updated = await db.SepsisBundles.AsNoTracking().SingleAsync(); updated.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.NonCompliant); updated.CompletedAt.Should().NotBeNull(); } [Fact] public async Task DeadlinePassed_MonitorMarksNonCompliant() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await CreateSofaSepsisBundleAsync(scope); var bundle = await db.SepsisBundles.SingleAsync(); bundle.DeadlineAt = DateTimeOffset.UtcNow.AddHours(-1); await db.SaveChangesAsync(); var monitor = new SepsisBundleMonitorService( _fixture.Services, _fixture.Services.GetRequiredService(), _fixture.Services.GetRequiredService>()); await monitor.ScanOverdueBundlesAsync(CancellationToken.None); var updated = await db.SepsisBundles.AsNoTracking().SingleAsync(); updated.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.NonCompliant); updated.CompletedAt.Should().BeNull( "the monitor marks overdue bundles non-compliant without setting CompletedAt — " + "elements are still incomplete"); var elements = await db.SepsisBundleElements .AsNoTracking() .Where(e => e.BundleId == updated.Id) .ToListAsync(); elements.Should().AllSatisfy(e => e.Status.Should().Be(SepsisBundleElementStatus.Pending, "the monitor does not complete individual elements")); } }