gabriel / muse public
test_knowtation_index_rebuild.py python
195 lines 6.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.4 — Knowtation extension of ``muse code index rebuild``.
2
3 Verifies that when ``read_domain(root) == "knowtation"`` the rebuild command
4 also reconstructs the link graph (wikilinks + markdown refs + entity refs)
5 and surfaces it in the JSON output via the ``knowtation_link_graph_*`` keys.
6
7 Test tiers (Rule #0):
8 1. Unit — argparse / dispatch shape (the TypedDict extension)
9 2. Integration — full rebuild on a synthetic vault with mixed link types
10 3. Stress — rebuild on a 500-note vault completes well under budget
11 4. Security — corrupted note bytes do not crash the rebuild
12 """
13
14 from __future__ import annotations
15
16 import argparse
17 import io
18 import json
19 import pathlib
20 import types
21 from contextlib import redirect_stdout
22
23 import pytest
24
25 from muse.cli.commands import index_rebuild as idx_rebuild_cmd
26
27
28 # ============================================================================
29 # Helpers
30 # ============================================================================
31
32
33 def _make_vault(tmp_path: pathlib.Path, files: dict[str, bytes]) -> pathlib.Path:
34 for rel, content in files.items():
35 full = tmp_path / rel
36 full.parent.mkdir(parents=True, exist_ok=True)
37 full.write_bytes(content)
38 return tmp_path
39
40
41 def _stub_require_repo(monkeypatch: pytest.MonkeyPatch, root: pathlib.Path) -> None:
42 """Stub ``require_repo`` to return *root* without checking for ``.muse/``."""
43 monkeypatch.setattr(idx_rebuild_cmd, "require_repo", lambda: root)
44
45
46 def _stub_domain(monkeypatch: pytest.MonkeyPatch, domain: str) -> None:
47 """Force ``read_domain`` to return *domain* regardless of root."""
48 import muse.plugins.registry as registry
49 monkeypatch.setattr(registry, "read_domain", lambda _r: domain)
50
51
52 def _stub_symbol_history_build(monkeypatch: pytest.MonkeyPatch) -> None:
53 """Stub the Code-domain index builders so we don't need a real Muse repo."""
54 monkeypatch.setattr(idx_rebuild_cmd, "_build_symbol_history", lambda _r, **_kw: {})
55 monkeypatch.setattr(idx_rebuild_cmd, "_build_hash_occurrence", lambda _r: {})
56 monkeypatch.setattr(idx_rebuild_cmd, "save_symbol_history", lambda *_a, **_kw: None)
57 monkeypatch.setattr(idx_rebuild_cmd, "save_hash_occurrence", lambda *_a, **_kw: None)
58
59 # Stub SymbolCache so we don't need a real one.
60 class _FakeCache:
61 def save(self) -> None:
62 return None
63
64 monkeypatch.setattr(idx_rebuild_cmd, "load_symbol_cache", lambda _r: _FakeCache())
65
66
67 def _run_rebuild(as_json: bool = True) -> str:
68 """Invoke ``run_rebuild`` with a sensible default Namespace."""
69 args = argparse.Namespace(
70 index_name=None,
71 dry_run=True,
72 verbose=False,
73 as_json=as_json,
74 )
75 buf = io.StringIO()
76 with redirect_stdout(buf):
77 idx_rebuild_cmd.run_rebuild(args)
78 return buf.getvalue()
79
80
81 # ============================================================================
82 # TestKnowtationIndexExtension
83 # ============================================================================
84
85
86 class TestKnowtationIndexExtension:
87 def test_link_graph_keys_present_for_knowtation_domain(
88 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
89 ) -> None:
90 _make_vault(tmp_path, {
91 "a.md": b"[[B]] and [c](c.md)",
92 "b.md": b"# B",
93 "c.md": b"# C",
94 "broken.md": b"[[DoesNotExist]]",
95 })
96 _stub_require_repo(monkeypatch, tmp_path)
97 _stub_domain(monkeypatch, "knowtation")
98 _stub_symbol_history_build(monkeypatch)
99
100 out = _run_rebuild(as_json=True)
101 payload = json.loads(out)
102
103 assert "knowtation_link_graph_notes" in payload
104 assert payload["knowtation_link_graph_notes"] == 4
105 assert payload["knowtation_link_graph_edges_resolved"] >= 2
106 assert payload["knowtation_link_graph_edges_broken"] >= 1
107 assert "knowtation_link_graph" in payload["rebuilt"]
108
109 def test_link_graph_keys_absent_for_code_domain(
110 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
111 ) -> None:
112 _make_vault(tmp_path, {})
113 _stub_require_repo(monkeypatch, tmp_path)
114 _stub_domain(monkeypatch, "code")
115 _stub_symbol_history_build(monkeypatch)
116
117 out = _run_rebuild(as_json=True)
118 payload = json.loads(out)
119
120 assert "knowtation_link_graph_notes" not in payload
121 assert "knowtation_link_graph" not in payload["rebuilt"]
122
123 def test_entity_index_counted(
124 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
125 ) -> None:
126 _make_vault(tmp_path, {
127 "a.md": b"---\nentity:\n - Alice\n - Bob\n---\nA",
128 "b.md": b"---\nentity:\n - Bob\n---\nB",
129 })
130 _stub_require_repo(monkeypatch, tmp_path)
131 _stub_domain(monkeypatch, "knowtation")
132 _stub_symbol_history_build(monkeypatch)
133
134 out = _run_rebuild(as_json=True)
135 payload = json.loads(out)
136
137 assert payload["knowtation_link_graph_entities"] == 2 # Alice + Bob
138
139
140 # ============================================================================
141 # TestStress
142 # ============================================================================
143
144
145 class TestStress:
146 def test_500_note_vault_rebuilds_under_5s(
147 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
148 ) -> None:
149 import time
150
151 files: dict[str, bytes] = {}
152 for i in range(500):
153 files[f"note_{i:04d}.md"] = (
154 f"# Note {i}\n[[note_{(i + 1) % 500:04d}]] "
155 f"[neighbour](./note_{(i + 2) % 500:04d}.md)"
156 ).encode()
157 _make_vault(tmp_path, files)
158 _stub_require_repo(monkeypatch, tmp_path)
159 _stub_domain(monkeypatch, "knowtation")
160 _stub_symbol_history_build(monkeypatch)
161
162 start = time.monotonic()
163 out = _run_rebuild(as_json=True)
164 elapsed = time.monotonic() - start
165
166 payload = json.loads(out)
167 assert payload["knowtation_link_graph_notes"] == 500
168 assert elapsed < 5.0, f"500-note rebuild took {elapsed:.2f}s (budget 5s)"
169
170
171 # ============================================================================
172 # TestSecurity
173 # ============================================================================
174
175
176 class TestSecurity:
177 def test_corrupt_note_bytes_do_not_crash(
178 self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch
179 ) -> None:
180 _make_vault(tmp_path, {
181 "good.md": b"[[other]]",
182 "corrupt.md": bytes(range(256)) * 5,
183 "binary.md": b"\x00\x01\x02\x03[[other]]\xff\xfe",
184 "other.md": b"# Other",
185 })
186 _stub_require_repo(monkeypatch, tmp_path)
187 _stub_domain(monkeypatch, "knowtation")
188 _stub_symbol_history_build(monkeypatch)
189
190 try:
191 out = _run_rebuild(as_json=True)
192 payload = json.loads(out)
193 assert payload["knowtation_link_graph_notes"] == 4
194 except Exception as exc:
195 pytest.fail(f"Corrupt note bytes crashed rebuild: {exc}")
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