gabriel / muse public
test_knowtation_link_index.py python
618 lines 22.9 KB
Raw
sha256:660fcac1df3ab28f61862e961890bd2ca8b754fa0242079d93ca1e25037ec8a6 chore(tests): add docstring to tests/__init__.py so rc14 tr… Human 26 days ago
1 """Tests for Phase 3.1 — Knowtation link indexer.
2
3 Test tiers covered (Rule #0):
4
5 1. Unit — wikilink extraction (50 link-shape fixtures)
6 2. Unit — markdown .md link extraction
7 3. Unit — path resolution semantics (relative, absolute, escape attempts)
8 4. Unit — frontmatter strip semantics
9 5. Integration — build_link_index on synthetic vaults (broken links, entity,
10 case sensitivity, duplicates)
11 6. Data-integrity — round-trip on the real Knowtation vault when present
12 7. Performance — build < 10s on a 5k-note synthetic vault
13 8. Stress — large notes do not OOM; oversized notes are skipped
14 9. Security — path traversal in markdown links, deeply nested directories,
15 binary content, NUL bytes
16 """
17
18 from __future__ import annotations
19
20 import os
21 import pathlib
22 import time
23
24 import pytest
25
26 from muse.plugins.knowtation.link_index import (
27 LinkIndex,
28 ResolvedLink,
29 _normalise_rel_path,
30 _strip_frontmatter,
31 _vault_basename_key,
32 _wikilink_target_key,
33 build_link_index,
34 extract_md_links,
35 extract_wikilinks,
36 )
37
38
39 # ============================================================================
40 # Helpers
41 # ============================================================================
42
43
44 def _note(*, fm: str = "", body: str = "") -> bytes:
45 if fm:
46 return f"---\n{fm}\n---\n{body}".encode()
47 return body.encode()
48
49
50 def _make_vault(tmp_path: pathlib.Path, files: dict[str, bytes | str]) -> pathlib.Path:
51 """Populate *tmp_path* with the given files and return the vault root."""
52 for rel, content in files.items():
53 full = tmp_path / rel
54 full.parent.mkdir(parents=True, exist_ok=True)
55 if isinstance(content, str):
56 full.write_text(content, encoding="utf-8")
57 else:
58 full.write_bytes(content)
59 return tmp_path
60
61
62 # ============================================================================
63 # TestWikilinkTargetKey (parity with lib/wikilink.mjs)
64 # ============================================================================
65
66
67 class TestWikilinkTargetKey:
68 def test_plain_name(self) -> None:
69 assert _wikilink_target_key("My Note") == "my note"
70
71 def test_strips_md_extension(self) -> None:
72 assert _wikilink_target_key("My Note.md") == "my note"
73
74 def test_strips_md_extension_uppercase(self) -> None:
75 assert _wikilink_target_key("My Note.MD") == "my note"
76
77 def test_takes_last_path_segment(self) -> None:
78 assert _wikilink_target_key("folder/sub/Note") == "note"
79
80 def test_handles_windows_separators(self) -> None:
81 assert _wikilink_target_key("folder\\sub\\Note") == "note"
82
83 def test_trims_whitespace(self) -> None:
84 assert _wikilink_target_key(" Note ") == "note"
85
86 def test_empty_string(self) -> None:
87 assert _wikilink_target_key("") == ""
88
89 def test_only_whitespace(self) -> None:
90 assert _wikilink_target_key(" ") == ""
91
92
93 class TestVaultBasenameKey:
94 def test_root_file(self) -> None:
95 assert _vault_basename_key("Note.md") == "note"
96
97 def test_nested_file(self) -> None:
98 assert _vault_basename_key("inbox/captures/Note.md") == "note"
99
100 def test_no_extension(self) -> None:
101 assert _vault_basename_key("inbox/Note") == "note"
102
103
104 # ============================================================================
105 # TestExtractWikilinks (50 link-shape fixtures via parametrise)
106 # ============================================================================
107
108
109 _WIKILINK_FIXTURES: list[tuple[str, list[tuple[str, str, str]]]] = [
110 # (body, expected_list_of_(target,fragment,alias))
111 ("[[Note]]", [("Note", "", "")]),
112 ("[[my note]]", [("my note", "", "")]),
113 ("[[folder/Note]]", [("folder/Note", "", "")]),
114 ("[[Note|Display Text]]", [("Note", "", "Display Text")]),
115 ("[[Note#Heading]]", [("Note", "Heading", "")]),
116 ("[[Note#Heading|Alias]]", [("Note", "Heading", "Alias")]),
117 ("[[a]] and [[b]]", [("a", "", ""), ("b", "", "")]),
118 ("no links here", []),
119 ("", []),
120 ("[[]]", []), # empty wikilink invalid
121 ("[[ ]]", [("", "", "")]), # whitespace-only — extractor strips to empty
122 ("[Markdown](url)", []), # not a wikilink
123 ("[[Note.md]]", [("Note.md", "", "")]),
124 ("[[Note With Spaces]]", [("Note With Spaces", "", "")]),
125 ("[[Note]] [[Note]]", [("Note", "", ""), ("Note", "", "")]),
126 ("[[CamelCase]]", [("CamelCase", "", "")]),
127 ("[[note-with-hyphens]]", [("note-with-hyphens", "", "")]),
128 ("[[note_with_underscores]]", [("note_with_underscores", "", "")]),
129 ("[[2025-01-15-daily]]", [("2025-01-15-daily", "", "")]),
130 ("text [[link]] more text", [("link", "", "")]),
131 ("a\n[[link]]\nb", [("link", "", "")]),
132 ("[[a/b/c]]", [("a/b/c", "", "")]),
133 ("[[note#section with spaces]]", [("note", "section with spaces", "")]),
134 ("[[note|alias with spaces]]", [("note", "", "alias with spaces")]),
135 ("[Not a wikilink]", []),
136 ("[Not a wikilink](url)", []),
137 ("[[a]]b[[c]]", [("a", "", ""), ("c", "", "")]),
138 ("[[日本語]]", [("日本語", "", "")]),
139 ("[[émoji-🚀]]", [("émoji-🚀", "", "")]),
140 ("[[note|alias|extra]]", [("note", "", "alias|extra")]),
141 ("[[a]][[b]][[c]]", [("a", "", ""), ("b", "", ""), ("c", "", "")]),
142 ("[[note ]] [[ note]]", [("note", "", ""), ("note", "", "")]),
143 ("```\n[[in code block]]\n```", [("in code block", "", "")]), # body parser doesn't strip code
144 ("[[A]]\n[[B]]\n[[C]]", [("A", "", ""), ("B", "", ""), ("C", "", "")]),
145 ("[[a#frag]] [[b]] [[c|alias]]", [("a", "frag", ""), ("b", "", ""), ("c", "", "alias")]),
146 # Fragment capture is `[^\]|]+` (no # exclusion) so the second # is part of the fragment
147 ("[[note#h1#h2]]", [("note", "h1#h2", "")]),
148 ("[[a||b]]", [("a", "", "|b")]),
149 ("[[a]]\\n[[b]]", [("a", "", ""), ("b", "", "")]),
150 ("[[a]]\t[[b]]", [("a", "", ""), ("b", "", "")]),
151 ("preface [[only]]", [("only", "", "")]),
152 ("[[only]] postscript", [("only", "", "")]),
153 # Target capture `[^\]|#]+` does not exclude newlines in Python re by default
154 ("[[multi\nline]]", [("multi\nline", "", "")]),
155 ("[[a]] [[b#c|d]] [[e|f]]", [("a", "", ""), ("b", "c", "d"), ("e", "", "f")]),
156 ("[[has.periods.in.name]]", [("has.periods.in.name", "", "")]),
157 ("[[trailing/slash/]]", [("trailing/slash/", "", "")]),
158 ("[[/leading/slash]]", [("/leading/slash", "", "")]),
159 ("[[a&b]]", [("a&b", "", "")]),
160 ("[[a%20b]]", [("a%20b", "", "")]),
161 ("[[a(b)c]]", [("a(b)c", "", "")]),
162 ("[[a[b]c]]", []), # [ in target ends match
163 ]
164
165
166 class TestExtractWikilinks:
167 @pytest.mark.parametrize("body, expected", _WIKILINK_FIXTURES)
168 def test_wikilink_extraction(
169 self, body: str, expected: list[tuple[str, str, str]]
170 ) -> None:
171 assert extract_wikilinks(body) == expected
172
173 def test_returns_list(self) -> None:
174 assert isinstance(extract_wikilinks(""), list)
175
176 def test_count_at_least_50_fixtures(self) -> None:
177 assert len(_WIKILINK_FIXTURES) >= 50
178
179
180 # ============================================================================
181 # TestExtractMdLinks
182 # ============================================================================
183
184
185 class TestExtractMdLinks:
186 def test_simple_md_link(self) -> None:
187 assert extract_md_links("[text](file.md)") == [("file.md", "", "text")]
188
189 def test_relative_path(self) -> None:
190 assert extract_md_links("[text](../foo/bar.md)") == [("../foo/bar.md", "", "text")]
191
192 def test_dot_slash_prefix(self) -> None:
193 assert extract_md_links("[text](./foo.md)") == [("./foo.md", "", "text")]
194
195 def test_with_fragment(self) -> None:
196 assert extract_md_links("[text](foo.md#heading)") == [("foo.md", "heading", "text")]
197
198 def test_http_link_ignored(self) -> None:
199 assert extract_md_links("[text](https://example.com/foo.md)") == []
200
201 def test_mailto_ignored(self) -> None:
202 assert extract_md_links("[text](mailto:[email protected])") == []
203
204 def test_non_md_extension_ignored(self) -> None:
205 assert extract_md_links("[text](file.txt)") == []
206
207 def test_multiple_links(self) -> None:
208 out = extract_md_links("[a](a.md) and [b](b.md)")
209 assert out == [("a.md", "", "a"), ("b.md", "", "b")]
210
211 def test_no_links(self) -> None:
212 assert extract_md_links("no links here") == []
213
214 def test_empty_string(self) -> None:
215 assert extract_md_links("") == []
216
217 def test_returns_list(self) -> None:
218 assert isinstance(extract_md_links(""), list)
219
220
221 # ============================================================================
222 # TestNormaliseRelPath (path resolution safety)
223 # ============================================================================
224
225
226 class TestNormaliseRelPath:
227 def test_simple_relative(self) -> None:
228 assert _normalise_rel_path("inbox", "note.md") == "inbox/note.md"
229
230 def test_parent_dir(self) -> None:
231 assert _normalise_rel_path("inbox/captures", "../note.md") == "inbox/note.md"
232
233 def test_dot_slash(self) -> None:
234 assert _normalise_rel_path("inbox", "./note.md") == "inbox/note.md"
235
236 def test_absolute_treated_as_vault_root(self) -> None:
237 assert _normalise_rel_path("inbox", "/projects/note.md") == "projects/note.md"
238
239 def test_escape_attempt_rejected(self) -> None:
240 # Trying to escape vault root with too many ../
241 assert _normalise_rel_path("inbox", "../../../etc/passwd.md") is None
242
243 def test_escape_from_root_rejected(self) -> None:
244 assert _normalise_rel_path("", "../foo.md") is None
245
246 def test_empty_source_dir(self) -> None:
247 assert _normalise_rel_path("", "note.md") == "note.md"
248
249 def test_nested_relative(self) -> None:
250 assert (
251 _normalise_rel_path("projects/born-free", "../../inbox/x.md")
252 == "inbox/x.md"
253 )
254
255
256 # ============================================================================
257 # TestStripFrontmatter
258 # ============================================================================
259
260
261 class TestStripFrontmatter:
262 def test_strips_frontmatter(self) -> None:
263 text = "---\ntitle: x\n---\nBody text."
264 assert _strip_frontmatter(text) == "\nBody text."
265
266 def test_no_frontmatter_returned_unchanged(self) -> None:
267 text = "# Heading\n\nBody."
268 assert _strip_frontmatter(text) == text
269
270 def test_empty_string(self) -> None:
271 assert _strip_frontmatter("") == ""
272
273 def test_only_frontmatter(self) -> None:
274 text = "---\ntitle: x\n---\n"
275 assert _strip_frontmatter(text) == "\n"
276
277 def test_dots_terminator(self) -> None:
278 text = "---\ntitle: x\n...\nBody."
279 assert _strip_frontmatter(text) == "\nBody."
280
281 def test_unterminated_frontmatter_returned_unchanged(self) -> None:
282 text = "---\ntitle: x\nNo terminator."
283 assert _strip_frontmatter(text) == text
284
285
286 # ============================================================================
287 # TestBuildLinkIndex (integration)
288 # ============================================================================
289
290
291 class TestBuildLinkIndex:
292 def test_empty_vault(self, tmp_path: pathlib.Path) -> None:
293 idx = build_link_index(tmp_path)
294 assert idx.notes_indexed == 0
295 assert idx.forward == {}
296 assert idx.backward == {}
297
298 def test_single_note_no_links(self, tmp_path: pathlib.Path) -> None:
299 _make_vault(tmp_path, {"a.md": b"# A\n\nNo links."})
300 idx = build_link_index(tmp_path)
301 assert idx.notes_indexed == 1
302 assert idx.forward["a.md"] == []
303
304 def test_wikilink_resolves(self, tmp_path: pathlib.Path) -> None:
305 _make_vault(tmp_path, {
306 "a.md": b"# A\n\nSee [[B]].",
307 "b.md": b"# B",
308 })
309 idx = build_link_index(tmp_path)
310 links = idx.outgoing("a.md")
311 assert len(links) == 1
312 assert links[0].kind == "wikilink"
313 assert links[0].target == "b.md"
314 assert links[0].broken is False
315
316 def test_case_insensitive_wikilink(self, tmp_path: pathlib.Path) -> None:
317 _make_vault(tmp_path, {
318 "a.md": b"[[NOTE]]",
319 "Note.md": b"# Note",
320 })
321 idx = build_link_index(tmp_path)
322 links = idx.outgoing("a.md")
323 assert len(links) == 1
324 assert links[0].target == "Note.md"
325
326 def test_broken_wikilink(self, tmp_path: pathlib.Path) -> None:
327 _make_vault(tmp_path, {"a.md": b"[[NonExistent]]"})
328 idx = build_link_index(tmp_path)
329 links = idx.outgoing("a.md")
330 assert len(links) == 1
331 assert links[0].broken is True
332 assert links[0].target is None
333 assert links[0] in idx.broken_links
334
335 def test_md_link_resolves(self, tmp_path: pathlib.Path) -> None:
336 _make_vault(tmp_path, {
337 "inbox/a.md": b"See [B](../projects/b.md).",
338 "projects/b.md": b"# B",
339 })
340 idx = build_link_index(tmp_path)
341 links = idx.outgoing("inbox/a.md")
342 assert len(links) == 1
343 assert links[0].kind == "md_link"
344 assert links[0].target == "projects/b.md"
345
346 def test_md_link_with_fragment(self, tmp_path: pathlib.Path) -> None:
347 _make_vault(tmp_path, {
348 "a.md": b"[B](b.md#heading)",
349 "b.md": b"# B",
350 })
351 idx = build_link_index(tmp_path)
352 links = idx.outgoing("a.md")
353 assert links[0].fragment == "heading"
354
355 def test_md_link_escape_attempt_rejected(self, tmp_path: pathlib.Path) -> None:
356 _make_vault(tmp_path, {
357 "inbox/a.md": b"[escape](../../../etc/passwd.md)",
358 })
359 idx = build_link_index(tmp_path)
360 # Cannot resolve outside vault → broken
361 links = idx.outgoing("inbox/a.md")
362 assert len(links) == 1
363 assert links[0].broken is True
364
365 def test_backward_index(self, tmp_path: pathlib.Path) -> None:
366 _make_vault(tmp_path, {
367 "a.md": b"[[Target]]",
368 "b.md": b"[[Target]]",
369 "Target.md": b"# Target",
370 })
371 idx = build_link_index(tmp_path)
372 incoming = idx.incoming("Target.md")
373 sources = {link.source for link in incoming}
374 assert sources == {"a.md", "b.md"}
375
376 def test_entity_index(self, tmp_path: pathlib.Path) -> None:
377 _make_vault(tmp_path, {
378 "a.md": _note(fm="entity:\n - Alice\n - Bob", body="A"),
379 "b.md": _note(fm="entity:\n - Bob", body="B"),
380 "c.md": _note(fm="entity:\n - Charlie", body="C"),
381 })
382 idx = build_link_index(tmp_path)
383 bob_notes = idx.entity_index["Bob"]
384 assert bob_notes == {"a.md", "b.md"}
385 assert idx.co_entity("a.md") == {"b.md"}
386 assert idx.co_entity("c.md") == set()
387
388 def test_duplicate_basenames_recorded(self, tmp_path: pathlib.Path) -> None:
389 _make_vault(tmp_path, {
390 "folder1/Note.md": b"# Note 1",
391 "folder2/Note.md": b"# Note 2",
392 })
393 idx = build_link_index(tmp_path)
394 assert "note" in idx.duplicate_basenames
395 assert len(idx.duplicate_basenames["note"]) == 2
396
397 def test_frontmatter_links_not_extracted(self, tmp_path: pathlib.Path) -> None:
398 """Wikilinks inside YAML frontmatter must NOT be treated as body links."""
399 _make_vault(tmp_path, {
400 "a.md": _note(fm="title: '[[FakeWikilink]]'", body="Real [[B]]"),
401 "b.md": b"# B",
402 })
403 idx = build_link_index(tmp_path)
404 links = idx.outgoing("a.md")
405 # Only the body wikilink should be extracted
406 assert len(links) == 1
407 assert links[0].raw == "B"
408
409 def test_ignored_directories_pruned(self, tmp_path: pathlib.Path) -> None:
410 _make_vault(tmp_path, {
411 "templates/README.md": b"[[Should Not Be Indexed]]",
412 "real.md": b"# Real",
413 })
414 idx = build_link_index(tmp_path)
415 assert idx.notes_indexed == 1
416 assert "templates/README.md" not in idx.forward
417
418 def test_hidden_directories_pruned(self, tmp_path: pathlib.Path) -> None:
419 _make_vault(tmp_path, {
420 ".muse/internal.md": b"# Internal",
421 ".obsidian/notes.md": b"# Obsidian",
422 "real.md": b"# Real",
423 })
424 idx = build_link_index(tmp_path)
425 assert idx.notes_indexed == 1
426
427 def test_nonexistent_root_raises(self, tmp_path: pathlib.Path) -> None:
428 with pytest.raises(ValueError):
429 build_link_index(tmp_path / "does-not-exist")
430
431 def test_file_as_root_raises(self, tmp_path: pathlib.Path) -> None:
432 f = tmp_path / "file.md"
433 f.write_bytes(b"")
434 with pytest.raises(ValueError):
435 build_link_index(f)
436
437
438 # ============================================================================
439 # TestResolvedLinkInvariants
440 # ============================================================================
441
442
443 class TestResolvedLinkInvariants:
444 def test_resolved_link_is_frozen(self) -> None:
445 import dataclasses
446
447 link = ResolvedLink(source="a.md", target="b.md", raw="b", kind="wikilink")
448 with pytest.raises((dataclasses.FrozenInstanceError, AttributeError, TypeError)):
449 link.target = "hacked.md" # type: ignore[misc]
450
451 def test_broken_link_has_no_target(self) -> None:
452 link = ResolvedLink(
453 source="a.md", target=None, raw="missing", kind="wikilink", broken=True
454 )
455 assert link.broken is True
456 assert link.target is None
457
458
459 # ============================================================================
460 # TestPerformance
461 # ============================================================================
462
463
464 class TestPerformance:
465 def test_build_under_10s_on_500_notes(self, tmp_path: pathlib.Path) -> None:
466 """Performance test scaled down to 500 notes for CI speed; real bar is
467 5k in <10s, so 500 should be ~1s. Skip if system is heavily loaded."""
468 files: dict[str, bytes | str] = {}
469 for i in range(500):
470 files[f"note_{i:04d}.md"] = (
471 f"# Note {i}\n\nSee [[note_{(i + 1) % 500:04d}]] "
472 f"and [neighbour](./note_{(i + 2) % 500:04d}.md)."
473 ).encode()
474 _make_vault(tmp_path, files)
475
476 start = time.monotonic()
477 idx = build_link_index(tmp_path)
478 elapsed = time.monotonic() - start
479
480 assert idx.notes_indexed == 500
481 assert elapsed < 5.0, f"500 notes took {elapsed:.2f}s (budget 5s)"
482
483
484 # ============================================================================
485 # TestStress
486 # ============================================================================
487
488
489 class TestStress:
490 def test_large_note_indexed(self, tmp_path: pathlib.Path) -> None:
491 body = "[[target]]\n" * 5000
492 _make_vault(tmp_path, {
493 "big.md": body.encode(),
494 "target.md": b"# T",
495 })
496 idx = build_link_index(tmp_path)
497 assert len(idx.outgoing("big.md")) == 5000
498 assert idx.outgoing("big.md")[0].broken is False
499
500 def test_oversized_note_skipped(
501 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
502 ) -> None:
503 # Lower the cap so we can test without writing 16 MiB.
504 from muse.plugins.knowtation import link_index as li
505
506 monkeypatch.setattr(li, "_MAX_NOTE_BYTES", 100)
507 _make_vault(tmp_path, {
508 "huge.md": b"[[target]]\n" * 100, # >100 bytes
509 "target.md": b"# T",
510 })
511 idx = build_link_index(tmp_path)
512 # huge.md was counted (notes_indexed = 2) but its links are not parsed
513 assert idx.outgoing("huge.md") == []
514
515
516 # ============================================================================
517 # TestSecurity
518 # ============================================================================
519
520
521 class TestSecurity:
522 def test_path_traversal_in_md_link_rejected(self, tmp_path: pathlib.Path) -> None:
523 _make_vault(tmp_path, {
524 "a.md": b"[escape](../../../etc/passwd.md)",
525 })
526 idx = build_link_index(tmp_path)
527 assert idx.outgoing("a.md")[0].broken is True
528
529 def test_absolute_path_with_etc_resolves_inside_vault_only(
530 self, tmp_path: pathlib.Path
531 ) -> None:
532 _make_vault(tmp_path, {
533 "a.md": b"[etc](/etc/passwd.md)",
534 })
535 idx = build_link_index(tmp_path)
536 # Resolves to vault-root "etc/passwd.md" which doesn't exist → broken
537 assert idx.outgoing("a.md")[0].broken is True
538
539 def test_binary_content_does_not_crash(self, tmp_path: pathlib.Path) -> None:
540 _make_vault(tmp_path, {
541 "binary.md": bytes(range(256)) * 10,
542 "real.md": b"# Real",
543 })
544 try:
545 idx = build_link_index(tmp_path)
546 assert idx.notes_indexed == 2
547 except Exception as exc:
548 pytest.fail(f"Binary content crashed indexer: {exc}")
549
550 def test_nul_bytes_in_body(self, tmp_path: pathlib.Path) -> None:
551 _make_vault(tmp_path, {
552 "nul.md": b"[[target]]\x00\x00\x00",
553 "target.md": b"# T",
554 })
555 try:
556 idx = build_link_index(tmp_path)
557 assert idx.notes_indexed == 2
558 except Exception as exc:
559 pytest.fail(f"NUL bytes crashed indexer: {exc}")
560
561 def test_yaml_injection_in_entity_field(self, tmp_path: pathlib.Path) -> None:
562 malicious_fm = "entity: !!python/object/apply:os.system ['echo PWNED']"
563 _make_vault(tmp_path, {
564 "a.md": _note(fm=malicious_fm, body="[[x]]"),
565 "x.md": b"# X",
566 })
567 try:
568 idx = build_link_index(tmp_path)
569 assert idx.notes_indexed == 2
570 except Exception:
571 pass # Acceptable to raise; must NOT execute the payload.
572
573 def test_deeply_nested_directories(self, tmp_path: pathlib.Path) -> None:
574 deep = "/".join([f"d{i}" for i in range(20)])
575 _make_vault(tmp_path, {
576 f"{deep}/note.md": b"[[other]]",
577 "other.md": b"# Other",
578 })
579 idx = build_link_index(tmp_path)
580 assert idx.notes_indexed == 2
581
582
583 # ============================================================================
584 # TestDataIntegrityRealVault
585 # ============================================================================
586
587
588 _REAL_VAULTS: tuple[pathlib.Path, ...] = (
589 pathlib.Path("/Users/aaronrenecarvajal/knowtation/vault"),
590 pathlib.Path("/Users/aaronrenecarvajal/knowtation-vault"),
591 )
592
593
594 def _find_real_vault() -> pathlib.Path | None:
595 for v in _REAL_VAULTS:
596 if v.exists() and v.is_dir():
597 return v
598 return None
599
600
601 class TestDataIntegrityRealVault:
602 """Round-trip on a real Knowtation vault when available."""
603
604 @pytest.mark.skipif(_find_real_vault() is None, reason="No real vault available")
605 def test_real_vault_indexes_without_error(self) -> None:
606 vault = _find_real_vault()
607 assert vault is not None
608 idx = build_link_index(vault)
609 assert idx.notes_indexed > 0
610
611 @pytest.mark.skipif(_find_real_vault() is None, reason="No real vault available")
612 def test_real_vault_completes_under_10s(self) -> None:
613 vault = _find_real_vault()
614 assert vault is not None
615 start = time.monotonic()
616 build_link_index(vault)
617 elapsed = time.monotonic() - start
618 assert elapsed < 10.0, f"Real vault took {elapsed:.2f}s (budget 10s)"
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