gabriel / muse public
test_knowtation_detector.py python
645 lines 23.2 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for muse/plugins/knowtation/detector.py — Phase 1.2.
2
3 Test tiers covered (per Rule #0 and plan §0.2):
4
5 - **Unit** — 12 per-rule fixture cases covering every branch of the
6 priority cascade (file-level and repo-level), plus edge
7 cases (empty content, binary-like, unknown keys, etc.).
8 - **Data-integrity** — real Knowtation vault notes at
9 /Users/aaronrenecarvajal/knowtation/vault/ are
10 classified; ≥99% must resolve to "knowtation". Skipped
11 when the vault is absent (CI / fresh checkout).
12
13 Tiers deferred to later phases:
14 - Integration — ``muse init --domain auto`` selects knowtation (Phase 1.5+).
15 - Security — adversarial frontmatter (YAML injection, oversized blocks)
16 tested in Phase 2.2.
17
18 Rule map for the 12 unit cases
19 -------------------------------
20 File-level (classify_note):
21 F1 source key present → knowtation (rule: source_key)
22 F2 source_type key present → knowtation (rule: source_key)
23 F3 source_id key alone → knowtation (rule: source_id_key)
24 F4 project + date → knowtation (rule: project_or_tags_plus_date)
25 F5 tags + date → knowtation (rule: project_or_tags_plus_date)
26 F6 project alone (no date) → mist (rule: fallback_mist)
27 F7 date alone (no project/tags) → mist (rule: fallback_mist)
28 F8 empty content → mist (rule: no_frontmatter)
29 F9 no frontmatter block → mist (rule: no_frontmatter)
30 F10 frontmatter with only unknown keys → mist (rule: fallback_mist)
31
32 Repo-level (classify_repo):
33 R11 .knowtation marker file present → knowtation (rule: marker_file)
34 R12 no .md files at all → mist (rule: no_markdown_notes)
35 """
36
37 from __future__ import annotations
38
39 import pathlib
40
41 import pytest
42
43 from muse.plugins.knowtation.detector import (
44 ClassificationResult,
45 DOMAIN_KNOWTATION,
46 DOMAIN_MIST,
47 MARKER_FILENAME,
48 REPO_MAJORITY_THRESHOLD,
49 classify_many,
50 classify_note,
51 classify_repo,
52 extract_frontmatter_keys,
53 knowtation_fraction,
54 )
55
56
57 # ---------------------------------------------------------------------------
58 # Fixtures — byte strings
59 # ---------------------------------------------------------------------------
60
61 _SOURCE_NOTE = b"""\
62 ---
63 source: telegram
64 date: 2026-05-12
65 ---
66 # Message from Telegram
67 """
68
69 _SOURCE_TYPE_NOTE = b"""\
70 ---
71 source_type: pdf
72 date: 2026-04-01
73 project: ai-safety
74 ---
75 # PDF Import
76 """
77
78 _SOURCE_ID_NOTE = b"""\
79 ---
80 source_id: msg-abc-123
81 title: Some note
82 ---
83 # Externally tracked note
84 """
85
86 _PROJECT_DATE_NOTE = b"""\
87 ---
88 title: My Note
89 project: born-free
90 date: 2026-01-15
91 ---
92 # Born Free Note
93 """
94
95 _TAGS_DATE_NOTE = b"""\
96 ---
97 tags: [research, alignment]
98 date: 2026-03-01
99 ---
100 # Tagged Note
101 """
102
103 _PROJECT_ONLY_NOTE = b"""\
104 ---
105 project: ai-safety
106 title: No date here
107 ---
108 # Project note with no date
109 """
110
111 _DATE_ONLY_NOTE = b"""\
112 ---
113 date: 2026-05-01
114 title: Just a date
115 ---
116 # Date only
117 """
118
119 _EMPTY_NOTE = b""
120
121 _NO_FRONTMATTER_NOTE = b"""\
122 # Just Markdown
123
124 No frontmatter at all.
125 """
126
127 _UNKNOWN_KEYS_NOTE = b"""\
128 ---
129 foo: bar
130 baz: qux
131 ---
132 # Only unknown keys
133 """
134
135
136 # ===========================================================================
137 # extract_frontmatter_keys — unit tests
138 # ===========================================================================
139
140
141 class TestExtractFrontmatterKeys:
142 """extract_frontmatter_keys must return only top-level key names."""
143
144 def test_source_note_keys(self) -> None:
145 keys = extract_frontmatter_keys(_SOURCE_NOTE)
146 assert "source" in keys
147 assert "date" in keys
148
149 def test_no_frontmatter_returns_empty(self) -> None:
150 assert extract_frontmatter_keys(_NO_FRONTMATTER_NOTE) == frozenset()
151
152 def test_empty_content_returns_empty(self) -> None:
153 assert extract_frontmatter_keys(_EMPTY_NOTE) == frozenset()
154
155 def test_yaml_list_tag_key_detected(self) -> None:
156 note = b"---\ntags: [a, b, c]\ndate: 2026-01-01\n---\n# Title"
157 keys = extract_frontmatter_keys(note)
158 assert "tags" in keys
159 assert "date" in keys
160
161 def test_comma_separated_tags_key_detected(self) -> None:
162 note = b"---\ntags: a, b, c\ndate: 2026-01-01\n---\n# Title"
163 keys = extract_frontmatter_keys(note)
164 assert "tags" in keys
165
166 def test_nested_key_not_counted(self) -> None:
167 note = b"---\nparent:\n child: value\ntitle: Test\n---\n# T"
168 keys = extract_frontmatter_keys(note)
169 assert "parent" in keys
170 assert "title" in keys
171 # "child" is indented — must NOT appear as a top-level key
172 assert "child" not in keys
173
174 def test_unknown_keys_only(self) -> None:
175 keys = extract_frontmatter_keys(_UNKNOWN_KEYS_NOTE)
176 assert keys == frozenset({"foo", "baz"})
177
178 def test_unclosed_frontmatter_returns_empty(self) -> None:
179 note = b"---\ntitle: No closing delimiter"
180 assert extract_frontmatter_keys(note) == frozenset()
181
182 def test_yaml_dot_dot_dot_delimiter(self) -> None:
183 note = b"---\ntitle: Using dots\ndate: 2026-01-01\n...\n# Body"
184 keys = extract_frontmatter_keys(note)
185 assert "title" in keys
186 assert "date" in keys
187
188 def test_stable_for_same_input(self) -> None:
189 k1 = extract_frontmatter_keys(_PROJECT_DATE_NOTE)
190 k2 = extract_frontmatter_keys(_PROJECT_DATE_NOTE)
191 assert k1 == k2
192
193
194 # ===========================================================================
195 # classify_note — 12 per-rule unit cases (F1–F10)
196 # ===========================================================================
197
198
199 class TestClassifyNoteRule1_SourceKey:
200 """F1 — 'source' key → knowtation."""
201
202 def test_domain_is_knowtation(self) -> None:
203 assert classify_note(_SOURCE_NOTE).domain == DOMAIN_KNOWTATION
204
205 def test_rule_is_source_key(self) -> None:
206 assert classify_note(_SOURCE_NOTE).rule == "source_key"
207
208 def test_is_knowtation_property(self) -> None:
209 assert classify_note(_SOURCE_NOTE).is_knowtation
210
211
212 class TestClassifyNoteRule1_SourceTypeKey:
213 """F2 — 'source_type' key → knowtation."""
214
215 def test_domain_is_knowtation(self) -> None:
216 assert classify_note(_SOURCE_TYPE_NOTE).domain == DOMAIN_KNOWTATION
217
218 def test_rule_is_source_key(self) -> None:
219 assert classify_note(_SOURCE_TYPE_NOTE).rule == "source_key"
220
221
222 class TestClassifyNoteRule2_SourceIdKey:
223 """F3 — 'source_id' key (alone, no source/source_type) → knowtation."""
224
225 def test_domain_is_knowtation(self) -> None:
226 assert classify_note(_SOURCE_ID_NOTE).domain == DOMAIN_KNOWTATION
227
228 def test_rule_is_source_id_key(self) -> None:
229 assert classify_note(_SOURCE_ID_NOTE).rule == "source_id_key"
230
231 def test_source_id_without_source_is_sufficient(self) -> None:
232 note = b"---\nsource_id: ticket-42\ntitle: Ticket\n---\n# Body"
233 result = classify_note(note)
234 assert result.domain == DOMAIN_KNOWTATION
235 assert result.rule == "source_id_key"
236
237
238 class TestClassifyNoteRule3_ProjectDate:
239 """F4 — project + date → knowtation."""
240
241 def test_domain_is_knowtation(self) -> None:
242 assert classify_note(_PROJECT_DATE_NOTE).domain == DOMAIN_KNOWTATION
243
244 def test_rule_is_project_or_tags_plus_date(self) -> None:
245 assert classify_note(_PROJECT_DATE_NOTE).rule == "project_or_tags_plus_date"
246
247
248 class TestClassifyNoteRule3_TagsDate:
249 """F5 — tags + date → knowtation."""
250
251 def test_domain_is_knowtation(self) -> None:
252 assert classify_note(_TAGS_DATE_NOTE).domain == DOMAIN_KNOWTATION
253
254 def test_rule_is_project_or_tags_plus_date(self) -> None:
255 assert classify_note(_TAGS_DATE_NOTE).rule == "project_or_tags_plus_date"
256
257
258 class TestClassifyNoteRule4_ProjectOnly:
259 """F6 — project without date → mist (rule 3 requires date)."""
260
261 def test_domain_is_mist(self) -> None:
262 assert classify_note(_PROJECT_ONLY_NOTE).domain == DOMAIN_MIST
263
264 def test_is_not_knowtation(self) -> None:
265 assert not classify_note(_PROJECT_ONLY_NOTE).is_knowtation
266
267
268 class TestClassifyNoteRule4_DateOnly:
269 """F7 — date without project/tags → mist."""
270
271 def test_domain_is_mist(self) -> None:
272 assert classify_note(_DATE_ONLY_NOTE).domain == DOMAIN_MIST
273
274 def test_is_not_knowtation(self) -> None:
275 assert not classify_note(_DATE_ONLY_NOTE).is_knowtation
276
277
278 class TestClassifyNoteEmptyContent:
279 """F8 — empty content → mist (no_frontmatter)."""
280
281 def test_domain_is_mist(self) -> None:
282 assert classify_note(_EMPTY_NOTE).domain == DOMAIN_MIST
283
284 def test_rule_is_no_frontmatter(self) -> None:
285 assert classify_note(_EMPTY_NOTE).rule == "no_frontmatter"
286
287 def test_confidence_is_zero(self) -> None:
288 assert classify_note(_EMPTY_NOTE).confidence == 0.0
289
290
291 class TestClassifyNoteNoFrontmatter:
292 """F9 — plain Markdown without frontmatter → mist (no_frontmatter)."""
293
294 def test_domain_is_mist(self) -> None:
295 assert classify_note(_NO_FRONTMATTER_NOTE).domain == DOMAIN_MIST
296
297 def test_rule_is_no_frontmatter(self) -> None:
298 assert classify_note(_NO_FRONTMATTER_NOTE).rule == "no_frontmatter"
299
300
301 class TestClassifyNoteUnknownKeys:
302 """F10 — frontmatter with no knowtation-identifying keys → mist."""
303
304 def test_domain_is_mist(self) -> None:
305 assert classify_note(_UNKNOWN_KEYS_NOTE).domain == DOMAIN_MIST
306
307 def test_rule_is_fallback_mist(self) -> None:
308 assert classify_note(_UNKNOWN_KEYS_NOTE).rule == "fallback_mist"
309
310
311 # ===========================================================================
312 # classify_repo — repo-level rules (R11–R12)
313 # ===========================================================================
314
315
316 class TestClassifyRepoMarkerFile:
317 """R11 — .knowtation marker file → knowtation."""
318
319 def test_marker_classifies_repo_as_knowtation(self, tmp_path: pathlib.Path) -> None:
320 (tmp_path / MARKER_FILENAME).write_text("")
321 result = classify_repo(tmp_path)
322 assert result.domain == DOMAIN_KNOWTATION
323
324 def test_rule_is_marker_file(self, tmp_path: pathlib.Path) -> None:
325 (tmp_path / MARKER_FILENAME).write_text("")
326 result = classify_repo(tmp_path)
327 assert result.rule == "marker_file"
328
329 def test_marker_overrides_absence_of_notes(self, tmp_path: pathlib.Path) -> None:
330 (tmp_path / MARKER_FILENAME).write_text("")
331 # No .md files at all — marker still wins
332 result = classify_repo(tmp_path)
333 assert result.domain == DOMAIN_KNOWTATION
334
335
336 class TestClassifyRepoNoMarkdown:
337 """R12 — no .md files → mist (no_markdown_notes)."""
338
339 def test_empty_repo_is_mist(self, tmp_path: pathlib.Path) -> None:
340 result = classify_repo(tmp_path)
341 assert result.domain == DOMAIN_MIST
342
343 def test_rule_is_no_markdown_notes(self, tmp_path: pathlib.Path) -> None:
344 result = classify_repo(tmp_path)
345 assert result.rule == "no_markdown_notes"
346
347 def test_non_markdown_files_ignored(self, tmp_path: pathlib.Path) -> None:
348 (tmp_path / "image.png").write_bytes(b"\x89PNG")
349 (tmp_path / "data.json").write_text('{"key": "value"}')
350 result = classify_repo(tmp_path)
351 assert result.domain == DOMAIN_MIST
352 assert result.rule == "no_markdown_notes"
353
354
355 class TestClassifyRepoMajorityVote:
356 """Majority-vote rule: ≥ REPO_MAJORITY_THRESHOLD fraction → knowtation."""
357
358 def _write_note(
359 self, root: pathlib.Path, name: str, content: bytes
360 ) -> None:
361 root.mkdir(parents=True, exist_ok=True)
362 (root / name).write_bytes(content)
363
364 def test_all_knowtation_notes(self, tmp_path: pathlib.Path) -> None:
365 for i in range(5):
366 (tmp_path / f"note{i}.md").write_bytes(_SOURCE_NOTE)
367 result = classify_repo(tmp_path)
368 assert result.domain == DOMAIN_KNOWTATION
369 assert result.rule == "majority_vote"
370 assert result.confidence == 1.0
371
372 def test_majority_threshold_met(self, tmp_path: pathlib.Path) -> None:
373 # 3 knowtation, 2 mist → 60% ≥ 50% threshold
374 for i in range(3):
375 (tmp_path / f"kt{i}.md").write_bytes(_SOURCE_NOTE)
376 for i in range(2):
377 (tmp_path / f"mist{i}.md").write_bytes(_NO_FRONTMATTER_NOTE)
378 result = classify_repo(tmp_path)
379 assert result.domain == DOMAIN_KNOWTATION
380 assert result.rule == "majority_vote"
381 assert abs(result.confidence - 0.6) < 1e-9
382
383 def test_below_threshold_is_mist(self, tmp_path: pathlib.Path) -> None:
384 # 2 knowtation, 6 mist → 25% < 50%
385 for i in range(2):
386 (tmp_path / f"kt{i}.md").write_bytes(_SOURCE_NOTE)
387 for i in range(6):
388 (tmp_path / f"mist{i}.md").write_bytes(_NO_FRONTMATTER_NOTE)
389 result = classify_repo(tmp_path)
390 assert result.domain == DOMAIN_MIST
391 assert result.rule == "fallback_mist"
392
393 def test_exactly_at_threshold(self, tmp_path: pathlib.Path) -> None:
394 # Exactly 50% → knowtation (≥ threshold)
395 for i in range(5):
396 (tmp_path / f"kt{i}.md").write_bytes(_SOURCE_NOTE)
397 for i in range(5):
398 (tmp_path / f"mist{i}.md").write_bytes(_NO_FRONTMATTER_NOTE)
399 result = classify_repo(tmp_path)
400 assert result.domain == DOMAIN_KNOWTATION
401
402 def test_nested_notes_are_found(self, tmp_path: pathlib.Path) -> None:
403 (tmp_path / "projects" / "ai").mkdir(parents=True)
404 (tmp_path / "projects" / "ai" / "research.md").write_bytes(_PROJECT_DATE_NOTE)
405 result = classify_repo(tmp_path)
406 assert result.domain == DOMAIN_KNOWTATION
407
408
409 # ===========================================================================
410 # ClassificationResult — property and dataclass tests
411 # ===========================================================================
412
413
414 class TestClassificationResult:
415 """ClassificationResult must behave correctly as a dataclass."""
416
417 def test_is_knowtation_true(self) -> None:
418 r = ClassificationResult(domain=DOMAIN_KNOWTATION, rule="source_key")
419 assert r.is_knowtation
420
421 def test_is_knowtation_false(self) -> None:
422 r = ClassificationResult(domain=DOMAIN_MIST, rule="fallback_mist", confidence=0.0)
423 assert not r.is_knowtation
424
425 def test_default_confidence_is_one(self) -> None:
426 r = ClassificationResult(domain=DOMAIN_KNOWTATION, rule="source_key")
427 assert r.confidence == 1.0
428
429 def test_detail_defaults_empty(self) -> None:
430 r = ClassificationResult(domain=DOMAIN_KNOWTATION, rule="source_key")
431 assert r.detail == ""
432
433
434 # ===========================================================================
435 # classify_many and knowtation_fraction helpers
436 # ===========================================================================
437
438
439 class TestClassifyMany:
440 """classify_many must apply classify_note to every entry."""
441
442 def test_returns_result_for_each_note(self) -> None:
443 notes = {
444 "a.md": _SOURCE_NOTE,
445 "b.md": _NO_FRONTMATTER_NOTE,
446 }
447 results = classify_many(notes)
448 assert set(results) == {"a.md", "b.md"}
449 assert results["a.md"].domain == DOMAIN_KNOWTATION
450 assert results["b.md"].domain == DOMAIN_MIST
451
452 def test_empty_dict_returns_empty(self) -> None:
453 assert classify_many({}) == {}
454
455
456 class TestKnowtationFraction:
457 """knowtation_fraction must return the correct ratio."""
458
459 def test_all_knowtation(self) -> None:
460 notes = {f"n{i}.md": _SOURCE_NOTE for i in range(10)}
461 assert knowtation_fraction(notes) == 1.0
462
463 def test_none_knowtation(self) -> None:
464 notes = {f"n{i}.md": _NO_FRONTMATTER_NOTE for i in range(5)}
465 assert knowtation_fraction(notes) == 0.0
466
467 def test_mixed_fraction(self) -> None:
468 notes = {
469 "a.md": _SOURCE_NOTE,
470 "b.md": _PROJECT_DATE_NOTE,
471 "c.md": _NO_FRONTMATTER_NOTE,
472 "d.md": _EMPTY_NOTE,
473 }
474 assert abs(knowtation_fraction(notes) - 0.5) < 1e-9
475
476 def test_empty_dict_returns_zero(self) -> None:
477 assert knowtation_fraction({}) == 0.0
478
479
480 # ===========================================================================
481 # Priority ordering — higher rules must beat lower ones
482 # ===========================================================================
483
484
485 class TestPriorityCascade:
486 """Verify that rules fire in the declared priority order."""
487
488 def test_source_beats_project_date(self) -> None:
489 # A note with both 'source' AND 'project'+'date' must fire rule 1 (source_key),
490 # not rule 3 (project_or_tags_plus_date).
491 note = b"---\nsource: telegram\nproject: ai\ndate: 2026-01-01\n---\n# T"
492 result = classify_note(note)
493 assert result.rule == "source_key"
494
495 def test_source_id_beats_project_date(self) -> None:
496 note = b"---\nsource_id: x\nproject: ai\ndate: 2026-01-01\n---\n# T"
497 result = classify_note(note)
498 assert result.rule == "source_id_key"
499
500 def test_source_beats_source_id(self) -> None:
501 # Both 'source' and 'source_id' present — rule 1 fires first.
502 note = b"---\nsource: slack\nsource_id: msg-1\n---\n# T"
503 result = classify_note(note)
504 assert result.rule == "source_key"
505
506 def test_tags_plus_date_is_rule3_not_fallback(self) -> None:
507 note = b"---\ntags: [a, b]\ndate: 2026-05-12\n---\n# T"
508 result = classify_note(note)
509 assert result.rule == "project_or_tags_plus_date"
510 assert result.domain == DOMAIN_KNOWTATION
511
512
513 # ===========================================================================
514 # Edge cases
515 # ===========================================================================
516
517
518 class TestEdgeCases:
519 """Edge cases: binary content, BOM, CRLF, deeply nested vault."""
520
521 def test_binary_content_no_frontmatter(self) -> None:
522 binary = bytes(range(256))
523 result = classify_note(binary)
524 assert result.domain == DOMAIN_MIST
525
526 def test_frontmatter_with_crlf_line_endings(self) -> None:
527 note = b"---\r\nsource: telegram\r\ndate: 2026-01-01\r\n---\r\n# T"
528 # CRLF endings — YAML key regex should still match
529 keys = extract_frontmatter_keys(note)
530 # source key may or may not be found depending on CRLF handling;
531 # what matters is we do not crash
532 assert isinstance(keys, frozenset)
533
534 def test_very_long_frontmatter_does_not_hang(self) -> None:
535 # 500-key frontmatter should parse quickly
536 lines = [f"key{i}: value{i}" for i in range(500)]
537 fm = b"---\n" + "\n".join(lines).encode() + b"\nsource: telegram\n---\n# T"
538 result = classify_note(fm)
539 assert result.domain == DOMAIN_KNOWTATION
540
541 def test_note_where_source_key_is_value_not_key(self) -> None:
542 # 'source' appears only as a frontmatter VALUE — not a key
543 note = b"---\ncategory: source\ndate: 2026-01-01\n---\n# T"
544 keys = extract_frontmatter_keys(note)
545 assert "category" in keys
546 assert "source" not in keys
547
548 def test_classify_repo_ignores_hidden_dirs(self, tmp_path: pathlib.Path) -> None:
549 # .obsidian folder with .md files must be skipped
550 hidden = tmp_path / ".obsidian"
551 hidden.mkdir()
552 (hidden / "workspace.md").write_bytes(_SOURCE_NOTE)
553 # Only add a mist note at the root
554 (tmp_path / "plain.md").write_bytes(_NO_FRONTMATTER_NOTE)
555 result = classify_repo(tmp_path)
556 assert result.domain == DOMAIN_MIST # the hidden .md was ignored
557
558
559 # ===========================================================================
560 # Data-integrity: real Knowtation vault
561 # ===========================================================================
562
563 _REAL_VAULT = pathlib.Path("/Users/aaronrenecarvajal/knowtation/vault")
564 _REAL_VAULT_AVAILABLE = _REAL_VAULT.is_dir()
565
566
567 @pytest.mark.skipif(
568 not _REAL_VAULT_AVAILABLE,
569 reason="Real Knowtation vault not present — skipping data-integrity test",
570 )
571 class TestRealVaultDataIntegrity:
572 """All real vault notes must classify ≥99% as knowtation.
573
574 The Knowtation vault at /Users/aaronrenecarvajal/knowtation/vault/ is the
575 production vault. Every note there should satisfy at least one of rules
576 1–3. A ≥99% pass rate leaves room for a single "bare" note that has no
577 knowtation frontmatter yet (e.g. a quick draft).
578
579 Plan §1.2 calls for "1000-note sample → ≥99% classified knowtation".
580 The actual vault has 84 notes at the time of implementation; we test all
581 of them and note the divergence. The threshold is unchanged at 99%.
582 """
583
584 # Structural directories that hold scaffold/documentation files, not
585 # authored vault notes. Per SPEC §1, these are optional with user-defined
586 # semantics and are explicitly NOT the content directories (inbox/, projects/,
587 # areas/). Template READMEs have no frontmatter by design.
588 _SKIP_DIRS: frozenset[str] = frozenset({"templates", "meta"})
589
590 def _load_vault_notes(self) -> dict[str, bytes]:
591 notes: dict[str, bytes] = {}
592 for md_path in _REAL_VAULT.rglob("*.md"):
593 # Skip structural scaffold directories.
594 parts = md_path.relative_to(_REAL_VAULT).parts
595 if parts and parts[0] in self._SKIP_DIRS:
596 continue
597 try:
598 notes[str(md_path.relative_to(_REAL_VAULT))] = md_path.read_bytes()
599 except OSError:
600 pass
601 return notes
602
603 def test_vault_note_count_is_positive(self) -> None:
604 notes = self._load_vault_notes()
605 assert len(notes) > 0, "Real vault returned no notes"
606
607 def test_knowtation_fraction_meets_threshold(self) -> None:
608 notes = self._load_vault_notes()
609 fraction = knowtation_fraction(notes)
610 total = len(notes)
611 knowtation_count = round(fraction * total)
612 assert fraction >= 0.99, (
613 f"Only {knowtation_count}/{total} vault notes classified as knowtation "
614 f"({fraction:.1%} < 99% threshold). "
615 f"Failing notes: "
616 + str(sorted(
617 path for path, content in notes.items()
618 if not classify_note(content).is_knowtation
619 ))
620 )
621
622 def test_classify_repo_returns_knowtation_for_vault_root(self) -> None:
623 result = classify_repo(_REAL_VAULT)
624 assert result.domain == DOMAIN_KNOWTATION, (
625 f"classify_repo returned {result.domain!r} (rule={result.rule!r}): {result.detail}"
626 )
627
628 def test_all_failing_notes_reported(self) -> None:
629 """Collect and log any notes that do NOT classify as knowtation."""
630 notes = self._load_vault_notes()
631 failures = {
632 path: classify_note(content)
633 for path, content in notes.items()
634 if not classify_note(content).is_knowtation
635 }
636 if failures:
637 # Emit as a warning (not failure — the 99% test above controls pass/fail).
638 import warnings
639 for path, result in sorted(failures.items()):
640 warnings.warn(
641 f"Note not classified as knowtation: {path!r} "
642 f"(rule={result.rule!r}, detail={result.detail!r})",
643 UserWarning,
644 stacklevel=1,
645 )
File History 2 commits
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
sha256:d8316ffae901be06347e16ab55be11868eb519dd16ade3e8aa16a99e662f7e62 baseline: rc14 re-baseline after rc3 store corruption recovery Human patch 26 days ago