mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 20:27:11 +01:00
Every Python consumer now speaks slot minutes:
- satisfaction is nearest-slot snapping (_slot_wire.satisfied_by_entries)
at every "is this slot logged?" site: gate, CLI status, MCP, catering
occupancy and dedup. Nothing on disk is rewritten; 07:00/10:00 entries
keep covering 07:15/10:15 after a schedule move.
- log_meal writes slot_fields(minute) before signing (slot + slot_min).
- schedule store and sync encode via schedule_to_wire/from_wire; a
whole-hour schedule stays byte-identical (f/l/n only).
- CLI ate --hour takes HH or HH:MM; MCP log_meal takes "HH:MM" or a
legacy int hour, validated against today's slots; get_slots returns
{minute, label}.
- gate History tab: first/last are HH:MM ttk.Comboboxes (quarter-hour
list, typed times, Up/Down step without posting the popdown), locked
as disabled; validation mirrors normalized().
- gate timer fires every 15 min; docs rewritten for minutes.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015npsYVB2PdtWkG7MPAi4tn
91 lines
3.8 KiB
Python
91 lines
3.8 KiB
Python
"""Tests for ``diet_guard ate --date/--hour`` — pre-logging a future meal.
|
|
|
|
Split out of test_cli.py's ``TestAte`` for the repo's 250-line cap.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from datetime import timedelta
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import patch
|
|
|
|
from diet_guard import _cli_log
|
|
from diet_guard._budget import write_budget
|
|
from diet_guard._cli import main
|
|
from diet_guard._estimator import Nutrition
|
|
from diet_guard._meal_schedule import MealSchedule
|
|
from diet_guard._meal_schedule_store import record_schedule_change
|
|
from diet_guard._state import load_log, now_local
|
|
|
|
if TYPE_CHECKING:
|
|
import pytest
|
|
|
|
_NUT = Nutrition(250, 12, 30, 10, 200, "manual")
|
|
|
|
|
|
class TestAteFutureDate:
|
|
"""Logging a meal against a future ``--date``/``--hour`` from the CLI."""
|
|
|
|
def test_future_date_and_hour_logs_under_that_date(
|
|
self, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""--date/--hour together pre-log against a future slot."""
|
|
write_budget(2000)
|
|
tomorrow = (now_local() + timedelta(days=1)).date().isoformat()
|
|
with patch.object(_cli_log, "resolve_nutrition", return_value=_NUT):
|
|
assert (
|
|
main(["ate", "future lunch", "--date", tomorrow, "--hour", "12"]) == 0
|
|
)
|
|
out = capsys.readouterr().out
|
|
assert "logged:" in out
|
|
assert tomorrow in out
|
|
assert load_log()[tomorrow][0]["slot"] == 12
|
|
|
|
def test_date_without_hour_errors(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
"""A lone --date without --hour is rejected before resolving food."""
|
|
assert main(["ate", "x", "--date", "2099-01-01"]) == 1
|
|
assert "must be given together" in capsys.readouterr().out
|
|
|
|
def test_invalid_future_hour_errors(
|
|
self, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""An hour that isn't a configured slot is rejected."""
|
|
tomorrow = (now_local() + timedelta(days=1)).date().isoformat()
|
|
assert main(["ate", "x", "--date", tomorrow, "--hour", "13"]) == 1
|
|
assert "not a meal slot" in capsys.readouterr().out
|
|
|
|
def test_past_date_errors(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
"""A non-future date is rejected."""
|
|
yesterday = (now_local() - timedelta(days=1)).date().isoformat()
|
|
assert main(["ate", "x", "--date", yesterday, "--hour", "12"]) == 1
|
|
assert "not a future date" in capsys.readouterr().out
|
|
|
|
def test_an_hhmm_hour_pre_logs_an_off_hour_slot(
|
|
self, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
"""--hour 07:15 names a minute slot and is echoed as HH:MM."""
|
|
record_schedule_change(MealSchedule(435, 1140, 5))
|
|
tomorrow = (now_local() + timedelta(days=1)).date().isoformat()
|
|
with patch.object(_cli_log, "resolve_nutrition", return_value=_NUT):
|
|
assert main(["ate", "oats", "--date", tomorrow, "--hour", "07:15"]) == 0
|
|
assert f"for {tomorrow} 07:15" in capsys.readouterr().out
|
|
(entry,) = load_log()[tomorrow]
|
|
assert (entry["slot"], entry["slot_min"]) == (7, 435)
|
|
assert entry["time"].startswith(f"{tomorrow}T07:15")
|
|
|
|
def test_a_whole_hour_is_echoed_as_hhmm(
|
|
self, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
tomorrow = (now_local() + timedelta(days=1)).date().isoformat()
|
|
with patch.object(_cli_log, "resolve_nutrition", return_value=_NUT):
|
|
assert main(["ate", "x", "--date", tomorrow, "--hour", "20:00"]) == 0
|
|
assert f"for {tomorrow} 20:00" in capsys.readouterr().out
|
|
assert "slot_min" not in load_log()[tomorrow][0]
|
|
|
|
def test_an_unparseable_hour_errors(
|
|
self, capsys: pytest.CaptureFixture[str]
|
|
) -> None:
|
|
tomorrow = (now_local() + timedelta(days=1)).date().isoformat()
|
|
assert main(["ate", "x", "--date", tomorrow, "--hour", "noon"]) == 1
|
|
assert "expected HH:MM" in capsys.readouterr().out
|