Files
step3/api/QuestionsApi.Tests/DashboardTests.cs
T
hzandClaude Sonnet 5 74398133f7 Exclude sample exam simulations from the dashboard's readiness score
The SUPER-role "preview results" QA tooling seeds a completed
ExamSimulation with scripted (not real) answers, flagged IsSample.
The dashboard's readiness lookup picked whichever completed
simulation was most recent regardless of that flag, so running the
QA preview on an account could override its real Exam Readiness card
with a synthetic pass probability. Recent-activity/weekly-activity
queries got the same exclusion since they read from the same
collection for the same reason.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
2026-07-26 14:47:07 -05:00

234 lines
8.6 KiB
C#

using QuestionsApi.Models;
using QuestionsApi.Services;
using Xunit;
namespace QuestionsApi.Tests;
public sealed class DashboardTests
{
[Fact]
public void PercentageRoundsToNearestWholeNumber()
{
Assert.Equal(67, DashboardStore.Percentage(2, 3));
Assert.Equal(0, DashboardStore.Percentage(0, 0));
}
[Fact]
public void StreakIncludesYesterdayWhenUserHasNotStudiedToday()
{
var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc);
var dates = new[] { today.AddDays(-1), today.AddDays(-2), today.AddDays(-3), today.AddDays(-5) };
Assert.Equal(3, DashboardStore.CalculateCurrentStreak(dates, today));
}
[Fact]
public void StreakStopsAtFirstMissingDay()
{
var today = new DateTime(2026, 7, 18, 0, 0, 0, DateTimeKind.Utc);
var dates = new[] { today, today.AddDays(-1), today.AddDays(-3) };
Assert.Equal(2, DashboardStore.CalculateCurrentStreak(dates, today));
}
[Fact]
public void SystemPerformanceReturnsEveryOfficialSystemInOrder()
{
var attempts = new[]
{
Attempt("cardio-1", true),
Attempt("cardio-2", false),
Attempt("ethics-1", true),
Attempt("ignored", true),
Attempt("skipped-cardio", false, true)
};
var questions = new Dictionary<string, Question>
{
["cardio-1"] = Question("Cardiology"),
["cardio-2"] = Question("Cardiovascular System"),
["ethics-1"] = Question("Medical Ethics"),
["ignored"] = Question("Unmapped Specialty"),
["skipped-cardio"] = Question("Cardiology")
};
var result = DashboardStore.BuildSystemPerformance(attempts, questions);
Assert.Equal(14, result.Count);
Assert.Equal(DashboardStore.Step3Systems, result.Select(value => value.System));
var cardiovascular = result.Single(value => value.System == "Cardiovascular System");
Assert.Equal(1, cardiovascular.Correct);
Assert.Equal(2, cardiovascular.Total);
Assert.Equal(50, cardiovascular.Accuracy);
var social = result.Single(value => value.System.StartsWith("Social Sciences"));
Assert.Equal(1, social.Correct);
Assert.Equal(1, social.Total);
Assert.All(result.Where(value => value.System != cardiovascular.System && value.System != social.System),
value => Assert.Equal(0, value.Total));
}
[Theory]
[InlineData("Preventive Medicine", "Human Development")]
[InlineData("Infectious Disease", "Immune System, Blood & Lymphoreticular System, and Multisystem Processes/Disorders")]
[InlineData("Dermatology", "Skin & Subcutaneous Tissue")]
[InlineData("Pulmonary", "Respiratory System")]
[InlineData("Pregnancy", "Pregnancy/Childbirth & Female Reproductive System & Breast")]
[InlineData("Biostatistics", "Biostatistics & Epidemiology/Population Health & Interpretation of the Medical Literature")]
[InlineData("Medical Ethics", "Social Sciences: Communication Skills/Ethics/Patient Safety")]
public void SystemAliasesMapToOfficialCategories(string source, string expected)
{
Assert.Equal(expected, DashboardStore.NormalizeStep3System(source));
}
[Fact]
public void TrendCarriesRatingForwardAndLeavesInactiveAccuracyAsGapData()
{
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
var baselineDate = today.AddDays(-2).AddHours(8);
var attempts = new[]
{
Attempt("one", true, createdUtc: today.AddDays(-2).AddHours(9),
rating: 1018, ratingUtc: today.AddDays(-2).AddHours(9)),
Attempt("two", false, createdUtc: today.AddDays(-2).AddHours(10),
rating: 1002, ratingUtc: today.AddDays(-2).AddHours(10)),
Attempt("three", true, createdUtc: today.AddHours(11),
rating: 1020, ratingUtc: today.AddHours(11)),
Attempt("skip", false, true, today.AddDays(-1).AddHours(4),
990, today.AddDays(-1).AddHours(4))
};
var rating = new UserRating
{
OverallElo = 1020,
HistoryBaselineElo = 1000,
HistoryStartedUtc = baselineDate
};
var result = DashboardStore.BuildPerformanceTrend(
attempts, rating, Array.Empty<CcsAttempt>(), new CcsUserRating(), today);
Assert.Equal(30, result.Count);
Assert.Null(result[^4].QuestionBankRating);
Assert.Equal(1002, result[^3].QuestionBankRating);
Assert.Equal(50, result[^3].Accuracy);
Assert.Equal(2, result[^3].Total);
Assert.Equal(990, result[^2].QuestionBankRating);
Assert.Equal(0, result[^2].Total);
Assert.Equal(1020, result[^1].QuestionBankRating);
Assert.Equal(100, result[^1].Accuracy);
}
[Fact]
public void TrendCarriesCcsRatingForwardIndependentlyOfQuestionBankRating()
{
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
var baselineDate = today.AddDays(-2).AddHours(8);
var rating = new UserRating();
var ccsRating = new CcsUserRating
{
Elo = 1050,
HistoryBaselineElo = 1000,
HistoryStartedUtc = baselineDate
};
var ccsAttempts = new[]
{
RatedCcsAttempt("one", today.AddDays(-1).AddHours(9), 1030),
RatedCcsAttempt("two", today.AddHours(10), 1050)
};
var result = DashboardStore.BuildPerformanceTrend(
Array.Empty<QuestionAttempt>(), rating, ccsAttempts, ccsRating, today);
Assert.Equal(30, result.Count);
Assert.Equal(1000, result[^3].CcsRating);
Assert.Equal(1030, result[^2].CcsRating);
Assert.Equal(1050, result[^1].CcsRating);
}
private static CcsAttempt RatedCcsAttempt(string caseExternalId, DateTime appliedUtc, int userElo) => new()
{
CaseExternalId = caseExternalId,
Rating = new CcsRatingChange { Rated = true, UserElo = userElo, AppliedUtc = appliedUtc }
};
private static QuestionAttempt Attempt(
string questionId,
bool correct,
bool skip = false,
DateTime? createdUtc = null,
int? rating = null,
DateTime? ratingUtc = null) => new()
{
QuestionExternalId = questionId,
IsCorrect = correct,
IsSkip = skip,
CreatedUtc = createdUtc ?? DateTime.UtcNow,
UserOverallEloAfter = rating,
RatingAppliedUtc = ratingUtc
};
private static Question Question(string system) => new()
{
ExternalId = system,
System = system
};
[Fact]
public void PercentileReturnsNullWithNoUsers()
{
Assert.Null(DashboardStore.Percentile(0, 0));
}
[Fact]
public void PercentileComputesTopPercentFromUsersRatedHigher()
{
// 18 of 100 users rate higher than this user - "top 18%"
Assert.Equal(18, DashboardStore.Percentile(18, 100));
}
[Fact]
public void PercentileNeverReturnsZero()
{
// Even the single best-rated user should read as "top 1%", not "top 0%"
Assert.Equal(1, DashboardStore.Percentile(0, 100));
}
[Theory]
[InlineData(0.85, "High")]
[InlineData(0.7, "High")]
[InlineData(0.55, "Medium")]
[InlineData(0.4, "Medium")]
[InlineData(0.1, "Low")]
public void ReadinessLabelUsesPassProbabilityThresholds(double passProbability, string expected)
{
Assert.Equal(expected, DashboardStore.ReadinessLabel(passProbability));
}
[Fact]
public void SelectLatestReadinessScoreSkipsSampleSimulationsEvenWhenMostRecent()
{
var today = new DateTime(2026, 7, 25, 0, 0, 0, DateTimeKind.Utc);
var simulations = new[]
{
CompletedSimulation(isSample: true, completedUtc: today, passProbability: 0.98),
CompletedSimulation(isSample: false, completedUtc: today.AddDays(-3), passProbability: 0.12)
};
var score = DashboardStore.SelectLatestReadinessScore(simulations);
Assert.NotNull(score);
Assert.Equal(0.12, score.PassProbability);
}
[Fact]
public void SelectLatestReadinessScoreReturnsNullWhenOnlySamplesExist()
{
var simulations = new[] { CompletedSimulation(isSample: true, completedUtc: DateTime.UtcNow, passProbability: 0.98) };
Assert.Null(DashboardStore.SelectLatestReadinessScore(simulations));
}
private static ExamSimulation CompletedSimulation(bool isSample, DateTime completedUtc, double passProbability) => new()
{
IsSample = isSample,
CompletedUtc = completedUtc,
Score = new ExamSimulationScore { PassProbability = passProbability }
};
}