mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 11:43:07 +02:00
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).
142 lines
5.2 KiB
Python
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
|