mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
On 2026-09-19 the phone was powered off after the bathroom tag and nothing on the PC noticed: every finished session was published and nothing read it. Now wake-alarm distils the phone's morning into an HMAC-signed ~/.local/state/wake_alarm/morning_session.json (wake-alarm-session.timer, 07:00-11:00 every 2 min) that screen-locker and leetcode-guard read to defer their locks -- provisionally until min(start + 90 min, 09:30) while the session is live, until 11:00 once it completed inside that cutoff, and not at all for a session finished later (the phone switched on at noon completes the legs then; that must un-lock nothing). The phone publishes the session at start as well as at finish (current_session, cleared with the finish stamp), so the PC can tell "started, not finished" from "never started". A required 1-5 energy tap joins the weigh-in and ships as latest_energy with a PC reader mirroring _weight (the shared half is _field_reader). Also fixes the vmbox harness, red since the 2026-09-11 home-tidy: the guest tree lives at ~/wake-alarm, and the INSTALL-OK grep matched vm's echo of the command line. Takes Flutter 3.47.5. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
103 lines
3.8 KiB
Python
103 lines
3.8 KiB
Python
"""The PC-side reader for the phone's morning weigh-in."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
from typing import TYPE_CHECKING
|
|
from unittest.mock import patch
|
|
|
|
from wake_alarm._weight import Weight, latest_weight, main, newest_weight
|
|
|
|
if TYPE_CHECKING:
|
|
import pytest
|
|
|
|
|
|
def _log(
|
|
kg: object, hlc: str = "2026-09-17T06:20:00.000Z-0000-phone", **extra: object
|
|
) -> str:
|
|
value = {"date": "2026-09-17", "kg": kg, **extra}
|
|
return json.dumps({"alarm": {"fields": {"latest_weight_kg": [value, hlc]}}})
|
|
|
|
|
|
class TestNewestWeight:
|
|
"""Ranking and parsing across devices."""
|
|
|
|
def test_picks_the_newest_stamp(self) -> None:
|
|
"""The later HLC wins regardless of listing order."""
|
|
published = [
|
|
("old", _log(70.0, "2026-09-10T06:20:00.000Z-0000-old")),
|
|
("new", _log(72.4, "2026-09-17T06:20:00.000Z-0000-new")),
|
|
]
|
|
assert newest_weight(published) == Weight("2026-09-17", 72.4, "new")
|
|
assert newest_weight(list(reversed(published))) == Weight(
|
|
"2026-09-17", 72.4, "new"
|
|
)
|
|
|
|
def test_device_id_breaks_an_exact_tie(self) -> None:
|
|
"""Two stamps in the same millisecond do not alternate."""
|
|
published = [("b", _log(71)), ("a", _log(70))]
|
|
assert newest_weight(published) == Weight("2026-09-17", 71.0, "b")
|
|
|
|
def test_skips_every_malformed_shape(self) -> None:
|
|
"""Garbage never wins, and never crashes."""
|
|
published = [
|
|
("junk", "{not json"),
|
|
("list", "[1, 2]"),
|
|
("no-record", json.dumps({})),
|
|
("no-fields", json.dumps({"alarm": 1})),
|
|
("no-pair", json.dumps({"alarm": {"fields": {}}})),
|
|
("empty-pair", json.dumps({"alarm": {"fields": {"latest_weight_kg": []}}})),
|
|
(
|
|
"scalar",
|
|
json.dumps({"alarm": {"fields": {"latest_weight_kg": [72, "h"]}}}),
|
|
),
|
|
("bool-kg", _log(kg=True)),
|
|
("str-kg", _log("72")),
|
|
(
|
|
"no-date",
|
|
json.dumps(
|
|
{"alarm": {"fields": {"latest_weight_kg": [{"kg": 1}, "h"]}}}
|
|
),
|
|
),
|
|
]
|
|
assert newest_weight(published) is None
|
|
assert newest_weight([*published, ("ok", _log(72))]) == Weight(
|
|
"2026-09-17", 72.0, "ok"
|
|
)
|
|
|
|
def test_an_unstamped_pair_still_parses(self) -> None:
|
|
"""A pair with no HLC sorts last but is not thrown away."""
|
|
raw = json.dumps(
|
|
{
|
|
"alarm": {
|
|
"fields": {"latest_weight_kg": [{"date": "2026-09-01", "kg": 69}]}
|
|
}
|
|
}
|
|
)
|
|
assert newest_weight([("x", raw)]) == Weight("2026-09-01", 69.0, "x")
|
|
|
|
|
|
class TestLatestWeight:
|
|
"""The fetch-and-rank entry point and its CLI."""
|
|
|
|
def test_unreadable_backend_is_none(self) -> None:
|
|
"""No Firebase means no weight, not an error."""
|
|
with patch("wake_alarm._field_reader.fetch_firebase_logs", return_value=None):
|
|
assert latest_weight() is None
|
|
assert main() == 1
|
|
|
|
def test_no_weigh_in_yet_is_none(self) -> None:
|
|
"""A phone that has not run a session publishes no weight."""
|
|
logs = [("phone", json.dumps({"alarm": {"fields": {"alarm_hour": [8, "h"]}}}))]
|
|
with patch("wake_alarm._field_reader.fetch_firebase_logs", return_value=logs):
|
|
assert latest_weight() is None
|
|
|
|
def test_main_prints_json(self, capsys: pytest.CaptureFixture[str]) -> None:
|
|
"""The CLI emits one JSON line diet-guard can consume."""
|
|
with patch(
|
|
"wake_alarm._field_reader.fetch_firebase_logs",
|
|
return_value=[("p", _log(72.4))],
|
|
):
|
|
assert main() == 0
|
|
assert json.loads(capsys.readouterr().out) == {"date": "2026-09-17", "kg": 72.4}
|