whatfirst-small / test_score.py
optiquant's picture
updates to speed up computation of ranked list
a56a725
Raw
History Blame Contribute Delete
5.14 kB
"""Self-contained tests for the scoring engine.
Numeric anchors are derived by hand from the formula (worked through on paper),
not copied from score.py's own output, so they actually constrain the port. The
rest assert structural invariants (monotonicity, tier order, quick-win
direction) that catch regressions a single magic number would miss.
"""
from datetime import datetime
import score as s
def approx(a, b, tol=1e-3):
assert abs(a - b) <= tol, f"expected {b}, got {a}"
# -- Hand-computed urgency anchors --------------------------------------------
def test_cal_urgency_week_out():
# d=7: base = 1 + 9/(1+(7/7)**1.5) = 1 + 9/2 = 5.5; d>=T so no accel.
approx(s.cal_urgency(7), 5.5)
def test_cal_urgency_overdue_saturates():
# d=0 -> 30; d=-5 -> 30 + 5*2 = 40 (capped at 40).
approx(s.cal_urgency(0), 30.0)
approx(s.cal_urgency(-5), 40.0)
approx(s.cal_urgency(-100), 40.0) # hard cap
def test_comp_urgency_comfortable_slack():
# d=7, e=4h -> e_days=1, slack=7, base = 1 + 9/(1+3.5) = 3; no accel.
approx(s.comp_urgency(7, 4), 3.0)
def test_comp_urgency_explodes_near_zero_slack():
# Tiny slack drives the inverse-distance term up hard.
assert s.comp_urgency(0.1, 4) > 20
# -- Hand-computed full-score anchor ------------------------------------------
def test_priority_default_no_due():
# I=5, R=8, E=0.05, no due -> U=1.5, R_eff=8,
# QW = 1 + 0.3*exp(-1/30) = 1.2901648,
# do = (5*1.5*8*QW)/10 = 7.740989; prep loses.
t = {"id": "x", "impact": 5, "readiness": 8, "effort_hours": 0.05}
approx(s.priority(t), 7.7410)
# -- Structural invariants ----------------------------------------------------
def test_readiness_moves_the_score():
# The whole point of READY_LIFT: readiness stays live. Higher R -> higher do.
base = {"id": "x", "impact": 6, "effort_hours": 1}
assert s.priority({**base, "readiness": 10}) > s.priority({**base, "readiness": 8})
def test_quick_win_boost_rewards_short_effort():
# Lower effort -> larger QW -> higher displayed score, all else equal.
base = {"id": "x", "impact": 6, "readiness": 8}
assert s.priority({**base, "effort_hours": 0.05}) > s.priority({**base, "effort_hours": 4})
def test_prep_wins_for_unready_high_impact():
# R=2, I=8, no due: prep (6.72*QW) beats do (2.4*QW) and R<=5.
c = s.score_components({"id": "x", "impact": 8, "readiness": 2})
assert c["prep_wins"] is True
assert c["prep_score"] > c["do_score"]
def test_risk_on_zeroes_prep_and_lifts_readiness():
c = s.score_components({"id": "x", "impact": 8, "readiness": 2, "execute_anyway": True})
assert c["prep_score"] == 0.0
assert c["R_eff"] >= 9
assert c["prep_wins"] is False
def test_value_bucket_monotone():
assert s.value_bucket(100) > s.value_bucket(10) > s.value_bucket(1)
def test_bad_fields_never_nan():
# A garbage field must fall back, not poison the score.
c = s.score_components({"id": "x", "impact": None, "readiness": float("nan"), "effort_hours": "oops"})
assert c["display"] == c["display"] # not NaN
assert 0.1 <= c["display"] <= s.SCORE_CAP
# -- Ranking ------------------------------------------------------------------
NOW = datetime(2026, 6, 7, 12, 0, 0)
def test_overdue_ranks_first():
tasks = [
{"id": "B", "impact": 10, "readiness": 10, "effort_hours": 0.1}, # high value, no due
{"id": "A", "impact": 2, "readiness": 8, "effort_hours": 1, "due_date": "2026-06-01"}, # overdue
{"id": "C", "impact": 7, "readiness": 8, "effort_hours": 1, "due_date": "2026-06-20"}, # future
]
order = [t["id"] for t in s.rank_active(tasks, NOW)]
assert order[0] == "A", order # overdue lifted above the value pack
def test_binding_deadline_lifts_above_value():
# A low-value task that genuinely won't finish in time must beat a juicy
# future task it would otherwise sit below.
tasks = [
{"id": "big", "impact": 10, "readiness": 9, "effort_hours": 2, "due_date": "2026-06-30"},
{"id": "tight", "impact": 3, "readiness": 9, "effort_hours": 10, "due_date": "2026-06-07T18:00:00"},
]
order = [t["id"] for t in s.rank_active(tasks, NOW)]
assert order[0] == "tight", order
def test_completed_tasks_excluded():
tasks = [
{"id": "done", "impact": 10, "completed": True},
{"id": "live", "impact": 4},
]
order = [t["id"] for t in s.rank_active(tasks, NOW)]
assert order == ["live"]
def test_due_label():
assert s.due_label("2026-06-08T12:00:00", NOW) == "tomorrow"
assert s.due_label("2026-06-12T12:00:00", NOW) == "5d"
assert s.due_label("2026-06-05T12:00:00", NOW) == "2d late"
if __name__ == "__main__":
import sys
fns = [v for k, v in sorted(globals().items()) if k.startswith("test_") and callable(v)]
failed = 0
for fn in fns:
try:
fn()
print(f" ok {fn.__name__}")
except AssertionError as e:
failed += 1
print(f" FAIL {fn.__name__}: {e}")
print(f"\n{len(fns) - failed}/{len(fns)} passed")
sys.exit(1 if failed else 0)