feat(meals): enforce three lower-calorie dinners with media
CI / backend (pytest + alembic) (push) Canceled after 0s
CI / frontend (build) (push) Canceled after 0s

This commit is contained in:
2026-09-01 10:11:36 -07:00
parent 7838c49721
commit 2695ba0232
85 changed files with 8103 additions and 795 deletions
+246
View File
@@ -11,6 +11,37 @@ pytestmark = pytest.mark.requires_postgres
WEEK = date(2026, 5, 8) # a Friday
def _add_quarantined_item(db, meal_plan, *, name, approval_status, day_of_week):
from app.models import (
MealPlanItem,
MealType,
Recipe,
RecipeCatalogStatus,
)
recipe = Recipe(
id=uuid4(),
name=name,
servings=4,
ingredients=[{"name": "legacy ingredient", "qty": "1", "unit": "cup"}],
instructions=["Do not include this legacy recipe."],
catalog_status=RecipeCatalogStatus.QUARANTINED,
)
db.add(recipe)
db.flush()
item = MealPlanItem(
id=uuid4(),
meal_plan_id=meal_plan.id,
recipe_id=recipe.id,
day_of_week=day_of_week,
meal_type=MealType.DINNER,
approval_status=approval_status,
)
db.add(item)
db.flush()
return item
@pytest.fixture()
def family(db):
from app.models import FamilyProfile
@@ -339,6 +370,38 @@ def test_step_email_escapes_recipe_name(
assert "<script>" in sent[0]["html"]
def test_step_email_suppresses_quarantined_recipes(
db, weekly_run_generated, meal_plan, pending_item, member, monkeypatch
):
from app.models import MealPlanItemStatus
_add_quarantined_item(
db,
meal_plan,
name="Legacy Mango Pork",
approval_status=MealPlanItemStatus.pending,
day_of_week=6,
)
sent = []
class FakeBackend:
def send(self, **kwargs):
sent.append(kwargs)
monkeypatch.setattr(
"app.services.orchestrator.steps.get_email_backend",
lambda: FakeBackend(),
)
from app.services.orchestrator.steps import step_email
step_email(weekly_run_generated, db)
assert len(sent) == 1
assert "Test Pasta" in sent[0]["html"]
assert "Legacy Mango Pork" not in sent[0]["html"]
assert "legacy ingredient" not in sent[0]["html"]
# ── step_reminder ──────────────────────────────────────────────────────────
@@ -447,6 +510,37 @@ def test_step_reminder_no_sends_when_all_voted(
assert weekly_run_emailed.reminded_at is not None
def test_step_reminder_suppresses_quarantined_recipes(
db, weekly_run_emailed, meal_plan, pending_item, member, monkeypatch
):
from app.models import MealPlanItemStatus
_add_quarantined_item(
db,
meal_plan,
name="Legacy Mango Pork",
approval_status=MealPlanItemStatus.pending,
day_of_week=6,
)
sent = []
class FakeBackend:
def send(self, **kwargs):
sent.append(kwargs)
monkeypatch.setattr(
"app.services.orchestrator.steps.get_email_backend",
lambda: FakeBackend(),
)
from app.services.orchestrator.steps import step_reminder
step_reminder(weekly_run_emailed, db)
assert len(sent) == 1
assert "Test Pasta" in sent[0]["html"]
assert "Legacy Mango Pork" not in sent[0]["html"]
# ---------------------------------------------------------------------------
# step_deadline tests
# ---------------------------------------------------------------------------
@@ -573,6 +667,158 @@ def test_step_finalize_no_approved_sends_empty_message(db, weekly_run_deadline_p
assert "No meals were approved" in sent[0]["html"]
def test_step_finalize_suppresses_quarantined_recipes(
db, weekly_run_deadline_passed, meal_plan, approved_item, member, monkeypatch
):
from app.models import MealPlanItemStatus
_add_quarantined_item(
db,
meal_plan,
name="Legacy Mango Pork",
approval_status=MealPlanItemStatus.approved,
day_of_week=7,
)
sent = []
class FakeBackend:
def send(self, **kwargs):
sent.append(kwargs)
monkeypatch.setattr(
"app.services.orchestrator.steps.get_email_backend",
lambda: FakeBackend(),
)
from app.services.orchestrator.steps import step_finalize
step_finalize(weekly_run_deadline_passed, db)
assert len(sent) == 1
assert "Approved Chicken" in sent[0]["html"]
assert "Legacy Mango Pork" not in sent[0]["html"]
assert "legacy ingredient" not in sent[0]["html"]
def test_step_finalize_stages_candidates_without_publishing(
db, weekly_run_deadline_passed, meal_plan, member, monkeypatch
):
from types import SimpleNamespace
from app.models import Recipe, RecipeImportStaging, RecipeImportStatus
class FakeBackend:
def send(self, **kwargs):
pass
analysis = SimpleNamespace(discovery_queries=["apple dinner"], confidence=0.9)
analysis.to_dict = lambda: {
"discovery_queries": analysis.discovery_queries,
"confidence": analysis.confidence,
}
monkeypatch.setattr(
"app.services.orchestrator.steps.get_email_backend", lambda: FakeBackend()
)
monkeypatch.setattr(
"app.services.orchestrator.steps.FeedbackAnalyzer.analyze",
lambda self, database, family_id: analysis,
)
external_id = str(uuid4())
def stage(service, database, limit, queries):
assert queries == ["apple dinner"]
row = RecipeImportStaging(
source="wikibooks",
external_id=external_id,
source_url="https://en.wikibooks.org/wiki/Cookbook:Finalize_Test",
source_revision=str(uuid4()),
license_name="CC BY-SA 4.0",
license_url="https://creativecommons.org/licenses/by-sa/4.0/",
attribution="Wikibooks contributors",
raw_payload={"title": "Cookbook:Finalize Test"},
payload_sha256="a" * 64,
normalized_recipe={"name": "Finalize Test"},
validation_report={"valid": True, "errors": [], "warnings": []},
status=RecipeImportStatus.PENDING,
)
database.add(row)
database.flush()
return [row]
monkeypatch.setattr(
"app.services.orchestrator.steps.WikibooksImportService.stage_discovered_pages",
stage,
)
before = db.query(Recipe).count()
from app.services.orchestrator.steps import step_finalize
step_finalize(weekly_run_deadline_passed, db)
assert weekly_run_deadline_passed.status == "completed"
assert (
db.query(RecipeImportStaging).filter_by(external_id=external_id).count() == 1
)
assert db.query(Recipe).count() == before
def test_step_finalize_rolls_back_partial_staging_failure(
db, weekly_run_deadline_passed, meal_plan, member, monkeypatch
):
from types import SimpleNamespace
from app.models import RecipeImportStaging, RecipeImportStatus
class FakeBackend:
def send(self, **kwargs):
pass
analysis = SimpleNamespace(discovery_queries=["apple dinner"], confidence=0.9)
analysis.to_dict = lambda: {
"discovery_queries": analysis.discovery_queries,
"confidence": analysis.confidence,
}
monkeypatch.setattr(
"app.services.orchestrator.steps.get_email_backend", lambda: FakeBackend()
)
monkeypatch.setattr(
"app.services.orchestrator.steps.FeedbackAnalyzer.analyze",
lambda self, database, family_id: analysis,
)
external_id = str(uuid4())
def stage_then_fail(service, database, limit, queries):
assert queries == ["apple dinner"]
database.add(
RecipeImportStaging(
source="wikibooks",
external_id=external_id,
source_url="https://en.wikibooks.org/wiki/Cookbook:Partial_Test",
source_revision=str(uuid4()),
license_name="CC BY-SA 4.0",
license_url="https://creativecommons.org/licenses/by-sa/4.0/",
attribution="Wikibooks contributors",
raw_payload={"title": "Cookbook:Partial Test"},
payload_sha256="b" * 64,
normalized_recipe={"name": "Partial Test"},
validation_report={"valid": True, "errors": [], "warnings": []},
status=RecipeImportStatus.PENDING,
)
)
database.flush()
raise RuntimeError("upstream failed after first page")
monkeypatch.setattr(
"app.services.orchestrator.steps.WikibooksImportService.stage_discovered_pages",
stage_then_fail,
)
from app.services.orchestrator.steps import step_finalize
step_finalize(weekly_run_deadline_passed, db)
assert weekly_run_deadline_passed.status == "completed"
assert (
db.query(RecipeImportStaging).filter_by(external_id=external_id).count() == 0
)
# ---------------------------------------------------------------------------
# runner tests
# ---------------------------------------------------------------------------