151 lines
4.8 KiB
C#
151 lines
4.8 KiB
C#
public static class ExpectedOutcomeValidator
|
|
{
|
|
private static readonly TimeSpan AsyncSettleDelay = TimeSpan.FromSeconds(8);
|
|
|
|
public static async Task<List<string>> ValidateAsync(
|
|
VigilCareApiClient client,
|
|
Guid encounterId,
|
|
ScenarioFile scenario,
|
|
CancellationToken ct = default)
|
|
{
|
|
if (scenario.ExpectedOutcomes is not { Count: > 0 })
|
|
return [];
|
|
|
|
await Task.Delay(AsyncSettleDelay, ct);
|
|
|
|
var failures = new List<string>();
|
|
var alerts = await client.GetAlertsAsync(encounterId);
|
|
|
|
foreach (var outcome in scenario.ExpectedOutcomes)
|
|
{
|
|
switch (outcome.Type)
|
|
{
|
|
case "alert":
|
|
failures.AddRange(ValidateAlertOutcome(outcome, alerts, scenario.Scenario.Id));
|
|
break;
|
|
case "score":
|
|
failures.AddRange(await ValidateScoreOutcomeAsync(client, encounterId, outcome, scenario.Scenario.Id, ct));
|
|
break;
|
|
case "bundle":
|
|
failures.AddRange(await ValidateBundleOutcomeAsync(client, encounterId, outcome, scenario.Scenario.Id, ct));
|
|
break;
|
|
default:
|
|
failures.Add($"[{scenario.Scenario.Id}] Unknown expected outcome type '{outcome.Type}'");
|
|
break;
|
|
}
|
|
}
|
|
|
|
return failures;
|
|
}
|
|
|
|
private static IEnumerable<string> ValidateAlertOutcome(
|
|
ExpectedOutcome outcome, List<AlertResponse> alerts, string scenarioId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(outcome.AlertType))
|
|
{
|
|
yield return $"[{scenarioId}] Alert outcome at T+{outcome.AfterOffsetMinutes}m missing alertType";
|
|
yield break;
|
|
}
|
|
|
|
var matching = alerts
|
|
.Where(a => string.Equals(a.AlertType, outcome.AlertType, StringComparison.OrdinalIgnoreCase))
|
|
.ToList();
|
|
|
|
if (matching.Count == 0)
|
|
{
|
|
yield return
|
|
$"[{scenarioId}] Expected alert {outcome.AlertType} at T+{outcome.AfterOffsetMinutes}m — not found " +
|
|
$"(description: {outcome.Description ?? "n/a"})";
|
|
yield break;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(outcome.NarrativeContains))
|
|
{
|
|
var withNarrative = matching.FirstOrDefault(a =>
|
|
a.Explanation?.NarrativeSummary?.Contains(
|
|
outcome.NarrativeContains, StringComparison.OrdinalIgnoreCase) == true);
|
|
|
|
if (withNarrative is null)
|
|
{
|
|
var summaries = matching
|
|
.Select(a => a.Explanation?.NarrativeSummary ?? a.Details)
|
|
.Take(2);
|
|
yield return
|
|
$"[{scenarioId}] Alert {outcome.AlertType} at T+{outcome.AfterOffsetMinutes}m — " +
|
|
$"narrative does not contain '{outcome.NarrativeContains}' " +
|
|
$"(got: {string.Join(" | ", summaries)})";
|
|
}
|
|
}
|
|
}
|
|
|
|
private static async Task<IEnumerable<string>> ValidateScoreOutcomeAsync(
|
|
VigilCareApiClient client,
|
|
Guid encounterId,
|
|
ExpectedOutcome outcome,
|
|
string scenarioId,
|
|
CancellationToken ct)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(outcome.ScoreType))
|
|
return [$"[{scenarioId}] Score outcome at T+{outcome.AfterOffsetMinutes}m missing scoreType"];
|
|
|
|
if (!outcome.ExpectedMinimum.HasValue)
|
|
return [$"[{scenarioId}] Score outcome at T+{outcome.AfterOffsetMinutes}m missing expectedMinimum"];
|
|
|
|
var actual = await GetScoreAsync(client, encounterId, outcome.ScoreType, ct);
|
|
if (actual is null)
|
|
{
|
|
return
|
|
[
|
|
$"[{scenarioId}] Expected {outcome.ScoreType} ≥ {outcome.ExpectedMinimum} at T+{outcome.AfterOffsetMinutes}m — score not available"
|
|
];
|
|
}
|
|
|
|
if (actual.Value < outcome.ExpectedMinimum.Value)
|
|
{
|
|
return
|
|
[
|
|
$"[{scenarioId}] Expected {outcome.ScoreType} ≥ {outcome.ExpectedMinimum} at T+{outcome.AfterOffsetMinutes}m — got {actual.Value} " +
|
|
$"(description: {outcome.Description ?? "n/a"})"
|
|
];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static async Task<IEnumerable<string>> ValidateBundleOutcomeAsync(
|
|
VigilCareApiClient client,
|
|
Guid encounterId,
|
|
ExpectedOutcome outcome,
|
|
string scenarioId,
|
|
CancellationToken ct)
|
|
{
|
|
var bundle = await client.GetSepsisBundleAsync(encounterId);
|
|
if (bundle is null)
|
|
{
|
|
return
|
|
[
|
|
$"[{scenarioId}] Expected sepsis bundle at T+{outcome.AfterOffsetMinutes}m — bundle not found " +
|
|
$"(description: {outcome.Description ?? "n/a"})"
|
|
];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
|
|
private static async Task<int?> GetScoreAsync(
|
|
VigilCareApiClient client,
|
|
Guid encounterId,
|
|
string scoreType,
|
|
CancellationToken ct)
|
|
{
|
|
return scoreType.ToUpperInvariant() switch
|
|
{
|
|
"NEWS2" => (await client.GetCurrentNews2Async(encounterId))?.TotalScore,
|
|
"GCS" => (await client.GetCurrentGcsAsync(encounterId))?.TotalScore,
|
|
"SOFA" => (await client.GetCurrentSofaAsync(encounterId))?.TotalScore,
|
|
"QSOFA" => (await client.GetCurrentQsofaAsync(encounterId))?.ActiveCriteria,
|
|
_ => null
|
|
};
|
|
}
|
|
}
|