Files

108 lines
2.9 KiB
Python

from datetime import date, timedelta
from decimal import Decimal
from uuid import uuid4
from app.services.planner.config import PlannerConfig
from app.services.planner.score import (
score_recipes,
time_bonus,
recency_bonus,
normalize_savings,
)
from app.services.planner.types import RecipeCost
_CFG = PlannerConfig()
def test_time_bonus_capped_at_ideal():
assert time_bonus(20, _CFG) == 1.0
assert time_bonus(25, _CFG) == 1.0
def test_time_bonus_decays_to_zero_at_full():
assert time_bonus(45, _CFG) == 0.0
def test_time_bonus_linear_midpoint():
# 35 min is halfway between 25 and 45
assert abs(time_bonus(35, _CFG) - 0.5) < 1e-6
def test_recency_bonus_full_when_long_ago():
today = date(2026, 5, 5)
long_ago = today - timedelta(weeks=20)
assert recency_bonus(long_ago, today, _CFG) == 1.0
def test_recency_bonus_zero_when_just_eligible():
# right at recency_weeks boundary → 0
today = date(2026, 5, 5)
cutoff = today - timedelta(weeks=_CFG.recency_weeks)
assert recency_bonus(cutoff, today, _CFG) == 0.0
def test_recency_bonus_full_when_never_cooked():
assert recency_bonus(None, date(2026, 5, 5), _CFG) == 1.0
def test_normalize_savings_minmax():
out = normalize_savings([Decimal("0"), Decimal("5"), Decimal("10")])
assert out == [0.0, 0.5, 1.0]
def test_normalize_savings_uniform_returns_zeros():
out = normalize_savings([Decimal("3"), Decimal("3"), Decimal("3")])
assert out == [0.0, 0.0, 0.0]
def test_score_recipes_orders_by_combined_score():
recipes = [
{
"id": uuid4(),
"name": "low_savings",
"prep_time_minutes": 10,
"cook_time_minutes": 30,
"protein_type": "chicken",
"cuisine_tags": ["american"],
},
{
"id": uuid4(),
"name": "high_savings",
"prep_time_minutes": 10,
"cook_time_minutes": 15, # also lower time
"protein_type": "beef",
"cuisine_tags": ["mexican"],
},
]
costs = {
recipes[0]["id"]: RecipeCost(
recipe_id=recipes[0]["id"],
total_cost=Decimal("10"),
total_savings=Decimal("1"),
sale_ingredient_count=1,
matched_ingredient_count=4,
total_ingredient_count=4,
pantry_hit_count=0,
line_items=[],
),
recipes[1]["id"]: RecipeCost(
recipe_id=recipes[1]["id"],
total_cost=Decimal("12"),
total_savings=Decimal("8"),
sale_ingredient_count=3,
matched_ingredient_count=4,
total_ingredient_count=4,
pantry_hit_count=2,
line_items=[],
),
}
scored = score_recipes(
recipes=recipes,
recipe_costs=costs,
last_cooked_at={},
config=_CFG,
today=date(2026, 5, 5),
)
assert scored[0].recipe_id == recipes[1]["id"] # high_savings first