Public Access
108 lines
2.9 KiB
Python
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
|