mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-11-10 19:07:15 +01:00
gate: stand down on a sick day (sickdays 0.1.0)
due_slots() returns nothing on a sick day, like a free day: no lock, no debt; logging still works. Unreadable pool reads as not sick (fail closed). Pins sickdays-v0.1.0 and freedays-v0.4.0 (pyproject, requirements, pylint hook env). Phone gap documented in docs/DOCS-sick-days.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
This commit is contained in:
parent
f2606fe0ca
commit
419ada929a
@ -107,8 +107,9 @@ repos:
|
||||
# the manifest becomes an unresolvable hook environment rather
|
||||
# than merely an old one.
|
||||
- gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#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.3.0#subdirectory=freedays
|
||||
- crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
|
||||
- freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays
|
||||
- sickdays @ git+https://github.com/kuhyx/utils@sickdays-v0.1.0#subdirectory=sickdays
|
||||
- mcp>=2,<3
|
||||
- pydantic
|
||||
- defusedxml==0.7.1
|
||||
|
||||
@ -32,8 +32,8 @@ catch-up rule).
|
||||
`diet-guard-gate.timer` is wall-clock (`OnCalendar=*-*-* *:00/15:00`,
|
||||
`Persistent=true`), not boot-relative — a boot-relative timer collided with
|
||||
fullscreen games grabbing input. `diet-guard-gate.service` is `Type=oneshot`
|
||||
and exits 0 immediately when no lock is due. It needs `DISPLAY`/`XAUTHORITY`;
|
||||
see the unit file's comments and `wait_for_display()`.
|
||||
and exits 0 at once when no lock is due (free/sick day: [docs/DOCS-sick-days.md](docs/DOCS-sick-days.md)).
|
||||
Needs `DISPLAY`/`XAUTHORITY`; see the unit file and `wait_for_display()`.
|
||||
|
||||
## Cross-device sync
|
||||
|
||||
|
||||
@ -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.111+1
|
||||
version: 1.0.112+1
|
||||
|
||||
environment:
|
||||
sdk: ^3.12.2
|
||||
|
||||
@ -18,6 +18,7 @@ from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
import freedays
|
||||
import sickdays
|
||||
|
||||
from diet_guard._meal_schedule_store import current_schedule
|
||||
from diet_guard._slots import missing_slots, slot_label
|
||||
@ -41,13 +42,16 @@ def due_slots(now: datetime | None = None) -> tuple[int, ...]:
|
||||
Returns:
|
||||
The slot minutes 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.
|
||||
Empty on a shared free day or a sick day, whatever the schedule says: the
|
||||
gate stands down, so no lock opens and nothing accrues -- unlogged meals
|
||||
leave no debt, and logging a meal still works as on any day. Each 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" / "not
|
||||
sick" -- 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()):
|
||||
day = reference.date()
|
||||
if freedays.is_free_day(day) or sickdays.is_sick_day(day):
|
||||
return ()
|
||||
# One schedule for both halves: snapping the log onto a different schedule
|
||||
# than the one deriving the due slots would leave a logged meal covering a
|
||||
|
||||
@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
import freedays
|
||||
import sickdays
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -28,6 +29,12 @@ def state_redirects(tmp_path: Path) -> list[_patch[object]]:
|
||||
"freedays._api.resolve_paths",
|
||||
lambda paths: paths or freedays.Paths.under(tmp_path / "freedays"),
|
||||
),
|
||||
# Same for the sick-day pool: a real sick day on this machine must not
|
||||
# silently stand the gate down for the whole suite.
|
||||
patch(
|
||||
"sickdays._api.resolve_paths",
|
||||
lambda paths: paths or sickdays.Paths.under(tmp_path / "sickdays"),
|
||||
),
|
||||
patch(
|
||||
"diet_guard._budget.BUDGET_FILE",
|
||||
tmp_path / ".budget",
|
||||
|
||||
@ -6,13 +6,17 @@ here the logged set is mocked and ``now`` is injected to drive each decision.
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, date, datetime
|
||||
from datetime import UTC, date, datetime, time, timedelta
|
||||
from typing import TYPE_CHECKING
|
||||
from unittest.mock import patch
|
||||
|
||||
import freedays
|
||||
import sickdays
|
||||
|
||||
from diet_guard._estimator import Nutrition
|
||||
from diet_guard._gate import due_slots, gate_is_due, gate_message
|
||||
from diet_guard._state import log_meal, now_local
|
||||
from diet_guard._state_today import logged_slots_today, today_total_kcal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from pathlib import Path
|
||||
@ -116,3 +120,55 @@ class TestFreeDays:
|
||||
(pool_dir / "free_days.json").write_text("{ not json", encoding="utf-8")
|
||||
with _logged(set()):
|
||||
assert gate_is_due(_at(20))
|
||||
|
||||
|
||||
def _mark_sick(tmp_path: Path, day: date) -> None:
|
||||
"""Put ``day`` in the (temp) sick-day pool; the redirect lives in conftest."""
|
||||
sickdays.add_sick_day(
|
||||
day,
|
||||
is_free=lambda _day: False,
|
||||
paths=sickdays.Paths.under(tmp_path / "sickdays"),
|
||||
now=day,
|
||||
)
|
||||
|
||||
|
||||
class TestSickDays:
|
||||
"""The sick-day pool stands the gate down, like a free day."""
|
||||
|
||||
def test_a_sick_day_leaves_nothing_due(self, tmp_path: Path) -> None:
|
||||
with _logged(set()):
|
||||
assert due_slots(_at(20)), "precondition: slots are due without a sick day"
|
||||
_mark_sick(tmp_path, _at(20).date())
|
||||
assert due_slots(_at(20)) == ()
|
||||
assert not gate_is_due(_at(20))
|
||||
assert gate_message(_at(20)) == "All meals are logged. You're up to date."
|
||||
|
||||
def test_a_sick_day_on_another_date_changes_nothing(self, tmp_path: Path) -> None:
|
||||
with _logged(set()):
|
||||
_mark_sick(tmp_path, date(2026, 12, 24))
|
||||
assert due_slots(_at(20))
|
||||
assert gate_is_due(_at(20))
|
||||
|
||||
def test_the_day_after_a_sick_day_is_armed_again(self, tmp_path: Path) -> None:
|
||||
_mark_sick(tmp_path, _at(20).date())
|
||||
tomorrow = _at(20) + timedelta(days=1)
|
||||
with _logged(set()):
|
||||
assert gate_is_due(tomorrow)
|
||||
|
||||
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 / "sickdays"
|
||||
pool_dir.mkdir(exist_ok=True)
|
||||
(pool_dir / "sick_days.json").write_text("{ not json", encoding="utf-8")
|
||||
with _logged(set()):
|
||||
assert gate_is_due(_at(20))
|
||||
|
||||
def test_logging_still_works_on_a_sick_day(self, tmp_path: Path) -> None:
|
||||
"""The gate stands down; the log does not: a meal is still recorded."""
|
||||
today = now_local().date()
|
||||
_mark_sick(tmp_path, today)
|
||||
log_meal("soup", Nutrition(300, 10, 30, 5, 250, "manual"), slot=480)
|
||||
assert logged_slots_today() == {480}
|
||||
assert today_total_kcal() == 300
|
||||
late = datetime.combine(today, time(23, 0), tzinfo=now_local().tzinfo)
|
||||
assert not gate_is_due(late)
|
||||
|
||||
21
docs/DOCS-sick-days.md
Normal file
21
docs/DOCS-sick-days.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Free days and sick days
|
||||
|
||||
`_gate.due_slots` returns `()` on a shared free day (`freedays`) or a sick day
|
||||
(`sickdays` 0.1.0, one pool for every gate). Nothing is due, so no lock opens,
|
||||
nothing is said about missing meals and no debt accrues. Logging a meal works
|
||||
as on any other day (the log path never consults the gate); diet-guard earns
|
||||
nothing, so there is nothing to credit.
|
||||
|
||||
- Each pool is one local file, never the network, so the systemd-fired gate
|
||||
cannot hang on it.
|
||||
- **Fail closed:** an unreadable pool reads as "not free" / "not sick", so the
|
||||
gate stays armed. `sickdays.is_sick_day` never raises.
|
||||
- Only the calendar day of `now` is asked (the last slot is at most 22:00, so
|
||||
the gate never runs on yesterday's slots after midnight).
|
||||
- Tests: `tests/_state_redirects.py` points both pools into `tmp_path`; a real
|
||||
sick day on the dev machine must not stand the suite's gate down.
|
||||
- **Phone gap:** the companion app does not read either pool.
|
||||
`due_slot_check.dart` (`checkAndNotify`) still posts "log your meal"
|
||||
notifications on a free or sick day. It would need `crdt_sync_dart` syncing
|
||||
`sickdays-sync/devices/*/sick_days.json` (see the sickdays README) and an
|
||||
early return before `_reconcileNotifications` that cancels pending ones.
|
||||
@ -6,7 +6,8 @@ requires-python = ">=3.14"
|
||||
dependencies = [
|
||||
"gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock",
|
||||
"crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync",
|
||||
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays",
|
||||
"freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays",
|
||||
"sickdays @ git+https://github.com/kuhyx/utils@sickdays-v0.1.0#subdirectory=sickdays",
|
||||
"defusedxml==0.7.1",
|
||||
"requests==2.34.2",
|
||||
]
|
||||
|
||||
@ -5,7 +5,7 @@ codespell==2.4.3
|
||||
coverage==7.16.2
|
||||
crdt-sync @ git+https://github.com/kuhyx/utils@crdt-sync-v0.9.2#subdirectory=crdt-sync
|
||||
defusedxml==0.7.1
|
||||
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.3.1#subdirectory=freedays
|
||||
freedays @ git+https://github.com/kuhyx/utils@freedays-v0.4.0#subdirectory=freedays
|
||||
gatelock @ git+https://github.com/kuhyx/utils@gatelock-v0.9.0#subdirectory=gatelock
|
||||
mcp==2.3.0
|
||||
mypy==2.4.0
|
||||
@ -20,3 +20,4 @@ pytest-xdist==3.8.0
|
||||
pytest-xvfb==3.1.1
|
||||
requests==2.34.2
|
||||
ruff==0.17.0
|
||||
sickdays @ git+https://github.com/kuhyx/utils@sickdays-v0.1.0#subdirectory=sickdays
|
||||
|
||||
Loading…
Reference in New Issue
Block a user