gabriel / muse public

test_knowtation_policies.py file-level

at sha256:6 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:0 test: validate staging push and MP workflow for aaronrene · · Jul 7, 2026
1 """Tests for Phase 2.3 — Knowtation Harmony merge policies.
2
3 Test tiers covered
4 ------------------
5 1. Unit — HARMONY_POLICIES: exactly 6 policies, unique names, valid strategies,
6 correct priority ordering, required fields present.
7 2. Unit — Per-policy fixture: each policy row resolves the expected strategy
8 for a matching path+dimension pair.
9 3. Unit — to_attribute_rules(): returns 6 rules sorted by priority descending.
10 4. Unit — resolve_harmony_strategy(): first-match semantics, dimension=frontmatter
11 vs dimension=*, default catch-all.
12 5. Integration — write_harmony_museattributes(): file written, valid TOML,
13 all 6 policies present; overwrite=False prevents re-write; overwrite=True
14 replaces.
15 6. Integration — synthetic merge scenario per policy: strategy resolved matches
16 expectation for representative path+dimension pairs.
17 """
18
19 from __future__ import annotations
20
21 import pathlib
22 import textwrap
23
24 import pytest
25
26 try:
27 import tomllib
28 except ImportError:
29 import tomli as tomllib # type: ignore[no-reattr]
30
31 from muse.core.attributes import VALID_STRATEGIES
32 from muse.plugins.knowtation.policies import (
33 HARMONY_POLICIES,
34 HarmonyPolicy,
35 resolve_harmony_strategy,
36 to_attribute_rules,
37 write_harmony_museattributes,
38 )
39
40
41 # ============================================================================
42 # TestHarmonyPoliciesTable
43 # ============================================================================
44
45
46 class TestHarmonyPoliciesTable:
47 def test_exactly_six_policies(self) -> None:
48 assert len(HARMONY_POLICIES) == 6
49
50 def test_all_names_unique(self) -> None:
51 names = [p.name for p in HARMONY_POLICIES]
52 assert len(names) == len(set(names)), f"Duplicate policy names: {names}"
53
54 def test_all_strategies_valid(self) -> None:
55 for p in HARMONY_POLICIES:
56 assert p.strategy in VALID_STRATEGIES, (
57 f"Policy '{p.name}' uses invalid strategy '{p.strategy}'"
58 )
59
60 def test_priorities_non_negative(self) -> None:
61 for p in HARMONY_POLICIES:
62 assert p.priority >= 0, f"Policy '{p.name}' has negative priority"
63
64 def test_priorities_all_different(self) -> None:
65 priorities = [p.priority for p in HARMONY_POLICIES]
66 assert len(priorities) == len(set(priorities)), (
67 f"Duplicate priorities: {priorities}"
68 )
69
70 def test_all_have_comments(self) -> None:
71 for p in HARMONY_POLICIES:
72 assert p.comment, f"Policy '{p.name}' has no comment"
73
74 def test_all_have_path_pattern(self) -> None:
75 for p in HARMONY_POLICIES:
76 assert p.path_pattern, f"Policy '{p.name}' has empty path_pattern"
77
78 def test_all_have_dimension(self) -> None:
79 for p in HARMONY_POLICIES:
80 assert p.dimension, f"Policy '{p.name}' has empty dimension"
81
82 def test_default_policy_has_lowest_priority(self) -> None:
83 default = next((p for p in HARMONY_POLICIES if p.name == "default"), None)
84 assert default is not None, "'default' policy not found"
85 min_priority = min(p.priority for p in HARMONY_POLICIES)
86 assert default.priority == min_priority
87
88 def test_archive_policy_has_highest_priority(self) -> None:
89 archive = next((p for p in HARMONY_POLICIES if p.name == "archive"), None)
90 assert archive is not None, "'archive' policy not found"
91 max_priority = max(p.priority for p in HARMONY_POLICIES)
92 assert archive.priority == max_priority
93
94 def test_harmony_policy_is_frozen(self) -> None:
95 import dataclasses
96
97 p = HARMONY_POLICIES[0]
98 with pytest.raises((AttributeError, TypeError, dataclasses.FrozenInstanceError)):
99 p.strategy = "hacked" # type: ignore[misc]
100
101
102 # ============================================================================
103 # TestPerPolicyFixtures (one fixture per policy row)
104 # ============================================================================
105
106
107 class TestPerPolicyFixtures:
108 """Each test verifies that a canonical path+dimension pair resolves to the
109 expected strategy via resolve_harmony_strategy()."""
110
111 def test_archive_policy(self) -> None:
112 assert resolve_harmony_strategy("archive/old-note.md", "*") == "ours"
113
114 def test_archive_policy_nested(self) -> None:
115 assert resolve_harmony_strategy("archive/2024/jan/note.md", "*") == "ours"
116
117 def test_inbox_policy(self) -> None:
118 strategy = resolve_harmony_strategy("inbox/capture.md", "*")
119 assert strategy == "prefer-newer-date"
120
121 def test_inbox_policy_nested(self) -> None:
122 assert (
123 resolve_harmony_strategy("inbox/subdir/item.md", "*")
124 == "prefer-newer-date"
125 )
126
127 def test_capture_policy(self) -> None:
128 assert (
129 resolve_harmony_strategy("captures/pdf-import.md", "*")
130 == "prefer-newer-date"
131 )
132
133 def test_tags_policy_frontmatter_dimension(self) -> None:
134 strategy = resolve_harmony_strategy("projects/born-free/note.md", "frontmatter")
135 assert strategy == "union-sorted"
136
137 def test_tags_policy_any_md_file(self) -> None:
138 strategy = resolve_harmony_strategy("areas/health/daily.md", "frontmatter")
139 assert strategy == "union-sorted"
140
141 def test_project_policy_sections_dimension(self) -> None:
142 # The tags rule (frontmatter dim) doesn't match dimension="sections";
143 # so the project rule (knowtation-3way, priority 20) fires first.
144 strategy = resolve_harmony_strategy("projects/born-free/analysis.md", "sections")
145 assert strategy == "knowtation-3way"
146
147 def test_project_policy_star_dimension_tags_wins(self) -> None:
148 # When dimension="*", the **/*.md tags rule (priority 30) matches before
149 # the projects/** rule (priority 20) because 30 > 20.
150 strategy = resolve_harmony_strategy("projects/born-free/analysis.md", "*")
151 assert strategy == "union-sorted"
152
153 def test_project_policy_nested_path(self) -> None:
154 # projects/born-free/inbox/note.md — "inbox/**" only matches paths that
155 # start with "inbox/", not nested. Tags rule (priority 30) matches first.
156 strategy = resolve_harmony_strategy("projects/born-free/inbox/note.md", "*")
157 assert strategy == "union-sorted"
158
159 def test_default_policy_catch_all_non_md(self) -> None:
160 # .txt files don't match "**/*.md", so default (auto) fires.
161 assert resolve_harmony_strategy("uncategorized/random.txt", "*") == "auto"
162
163 def test_default_policy_unknown_dimension_non_md(self) -> None:
164 assert resolve_harmony_strategy("something/entirely/different.json", "notes") == "auto"
165
166 def test_archive_beats_tags_for_frontmatter(self) -> None:
167 # archive (priority 50) beats tags rule (priority 30) on any dimension.
168 assert resolve_harmony_strategy("archive/note.md", "frontmatter") == "ours"
169
170 def test_archive_beats_tags_for_sections(self) -> None:
171 assert resolve_harmony_strategy("archive/note.md", "sections") == "ours"
172
173
174 # ============================================================================
175 # TestToAttributeRules
176 # ============================================================================
177
178
179 class TestToAttributeRules:
180 def test_returns_six_rules(self) -> None:
181 rules = to_attribute_rules()
182 assert len(rules) == 6
183
184 def test_sorted_by_priority_descending(self) -> None:
185 rules = to_attribute_rules()
186 priorities = [r.priority for r in rules]
187 assert priorities == sorted(priorities, reverse=True)
188
189 def test_all_strategies_valid(self) -> None:
190 for rule in to_attribute_rules():
191 assert rule.strategy in VALID_STRATEGIES
192
193 def test_highest_priority_rule_first(self) -> None:
194 rules = to_attribute_rules()
195 max_p = max(p.priority for p in HARMONY_POLICIES)
196 assert rules[0].priority == max_p
197
198 def test_lowest_priority_rule_last(self) -> None:
199 rules = to_attribute_rules()
200 min_p = min(p.priority for p in HARMONY_POLICIES)
201 assert rules[-1].priority == min_p
202
203
204 # ============================================================================
205 # TestResolveHarmonyStrategy
206 # ============================================================================
207
208
209 class TestResolveHarmonyStrategy:
210 def test_archive_star_dimension(self) -> None:
211 # archive rule (priority 50, dimension=*) → ours
212 strategy = resolve_harmony_strategy("archive/note.md", "notes")
213 assert strategy == "ours"
214
215 def test_frontmatter_dimension_matched(self) -> None:
216 strategy = resolve_harmony_strategy("areas/health.md", "frontmatter")
217 assert strategy == "union-sorted"
218
219 def test_notes_dimension_falls_to_project(self) -> None:
220 # The tags rule has dimension=frontmatter; doesn't match "notes"
221 # → project rule (knowtation-3way) fires.
222 strategy = resolve_harmony_strategy("projects/x/note.md", "notes")
223 assert strategy == "knowtation-3way"
224
225 def test_default_returns_auto_for_non_md(self) -> None:
226 strategy = resolve_harmony_strategy("random_path.toml", "config")
227 assert strategy == "auto"
228
229 def test_returns_string(self) -> None:
230 assert isinstance(resolve_harmony_strategy("*", "*"), str)
231
232
233 # ============================================================================
234 # TestWriteHarmonyMuseattributes
235 # ============================================================================
236
237
238 class TestWriteHarmonyMuseattributes:
239 def test_creates_file(self, tmp_path: pathlib.Path) -> None:
240 write_harmony_museattributes(tmp_path)
241 assert (tmp_path / ".museattributes").exists()
242
243 def test_written_file_is_valid_toml(self, tmp_path: pathlib.Path) -> None:
244 write_harmony_museattributes(tmp_path)
245 content = (tmp_path / ".museattributes").read_text(encoding="utf-8")
246 parsed = tomllib.loads(content)
247 assert isinstance(parsed, dict)
248
249 def test_written_file_has_six_rules(self, tmp_path: pathlib.Path) -> None:
250 write_harmony_museattributes(tmp_path)
251 content = (tmp_path / ".museattributes").read_text(encoding="utf-8")
252 parsed = tomllib.loads(content)
253 assert len(parsed.get("rules", [])) == 6
254
255 def test_written_file_meta_domain_is_knowtation(self, tmp_path: pathlib.Path) -> None:
256 write_harmony_museattributes(tmp_path)
257 content = (tmp_path / ".museattributes").read_text(encoding="utf-8")
258 parsed = tomllib.loads(content)
259 assert parsed.get("meta", {}).get("domain") == "knowtation"
260
261 def test_overwrite_false_raises_on_existing(self, tmp_path: pathlib.Path) -> None:
262 write_harmony_museattributes(tmp_path)
263 with pytest.raises(FileExistsError):
264 write_harmony_museattributes(tmp_path, overwrite=False)
265
266 def test_overwrite_true_replaces_file(self, tmp_path: pathlib.Path) -> None:
267 write_harmony_museattributes(tmp_path)
268 write_harmony_museattributes(tmp_path, overwrite=True)
269 content = (tmp_path / ".museattributes").read_text(encoding="utf-8")
270 parsed = tomllib.loads(content)
271 assert len(parsed.get("rules", [])) == 6
272
273 def test_all_policy_strategies_in_written_file(self, tmp_path: pathlib.Path) -> None:
274 write_harmony_museattributes(tmp_path)
275 content = (tmp_path / ".museattributes").read_text(encoding="utf-8")
276 for policy in HARMONY_POLICIES:
277 assert policy.strategy in content, (
278 f"Strategy '{policy.strategy}' missing from written file"
279 )
280
281 def test_written_file_loadable_by_load_attributes(self, tmp_path: pathlib.Path) -> None:
282 from muse.core.attributes import load_attributes
283
284 write_harmony_museattributes(tmp_path)
285 rules = load_attributes(tmp_path, domain="knowtation")
286 assert len(rules) == 6
287
288 def test_written_file_resolves_archive_correctly(self, tmp_path: pathlib.Path) -> None:
289 from muse.core.attributes import load_attributes, resolve_strategy
290
291 write_harmony_museattributes(tmp_path)
292 rules = load_attributes(tmp_path, domain="knowtation")
293 assert resolve_strategy(rules, "archive/note.md") == "ours"
294
295 def test_written_file_resolves_inbox_correctly(self, tmp_path: pathlib.Path) -> None:
296 from muse.core.attributes import load_attributes, resolve_strategy
297
298 write_harmony_museattributes(tmp_path)
299 rules = load_attributes(tmp_path, domain="knowtation")
300 assert resolve_strategy(rules, "inbox/note.md") == "prefer-newer-date"
301
302
303 # ============================================================================
304 # TestSyntheticMergeScenarios (integration — per policy)
305 # ============================================================================
306
307
308 class TestSyntheticMergeScenarios:
309 """Simulate what the merger would see by checking resolved strategies."""
310
311 def _rules(self) -> list:
312 return to_attribute_rules()
313
314 def test_scenario_archive_note_conflict(self) -> None:
315 from muse.core.attributes import resolve_strategy
316
317 strategy = resolve_strategy(self._rules(), "archive/2024/jan/note.md", "*")
318 assert strategy == "ours"
319
320 def test_scenario_inbox_telegram_capture(self) -> None:
321 from muse.core.attributes import resolve_strategy
322
323 strategy = resolve_strategy(self._rules(), "inbox/telegram-2025-06-01.md", "*")
324 assert strategy == "prefer-newer-date"
325
326 def test_scenario_project_collaboration_note_sections(self) -> None:
327 from muse.core.attributes import resolve_strategy
328
329 # Sections dimension: tags rule (frontmatter) doesn't match → project rule
330 strategy = resolve_strategy(self._rules(), "projects/born-free/meeting-2025.md", "sections")
331 assert strategy == "knowtation-3way"
332
333 def test_scenario_frontmatter_tag_update(self) -> None:
334 from muse.core.attributes import resolve_strategy
335
336 strategy = resolve_strategy(
337 self._rules(), "projects/born-free/analysis.md", "frontmatter"
338 )
339 # frontmatter dimension → union-sorted (tags rule, priority 30)
340 assert strategy == "union-sorted"
341
342 def test_scenario_pdf_import(self) -> None:
343 from muse.core.attributes import resolve_strategy
344
345 strategy = resolve_strategy(self._rules(), "captures/imported-pdf.md", "*")
346 assert strategy == "prefer-newer-date"
347
348 def test_scenario_uncategorised_md_note_star_dim(self) -> None:
349 from muse.core.attributes import resolve_strategy
350
351 # .md file with dimension="*": tags rule (priority 30) matches **/*.md
352 strategy = resolve_strategy(self._rules(), "ideas/random-thought.md", "*")
353 assert strategy == "union-sorted"
354
355 def test_scenario_uncategorised_non_md_default(self) -> None:
356 from muse.core.attributes import resolve_strategy
357
358 # Non-.md file: only the default rule (*) matches
359 strategy = resolve_strategy(self._rules(), "ideas/sketch.txt", "*")
360 assert strategy == "auto"
361
362 def test_scenario_archive_beats_everything(self) -> None:
363 from muse.core.attributes import resolve_strategy
364
365 strategy = resolve_strategy(
366 self._rules(), "archive/projects/old-capture.md", "frontmatter"
367 )
368 assert strategy == "ours"