feature: Sepsis Engine Refactor: Remove SIRS, Rewire qSOFA + Bundle

Frontend: GCS Entry, SOFA Display, Sepsis UI Refactor
This commit is contained in:
voltsrage
2026-06-21 03:56:27 +08:00
parent 93ea473d2b
commit bf46e6554a
48 changed files with 2686 additions and 714 deletions
+54 -38
View File
@@ -43,28 +43,47 @@ public class SepsisBundleTests : IAsyncLifetime
var cache = redis.GetDatabase();
foreach (var key in QsofaCalculator.AllCriterionKeys(_encounterId))
await cache.KeyDeleteAsync(key);
foreach (var key in SirsEvaluator.AllCriterionKeys(_encounterId))
await cache.KeyDeleteAsync(key);
}
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task QsofaAlert_CreatesBundleWithFourOrders()
private async Task CreateSofaSepsisBundleAsync(IServiceScope scope)
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var handler = scope.ServiceProvider.GetRequiredService<SepsisAlertHandler>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
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<AppDbContext>();
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("QSOFA_WARNING");
bundle.TriggeringAlertType.Should().Be("SOFA_SEPSIS");
bundle.DeadlineAt.Should().BeCloseTo(bundle.RecognizedAt.AddHours(1), TimeSpan.FromSeconds(5));
bundle.Elements.Should().HaveCount(4);
@@ -86,40 +105,45 @@ public class SepsisBundleTests : IAsyncLifetime
}
[Fact]
public async Task SirsAlert_CreatesBundle()
public async Task QsofaScreen_DoesNotTriggerBundle()
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<SirsDetector>();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "HEART_RATE", 95m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "TEMP_C", 38.5m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
var bundle = await db.SepsisBundles
.Include(b => b.Elements)
.SingleAsync(b => b.EncounterId == _encounterId);
bundle.TriggeringAlertType.Should().Be("SEPSIS_WARNING");
bundle.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.InProgress);
bundle.Elements.Should().HaveCount(4);
(await db.SepsisBundles.CountAsync()).Should().Be(0);
}
[Fact]
public async Task SecondAlert_IdempotentBundle()
{
using var scope = _fixture.Services.CreateScope();
var qsofaDetector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var sirsDetector = scope.ServiceProvider.GetRequiredService<SirsDetector>();
var handler = scope.ServiceProvider.GetRequiredService<SepsisAlertHandler>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await qsofaDetector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await qsofaDetector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
await CreateSofaSepsisBundleAsync(scope);
var bundleCountAfterFirst = await db.SepsisBundles.CountAsync(b => b.EncounterId == _encounterId);
bundleCountAfterFirst.Should().Be(1);
await sirsDetector.ProcessObservationAsync(_encounterId, _patientId, "HEART_RATE", 95m);
await sirsDetector.ProcessObservationAsync(_encounterId, _patientId, "TEMP_C", 38.5m);
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");
@@ -132,12 +156,10 @@ public class SepsisBundleTests : IAsyncLifetime
public async Task OrderResulted_CompletesElement()
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var bundleService = scope.ServiceProvider.GetRequiredService<ISepsisBundleService>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
await CreateSofaSepsisBundleAsync(scope);
var lactateElement = await db.SepsisBundleElements
.Include(e => e.Order)
@@ -161,12 +183,10 @@ public class SepsisBundleTests : IAsyncLifetime
public async Task AllElementsResulted_BundleCompliant()
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var bundleService = scope.ServiceProvider.GetRequiredService<ISepsisBundleService>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
await CreateSofaSepsisBundleAsync(scope);
var elements = await db.SepsisBundleElements.ToListAsync();
foreach (var element in elements)
@@ -188,12 +208,10 @@ public class SepsisBundleTests : IAsyncLifetime
public async Task DeadlinePassed_CompletionMarksNonCompliant()
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var bundleService = scope.ServiceProvider.GetRequiredService<ISepsisBundleService>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
await CreateSofaSepsisBundleAsync(scope);
var bundle = await db.SepsisBundles.SingleAsync();
bundle.DeadlineAt = DateTimeOffset.UtcNow.AddHours(-1);
@@ -212,11 +230,9 @@ public class SepsisBundleTests : IAsyncLifetime
public async Task DeadlinePassed_MonitorMarksNonCompliant()
{
using var scope = _fixture.Services.CreateScope();
var detector = scope.ServiceProvider.GetRequiredService<QsofaDetector>();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
await detector.ProcessObservationAsync(_encounterId, _patientId, "RESP_RATE", 24m);
await detector.ProcessObservationAsync(_encounterId, _patientId, "SYSTOLIC_BP", 95m);
await CreateSofaSepsisBundleAsync(scope);
var bundle = await db.SepsisBundles.SingleAsync();
bundle.DeadlineAt = DateTimeOffset.UtcNow.AddHours(-1);