Spaces:
Sleeping
Sleeping
File size: 4,067 Bytes
c6253b2 57ed4c2 c6253b2 | 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 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | """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"
|