Spaces:
Sleeping
Sleeping
| """Unit tests for schedule math: d_hat, overlaps, P0 lock, strong feedback.""" | |
| from app.schedule_math import ( | |
| d_hat, | |
| is_strong_feedback, | |
| overlaps, | |
| recompute_priors, | |
| validate_blocks, | |
| ) | |
| from app.schedule_reschedule import rules_backup_reschedule | |
| def test_d_hat_shrinkage() -> None: | |
| # n=3, mean=40, prior=30, m=3 → 0.5*40 + 0.5*30 = 35 | |
| assert abs(d_hat(40.0, 3, 30.0, m=3) - 35.0) < 1e-9 | |
| assert d_hat(None, 0, 45.0, m=3) == 45.0 | |
| def test_strong_feedback() -> None: | |
| assert is_strong_feedback( | |
| {"did": "done", "actual_min": 20, "quality": 4, "fun": 3} | |
| ) | |
| assert not is_strong_feedback({"did": "skipped", "actual_min": 20, "quality": 4, "fun": 3}) | |
| assert not is_strong_feedback({"did": "done", "actual_min": None, "quality": 4, "fun": 3}) | |
| def test_overlap_and_p0_lock() -> None: | |
| assert overlaps("09:00", "10:00", "09:30", "10:30") | |
| assert not overlaps("09:00", "10:00", "10:00", "11:00") | |
| blocks = [ | |
| { | |
| "id": "a", | |
| "start": "09:00", | |
| "end": "10:00", | |
| "planned_min": 60, | |
| "priority": "P0", | |
| "locked": True, | |
| } | |
| ] | |
| prev = [{"id": "a", "start": "09:00", "end": "10:00", "priority": "P0", "locked": True}] | |
| moved = [ | |
| { | |
| "id": "a", | |
| "start": "11:00", | |
| "end": "12:00", | |
| "planned_min": 60, | |
| "priority": "P0", | |
| "locked": True, | |
| } | |
| ] | |
| errors_moved, _ = validate_blocks(moved, previous_p0=prev, allow_p0_move=False) | |
| errors_ok, _ = validate_blocks(blocks, previous_p0=prev, allow_p0_move=False) | |
| assert errors_moved | |
| assert not errors_ok | |
| def test_all_grind_soft_warn() -> None: | |
| duty = [ | |
| { | |
| "id": "a", | |
| "start": "09:00", | |
| "end": "10:00", | |
| "planned_min": 60, | |
| "intent": "duty", | |
| "priority": "P1", | |
| } | |
| ] | |
| errors, warnings = validate_blocks( | |
| duty, | |
| must_include_explore_or_restore=True, | |
| capacity_hint=0.7, | |
| hard_explore=False, | |
| ) | |
| assert not errors | |
| assert any("all-grind" in w for w in warnings) | |
| errors_hard, _ = validate_blocks( | |
| duty, | |
| must_include_explore_or_restore=True, | |
| capacity_hint=0.7, | |
| hard_explore=True, | |
| ) | |
| assert any("all-grind" in e for e in errors_hard) | |
| def test_recompute_priors() -> None: | |
| blocks = { | |
| "b1": {"id": "b1", "kind": "earn_ship", "planned_min": 45}, | |
| } | |
| feedback = [ | |
| { | |
| "block_id": "b1", | |
| "did": "done", | |
| "actual_min": 50, | |
| "quality": 4, | |
| "fun": 2, | |
| "strong": True, | |
| "would_repeat": "yes", | |
| } | |
| ] | |
| priors = recompute_priors(feedback, blocks, shrink_k=3) | |
| assert priors["earn_ship"]["n"] == 1 | |
| assert priors["earn_ship"]["d_hat"] > 0 | |
| def test_rules_backup_keeps_p0() -> None: | |
| blocks = [ | |
| { | |
| "id": "p0", | |
| "date": "2026-07-19", | |
| "start": "08:00", | |
| "end": "08:30", | |
| "title": "Food", | |
| "kind": "food_out", | |
| "intent": "duty", | |
| "priority": "P0", | |
| "planned_min": 30, | |
| "status": "planned", | |
| "locked": True, | |
| "notes": "", | |
| "version_added": 1, | |
| "source": "user", | |
| }, | |
| { | |
| "id": "p2", | |
| "date": "2026-07-19", | |
| "start": "14:00", | |
| "end": "16:00", | |
| "title": "Flex", | |
| "kind": "other", | |
| "intent": "duty", | |
| "priority": "P2", | |
| "planned_min": 120, | |
| "status": "planned", | |
| "locked": False, | |
| "notes": "", | |
| "version_added": 1, | |
| "source": "user", | |
| }, | |
| ] | |
| out = rules_backup_reschedule( | |
| blocks, | |
| day="2026-07-19", | |
| risk_score=2.0, | |
| capacity_hint=0.4, | |
| ) | |
| p0 = next(b for b in out if b.id == "p0") | |
| assert p0.start == "08:00" and p0.end == "08:30" | |