gabriel / muse public
test_knowtation_parser.py python
752 lines 27.1 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/parser.py — Phase 1.3.
2
3 Test tiers covered
4 ------------------
5 1. Unit — every public function and property, all SPEC §2 field groups,
6 coercion edge cases, slug normalisation, validation, migration pipeline.
7 2. Data-integrity — parse every content note in the real Knowtation vault
8 without error; verify round-trip fidelity (parse → to_dict → parse).
9 """
10
11 from __future__ import annotations
12
13 import pathlib
14 import textwrap
15 import warnings
16 from typing import Any
17
18 import pytest
19
20 from muse.plugins.knowtation.parser import (
21 FRONTMATTER_SCHEMA_VERSION,
22 SCHEMA_VERSIONS,
23 ParsedFrontmatter,
24 _coerce_optional_str,
25 _coerce_str_or_list,
26 _extract_frontmatter_block,
27 migrate_frontmatter,
28 normalize_slug,
29 parse_frontmatter,
30 validate_inbox_note,
31 validate_slug_field,
32 )
33
34 # ---------------------------------------------------------------------------
35 # Helpers
36 # ---------------------------------------------------------------------------
37
38 _REAL_VAULT = pathlib.Path("/Users/aaronrenecarvajal/knowtation/vault")
39
40 # Structural directories that hold scaffold docs, not authored vault notes.
41 _SKIP_DIRS: frozenset[str] = frozenset({"templates", "meta"})
42
43
44 def _note(frontmatter: str, body: str = "") -> bytes:
45 """Build raw note bytes from a frontmatter YAML string and optional body."""
46 return f"---\n{frontmatter}\n---\n{body}".encode()
47
48
49 def _load_content_notes() -> dict[str, bytes]:
50 notes: dict[str, bytes] = {}
51 for md_path in _REAL_VAULT.rglob("*.md"):
52 parts = md_path.relative_to(_REAL_VAULT).parts
53 if parts and parts[0] in _SKIP_DIRS:
54 continue
55 try:
56 notes[str(md_path.relative_to(_REAL_VAULT))] = md_path.read_bytes()
57 except OSError:
58 pass
59 return notes
60
61
62 # ============================================================================
63 # TestExtractFrontmatterBlock
64 # ============================================================================
65
66
67 class TestExtractFrontmatterBlock:
68 def test_basic_extraction(self) -> None:
69 raw = _extract_frontmatter_block(_note("title: Hello"))
70 assert raw is not None
71 assert "title: Hello" in raw
72
73 def test_no_frontmatter_returns_none(self) -> None:
74 assert _extract_frontmatter_block(b"# Just a heading\n") is None
75
76 def test_empty_content_returns_none(self) -> None:
77 assert _extract_frontmatter_block(b"") is None
78
79 def test_unclosed_block_returns_none(self) -> None:
80 assert _extract_frontmatter_block(b"---\ntitle: oops\n") is None
81
82 def test_yaml_dots_delimiter(self) -> None:
83 content = b"---\ntitle: dot\n...\nbody here"
84 raw = _extract_frontmatter_block(content)
85 assert raw is not None
86 assert "title: dot" in raw
87
88 def test_crlf_line_endings(self) -> None:
89 content = b"---\r\ntitle: crlf\r\n---\r\nbody"
90 raw = _extract_frontmatter_block(content)
91 assert raw is not None
92
93 def test_binary_content_returns_none(self) -> None:
94 assert _extract_frontmatter_block(b"\x00\x01\x02\x03") is None
95
96
97 # ============================================================================
98 # TestCoerceHelpers
99 # ============================================================================
100
101
102 class TestCoerceOptionalStr:
103 def test_none_returns_none(self) -> None:
104 assert _coerce_optional_str(None) is None
105
106 def test_string_passthrough(self) -> None:
107 assert _coerce_optional_str("hello") == "hello"
108
109 def test_integer_stringified(self) -> None:
110 assert _coerce_optional_str(42) == "42"
111
112 def test_bool_stringified(self) -> None:
113 assert _coerce_optional_str(True) == "True"
114
115
116 class TestCoerceStrOrList:
117 def test_none_returns_empty(self) -> None:
118 assert _coerce_str_or_list(None) == []
119
120 def test_yaml_list_preserved(self) -> None:
121 assert _coerce_str_or_list(["foo", "bar"]) == ["foo", "bar"]
122
123 def test_comma_separated_string_split(self) -> None:
124 assert _coerce_str_or_list("foo, bar, baz") == ["foo", "bar", "baz"]
125
126 def test_single_string_no_comma(self) -> None:
127 assert _coerce_str_or_list("solo") == ["solo"]
128
129 def test_empty_string_returns_empty(self) -> None:
130 assert _coerce_str_or_list("") == []
131
132 def test_integer_element_stringified(self) -> None:
133 assert _coerce_str_or_list([1, 2]) == ["1", "2"]
134
135 def test_mixed_list_stringified(self) -> None:
136 assert _coerce_str_or_list(["a", 1, None]) == ["a", "1", "None"]
137
138
139 # ============================================================================
140 # TestNormalizeSlug
141 # ============================================================================
142
143
144 class TestNormalizeSlug:
145 def test_lowercase(self) -> None:
146 assert normalize_slug("BornFree") == "bornfree"
147
148 def test_spaces_become_hyphens(self) -> None:
149 assert normalize_slug("born free") == "born-free"
150
151 def test_underscores_become_hyphens(self) -> None:
152 assert normalize_slug("born_free") == "born-free"
153
154 def test_leading_trailing_hyphens_stripped(self) -> None:
155 assert normalize_slug("-hello-") == "hello"
156
157 def test_already_valid_slug_unchanged(self) -> None:
158 assert normalize_slug("born-free") == "born-free"
159
160 def test_numbers_preserved(self) -> None:
161 assert normalize_slug("phase1") == "phase1"
162
163 def test_special_chars_replaced(self) -> None:
164 result = normalize_slug("hello!world")
165 assert result == "hello-world"
166
167
168 # ============================================================================
169 # TestParseFrontmatterSection21Common
170 # ============================================================================
171
172
173 class TestParseFrontmatterSection21Common:
174 def test_title_parsed(self) -> None:
175 fm = parse_frontmatter(_note("title: My Note"))
176 assert fm is not None
177 assert fm.title == "My Note"
178
179 def test_project_parsed(self) -> None:
180 fm = parse_frontmatter(_note("project: born-free"))
181 assert fm is not None
182 assert fm.project == "born-free"
183
184 def test_tags_yaml_list(self) -> None:
185 fm = parse_frontmatter(_note("tags:\n - ai\n - research"))
186 assert fm is not None
187 assert fm.tags == ["ai", "research"]
188
189 def test_tags_comma_separated_string(self) -> None:
190 fm = parse_frontmatter(_note("tags: ai, research, memory"))
191 assert fm is not None
192 assert fm.tags == ["ai", "research", "memory"]
193
194 def test_date_parsed(self) -> None:
195 fm = parse_frontmatter(_note("date: 2025-01-15"))
196 assert fm is not None
197 assert fm.date == "2025-01-15"
198
199 def test_updated_parsed(self) -> None:
200 fm = parse_frontmatter(_note("updated: 2025-06-01"))
201 assert fm is not None
202 assert fm.updated == "2025-06-01"
203
204 def test_all_common_fields(self) -> None:
205 content = _note(
206 "title: Full Note\nproject: my-proj\ntags:\n - t1\ndate: 2025-01-01\nupdated: 2025-03-01"
207 )
208 fm = parse_frontmatter(content)
209 assert fm is not None
210 assert fm.title == "Full Note"
211 assert fm.project == "my-proj"
212 assert fm.tags == ["t1"]
213 assert fm.date == "2025-01-01"
214 assert fm.updated == "2025-03-01"
215
216 def test_schema_version_set(self) -> None:
217 fm = parse_frontmatter(_note("title: x"))
218 assert fm is not None
219 assert fm.schema_version == FRONTMATTER_SCHEMA_VERSION
220
221
222 # ============================================================================
223 # TestParseFrontmatterSection22Inbox
224 # ============================================================================
225
226
227 class TestParseFrontmatterSection22Inbox:
228 def _inbox_note(self) -> bytes:
229 return _note("source: telegram\ndate: 2025-05-01\nsource_id: msg-abc123")
230
231 def test_source_parsed(self) -> None:
232 fm = parse_frontmatter(self._inbox_note())
233 assert fm is not None
234 assert fm.source == "telegram"
235
236 def test_source_id_parsed(self) -> None:
237 fm = parse_frontmatter(self._inbox_note())
238 assert fm is not None
239 assert fm.source_id == "msg-abc123"
240
241 def test_source_type_alias_parsed(self) -> None:
242 fm = parse_frontmatter(_note("source_type: chatgpt-export\ndate: 2025-05-01"))
243 assert fm is not None
244 assert fm.source_type == "chatgpt-export"
245
246 def test_effective_source_prefers_source(self) -> None:
247 fm = parse_frontmatter(
248 _note("source: telegram\nsource_type: slack\ndate: 2025-05-01")
249 )
250 assert fm is not None
251 assert fm.effective_source == "telegram"
252
253 def test_effective_source_falls_back_to_source_type(self) -> None:
254 fm = parse_frontmatter(_note("source_type: chatgpt-export\ndate: 2025-05-01"))
255 assert fm is not None
256 assert fm.effective_source == "chatgpt-export"
257
258 def test_effective_source_none_when_absent(self) -> None:
259 fm = parse_frontmatter(_note("title: plain"))
260 assert fm is not None
261 assert fm.effective_source is None
262
263 def test_is_inbox_note_true(self) -> None:
264 fm = parse_frontmatter(self._inbox_note())
265 assert fm is not None
266 assert fm.is_inbox_note is True
267
268 def test_is_inbox_note_false_no_source(self) -> None:
269 fm = parse_frontmatter(_note("date: 2025-05-01"))
270 assert fm is not None
271 assert fm.is_inbox_note is False
272
273 def test_is_inbox_note_false_no_date(self) -> None:
274 fm = parse_frontmatter(_note("source: telegram"))
275 assert fm is not None
276 assert fm.is_inbox_note is False
277
278
279 # ============================================================================
280 # TestParseFrontmatterSection23Temporal
281 # ============================================================================
282
283
284 class TestParseFrontmatterSection23Temporal:
285 def test_follows_single_string(self) -> None:
286 fm = parse_frontmatter(_note("follows: inbox/note-a.md"))
287 assert fm is not None
288 assert fm.follows == ["inbox/note-a.md"]
289
290 def test_follows_yaml_list(self) -> None:
291 fm = parse_frontmatter(_note("follows:\n - inbox/a.md\n - inbox/b.md"))
292 assert fm is not None
293 assert fm.follows == ["inbox/a.md", "inbox/b.md"]
294
295 def test_causal_chain_id(self) -> None:
296 fm = parse_frontmatter(_note("causal_chain_id: chain-abc"))
297 assert fm is not None
298 assert fm.causal_chain_id == "chain-abc"
299
300 def test_entity_list(self) -> None:
301 fm = parse_frontmatter(_note("entity:\n - person:alice\n - project:muse"))
302 assert fm is not None
303 assert fm.entity == ["person:alice", "project:muse"]
304
305 def test_episode_id(self) -> None:
306 fm = parse_frontmatter(_note("episode_id: ep-2025-01"))
307 assert fm is not None
308 assert fm.episode_id == "ep-2025-01"
309
310 def test_summarizes_list(self) -> None:
311 fm = parse_frontmatter(_note("summarizes:\n - notes/old.md"))
312 assert fm is not None
313 assert fm.summarizes == ["notes/old.md"]
314
315 def test_summarizes_range(self) -> None:
316 fm = parse_frontmatter(_note("summarizes_range: 2025-01/2025-03"))
317 assert fm is not None
318 assert fm.summarizes_range == "2025-01/2025-03"
319
320 def test_state_snapshot_true(self) -> None:
321 fm = parse_frontmatter(_note("state_snapshot: true"))
322 assert fm is not None
323 assert fm.state_snapshot is True
324
325 def test_state_snapshot_false_by_default(self) -> None:
326 fm = parse_frontmatter(_note("title: no snapshot flag"))
327 assert fm is not None
328 assert fm.state_snapshot is False
329
330
331 # ============================================================================
332 # TestParseFrontmatterSection24Reserved
333 # ============================================================================
334
335
336 class TestParseFrontmatterSection24Reserved:
337 """Reserved Phase-12 fields are stored without validation."""
338
339 def test_network_parsed(self) -> None:
340 fm = parse_frontmatter(_note("network: icp"))
341 assert fm is not None
342 assert fm.network == "icp"
343
344 def test_wallet_address_parsed(self) -> None:
345 # Quote the value so PyYAML does not interpret it as a hex integer.
346 fm = parse_frontmatter(_note('wallet_address: "0xDEAD"'))
347 assert fm is not None
348 assert fm.wallet_address == "0xDEAD"
349
350 def test_tx_hash_parsed(self) -> None:
351 fm = parse_frontmatter(_note("tx_hash: abc123"))
352 assert fm is not None
353 assert fm.tx_hash == "abc123"
354
355 def test_payment_status_parsed(self) -> None:
356 fm = parse_frontmatter(_note("payment_status: completed"))
357 assert fm is not None
358 assert fm.payment_status == "completed"
359
360 def test_reserved_fields_all_none_by_default(self) -> None:
361 fm = parse_frontmatter(_note("title: plain"))
362 assert fm is not None
363 assert fm.network is None
364 assert fm.wallet_address is None
365 assert fm.tx_hash is None
366 assert fm.payment_status is None
367
368
369 # ============================================================================
370 # TestParseFrontmatterAttachments
371 # ============================================================================
372
373
374 class TestParseFrontmatterAttachments:
375 """Phase 1.7 attachments field (mist IDs)."""
376
377 def test_attachments_yaml_list(self) -> None:
378 fm = parse_frontmatter(_note("attachments:\n - abc123def456\n - 123456abcdef"))
379 assert fm is not None
380 assert fm.attachments == ["abc123def456", "123456abcdef"]
381
382 def test_attachments_empty_by_default(self) -> None:
383 fm = parse_frontmatter(_note("title: plain"))
384 assert fm is not None
385 assert fm.attachments == []
386
387
388 # ============================================================================
389 # TestParseFrontmatterExtraKeys
390 # ============================================================================
391
392
393 class TestParseFrontmatterExtraKeys:
394 def test_unknown_key_goes_to_extra(self) -> None:
395 fm = parse_frontmatter(_note("custom_field: my_value"))
396 assert fm is not None
397 assert fm.extra == {"custom_field": "my_value"}
398
399 def test_known_keys_not_in_extra(self) -> None:
400 fm = parse_frontmatter(_note("title: x\ncustom: y"))
401 assert fm is not None
402 assert "title" not in fm.extra
403 assert fm.extra == {"custom": "y"}
404
405 def test_no_unknown_keys_extra_empty(self) -> None:
406 fm = parse_frontmatter(_note("title: clean\ndate: 2025-01-01"))
407 assert fm is not None
408 assert fm.extra == {}
409
410
411 # ============================================================================
412 # TestParseFrontmatterEdgeCases
413 # ============================================================================
414
415
416 class TestParseFrontmatterEdgeCases:
417 def test_no_frontmatter_returns_none(self) -> None:
418 assert parse_frontmatter(b"# Just a heading\n") is None
419
420 def test_empty_content_returns_none(self) -> None:
421 assert parse_frontmatter(b"") is None
422
423 def test_invalid_yaml_returns_none(self) -> None:
424 content = b"---\n: bad: yaml: here\n---\n"
425 result = parse_frontmatter(content)
426 # Either None (parse error) or a valid ParsedFrontmatter — never raises.
427 assert result is None or isinstance(result, ParsedFrontmatter)
428
429 def test_frontmatter_is_scalar_returns_none(self) -> None:
430 content = b"---\njust a string\n---\n"
431 assert parse_frontmatter(content) is None
432
433 def test_frontmatter_is_list_returns_none(self) -> None:
434 content = b"---\n- item1\n- item2\n---\n"
435 assert parse_frontmatter(content) is None
436
437 def test_binary_content_returns_none(self) -> None:
438 assert parse_frontmatter(b"\x00\x01\x02") is None
439
440 def test_crlf_note_parsed_correctly(self) -> None:
441 content = b"---\r\nsource: slack\r\ndate: 2025-01-01\r\n---\r\nbody"
442 fm = parse_frontmatter(content)
443 assert fm is not None
444 assert fm.source == "slack"
445
446 def test_date_as_yaml_date_object_coerced(self) -> None:
447 # PyYAML parses `date: 2025-01-01` as a Python date object; coerce to str.
448 fm = parse_frontmatter(_note("date: 2025-01-01"))
449 assert fm is not None
450 assert fm.date is not None
451 assert "2025" in fm.date
452
453
454 # ============================================================================
455 # TestParsedFrontmatterToDict (round-trip)
456 # ============================================================================
457
458
459 class TestParsedFrontmatterToDict:
460 def test_omits_none_fields(self) -> None:
461 fm = ParsedFrontmatter(title="x")
462 d = fm.to_dict()
463 assert "project" not in d
464 assert "tags" not in d
465 assert d["title"] == "x"
466
467 def test_omits_empty_lists(self) -> None:
468 fm = ParsedFrontmatter()
469 d = fm.to_dict()
470 assert "tags" not in d
471 assert "follows" not in d
472
473 def test_omits_false_state_snapshot(self) -> None:
474 fm = ParsedFrontmatter(state_snapshot=False)
475 assert "state_snapshot" not in fm.to_dict()
476
477 def test_includes_true_state_snapshot(self) -> None:
478 fm = ParsedFrontmatter(state_snapshot=True)
479 assert fm.to_dict()["state_snapshot"] is True
480
481 def test_extra_keys_included(self) -> None:
482 fm = ParsedFrontmatter(extra={"my_custom": "val"})
483 assert fm.to_dict()["my_custom"] == "val"
484
485 def test_round_trip_inbox_note(self) -> None:
486 original = _note(
487 "source: telegram\nsource_id: msg-1\ndate: 2025-05-01\nproject: born-free\ntags:\n - ai\n"
488 )
489 fm1 = parse_frontmatter(original)
490 assert fm1 is not None
491 # Serialise to dict then wrap in YAML + re-parse.
492 import yaml
493
494 serialised = ("---\n" + yaml.dump(fm1.to_dict()) + "---\n").encode()
495 fm2 = parse_frontmatter(serialised)
496 assert fm2 is not None
497 assert fm2.source == fm1.source
498 assert fm2.source_id == fm1.source_id
499 assert fm2.date == fm1.date
500 assert fm2.project == fm1.project
501 assert fm2.tags == fm1.tags
502
503 def test_round_trip_temporal_note(self) -> None:
504 import yaml
505
506 original = _note(
507 "follows: inbox/a.md\ncausal_chain_id: chain-1\nentity:\n - alice\n"
508 "episode_id: ep-1\nsummarizes:\n - notes/old.md\n"
509 "summarizes_range: 2025-01/2025-03\nstate_snapshot: true\n"
510 )
511 fm1 = parse_frontmatter(original)
512 assert fm1 is not None
513 serialised = ("---\n" + yaml.dump(fm1.to_dict()) + "---\n").encode()
514 fm2 = parse_frontmatter(serialised)
515 assert fm2 is not None
516 assert fm2.follows == fm1.follows
517 assert fm2.causal_chain_id == fm1.causal_chain_id
518 assert fm2.entity == fm1.entity
519 assert fm2.state_snapshot is True
520
521
522 # ============================================================================
523 # TestValidateInboxNote
524 # ============================================================================
525
526
527 class TestValidateInboxNote:
528 def test_valid_inbox_note_no_errors(self) -> None:
529 fm = ParsedFrontmatter(source="telegram", date="2025-05-01")
530 assert validate_inbox_note(fm) == []
531
532 def test_valid_with_source_type_alias(self) -> None:
533 fm = ParsedFrontmatter(source_type="chatgpt-export", date="2025-05-01")
534 assert validate_inbox_note(fm) == []
535
536 def test_missing_source_produces_error(self) -> None:
537 fm = ParsedFrontmatter(date="2025-05-01")
538 errors = validate_inbox_note(fm)
539 assert any("source" in e for e in errors)
540
541 def test_missing_date_produces_error(self) -> None:
542 fm = ParsedFrontmatter(source="telegram")
543 errors = validate_inbox_note(fm)
544 assert any("date" in e for e in errors)
545
546 def test_missing_both_produces_two_errors(self) -> None:
547 fm = ParsedFrontmatter()
548 assert len(validate_inbox_note(fm)) == 2
549
550
551 # ============================================================================
552 # TestValidateSlugField
553 # ============================================================================
554
555
556 class TestValidateSlugField:
557 def test_valid_slug_no_errors(self) -> None:
558 assert validate_slug_field("born-free", "project") == []
559
560 def test_uppercase_produces_error(self) -> None:
561 errors = validate_slug_field("BornFree", "project")
562 assert len(errors) == 1
563 # normalize_slug("BornFree") → "bornfree" (no hyphen inserted for case change).
564 assert "bornfree" in errors[0]
565
566 def test_spaces_produce_error(self) -> None:
567 errors = validate_slug_field("born free", "project")
568 assert len(errors) == 1
569
570 def test_underscore_produces_error(self) -> None:
571 errors = validate_slug_field("born_free", "project")
572 assert len(errors) == 1
573
574 def test_field_name_in_error_message(self) -> None:
575 errors = validate_slug_field("BAD", "my_field")
576 assert "my_field" in errors[0]
577
578
579 # ============================================================================
580 # TestSchemaVersionConstant
581 # ============================================================================
582
583
584 class TestSchemaVersionConstant:
585 def test_schema_version_is_string(self) -> None:
586 assert isinstance(FRONTMATTER_SCHEMA_VERSION, str)
587
588 def test_schema_version_in_versions_tuple(self) -> None:
589 assert FRONTMATTER_SCHEMA_VERSION in SCHEMA_VERSIONS
590
591 def test_schema_versions_ordered(self) -> None:
592 assert list(SCHEMA_VERSIONS) == sorted(SCHEMA_VERSIONS, key=int)
593
594 def test_parsed_note_carries_current_version(self) -> None:
595 fm = parse_frontmatter(_note("title: x"))
596 assert fm is not None
597 assert fm.schema_version == FRONTMATTER_SCHEMA_VERSION
598
599
600 # ============================================================================
601 # TestSchemaMigration
602 # ============================================================================
603
604
605 class TestSchemaMigration:
606 def test_same_version_returns_same_object(self) -> None:
607 data: dict[str, Any] = {"title": "x"}
608 result = migrate_frontmatter(data, FRONTMATTER_SCHEMA_VERSION)
609 assert result is data # unchanged — same object
610
611 def test_v1_to_v2_stub_preserves_all_keys(self) -> None:
612 data: dict[str, Any] = {
613 "title": "migration test",
614 "source": "telegram",
615 "date": "2025-01-01",
616 "custom": "value",
617 }
618 result = migrate_frontmatter(data, "1")
619 assert result["title"] == "migration test" # not lost
620 assert result["source"] == "telegram"
621 assert result["custom"] == "value"
622
623 def test_v1_to_v2_stub_no_data_loss(self) -> None:
624 data: dict[str, Any] = {"tags": ["a", "b"], "project": "born-free"}
625 result = migrate_frontmatter(data, "1")
626 assert result["tags"] == ["a", "b"]
627 assert result["project"] == "born-free"
628
629 def test_unknown_version_raises(self) -> None:
630 with pytest.raises(ValueError, match="Unknown frontmatter schema version"):
631 migrate_frontmatter({}, "99")
632
633 def test_migration_result_is_parseable(self) -> None:
634 import yaml
635
636 data = {"source": "slack", "date": "2025-05-01"}
637 migrated = migrate_frontmatter(data, "1")
638 yaml_str = yaml.dump(migrated)
639 content = f"---\n{yaml_str}---\n".encode()
640 fm = parse_frontmatter(content)
641 assert fm is not None
642 assert fm.source == "slack"
643
644
645 # ============================================================================
646 # TestRealVaultDataIntegrity
647 # ============================================================================
648
649 _REAL_VAULT_NOTES: dict[str, bytes] = {}
650
651
652 def _get_real_vault_notes() -> dict[str, bytes]:
653 global _REAL_VAULT_NOTES
654 if not _REAL_VAULT_NOTES:
655 _REAL_VAULT_NOTES = _load_content_notes()
656 return _REAL_VAULT_NOTES
657
658
659 @pytest.mark.skipif(
660 not _REAL_VAULT.exists(),
661 reason=f"Real vault not found at {_REAL_VAULT}",
662 )
663 class TestRealVaultDataIntegrity:
664 """Parse every content note in the real vault; verify no crashes and round-trip."""
665
666 def test_vault_has_content_notes(self) -> None:
667 notes = _get_real_vault_notes()
668 assert len(notes) > 0, "Vault should contain at least one content note."
669
670 def test_all_notes_parse_without_exception(self) -> None:
671 """parse_frontmatter must never raise — it returns None or ParsedFrontmatter."""
672 notes = _get_real_vault_notes()
673 for path, content in notes.items():
674 try:
675 result = parse_frontmatter(content)
676 assert result is None or isinstance(result, ParsedFrontmatter), (
677 f"Unexpected return type for {path}: {type(result)}"
678 )
679 except Exception as exc:
680 pytest.fail(f"parse_frontmatter raised on '{path}': {exc}")
681
682 def test_notes_with_frontmatter_carry_schema_version(self) -> None:
683 notes = _get_real_vault_notes()
684 for path, content in notes.items():
685 fm = parse_frontmatter(content)
686 if fm is not None:
687 assert fm.schema_version == FRONTMATTER_SCHEMA_VERSION, (
688 f"schema_version mismatch for '{path}'"
689 )
690
691 def test_round_trip_lossless_for_known_fields(self) -> None:
692 """parse → to_dict → parse produces the same known fields."""
693 import yaml
694
695 notes = _get_real_vault_notes()
696 failed: list[str] = []
697 for path, content in notes.items():
698 fm1 = parse_frontmatter(content)
699 if fm1 is None:
700 continue
701 d1 = fm1.to_dict()
702 try:
703 serialised = ("---\n" + yaml.dump(d1) + "---\n").encode()
704 fm2 = parse_frontmatter(serialised)
705 except Exception as exc:
706 failed.append(f"{path}: serialisation error — {exc}")
707 continue
708
709 if fm2 is None:
710 failed.append(f"{path}: re-parse returned None after round-trip")
711 continue
712
713 # Key known fields must survive the round-trip.
714 for attr in ("title", "project", "date", "source", "source_id"):
715 v1 = getattr(fm1, attr)
716 v2 = getattr(fm2, attr)
717 if v1 != v2:
718 failed.append(
719 f"{path}: field '{attr}' changed: {v1!r} → {v2!r}"
720 )
721
722 if failed:
723 warnings.warn(
724 f"{len(failed)} round-trip failures:\n" + "\n".join(failed[:10]),
725 UserWarning,
726 stacklevel=2,
727 )
728 pytest.fail(
729 f"{len(failed)} notes failed round-trip fidelity check. "
730 "See warnings for details."
731 )
732
733 def test_inbox_notes_satisfy_spec_contract(self) -> None:
734 """Every parsed note in inbox/ that claims a source must also have a date."""
735 notes = _get_real_vault_notes()
736 violations: list[str] = []
737 for path, content in notes.items():
738 if "inbox" not in path:
739 continue
740 fm = parse_frontmatter(content)
741 if fm is None:
742 continue
743 errors = validate_inbox_note(fm)
744 if errors:
745 violations.append(f"{path}: {errors}")
746 if violations:
747 warnings.warn(
748 f"{len(violations)} inbox note(s) violate SPEC §2.2:\n"
749 + "\n".join(violations[:10]),
750 UserWarning,
751 stacklevel=2,
752 )
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