Files
Fizz ddc0fb1a5e Fix three bugs found by running against the live instance
suggest_recipes returned 422 for every call. The endpoint takes food UUIDs as
repeated query parameters, not a comma-joined string of names — the rule
inherited from the skill was never actually verified. Added resolve_foods to
map names to food records first, and report anything unresolved instead of
dropping it.

scale_ingredients silently returned unscaled lines. It read the structured
quantity field and string-replaced str(quantity) into the display text, which
failed twice over: unparsed recipes store quantity 0.0 with the real amount only
in the text, and str(800.0) never matches "800 g kycklinglårfilé". Amounts are
now read from the display text, ranges scale at both ends ("1-1,5 msk"), mixed
numbers scale as one value ("1 1/2 msk"), and lines with no amount are reported
in `unscaled` rather than presented as if they had been scaled.

looks_mangled flagged the readable "1 1/2 msk tomatpuré" because its duplicate-word
rule matched the repeated digit. Restricted to alphabetic words, so it still
catches "2 dl dl grädde".

Verified against all 238 recipes in the live library: no false positives remain.

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

126 lines
5.1 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.
"""Scaling must never present an unscaled line as scaled.
The original implementation read the structured `quantity` field and string-replaced
`str(quantity)` into the display text. Against the live library that silently did
nothing twice over: unparsed recipes store `quantity: 0.0` with the real amount only
in the text, and `str(800.0)` == "800.0" never matches "800 g kycklinglårfilé".
The result was a confident wrong answer — the worst kind for a shopping list.
"""
from __future__ import annotations
from decimal import Decimal
import pytest
from mealie_mcp.normalize import format_amount, scale_line
from mealie_mcp.server import scale_ingredients
class TestFormatAmount:
def test_whole_numbers_lose_the_decimal(self):
assert format_amount(Decimal("1200")) == "1200"
def test_fractions_use_the_swedish_decimal_comma(self):
assert format_amount(Decimal("1.5")) == "1,5"
def test_rounds_to_one_decimal(self):
assert format_amount(Decimal("0.66")) == "0,7"
class TestScaleLine:
def test_scales_a_leading_gram_amount(self):
assert scale_line("800 g kycklinglårfilé", Decimal("1.5")) == ("1200 g kycklinglårfilé", True)
def test_scales_a_swedish_decimal_amount(self):
assert scale_line("4,7 dl basmatiris", Decimal("2")) == ("9,4 dl basmatiris", True)
def test_scales_a_discrete_item(self):
assert scale_line("1 klyfta vitlök", Decimal("3")) == ("3 klyfta vitlök", True)
def test_line_without_an_amount_is_reported_as_unscaled(self):
assert scale_line("salt", Decimal("2")) == ("salt", False)
def test_parenthetical_line_without_amount_is_untouched(self):
assert scale_line("smör (att steka i)", Decimal("2")) == ("smör (att steka i)", False)
def test_does_not_touch_a_number_that_is_not_leading(self):
# "Cheddar 40%" must not become "Cheddar 80%".
assert scale_line("cheddar 40% fetthalt", Decimal("2")) == ("cheddar 40% fetthalt", False)
class TestScaleIngredients:
def _recipe(self, **overrides):
recipe = {
"name": "Provencalsk kycklinggryta",
"recipeYield": "",
"recipeServings": 4.0,
"recipeYieldQuantity": 0.0,
"recipeIngredient": [
# Verified live shape: unparsed rows carry quantity 0.0.
{"display": "800 g kycklinglårfilé", "quantity": 0.0},
{"display": "salt", "quantity": 0.0},
{"display": "1 klyfta vitlök", "quantity": 0.0},
],
}
recipe.update(overrides)
return recipe
def test_scales_from_recipe_servings_when_yield_is_empty(self, monkeypatch):
monkeypatch.setattr("mealie_mcp.server.get_recipe", lambda _s: self._recipe())
result = scale_ingredients("x", 6)
assert result["factor"] == 1.5
assert result["ingredients"][0] == "1200 g kycklinglårfilé"
assert result["ingredients"][2] == "1,5 klyfta vitlök"
def test_lines_without_amounts_are_listed_not_hidden(self, monkeypatch):
monkeypatch.setattr("mealie_mcp.server.get_recipe", lambda _s: self._recipe())
assert scale_ingredients("x", 6)["unscaled"] == ["salt"]
def test_falls_back_to_a_numeric_leading_recipe_yield(self, monkeypatch):
recipe = self._recipe(recipeServings=0.0, recipeYield="4 portioner")
monkeypatch.setattr("mealie_mcp.server.get_recipe", lambda _s: recipe)
assert scale_ingredients("x", 8)["factor"] == 2.0
def test_refuses_to_guess_without_serving_metadata(self, monkeypatch):
recipe = self._recipe(recipeServings=0.0, recipeYield="", recipeYieldQuantity=0.0)
monkeypatch.setattr("mealie_mcp.server.get_recipe", lambda _s: recipe)
with pytest.raises(ValueError, match="serving metadata"):
scale_ingredients("x", 6)
class TestRanges:
"""Live data: 'Provencalsk kycklinggryta' stores '1-1,5 msk koncentrerad kycklingfond'."""
def test_both_ends_of_a_range_are_scaled(self):
assert scale_line("1-1,5 msk koncentrerad kycklingfond", Decimal("1.5")) == (
"1,5-2,3 msk koncentrerad kycklingfond",
True,
)
def test_integer_range_stays_readable(self):
assert scale_line("2-3 dl vatten", Decimal("2")) == ("4-6 dl vatten", True)
def test_en_dash_range_is_handled(self):
assert scale_line("12 st lök", Decimal("2")) == ("2-4 st lök", True)
class TestMixedFractions:
"""Live data: 'tortillakebab-pa-grillen' stores '1 1/2 msk tomatpuré'."""
def test_mixed_number_scales_as_one_value(self):
# Naively scaling the leading "1" would yield "2 1/2 msk" instead of "3".
assert scale_line("1 1/2 msk tomatpuré", Decimal("2")) == ("3 msk tomatpuré", True)
def test_bare_fraction_scales(self):
assert scale_line("1/2 tsk salt", Decimal("3")) == ("1,5 tsk salt", True)
def test_mixed_number_is_readable_not_mangled(self):
from mealie_mcp.normalize import looks_mangled
assert not looks_mangled("1 1/2 msk tomatpuré")
def test_duplicated_unit_is_still_mangled(self):
from mealie_mcp.normalize import looks_mangled
assert looks_mangled("2 dl dl grädde")