diet-guard/diet_guard/tests/test_cli_future_ate.py
Krzysztof kuhy Rudnicki c310861855 slots: minute-of-day consumers (phase 2 of 3: Python side)
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
2026-10-10 19:53:39 +02:00

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