mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 17:14:03 +01:00
The hook entry was an absolute /home/kuhy/utils path, copied from a config written for ~/.claude where it only ever ran locally. On an Actions runner that is exit code 127 — "No such file or directory" — so gatelock-precommit and crdt-sync-precommit failed on every commit of this refactor while the hook silently checked nothing. Now repo-relative, matching the sibling hooks (`entry: scripts/check_subproject_scope.sh`): pre-commit always runs from the git root, so it resolves the same in both places. Fixing that made the CI configs runnable locally, and `--all-files` then found what per-file runs had missed — ruff pruning imports that exist *for* their re-export. `import crdt_sync` was broken three separate ways (FileSyncStateStore, encode_key, FileCredentialStore). Each affected module now states an explicit __all__ so the autofixer cannot take them again, and the two files that pushed past the cap once __all__ was added are split further: _firebase.py 260 -> 247 + _fberrors 25 _sync.py 257 -> 188 + _syncargs 91 Test patch targets followed the code they patch: gatelock._outputs.shutil -> _xrandr.shutil, gatelock._arbiter.Path -> _claims.Path. gatelock and crdt-sync both pass `pre-commit run --all-files` in full; 389 and 259 tests at 100% branch coverage; the root gate exits 0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CFSGeWxHPY5rHUGrPqhcmG
229 lines
8.5 KiB
Python
229 lines
8.5 KiB
Python
"""Tests for mirror fallback reads, revision maps and lifecycle.
|
|
|
|
Split from ``test_mirror.py`` (250-line cap), which keeps plain writes,
|
|
reads and directory listing.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from crdt_sync import (
|
|
Hlc,
|
|
Log,
|
|
LogCodec,
|
|
MirrorSyncClient,
|
|
Record,
|
|
RemoteStore,
|
|
RemoteSyncError,
|
|
SyncTarget,
|
|
dump_log,
|
|
load_log,
|
|
sync_log,
|
|
)
|
|
from crdt_sync.tests.conftest import FakeStore, FakeStoreWithoutBulkRead
|
|
|
|
|
|
def _mirror(
|
|
*,
|
|
primary_files: dict[str, str] | None = None,
|
|
mirror_files: dict[str, str] | None = None,
|
|
primary_failing: bool = False,
|
|
mirror_failing: bool = False,
|
|
) -> tuple[MirrorSyncClient, FakeStore, FakeStore, list[str]]:
|
|
primary = FakeStore(primary_files, failing=primary_failing)
|
|
mirror = FakeStore(mirror_files, failing=mirror_failing)
|
|
failures: list[str] = []
|
|
client = MirrorSyncClient(
|
|
primary,
|
|
mirror,
|
|
on_mirror_failure=lambda operation, _: failures.append(operation),
|
|
)
|
|
return client, primary, mirror, failures
|
|
|
|
|
|
class TestPrimaryReadFallback:
|
|
"""A failing primary must never hide data the mirror can still serve."""
|
|
|
|
def test_get_file_text_falls_back_to_the_mirror(self) -> None:
|
|
"""Get file text falls back to the mirror."""
|
|
client, _, _, _ = _mirror(
|
|
primary_failing=True, mirror_files={"ns/phone/log.json": "{}"}
|
|
)
|
|
assert client.get_file_text("ns/phone/log.json") == "{}"
|
|
|
|
def test_get_file_text_raises_when_both_fail(self) -> None:
|
|
"""Get file text raises when both fail."""
|
|
client, _, _, _ = _mirror(primary_failing=True, mirror_failing=True)
|
|
with pytest.raises(RemoteSyncError):
|
|
client.get_file_text("ns/phone/log.json")
|
|
|
|
def test_get_file_text_raises_when_primary_fails_and_mirror_lacks_it(
|
|
self,
|
|
) -> None:
|
|
# A reachable mirror that simply does not hold the file is not an
|
|
# answer either, since the primary's copy was never read.
|
|
"""Get file text raises when primary fails and mirror lacks it."""
|
|
client, _, _, _ = _mirror(primary_failing=True)
|
|
with pytest.raises(RemoteSyncError):
|
|
client.get_file_text("ns/phone/log.json")
|
|
|
|
def test_revision_map_falls_back_to_the_mirror(self) -> None:
|
|
"""Revision map falls back to the mirror."""
|
|
client, _, _, _ = _mirror(
|
|
primary_failing=True, mirror_files={"ns/revs/phone": "r1"}
|
|
)
|
|
assert client.get_string_map("ns/revs") == {"phone": "r1"}
|
|
|
|
def test_revision_map_raises_when_both_fail(self) -> None:
|
|
"""Revision map raises when both fail."""
|
|
client, _, _, _ = _mirror(primary_failing=True, mirror_failing=True)
|
|
with pytest.raises(RemoteSyncError):
|
|
client.get_string_map("ns/revs")
|
|
|
|
def test_revision_map_tolerates_a_mirror_that_cannot_bulk_read(
|
|
self,
|
|
) -> None:
|
|
# A mirror with no bulk-read capability (GitHub) contributes nothing,
|
|
# but that is not a failure -- it simply has no revisions to add, so a
|
|
# working primary's map must still come back. Matches the Dart side,
|
|
# where the capability check runs inside the guarded closure.
|
|
"""Revision map tolerates a mirror that cannot bulk read."""
|
|
client = MirrorSyncClient(
|
|
FakeStore({"ns/revs/pc": "r1"}),
|
|
FakeStoreWithoutBulkRead(),
|
|
on_mirror_failure=lambda *_: None,
|
|
)
|
|
assert client.get_string_map("ns/revs") == {"pc": "r1"}
|
|
|
|
def test_writes_stay_fail_closed_on_a_primary_failure(self) -> None:
|
|
# The read fix must NOT loosen writes: an unaccepted write has not
|
|
# happened, so it must still fail the tick.
|
|
"""Writes stay fail closed on a primary failure."""
|
|
client, _, _, _ = _mirror(primary_failing=True)
|
|
with pytest.raises(RemoteSyncError):
|
|
client.put_file_text("ns/pc/log.json", "{}", message="m")
|
|
with pytest.raises(RemoteSyncError):
|
|
client.delete_file("ns/pc/log.json")
|
|
|
|
|
|
class TestRevisionMaps:
|
|
"""Revision maps."""
|
|
|
|
def test_merge_both_backends_with_the_primary_winning(self) -> None:
|
|
"""Merge both backends with the primary winning."""
|
|
client, _, _, _ = _mirror(
|
|
primary_files={"ns/revs/pc": "primary-rev"},
|
|
mirror_files={"ns/revs/pc": "mirror-rev", "ns/revs/phone": "old"},
|
|
)
|
|
# An un-migrated device publishes revisions only to the mirror;
|
|
# without this it would look revision-less and be re-downloaded
|
|
# every tick for the whole trial.
|
|
assert client.get_string_map("ns/revs") == {
|
|
"pc": "primary-rev",
|
|
"phone": "old",
|
|
}
|
|
|
|
def test_a_mirror_failure_degrades_to_primary_revisions(self) -> None:
|
|
"""A mirror failure degrades to primary revisions."""
|
|
client, _, _, failures = _mirror(
|
|
primary_files={"ns/revs/pc": "primary-rev"}, mirror_failing=True
|
|
)
|
|
assert client.get_string_map("ns/revs") == {"pc": "primary-rev"}
|
|
assert failures == ["get_string_map ns/revs"]
|
|
|
|
def test_a_backend_without_bulk_reads_contributes_nothing(self) -> None:
|
|
"""A backend without bulk reads contributes nothing."""
|
|
client = MirrorSyncClient(
|
|
FakeStore({"ns/revs/pc": "primary-rev"}),
|
|
FakeStoreWithoutBulkRead({"ns/revs/phone": "ignored"}),
|
|
)
|
|
assert client.get_string_map("ns/revs") == {"pc": "primary-rev"}
|
|
|
|
def test_is_empty_when_neither_backend_has_bulk_reads(self) -> None:
|
|
"""Is empty when neither backend has bulk reads."""
|
|
client = MirrorSyncClient(
|
|
FakeStoreWithoutBulkRead(), FakeStoreWithoutBulkRead()
|
|
)
|
|
assert not client.get_string_map("ns/revs")
|
|
|
|
|
|
class TestLifecycle:
|
|
"""Lifecycle."""
|
|
|
|
def test_can_access_remote_reports_only_the_primary(self) -> None:
|
|
# A Test-connection button must not report success because the
|
|
# backend being retired happens to answer.
|
|
"""Can access remote reports only the primary."""
|
|
broken, _, _, _ = _mirror(primary_failing=True)
|
|
assert broken.can_access_remote() is False
|
|
healthy, _, _, _ = _mirror(mirror_failing=True)
|
|
assert healthy.can_access_remote() is True
|
|
|
|
def test_a_mirror_failure_with_no_handler_is_logged_not_raised(
|
|
self,
|
|
caplog: pytest.LogCaptureFixture,
|
|
) -> None:
|
|
"""A mirror failure with no handler is logged not raised."""
|
|
client = MirrorSyncClient(FakeStore(), FakeStore(failing=True))
|
|
client.put_file_text("ns/pc/log.json", "{}", message="m")
|
|
assert "Mirror backend failed" in caplog.text
|
|
|
|
def test_satisfies_the_remote_store_contract(self) -> None:
|
|
"""Satisfies the remote store contract."""
|
|
client, _, _, _ = _mirror()
|
|
assert isinstance(client, RemoteStore)
|
|
|
|
|
|
class TestEndToEndThroughSyncLog:
|
|
"""End to end through sync log."""
|
|
|
|
def test_a_half_migrated_pair_still_converges_both_ways(self) -> None:
|
|
# phone has not migrated and still writes only to GitHub; pc is on
|
|
# Firebase with the GitHub mirror. Both must see both records.
|
|
"""A half migrated pair still converges both ways."""
|
|
github = FakeStore()
|
|
firebase = FakeStore()
|
|
|
|
def log(record_id: str, node_id: str) -> Log:
|
|
return {
|
|
record_id: Record(
|
|
id=record_id,
|
|
fields={
|
|
"v": (
|
|
record_id,
|
|
Hlc(wall_time_ms=1000, counter=0, node_id=node_id),
|
|
)
|
|
},
|
|
)
|
|
}
|
|
|
|
sync_log(
|
|
SyncTarget(
|
|
client=github,
|
|
device_id="phone",
|
|
path_prefix="ns/devices",
|
|
),
|
|
log("from-phone", "node-phone"),
|
|
LogCodec(
|
|
decode=load_log,
|
|
encode=dump_log,
|
|
),
|
|
)
|
|
merged = sync_log(
|
|
SyncTarget(
|
|
client=MirrorSyncClient(firebase, github),
|
|
device_id="pc",
|
|
path_prefix="ns/devices",
|
|
),
|
|
log("from-pc", "node-pc"),
|
|
LogCodec(
|
|
decode=load_log,
|
|
encode=dump_log,
|
|
),
|
|
)
|
|
assert {"from-phone", "from-pc"} <= set(merged)
|
|
# The pc's merged result is mirrored back to GitHub, so the
|
|
# un-migrated phone sees it on its next tick.
|
|
assert "ns/devices/pc/log.json" in github.files
|