"""Append-only log tests: hash chaining, sequence rules, and merge semantics (spec/02-wire-format/03-log-and-merge.md).""" from __future__ import annotations import copy import pytest from fpcf import log as fpcf_log from fpcf.errors import ConformanceError from helpers import assert_all, invalid_logs, valid_logs # --------------------------------------------------------------------------- # Chain verification # --------------------------------------------------------------------------- def test_valid_log_vectors_verify(vectors_dir, manifest): logs = assert_all( valid_logs(vectors_dir, manifest), "vector suite must contain valid log vectors", ) for entry, payload in logs: fpcf_log.verify_log(payload["ops"]) # must not raise def test_invalid_log_vectors_are_rejected(vectors_dir, manifest): logs = assert_all( invalid_logs(vectors_dir, manifest), "vector suite must contain invalid log vectors", ) for entry, payload in logs: with pytest.raises(ConformanceError): fpcf_log.verify_log(payload["ops"]) def test_tampered_log_entry_breaks_the_chain(vectors_dir, manifest): _, payload = valid_logs(vectors_dir, manifest)[0] ops = copy.deepcopy(payload["ops"]) assert ops, "valid log vector must contain at least one operation" # Corrupt the last operation's body without re-signing: its signature, # op_id, and any prev-hash reference to it must now fail to verify. ops[-1]["body"]["x_tampered"] = True with pytest.raises(ConformanceError): fpcf_log.verify_log(ops) def test_dropped_log_entry_is_detected(vectors_dir, manifest): """Removing an interior entry must break prev-hash / seq continuity.""" multi = [ (e, p) for e, p in valid_logs(vectors_dir, manifest) if len(p["ops"]) >= 3 ] assert multi, "vector suite must contain a valid log with >= 3 operations" _, payload = multi[0] ops = copy.deepcopy(payload["ops"]) del ops[1] with pytest.raises(ConformanceError): fpcf_log.verify_log(ops) # --------------------------------------------------------------------------- # Merge semantics # --------------------------------------------------------------------------- def _multi_op_log(vectors_dir, manifest) -> list[dict]: for _, payload in valid_logs(vectors_dir, manifest): if len(payload["ops"]) >= 2: return payload["ops"] pytest.fail("vector suite must contain a valid log with >= 2 operations") def test_merge_is_commutative(vectors_dir, manifest): ops = _multi_op_log(vectors_dir, manifest) a, b = ops[0::2], ops[1::2] m1 = fpcf_log.merge_ops(a, b) m2 = fpcf_log.merge_ops(b, a) assert m1 == m2 def test_merge_preserves_all_operations(vectors_dir, manifest): ops = _multi_op_log(vectors_dir, manifest) a, b = ops[0::2], ops[1::2] merged = fpcf_log.merge_ops(a, b) assert len(merged) == len(ops) assert sorted(op["op_id"] for op in merged) == sorted( op["op_id"] for op in ops ) def test_merge_deduplicates_by_op_id(vectors_dir, manifest): ops = _multi_op_log(vectors_dir, manifest) merged = fpcf_log.merge_ops(ops, ops) assert len(merged) == len(ops) assert sorted(op["op_id"] for op in merged) == sorted( op["op_id"] for op in ops ) def test_merge_is_deterministic_under_input_shuffling(vectors_dir, manifest): ops = _multi_op_log(vectors_dir, manifest) reversed_ops = list(reversed(ops)) m1 = fpcf_log.merge_ops(ops, []) m2 = fpcf_log.merge_ops(reversed_ops, []) assert m1 == m2