87 lines
3.7 KiB
C#
87 lines
3.7 KiB
C#
using QuestionsApi.Models;
|
|
using QuestionsApi.Services;
|
|
using Xunit;
|
|
|
|
namespace QuestionsApi.Tests;
|
|
|
|
public sealed class MasteryCalculatorTests
|
|
{
|
|
private static readonly DateTime Now = new(2026, 7, 31, 12, 0, 0, DateTimeKind.Utc);
|
|
|
|
[Fact]
|
|
public void NoEvidenceIsUnseen()
|
|
{
|
|
var result = MasteryCalculator.Calculate([], [], [], new Dictionary<string, Question>(), Now);
|
|
Assert.All(result, item => Assert.Equal(MasteryStates.Unseen, item.MasteryState));
|
|
}
|
|
|
|
[Fact]
|
|
public void EvidenceGatesStrongAndMasteredStates()
|
|
{
|
|
var questions = Enumerable.Range(0, 25).ToDictionary(
|
|
index => $"q{index}",
|
|
index => Question($"q{index}", index < 5 ? 4 : 3));
|
|
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now.AddDays(-1))).ToList();
|
|
var strong = MasteryCalculator.Calculate(attempts.Take(15), [], [], questions, Now)[6];
|
|
var mastered = MasteryCalculator.Calculate(attempts, [], [], questions, Now)[6];
|
|
Assert.Equal(MasteryStates.Strong, strong.MasteryState);
|
|
Assert.Equal(MasteryStates.Mastered, mastered.MasteryState);
|
|
}
|
|
|
|
[Fact]
|
|
public void PendingReviewBlocksMasteredAndBecomesRecommendedAction()
|
|
{
|
|
var questions = Enumerable.Range(0, 25).ToDictionary(
|
|
index => $"q{index}", index => Question($"q{index}", index < 5 ? 4 : 3));
|
|
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now)).ToList();
|
|
var queue = new[] { new ReviewQueueItem { QuestionExternalId = "q0", State = ReviewQueueStates.Pending } };
|
|
var result = MasteryCalculator.Calculate(attempts, [], queue, questions, Now)[6];
|
|
Assert.Equal(MasteryStates.Strong, result.MasteryState);
|
|
Assert.Equal("review", result.RecommendedAction.Type);
|
|
}
|
|
|
|
[Fact]
|
|
public void DelayedReviewReceivesPartialRecoveryAndOldReviewDoesNotRecoverNewCycle()
|
|
{
|
|
var questions = new Dictionary<string, Question> { ["q"] = Question("q", 3) };
|
|
var first = Attempt("q", false, 0, Now.AddDays(-10));
|
|
var second = Attempt("q", false, 1, Now.AddDays(-2));
|
|
var reviews = new[]
|
|
{
|
|
new QuestionReviewAttempt { QuestionExternalId = "q", IsCorrect = true, CreatedUtc = Now.AddDays(-8) }
|
|
};
|
|
var result = MasteryCalculator.Calculate([first, second], reviews, [], questions, Now)[6];
|
|
Assert.Equal(25, result.MasteryScore);
|
|
}
|
|
|
|
[Fact]
|
|
public void StrongMasteryIsMarkedDueWithoutDecayingState()
|
|
{
|
|
var questions = Enumerable.Range(0, 15).ToDictionary(index => $"q{index}", index => Question($"q{index}", 4));
|
|
var attempts = questions.Keys.Select(id => Attempt(id, true, 0, Now.AddDays(-31))).ToList();
|
|
var result = MasteryCalculator.Calculate(attempts, [], [], questions, Now)[6];
|
|
Assert.Equal(MasteryStates.Strong, result.MasteryState);
|
|
Assert.True(result.RefreshDue);
|
|
}
|
|
|
|
[Fact]
|
|
public void SkipsDoNotCountAsEvidence()
|
|
{
|
|
var questions = new Dictionary<string, Question> { ["q"] = Question("q", 5) };
|
|
var skip = Attempt("q", false, 0, Now);
|
|
skip.IsSkip = true;
|
|
var result = MasteryCalculator.Calculate([skip], [], [], questions, Now)[6];
|
|
Assert.Equal(MasteryStates.Unseen, result.MasteryState);
|
|
}
|
|
|
|
private static Question Question(string id, int difficulty) => new()
|
|
{
|
|
ExternalId = id, System = "Cardiovascular System", Step3System = "Cardiovascular System", Difficulty = difficulty
|
|
};
|
|
|
|
private static QuestionAttempt Attempt(string id, bool correct, int cycle, DateTime created) => new()
|
|
{
|
|
QuestionExternalId = id, UserId = "user", IsCorrect = correct, BankCycle = cycle, CreatedUtc = created
|
|
};
|
|
}
|