"""Tests for Phase 3.4 — Knowtation extension of ``muse code index rebuild``. Verifies that when ``read_domain(root) == "knowtation"`` the rebuild command also reconstructs the link graph (wikilinks + markdown refs + entity refs) and surfaces it in the JSON output via the ``knowtation_link_graph_*`` keys. Test tiers (Rule #0): 1. Unit — argparse / dispatch shape (the TypedDict extension) 2. Integration — full rebuild on a synthetic vault with mixed link types 3. Stress — rebuild on a 500-note vault completes well under budget 4. Security — corrupted note bytes do not crash the rebuild """ from __future__ import annotations import argparse import io import json import pathlib import types from contextlib import redirect_stdout import pytest from muse.cli.commands import index_rebuild as idx_rebuild_cmd # ============================================================================ # Helpers # ============================================================================ def _make_vault(tmp_path: pathlib.Path, files: dict[str, bytes]) -> pathlib.Path: for rel, content in files.items(): full = tmp_path / rel full.parent.mkdir(parents=True, exist_ok=True) full.write_bytes(content) return tmp_path def _stub_require_repo(monkeypatch: pytest.MonkeyPatch, root: pathlib.Path) -> None: """Stub ``require_repo`` to return *root* without checking for ``.muse/``.""" monkeypatch.setattr(idx_rebuild_cmd, "require_repo", lambda: root) def _stub_domain(monkeypatch: pytest.MonkeyPatch, domain: str) -> None: """Force ``read_domain`` to return *domain* regardless of root.""" import muse.plugins.registry as registry monkeypatch.setattr(registry, "read_domain", lambda _r: domain) def _stub_symbol_history_build(monkeypatch: pytest.MonkeyPatch) -> None: """Stub the Code-domain index builders so we don't need a real Muse repo.""" monkeypatch.setattr(idx_rebuild_cmd, "_build_symbol_history", lambda _r, **_kw: {}) monkeypatch.setattr(idx_rebuild_cmd, "_build_hash_occurrence", lambda _r: {}) monkeypatch.setattr(idx_rebuild_cmd, "save_symbol_history", lambda *_a, **_kw: None) monkeypatch.setattr(idx_rebuild_cmd, "save_hash_occurrence", lambda *_a, **_kw: None) # Stub SymbolCache so we don't need a real one. class _FakeCache: def save(self) -> None: return None monkeypatch.setattr(idx_rebuild_cmd, "load_symbol_cache", lambda _r: _FakeCache()) def _run_rebuild(as_json: bool = True) -> str: """Invoke ``run_rebuild`` with a sensible default Namespace.""" args = argparse.Namespace( index_name=None, dry_run=True, verbose=False, as_json=as_json, ) buf = io.StringIO() with redirect_stdout(buf): idx_rebuild_cmd.run_rebuild(args) return buf.getvalue() # ============================================================================ # TestKnowtationIndexExtension # ============================================================================ class TestKnowtationIndexExtension: def test_link_graph_keys_present_for_knowtation_domain( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: _make_vault(tmp_path, { "a.md": b"[[B]] and [c](c.md)", "b.md": b"# B", "c.md": b"# C", "broken.md": b"[[DoesNotExist]]", }) _stub_require_repo(monkeypatch, tmp_path) _stub_domain(monkeypatch, "knowtation") _stub_symbol_history_build(monkeypatch) out = _run_rebuild(as_json=True) payload = json.loads(out) assert "knowtation_link_graph_notes" in payload assert payload["knowtation_link_graph_notes"] == 4 assert payload["knowtation_link_graph_edges_resolved"] >= 2 assert payload["knowtation_link_graph_edges_broken"] >= 1 assert "knowtation_link_graph" in payload["rebuilt"] def test_link_graph_keys_absent_for_code_domain( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: _make_vault(tmp_path, {}) _stub_require_repo(monkeypatch, tmp_path) _stub_domain(monkeypatch, "code") _stub_symbol_history_build(monkeypatch) out = _run_rebuild(as_json=True) payload = json.loads(out) assert "knowtation_link_graph_notes" not in payload assert "knowtation_link_graph" not in payload["rebuilt"] def test_entity_index_counted( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: _make_vault(tmp_path, { "a.md": b"---\nentity:\n - Alice\n - Bob\n---\nA", "b.md": b"---\nentity:\n - Bob\n---\nB", }) _stub_require_repo(monkeypatch, tmp_path) _stub_domain(monkeypatch, "knowtation") _stub_symbol_history_build(monkeypatch) out = _run_rebuild(as_json=True) payload = json.loads(out) assert payload["knowtation_link_graph_entities"] == 2 # Alice + Bob # ============================================================================ # TestStress # ============================================================================ class TestStress: def test_500_note_vault_rebuilds_under_5s( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: import time files: dict[str, bytes] = {} for i in range(500): files[f"note_{i:04d}.md"] = ( f"# Note {i}\n[[note_{(i + 1) % 500:04d}]] " f"[neighbour](./note_{(i + 2) % 500:04d}.md)" ).encode() _make_vault(tmp_path, files) _stub_require_repo(monkeypatch, tmp_path) _stub_domain(monkeypatch, "knowtation") _stub_symbol_history_build(monkeypatch) start = time.monotonic() out = _run_rebuild(as_json=True) elapsed = time.monotonic() - start payload = json.loads(out) assert payload["knowtation_link_graph_notes"] == 500 assert elapsed < 5.0, f"500-note rebuild took {elapsed:.2f}s (budget 5s)" # ============================================================================ # TestSecurity # ============================================================================ class TestSecurity: def test_corrupt_note_bytes_do_not_crash( self, tmp_path: pathlib.Path, monkeypatch: pytest.MonkeyPatch ) -> None: _make_vault(tmp_path, { "good.md": b"[[other]]", "corrupt.md": bytes(range(256)) * 5, "binary.md": b"\x00\x01\x02\x03[[other]]\xff\xfe", "other.md": b"# Other", }) _stub_require_repo(monkeypatch, tmp_path) _stub_domain(monkeypatch, "knowtation") _stub_symbol_history_build(monkeypatch) try: out = _run_rebuild(as_json=True) payload = json.loads(out) assert payload["knowtation_link_graph_notes"] == 4 except Exception as exc: pytest.fail(f"Corrupt note bytes crashed rebuild: {exc}")