Files
voltsrage a6a491a2da
CI / backend (push) Failing after 8m50s
CI / frontend (push) Failing after 1m45s
Fix tests
2026-08-05 20:21:23 +08:00

170 lines
7.1 KiB
C#

using System.Net.Http.Json;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
[Collection("Integration")]
public class ExplainableAlertsTests : IAsyncLifetime
{
private readonly ApiFixture _fixture;
public ExplainableAlertsTests(ApiFixture fixture) => _fixture = fixture;
public async Task InitializeAsync() =>
await ExplainableAlertsTestHelper.ResetAsync(_fixture);
public Task DisposeAsync() => Task.CompletedTask;
[Fact]
public async Task News2Alert_IncludesScoreContributors()
{
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
_fixture, respRate: 28, spo2: 92, systolicBp: 95, heartRate: 110,
tempC: 38.5m, supplementalO2: 1m);
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
_fixture, encounterId, AlertType.News2Emergency);
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
body.Explanation.Should().NotBeNull();
body.Explanation!.ScoreContributors.Should().HaveCount(7);
body.Explanation.ScoreContributors.Should().Contain(c =>
c.Parameter == "Respiratory Rate" && c.Points > 0);
body.Explanation.NarrativeSummary.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public async Task SofaAlert_IncludesOrganContributors()
{
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedSofaBaselineAsync(_fixture);
await ExplainableAlertsTestHelper.AdvanceSofaDeltaAsync(_fixture, encounterId, patientId, delta: 2);
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
_fixture, encounterId, AlertType.SofaSepsis);
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
body.Explanation.Should().NotBeNull();
body.Explanation!.ScoreContributors.Should().NotBeEmpty();
body.Explanation.ScoreContributors.Should().Contain(c => c.Parameter == "Respiratory");
}
[Fact]
public async Task GcsAlert_IncludesEyeVerbalMotor()
{
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
await ExplainableAlertsTestHelper.RecordGcsAsync(
_fixture, encounterId, patientId, eye: 2, verbal: 2, motor: 3);
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
_fixture, encounterId, AlertType.GcsCritical);
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
body.Explanation.Should().NotBeNull();
body.Explanation!.ScoreContributors.Should().HaveCount(3);
body.Explanation.ScoreContributors.Should().Contain(c => c.Parameter == "Eye");
}
[Fact]
public async Task TrendAlert_IncludesTrendContext()
{
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
await ExplainableAlertsTestHelper.SimulateRapidRespRateRiseAsync(
_fixture, encounterId, patientId);
var alert = await ExplainableAlertsTestHelper.WaitForAlertAsync(
_fixture, encounterId, AlertType.RapidDeterioration);
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, alert.Id);
body.Explanation.Should().NotBeNull();
body.Explanation!.Trend.Should().NotBeNull();
body.Explanation.Trend!.PercentChange.Should().BeGreaterThan(0);
body.Explanation.Trend.Duration.Should().NotBe(default);
}
[Fact]
public async Task MedicationAlert_IncludesDrugContext()
{
var (encounterId, patientId) = await ExplainableAlertsTestHelper.SeedEncounterAsync(_fixture);
await ExplainableAlertsTestHelper.AdministerMedicationAsync(
_fixture, encounterId, "acetaminophen", dose: 1000, unit: "mg");
await ExplainableAlertsTestHelper.FeedNews2VitalsAsync(
_fixture, encounterId, patientId,
respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
tempC: 39.2m, supplementalO2: 0m);
var alert = await ExplainableAlertsTestHelper.WaitForFirstAlertWithExplanationAsync(
_fixture, encounterId);
alert.Explanation.Should().NotBeNull();
alert.Explanation!.MedicationContext.Should().NotBeNull();
alert.Explanation.MedicationContext!.DrugName.Should().BeEquivalentTo("acetaminophen");
}
[Fact]
public async Task NarrativeSummary_IsHumanReadable()
{
var alert = await ExplainableAlertsTestHelper.GetAnyAlertWithExplanationAsync(_fixture);
alert.Explanation.Should().NotBeNull();
alert.Explanation!.NarrativeSummary.Should().MatchRegex("(NEWS2|SOFA|GCS|rapid deterioration)", "i");
}
[Fact]
public async Task LegacyAlert_NullExplanation_Serializes()
{
var legacyId = await ExplainableAlertsTestHelper.SeedLegacyAlertWithoutExplanationAsync(_fixture);
var body = await ExplainableAlertsTestHelper.GetAlertAsync<AlertResponse>(_fixture, legacyId);
body.Explanation.Should().BeNull();
}
[Fact]
public async Task ListByEncounter_ReturnsExplanation()
{
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
_fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
tempC: 37.0m, supplementalO2: 0m);
using var client = _fixture.CreateClient();
var response = await client.GetAsync($"/api/v1/encounters/{encounterId}/alerts");
response.EnsureSuccessStatusCode();
var body = await response.Content.ReadFromJsonAsync<ApiResponse<PagedResult<AlertResponse>>>(
ExplainableAlertsTestHelper.JsonOptions);
body!.Data!.Items.Should().NotBeEmpty();
body.Data.Items.Should().AllSatisfy(a =>
{
a.Explanation.Should().NotBeNull();
a.Explanation!.NarrativeSummary.Should().NotBeNullOrWhiteSpace();
});
}
[Fact]
public async Task ScoringOutputs_Unchanged()
{
var before = await ExplainableAlertsTestHelper.RunNews2HighScoreReplayAsync(_fixture);
var after = await ExplainableAlertsTestHelper.RunNews2HighScoreReplayAsync(_fixture);
after.TotalScore.Should().Be(before.TotalScore);
after.AlertType.Should().Be(before.AlertType);
}
[Fact]
public async Task ExplainableAlert_OutboxPayload_IncludesExplanation()
{
var (encounterId, _) = await ExplainableAlertsTestHelper.SeedEncounterWithVitalsAsync(
_fixture, respRate: 25, spo2: 91, systolicBp: 95, heartRate: 72,
tempC: 37.0m, supplementalO2: 0m);
using var scope = _fixture.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
var outbox = await db.OutboxEvents
.Where(e => e.Topic == "alert.generated" && e.PartitionKey == encounterId.ToString())
.OrderByDescending(e => e.CreatedAt)
.FirstAsync();
outbox.Payload.Should().Contain("explanation");
outbox.Payload.Should().Contain("narrativeSummary");
}
}