utils/crdt-sync/crdt_sync/tests/test_record.py
Krzysztof Rudnicki 9a7fd2daae Add cross-language wire-format fixture test and wrong-shape sync coverage
The Python and Dart test suites each only verified round-tripping through
their own encoder/decoder, so a Record.to_dict()/toJson() key mismatch
between the two (deleted_hlc vs deletedHlc) would have gone undetected
despite being the exact thing this shared library exists to prevent. Pin
one canonical fixture in both suites so a future divergence fails loudly
on at least one side. Also add a test for a valid-JSON-but-wrong-shape
remote push, exercising the full (ValueError, KeyError, TypeError) skip
path, not just JSON syntax errors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017McuA1nAi6jBT1EaS5BrVf
2026-07-05 20:55:03 +02:00

141 lines
5.6 KiB
Python

"""Tests for Record, merge_field, and merge_record."""
from __future__ import annotations
from typing import TYPE_CHECKING
import pytest
from crdt_sync import Hlc, Record, merge_field, merge_record
if TYPE_CHECKING:
from collections.abc import Callable
class TestMergeField:
def test_greater_hlc_wins(self, make_hlc: Callable[..., Hlc]) -> None:
older = ("a", make_hlc(100))
newer = ("b", make_hlc(200))
assert merge_field(older, newer) == newer
assert merge_field(newer, older) == newer
def test_equal_hlc_keeps_the_first_argument(
self, make_hlc: Callable[..., Hlc]
) -> None:
clock = make_hlc(100)
a = ("a", clock)
b = ("a", clock)
assert merge_field(a, b) == a
class TestMergeRecord:
def test_raises_when_ids_differ(self) -> None:
a = Record(id="a", fields={})
b = Record(id="b", fields={})
with pytest.raises(ValueError, match="different ids"):
merge_record(a, b)
def test_merges_disjoint_fields(self, make_hlc: Callable[..., Hlc]) -> None:
a = Record(id="x", fields={"text": ("hello", make_hlc(100))})
b = Record(id="x", fields={"priority": ("high", make_hlc(50))})
merged = merge_record(a, b)
assert merged.fields == {
"text": ("hello", make_hlc(100)),
"priority": ("high", make_hlc(50)),
}
def test_shared_field_keeps_the_later_write(
self, make_hlc: Callable[..., Hlc]
) -> None:
a = Record(id="x", fields={"text": ("old", make_hlc(100))})
b = Record(id="x", fields={"text": ("new", make_hlc(200))})
merged = merge_record(a, b)
assert merged.fields["text"] == ("new", make_hlc(200))
def test_delete_is_sticky_against_an_older_non_deleted_copy(
self, make_hlc: Callable[..., Hlc]
) -> None:
deleted = Record(id="x", fields={}, deleted=True, deleted_hlc=make_hlc(200))
stale = Record(id="x", fields={"text": ("resurrected", make_hlc(100))})
assert merge_record(deleted, stale).deleted is True
assert merge_record(stale, deleted).deleted is True
def test_neither_side_deleted_stays_not_deleted(self) -> None:
a = Record(id="x", fields={})
b = Record(id="x", fields={})
merged = merge_record(a, b)
assert merged.deleted is False
assert merged.deleted_hlc is None
def test_both_sides_deleted_keeps_the_later_delete_clock(
self, make_hlc: Callable[..., Hlc]
) -> None:
a = Record(id="x", fields={}, deleted=True, deleted_hlc=make_hlc(100))
b = Record(id="x", fields={}, deleted=True, deleted_hlc=make_hlc(200))
merged = merge_record(a, b)
assert merged.deleted_hlc == make_hlc(200)
def test_both_deleted_but_one_side_missing_a_clock_keeps_the_other(
self, make_hlc: Callable[..., Hlc]
) -> None:
a = Record(id="x", fields={}, deleted=True, deleted_hlc=None)
b = Record(id="x", fields={}, deleted=True, deleted_hlc=make_hlc(200))
assert merge_record(a, b).deleted_hlc == make_hlc(200)
assert merge_record(b, a).deleted_hlc == make_hlc(200)
def test_is_commutative(self, make_hlc: Callable[..., Hlc]) -> None:
record_a = Record(id="x", fields={"text": ("a", make_hlc(100))})
record_b = Record(
id="x", fields={"text": ("b", make_hlc(200)), "extra": ("e", make_hlc(1))}
)
assert merge_record(record_a, record_b) == merge_record(record_b, record_a)
def test_is_idempotent(self, make_hlc: Callable[..., Hlc]) -> None:
record = Record(id="x", fields={"text": ("a", make_hlc(100))})
assert merge_record(record, record) == record
class TestRecordDictRoundTrip:
def test_round_trips_a_record_with_fields(
self, make_hlc: Callable[..., Hlc]
) -> None:
record = Record(id="x", fields={"text": ("hello", make_hlc(100, node_id="pc"))})
assert Record.from_dict(record.to_dict()) == record
def test_round_trips_a_deleted_record(self, make_hlc: Callable[..., Hlc]) -> None:
record = Record(id="x", fields={}, deleted=True, deleted_hlc=make_hlc(200))
assert Record.from_dict(record.to_dict()) == record
def test_round_trips_a_record_with_no_delete_clock(self) -> None:
record = Record(id="x", fields={}, deleted=False, deleted_hlc=None)
assert Record.from_dict(record.to_dict()) == record
class TestCrossLanguageWireFormat:
"""Pins the exact wire shape shared with crdt_sync_dart's Record.
Values chosen here are duplicated verbatim in crdt_sync_dart's
`test/record_test.dart` (see its ``TestCrossLanguageWireFormat``
group). If this test and that one both pass, the two languages agree
on the wire format; if only one changes, the two suites diverge and
at least one of them fails -- catching exactly the kind of
key-naming mismatch (``deleted_hlc`` vs ``deletedHlc``) that neither
language's own round-trip test can see on its own.
"""
def test_matches_the_fixture_shared_with_the_dart_package(self) -> None:
record = Record(
id="abc123",
fields={"text": ("hello", Hlc(wall_time_ms=100, counter=0, node_id="pc"))},
deleted=True,
deleted_hlc=Hlc(wall_time_ms=1000, counter=0, node_id="node-a"),
)
expected = {
"id": "abc123",
"fields": {"text": ["hello", "1970-01-01T00:00:00.100Z-0000-pc"]},
"deleted": True,
"deleted_hlc": "1970-01-01T00:00:01.000Z-0000-node-a",
}
assert record.to_dict() == expected
assert Record.from_dict(expected) == record