from decimal import Decimal from uuid import uuid4 from app.services.planner.config import PlannerConfig from app.services.planner.select import select_set, set_diversity_penalty from app.services.planner.types import RecipeCost, ScoredRecipe _CFG = PlannerConfig() _CFG_3 = PlannerConfig(w_savings=0.30, w_coverage=0.25, w_pantry=0.10, w_time=0.15, w_recency=0.20, set_size=3, top_k=20, p_protein=0.15, p_cuisine=0.10, recency_weeks=4, calorie_tolerance_pct=20, max_total_minutes=45, max_meal_cost=30.00, time_ideal_minutes=25, time_full_minutes=45, recency_full_weeks=12) def _mk(score, protein, cuisine): rid = uuid4() return ScoredRecipe( recipe_id=rid, score=score, components={}, cost=RecipeCost( recipe_id=rid, total_cost=Decimal("10"), total_savings=Decimal("1"), sale_ingredient_count=1, matched_ingredient_count=1, total_ingredient_count=1, pantry_hit_count=0, line_items=[], ), protein=protein, cuisine=cuisine, ) def test_diversity_penalty_zero_when_all_unique(): a = _mk(0.5, "chicken", "american") b = _mk(0.5, "beef", "mexican") c = _mk(0.5, "fish", "italian") assert set_diversity_penalty([a, b, c], _CFG_3) == 0.0 def test_diversity_penalty_three_chickens(): a = _mk(0.5, "chicken", "american") b = _mk(0.5, "chicken", "italian") c = _mk(0.5, "chicken", "mexican") # 3 protein pairs * 0.15 = 0.45, no cuisine pairs p = set_diversity_penalty([a, b, c], _CFG_3) assert abs(p - 0.45) < 1e-6 def test_select_set_picks_diverse_over_homogeneous(): """Three chicken/american (each 0.95) lose to fully-diverse mixed (each 0.90) once protein and cuisine penalties apply. Mix candidates each overlap high on exactly one attribute so that *any* "1 high + 2 mix" pairing also incurs penalty -- otherwise the optimum would mix a single high with two unrelated mixes.""" high1 = _mk(0.95, "chicken", "american") high2 = _mk(0.95, "chicken", "american") high3 = _mk(0.95, "chicken", "american") mix1 = _mk(0.90, "pork", "thai") # fully distinct from high mix2 = _mk(0.90, "chicken", "mexican") # shares protein with high mix3 = _mk(0.90, "fish", "american") # shares cuisine with high chosen, set_score = select_set([high1, high2, high3, mix1, mix2, mix3], _CFG_3) chosen_ids = {s.recipe_id for s in chosen} assert chosen_ids == {mix1.recipe_id, mix2.recipe_id, mix3.recipe_id} def test_select_set_handles_too_few(): a = _mk(0.5, "x", "y") b = _mk(0.5, "x", "y") chosen, _ = select_set([a, b], _CFG_3) assert len(chosen) == 2 # less than set_size returns what we have def test_select_set_empty_input(): chosen, score = select_set([], _CFG_3) assert chosen == [] assert score == 0.0