using FluentAssertions; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using StackExchange.Redis; [Collection("Integration")] public class AlertSuppressionTests : IAsyncLifetime { private readonly ApiFixture _fixture; private Guid _patientId; private Guid _encounterId; public AlertSuppressionTests(ApiFixture fixture) => _fixture = fixture; public async Task InitializeAsync() => await ResetAndSeedThresholdAsync(); public Task DisposeAsync() => Task.CompletedTask; private async Task ResetAndSeedThresholdAsync() { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await DbResetHelper.ResetAsync(db); var patient = new Patient { Id = Guid.NewGuid(), Mrn = "MRN-SUP-001", FirstName = "Suppress", LastName = "Test", DateOfBirth = new DateOnly(1982, 8, 10), Gender = "M", CreatedAt = DateTimeOffset.UtcNow }; var encounter = new Encounter { Id = Guid.NewGuid(), PatientId = patient.Id, EncounterType = EncounterType.Inpatient, Status = EncounterStatus.Active, Department = Department.Icu, AttendingPhysician = "Dr. Suppress", AdmittedAt = DateTimeOffset.UtcNow, CreatedAt = DateTimeOffset.UtcNow }; db.Patients.Add(patient); db.Encounters.Add(encounter); db.AlertThresholds.Add(new AlertThreshold { Id = Guid.NewGuid(), ObservationCode = "HEART_RATE", DisplayName = "Heart Rate", Unit = "bpm", CriticalLow = 30, WarningLow = 50, WarningHigh = 100, CriticalHigh = 150, CreatedAt = DateTimeOffset.UtcNow }); await db.SaveChangesAsync(); var redis = scope.ServiceProvider.GetRequiredService(); await redis.GetDatabase().StringSetAsync("threshold:HEART_RATE", """{"ObservationCode":"HEART_RATE","CriticalLow":30,"WarningLow":50,"WarningHigh":100,"CriticalHigh":150}"""); _patientId = patient.Id; _encounterId = encounter.Id; } [Fact] public async Task AcknowledgeWarning_SetsSuppressionKey() { await ResetAndSeedThresholdAsync(); var alertId = await SeedAlertAsync(AlertType.WarningHeartRate, AlertSeverity.Warning); using var scope = _fixture.Services.CreateScope(); TestAuthContext.AsNurse(scope.ServiceProvider); var alerts = scope.ServiceProvider.GetRequiredService(); var redis = scope.ServiceProvider.GetRequiredService(); await alerts.AcknowledgeAsync(alertId, new AcknowledgeAlertRequest("monitoring")); var key = AlertSuppressionService.SuppressionKey(_encounterId, AlertType.WarningHeartRate); (await redis.GetDatabase().KeyExistsAsync(key)).Should().BeTrue(); } [Fact] public async Task SuppressedWarning_SkipsAlertCreation() { await ResetAndSeedThresholdAsync(); var alertId = await SeedAlertAsync(AlertType.WarningHeartRate, AlertSeverity.Warning); using var scope = _fixture.Services.CreateScope(); TestAuthContext.AsNurse(scope.ServiceProvider); var alerts = scope.ServiceProvider.GetRequiredService(); var evaluator = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await alerts.AcknowledgeAsync(alertId, new AcknowledgeAlertRequest("monitoring")); await alerts.ResolveAsync(alertId); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 105m); created.Should().BeFalse(); (await db.ClinicalAlerts.CountAsync()).Should().Be(1, "suppressed warning must not create a new alert after resolve"); } [Fact] public async Task CriticalAlert_NotSuppressible() { await ResetAndSeedThresholdAsync(); var alertId = await SeedAlertAsync(AlertType.CriticalHeartRate, AlertSeverity.Critical); using var scope = _fixture.Services.CreateScope(); TestAuthContext.AsNurse(scope.ServiceProvider, displayName: "Test Physician"); var alerts = scope.ServiceProvider.GetRequiredService(); var redis = scope.ServiceProvider.GetRequiredService(); await alerts.AcknowledgeAsync(alertId, new AcknowledgeAlertRequest("treating")); var key = AlertSuppressionService.SuppressionKey(_encounterId, AlertType.CriticalHeartRate); (await redis.GetDatabase().KeyExistsAsync(key)).Should().BeFalse(); } [Fact] public async Task News2Emergency_NotSuppressible() { await ResetAndSeedThresholdAsync(); var alertId = await SeedAlertAsync(AlertType.News2Emergency, AlertSeverity.Critical); using var scope = _fixture.Services.CreateScope(); TestAuthContext.AsNurse(scope.ServiceProvider, displayName: "Test Physician"); var alerts = scope.ServiceProvider.GetRequiredService(); var redis = scope.ServiceProvider.GetRequiredService(); await alerts.AcknowledgeAsync(alertId, new AcknowledgeAlertRequest("reviewed")); var key = AlertSuppressionService.SuppressionKey(_encounterId, AlertType.News2Emergency); (await redis.GetDatabase().KeyExistsAsync(key)).Should().BeFalse(); } [Fact] public async Task News2Warning_Suppressible() { await ResetAndSeedThresholdAsync(); var alertId = await SeedAlertAsync(AlertType.News2Warning, AlertSeverity.Warning); using var scope = _fixture.Services.CreateScope(); TestAuthContext.AsNurse(scope.ServiceProvider); var alerts = scope.ServiceProvider.GetRequiredService(); var redis = scope.ServiceProvider.GetRequiredService(); await alerts.AcknowledgeAsync(alertId, new AcknowledgeAlertRequest("monitoring")); var key = AlertSuppressionService.SuppressionKey(_encounterId, AlertType.News2Warning); (await redis.GetDatabase().KeyExistsAsync(key)).Should().BeTrue(); } [Fact] public async Task SuppressionExpires_AllowsNewAlert() { await ResetAndSeedThresholdAsync(); using var scope = _fixture.Services.CreateScope(); var suppression = scope.ServiceProvider.GetRequiredService(); var evaluator = scope.ServiceProvider.GetRequiredService(); var db = scope.ServiceProvider.GetRequiredService(); await suppression.SetSuppressionAsync( _encounterId, AlertType.WarningHeartRate, TimeSpan.FromSeconds(1)); var blocked = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 105m); blocked.Should().BeFalse(); await Task.Delay(1500); var created = await evaluator.EvaluateAsync( Guid.NewGuid(), _encounterId, _patientId, "HEART_RATE", 105m); created.Should().BeTrue(); var alert = await db.ClinicalAlerts.SingleAsync(); alert.AlertType.Should().Be(AlertType.WarningHeartRate); } private async Task SeedAlertAsync(AlertType alertType, AlertSeverity severity) { using var scope = _fixture.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var alert = new ClinicalAlert { Id = Guid.NewGuid(), EncounterId = _encounterId, PatientId = _patientId, AlertType = alertType, Severity = severity, Details = $"Test {alertType.ToDbString()} alert.", Status = AlertStatus.Open, TriggeredAt = DateTimeOffset.UtcNow }; db.ClinicalAlerts.Add(alert); await db.SaveChangesAsync(); return alert.Id; } }