Fastwhisper / tests /test_schedule_math.py
Mbonea's picture
Close P1 arch-code gaps: loop summary, template-echo Rules, Move now, proof kinds.
57ed4c2
Raw
History Blame Contribute Delete
4.07 kB
"""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"