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:
Krzysztof Rudnicki 2026-10-16 15:31:11 +02:00
parent ee7e380278
commit 597901dc08
5 changed files with 281 additions and 75 deletions

View File

@ -6,7 +6,11 @@ day. Read its README for the rules and the RTDB wire format. This file covers
the phone half. the phone half.
On a sick day the phone does not ring, the stick does not judge the day, and 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 separate RTDB node (`sickdays-sync/devices`), separate native cache, separate
screen, and every log line and event says `sick_day`. 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 | | 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 | | 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` | | 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 | | 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` 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 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** icon. **Today is tappable** (unlike the free-day grid); the past is inert.
(unlike the free-day grid); the past is inert. No count, no confirmation. 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 ## A date is never in both pools
@ -40,6 +53,15 @@ exactly as the Python README prescribes.
## The ring ## 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 `AlarmReceiver` asks `RingDecision.skip(free, sick)`. On a sick day it logs
`sick day: not ringing; re-armed for tomorrow`, appends `alarm_fired` with `sick day: not ringing; re-armed for tomorrow`, appends `alarm_fired` with
`reason: "sick_day"`, re-arms and returns: no `AlarmService`, no `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 `PoolSyncScheduleTest`, `StickCheckSickTest`. Run with
`cd phone_app/android && ./gradlew :app:testDebugUnitTest`, after one `cd phone_app/android && ./gradlew :app:testDebugUnitTest`, after one
`flutter build apk` has written `gradlew` and `local.properties`. `flutter build apk` has written `gradlew` and `local.properties`.
- Cross-language: `phone_app/scripts/sick_days_interop/check.sh` writes a - Keep: `test/services/sick_day_keep_test.dart` (wire, read rules 1-5, a
pool through Dart and reads it with `sickdays` (Python), failing on any keep edit after a future PC tick, concurrent clear), `test/sick_day_sheet_test.dart`,
disagreement. Needs `SICKDAYS_PYTHON` or the sickdays venv. `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 ## Do NOT
- Don't fold sick days into `FreeDays`/`isFree`, or log them as `free_day`. - 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 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 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.

View File

@ -1,15 +1,19 @@
#!/bin/bash #!/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 # 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 # ~/src/utils/sickdays (Python) owns. Both directions, Python as the oracle:
# rules and reads it back through the Python ones, and fails on any # 1. Dart writes a pool; Python reads it and replays the same edits.
# disagreement about which days are sick. # 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 # Requires a Python with sickdays 0.2.0 importable: SICKDAYS_PYTHON, else
# sickdays/install.sh creates. # 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 # 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; } 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() { main() {
validate_requirements validate_requirements
TEMP_DIR="$(mktemp -d)" 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 ==" echo "== sick-day interop: Dart writes, Python reads =="
( dart_test test/sick_day_interop_writer_test.dart \
cd "$APP_DIR" "SICK_DAYS_INTEROP_OUT=$dart_pool"
SICK_DAYS_INTEROP_OUT="$pool" flutter test test/sick_day_interop_writer_test.dart \ [[ -s "$dart_pool" ]] || { echo "Error: Dart wrote no fixture" >&2; exit 1; }
| tail -1 oracle dart "$dart_pool"
)
[[ -s "$pool" ]] || { echo "Error: Dart wrote no fixture" >&2; exit 1; }
echo "== sick-day interop: Python reads ==" echo "== sick-day interop: Python writes, Dart reads =="
"$SICKDAYS_PYTHON" -I "$SCRIPT_DIR/verify.py" "$pool" 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 "$@" main "$@"

View File

@ -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 ``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 ``~/src/utils/sickdays`` owns (0.2.0, with the per-locker ``keep`` set). The
the writer test's edits into a scratch pool, and every reading a gate relies Python package is the oracle in both directions:
on (``is_sick_day`` per date, ``sick_days``) has to agree between the two
files. Reads only; writes nothing outside a temporary directory. ``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 from __future__ import annotations
@ -15,70 +30,147 @@ from pathlib import Path
import sys import sys
import tempfile import tempfile
from crdt_sync import Hlc
import sickdays import sickdays
from sickdays._paths import Paths from sickdays._paths import Paths
EXPECTED_ARGC = 2
TODAY = date(2026, 10, 16) 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: def _never_free(_day: date) -> bool:
return False return False
def python_pool(root: Path) -> Path: def _add(paths: Paths, day: date, keep: tuple[str, ...] = ()) -> None:
"""Write the oracle pool with the edits the Dart writer made.""" 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) paths = Paths.under(root)
sickdays.add_sick_day(TODAY, is_free=_never_free, paths=paths, now=TODAY) _add(paths, TODAY)
sickdays.add_sick_day( _add(paths, date(2026, 10, 20), (sickdays.LOCKER_WORKOUT, sickdays.LOCKER_ALARM))
date(2026, 10, 20), is_free=_never_free, paths=paths, now=TODAY _add(paths, date(2026, 10, 21), (sickdays.LOCKER_DIET,))
)
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) 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 return paths.log
def reading(pool: Path) -> dict[str, object]: def write_python(out: Path) -> None:
"""Every view of a pool a gate relies on.""" """A Python pool for the Dart reader, then the legacy shapes by hand."""
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: 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") sys.stdout.write(f"dart: {json.dumps(dart)}\npython: {json.dumps(python)}\n")
if dart != python: if dart != python:
sys.stderr.write("MISMATCH: Dart and Python read different pools\n") sys.stderr.write("MISMATCH: Dart and Python read different pools\n")
return 1 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") sys.stdout.write("OK: Dart and Python agree\n")
return 0 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__": if __name__ == "__main__":
sys.exit(main(sys.argv)) sys.exit(main(sys.argv))

View 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));
});
}

View File

@ -2,14 +2,15 @@ import 'dart:io';
import 'package:crdt_sync/crdt_sync.dart'; import 'package:crdt_sync/crdt_sync.dart';
import 'package:flutter_test/flutter_test.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'; import 'package:wake_alarm_sync/services/sick_day_pool.dart';
/// Half of the cross-language check (`scripts/sick_days_interop/check.sh`). /// 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 /// 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 /// 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 /// there. The other direction is `sick_day_interop_reader_test.dart`.
/// nothing. /// Under a plain `flutter test` the variable is unset and this does nothing.
class _FilePersistence implements LogPersistence { class _FilePersistence implements LogPersistence {
_FilePersistence(this.file); _FilePersistence(this.file);
@ -33,11 +34,25 @@ void main() {
); );
final now = DateTime(2026, 10, 16); final now = DateTime(2026, 10, 16);
bool never(DateTime _) => false; bool never(DateTime _) => false;
// Today sick; a future day sick; a future day marked then cleared; // Today sick keeping nothing; 2026-10-20 sick keeping workout + alarm
// 2026-10-23 untouched. // (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(now, isFree: never, now: now);
await pool.add(DateTime(2026, 10, 20), isFree: never, now: now); await pool.add(
await pool.add(DateTime(2026, 10, 21), isFree: never, now: now); 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.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);
}); });
} }