mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-07-04 17:23:05 +02:00
Add the diet_guard package: a screen-locking meal-logging gate that fires on 4-hour slots (08/12/16/20) and records calories/macros, persisting an autocompleting food bank. - Trigger fix: the systemd timer fires at session start (Persistent=true) before lightdm has written ~/.Xauthority, so the gate crashed with a TclError instead of locking the screen. Add wait_for_display() / _display_is_ready() in _gatelock.py and wire it into _cli._cmd_gate so the gate retries on the next tick instead of crashing; add Environment=XAUTHORITY=%h/.Xauthority to the service as belt-and-suspenders. - Food-bank hardening: a transiently corrupt food_bank.json was warned about on every keystroke and then silently overwritten (data loss). _read_bank now quarantines it via _quarantine_corrupt_bank() (warn-once + timestamped backup) before starting fresh. - Multi-item meals: new _meal.py (MealItem, meal_total, MEAL_SOURCE), remember_meal() + _upsert() in _foodbank.py, and a "+ Add item" control in the gate that logs both the individual items and the composite meal. - Bundle resolve_nutrition's manual macros into a ManualMacros dataclass to stay within the argument-count limit. diet_guard at 100% branch coverage; full pre-commit suite passes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
142 lines
5.3 KiB
Python
142 lines
5.3 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 python_pkg.diet_guard._estimator import Nutrition
|
|
from python_pkg.diet_guard._foodbank import remember_food
|
|
from python_pkg.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(
|
|
"python_pkg.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(
|
|
"python_pkg.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
|