"""Tests for ``muse/plugins/knowtation/rerere.py`` — Phase 2.5. Covers all seven Rule #0 tiers for the knowtation domain rerere plugin: 1. **Unit — fingerprint distinguishability** Ten conflict pairs that *must* produce different fingerprints (different conflicting content, different conflicting sections, different conflict counts, etc.) and five pairs that *must* produce identical fingerprints (cosmetic whitespace, frontmatter tag additions, frontmatter key reorderings, non-conflicting section reorderings, CRLF / LF differences). 2. **Unit — record / lookup round-trip** Hash-only resolution storage round-trips through ``record_resolution`` → ``lookup_resolution``; missing fingerprints surface as ``None``; distinct fingerprints do not collide on disk. 3. **Unit — record_full_resolution / replay_full_resolution** Bytes-preserving storage round-trips; unknown fingerprint returns ``None``; canonical form is what's stored on disk. 4. **Integration — replay after cosmetic rewrite** Record a full resolution for conflict A, rewrite the surrounding note cosmetically (add a tag, reorder non-conflicting sections), confirm the fingerprint still matches and ``replay_full_resolution`` succeeds. 5. **Integration — rerere CLI hook (compute_fingerprint dispatch)** The ``KnowtationPlugin`` registered in ``muse.plugins.registry`` is detected as a ``RererePlugin`` by ``muse.core.rerere.compute_fingerprint`` and the resulting fingerprint matches the bytes-level fingerprint, so ``muse rerere`` will index its rr-cache by knowtation-aware fingerprints. 6. **Data-integrity — replay across 50 historical conflict stubs** Synthesise 50 distinct conflict pairs. Record full resolutions for the first 25 — the lookup must succeed for those 25 and miss for the other 25. No collisions across the 50 fingerprints. 7. **Security** * Fingerprint cannot be forged by injecting ``"|"`` or ``":"`` separators into a section title or body. * Path-traversal fingerprints (``"/"`` or ``".."``) raise ``ValueError`` before any filesystem access. * 1 MiB-per-section conflicting input does not crash and produces a valid fingerprint within a sane time budget. * NUL bytes embedded in section bodies survive end-to-end. * Oversized inputs (> 16 MiB) raise ``ValueError`` before parsing. Conventions ----------- * Helpers build canonical-form note bytes via the differ's :func:`canonicalize` so byte-stable assertions are reliable. * No real Muse repository is required for the unit tests; integration tests construct a minimal repo via ``muse init --domain knowtation`` through ``CliRunner``. """ from __future__ import annotations import hashlib import json import pathlib import time from typing import Any import pytest import yaml from muse.plugins.knowtation.differ import canonicalize from muse.plugins.knowtation.rerere import ( DOMAIN, KnowtationRererePlugin, _FINGERPRINT_RE, _MAX_NOTE_BYTES, conflict_fingerprint, lookup_resolution, plugin as knowtation_rerere_plugin, record_full_resolution, record_resolution, replay_full_resolution, replay_resolution, ) # ────────────────────────────────────────────────────────────────────────────── # Fixtures and helpers # ────────────────────────────────────────────────────────────────────────────── def _note(frontmatter: dict[str, Any] | None, body: str) -> bytes: """Build canonical-form note bytes for a frontmatter mapping + body. The result is what :func:`canonicalize` produces, so two notes that differ only in YAML key order or list quoting compare equal at the byte level after this helper. """ if not frontmatter: return canonicalize(body.encode("utf-8")) yaml_text = yaml.safe_dump( frontmatter, sort_keys=False, default_flow_style=False, allow_unicode=True, width=10000, ) return canonicalize(f"---\n{yaml_text}---\n{body}".encode("utf-8")) def _h64(b: bytes) -> str: return hashlib.sha256(b).hexdigest() @pytest.fixture() def tmp_root(tmp_path: pathlib.Path) -> pathlib.Path: """Return a tmp directory acting as a Muse repo root. The unit and security tiers do not need a fully-initialised repo — only ``.muse/`` writes are exercised, and the rerere code creates that directory on demand. """ return tmp_path # ────────────────────────────────────────────────────────────────────────────── # Tier 1 — Unit: fingerprint collision / distinguishability table # ────────────────────────────────────────────────────────────────────────────── class TestFingerprintDistinguishability: """Ten pairs that must DIFFER, five pairs that must MATCH.""" # -- Pairs that MUST differ ------------------------------------------------ def test_different_ours_body_changes_fingerprint(self) -> None: ours_a = _note({"title": "T"}, "# §Summary\n\nours-A\n") ours_b = _note({"title": "T"}, "# §Summary\n\nours-B\n") theirs = _note({"title": "T"}, "# §Summary\n\ntheirs\n") base = _note({"title": "T"}, "# §Summary\n\nbase\n") assert ( conflict_fingerprint(ours_a, theirs, base) != conflict_fingerprint(ours_b, theirs, base) ) def test_different_theirs_body_changes_fingerprint(self) -> None: ours = _note(None, "# H\n\nours\n") theirs_a = _note(None, "# H\n\ntheirs-A\n") theirs_b = _note(None, "# H\n\ntheirs-B\n") base = _note(None, "# H\n\nbase\n") assert ( conflict_fingerprint(ours, theirs_a, base) != conflict_fingerprint(ours, theirs_b, base) ) def test_different_base_body_changes_fingerprint(self) -> None: ours = _note(None, "# H\n\nours\n") theirs = _note(None, "# H\n\ntheirs\n") base_a = _note(None, "# H\n\nbase-A\n") base_b = _note(None, "# H\n\nbase-B\n") assert ( conflict_fingerprint(ours, theirs, base_a) != conflict_fingerprint(ours, theirs, base_b) ) def test_different_section_id_changes_fingerprint(self) -> None: ours_a = _note(None, "## A\n\nours\n") theirs_a = _note(None, "## A\n\ntheirs\n") ours_b = _note(None, "## B\n\nours\n") theirs_b = _note(None, "## B\n\ntheirs\n") base = b"" assert ( conflict_fingerprint(ours_a, theirs_a, base) != conflict_fingerprint(ours_b, theirs_b, base) ) def test_different_section_level_changes_fingerprint(self) -> None: ours_h2 = _note(None, "## A\n\nours\n") theirs_h2 = _note(None, "## A\n\ntheirs\n") ours_h3 = _note(None, "### A\n\nours\n") theirs_h3 = _note(None, "### A\n\ntheirs\n") assert ( conflict_fingerprint(ours_h2, theirs_h2, b"") != conflict_fingerprint(ours_h3, theirs_h3, b"") ) def test_one_conflict_vs_two_conflicts_different(self) -> None: ours_one = _note(None, "## A\n\nours\n\n## B\n\nshared\n") theirs_one = _note(None, "## A\n\ntheirs\n\n## B\n\nshared\n") ours_two = _note(None, "## A\n\nours\n\n## B\n\nours-too\n") theirs_two = _note(None, "## A\n\ntheirs\n\n## B\n\ntheirs-too\n") assert ( conflict_fingerprint(ours_one, theirs_one, b"") != conflict_fingerprint(ours_two, theirs_two, b"") ) def test_swap_ours_theirs_changes_fingerprint(self) -> None: """The fingerprint encodes an ordered (ours, theirs) pair.""" ours = _note(None, "## A\n\nours\n") theirs = _note(None, "## A\n\ntheirs\n") assert ( conflict_fingerprint(ours, theirs, b"") != conflict_fingerprint(theirs, ours, b"") ) def test_duplicate_heading_occurrences_distinguished(self) -> None: ours_first = _note( None, "## A\n\nours\n\n## A\n\nshared\n" ) theirs_first = _note( None, "## A\n\ntheirs\n\n## A\n\nshared\n" ) ours_second = _note( None, "## A\n\nshared\n\n## A\n\nours\n" ) theirs_second = _note( None, "## A\n\nshared\n\n## A\n\ntheirs\n" ) assert ( conflict_fingerprint(ours_first, theirs_first, b"") != conflict_fingerprint(ours_second, theirs_second, b"") ) def test_no_conflict_vs_real_conflict_different(self) -> None: ours = _note(None, "## A\n\nshared\n") theirs = _note(None, "## A\n\nshared\n") # identical → no conflict ours_real = _note(None, "## A\n\nA\n") theirs_real = _note(None, "## A\n\nB\n") assert ( conflict_fingerprint(ours, theirs, b"") != conflict_fingerprint(ours_real, theirs_real, b"") ) def test_section_addition_does_not_collide_with_section_modification( self, ) -> None: # Side A: ours adds a section that theirs lacks → not a conflict per spec ours_add = _note(None, "## A\n\nshared\n\n## B\n\nnew\n") theirs_add = _note(None, "## A\n\nshared\n") # Side B: real conflict on existing section ours_mod = _note(None, "## A\n\nours\n") theirs_mod = _note(None, "## A\n\ntheirs\n") assert ( conflict_fingerprint(ours_add, theirs_add, b"") != conflict_fingerprint(ours_mod, theirs_mod, b"") ) # -- Pairs that MUST match ------------------------------------------------- def test_cosmetic_crlf_line_endings_dont_change_fingerprint(self) -> None: ours_lf = _note(None, "## A\n\nours\n") theirs_lf = _note(None, "## A\n\ntheirs\n") ours_crlf = ours_lf.replace(b"\n", b"\r\n") theirs_crlf = theirs_lf.replace(b"\n", b"\r\n") assert ( conflict_fingerprint(ours_lf, theirs_lf, b"") == conflict_fingerprint(ours_crlf, theirs_crlf, b"") ) def test_frontmatter_tag_addition_doesnt_change_fingerprint(self) -> None: ours_no_tags = _note({"title": "T"}, "## A\n\nours\n") theirs_no_tags = _note({"title": "T"}, "## A\n\ntheirs\n") ours_with_tags = _note( {"title": "T", "tags": ["alpha"]}, "## A\n\nours\n" ) theirs_with_tags = _note( {"title": "T", "tags": ["alpha"]}, "## A\n\ntheirs\n" ) assert ( conflict_fingerprint(ours_no_tags, theirs_no_tags, b"") == conflict_fingerprint(ours_with_tags, theirs_with_tags, b"") ) def test_frontmatter_key_reordering_doesnt_change_fingerprint(self) -> None: ours_a = _note( {"title": "T", "project": "P", "tags": ["a"]}, "## H\n\nours\n" ) theirs_a = _note( {"title": "T", "project": "P", "tags": ["a"]}, "## H\n\ntheirs\n" ) ours_b = _note( {"project": "P", "tags": ["a"], "title": "T"}, "## H\n\nours\n" ) theirs_b = _note( {"project": "P", "tags": ["a"], "title": "T"}, "## H\n\ntheirs\n" ) assert ( conflict_fingerprint(ours_a, theirs_a, b"") == conflict_fingerprint(ours_b, theirs_b, b"") ) def test_non_conflicting_section_reorder_doesnt_change_fingerprint( self, ) -> None: # Both sides have the SAME ## B body → not a conflict; only ## A # differs → only ## A enters the fingerprint. Reordering A vs B # in the document changes the section ordering but the conflict # set, sorted by section_id, is identical. ours_ab = _note(None, "## A\n\nours\n\n## B\n\nshared\n") theirs_ab = _note(None, "## A\n\ntheirs\n\n## B\n\nshared\n") ours_ba = _note(None, "## B\n\nshared\n\n## A\n\nours\n") theirs_ba = _note(None, "## B\n\nshared\n\n## A\n\ntheirs\n") assert ( conflict_fingerprint(ours_ab, theirs_ab, b"") == conflict_fingerprint(ours_ba, theirs_ba, b"") ) def test_preamble_whitespace_doesnt_change_fingerprint(self) -> None: # When the only difference is the body of a section that both sides # share identically, no conflict is registered for that section, # so the only conflict comes from ## A. Adding extra blank lines # to the matching ## B is absorbed by the splitter. ours = _note(None, "## A\n\nours\n\n## B\n\nshared\n") theirs = _note(None, "## A\n\ntheirs\n\n## B\n\nshared\n") ours_extra_blank = _note( None, "## A\n\nours\n\n## B\n\nshared\n\n" ) theirs_extra_blank = _note( None, "## A\n\ntheirs\n\n## B\n\nshared\n\n" ) assert ( conflict_fingerprint(ours, theirs, b"") == conflict_fingerprint(ours_extra_blank, theirs_extra_blank, b"") ) # -- Format guarantees ----------------------------------------------------- def test_fingerprint_is_64_lowercase_hex(self) -> None: fp = conflict_fingerprint( _note(None, "## A\n\nours\n"), _note(None, "## A\n\ntheirs\n"), b"", ) assert _FINGERPRINT_RE.match(fp), f"not 64-char hex: {fp!r}" def test_fingerprint_is_deterministic(self) -> None: ours = _note(None, "## A\n\nours\n") theirs = _note(None, "## A\n\ntheirs\n") assert ( conflict_fingerprint(ours, theirs, b"") == conflict_fingerprint(ours, theirs, b"") ) def test_no_conflicting_sections_returns_empty_sha256(self) -> None: # When ours == theirs (no conflicting sections) the fingerprint # is the SHA-256 of the empty byte sequence. n = _note(None, "## A\n\nshared\n") empty_sha = hashlib.sha256(b"").hexdigest() assert conflict_fingerprint(n, n, b"") == empty_sha # ────────────────────────────────────────────────────────────────────────────── # Tier 2 — Unit: hash-only record / lookup round-trip # ────────────────────────────────────────────────────────────────────────────── class TestHashRecordRoundtrip: def test_record_then_lookup_returns_resolved_hash( self, tmp_root: pathlib.Path ) -> None: fp = conflict_fingerprint( _note(None, "## A\n\nours\n"), _note(None, "## A\n\ntheirs\n"), b"", ) resolved = _note(None, "## A\n\nresolved\n") record_resolution(tmp_root, fp, resolved) assert lookup_resolution(tmp_root, fp) == _h64(canonicalize(resolved)) def test_lookup_unknown_fingerprint_returns_none( self, tmp_root: pathlib.Path ) -> None: assert lookup_resolution(tmp_root, "f" * 64) is None def test_two_distinct_fingerprints_dont_collide( self, tmp_root: pathlib.Path ) -> None: fp_a = conflict_fingerprint( _note(None, "## A\n\nours-A\n"), _note(None, "## A\n\ntheirs-A\n"), b"", ) fp_b = conflict_fingerprint( _note(None, "## B\n\nours-B\n"), _note(None, "## B\n\ntheirs-B\n"), b"", ) assert fp_a != fp_b record_resolution(tmp_root, fp_a, _note(None, "## A\n\nA\n")) record_resolution(tmp_root, fp_b, _note(None, "## B\n\nB\n")) assert lookup_resolution(tmp_root, fp_a) == _h64( canonicalize(_note(None, "## A\n\nA\n")) ) assert lookup_resolution(tmp_root, fp_b) == _h64( canonicalize(_note(None, "## B\n\nB\n")) ) assert lookup_resolution(tmp_root, fp_a) != lookup_resolution( tmp_root, fp_b ) def test_record_creates_sharded_path(self, tmp_root: pathlib.Path) -> None: fp = "abcdef0123456789" * 4 # exactly 64 lowercase hex chars record_resolution(tmp_root, fp, _note(None, "## A\n\nx\n")) sharded = tmp_root / ".muse" / "rerere" / "ab" / f"{fp[2:]}.json" assert sharded.exists() def test_replay_resolution_matches_when_current_already_resolved( self, tmp_root: pathlib.Path ) -> None: fp = conflict_fingerprint( _note(None, "## A\n\nours\n"), _note(None, "## A\n\ntheirs\n"), b"", ) resolved = _note(None, "## A\n\nresolved\n") record_resolution(tmp_root, fp, resolved) assert replay_resolution(tmp_root, fp, resolved) == resolved def test_replay_resolution_returns_none_when_diverged( self, tmp_root: pathlib.Path ) -> None: fp = conflict_fingerprint( _note(None, "## A\n\nours\n"), _note(None, "## A\n\ntheirs\n"), b"", ) record_resolution( tmp_root, fp, _note(None, "## A\n\nresolved\n") ) diverged = _note(None, "## A\n\nsomething-else\n") assert replay_resolution(tmp_root, fp, diverged) is None def test_replay_resolution_returns_none_when_no_record( self, tmp_root: pathlib.Path ) -> None: assert ( replay_resolution( tmp_root, "0" * 64, _note(None, "## A\n\nx\n") ) is None ) def test_record_is_idempotent(self, tmp_root: pathlib.Path) -> None: fp = "1" * 64 resolved = _note(None, "## A\n\nA\n") record_resolution(tmp_root, fp, resolved) first_hash = lookup_resolution(tmp_root, fp) record_resolution(tmp_root, fp, resolved) assert lookup_resolution(tmp_root, fp) == first_hash def test_record_overwrites_atomically( self, tmp_root: pathlib.Path ) -> None: fp = "2" * 64 record_resolution(tmp_root, fp, _note(None, "## A\n\nfirst\n")) record_resolution(tmp_root, fp, _note(None, "## A\n\nsecond\n")) # Latest record wins. assert lookup_resolution(tmp_root, fp) == _h64( canonicalize(_note(None, "## A\n\nsecond\n")) ) # ────────────────────────────────────────────────────────────────────────────── # Tier 3 — Unit: full-bytes record / replay # ────────────────────────────────────────────────────────────────────────────── class TestFullResolutionRoundtrip: def test_record_full_then_replay_returns_canonical_bytes( self, tmp_root: pathlib.Path ) -> None: fp = "3" * 64 resolved = _note(None, "## A\n\nresolved\n") record_full_resolution(tmp_root, fp, resolved) replayed = replay_full_resolution( tmp_root, fp, _note(None, "## A\n\nany\n") ) assert replayed == canonicalize(resolved) def test_replay_full_returns_none_for_missing_fingerprint( self, tmp_root: pathlib.Path ) -> None: assert ( replay_full_resolution( tmp_root, "4" * 64, _note(None, "## A\n\nx\n") ) is None ) def test_record_full_uses_full_suffix( self, tmp_root: pathlib.Path ) -> None: fp = "5" * 64 record_full_resolution(tmp_root, fp, _note(None, "## A\n\nx\n")) full_path = ( tmp_root / ".muse" / "rerere" / "55" / f"{fp[2:]}.full" ) assert full_path.exists() # Hash-only suffix path must NOT have been created by full record. json_path = ( tmp_root / ".muse" / "rerere" / "55" / f"{fp[2:]}.json" ) assert not json_path.exists() def test_full_and_hash_records_coexist( self, tmp_root: pathlib.Path ) -> None: fp = "6" * 64 resolved = _note(None, "## A\n\nresolved\n") record_resolution(tmp_root, fp, resolved) record_full_resolution(tmp_root, fp, resolved) assert lookup_resolution(tmp_root, fp) == _h64(canonicalize(resolved)) assert replay_full_resolution(tmp_root, fp, b"") == canonicalize( resolved ) def test_full_replay_returns_canonical_form( self, tmp_root: pathlib.Path ) -> None: """The stored bytes are canonical — non-canonical input is normalised.""" fp = "7" * 64 # Non-canonical: CRLF and unsorted YAML keys non_canonical = ( b"---\r\nproject: P\r\ntitle: T\r\n---\r\n## A\r\n\r\nx\r\n" ) record_full_resolution(tmp_root, fp, non_canonical) replayed = replay_full_resolution( tmp_root, fp, _note(None, "## A\n\nx\n") ) assert replayed == canonicalize(non_canonical) # Canonical form has LF endings. assert replayed is not None and b"\r\n" not in replayed # ────────────────────────────────────────────────────────────────────────────── # Tier 4 — Integration: replay after cosmetic rewrite # ────────────────────────────────────────────────────────────────────────────── class TestCosmeticRewriteReplay: def test_replay_succeeds_after_tag_addition( self, tmp_root: pathlib.Path ) -> None: # Original conflict ours_v1 = _note( {"title": "Note"}, "## §Summary\n\nours-summary\n" ) theirs_v1 = _note( {"title": "Note"}, "## §Summary\n\ntheirs-summary\n" ) resolved_v1 = _note( {"title": "Note"}, "## §Summary\n\nmerged-summary\n" ) fp_v1 = conflict_fingerprint(ours_v1, theirs_v1, b"") record_full_resolution(tmp_root, fp_v1, resolved_v1) # Same conflict after the user reformats the note: adds a tag # and a project key. The conflicting section is unchanged. ours_v2 = _note( {"title": "Note", "project": "P", "tags": ["new"]}, "## §Summary\n\nours-summary\n", ) theirs_v2 = _note( {"title": "Note", "project": "P", "tags": ["new"]}, "## §Summary\n\ntheirs-summary\n", ) fp_v2 = conflict_fingerprint(ours_v2, theirs_v2, b"") assert fp_v1 == fp_v2, ( "fingerprint must be stable across cosmetic frontmatter rewrites" ) replayed = replay_full_resolution(tmp_root, fp_v2, ours_v2) assert replayed == canonicalize(resolved_v1) def test_replay_succeeds_after_non_conflicting_section_reorder( self, tmp_root: pathlib.Path ) -> None: # Two-section note; only ## §Summary differs across branches. ours = _note( None, "## §Summary\n\nours\n\n## Notes\n\nshared\n" ) theirs = _note( None, "## §Summary\n\ntheirs\n\n## Notes\n\nshared\n" ) resolved = _note( None, "## §Summary\n\nmerged\n\n## Notes\n\nshared\n" ) fp = conflict_fingerprint(ours, theirs, b"") record_full_resolution(tmp_root, fp, resolved) # Reorder the non-conflicting ## Notes section to come first. ours_reordered = _note( None, "## Notes\n\nshared\n\n## §Summary\n\nours\n" ) theirs_reordered = _note( None, "## Notes\n\nshared\n\n## §Summary\n\ntheirs\n" ) fp_reordered = conflict_fingerprint( ours_reordered, theirs_reordered, b"" ) assert fp == fp_reordered replayed = replay_full_resolution( tmp_root, fp_reordered, ours_reordered ) assert replayed == canonicalize(resolved) # ────────────────────────────────────────────────────────────────────────────── # Tier 5 — Integration: rerere CLI hook (compute_fingerprint dispatch) # ────────────────────────────────────────────────────────────────────────────── class TestRerereCliHook: """Verify the CLI dispatch sees the knowtation plugin as a RererePlugin.""" def test_knowtation_plugin_is_recognised_as_rereeplugin(self) -> None: from muse.domain import RererePlugin from muse.plugins.knowtation.plugin import plugin as kn_plugin assert isinstance(kn_plugin, RererePlugin) def test_compute_fingerprint_uses_knowtation_aware_fingerprint( self, tmp_path: pathlib.Path ) -> None: """``muse.core.rerere.compute_fingerprint`` must call into the knowtation plugin for repos with ``domain="knowtation"``. We exercise the bytes path by storing two note blobs in the local object store and calling ``compute_fingerprint`` with their IDs. The result must equal the bytes-level fingerprint computed by :func:`conflict_fingerprint` (with empty base since the protocol does not pass one). """ from muse.core.object_store import write_object from muse.core.rerere import compute_fingerprint from muse.plugins.knowtation.plugin import plugin as kn_plugin ours = _note(None, "## §Summary\n\nours-text\n") theirs = _note(None, "## §Summary\n\ntheirs-text\n") ours_id = _h64(ours) theirs_id = _h64(theirs) # write_object stores under .muse/objects/. (tmp_path / ".muse").mkdir(parents=True, exist_ok=True) write_object(tmp_path, ours_id, ours) write_object(tmp_path, theirs_id, theirs) cli_fp = compute_fingerprint( "notes/test.md", ours_id, theirs_id, kn_plugin, tmp_path ) domain_fp = conflict_fingerprint(ours, theirs, b"") assert cli_fp == domain_fp, ( "muse.core.rerere.compute_fingerprint must dispatch to the " "knowtation plugin's RererePlugin protocol method" ) def test_compute_fingerprint_falls_back_when_blob_missing( self, tmp_path: pathlib.Path ) -> None: """When ours_id or theirs_id is not in the local object store the plugin's protocol method raises and ``compute_fingerprint`` falls back to the default content fingerprint. """ from muse.core.rerere import ( compute_fingerprint, conflict_fingerprint as default_fp, ) from muse.plugins.knowtation.plugin import plugin as kn_plugin # Two valid 64-char hex IDs that are NOT in the object store. ours_id = "a" * 64 theirs_id = "b" * 64 (tmp_path / ".muse").mkdir(parents=True, exist_ok=True) result = compute_fingerprint( "notes/x.md", ours_id, theirs_id, kn_plugin, tmp_path ) # Falls back to the default ID-based fingerprint. assert result == default_fp(ours_id, theirs_id) # ────────────────────────────────────────────────────────────────────────────── # Tier 6 — Data-integrity: 50 historical conflict stubs # ────────────────────────────────────────────────────────────────────────────── class TestFiftyHistoricalConflicts: """Synthesise 50 distinct conflicts; record half; verify lookups.""" def _make_pair(self, idx: int) -> tuple[bytes, bytes, bytes]: """Return ``(ours, theirs, resolved)`` for the *idx*-th synthetic conflict.""" ours = _note( {"title": f"N-{idx}"}, f"## §Summary\n\nours body {idx}\n", ) theirs = _note( {"title": f"N-{idx}"}, f"## §Summary\n\ntheirs body {idx}\n", ) resolved = _note( {"title": f"N-{idx}"}, f"## §Summary\n\nmerged body {idx}\n", ) return ours, theirs, resolved def test_25_recorded_25_missing_no_collisions( self, tmp_root: pathlib.Path ) -> None: fingerprints: list[str] = [] for i in range(50): ours, theirs, resolved = self._make_pair(i) fp = conflict_fingerprint(ours, theirs, b"") fingerprints.append(fp) if i < 25: record_full_resolution(tmp_root, fp, resolved) assert len(set(fingerprints)) == 50, ( f"Expected 50 unique fingerprints, got {len(set(fingerprints))} — " "collisions detected across synthetic stubs" ) for i, fp in enumerate(fingerprints): ours, _theirs, resolved = self._make_pair(i) replayed = replay_full_resolution(tmp_root, fp, ours) if i < 25: assert replayed == canonicalize(resolved), ( f"Stub {i}: full replay missed a recorded resolution" ) else: assert replayed is None, ( f"Stub {i}: full replay returned data for an unrecorded " "fingerprint" ) # ────────────────────────────────────────────────────────────────────────────── # Tier 7 — Security # ────────────────────────────────────────────────────────────────────────────── class TestSecurity: # -- Separator-injection forgery defence ----------------------------------- def test_pipe_in_section_title_does_not_collide(self) -> None: """A section title containing ``|`` must not let a hostile note produce the same fingerprint as a *different* logical conflict.""" ours_a = _note(None, "## A\n\nours-A\n\n## B\n\nours-B\n") theirs_a = _note(None, "## A\n\ntheirs-A\n\n## B\n\ntheirs-B\n") # Adversarial: title containing the joiner character that an # unsafe stringly fingerprint encoder might use as a delimiter. ours_b = _note( None, "## A|\n\nours-A\n\n## B\n\nours-B\n" ) theirs_b = _note( None, "## A|\n\ntheirs-A\n\n## B\n\ntheirs-B\n", ) fp_a = conflict_fingerprint(ours_a, theirs_a, b"") fp_b = conflict_fingerprint(ours_b, theirs_b, b"") assert fp_a != fp_b, ( "fingerprint must distinguish the original conflict from the " "pipe-injected adversarial title" ) def test_colon_in_section_title_does_not_collide(self) -> None: ours_a = _note(None, "## A\n\nours\n") theirs_a = _note(None, "## A\n\ntheirs\n") ours_b = _note(None, "## A:section:1:B#0\n\nours\n") theirs_b = _note(None, "## A:section:1:B#0\n\ntheirs\n") fp_a = conflict_fingerprint(ours_a, theirs_a, b"") fp_b = conflict_fingerprint(ours_b, theirs_b, b"") assert fp_a != fp_b # -- Path-traversal defence ------------------------------------------------ def test_path_traversal_fingerprint_rejected( self, tmp_root: pathlib.Path ) -> None: for bad in ("../evil", "/abs/path", "..", "abc/def", "X" * 64): with pytest.raises(ValueError): record_resolution( tmp_root, bad, _note(None, "## A\n\nx\n") ) with pytest.raises(ValueError): lookup_resolution(tmp_root, bad) with pytest.raises(ValueError): record_full_resolution( tmp_root, bad, _note(None, "## A\n\nx\n") ) with pytest.raises(ValueError): replay_full_resolution( tmp_root, bad, _note(None, "## A\n\nx\n") ) def test_uppercase_hex_fingerprint_rejected( self, tmp_root: pathlib.Path ) -> None: # Strict lowercase-only contract. with pytest.raises(ValueError): record_resolution( tmp_root, "A" * 64, _note(None, "## A\n\nx\n") ) def test_whitespace_in_fingerprint_rejected( self, tmp_root: pathlib.Path ) -> None: with pytest.raises(ValueError): lookup_resolution(tmp_root, "a" * 63 + " ") # -- 1 MiB conflicting section --------------------------------------------- def test_one_mib_conflicting_section_does_not_crash(self) -> None: big_ours = "x" * (1024 * 1024) big_theirs = "y" * (1024 * 1024) ours = _note(None, f"## §Summary\n\n{big_ours}\n") theirs = _note(None, f"## §Summary\n\n{big_theirs}\n") start = time.perf_counter() fp = conflict_fingerprint(ours, theirs, b"") elapsed = time.perf_counter() - start assert _FINGERPRINT_RE.match(fp) # Generous bound — pure SHA-256 of 1 MiB takes ~10 ms; canonicalize # adds parse overhead but should still finish in well under 5 s. assert elapsed < 5.0, f"1 MiB fingerprint took {elapsed:.2f}s" # -- NUL bytes in section bodies ------------------------------------------- def test_nul_bytes_in_section_bodies_handled( self, tmp_root: pathlib.Path ) -> None: ours = b"## A\n\nours\x00with\x00nul\n" theirs = b"## A\n\ntheirs\x00with\x00nul\n" fp = conflict_fingerprint(ours, theirs, b"") assert _FINGERPRINT_RE.match(fp) # Round-trip a hash record on the same NUL-containing data. record_resolution(tmp_root, fp, ours) assert lookup_resolution(tmp_root, fp) == _h64(canonicalize(ours)) # -- Oversized inputs ------------------------------------------------------ def test_oversize_ours_raises_value_error(self) -> None: big = b"# H\n\n" + b"a" * (_MAX_NOTE_BYTES + 1) with pytest.raises(ValueError): conflict_fingerprint(big, b"# H\n\nb\n", b"") def test_oversize_theirs_raises_value_error(self) -> None: big = b"# H\n\n" + b"a" * (_MAX_NOTE_BYTES + 1) with pytest.raises(ValueError): conflict_fingerprint(b"# H\n\nb\n", big, b"") def test_oversize_base_raises_value_error(self) -> None: big = b"# H\n\n" + b"a" * (_MAX_NOTE_BYTES + 1) with pytest.raises(ValueError): conflict_fingerprint(b"# H\n\nb\n", b"# H\n\nc\n", big) def test_oversize_resolved_record_raises_value_error( self, tmp_root: pathlib.Path ) -> None: big = b"# H\n\n" + b"a" * (_MAX_NOTE_BYTES + 1) with pytest.raises(ValueError): record_resolution(tmp_root, "0" * 64, big) with pytest.raises(ValueError): record_full_resolution(tmp_root, "0" * 64, big) # -- Malformed stored record returns None gracefully ----------------------- def test_malformed_json_record_treated_as_missing( self, tmp_root: pathlib.Path ) -> None: fp = "9" * 64 shard = tmp_root / ".muse" / "rerere" / "99" shard.mkdir(parents=True, exist_ok=True) (shard / f"{fp[2:]}.json").write_text("not-valid-json{", encoding="utf-8") assert lookup_resolution(tmp_root, fp) is None def test_record_with_non_string_resolved_hash_treated_as_missing( self, tmp_root: pathlib.Path ) -> None: fp = "8" * 64 shard = tmp_root / ".muse" / "rerere" / "88" shard.mkdir(parents=True, exist_ok=True) (shard / f"{fp[2:]}.json").write_text( json.dumps({"resolved_hash": 12345}), encoding="utf-8" ) assert lookup_resolution(tmp_root, fp) is None def test_yaml_safe_load_used(self) -> None: """The module must NOT call yaml.unsafe_load anywhere.""" from muse.plugins.knowtation import rerere as rr_mod import inspect src = inspect.getsource(rr_mod) assert "unsafe_load" not in src assert "Loader" not in src or "safe" in src # ────────────────────────────────────────────────────────────────────────────── # Plugin façade # ────────────────────────────────────────────────────────────────────────────── class TestPluginFacade: def test_module_singleton_is_a_plugin_instance(self) -> None: assert isinstance(knowtation_rerere_plugin, KnowtationRererePlugin) assert knowtation_rerere_plugin.domain == DOMAIN == "knowtation" def test_plugin_methods_delegate(self, tmp_root: pathlib.Path) -> None: p = KnowtationRererePlugin() ours = _note(None, "## A\n\nours\n") theirs = _note(None, "## A\n\ntheirs\n") resolved = _note(None, "## A\n\nresolved\n") fp_method = p.conflict_fingerprint(ours, theirs, b"") fp_module = conflict_fingerprint(ours, theirs, b"") assert fp_method == fp_module p.record_resolution(tmp_root, fp_method, resolved) assert p.lookup_resolution(tmp_root, fp_method) == _h64( canonicalize(resolved) ) assert p.replay_resolution(tmp_root, fp_method, resolved) == resolved p.record_full_resolution(tmp_root, fp_method, resolved) assert p.replay_full_resolution(tmp_root, fp_method, ours) == ( canonicalize(resolved) )