mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 18:07:05 +01:00
phone: sick-day interop both ways with keep; docs
check.sh now runs Dart->Python (keep, unsorted/duplicate input, a cleared keep day, a keep-only edit that must not re-stamp state) and Python->Dart (0.2.0 keep sets plus a 0.1.0 record with no keep, a stale keep and a malformed one), Python 0.2.0 as the oracle with explicit paths and HOME pointed at a scratch dir. DOCS-sick-days.md and the SickDays.kt contract describe the alarm-only native cache. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PAhbNszs5JKdtnD4uBi3xp
This commit is contained in:
parent
ee7e380278
commit
597901dc08
@ -6,7 +6,11 @@ day. Read its README for the rules and the RTDB wire format. This file covers
|
||||
the phone half.
|
||||
|
||||
On a sick day the phone does not ring, the stick does not judge the day, and
|
||||
nothing ring-related is recorded. It is **not** a free day: separate pool,
|
||||
nothing ring-related is recorded -- **unless the day keeps the alarm**.
|
||||
Since sickdays 0.2.0 a sick day carries a per-locker `keep` set (README
|
||||
"keep (0.2.0)"): a kept locker behaves exactly as on a normal day, every other
|
||||
one stands down. This app is the `alarm` locker (ring, PC fallback ring, RTC
|
||||
self-wake, missed-alarm penalty / stick, morning routine). It is **not** a free day: separate pool,
|
||||
separate RTDB node (`sickdays-sync/devices`), separate native cache, separate
|
||||
screen, and every log line and event says `sick_day`.
|
||||
|
||||
@ -15,7 +19,8 @@ screen, and every log line and event says `sick_day`.
|
||||
| | |
|
||||
|---|---|
|
||||
| Picker | `phone_app/lib/screens/sick_days_screen.dart` (+ `sick_days_body.dart`, the shared `day_calendar.dart`); the sick-face icon on the home screen |
|
||||
| Rules, Dart port | `phone_app/lib/services/sick_day_pool.dart`: records `{state: sick/cleared, actor}`, one HLC per write, never a tombstone |
|
||||
| Rules, Dart port | `phone_app/lib/services/sick_day_pool.dart`: records `{state: sick/cleared, actor, keep}`, one HLC per mark/clear, never a tombstone; keep read rules in `sick_day_keep.dart` |
|
||||
| Per-locker sheet | `phone_app/lib/screens/sick_day_sheet.dart` (+ the dot in `day_cell.dart`) |
|
||||
| Sync | `phone_app/lib/services/sick_day_sync.dart` (shared tick: `day_pool_sync.dart`), Firebase only |
|
||||
| Ring path | `android/.../SickDays.kt` (+ `DayCache.kt`), read in `AlarmReceiver` through `RingDecision.kt` |
|
||||
| Stick | `StickCheck.kt`: verdict `sick_day`; the streak skips sick days like free days |
|
||||
@ -25,8 +30,16 @@ screen, and every log line and event says `sick_day`.
|
||||
|
||||
Same Monday-first grid as the free days, in `cat-2` blue (`DayMarkColors`
|
||||
in `ui/theme.dart`) rather than the accent gold, under its own title and
|
||||
icon. One tap marks a day sick, another clears it. **Today is tappable**
|
||||
(unlike the free-day grid); the past is inert. No count, no confirmation.
|
||||
icon. **Today is tappable** (unlike the free-day grid); the past is inert.
|
||||
No count, no reason.
|
||||
|
||||
Tapping a normal day opens a bottom sheet: one switch per locker (Workout
|
||||
lock, LeetCode, Book, Diet, Wake alarm), **all off** (off = stands down), and
|
||||
"Mark sick". Tapping a sick day reopens it with the day's switches, "Save"
|
||||
and "Clear sick day". Save is a keep-only edit: it re-stamps `keep` and
|
||||
`actor`, never `state`, so it cannot re-assert "sick" over a concurrent
|
||||
clear; an unchanged Save writes nothing. A sick day keeping any locker shows
|
||||
a dot (in `on-fill` on the `cat-2` cell) and a `Kept: ...` line in the list.
|
||||
|
||||
## A date is never in both pools
|
||||
|
||||
@ -40,6 +53,15 @@ exactly as the Python README prescribes.
|
||||
|
||||
## The ring
|
||||
|
||||
**The native cache holds only the days on which the alarm stands down** --
|
||||
sick and `alarm` not kept, past days included (`SickDaySync.publishToNative`
|
||||
-> `SickDayPool.standDownIsoDates(SickLocker.alarm)`, the one place the keep
|
||||
set is applied for Kotlin). A sick day keeping the alarm is simply absent, so
|
||||
it rings, the stick judges it and the stakes card shows its stakes; Kotlin
|
||||
never sees the keep set. The pre-alarm sync goes through the same publish.
|
||||
The `Alarm cache:` line on the picker reads this cache back, so a kept-alarm
|
||||
sick day is (correctly) not listed there.
|
||||
|
||||
`AlarmReceiver` asks `RingDecision.skip(free, sick)`. On a sick day it logs
|
||||
`sick day: not ringing; re-armed for tomorrow`, appends `alarm_fired` with
|
||||
`reason: "sick_day"`, re-arms and returns: no `AlarmService`, no
|
||||
@ -94,12 +116,21 @@ sync runs between A and the session's end anyway.
|
||||
`PoolSyncScheduleTest`, `StickCheckSickTest`. Run with
|
||||
`cd phone_app/android && ./gradlew :app:testDebugUnitTest`, after one
|
||||
`flutter build apk` has written `gradlew` and `local.properties`.
|
||||
- Cross-language: `phone_app/scripts/sick_days_interop/check.sh` writes a
|
||||
pool through Dart and reads it with `sickdays` (Python), failing on any
|
||||
disagreement. Needs `SICKDAYS_PYTHON` or the sickdays venv.
|
||||
- Keep: `test/services/sick_day_keep_test.dart` (wire, read rules 1-5, a
|
||||
keep edit after a future PC tick, concurrent clear), `test/sick_day_sheet_test.dart`,
|
||||
`test/sick_day_apply_test.dart`.
|
||||
- Cross-language, both ways: `phone_app/scripts/sick_days_interop/check.sh`.
|
||||
Dart writes (incl. keep and a keep-only edit) and Python 0.2.0 reads and
|
||||
replays it; then Python writes (incl. a 0.1.0 record with no keep, a stale
|
||||
keep and a malformed one) and Dart's verdicts are compared with Python's.
|
||||
Needs `SICKDAYS_PYTHON` or the sickdays venv; Python runs with `HOME`
|
||||
pointed at a scratch dir.
|
||||
|
||||
## Do NOT
|
||||
|
||||
- Don't fold sick days into `FreeDays`/`isFree`, or log them as `free_day`.
|
||||
- Don't sync inside `AlarmReceiver`. Don't tombstone a date to clear it.
|
||||
- Don't add a count, a reason prompt or a warning. There is no budget.
|
||||
- Don't put a non-alarm-stand-down date in the native cache, and don't teach
|
||||
Kotlin the keep set: filter in `publishToNative` only.
|
||||
- Don't re-stamp `state` on a keep edit.
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
#!/bin/bash
|
||||
|
||||
# ============================================================================
|
||||
# Cross-language check for the sick-day pool.
|
||||
# Cross-language check for the sick-day pool (sickdays 0.2.0, with keep).
|
||||
#
|
||||
# phone_app/lib/services/sick_day_pool.dart writes the wire format that
|
||||
# ~/src/utils/sickdays (Python) owns. This writes a pool through the Dart
|
||||
# rules and reads it back through the Python ones, and fails on any
|
||||
# disagreement about which days are sick.
|
||||
# ~/src/utils/sickdays (Python) owns. Both directions, Python as the oracle:
|
||||
# 1. Dart writes a pool; Python reads it and replays the same edits.
|
||||
# 2. Python writes a pool (incl. a 0.1.0 record with no keep, a stale keep
|
||||
# and a malformed one); Dart reads it; Python compares the verdicts.
|
||||
# Fails on any disagreement about which days are sick or which lockers
|
||||
# stand down.
|
||||
#
|
||||
# Requires a Python with sickdays importable: SICKDAYS_PYTHON, else the venv
|
||||
# sickdays/install.sh creates.
|
||||
# Requires a Python with sickdays 0.2.0 importable: SICKDAYS_PYTHON, else
|
||||
# the venv sickdays/install.sh creates. Python runs with HOME pointed at a
|
||||
# scratch directory, so no default path can ever reach the real pool.
|
||||
#
|
||||
# Usage: phone_app/scripts/sick_days_interop/check.sh
|
||||
# ============================================================================
|
||||
@ -37,21 +41,38 @@ validate_requirements() {
|
||||
command -v flutter >/dev/null || { echo "Error: flutter not on PATH" >&2; exit 1; }
|
||||
}
|
||||
|
||||
# The oracle, isolated from the real pool under ~/.local/share/sickdays.
|
||||
oracle() {
|
||||
HOME="$TEMP_DIR/home" "$SICKDAYS_PYTHON" -I "$SCRIPT_DIR/verify.py" "$@"
|
||||
}
|
||||
|
||||
# Runs one Dart interop test with the given VAR=value environment.
|
||||
dart_test() {
|
||||
local test_file="$1"
|
||||
shift
|
||||
(cd "$APP_DIR" && env "$@" flutter test "$test_file" | tail -1)
|
||||
}
|
||||
|
||||
main() {
|
||||
validate_requirements
|
||||
TEMP_DIR="$(mktemp -d)"
|
||||
local pool="$TEMP_DIR/sick_days.json"
|
||||
mkdir -p "$TEMP_DIR/home"
|
||||
local dart_pool="$TEMP_DIR/dart_sick_days.json"
|
||||
local python_pool="$TEMP_DIR/python_sick_days.json"
|
||||
local dart_reading="$TEMP_DIR/dart_reading.json"
|
||||
|
||||
echo "== sick-day interop: Dart writes =="
|
||||
(
|
||||
cd "$APP_DIR"
|
||||
SICK_DAYS_INTEROP_OUT="$pool" flutter test test/sick_day_interop_writer_test.dart \
|
||||
| tail -1
|
||||
)
|
||||
[[ -s "$pool" ]] || { echo "Error: Dart wrote no fixture" >&2; exit 1; }
|
||||
echo "== sick-day interop: Dart writes, Python reads =="
|
||||
dart_test test/sick_day_interop_writer_test.dart \
|
||||
"SICK_DAYS_INTEROP_OUT=$dart_pool"
|
||||
[[ -s "$dart_pool" ]] || { echo "Error: Dart wrote no fixture" >&2; exit 1; }
|
||||
oracle dart "$dart_pool"
|
||||
|
||||
echo "== sick-day interop: Python reads =="
|
||||
"$SICKDAYS_PYTHON" -I "$SCRIPT_DIR/verify.py" "$pool"
|
||||
echo "== sick-day interop: Python writes, Dart reads =="
|
||||
oracle write "$python_pool"
|
||||
dart_test test/sick_day_interop_reader_test.dart \
|
||||
"SICK_DAYS_INTEROP_IN=$python_pool" "SICK_DAYS_INTEROP_READING=$dart_reading"
|
||||
[[ -s "$dart_reading" ]] || { echo "Error: Dart wrote no reading" >&2; exit 1; }
|
||||
oracle read "$python_pool" "$dart_reading"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
||||
@ -1,10 +1,25 @@
|
||||
"""Read a Dart-written sick-day pool with the Python package and compare.
|
||||
"""Cross-check the Dart sick-day pool against the Python package, both ways.
|
||||
|
||||
``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.
|
||||
``~/src/utils/sickdays`` owns (0.2.0, with the per-locker ``keep`` set). The
|
||||
Python package is the oracle in both directions:
|
||||
|
||||
``dart <dart.json>``
|
||||
Replays the Dart writer test's edits into a scratch pool and fails on any
|
||||
disagreement in a reading a gate relies on (``is_sick_day``,
|
||||
``kept_lockers``, ``stands_down`` per locker, ``sick_days``), or on a
|
||||
wire-format slip: an unsorted keep, a mark/clear with mixed ticks, or a
|
||||
keep-only edit that re-stamped ``state``.
|
||||
``write <out.json>``
|
||||
Writes a Python pool for the Dart reader test, including the records a
|
||||
0.2.0 writer can no longer produce: a 0.1.0 record with no ``keep``, a
|
||||
stale ``keep`` (an old client re-added the day) and a malformed one.
|
||||
``read <python.json> <dart-reading.json>``
|
||||
Compares the Dart reader's verdicts with Python's for that same pool.
|
||||
|
||||
Every sickdays call gets an explicit ``paths=`` / ``log_path=``: a default
|
||||
would resolve the real pool under ``~/.local/share/sickdays``. ``check.sh``
|
||||
also points ``HOME`` at a scratch directory, as a second line of defence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@ -15,70 +30,147 @@ from pathlib import Path
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
from crdt_sync import Hlc
|
||||
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)]
|
||||
DART_DAYS = [TODAY, *(date(2026, 10, d) for d in (20, 21, 22, 23))]
|
||||
PY_DAYS = [date(2026, 10, d) for d in (20, 22, 23, 24, 25, 26)]
|
||||
#: Dart-written records that were keep-only edited after the mark.
|
||||
KEEP_EDITED = {"2026-10-22"}
|
||||
USAGE = (
|
||||
"usage: verify.py dart <dart.json> | write <out.json> | "
|
||||
"read <python.json> <dart-reading.json>\n"
|
||||
)
|
||||
|
||||
|
||||
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."""
|
||||
def _add(paths: Paths, day: date, keep: tuple[str, ...] = ()) -> None:
|
||||
sickdays.add_sick_day(day, keep=keep, is_free=_never_free, paths=paths, now=TODAY)
|
||||
|
||||
|
||||
def dart_oracle(root: Path) -> Path:
|
||||
"""The Dart writer test's edits, made by the Python package."""
|
||||
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
|
||||
)
|
||||
_add(paths, TODAY)
|
||||
_add(paths, date(2026, 10, 20), (sickdays.LOCKER_WORKOUT, sickdays.LOCKER_ALARM))
|
||||
_add(paths, date(2026, 10, 21), (sickdays.LOCKER_DIET,))
|
||||
sickdays.remove_sick_day(date(2026, 10, 21), paths=paths, now=TODAY)
|
||||
_add(paths, date(2026, 10, 22))
|
||||
sickdays.set_kept_lockers(
|
||||
date(2026, 10, 22), [sickdays.LOCKER_LEETCODE], 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]))
|
||||
def write_python(out: Path) -> None:
|
||||
"""A Python pool for the Dart reader, then the legacy shapes by hand."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
python = reading(python_pool(Path(tmp)))
|
||||
paths = Paths.under(Path(tmp))
|
||||
_add(
|
||||
paths,
|
||||
date(2026, 10, 20),
|
||||
(sickdays.LOCKER_LEETCODE, sickdays.LOCKER_WORKOUT),
|
||||
)
|
||||
_add(paths, date(2026, 10, 22))
|
||||
sickdays.set_kept_lockers(
|
||||
date(2026, 10, 22), [sickdays.LOCKER_ALARM], paths=paths, now=TODAY
|
||||
)
|
||||
_add(paths, date(2026, 10, 23))
|
||||
for day in (24, 25, 26):
|
||||
_add(paths, date(2026, 10, day), (sickdays.LOCKER_ALARM,))
|
||||
log = json.loads(paths.log.read_text(encoding="utf-8"))
|
||||
fields = log["2026-10-24"]["fields"]
|
||||
del fields["keep"] # a 0.1.0 record: keep nothing
|
||||
fields = log["2026-10-25"]["fields"]
|
||||
re_add = Hlc.new_tick("old-client", Hlc.from_str(fields["keep"][1])).to_str()
|
||||
fields["state"] = ["sick", re_add] # a 0.1.0 re-add over a 0.2.0 keep: stale
|
||||
fields["actor"] = ["old-client", re_add]
|
||||
log["2026-10-26"]["fields"]["keep"][0] = "alarm" # malformed: keep all
|
||||
out.write_text(json.dumps(log), encoding="utf-8")
|
||||
|
||||
|
||||
def reading(pool: Path, days: list[date]) -> dict[str, object]:
|
||||
"""Every per-day verdict a gate relies on."""
|
||||
return {
|
||||
d.isoformat(): {
|
||||
"sick": sickdays.is_sick_day(d, log_path=pool),
|
||||
"kept": sorted(sickdays.kept_lockers(d, log_path=pool)),
|
||||
"stands_down": {
|
||||
locker: sickdays.stands_down(locker, d, log_path=pool)
|
||||
for locker in sickdays.LOCKERS
|
||||
},
|
||||
}
|
||||
for d in days
|
||||
} | {"sick_days": sorted(d.isoformat() for d in sickdays.sick_days(log_path=pool))}
|
||||
|
||||
|
||||
def wire_problems(pool: Path) -> list[str]:
|
||||
"""Wire-format slips in a Dart-written pool (empty when clean)."""
|
||||
problems = []
|
||||
for iso, record in json.loads(pool.read_text(encoding="utf-8")).items():
|
||||
fields = record["fields"]
|
||||
keep = fields.get("keep")
|
||||
if keep is None:
|
||||
problems.append(f"{iso}: no keep field")
|
||||
continue
|
||||
if keep[0] != sorted(set(keep[0])):
|
||||
problems.append(f"{iso}: keep {keep[0]} not sorted/deduplicated")
|
||||
if record["deleted"]:
|
||||
problems.append(f"{iso}: tombstoned")
|
||||
state, actor = fields["state"][1], fields["actor"][1]
|
||||
if iso in KEEP_EDITED:
|
||||
if not (keep[1] == actor and Hlc.from_str(keep[1]) > Hlc.from_str(state)):
|
||||
problems.append(f"{iso}: keep-only edit re-stamped state")
|
||||
elif len({state, actor, keep[1]}) != 1:
|
||||
problems.append(f"{iso}: mark/clear fields carry mixed ticks")
|
||||
return problems
|
||||
|
||||
|
||||
def _compare(dart: object, python: object) -> int:
|
||||
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
|
||||
|
||||
|
||||
def check_dart(dart_pool: Path) -> int:
|
||||
"""Compare the Dart-written fixture with the Python oracle."""
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
python = reading(dart_oracle(Path(tmp)), DART_DAYS)
|
||||
problems = wire_problems(dart_pool)
|
||||
if problems:
|
||||
sys.stderr.write("MISMATCH: " + "; ".join(problems) + "\n")
|
||||
return 1
|
||||
return _compare(reading(dart_pool, DART_DAYS), python)
|
||||
|
||||
|
||||
def check_read(python_pool: Path, dart_reading: Path) -> int:
|
||||
"""Compare the Dart reader's verdicts on a Python pool with Python's."""
|
||||
dart = json.loads(dart_reading.read_text(encoding="utf-8"))
|
||||
return _compare(dart, reading(python_pool, PY_DAYS))
|
||||
|
||||
|
||||
def main(argv: list[str]) -> int:
|
||||
"""Dispatch ``dart`` / ``write`` / ``read`` (see the module docstring)."""
|
||||
match argv[1:]:
|
||||
case ["dart", dart_pool]:
|
||||
return check_dart(Path(dart_pool))
|
||||
case ["write", out]:
|
||||
write_python(Path(out))
|
||||
return 0
|
||||
case ["read", python_pool, dart_reading]:
|
||||
return check_read(Path(python_pool), Path(dart_reading))
|
||||
case _:
|
||||
sys.stderr.write(USAGE)
|
||||
return 2
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main(sys.argv))
|
||||
|
||||
47
phone_app/test/sick_day_interop_reader_test.dart
Normal file
47
phone_app/test/sick_day_interop_reader_test.dart
Normal file
@ -0,0 +1,47 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wake_alarm_sync/services/free_day_pool.dart';
|
||||
import 'package:wake_alarm_sync/services/sick_day_keep.dart';
|
||||
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
||||
|
||||
import 'fake_day_pool_store.dart';
|
||||
|
||||
/// The Python -> Dart half of `scripts/sick_days_interop/check.sh`.
|
||||
///
|
||||
/// Reads the pool `verify.py write` made with `sickdays` (Python) from
|
||||
/// `SICK_DAYS_INTEROP_IN` -- 0.2.0 keep sets plus a 0.1.0 record with no
|
||||
/// keep, a stale keep and a malformed one -- and writes the Dart verdicts to
|
||||
/// `SICK_DAYS_INTEROP_READING` for `verify.py read` to compare with
|
||||
/// Python's. Under a plain `flutter test` both are unset and this does
|
||||
/// nothing.
|
||||
void main() {
|
||||
test('writes the Dart reading of a Python pool when asked', () async {
|
||||
final input = Platform.environment['SICK_DAYS_INTEROP_IN'];
|
||||
final output = Platform.environment['SICK_DAYS_INTEROP_READING'];
|
||||
if (input == null || output == null) return;
|
||||
final pool = await SickDayPool.open(
|
||||
persistence: MemPersistence(),
|
||||
deviceId: 'dart-interop',
|
||||
);
|
||||
await pool.replaceAll(logFromJson(File(input).readAsStringSync()));
|
||||
final days = [
|
||||
for (final d in [20, 22, 23, 24, 25, 26]) DateTime(2026, 10, d),
|
||||
];
|
||||
final reading = <String, Object?>{
|
||||
for (final day in days)
|
||||
FreeDayPool.toIso(day): {
|
||||
'sick': pool.isSick(day),
|
||||
'kept': pool.keptLockers(day).toList()..sort(),
|
||||
'stands_down': {
|
||||
for (final locker in SickLocker.all)
|
||||
locker: pool.standsDown(locker, day),
|
||||
},
|
||||
},
|
||||
'sick_days': pool.sickIsoDates(),
|
||||
};
|
||||
File(output).writeAsStringSync(jsonEncode(reading));
|
||||
});
|
||||
}
|
||||
@ -2,14 +2,15 @@ import 'dart:io';
|
||||
|
||||
import 'package:crdt_sync/crdt_sync.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:wake_alarm_sync/services/sick_day_keep.dart';
|
||||
import 'package:wake_alarm_sync/services/sick_day_pool.dart';
|
||||
|
||||
/// Half of the cross-language check (`scripts/sick_days_interop/check.sh`).
|
||||
///
|
||||
/// Writes a pool through the Dart rules to `SICK_DAYS_INTEROP_OUT`; the
|
||||
/// script reads it back with `sickdays` (Python) and replays the same edits
|
||||
/// there. Under a plain `flutter test` the variable is unset and this does
|
||||
/// nothing.
|
||||
/// there. The other direction is `sick_day_interop_reader_test.dart`.
|
||||
/// Under a plain `flutter test` the variable is unset and this does nothing.
|
||||
class _FilePersistence implements LogPersistence {
|
||||
_FilePersistence(this.file);
|
||||
|
||||
@ -33,11 +34,25 @@ void main() {
|
||||
);
|
||||
final now = DateTime(2026, 10, 16);
|
||||
bool never(DateTime _) => false;
|
||||
// Today sick; a future day sick; a future day marked then cleared;
|
||||
// 2026-10-23 untouched.
|
||||
// Today sick keeping nothing; 2026-10-20 sick keeping workout + alarm
|
||||
// (given unsorted, with a duplicate); 2026-10-21 marked keeping diet,
|
||||
// then cleared; 2026-10-22 marked, then a keep-only edit to leetcode;
|
||||
// 2026-10-23 untouched. `verify.py dart` replays exactly this.
|
||||
await pool.add(now, isFree: never, now: now);
|
||||
await pool.add(DateTime(2026, 10, 20), isFree: never, now: now);
|
||||
await pool.add(DateTime(2026, 10, 21), isFree: never, now: now);
|
||||
await pool.add(
|
||||
DateTime(2026, 10, 20),
|
||||
isFree: never,
|
||||
now: now,
|
||||
keep: [SickLocker.workout, SickLocker.alarm, SickLocker.workout],
|
||||
);
|
||||
await pool.add(
|
||||
DateTime(2026, 10, 21),
|
||||
isFree: never,
|
||||
now: now,
|
||||
keep: [SickLocker.diet],
|
||||
);
|
||||
await pool.remove(DateTime(2026, 10, 21), now: now);
|
||||
await pool.add(DateTime(2026, 10, 22), isFree: never, now: now);
|
||||
await pool.setKept(DateTime(2026, 10, 22), [SickLocker.leetcode], now: now);
|
||||
});
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user