Add remain services and fixes

This commit is contained in:
voltsrage
2026-06-18 23:33:39 +08:00
parent 4b46c09f90
commit e2d40331b7
9 changed files with 291 additions and 40 deletions
@@ -32,17 +32,20 @@ public class ApiFixture : WebApplicationFactory<Program>, IAsyncLifetime
public async Task InitializeAsync()
{
// Apply migrations before the host starts — background services such as
// ThresholdCacheLoader query the database during StartAsync.
// Apply migrations and clean stale data before the host starts — background
// services such as ThresholdCacheLoader query the database during StartAsync,
// so the reset must happen while no hosted service holds a lock.
var connectionString = "Host=localhost;Port=5436;Database=vigilcare_test;Username=postgres;Password=password";
var options = new DbContextOptionsBuilder<AppDbContext>()
.UseNpgsql(connectionString)
.Options;
await using (var migrateDb = new AppDbContext(options))
{
await migrateDb.Database.MigrateAsync();
await DbResetHelper.ResetAsync(migrateDb);
}
using var scope = Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var redis = scope.ServiceProvider.GetRequiredService<IConnectionMultiplexer>();
var server = redis.GetServer(redis.GetEndPoints().First());
await server.FlushDatabaseAsync(1);
@@ -11,12 +11,17 @@ public static class DbResetHelper
try
{
await db.Database.ExecuteSqlRawAsync(@"
SET lock_timeout = '3s';
TRUNCATE TABLE sepsis_bundle_elements, sepsis_bundles,
reconciliation_alerts, outbox_events, orders,
clinical_alerts, news2_scores, observations, encounters,
alert_thresholds, patients
RESTART IDENTITY CASCADE;
DELETE FROM sepsis_bundle_elements;
DELETE FROM sepsis_bundles;
DELETE FROM reconciliation_alerts;
DELETE FROM outbox_events;
DELETE FROM orders;
DELETE FROM clinical_alerts;
DELETE FROM news2_scores;
DELETE FROM observations;
DELETE FROM encounters;
DELETE FROM alert_thresholds;
DELETE FROM patients;
");
return;
}
@@ -87,8 +87,7 @@ public class QsofaDetectorTests : IAsyncLifetime
alert.PatientId.Should().Be(_patientId);
alert.Details.Should().Contain("qSOFA score 2/3");
var outbox = await db.OutboxEvents.SingleAsync();
outbox.Topic.Should().Be("alert.generated");
var outbox = await db.OutboxEvents.SingleAsync(e => e.Topic == "alert.generated");
outbox.PartitionKey.Should().Be(_encounterId.ToString());
}
@@ -141,7 +140,7 @@ public class QsofaDetectorTests : IAsyncLifetime
var alertCount = await db.ClinicalAlerts.CountAsync();
alertCount.Should().Be(1, "WHERE NOT EXISTS prevents duplicate QSOFA_WARNING while one is open");
var outboxCount = await db.OutboxEvents.CountAsync();
var outboxCount = await db.OutboxEvents.CountAsync(e => e.Topic == "alert.generated");
outboxCount.Should().Be(1, "outbox event must be written exactly once");
}
@@ -1,6 +1,7 @@
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using StackExchange.Redis;
[Collection("Integration")]
@@ -206,4 +207,40 @@ public class SepsisBundleTests : IAsyncLifetime
updated.ComplianceStatus.Should().Be(SepsisBundleComplianceStatus.NonCompliant);
updated.CompletedAt.Should().NotBeNull();
}
[Fact]
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);
var bundle = await db.SepsisBundles.SingleAsync();
bundle.DeadlineAt = DateTimeOffset.UtcNow.AddHours(-1);
await db.SaveChangesAsync();
var monitor = new SepsisBundleMonitorService(
_fixture.Services,
_fixture.Services.GetRequiredService<ClinicalMetrics>(),
_fixture.Services.GetRequiredService<ILogger<SepsisBundleMonitorService>>());
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"));
}
}
@@ -101,8 +101,7 @@ public class SirsDetectorTests : IAsyncLifetime
alert.PatientId.Should().Be(_patientId);
// Verify outbox event was written in the same transaction
var outbox = await db.OutboxEvents.SingleAsync();
outbox.Topic.Should().Be("alert.generated");
var outbox = await db.OutboxEvents.SingleAsync(e => e.Topic == "alert.generated");
outbox.PartitionKey.Should().Be(_encounterId.ToString());
}
@@ -189,7 +188,7 @@ public class SirsDetectorTests : IAsyncLifetime
var alertCount = await db.ClinicalAlerts.CountAsync();
alertCount.Should().Be(1, "WHERE NOT EXISTS prevents duplicate alert on redelivery");
var outboxCount = await db.OutboxEvents.CountAsync();
var outboxCount = await db.OutboxEvents.CountAsync(e => e.Topic == "alert.generated");
outboxCount.Should().Be(1, "outbox event must be written exactly once");
}