"""Tests for ``muse/plugins/knowtation/merger.py`` — Phase 2.4. Covers all seven tiers required by Rule #0: 1. **Unit** — per-dimension merge primitives (title clean / conflict, scalar clean / conflict, set union / deletion, section add / remove / move, line diff3, marker defang). 2. **Integration** — 20 hand-crafted three-way merge fixtures spanning the common cases (clean tags merge, title change on one side, scalar conflict, section add on one side, body edit on different sections, body edit on same section). 3. **End-to-end** — ``merge_vault`` over a synthetic 20-note vault returns the expected merged manifest and conflict list. 4. **Stress** — 100-branch fan-in: 100 notes against a shared base, each touching a different section → all clean. 5. **Data-integrity** — round-trip after a clean merge: ``apply(diff(M, M), M) == M`` and ``M`` is byte-stable through ``canonicalize``. 6. **Performance** — merge of two 500-section notes completes in < 2 s. 7. **Security** — YAML injection neutralised by ``safe_load``; conflict-marker injection in input is defanged; NUL bytes survive end-to-end; oversize inputs raise the documented ``ValueError``. Conventions ----------- * Notes are constructed via :func:`_note` and passed through :func:`canonicalize` so byte-for-byte assertions are stable. * No real Muse repository is required; ``merge_vault`` reads blobs from a flat ``.knowtation_blobs/`` directory inside the test ``tmp_path``. """ from __future__ import annotations import hashlib import json import pathlib import time from typing import Any import pytest import yaml from muse.core.attributes import AttributeRule from muse.plugins.knowtation.differ import ( apply, canonicalize, diff_notes, ) from muse.plugins.knowtation.merger import ( _CONFLICT_DIR, _MARK_OURS, _MARK_SEP, _MARK_THEIRS, _MARKER_DEFANG_PREFIX, _conflict_fingerprint, _is_summary_section, _merge_frontmatter, _merge_notes_internal, _merge_scalar_field, _merge_section_body, _merge_set_field, _merge_title, _NoteConflict, _three_way_merge_lines, merge_notes, merge_vault, ) from muse.plugins.knowtation.differ import _split_sections_typed # ───────────────────────────────────────────────────────────────────────────── # Construction helpers # ───────────────────────────────────────────────────────────────────────────── def _note(frontmatter: dict[str, Any] | None, body: str = "") -> bytes: """Build canonical-form note bytes. Args: frontmatter: Frontmatter mapping (``None`` for no frontmatter). body: Body text following the frontmatter. Returns: Canonical-form note bytes. """ 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, ) raw = f"---\n{yaml_text}---\n{body}".encode("utf-8") return canonicalize(raw) def _hash(b: bytes) -> str: return hashlib.sha256(b).hexdigest() def _store_blob(root: pathlib.Path, content: bytes) -> str: """Store *content* in the test blob store and return its SHA-256 hex.""" h = _hash(content) blob_dir = root / ".knowtation_blobs" blob_dir.mkdir(parents=True, exist_ok=True) (blob_dir / h).write_bytes(content) return h # ============================================================================= # Tier 1 — Unit tests # ============================================================================= class TestMergeTitle: """``_merge_title`` — Dimension 1.""" def test_both_unchanged(self) -> None: result, conflict = _merge_title("Hello", "Hello", "Hello") assert result == "Hello" assert conflict is None def test_only_ours_changed(self) -> None: result, conflict = _merge_title("New", "Old", "Old") assert result == "New" assert conflict is None def test_only_theirs_changed(self) -> None: result, conflict = _merge_title("Old", "New", "Old") assert result == "New" assert conflict is None def test_both_changed_to_same_value(self) -> None: result, conflict = _merge_title("Same", "Same", "Old") assert result == "Same" assert conflict is None def test_both_changed_differently(self) -> None: result, conflict = _merge_title("OursTitle", "TheirsTitle", "BaseTitle") assert isinstance(result, str) assert _MARK_OURS in result assert _MARK_THEIRS in result assert "OursTitle" in result assert "TheirsTitle" in result assert conflict is not None assert conflict.dimension == "title" def test_neither_set(self) -> None: result, conflict = _merge_title(None, None, None) assert result is None assert conflict is None class TestMergeScalarField: """``_merge_scalar_field`` — Dimension 2.""" def test_consensus(self) -> None: val, conflict, present = _merge_scalar_field("v", "v", "v") assert (val, conflict, present) == ("v", False, True) def test_only_ours_changed(self) -> None: val, conflict, present = _merge_scalar_field("new", "old", "old") assert val == "new" assert conflict is False assert present is True def test_only_theirs_changed(self) -> None: val, conflict, present = _merge_scalar_field("old", "new", "old") assert val == "new" assert conflict is False def test_both_changed_differently_ours_wins(self) -> None: val, conflict, present = _merge_scalar_field("X", "Y", "B") assert val == "X" assert conflict is True assert present is True def test_both_deleted(self) -> None: val, conflict, present = _merge_scalar_field(None, None, "B") assert val is None assert conflict is False assert present is False def test_added_by_one_side(self) -> None: val, conflict, present = _merge_scalar_field("v", None, None) assert val == "v" assert present is True class TestMergeSetField: """``_merge_set_field`` — Dimension 3.""" def test_clean_union(self) -> None: result = _merge_set_field(["a", "b"], ["b", "c"], ["b"]) assert result == ["a", "b", "c"] def test_consensus_delete(self) -> None: result = _merge_set_field(["a"], ["a"], ["a", "b"]) assert result == ["a"] def test_one_side_deletes_other_keeps(self) -> None: result = _merge_set_field(["a"], ["a", "b"], ["a", "b"]) assert result == ["a", "b"] def test_added_by_both_independently(self) -> None: result = _merge_set_field(["x"], ["y"], []) assert result == ["x", "y"] def test_empty_sides(self) -> None: assert _merge_set_field(None, None, None) == [] assert _merge_set_field([], [], []) == [] def test_dedup_and_sort(self) -> None: result = _merge_set_field(["b", "a"], ["a", "c"], []) assert result == ["a", "b", "c"] class TestThreeWayMergeLines: """``_three_way_merge_lines`` — diff3 line algorithm.""" def test_no_changes(self) -> None: merged, n = _three_way_merge_lines( ["a\n", "b\n"], ["a\n", "b\n"], ["a\n", "b\n"] ) assert merged == ["a\n", "b\n"] assert n == 0 def test_only_ours_changed(self) -> None: merged, n = _three_way_merge_lines( ["a\n", "b\n"], ["a\n", "X\n"], ["a\n", "b\n"] ) assert merged == ["a\n", "X\n"] assert n == 0 def test_only_theirs_changed(self) -> None: merged, n = _three_way_merge_lines( ["a\n", "b\n"], ["a\n", "b\n"], ["a\n", "Y\n"] ) assert merged == ["a\n", "Y\n"] assert n == 0 def test_non_overlapping_changes(self) -> None: merged, n = _three_way_merge_lines( ["a\n", "b\n", "c\n"], ["X\n", "b\n", "c\n"], ["a\n", "b\n", "Y\n"], ) assert merged == ["X\n", "b\n", "Y\n"] assert n == 0 def test_overlapping_change_emits_conflict(self) -> None: merged, n = _three_way_merge_lines( ["a\n"], ["X\n"], ["Y\n"] ) assert n == 1 assert _MARK_OURS in merged assert _MARK_THEIRS in merged def test_inbound_marker_defanged(self) -> None: evil = "<<<<<<< inbound\n" merged, n = _three_way_merge_lines( [], [evil], [] ) assert merged assert merged[0].startswith(_MARKER_DEFANG_PREFIX) class TestMergeSectionBody: """``_merge_section_body`` — text-level wrapper.""" def test_consensus(self) -> None: text = "## A\nbody\n" merged, n = _merge_section_body(text, text, text) assert merged == text assert n == 0 def test_only_one_changed(self) -> None: merged, n = _merge_section_body("## A\nb\n", "## A\nNEW\n", "## A\nb\n") assert merged == "## A\nNEW\n" assert n == 0 class TestSummarySectionDetection: def test_summary_title_is_summary(self) -> None: from muse.plugins.knowtation.differ import _Section sec = _Section(sid="section:2:Summary#0", level=2, title="Summary", text="") assert _is_summary_section(sec) is True def test_section_marker_glyph_summary(self) -> None: from muse.plugins.knowtation.differ import _Section sec = _Section(sid="x", level=2, title="§Summary", text="") assert _is_summary_section(sec) is True def test_other_title_not_summary(self) -> None: from muse.plugins.knowtation.differ import _Section sec = _Section(sid="x", level=2, title="Background", text="") assert _is_summary_section(sec) is False class TestMergeFrontmatterUnit: """``_merge_frontmatter`` end-to-end.""" def test_clean_merge(self) -> None: o = {"title": "T", "tags": ["a", "b"], "date": "2025-01-01"} t = {"title": "T", "tags": ["b", "c"], "date": "2025-01-01"} b = {"title": "T", "tags": ["b"], "date": "2025-01-01"} merged, conflicts = _merge_frontmatter(o, t, b) assert merged["title"] == "T" assert merged["tags"] == ["a", "b", "c"] assert merged["date"] == "2025-01-01" assert conflicts == [] def test_scalar_conflict_ours_wins_and_records(self) -> None: o = {"date": "2025-02-01"} t = {"date": "2025-03-01"} b = {"date": "2025-01-01"} merged, conflicts = _merge_frontmatter(o, t, b) assert merged["date"] == "2025-02-01" assert len(conflicts) == 1 assert conflicts[0].dimension == "frontmatter" assert "date" in conflicts[0].section # ============================================================================= # Tier 2 — Integration: 20 hand-crafted three-way fixtures # ============================================================================= def _fixture_set() -> list[tuple[str, bytes, bytes, bytes, dict[str, Any]]]: """Return 20 (name, ours, theirs, base, expectations) fixtures. Each fixture's ``expectations`` dict supports the keys: - ``clean`` (bool, default True) - ``contains_marker`` (bool, default False) - ``expected_tags`` (list[str], optional) - ``expected_title`` (str, optional) """ def n(fm: dict[str, Any] | None, body: str = "") -> bytes: return _note(fm, body) f: list[tuple[str, bytes, bytes, bytes, dict[str, Any]]] = [] base = n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n") # 1. No-op merge f.append(("noop", base, base, base, {"clean": True})) # 2. Clean tag union f.append(( "tags-clean-union", n({"title": "T", "tags": ["a", "b"]}, "# T\n\nBody.\n"), n({"title": "T", "tags": ["a", "c"]}, "# T\n\nBody.\n"), base, {"clean": True, "expected_tags": ["a", "b", "c"]}, )) # 3. Title changed on one side only f.append(( "title-only-ours", n({"title": "Better", "tags": ["a"]}, "# T\n\nBody.\n"), base, base, {"clean": True, "expected_title": "Better"}, )) # 4. Title changed on theirs only f.append(( "title-only-theirs", base, n({"title": "Better", "tags": ["a"]}, "# T\n\nBody.\n"), base, {"clean": True, "expected_title": "Better"}, )) # 5. Title conflict — both changed differently f.append(( "title-conflict", n({"title": "OursTitle", "tags": ["a"]}, "Body.\n"), n({"title": "TheirsTitle", "tags": ["a"]}, "Body.\n"), base, {"clean": False, "contains_marker": True}, )) # 6. Title same value on both sides (consensus) f.append(( "title-same-consensus", n({"title": "Agreed", "tags": ["a"]}, "Body.\n"), n({"title": "Agreed", "tags": ["a"]}, "Body.\n"), base, {"clean": True, "expected_title": "Agreed"}, )) # 7. Frontmatter scalar conflict — ours wins f.append(( "scalar-conflict-ours-wins", n({"title": "T", "date": "2025-02-01"}, "Body.\n"), n({"title": "T", "date": "2025-03-01"}, "Body.\n"), n({"title": "T", "date": "2025-01-01"}, "Body.\n"), {"clean": False}, )) # 8. Set deletion: both sides remove the same tag f.append(( "tag-consensus-delete", n({"title": "T", "tags": ["a"]}, "Body.\n"), n({"title": "T", "tags": ["a"]}, "Body.\n"), n({"title": "T", "tags": ["a", "to-remove"]}, "Body.\n"), {"clean": True, "expected_tags": ["a"]}, )) # 9. Tag deletion only on ours, theirs unchanged f.append(( "tag-deleted-by-ours-only", n({"title": "T", "tags": ["a"]}, "Body.\n"), n({"title": "T", "tags": ["a", "still"]}, "Body.\n"), n({"title": "T", "tags": ["a", "still"]}, "Body.\n"), {"clean": True, "expected_tags": ["a", "still"]}, )) # 10. New section added by ours only f.append(( "section-added-ours", n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## New\nstuff\n"), base, base, {"clean": True}, )) # 11. New section added by theirs only f.append(( "section-added-theirs", base, n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## New\nstuff\n"), base, {"clean": True}, )) # 12. Same new section added by both → consensus, no duplicate same_new = n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## Both\nadded\n") f.append(( "section-added-both-same-content", same_new, same_new, base, {"clean": True}, )) # 13. Same new section title from both with different content → body merge f.append(( "section-added-both-different-content", n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## Both\nours version\n"), n({"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## Both\ntheirs version\n"), base, {"clean": False, "contains_marker": True}, )) # 14. Section deleted by both base_with_extra = n( {"title": "T", "tags": ["a"]}, "# T\n\nBody.\n\n## Drop\nold\n", ) f.append(( "section-consensus-delete", base, base, base_with_extra, {"clean": True}, )) # 15. Section bodies edited differently in different sections — clean base_two = n( {"title": "T", "tags": ["a"]}, "# T\n\n## A\nalpha\n\n## B\nbeta\n", ) ours_two = n( {"title": "T", "tags": ["a"]}, "# T\n\n## A\nALPHA\n\n## B\nbeta\n", ) theirs_two = n( {"title": "T", "tags": ["a"]}, "# T\n\n## A\nalpha\n\n## B\nBETA\n", ) f.append(( "section-edits-different-sections", ours_two, theirs_two, base_two, {"clean": True}, )) # 16. Same section body edited on both sides differently → conflict base_one = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nline1\nline2\n") ours_one = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nline1\nOURS\n") theirs_one = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nline1\nTHEIRS\n") f.append(( "section-same-body-conflict", ours_one, theirs_one, base_one, {"clean": False, "contains_marker": True}, )) # 17. Same section, same edit on both sides → consensus f.append(( "section-same-edit-both", ours_one, ours_one, base_one, {"clean": True}, )) # 18. Section moved on one side (still a clean merge under our merger) base_mv = n({"title": "T", "tags": ["a"]}, "# T\n\n## A\na\n\n## B\nb\n") ours_mv = n({"title": "T", "tags": ["a"]}, "# T\n\n## B\nb\n\n## A\na\n") f.append(( "section-moved-by-ours", ours_mv, base_mv, base_mv, {"clean": True}, )) # 19. Body line inserted by both at different positions → clean base_lines = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nA\nB\nC\n") ours_lines = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nA\nO1\nB\nC\n") theirs_lines = n({"title": "T", "tags": ["a"]}, "# T\n\n## X\nA\nB\nT1\nC\n") f.append(( "body-line-insert-different-positions", ours_lines, theirs_lines, base_lines, {"clean": True}, )) # 20. Frontmatter added on one side only f.append(( "frontmatter-added-on-ours", n({"title": "T", "tags": ["a"], "project": "p"}, "Body.\n"), n({"title": "T", "tags": ["a"]}, "Body.\n"), n({"title": "T", "tags": ["a"]}, "Body.\n"), {"clean": True}, )) return f @pytest.mark.parametrize("fixture", _fixture_set(), ids=lambda f: f[0]) def test_three_way_fixture(fixture: tuple[str, bytes, bytes, bytes, dict[str, Any]]) -> None: name, ours, theirs, base, expect = fixture merged_bytes, conflicts = _merge_notes_internal(ours, theirs, base, path=name) is_clean = len(conflicts) == 0 if expect.get("clean", True): assert is_clean, f"{name}: expected clean merge, got conflicts={conflicts!r}" else: assert not is_clean, f"{name}: expected conflicts but got clean merge" if expect.get("contains_marker"): text = merged_bytes.decode("utf-8", errors="replace") assert _MARK_OURS in text and _MARK_THEIRS in text, name if "expected_tags" in expect: text = merged_bytes.decode("utf-8", errors="replace") for tag in expect["expected_tags"]: assert tag in text, f"{name}: missing tag {tag}" if "expected_title" in expect: text = merged_bytes.decode("utf-8", errors="replace") assert expect["expected_title"] in text, name # ============================================================================= # Tier 3 — End-to-end: synthetic 20-note vault via merge_vault # ============================================================================= def _build_synthetic_vault( root: pathlib.Path, n: int = 20 ) -> tuple[dict[str, str], dict[str, str], dict[str, str]]: """Build base / ours / theirs manifests for a synthetic n-note vault. For each i in 0..n-1: - Notes 0..n/2: ours adds a tag, theirs unchanged - Notes n/2..n-2: theirs adds a tag, ours unchanged - Note n-1: both modify same scalar field → conflict """ base_files: dict[str, str] = {} ours_files: dict[str, str] = {} theirs_files: dict[str, str] = {} half = n // 2 for i in range(n): path = f"notes/note-{i:03d}.md" base_bytes = _note( {"title": f"Note {i}", "tags": ["base"], "date": "2025-01-01"}, f"# Note {i}\n\nOriginal body.\n", ) b_hash = _store_blob(root, base_bytes) base_files[path] = b_hash if i == n - 1: # Conflict note — both change date differently ours_b = _note( {"title": f"Note {i}", "tags": ["base"], "date": "2025-02-01"}, f"# Note {i}\n\nOriginal body.\n", ) theirs_b = _note( {"title": f"Note {i}", "tags": ["base"], "date": "2025-03-01"}, f"# Note {i}\n\nOriginal body.\n", ) elif i < half: ours_b = _note( {"title": f"Note {i}", "tags": ["base", "ours-extra"], "date": "2025-01-01"}, f"# Note {i}\n\nOriginal body.\n", ) theirs_b = base_bytes else: ours_b = base_bytes theirs_b = _note( {"title": f"Note {i}", "tags": ["base", "theirs-extra"], "date": "2025-01-01"}, f"# Note {i}\n\nOriginal body.\n", ) ours_files[path] = _store_blob(root, ours_b) theirs_files[path] = _store_blob(root, theirs_b) return base_files, ours_files, theirs_files def test_merge_vault_end_to_end(tmp_path: pathlib.Path) -> None: base_files, ours_files, theirs_files = _build_synthetic_vault(tmp_path, n=20) merged, conflicts = merge_vault( {"files": ours_files, "domain": "knowtation", "directories": []}, {"files": theirs_files, "domain": "knowtation", "directories": []}, {"files": base_files, "domain": "knowtation", "directories": []}, tmp_path, ) assert set(merged.keys()) == set(base_files.keys()), "all 20 notes should survive" assert len(conflicts) >= 1, "the conflict note should produce a conflict entry" conflict_paths = {c["path"] for c in conflicts} assert "notes/note-019.md" in conflict_paths def test_merge_vault_only_ours_added(tmp_path: pathlib.Path) -> None: base = _note({"title": "B"}, "B\n") ours_extra = _note({"title": "Extra"}, "Extra body\n") base_h = _store_blob(tmp_path, base) extra_h = _store_blob(tmp_path, ours_extra) merged, conflicts = merge_vault( {"files": {"a.md": base_h, "extra.md": extra_h}}, {"files": {"a.md": base_h}}, {"files": {"a.md": base_h}}, tmp_path, ) assert merged == {"a.md": base_h, "extra.md": extra_h} assert conflicts == [] def test_merge_vault_consensus_delete(tmp_path: pathlib.Path) -> None: base = _note({"title": "B"}, "Body\n") base_h = _store_blob(tmp_path, base) merged, conflicts = merge_vault( {"files": {}}, {"files": {}}, {"files": {"deleted.md": base_h}}, tmp_path, ) assert merged == {} assert conflicts == [] def test_merge_vault_delete_edit_conflict(tmp_path: pathlib.Path) -> None: base = _note({"title": "B"}, "Old\n") modified = _note({"title": "B"}, "New\n") base_h = _store_blob(tmp_path, base) mod_h = _store_blob(tmp_path, modified) merged, conflicts = merge_vault( {"files": {}}, # ours deleted {"files": {"a.md": mod_h}}, # theirs modified {"files": {"a.md": base_h}}, tmp_path, ) assert "a.md" in merged assert any("delete" in c["description"].lower() for c in conflicts) def test_merge_vault_summary_conflict_writes_stub(tmp_path: pathlib.Path) -> None: base = _note( {"title": "T"}, "# T\n\n## Summary\nold summary line\n", ) ours = _note( {"title": "T"}, "# T\n\n## Summary\nOURS SUMMARY\n", ) theirs = _note( {"title": "T"}, "# T\n\n## Summary\nTHEIRS SUMMARY\n", ) b_h = _store_blob(tmp_path, base) o_h = _store_blob(tmp_path, ours) t_h = _store_blob(tmp_path, theirs) merged, conflicts = merge_vault( {"files": {"a.md": o_h}}, {"files": {"a.md": t_h}}, {"files": {"a.md": b_h}}, tmp_path, ) stub_dir = tmp_path / _CONFLICT_DIR assert stub_dir.exists() stubs = list(stub_dir.glob("*.json")) assert len(stubs) == 1 payload = json.loads(stubs[0].read_text()) assert payload["path"] == "a.md" assert "summary" in payload["section"].lower() assert payload["fingerprint"] == stubs[0].stem # ============================================================================= # Tier 4 — Stress: 100-branch fan-in # ============================================================================= def test_stress_100_branches_against_shared_base() -> None: """100 ours/theirs pairs each touching a different section all merge cleanly.""" sections_body = "# T\n\n" + "\n".join( f"## Sec{i}\nbase line {i}\n" for i in range(100) ) base_bytes = _note({"title": "T"}, sections_body) for i in range(100): modified_body = "# T\n\n" + "\n".join( f"## Sec{j}\n" + (f"OURS line {j}\n" if j == i else f"base line {j}\n") for j in range(100) ) ours = _note({"title": "T"}, modified_body) merged, conflicts = _merge_notes_internal( ours, base_bytes, base_bytes, path=f"section-{i}" ) assert conflicts == [] text = merged.decode("utf-8") assert f"OURS line {i}" in text # ============================================================================= # Tier 5 — Data integrity: round-trip and idempotency # ============================================================================= class TestDataIntegrity: def test_idempotent_clean_merge(self) -> None: base = _note({"title": "T", "tags": ["a"]}, "# T\n\n## X\nbody\n") ours = _note({"title": "T", "tags": ["a", "b"]}, "# T\n\n## X\nbody\n") theirs = _note({"title": "T", "tags": ["a", "c"]}, "# T\n\n## X\nbody\n") m1 = merge_notes(ours, theirs, base) m2 = merge_notes(ours, theirs, base) assert m1 == m2, "merge_notes is not idempotent" # Re-merging the merged output against itself should yield the same bytes. m3 = merge_notes(m1, m1, m1) assert m3 == m1 def test_clean_merge_round_trips_through_diff(self) -> None: base = _note({"title": "T", "tags": ["a"]}, "# T\n\n## X\nbody\n") ours = _note({"title": "T", "tags": ["a", "b"]}, "# T\n\n## X\nbody\n") theirs = _note({"title": "T", "tags": ["a", "c"]}, "# T\n\n## X\nbody\n") merged = merge_notes(ours, theirs, base) # Round trip: diff(M, M) is empty; apply(empty, M) == M. delta = diff_notes(merged, merged) assert apply(delta, merged) == merged def test_canonicalisation_stable(self) -> None: base = _note({"title": "T", "tags": ["a"]}, "# T\n\nbody\n") ours = _note({"title": "T", "tags": ["a", "b"]}, "# T\n\nbody\n") theirs = _note({"title": "T", "tags": ["a", "c"]}, "# T\n\nbody\n") merged = merge_notes(ours, theirs, base) assert canonicalize(merged) == merged # ============================================================================= # Tier 6 — Performance: 500-section notes in < 2 s # ============================================================================= @pytest.mark.perf def test_perf_500_section_merge_under_2s() -> None: sections = [f"## Sec{i}\nline-{i}-A\nline-{i}-B\n" for i in range(500)] base_body = "# T\n\n" + "\n".join(sections) base = _note({"title": "T"}, base_body) # Ours edits the first half; theirs edits the second half — fully clean. ours_sections = [ (s.replace("line-{i}-A".format(i=i), "OURS-A") if i < 250 else s) for i, s in enumerate(sections) ] theirs_sections = [ (s.replace("line-{i}-A".format(i=i), "THEIRS-A") if i >= 250 else s) for i, s in enumerate(sections) ] ours = _note({"title": "T"}, "# T\n\n" + "\n".join(ours_sections)) theirs = _note({"title": "T"}, "# T\n\n" + "\n".join(theirs_sections)) start = time.perf_counter() merged = merge_notes(ours, theirs, base) elapsed = time.perf_counter() - start assert elapsed < 2.0, f"500-section merge took {elapsed:.3f}s (> 2s budget)" assert merged assert b"OURS-A" in merged assert b"THEIRS-A" in merged # ============================================================================= # Tier 7 — Security # ============================================================================= class TestSecurity: def test_oversize_input_raises(self) -> None: from muse.plugins.knowtation.differ import _MAX_NOTE_BYTES oversize = b"x" * (_MAX_NOTE_BYTES + 1) with pytest.raises(ValueError): merge_notes(oversize, b"", b"") def test_yaml_safe_load_blocks_python_object_construction(self) -> None: # Crafted frontmatter that, under unsafe yaml, would instantiate # an arbitrary Python object via ``!!python/object``. safe_load # rejects this — the merger must process the input without # executing any embedded payload, raising any exception, or # blocking on subprocess calls. We verify by: # 1. Confirming yaml.safe_load itself rejects the payload. # 2. Confirming merge_notes returns deterministically. # 3. Confirming the merged bytes are idempotent under # canonicalize (proving the merger neither parsed nor # executed the unsafe construct). evil_payload = ( b"---\n" b"!!python/object/apply:os.system [\"echo MERGER_SHOULD_NOT_EXECUTE\"]\n" b"---\n" b"Body\n" ) with pytest.raises(yaml.YAMLError): yaml.safe_load(evil_payload[4:].split(b"\n---")[0].decode()) merged = merge_notes(evil_payload, evil_payload, evil_payload) assert isinstance(merged, bytes) assert canonicalize(merged) == merged def test_inbound_conflict_marker_defanged(self) -> None: # Attacker stuffs a fake conflict marker into ours's body — the # merger must defang it so downstream tooling cannot mistake it # for a real merge marker. base = _note({"title": "T"}, "# T\n\nbody-base\n") ours = _note({"title": "T"}, "# T\n\n<<<<<<< INBOUND\nbody-ours\n=======\nfake\n>>>>>>> END\n") theirs = _note({"title": "T"}, "# T\n\nbody-theirs\n") merged = merge_notes(ours, theirs, base) text = merged.decode("utf-8") # Real merge markers may still appear as part of the actual merge, # but the inbound markers must be defanged with the ZWS prefix. assert _MARKER_DEFANG_PREFIX + "<<<<<<< INBOUND" in text assert _MARKER_DEFANG_PREFIX + ">>>>>>> END" in text def test_nul_bytes_in_body_safe(self) -> None: body_with_nul = "# T\n\nbefore\x00after\n" ours = _note({"title": "T"}, body_with_nul) theirs = _note({"title": "T"}, "# T\n\nbody\n") base = _note({"title": "T"}, "# T\n\nbody\n") merged = merge_notes(ours, theirs, base) assert merged assert b"\x00" in merged def test_binary_content_does_not_crash(self) -> None: binary = bytes(range(256)) merged = merge_notes(binary, binary, binary) # safe_load rejects binary content as YAML; merger treats as body. assert isinstance(merged, bytes) def test_idempotent_under_marker_injection(self) -> None: # Re-merging a previously-merged conflict result must not introduce # additional spurious markers. base = _note({"title": "T"}, "# T\n\n## X\nA\n") ours = _note({"title": "T"}, "# T\n\n## X\nB\n") theirs = _note({"title": "T"}, "# T\n\n## X\nC\n") merged_once = merge_notes(ours, theirs, base) merged_twice = merge_notes(merged_once, merged_once, merged_once) assert merged_twice == merged_once # ============================================================================= # Helper: fingerprint determinism # ============================================================================= def test_conflict_fingerprint_is_order_independent() -> None: a = _NoteConflict( path="x", section="s1", dimension="sections", description="", ours_hash="o1", theirs_hash="t1", base_hash="b1", ) b = _NoteConflict( path="x", section="s2", dimension="sections", description="", ours_hash="o2", theirs_hash="t2", base_hash="b2", ) fp1 = _conflict_fingerprint([a, b]) fp2 = _conflict_fingerprint([b, a]) assert fp1 == fp2 assert len(fp1) == 64 # ============================================================================= # Helper: attrs-driven file-level strategy override # ============================================================================= def test_attrs_file_level_override_to_union_sorted() -> None: """`union-sorted` strategy at "*" overrides the per-dimension logic.""" rule = AttributeRule( path_pattern="notes/**", dimension="*", strategy="union-sorted", comment="", priority=10, source_index=0, ) base = _note({"title": "T", "tags": ["a"]}, "Body\n") ours = _note({"title": "T", "tags": ["a", "b"]}, "Body\n") theirs = _note({"title": "T", "tags": ["a", "c"]}, "Body\n") merged = merge_notes(ours, theirs, base, path="notes/foo.md", attrs=[rule]) text = merged.decode("utf-8") for tag in ("a", "b", "c"): assert tag in text def test_attrs_per_dimension_override_for_frontmatter() -> None: """A 'frontmatter' dimension override should swap the dimension result.""" rule = AttributeRule( path_pattern="notes/**", dimension="frontmatter", strategy="union-sorted", comment="", priority=10, source_index=0, ) base = _note({"title": "T", "tags": ["a"]}, "## X\nbase body\n") ours = _note({"title": "T", "tags": ["a", "b"]}, "## X\nours body\n") theirs = _note({"title": "T", "tags": ["a", "c"]}, "## X\nbase body\n") merged = merge_notes(ours, theirs, base, path="notes/foo.md", attrs=[rule]) text = merged.decode("utf-8") for tag in ("a", "b", "c"): assert tag in text # The body merge is untouched by the frontmatter override → ours's edit. assert "ours body" in text