mirror of
https://github.com/kuhyx/diet-guard.git
synced 2026-07-04 13:23:11 +02:00
110 lines
4.1 KiB
Python
110 lines
4.1 KiB
Python
|
|
"""Tests for the pure cross-device log-merge logic."""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
from diet_guard._sync_merge import merge_logs
|
||
|
|
|
||
|
|
|
||
|
|
def _entry(**overrides: object) -> dict[str, object]:
|
||
|
|
"""Build a minimal valid entry, overriding only what a test cares about."""
|
||
|
|
entry: dict[str, object] = {
|
||
|
|
"id": "id-1",
|
||
|
|
"time": "2026-06-22T08:00:00+02:00",
|
||
|
|
"desc": "oatmeal",
|
||
|
|
"kcal": 300.0,
|
||
|
|
"protein_g": 10.0,
|
||
|
|
"carbs_g": 50.0,
|
||
|
|
"fat_g": 5.0,
|
||
|
|
"grams": 200.0,
|
||
|
|
"source": "manual",
|
||
|
|
}
|
||
|
|
entry.update(overrides)
|
||
|
|
return entry
|
||
|
|
|
||
|
|
|
||
|
|
class TestUnionById:
|
||
|
|
def test_disjoint_logs_union_into_one(self) -> None:
|
||
|
|
a = {"2026-06-22": [_entry(id="a", time="2026-06-22T08:00:00+02:00")]}
|
||
|
|
b = {"2026-06-22": [_entry(id="b", time="2026-06-22T12:00:00+02:00")]}
|
||
|
|
merged = merge_logs(a, b)
|
||
|
|
assert {e["id"] for e in merged["2026-06-22"]} == {"a", "b"}
|
||
|
|
|
||
|
|
def test_same_id_in_both_logs_is_not_duplicated(self) -> None:
|
||
|
|
shared = _entry(id="shared")
|
||
|
|
merged = merge_logs({"2026-06-22": [shared]}, {"2026-06-22": [shared]})
|
||
|
|
assert len(merged["2026-06-22"]) == 1
|
||
|
|
|
||
|
|
def test_legacy_entries_without_id_dedup_by_time_and_desc(self) -> None:
|
||
|
|
legacy_a = _entry(id=None, time="2026-06-20T08:00:00+02:00", desc="toast")
|
||
|
|
legacy_a.pop("id")
|
||
|
|
legacy_b = dict(legacy_a)
|
||
|
|
merged = merge_logs({"2026-06-20": [legacy_a]}, {"2026-06-20": [legacy_b]})
|
||
|
|
assert len(merged["2026-06-20"]) == 1
|
||
|
|
|
||
|
|
def test_legacy_and_id_entries_with_different_keys_both_survive(self) -> None:
|
||
|
|
legacy = _entry(time="2026-06-20T08:00:00+02:00", desc="toast")
|
||
|
|
legacy.pop("id")
|
||
|
|
with_id = _entry(id="x", time="2026-06-20T09:00:00+02:00", desc="eggs")
|
||
|
|
merged = merge_logs({"2026-06-20": [legacy]}, {"2026-06-20": [with_id]})
|
||
|
|
assert len(merged["2026-06-20"]) == 2
|
||
|
|
|
||
|
|
|
||
|
|
class TestTombstoneWins:
|
||
|
|
def test_tombstone_beats_a_non_deleted_copy_either_order(self) -> None:
|
||
|
|
normal = _entry(id="x", deleted=False)
|
||
|
|
tombstoned = _entry(id="x", deleted=True)
|
||
|
|
|
||
|
|
forward = merge_logs(
|
||
|
|
{"2026-06-22": [normal]},
|
||
|
|
{"2026-06-22": [tombstoned]},
|
||
|
|
)
|
||
|
|
backward = merge_logs(
|
||
|
|
{"2026-06-22": [tombstoned]},
|
||
|
|
{"2026-06-22": [normal]},
|
||
|
|
)
|
||
|
|
|
||
|
|
assert forward["2026-06-22"][0]["deleted"] is True
|
||
|
|
assert backward["2026-06-22"][0]["deleted"] is True
|
||
|
|
|
||
|
|
def test_two_tombstoned_copies_stay_tombstoned(self) -> None:
|
||
|
|
tombstoned = _entry(id="x", deleted=True)
|
||
|
|
merged = merge_logs(
|
||
|
|
{"2026-06-22": [tombstoned]},
|
||
|
|
{"2026-06-22": [dict(tombstoned)]},
|
||
|
|
)
|
||
|
|
assert merged["2026-06-22"][0]["deleted"] is True
|
||
|
|
|
||
|
|
|
||
|
|
class TestRebucketingAndOrdering:
|
||
|
|
def test_entry_is_filed_under_its_own_times_date_not_the_arrival_bucket(
|
||
|
|
self,
|
||
|
|
) -> None:
|
||
|
|
misfiled = _entry(id="x", time="2026-06-21T23:00:00+02:00")
|
||
|
|
merged = merge_logs({"2026-06-22": [misfiled]}, {})
|
||
|
|
assert merged == {"2026-06-21": [misfiled]}
|
||
|
|
|
||
|
|
def test_a_days_entries_are_sorted_oldest_first(self) -> None:
|
||
|
|
late = _entry(id="late", time="2026-06-22T20:00:00+02:00")
|
||
|
|
early = _entry(id="early", time="2026-06-22T08:00:00+02:00")
|
||
|
|
merged = merge_logs({"2026-06-22": [late]}, {"2026-06-22": [early]})
|
||
|
|
assert [e["id"] for e in merged["2026-06-22"]] == ["early", "late"]
|
||
|
|
|
||
|
|
|
||
|
|
class TestAlgebraicProperties:
|
||
|
|
def test_merge_is_commutative(self) -> None:
|
||
|
|
a = {"2026-06-22": [_entry(id="a")]}
|
||
|
|
b = {"2026-06-22": [_entry(id="b", time="2026-06-22T09:00:00+02:00")]}
|
||
|
|
assert merge_logs(a, b) == merge_logs(b, a)
|
||
|
|
|
||
|
|
def test_merge_is_idempotent(self) -> None:
|
||
|
|
canonical = {"2026-06-22": [_entry(id="a")]}
|
||
|
|
assert merge_logs(canonical, canonical) == canonical
|
||
|
|
|
||
|
|
def test_merging_with_an_empty_log_is_a_no_op(self) -> None:
|
||
|
|
log = {"2026-06-22": [_entry(id="a")]}
|
||
|
|
assert merge_logs(log, {}) == log
|
||
|
|
assert merge_logs({}, log) == log
|
||
|
|
|
||
|
|
def test_merging_two_empty_logs_is_empty(self) -> None:
|
||
|
|
assert merge_logs({}, {}) == {}
|