diet-guard/diet_guard/tests/test_resolve.py
Krzysztof kuhy Rudnicki 843f5e0221 Extract diet_guard from testsAndMisc as a standalone repo
Rewrites python_pkg.diet_guard imports to diet_guard, vendors the
shared as_float coercion helper, drops the monorepo PYTHONPATH from
install.sh and the systemd unit (package is now pip-installed), and
scaffolds standalone lint/test config matching testsAndMisc's real
enforced bar (pylint --fail-under=10 with tests excluded and the
use-implicit-booleaness/consider-using-with disables, mypy's actual
disabled-error-code set, ruff ALL, bandit, 100% branch coverage).
2026-06-22 12:18:39 +02:00

142 lines
5.2 KiB
Python

"""Tests for _resolve.py — the manual/bank/staple/OFF resolution precedence.
Real food-bank and staple lookups are used (both isolated/offline); only the
Open Food Facts network layer is mocked, via the estimator it delegates to.
"""
from __future__ import annotations
from unittest.mock import patch
from diet_guard._estimator import Nutrition
from diet_guard._foodbank import remember_food
from diet_guard._resolve import (
ManualMacros,
lookup_candidates,
resolve_nutrition,
suggest_foods,
)
_OFF = Nutrition(260, 12, 30, 10, 100, "openfoodfacts: Big Mac")
class TestResolveManual:
"""A manual calorie value, scaled from its stated basis."""
def test_per_grams_scaled_to_eaten(self) -> None:
"""200 kcal per 100 g eaten as 330 g logs 660 kcal."""
result = resolve_nutrition(
"pasta",
grams=330,
manual_macros=ManualMacros(kcal=200, per_grams=100),
)
assert result is not None
assert result.kcal == 660.0
def test_no_basis_keeps_value(self) -> None:
"""With neither grams nor per-grams, the manual value is taken as-is."""
result = resolve_nutrition("shake", manual_macros=ManualMacros(kcal=180))
assert result is not None
assert result.kcal == 180.0
def test_grams_only_is_the_basis(self) -> None:
"""With grams but no per-grams, grams is the reference (no double scale)."""
result = resolve_nutrition(
"soup",
grams=250,
manual_macros=ManualMacros(kcal=300),
)
assert result is not None
assert result.kcal == 300.0
class TestResolveBankAndStaple:
"""Local sources, before any network call."""
def test_banked_food_scaled(self) -> None:
"""A banked food is rescaled to the amount eaten."""
remember_food("carbonara", Nutrition(700, 20, 80, 30, 350, "manual"))
result = resolve_nutrition("carbonara", grams=700)
assert result is not None
assert result.kcal == 1400.0
def test_banked_food_no_grams(self) -> None:
"""Without grams, the banked macros are returned unscaled."""
remember_food("carbonara", Nutrition(700, 20, 80, 30, 350, "manual"))
result = resolve_nutrition("carbonara")
assert result is not None
assert result.kcal == 700.0
def test_staple_before_off(self) -> None:
"""A bare staple resolves locally (and never hits OFF)."""
result = resolve_nutrition("apple", grams=200)
assert result is not None
assert "staple: apple" in result.source
def test_staple_no_grams(self) -> None:
"""A staple with no grams returns its per-100 g basis."""
result = resolve_nutrition("egg")
assert result is not None
assert result.grams == 100.0
def test_off_fallback(self) -> None:
"""An unknown, non-staple food falls through to Open Food Facts."""
with patch(
"diet_guard._resolve.estimate_off",
return_value=_OFF,
):
result = resolve_nutrition("exotic dish")
assert result is not None
assert "openfoodfacts" in result.source
class TestLookupCandidates:
"""Reviewable candidates for the blank-calorie gate path."""
def test_banked_candidate(self) -> None:
"""A banked food yields a single scaled candidate under its name."""
remember_food("oats", Nutrition(380, 13, 67, 7, 100, "manual"))
candidates = lookup_candidates("oats", grams=200)
assert candidates[0][0] == "oats"
assert candidates[0][1].kcal == 760.0
def test_banked_candidate_no_grams(self) -> None:
"""Without grams the banked candidate is unscaled."""
remember_food("oats", Nutrition(380, 13, 67, 7, 100, "manual"))
assert lookup_candidates("oats")[0][1].kcal == 380.0
def test_staple_candidate(self) -> None:
"""A staple yields a candidate labelled by its source."""
candidates = lookup_candidates("banana", grams=100)
assert "staple: banana" in candidates[0][0]
def test_staple_candidate_no_grams(self) -> None:
"""A staple candidate with no grams stays at its 100 g basis."""
assert lookup_candidates("banana")[0][1].grams == 100.0
def test_off_candidates(self) -> None:
"""An unknown food returns the OFF alternatives, labelled by source."""
with patch(
"diet_guard._resolve.off_candidates",
return_value=[_OFF],
):
candidates = lookup_candidates("exotic dish")
assert candidates[0][0] == _OFF.source
class TestSuggestFoods:
"""Merged bank + staple autocomplete."""
def test_bank_ranked_first(self) -> None:
"""A banked food appears ahead of staples for the same query."""
remember_food("apple pie", Nutrition(300, 3, 50, 12, 120, "manual"))
names = [name for name, _ in suggest_foods("apple")]
assert names[0] == "apple pie"
assert "apple" in names
def test_staple_not_duplicated(self) -> None:
"""A staple already banked under the same name is not duplicated."""
remember_food("apple", Nutrition(95, 0, 25, 0, 182, "manual"))
names = [name for name, _ in suggest_foods("apple")]
assert names.count("apple") == 1