Files
mealie-mcp/tests/test_verify.py
fredamn76 2846289df1 Treat Mealie's "no image" sentinel as a missing cover
Mealie stores the literal string "no image" in the recipe's image field when
there is no cover, so bool(recipe["image"]) reported a cover that is not
there: the check meant to catch a missing image downgraded to a warning
instead of failing. Observed on a recipe created through Mealie's own image
import, which returned image: "no image".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-18 12:55:27 +02:00

167 lines
6.6 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""The finished-import checklist must fail loudly on every verified failure mode."""
from __future__ import annotations
import pytest
from mealie_mcp.verify import is_parsed, verify_recipe
def complete_recipe(**overrides):
"""A recipe that satisfies every check, so each test can break exactly one thing."""
recipe = {
"name": "Kycklinggryta med kokos",
"slug": "kycklinggryta-med-kokos",
"description": "Krämig gryta med kokosmjölk och röd curry.",
"recipeInstructions": [{"text": "Bryn kycklingen."}, {"text": "Häll i kokosmjölken."}],
"recipeIngredient": [
{
"display": "400 g kycklinglårfilé",
"quantity": 400,
"unit": {"id": "u1", "name": "g"},
"food": {"id": "f1", "name": "kycklinglårfilé"},
},
{
"display": "1 burk kokosmjölk",
"quantity": 1,
"unit": {"id": "u2", "name": "burk"},
"food": {"id": "f2", "name": "kokosmjölk"},
},
],
"image": "abc123",
"recipeCategory": [{"name": "Huvudrätter"}],
"tags": [{"name": "Källa: Köket"}],
"orgURL": "https://www.koket.se/kycklinggryta",
"recipeServings": 4,
"nutrition": {"calories": "520", "proteinContent": "31", "fatContent": "34",
"carbohydrateContent": "18"},
}
recipe.update(overrides)
return recipe
def status_of(report, check):
return next(c["status"] for c in report["checks"] if c["check"] == check)
class TestCompleteRecipe:
def test_fully_finished_import_passes(self):
report = verify_recipe(complete_recipe(), image_verified=True)
assert report["complete"] is True
assert report["failed"] == []
class TestCoverImage:
def test_image_field_alone_is_only_a_warning_not_a_pass(self):
# Verified: a non-empty image field does not prove the UI shows a cover.
report = verify_recipe(complete_recipe(), image_verified=False)
assert status_of(report, "cover_image") == "warn"
def test_missing_image_fails(self):
report = verify_recipe(complete_recipe(image=None))
assert status_of(report, "cover_image") == "fail"
assert report["complete"] is False
@pytest.mark.parametrize("stored", ["no image", "No Image", " no image ", ""])
def test_mealies_no_image_sentinel_fails(self, stored):
# Verified against Mealie 3.22: a recipe without a cover stores the
# string "no image", which a truthiness test reads as a cover present.
report = verify_recipe(complete_recipe(image=stored))
assert status_of(report, "cover_image") == "fail"
assert report["complete"] is False
class TestSwedishContent:
def test_english_ingredient_line_fails(self):
recipe = complete_recipe()
recipe["recipeIngredient"][0]["display"] = "2 cups flour"
report = verify_recipe(recipe, image_verified=True)
assert status_of(report, "swedish_ingredients") == "fail"
assert report["complete"] is False
def test_english_instructions_fail(self):
recipe = complete_recipe(recipeInstructions=[{"text": "Add 2 tbsp of butter"}])
report = verify_recipe(recipe, image_verified=True)
assert status_of(report, "swedish_prose") == "fail"
def test_swedish_title_only_is_not_enough(self):
# Rule: translation is not done until the ingredient lines themselves are Swedish.
recipe = complete_recipe()
recipe["recipeIngredient"][1]["display"] = "1 clove garlic"
report = verify_recipe(recipe, image_verified=True)
assert report["complete"] is False
class TestParseState:
def test_unstructured_ingredients_leave_recipe_unparsed(self):
recipe = complete_recipe(
recipeIngredient=[{"display": "400 g kycklinglårfilé"}, {"display": "1 burk kokosmjölk"}]
)
assert is_parsed(recipe) is False
report = verify_recipe(recipe, image_verified=True)
assert status_of(report, "ingredients_parsed") == "fail"
def test_empty_recipe_is_not_parsed(self):
assert is_parsed({"recipeIngredient": []}) is False
def test_structured_ingredients_are_parsed(self):
assert is_parsed(complete_recipe()) is True
class TestReadability:
def test_parser_mangled_display_fails_even_when_structured(self):
# Structure is worthless if the human-facing line is garbage.
recipe = complete_recipe()
recipe["recipeIngredient"][0]["display"] = "4710 liter basmatiris"
report = verify_recipe(recipe, image_verified=True)
assert status_of(report, "ingredient_lines_readable") == "fail"
class TestExtractionPlaceholders:
def test_could_not_detect_placeholder_fails(self):
recipe = complete_recipe()
recipe["recipeIngredient"][0]["display"] = "Could not detect ingredients"
report = verify_recipe(recipe, image_verified=True)
assert status_of(report, "no_extraction_placeholders") == "fail"
class TestTaxonomy:
def test_untagged_recipe_fails_without_a_reason(self):
report = verify_recipe(
complete_recipe(recipeCategory=[], tags=[]), image_verified=True
)
assert status_of(report, "taxonomy") == "fail"
def test_deliberate_skip_passes(self):
report = verify_recipe(
complete_recipe(recipeCategory=[], tags=[]),
image_verified=True,
taxonomy_skipped_reason="Fredrik sorterar den manuellt",
)
assert status_of(report, "taxonomy") == "pass"
assert report["complete"] is True
class TestNutrition:
def test_missing_nutrition_fails(self):
report = verify_recipe(complete_recipe(nutrition={}), image_verified=True)
assert status_of(report, "nutrition") == "fail"
assert report["complete"] is False
def test_values_without_a_serving_count_fail(self):
# Mealie shows nutrition per serving, so figures with recipeServings 0
# are a numerator without a denominator — the state the TikTok import left.
report = verify_recipe(complete_recipe(recipeServings=0), image_verified=True)
assert status_of(report, "nutrition") == "fail"
assert "recipeServings" in next(
c["detail"] for c in report["checks"] if c["check"] == "nutrition"
)
class TestAttribution:
def test_missing_source_warns_but_does_not_block(self):
report = verify_recipe(
complete_recipe(orgURL=None, extras={}), image_verified=True
)
assert status_of(report, "attribution") == "warn"
assert report["complete"] is True