import base64 import copy import hashlib from datetime import UTC, datetime from uuid import uuid4 import pytest from k1link.device_plugins.vesc.archive import Archive def backup(): uid = "01" * 12 configs = {} for name, command in (("motor", 14), ("application", 17)): raw = bytes([command]) + b"test-synthetic-config" configs[name] = {"encoding": "base64", "payload": base64.b64encode(raw).decode(), "bytes": len(raw), "sha256": hashlib.sha256(raw).hexdigest(), "signature_hex": raw[1:5].hex()} return {"schema": "missioncore.vesc.config-backup/v1", "operation_id": "op_" + uuid4().hex, "device_id": "vesc_" + hashlib.sha256(("uuid:" + uid).encode()).hexdigest()[:32], "identity": {"uuid": uid, "version": "6.06"}, "configs": configs, "observed_at": datetime.now(UTC).isoformat()} def test_immutable_versions_survive_restart_and_do_not_cross_node_scope(tmp_path): archive = Archive(tmp_path / "archive") value = backup() archive.add("node-a", value) archive.add("node-a", copy.deepcopy(value)) reopened = Archive(tmp_path / "archive") assert len(reopened.listing("node-a", value["device_id"])["items"]) == 1 assert reopened.read("node-a", value["device_id"], value["operation_id"]) == value assert reopened.listing("node-b", value["device_id"])["items"] == [] with pytest.raises(ValueError): reopened.read("node-b", value["device_id"], value["operation_id"]) changed = copy.deepcopy(value) changed["observed_at"] = datetime.now(UTC).isoformat() with pytest.raises(ValueError, match="immutable"): reopened.add("node-a", changed) def test_corrupt_or_misattributed_payload_is_never_archived(tmp_path): archive = Archive(tmp_path / "archive") value = backup() value["configs"]["motor"]["sha256"] = "0" * 64 with pytest.raises(ValueError, match="integrity"): archive.add("a", value) value = backup() value["device_id"] = "vesc_" + "0" * 32 with pytest.raises(ValueError, match="mismatch"): archive.add("a", value) assert archive.export("a", 0)["items"] == [] def test_bounded_replication_replays_without_losing_versions_and_paginates(tmp_path): board, core = Archive(tmp_path / "board"), Archive(tmp_path / "core") values = [backup() for _ in range(53)] for value in values: board.add("local", value) after = 0 while True: batch = board.export("local", after) assert len(batch["items"]) <= 4 assert core.receive("paired-node", batch) == batch["next"] assert core.receive("paired-node", batch) == batch["next"] if batch["next"] == after: break after = batch["next"] page = core.listing("paired-node", values[0]["device_id"]) assert len(page["items"]) == 50 and page["next"] older = core.listing("paired-node", values[0]["device_id"], int(page["next"])) assert len(older["items"]) == 3 and older["next"] is None assert len({item["id"] for item in page["items"] + older["items"]}) == 53