wake-alarm/phone_app/scripts/sick_days_interop/verify.py
Krzysztof kuhy Rudnicki 34037e55c1 phone: sick days — picker, silent ring, stick stand-down, pre-alarm sync
- Sick days screen (own name, sick icon, cat-2 fill): tap today or a later
  day to mark it sick, tap again to clear; past days inert. Writes the
  sickdays wire format (state sick/cleared + actor, one HLC per write) to
  sickdays-sync/devices via crdt_sync syncLog.
- Mutual exclusion in both pickers: each syncs the other pool on open and
  refuses a date it holds, naming the pool; an unreadable pool refuses all.
- Native SickDays cache (fail closed via DayCache); AlarmReceiver skips the
  ring with reason "sick_day" (no markFired, no ring_started); StickCheck
  verdict sick_day, streak skips sick days; stakes card says so.
- Pre-alarm pool sync: allow-while-idle alarm at 21:00 and A-30 (request
  code 1006) runs a headless engine (poolSyncMain) that pulls both pools
  from RTDB and rewrites both native caches, so a PC mark arrives with the
  app closed. The ring decision stays a cache read.
- JVM unit tests (JUnit 4.13.2) for DayCache, RingDecision,
  PoolSyncSchedule, StickCheck; Dart tests; Dart->Python interop check.

pylint hook skipped: verify.py imports sickdays, not in the hook env until
the sickdays-v0.1.0 tag exists; run by hand in a sickdays venv (10.00/10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Xfj1sxKH9VBPqpcAni8Jry
2026-10-16 13:22:40 +02:00

85 lines
2.7 KiB
Python

"""Read a Dart-written sick-day pool with the Python package and compare.
``phone_app/lib/services/sick_day_pool.dart`` writes the wire format that
``~/src/utils/sickdays`` owns. The Python package is the oracle: it replays
the writer test's edits into a scratch pool, and every reading a gate relies
on (``is_sick_day`` per date, ``sick_days``) has to agree between the two
files. Reads only; writes nothing outside a temporary directory.
"""
from __future__ import annotations
from datetime import date
import json
from pathlib import Path
import sys
import tempfile
import sickdays
from sickdays._paths import Paths
EXPECTED_ARGC = 2
TODAY = date(2026, 10, 16)
DAYS = [TODAY, date(2026, 10, 20), date(2026, 10, 21), date(2026, 10, 23)]
def _never_free(_day: date) -> bool:
return False
def python_pool(root: Path) -> Path:
"""Write the oracle pool with the edits the Dart writer made."""
paths = Paths.under(root)
sickdays.add_sick_day(TODAY, is_free=_never_free, paths=paths, now=TODAY)
sickdays.add_sick_day(
date(2026, 10, 20), is_free=_never_free, paths=paths, now=TODAY
)
sickdays.add_sick_day(
date(2026, 10, 21), is_free=_never_free, paths=paths, now=TODAY
)
sickdays.remove_sick_day(date(2026, 10, 21), paths=paths, now=TODAY)
return paths.log
def reading(pool: Path) -> dict[str, object]:
"""Every view of a pool a gate relies on."""
return {
"is_sick_day": {
d.isoformat(): sickdays.is_sick_day(d, log_path=pool) for d in DAYS
},
"sick_days": sorted(d.isoformat() for d in sickdays.sick_days(log_path=pool)),
}
def one_tick_per_write(pool: Path) -> bool:
"""Whether every record's fields share one HLC, as the format requires."""
log = json.loads(pool.read_text(encoding="utf-8"))
return all(
len({hlc for _value, hlc in record["fields"].values()}) == 1
and not record["deleted"]
for record in log.values()
)
def main(argv: list[str]) -> int:
"""Compare the Dart fixture at ``argv[1]`` with the Python oracle."""
if len(argv) != EXPECTED_ARGC:
sys.stderr.write("usage: verify.py <dart sick_days.json>\n")
return 2
dart = reading(Path(argv[1]))
with tempfile.TemporaryDirectory() as tmp:
python = reading(python_pool(Path(tmp)))
sys.stdout.write(f"dart: {json.dumps(dart)}\npython: {json.dumps(python)}\n")
if dart != python:
sys.stderr.write("MISMATCH: Dart and Python read different pools\n")
return 1
if not one_tick_per_write(Path(argv[1])):
sys.stderr.write("MISMATCH: a Dart record has mixed HLCs or a tombstone\n")
return 1
sys.stdout.write("OK: Dart and Python agree\n")
return 0
if __name__ == "__main__":
sys.exit(main(sys.argv))