"""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("1–2 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")