Files
Meal-Planner/backend/tests/test_planner_generate.py
T
admin 2695ba0232
CI / backend (pytest + alembic) (push) Canceled after 0s
CI / frontend (build) (push) Canceled after 0s
feat(meals): enforce three lower-calorie dinners with media
2026-09-01 10:11:36 -07:00

180 lines
5.8 KiB
Python

from datetime import date
from decimal import Decimal
from uuid import UUID, uuid4
import pytest
pytestmark = pytest.mark.requires_postgres
def test_generate_meal_plan_against_seeded_data():
"""Smoke test: with the seeded 30 recipes and a seeded grocery scrape,
generate produces a MealPlan with up to 3 items.
"""
from datetime import datetime, timezone
from app.database import SessionLocal
from app.models import (
FamilyProfile,
GroceryItem,
Ingredient,
IngredientGroceryMatch,
IngredientMatchSource,
MealPlan,
MealPlanItem,
)
from app.services.matcher import run_match_job
from app.services.planner.generate import generate_meal_plan
setup = SessionLocal()
try:
family = FamilyProfile(
id=uuid4(),
name="Generate Smoke Family",
household_size=4,
adult_count=2,
child_count=2,
calorie_target=500,
)
setup.add(family)
setup.commit()
family_id = family.id
# Seed at least one grocery_item per ingredient that the recipes use
# so cost matching can find prices. Pick a handful to keep runtime low.
for ing in setup.query(Ingredient).limit(20).all():
setup.add(
GroceryItem(
id=uuid4(),
name=ing.name,
source="lucky_california",
external_id=f"ext-test-{ing.id}",
current_price=Decimal("3.99"),
regular_price=Decimal("4.99"),
is_on_sale=True,
scraped_at=datetime.now(timezone.utc),
)
)
setup.commit()
finally:
setup.close()
work = SessionLocal()
try:
run_match_job(work)
result = generate_meal_plan(work, family_id=family_id, week_start_date=date(2026, 5, 11))
assert result.meal_plan_id is not None
assert 1 <= len(result.selected) <= 3 # at least 1, at most 3
plan = work.query(MealPlan).filter(MealPlan.id == result.meal_plan_id).first()
assert plan is not None
items = work.query(MealPlanItem).filter(MealPlanItem.meal_plan_id == plan.id).all()
assert len(items) == len(result.selected)
finally:
cleanup = SessionLocal()
try:
cleanup.query(MealPlanItem).filter(
MealPlanItem.meal_plan_id.in_(
[r[0] for r in cleanup.query(MealPlan.id).filter(MealPlan.family_profile_id == family_id).all()]
)
).delete(synchronize_session=False)
cleanup.query(MealPlan).filter(MealPlan.family_profile_id == family_id).delete(synchronize_session=False)
cleanup.query(FamilyProfile).filter(FamilyProfile.id == family_id).delete(synchronize_session=False)
cleanup.query(GroceryItem).filter(GroceryItem.external_id.like("ext-test-%")).delete(synchronize_session=False)
cleanup.query(IngredientGroceryMatch).filter(
IngredientGroceryMatch.source == IngredientMatchSource.AUTO
).delete(synchronize_session=False)
cleanup.commit()
finally:
cleanup.close()
work.close()
def test_generate_meal_plan_uses_three_active_calorie_eligible_recipes(db):
from app.models import (
FamilyProfile,
MealPlanItem,
MealType,
Recipe,
RecipeCatalogStatus,
)
from app.services.planner.generate import generate_meal_plan
db.query(Recipe).update(
{Recipe.catalog_status: RecipeCatalogStatus.QUARANTINED},
synchronize_session=False,
)
family = FamilyProfile(
id=uuid4(),
name="Three Dinner Family",
household_size=2,
adult_count=2,
child_count=0,
calorie_target=400,
planner_config={"set_size": 7},
)
db.add(family)
eligible = [
Recipe(
id=uuid4(),
name=f"Lower Calorie Dinner {index}",
servings=4,
calories_per_serving=calories,
prep_time_minutes=10,
cook_time_minutes=20,
ingredients=[],
instructions=["Cook."],
catalog_status=RecipeCatalogStatus.ACTIVE,
)
for index, calories in enumerate((360, 400, 440), start=1)
]
high_calorie = Recipe(
id=uuid4(),
name="Old High Calorie Dinner",
servings=4,
calories_per_serving=700,
prep_time_minutes=10,
cook_time_minutes=20,
ingredients=[],
instructions=["Cook."],
catalog_status=RecipeCatalogStatus.ACTIVE,
)
quarantined = Recipe(
id=uuid4(),
name="Quarantined Legacy Dinner",
servings=4,
calories_per_serving=400,
prep_time_minutes=10,
cook_time_minutes=20,
ingredients=[],
instructions=["Cook."],
catalog_status=RecipeCatalogStatus.QUARANTINED,
)
db.add_all([*eligible, high_calorie, quarantined])
db.flush()
result = generate_meal_plan(
db,
family_id=family.id,
week_start_date=date(2026, 8, 31),
today=date(2026, 8, 31),
)
items = (
db.query(MealPlanItem)
.filter(MealPlanItem.meal_plan_id == result.meal_plan_id)
.order_by(MealPlanItem.day_of_week)
.all()
)
assert {selected.recipe_id for selected in result.selected} == {
recipe.id for recipe in eligible
}
assert [(item.day_of_week, item.meal_type) for item in items] == [
(1, MealType.DINNER),
(3, MealType.DINNER),
(5, MealType.DINNER),
]
assert high_calorie.id not in {item.recipe_id for item in items}
assert quarantined.id not in {item.recipe_id for item in items}
assert result.rejected_summary["calories"] == 1