Stop asking for meals on a shared free day

Third app onto freedays (~/utils/freedays), the shared pool of
pre-declared days when every gate app stands down. due_slots() returns
nothing on a free day, so no slot is ever due, the lock never opens and
nothing is said about the missing meals.

Read from one local file, never the network, so this cannot hang the
systemd-fired gate -- and an unreadable pool reads as "not free", which
means the normal rules apply rather than the gate quietly switching
itself off. Tested explicitly: fail closed, not open.

The conftest redirect is load-bearing. The pool lives under
~/.local/share, outside every diet_guard path the suite already isolates,
so without it a real free day on this machine would fail every "the gate
is due" test here for a reason that looks nothing like the cause.

gatelock moves v0.7.2 -> v0.7.3, and the pylint hook's own pins (which
had drifted further, to gatelock v0.7.1 and crdt-sync v0.6.0) come with
it. Not cosmetic: pip refuses two different direct URLs for one
distribution, so any stale pin alongside freedays -- which requires
v0.7.3 -- is an unresolvable environment rather than a warning.

requirements.txt also picks up pylint 4.0.8, pytest-randomly 5.0.0 and
ruff 0.16.6, which the freshness gate flagged once this file was touched.
1043 tests pass at 100% coverage across the pytest-randomly major bump.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-09-07 15:22:38 +02:00
parent 862a114909
commit b77145010c
7 changed files with 73 additions and 9 deletions

View File

@ -102,8 +102,13 @@ repos:
additional_dependencies:
- pytest
- requests
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.1#subdirectory=gatelock
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.6.0#subdirectory=crdt-sync
# Kept in step with pyproject.toml on purpose: pip refuses two
# different direct URLs for one distribution, so a stale pin here
# becomes an unresolvable hook environment the moment freedays --
# which requires gatelock v0.7.3 -- joins the list.
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.1.0#subdirectory=freedays
- mcp>=2,<3
- pydantic
# Test suites intentionally use patterns (protected-access, magic-value

View File

@ -2,7 +2,7 @@ name: diet_guard_app
description: "Companion phone app for diet_guard: log meals on the go."
publish_to: 'none'
version: 1.0.76+1
version: 1.0.77+1
environment:
sdk: ^3.12.2

View File

@ -17,6 +17,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
import freedays
from diet_guard._meal_schedule_store import current_schedule
from diet_guard._slots import missing_slots, slot_label
from diet_guard._state import (
@ -38,8 +40,15 @@ def due_slots(now: datetime | None = None) -> tuple[int, ...]:
Returns:
The slot hours that still need a meal logged (empty == nothing due).
Empty on a shared free day, whatever the schedule says. The pool is read
from a single local file and never the network, so this cannot hang the
systemd-fired gate, and an unreadable pool reads as "not free" -- the
normal rules apply rather than the gate switching itself off.
"""
reference = now if now is not None else now_local()
if freedays.is_free_day(reference.date()):
return ()
return missing_slots(reference, logged_slots_today(), current_schedule())

View File

@ -24,6 +24,7 @@ from contextlib import ExitStack
from typing import TYPE_CHECKING
from unittest.mock import patch
import freedays
import pytest
from diet_guard._estimator import Nutrition
@ -103,6 +104,14 @@ def _isolate_state(tmp_path: Path) -> Iterator[None]:
~20 redirects CPython refuses to compile it.
"""
redirects = [
# The shared free-day pool lives under ~/.local/share, outside every
# diet_guard path below. Without this a real free day on the
# developer's machine would make every "the gate is due" test here
# fail, for a reason that looks nothing like the cause.
patch(
"freedays._api.resolve_paths",
lambda paths: paths or freedays.Paths.under(tmp_path / "freedays"),
),
patch(
"diet_guard._budget.BUDGET_FILE",
tmp_path / ".budget",

View File

@ -6,11 +6,17 @@ here the logged set is mocked and ``now`` is injected to drive each decision.
from __future__ import annotations
from datetime import UTC, datetime
from datetime import UTC, date, datetime
from typing import TYPE_CHECKING
from unittest.mock import patch
import freedays
from diet_guard._gate import due_slots, gate_is_due, gate_message
if TYPE_CHECKING:
from pathlib import Path
def _at(hour: int) -> datetime:
"""Return a fixed local datetime at ``hour``."""
@ -77,3 +83,36 @@ class TestGateMessage:
with _logged(set()):
message = gate_message(_at(17))
assert message == "Log your meals for 08:00, 12:00, 16:00 to unlock."
class TestFreeDays:
"""The shared pool stands the gate down entirely."""
def test_a_free_day_leaves_nothing_due(self, tmp_path: Path) -> None:
with _logged(set()):
assert due_slots(_at(20)), "precondition: slots are due without a free day"
freedays.mark(
_at(20).date(),
paths=freedays.Paths.under(tmp_path / "freedays"),
now=_at(20).date(),
)
assert due_slots(_at(20)) == ()
assert not gate_is_due(_at(20))
def test_a_free_day_on_another_date_changes_nothing(self, tmp_path: Path) -> None:
with _logged(set()):
freedays.mark(
date(2026, 12, 24),
paths=freedays.Paths.under(tmp_path / "freedays"),
now=_at(20).date(),
)
assert due_slots(_at(20))
assert gate_is_due(_at(20))
def test_an_unreadable_pool_leaves_the_gate_armed(self, tmp_path: Path) -> None:
"""Fail closed: a corrupt pool must not switch the gate off."""
pool_dir = tmp_path / "freedays"
pool_dir.mkdir(exist_ok=True)
(pool_dir / "free_days.json").write_text("{ not json", encoding="utf-8")
with _logged(set()):
assert gate_is_due(_at(20))

View File

@ -4,8 +4,9 @@ version = "1.0.0"
description = "Tkinter/systemd log-to-unlock meal gate with calorie/macro budget tracking"
requires-python = ">=3.14"
dependencies = [
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.2#subdirectory=gatelock",
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock",
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync",
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.1.0#subdirectory=freedays",
"requests==2.34.2",
]

View File

@ -4,16 +4,17 @@ bandit==1.9.4
codespell==2.4.3
coverage==7.16.0
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.1#subdirectory=crdt-sync
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.2#subdirectory=gatelock
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.1.0#subdirectory=freedays
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.7.3#subdirectory=gatelock
mcp==2.1.1
mypy==2.3.1
pre-commit==4.6.2
pydantic==2.13.5
pylint==4.0.7
pylint==4.0.8
pytest==9.1.1
pytest-cov==7.1.0
pytest-randomly==4.1.0
pytest-randomly==5.0.0
pytest-sugar==1.1.1
pytest-xdist==3.8.0
requests==2.34.2
ruff==0.16.5
ruff==0.16.6