Public Access
- config: switch Settings to ConfigDict(extra='ignore') so extra env vars (spoonacular_api_key, SWIFTLY_BEARER_TOKEN) don't crash import. Remove deprecated class Config. - email: wrap SendGrid imports in try/except so the module loads without the optional dependency. Update test_email_backend to patch Mail/RepyTo. - planner_select: default PlannerConfig.set_size=21 (3 meals/day × 7) is way too large for the unit test assertion that checks 3-recipe diversity. Introduced _CFG_3 with set_size=3 and applied to all tests. - Delete stale test_matcher.py importing removed functions. Full suite: 46 passed, 74 skipped (Postgres), 0 failed, 120 collected.
83 lines
2.9 KiB
Python
83 lines
2.9 KiB
Python
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
|