File size: 3,393 Bytes
a7ffb22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"""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