gabriel / muse public

test_knowtation_symbols.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 muse/plugins/knowtation/symbols.py — Phase 1.4.
2
3 Test tiers covered
4 ------------------
5 1. Unit — 30+ note shapes: address helpers, section splitter, field extraction,
6 NoteSymbol title resolution, full extract_symbols pipeline, edge cases.
7 2. Integration — extract_symbols output matches the muse code symbols JSON schema
8 (domain-aware output contract); real-vault notes parsed without error.
9 3. Data-integrity — every content note in the real vault produces at least one
10 NoteSymbol without raising; section and field counts are non-negative.
11 """
12
13 from __future__ import annotations
14
15 import json
16 import pathlib
17 from typing import Any
18
19 import pytest
20
21 from muse.plugins.knowtation.symbols import (
22 ADDRESS_SEP,
23 FIELD_PREFIX,
24 SECTION_PREFIX,
25 FieldSymbol,
26 NoteSymbol,
27 SectionSymbol,
28 extract_fields,
29 extract_sections,
30 extract_symbols,
31 make_field_address,
32 make_note_address,
33 make_section_address,
34 parse_address,
35 split_sections,
36 symbols_to_json,
37 )
38
39 # ---------------------------------------------------------------------------
40 # Helpers
41 # ---------------------------------------------------------------------------
42
43 _REAL_VAULT = pathlib.Path("/Users/aaronrenecarvajal/knowtation/vault")
44 _SKIP_DIRS: frozenset[str] = frozenset({"templates", "meta"})
45
46
47 def _note(frontmatter: str = "", body: str = "") -> bytes:
48 if frontmatter:
49 return f"---\n{frontmatter}\n---\n{body}".encode()
50 return body.encode()
51
52
53 def _load_content_notes() -> dict[str, bytes]:
54 notes: dict[str, bytes] = {}
55 for md_path in _REAL_VAULT.rglob("*.md"):
56 parts = md_path.relative_to(_REAL_VAULT).parts
57 if parts and parts[0] in _SKIP_DIRS:
58 continue
59 try:
60 notes[str(md_path.relative_to(_REAL_VAULT))] = md_path.read_bytes()
61 except OSError:
62 pass
63 return notes
64
65
66 # ============================================================================
67 # TestAddressHelpers
68 # ============================================================================
69
70
71 class TestMakeNoteAddress:
72 def test_returns_path(self) -> None:
73 assert make_note_address("inbox/foo.md") == "inbox/foo.md"
74
75 def test_nested_path(self) -> None:
76 assert make_note_address("projects/born-free/notes.md") == "projects/born-free/notes.md"
77
78 def test_root_level_path(self) -> None:
79 assert make_note_address("README.md") == "README.md"
80
81
82 class TestMakeFieldAddress:
83 def test_basic(self) -> None:
84 assert make_field_address("inbox/foo.md", "source") == "inbox/foo.md::field:source"
85
86 def test_separator_present(self) -> None:
87 addr = make_field_address("p/note.md", "tags")
88 assert ADDRESS_SEP in addr
89 assert FIELD_PREFIX in addr
90
91 def test_key_in_address(self) -> None:
92 addr = make_field_address("p/note.md", "causal_chain_id")
93 assert "causal_chain_id" in addr
94
95 def test_path_in_address(self) -> None:
96 addr = make_field_address("projects/a/b.md", "date")
97 assert "projects/a/b.md" in addr
98
99
100 class TestMakeSectionAddress:
101 def test_basic_h2(self) -> None:
102 assert make_section_address("f.md", 2, "Background") == "f.md::section:2:Background"
103
104 def test_preamble_level_zero(self) -> None:
105 addr = make_section_address("f.md", 0, "(preamble)")
106 assert "section:0:(preamble)" in addr
107
108 def test_h1_level(self) -> None:
109 addr = make_section_address("f.md", 1, "Overview")
110 assert "section:1:Overview" in addr
111
112 def test_h6_level(self) -> None:
113 addr = make_section_address("f.md", 6, "Deep")
114 assert "section:6:Deep" in addr
115
116 def test_separator_in_address(self) -> None:
117 assert ADDRESS_SEP in make_section_address("f.md", 2, "Title")
118
119
120 class TestParseAddress:
121 def test_note_address(self) -> None:
122 result = parse_address("inbox/foo.md")
123 assert result["kind"] == "note"
124 assert result["path"] == "inbox/foo.md"
125
126 def test_field_address(self) -> None:
127 result = parse_address("inbox/foo.md::field:source")
128 assert result["kind"] == "field"
129 assert result["path"] == "inbox/foo.md"
130 assert result["key"] == "source"
131
132 def test_section_address(self) -> None:
133 result = parse_address("inbox/foo.md::section:2:Background")
134 assert result["kind"] == "section"
135 assert result["path"] == "inbox/foo.md"
136 assert result["level"] == "2"
137 assert result["title"] == "Background"
138
139 def test_preamble_section_address(self) -> None:
140 result = parse_address("f.md::section:0:(preamble)")
141 assert result["kind"] == "section"
142 assert result["level"] == "0"
143 assert result["title"] == "(preamble)"
144
145 def test_section_title_with_colon(self) -> None:
146 # Title contains a colon — only first two colons are separators.
147 result = parse_address("f.md::section:1:My: Title")
148 assert result["kind"] == "section"
149 assert result["title"] == "My: Title"
150
151 def test_unknown_qualifier(self) -> None:
152 result = parse_address("f.md::custom:foo")
153 assert result["kind"] == "unknown"
154
155 def test_round_trip_field(self) -> None:
156 addr = make_field_address("inbox/foo.md", "date")
157 parsed = parse_address(addr)
158 assert parsed["kind"] == "field"
159 assert parsed["key"] == "date"
160
161 def test_round_trip_section(self) -> None:
162 addr = make_section_address("inbox/foo.md", 3, "Details")
163 parsed = parse_address(addr)
164 assert parsed["kind"] == "section"
165 assert parsed["level"] == "3"
166 assert parsed["title"] == "Details"
167
168
169 # ============================================================================
170 # TestSplitSections (30+ note shapes)
171 # ============================================================================
172
173
174 class TestSplitSections:
175 # ── Empty / blank inputs ────────────────────────────────────────────────
176
177 def test_empty_string(self) -> None:
178 assert split_sections("") == []
179
180 def test_whitespace_only(self) -> None:
181 assert split_sections(" \n ") == []
182
183 # ── No headings ─────────────────────────────────────────────────────────
184
185 def test_plain_text_no_headings(self) -> None:
186 body = "Just plain text.\nNo headings here."
187 secs = split_sections(body)
188 assert len(secs) == 1
189 level, title, _, _ = secs[0]
190 assert level == 0
191 assert title == "(preamble)"
192
193 def test_body_only_preamble_content(self) -> None:
194 body = "First paragraph.\n\nSecond paragraph."
195 secs = split_sections(body)
196 assert len(secs) == 1
197 assert secs[0][1] == "(preamble)"
198
199 # ── H1 headings ─────────────────────────────────────────────────────────
200
201 def test_single_h1(self) -> None:
202 body = "# Introduction\nSome content."
203 secs = split_sections(body)
204 assert len(secs) == 1
205 level, title, _, _ = secs[0]
206 assert level == 1
207 assert title == "Introduction"
208
209 def test_two_h1_sections(self) -> None:
210 body = "# Part A\nContent A.\n# Part B\nContent B."
211 secs = split_sections(body)
212 assert len(secs) == 2
213 assert secs[0][0] == 1
214 assert secs[0][1] == "Part A"
215 assert secs[1][1] == "Part B"
216
217 # ── H2 headings (chunk.mjs canonical split level) ───────────────────────
218
219 def test_single_h2(self) -> None:
220 secs = split_sections("## Background\nSome text.")
221 assert len(secs) == 1
222 assert secs[0][0] == 2
223 assert secs[0][1] == "Background"
224
225 def test_two_h2_sections(self) -> None:
226 body = "## Alpha\nContent alpha.\n## Beta\nContent beta."
227 secs = split_sections(body)
228 assert len(secs) == 2
229 assert secs[0][1] == "Alpha"
230 assert secs[1][1] == "Beta"
231
232 def test_three_h2_sections(self) -> None:
233 body = "## A\n## B\n## C\n"
234 assert len(split_sections(body)) == 3
235
236 # ── H3 headings ─────────────────────────────────────────────────────────
237
238 def test_h3_section(self) -> None:
239 secs = split_sections("### Details\nFoo.")
240 assert secs[0][0] == 3
241
242 # ── Mixed heading levels ─────────────────────────────────────────────────
243
244 def test_preamble_then_h2(self) -> None:
245 body = "Intro text.\n## Section\nContent."
246 secs = split_sections(body)
247 assert len(secs) == 2
248 assert secs[0][1] == "(preamble)"
249 assert secs[1][1] == "Section"
250
251 def test_h1_then_h2(self) -> None:
252 body = "# Title\n## Sub\nContent."
253 secs = split_sections(body)
254 assert len(secs) == 2
255 assert secs[0][0] == 1
256 assert secs[1][0] == 2
257
258 def test_h2_then_h3_under_it(self) -> None:
259 body = "## Parent\n### Child\nContent."
260 secs = split_sections(body)
261 assert len(secs) == 2
262 assert secs[0][1] == "Parent"
263 assert secs[1][1] == "Child"
264
265 def test_mixed_h1_h2_h3(self) -> None:
266 body = "# A\n## B\n### C\n## D\n"
267 secs = split_sections(body)
268 assert len(secs) == 4
269 levels = [s[0] for s in secs]
270 assert levels == [1, 2, 3, 2]
271
272 # ── Edge cases ───────────────────────────────────────────────────────────
273
274 def test_heading_with_trailing_hashes(self) -> None:
275 # ATX-style headings may have trailing #: "## Title ##"
276 secs = split_sections("## Title ##\nContent.")
277 assert secs[0][1] == "Title"
278
279 def test_heading_with_inline_emphasis(self) -> None:
280 secs = split_sections("## **Bold** Section\nContent.")
281 assert "Bold" in secs[0][1]
282
283 def test_heading_not_at_line_start_ignored(self) -> None:
284 # Indented "heading" should not be treated as a heading.
285 body = " ## Not a heading\nPlain text."
286 secs = split_sections(body)
287 assert len(secs) == 1
288 assert secs[0][1] == "(preamble)"
289
290 def test_heading_in_code_block_still_matched(self) -> None:
291 # Our regex doesn't know about code blocks; this is an intentional
292 # limitation documented in the module.
293 body = "```\n## In code\n```\n## Real heading\n"
294 secs = split_sections(body)
295 assert len(secs) >= 1
296
297 def test_section_body_includes_heading_line(self) -> None:
298 body = "## My Section\nBody text here."
299 secs = split_sections(body)
300 assert secs[0][2].startswith("## My Section")
301
302 def test_section_body_ends_before_next_heading(self) -> None:
303 body = "## A\nContent A.\n## B\nContent B."
304 secs = split_sections(body)
305 assert "## B" not in secs[0][2]
306
307 def test_line_number_first_section(self) -> None:
308 body = "## First\nContent."
309 secs = split_sections(body)
310 assert secs[0][3] == 1 # first line
311
312 def test_line_number_second_section(self) -> None:
313 body = "## First\nLine 2.\n## Second\nLine 4."
314 secs = split_sections(body)
315 assert secs[1][3] == 3 # "## Second" is on line 3
316
317 def test_h4_h5_h6_detected(self) -> None:
318 body = "#### Four\n##### Five\n###### Six\n"
319 secs = split_sections(body)
320 levels = [s[0] for s in secs]
321 assert 4 in levels
322 assert 5 in levels
323 assert 6 in levels
324
325 def test_empty_section_body(self) -> None:
326 body = "## A\n## B\n"
327 secs = split_sections(body)
328 # Section A has no body text (immediately followed by B)
329 assert len(secs) == 2
330
331 def test_note_with_only_newlines(self) -> None:
332 assert split_sections("\n\n\n") == []
333
334 def test_unicode_heading(self) -> None:
335 body = "## 日本語セクション\nContent."
336 secs = split_sections(body)
337 assert "日本語セクション" in secs[0][1]
338
339 def test_heading_with_numbers(self) -> None:
340 body = "## Section 1.2.3\nContent."
341 secs = split_sections(body)
342 assert secs[0][1] == "Section 1.2.3"
343
344 def test_many_sections(self) -> None:
345 headings = "\n".join(f"## Section {i}\nContent {i}." for i in range(20))
346 secs = split_sections(headings)
347 assert len(secs) == 20
348
349 def test_preamble_not_added_when_absent(self) -> None:
350 body = "## Heading\nContent."
351 secs = split_sections(body)
352 assert secs[0][1] != "(preamble)"
353
354 def test_crlf_body(self) -> None:
355 body = "## Section\r\nContent.\r\n## Next\r\nMore."
356 secs = split_sections(body)
357 assert len(secs) >= 1
358
359
360 # ============================================================================
361 # TestExtractSymbolsPipeline
362 # ============================================================================
363
364
365 class TestExtractSymbolsNoteOnly:
366 """Notes with no frontmatter and no headings."""
367
368 def test_returns_one_note_symbol(self) -> None:
369 syms = extract_symbols("plain.md", b"Just plain text.")
370 assert len([s for s in syms if isinstance(s, NoteSymbol)]) == 1
371
372 def test_note_symbol_is_first(self) -> None:
373 syms = extract_symbols("plain.md", b"Plain text.")
374 assert isinstance(syms[0], NoteSymbol)
375
376 def test_note_address(self) -> None:
377 syms = extract_symbols("inbox/foo.md", b"Body.")
378 note = syms[0]
379 assert isinstance(note, NoteSymbol)
380 assert note.address == "inbox/foo.md"
381
382 def test_note_title_from_filename(self) -> None:
383 syms = extract_symbols("inbox/my-note.md", b"Body.")
384 note = syms[0]
385 assert isinstance(note, NoteSymbol)
386 assert note.title == "my-note"
387
388 def test_note_title_from_h1(self) -> None:
389 syms = extract_symbols("inbox/foo.md", b"# My Title\nBody.")
390 note = syms[0]
391 assert isinstance(note, NoteSymbol)
392 assert note.title == "My Title"
393
394
395 class TestExtractSymbolsWithFrontmatter:
396 def test_title_from_frontmatter(self) -> None:
397 content = _note("title: Vault Note", "# H1 ignored\nBody.")
398 syms = extract_symbols("note.md", content)
399 note = next(s for s in syms if isinstance(s, NoteSymbol))
400 assert note.title == "Vault Note"
401
402 def test_project_from_frontmatter(self) -> None:
403 content = _note("project: born-free", "Body.")
404 syms = extract_symbols("note.md", content)
405 note = next(s for s in syms if isinstance(s, NoteSymbol))
406 assert note.project == "born-free"
407
408 def test_tags_from_frontmatter(self) -> None:
409 content = _note("tags:\n - ai\n - memory", "Body.")
410 syms = extract_symbols("note.md", content)
411 note = next(s for s in syms if isinstance(s, NoteSymbol))
412 assert "ai" in note.tags
413
414 def test_field_symbols_emitted(self) -> None:
415 content = _note("source: telegram\ndate: 2025-01-01", "Body.")
416 syms = extract_symbols("note.md", content)
417 field_syms = [s for s in syms if isinstance(s, FieldSymbol)]
418 keys = {s.key for s in field_syms}
419 assert "source" in keys
420 assert "date" in keys
421
422 def test_field_address_format(self) -> None:
423 content = _note("source: telegram", "Body.")
424 syms = extract_symbols("inbox/foo.md", content)
425 source_field = next(
426 s for s in syms if isinstance(s, FieldSymbol) and s.key == "source"
427 )
428 assert source_field.address == "inbox/foo.md::field:source"
429
430 def test_empty_tags_not_emitted(self) -> None:
431 content = _note("title: No tags", "Body.")
432 syms = extract_symbols("note.md", content)
433 field_keys = {s.key for s in syms if isinstance(s, FieldSymbol)}
434 assert "tags" not in field_keys
435
436 def test_false_state_snapshot_not_emitted(self) -> None:
437 content = _note("state_snapshot: false", "Body.")
438 syms = extract_symbols("note.md", content)
439 field_keys = {s.key for s in syms if isinstance(s, FieldSymbol)}
440 assert "state_snapshot" not in field_keys
441
442 def test_true_state_snapshot_emitted(self) -> None:
443 content = _note("state_snapshot: true", "Body.")
444 syms = extract_symbols("note.md", content)
445 field_keys = {s.key for s in syms if isinstance(s, FieldSymbol)}
446 assert "state_snapshot" in field_keys
447
448
449 class TestExtractSymbolsSections:
450 def test_section_symbols_emitted(self) -> None:
451 content = _note("title: Note", "## Background\nInfo.\n## Conclusion\nEnd.")
452 syms = extract_symbols("note.md", content)
453 secs = [s for s in syms if isinstance(s, SectionSymbol)]
454 assert len(secs) == 2
455
456 def test_section_address_format(self) -> None:
457 content = _note("", "## Background\nInfo.")
458 syms = extract_symbols("note.md", content)
459 sec = next(s for s in syms if isinstance(s, SectionSymbol))
460 assert sec.address == "note.md::section:2:Background"
461
462 def test_section_level_correct(self) -> None:
463 content = _note("", "### Deep\nContent.")
464 syms = extract_symbols("note.md", content)
465 sec = next(s for s in syms if isinstance(s, SectionSymbol))
466 assert sec.level == 3
467
468 def test_section_index_increments(self) -> None:
469 content = _note("", "## A\n## B\n## C\n")
470 syms = extract_symbols("note.md", content)
471 secs = [s for s in syms if isinstance(s, SectionSymbol)]
472 assert [s.index for s in secs] == [0, 1, 2]
473
474 def test_preamble_section_included(self) -> None:
475 content = _note("", "Intro text.\n## Section\nContent.")
476 syms = extract_symbols("note.md", content)
477 secs = [s for s in syms if isinstance(s, SectionSymbol)]
478 titles = [s.title for s in secs]
479 assert "(preamble)" in titles
480
481 def test_section_body_not_empty(self) -> None:
482 content = _note("", "## Section\nHas content.")
483 syms = extract_symbols("note.md", content)
484 sec = next(s for s in syms if isinstance(s, SectionSymbol))
485 assert sec.body.strip()
486
487 def test_symbol_order_note_fields_sections(self) -> None:
488 content = _note("source: telegram\ndate: 2025-01-01", "## My Section\nContent.")
489 syms = extract_symbols("note.md", content)
490 kinds = [s.kind for s in syms]
491 # NoteSymbol always first
492 assert kinds[0] == "note"
493 # FieldSymbols before SectionSymbols
494 first_field = next(i for i, k in enumerate(kinds) if k == "field")
495 first_section = next(i for i, k in enumerate(kinds) if k == "section")
496 assert first_field < first_section
497
498
499 class TestExtractHelpers:
500 def test_extract_sections_returns_only_sections(self) -> None:
501 content = _note("source: t", "## A\n## B\n")
502 secs = extract_sections("note.md", content)
503 assert all(isinstance(s, SectionSymbol) for s in secs)
504 assert len(secs) == 2
505
506 def test_extract_fields_returns_only_fields(self) -> None:
507 content = _note("source: telegram\ndate: 2025-01-01", "Body.")
508 fields = extract_fields("note.md", content)
509 assert all(isinstance(f, FieldSymbol) for f in fields)
510 assert len(fields) >= 2
511
512 def test_extract_sections_empty_note(self) -> None:
513 assert extract_sections("note.md", b"") == []
514
515 def test_extract_fields_no_frontmatter(self) -> None:
516 assert extract_fields("note.md", b"# Just a heading\nBody.") == []
517
518
519 class TestExtractSymbolsEdgeCases:
520 def test_empty_content_returns_note_symbol(self) -> None:
521 syms = extract_symbols("note.md", b"")
522 assert len(syms) == 1
523 assert isinstance(syms[0], NoteSymbol)
524
525 def test_binary_content_returns_note_symbol(self) -> None:
526 syms = extract_symbols("note.md", b"\x00\x01\x02")
527 assert len(syms) >= 1
528 assert isinstance(syms[0], NoteSymbol)
529
530 def test_never_raises(self) -> None:
531 for content in [b"", b"---\n", b"---\n---\n", b"# H\n", b"\xff\xfe"]:
532 result = extract_symbols("note.md", content)
533 assert isinstance(result, list)
534
535 def test_crlf_content(self) -> None:
536 content = b"---\r\nsource: slack\r\ndate: 2025-01-01\r\n---\r\n## Sec\r\nBody."
537 syms = extract_symbols("note.md", content)
538 secs = [s for s in syms if isinstance(s, SectionSymbol)]
539 assert len(secs) >= 1
540
541
542 # ============================================================================
543 # TestSymbolsToJson (integration — muse code symbols --json schema)
544 # ============================================================================
545
546
547 class TestSymbolsToJson:
548 def _build_and_serialize(self, content: bytes) -> dict[str, Any]:
549 syms = extract_symbols("inbox/foo.md", content)
550 return symbols_to_json(syms)
551
552 def test_domain_key(self) -> None:
553 d = self._build_and_serialize(_note("source: t\ndate: 2025-01-01", "Body."))
554 assert d["domain"] == "knowtation"
555
556 def test_total_symbols_count(self) -> None:
557 content = _note("source: telegram\ndate: 2025-01-01", "## A\n## B\n")
558 syms = extract_symbols("inbox/foo.md", content)
559 d = symbols_to_json(syms)
560 assert d["total_symbols"] == len(syms)
561
562 def test_results_is_list(self) -> None:
563 d = self._build_and_serialize(_note("title: x", "## S\n"))
564 assert isinstance(d["results"], list)
565
566 def test_every_result_has_kind_and_address(self) -> None:
567 content = _note("source: telegram\ndate: 2025-01-01", "## Sec\nContent.")
568 d = self._build_and_serialize(content)
569 for item in d["results"]:
570 assert "kind" in item
571 assert "address" in item
572
573 def test_json_serialisable(self) -> None:
574 content = _note("title: x\ntags:\n - ai", "## Sec\nBody.")
575 d = self._build_and_serialize(content)
576 dumped = json.dumps(d)
577 loaded = json.loads(dumped)
578 assert loaded["domain"] == "knowtation"
579
580 def test_note_symbol_in_results(self) -> None:
581 d = self._build_and_serialize(_note("title: x", "Body."))
582 kinds = [r["kind"] for r in d["results"]]
583 assert "note" in kinds
584
585 def test_field_symbol_in_results(self) -> None:
586 content = _note("source: telegram\ndate: 2025-01-01", "Body.")
587 d = self._build_and_serialize(content)
588 kinds = [r["kind"] for r in d["results"]]
589 assert "field" in kinds
590
591 def test_section_symbol_in_results(self) -> None:
592 content = _note("", "## My Section\nContent.")
593 d = self._build_and_serialize(content)
594 kinds = [r["kind"] for r in d["results"]]
595 assert "section" in kinds
596
597 def test_real_vault_note_integration(self) -> None:
598 """Integration: real vault note produces valid JSON matching CLI schema."""
599 note_path = _REAL_VAULT / "inbox" / "foo.md"
600 if not note_path.exists():
601 pytest.skip("Real vault note not found")
602 content = note_path.read_bytes()
603 syms = extract_symbols("inbox/foo.md", content)
604 d = symbols_to_json(syms)
605 assert d["domain"] == "knowtation"
606 assert d["total_symbols"] >= 1
607 # Matches the muse code symbols --json schema (domain-aware extension)
608 assert "results" in d
609 note_results = [r for r in d["results"] if r["kind"] == "note"]
610 assert len(note_results) == 1
611
612
613 # ============================================================================
614 # TestRealVaultDataIntegrity
615 # ============================================================================
616
617
618 @pytest.mark.skipif(
619 not _REAL_VAULT.exists(),
620 reason=f"Real vault not found at {_REAL_VAULT}",
621 )
622 class TestRealVaultDataIntegrity:
623 def test_all_notes_produce_note_symbol(self) -> None:
624 notes = _load_content_notes()
625 for path, content in notes.items():
626 syms = extract_symbols(path, content)
627 assert len(syms) >= 1, f"No symbols for '{path}'"
628 assert isinstance(syms[0], NoteSymbol), f"First symbol not NoteSymbol for '{path}'"
629
630 def test_no_note_raises(self) -> None:
631 notes = _load_content_notes()
632 for path, content in notes.items():
633 try:
634 extract_symbols(path, content)
635 except Exception as exc:
636 pytest.fail(f"extract_symbols raised on '{path}': {exc}")
637
638 def test_section_counts_non_negative(self) -> None:
639 notes = _load_content_notes()
640 for path, content in notes.items():
641 secs = extract_sections(path, content)
642 assert len(secs) >= 0
643
644 def test_field_counts_non_negative(self) -> None:
645 notes = _load_content_notes()
646 for path, content in notes.items():
647 fields = extract_fields(path, content)
648 assert len(fields) >= 0
649
650 def test_all_addresses_contain_path(self) -> None:
651 notes = _load_content_notes()
652 for path, content in notes.items():
653 for sym in extract_symbols(path, content):
654 assert path in sym.address, (
655 f"Address '{sym.address}' does not contain path '{path}'"
656 )
657
658 def test_symbols_to_json_valid_for_all_notes(self) -> None:
659 notes = _load_content_notes()
660 for path, content in notes.items():
661 syms = extract_symbols(path, content)
662 d = symbols_to_json(syms)
663 # Must be JSON-serialisable.
664 try:
665 json.dumps(d)
666 except (TypeError, ValueError) as exc:
667 pytest.fail(f"symbols_to_json not serialisable for '{path}': {exc}")