Files
mealie-mcp/tests/test_verify.py
T
fredamn76 e5c134b045 Initial commit: Mealie MCP server
Ports a working Hermes agent skill to an MCP server. The skill's value was not
its Mealie endpoints but ~25 operational rules found by running imports against
the live instance; those are now code with tests rather than prompt text.

The server owns deterministic mechanics — auth, the non-default User-Agent
Cloudflare requires, the `extension` field on cover uploads, PATCH instead of
the 500-ing bulk-action endpoints, decimal-comma normalization, and restoring
Swedish display text after parsing. Language and taste judgement stay with the
model, fed by the four reference notes shipped as MCP resources.

verify_recipe runs the finished-import definition as code so an agent cannot
report success on a recipe with an English ingredient line, a missing cover, or
ingredients still in the "Click Parse" state.

Home Assistant is optional and isolated; the Obsidian meal plan is out of scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-31 07:31:10 +02:00

140 lines
5.3 KiB
Python
Raw 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",
}
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
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 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