mathly / mathly2 /tests /test_comets.py
3v324v23's picture
Add specifications for exercise tests, grades, learning path, lesson quizzes, readiness meter, real tests, rewards, simulation tests, skill mastery, smart board, student home, and test countdown
a7ffb22
Raw History Blame Contribute Delete
3.39 kB
"""The comets and the rule for when one is due (add-rewards 3.1)."""
from datetime import date
from mathly2 import comets
from mathly2.store import InMemoryStore
from mathly2.tests.test_store import Recorder, U, V
TODAY, YESTERDAY = date(2026, 11, 9), date(2026, 11, 8)
def test_there_are_twelve_comets_with_unique_ids_and_a_symbol_name_and_fact():
assert len(comets.COMETS) == 12 and len(set(comets.IDS)) == 12
assert all(c["sym"] and c["name"] and len(c["fact"]) > 20 for c in comets.COMETS)
assert [c["name"] for c in comets.COMETS][:3] == ["פאי", "פיבונאצ׳י", "יחס הזהב"] and comets.get("pi")["sym"] == "π"
assert comets.get("nope") is None
def test_all_caught_means_never_due():
assert comets.due({"last_comet_day": YESTERDAY}, set(comets.IDS), TODAY, True, roll=0.0) is False
def test_at_most_one_comet_a_day():
assert comets.due({"last_comet_day": TODAY}, set(), TODAY, True, roll=0.0) is False
def test_the_first_comet_comes_only_on_the_answer_that_reaches_the_goal():
assert comets.due(None, set(), TODAY, goal_just_reached=False, roll=0.0) is False # no chance roll before the first comet
assert comets.due({"last_comet_day": None}, set(), TODAY, goal_just_reached=False, roll=0.0) is False
assert comets.due(None, set(), TODAY, goal_just_reached=True, roll=0.99) is True # certain, whatever the roll
def test_after_a_comet_was_offered_each_right_answer_has_a_12_percent_chance():
state = {"last_comet_day": YESTERDAY}
assert comets.due(state, {"pi"}, TODAY, False, roll=0.11) is True
assert comets.due(state, {"pi"}, TODAY, False, roll=0.13) is False
def test_pick_gives_a_comet_the_student_does_not_have_and_none_when_all_are_held():
held = set(comets.IDS[:11])
assert comets.pick(held, roll=0.0)["id"] == comets.IDS[11]
assert comets.pick(set(comets.IDS)) is None
seen = {comets.pick({"pi"}, roll=i / 20)["id"] for i in range(20)}
assert "pi" not in seen and len(seen) == 11
def test_store_remembers_caught_comets_and_the_reward_days_per_student():
store = InMemoryStore()
assert store.comets_of(U) == set() and store.reward_state(U) is None
store.add_comet(U, "pi"); store.add_comet(U, "pi"); store.add_comet(V, "e")
assert store.comets_of(U) == {"pi"} and store.comets_of(V) == {"e"}
store.save_reward_state(U, last_comet_day=TODAY)
store.save_reward_state(U, last_catch_day=TODAY) # the other field stays
assert store.reward_state(U)["last_comet_day"] == TODAY and store.reward_state(U)["last_catch_day"] == TODAY
assert store.reward_state(V) is None
def test_supabase_comet_and_reward_requests():
s = Recorder([[{"comet_id": "pi"}], None, [{"user_id": U, "last_comet_day": "2026-11-09", "last_catch_day": None}],
[{"user_id": U, "last_comet_day": "2026-11-09", "last_catch_day": None}], []])
assert s.comets_of(U) == {"pi"}
s.add_comet(U, "e")
assert s.calls[1][:2] == ("POST", "m2_comets") and s.calls[1][3] == {"user_id": U, "comet_id": "e"} and s.calls[1][5] is True
assert s.reward_state(U) == {"user_id": U, "last_comet_day": TODAY, "last_catch_day": None}
saved = s.save_reward_state(U, last_comet_day=TODAY)
assert s.calls[3][3] == {"user_id": U, "last_comet_day": "2026-11-09"} and saved["last_comet_day"] == TODAY
assert s.reward_state(V) is None