"""Tests for Phase 2.3 — Knowtation Harmony merge policies. Test tiers covered ------------------ 1. Unit — HARMONY_POLICIES: exactly 6 policies, unique names, valid strategies, correct priority ordering, required fields present. 2. Unit — Per-policy fixture: each policy row resolves the expected strategy for a matching path+dimension pair. 3. Unit — to_attribute_rules(): returns 6 rules sorted by priority descending. 4. Unit — resolve_harmony_strategy(): first-match semantics, dimension=frontmatter vs dimension=*, default catch-all. 5. Integration — write_harmony_museattributes(): file written, valid TOML, all 6 policies present; overwrite=False prevents re-write; overwrite=True replaces. 6. Integration — synthetic merge scenario per policy: strategy resolved matches expectation for representative path+dimension pairs. """ from __future__ import annotations import pathlib import textwrap import pytest try: import tomllib except ImportError: import tomli as tomllib # type: ignore[no-reattr] from muse.core.attributes import VALID_STRATEGIES from muse.plugins.knowtation.policies import ( HARMONY_POLICIES, HarmonyPolicy, resolve_harmony_strategy, to_attribute_rules, write_harmony_museattributes, ) # ============================================================================ # TestHarmonyPoliciesTable # ============================================================================ class TestHarmonyPoliciesTable: def test_exactly_six_policies(self) -> None: assert len(HARMONY_POLICIES) == 6 def test_all_names_unique(self) -> None: names = [p.name for p in HARMONY_POLICIES] assert len(names) == len(set(names)), f"Duplicate policy names: {names}" def test_all_strategies_valid(self) -> None: for p in HARMONY_POLICIES: assert p.strategy in VALID_STRATEGIES, ( f"Policy '{p.name}' uses invalid strategy '{p.strategy}'" ) def test_priorities_non_negative(self) -> None: for p in HARMONY_POLICIES: assert p.priority >= 0, f"Policy '{p.name}' has negative priority" def test_priorities_all_different(self) -> None: priorities = [p.priority for p in HARMONY_POLICIES] assert len(priorities) == len(set(priorities)), ( f"Duplicate priorities: {priorities}" ) def test_all_have_comments(self) -> None: for p in HARMONY_POLICIES: assert p.comment, f"Policy '{p.name}' has no comment" def test_all_have_path_pattern(self) -> None: for p in HARMONY_POLICIES: assert p.path_pattern, f"Policy '{p.name}' has empty path_pattern" def test_all_have_dimension(self) -> None: for p in HARMONY_POLICIES: assert p.dimension, f"Policy '{p.name}' has empty dimension" def test_default_policy_has_lowest_priority(self) -> None: default = next((p for p in HARMONY_POLICIES if p.name == "default"), None) assert default is not None, "'default' policy not found" min_priority = min(p.priority for p in HARMONY_POLICIES) assert default.priority == min_priority def test_archive_policy_has_highest_priority(self) -> None: archive = next((p for p in HARMONY_POLICIES if p.name == "archive"), None) assert archive is not None, "'archive' policy not found" max_priority = max(p.priority for p in HARMONY_POLICIES) assert archive.priority == max_priority def test_harmony_policy_is_frozen(self) -> None: import dataclasses p = HARMONY_POLICIES[0] with pytest.raises((AttributeError, TypeError, dataclasses.FrozenInstanceError)): p.strategy = "hacked" # type: ignore[misc] # ============================================================================ # TestPerPolicyFixtures (one fixture per policy row) # ============================================================================ class TestPerPolicyFixtures: """Each test verifies that a canonical path+dimension pair resolves to the expected strategy via resolve_harmony_strategy().""" def test_archive_policy(self) -> None: assert resolve_harmony_strategy("archive/old-note.md", "*") == "ours" def test_archive_policy_nested(self) -> None: assert resolve_harmony_strategy("archive/2024/jan/note.md", "*") == "ours" def test_inbox_policy(self) -> None: strategy = resolve_harmony_strategy("inbox/capture.md", "*") assert strategy == "prefer-newer-date" def test_inbox_policy_nested(self) -> None: assert ( resolve_harmony_strategy("inbox/subdir/item.md", "*") == "prefer-newer-date" ) def test_capture_policy(self) -> None: assert ( resolve_harmony_strategy("captures/pdf-import.md", "*") == "prefer-newer-date" ) def test_tags_policy_frontmatter_dimension(self) -> None: strategy = resolve_harmony_strategy("projects/born-free/note.md", "frontmatter") assert strategy == "union-sorted" def test_tags_policy_any_md_file(self) -> None: strategy = resolve_harmony_strategy("areas/health/daily.md", "frontmatter") assert strategy == "union-sorted" def test_project_policy_sections_dimension(self) -> None: # The tags rule (frontmatter dim) doesn't match dimension="sections"; # so the project rule (knowtation-3way, priority 20) fires first. strategy = resolve_harmony_strategy("projects/born-free/analysis.md", "sections") assert strategy == "knowtation-3way" def test_project_policy_star_dimension_tags_wins(self) -> None: # When dimension="*", the **/*.md tags rule (priority 30) matches before # the projects/** rule (priority 20) because 30 > 20. strategy = resolve_harmony_strategy("projects/born-free/analysis.md", "*") assert strategy == "union-sorted" def test_project_policy_nested_path(self) -> None: # projects/born-free/inbox/note.md — "inbox/**" only matches paths that # start with "inbox/", not nested. Tags rule (priority 30) matches first. strategy = resolve_harmony_strategy("projects/born-free/inbox/note.md", "*") assert strategy == "union-sorted" def test_default_policy_catch_all_non_md(self) -> None: # .txt files don't match "**/*.md", so default (auto) fires. assert resolve_harmony_strategy("uncategorized/random.txt", "*") == "auto" def test_default_policy_unknown_dimension_non_md(self) -> None: assert resolve_harmony_strategy("something/entirely/different.json", "notes") == "auto" def test_archive_beats_tags_for_frontmatter(self) -> None: # archive (priority 50) beats tags rule (priority 30) on any dimension. assert resolve_harmony_strategy("archive/note.md", "frontmatter") == "ours" def test_archive_beats_tags_for_sections(self) -> None: assert resolve_harmony_strategy("archive/note.md", "sections") == "ours" # ============================================================================ # TestToAttributeRules # ============================================================================ class TestToAttributeRules: def test_returns_six_rules(self) -> None: rules = to_attribute_rules() assert len(rules) == 6 def test_sorted_by_priority_descending(self) -> None: rules = to_attribute_rules() priorities = [r.priority for r in rules] assert priorities == sorted(priorities, reverse=True) def test_all_strategies_valid(self) -> None: for rule in to_attribute_rules(): assert rule.strategy in VALID_STRATEGIES def test_highest_priority_rule_first(self) -> None: rules = to_attribute_rules() max_p = max(p.priority for p in HARMONY_POLICIES) assert rules[0].priority == max_p def test_lowest_priority_rule_last(self) -> None: rules = to_attribute_rules() min_p = min(p.priority for p in HARMONY_POLICIES) assert rules[-1].priority == min_p # ============================================================================ # TestResolveHarmonyStrategy # ============================================================================ class TestResolveHarmonyStrategy: def test_archive_star_dimension(self) -> None: # archive rule (priority 50, dimension=*) → ours strategy = resolve_harmony_strategy("archive/note.md", "notes") assert strategy == "ours" def test_frontmatter_dimension_matched(self) -> None: strategy = resolve_harmony_strategy("areas/health.md", "frontmatter") assert strategy == "union-sorted" def test_notes_dimension_falls_to_project(self) -> None: # The tags rule has dimension=frontmatter; doesn't match "notes" # → project rule (knowtation-3way) fires. strategy = resolve_harmony_strategy("projects/x/note.md", "notes") assert strategy == "knowtation-3way" def test_default_returns_auto_for_non_md(self) -> None: strategy = resolve_harmony_strategy("random_path.toml", "config") assert strategy == "auto" def test_returns_string(self) -> None: assert isinstance(resolve_harmony_strategy("*", "*"), str) # ============================================================================ # TestWriteHarmonyMuseattributes # ============================================================================ class TestWriteHarmonyMuseattributes: def test_creates_file(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) assert (tmp_path / ".museattributes").exists() def test_written_file_is_valid_toml(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) content = (tmp_path / ".museattributes").read_text(encoding="utf-8") parsed = tomllib.loads(content) assert isinstance(parsed, dict) def test_written_file_has_six_rules(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) content = (tmp_path / ".museattributes").read_text(encoding="utf-8") parsed = tomllib.loads(content) assert len(parsed.get("rules", [])) == 6 def test_written_file_meta_domain_is_knowtation(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) content = (tmp_path / ".museattributes").read_text(encoding="utf-8") parsed = tomllib.loads(content) assert parsed.get("meta", {}).get("domain") == "knowtation" def test_overwrite_false_raises_on_existing(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) with pytest.raises(FileExistsError): write_harmony_museattributes(tmp_path, overwrite=False) def test_overwrite_true_replaces_file(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) write_harmony_museattributes(tmp_path, overwrite=True) content = (tmp_path / ".museattributes").read_text(encoding="utf-8") parsed = tomllib.loads(content) assert len(parsed.get("rules", [])) == 6 def test_all_policy_strategies_in_written_file(self, tmp_path: pathlib.Path) -> None: write_harmony_museattributes(tmp_path) content = (tmp_path / ".museattributes").read_text(encoding="utf-8") for policy in HARMONY_POLICIES: assert policy.strategy in content, ( f"Strategy '{policy.strategy}' missing from written file" ) def test_written_file_loadable_by_load_attributes(self, tmp_path: pathlib.Path) -> None: from muse.core.attributes import load_attributes write_harmony_museattributes(tmp_path) rules = load_attributes(tmp_path, domain="knowtation") assert len(rules) == 6 def test_written_file_resolves_archive_correctly(self, tmp_path: pathlib.Path) -> None: from muse.core.attributes import load_attributes, resolve_strategy write_harmony_museattributes(tmp_path) rules = load_attributes(tmp_path, domain="knowtation") assert resolve_strategy(rules, "archive/note.md") == "ours" def test_written_file_resolves_inbox_correctly(self, tmp_path: pathlib.Path) -> None: from muse.core.attributes import load_attributes, resolve_strategy write_harmony_museattributes(tmp_path) rules = load_attributes(tmp_path, domain="knowtation") assert resolve_strategy(rules, "inbox/note.md") == "prefer-newer-date" # ============================================================================ # TestSyntheticMergeScenarios (integration — per policy) # ============================================================================ class TestSyntheticMergeScenarios: """Simulate what the merger would see by checking resolved strategies.""" def _rules(self) -> list: return to_attribute_rules() def test_scenario_archive_note_conflict(self) -> None: from muse.core.attributes import resolve_strategy strategy = resolve_strategy(self._rules(), "archive/2024/jan/note.md", "*") assert strategy == "ours" def test_scenario_inbox_telegram_capture(self) -> None: from muse.core.attributes import resolve_strategy strategy = resolve_strategy(self._rules(), "inbox/telegram-2025-06-01.md", "*") assert strategy == "prefer-newer-date" def test_scenario_project_collaboration_note_sections(self) -> None: from muse.core.attributes import resolve_strategy # Sections dimension: tags rule (frontmatter) doesn't match → project rule strategy = resolve_strategy(self._rules(), "projects/born-free/meeting-2025.md", "sections") assert strategy == "knowtation-3way" def test_scenario_frontmatter_tag_update(self) -> None: from muse.core.attributes import resolve_strategy strategy = resolve_strategy( self._rules(), "projects/born-free/analysis.md", "frontmatter" ) # frontmatter dimension → union-sorted (tags rule, priority 30) assert strategy == "union-sorted" def test_scenario_pdf_import(self) -> None: from muse.core.attributes import resolve_strategy strategy = resolve_strategy(self._rules(), "captures/imported-pdf.md", "*") assert strategy == "prefer-newer-date" def test_scenario_uncategorised_md_note_star_dim(self) -> None: from muse.core.attributes import resolve_strategy # .md file with dimension="*": tags rule (priority 30) matches **/*.md strategy = resolve_strategy(self._rules(), "ideas/random-thought.md", "*") assert strategy == "union-sorted" def test_scenario_uncategorised_non_md_default(self) -> None: from muse.core.attributes import resolve_strategy # Non-.md file: only the default rule (*) matches strategy = resolve_strategy(self._rules(), "ideas/sketch.txt", "*") assert strategy == "auto" def test_scenario_archive_beats_everything(self) -> None: from muse.core.attributes import resolve_strategy strategy = resolve_strategy( self._rules(), "archive/projects/old-capture.md", "frontmatter" ) assert strategy == "ours"