"""Tests for LinkRegistry – Web-Link-Management als Wissensartefakte. Requirements: 8.1, 8.2, 8.4, 8.5, 8.6, 8.7 """ from __future__ import annotations from pathlib import Path from unittest.mock import patch import pytest from monorepo.knowledge.sources.link import LinkRegistry @pytest.fixture def context_path(tmp_path: Path) -> Path: """Create a minimal knowledge context path.""" knowledge = tmp_path / "bahn" / "knowledge" knowledge.mkdir(parents=True) return knowledge @pytest.fixture def registry(context_path: Path) -> LinkRegistry: """Create a LinkRegistry instance.""" return LinkRegistry(context_path) # --------------------------------------------------------------------------- # save_link tests # --------------------------------------------------------------------------- class TestSaveLink: """Tests for save_link.""" def test_save_link_creates_file( self, registry: LinkRegistry, context_path: Path ) -> None: """Req 8.1: Links stored as artifacts of type link in links/ folder.""" with patch.object(registry, "_fetch_metadata", return_value=("Example Page", "A description")): result = registry.save_link("https://example.com", title="Example") assert result.exists() assert result.parent == context_path / "links" assert result.suffix == ".md" def test_save_link_frontmatter_contains_required_fields( self, registry: LinkRegistry, context_path: Path ) -> None: """Req 8.2: Frontmatter must contain url, title, tags, created, type: link.""" with patch.object(registry, "_fetch_metadata", return_value=("", "")): result = registry.save_link( "https://example.com/page", title="My Link", tags=["python", "docs"], ) content = result.read_text(encoding="utf-8") assert "type: link" in content assert "title: My Link" in content assert "url: https://example.com/page" in content assert "python" in content assert "docs" in content assert "created:" in content def test_save_link_with_tags( self, registry: LinkRegistry, context_path: Path ) -> None: """Tags are included in the frontmatter.""" with patch.object(registry, "_fetch_metadata", return_value=("", "")): result = registry.save_link( "https://example.com", title="Test", tags=["tag1", "tag2"], ) content = result.read_text(encoding="utf-8") assert "tag1" in content assert "tag2" in content def test_save_link_empty_tags( self, registry: LinkRegistry, context_path: Path ) -> None: """Links can be saved without tags.""" with patch.object(registry, "_fetch_metadata", return_value=("", "")): result = registry.save_link("https://example.com", title="No Tags") assert result.exists() content = result.read_text(encoding="utf-8") assert "tags: []" in content def test_save_link_uses_fetched_title_if_none( self, registry: LinkRegistry, context_path: Path ) -> None: """Req 8.4: Fetch title from URL when not provided.""" with patch.object( registry, "_fetch_metadata", return_value=("Fetched Title", "Desc") ): result = registry.save_link("https://example.com") content = result.read_text(encoding="utf-8") assert "Fetched Title" in content def test_save_link_fetch_failed_sets_flag( self, registry: LinkRegistry, context_path: Path ) -> None: """Req 8.5: Set fetch_failed: true if URL is unreachable.""" with patch.object(registry, "_fetch_metadata", return_value=("", "")): result = registry.save_link("https://unreachable.example.com") content = result.read_text(encoding="utf-8") assert "fetch_failed: true" in content def test_save_link_creates_links_directory( self, registry: LinkRegistry, context_path: Path ) -> None: """links/ directory is created if it doesn't exist.""" assert not (context_path / "links").exists() with patch.object(registry, "_fetch_metadata", return_value=("", "")): result = registry.save_link("https://example.com", title="Test") assert (context_path / "links").exists() assert result.exists() # --------------------------------------------------------------------------- # Deduplication tests # --------------------------------------------------------------------------- class TestDeduplication: """Tests for URL deduplication with tag merging. Req 8.7.""" def test_duplicate_url_merges_tags( self, registry: LinkRegistry, context_path: Path ) -> None: """Req 8.7: Duplicate URLs merge tags instead of creating new artifact.""" with patch.object(registry, "_fetch_metadata", return_value=("Page", "")): first = registry.save_link( "https://example.com/dup", title="Page", tags=["tag1", "tag2"], ) second = registry.save_link( "https://example.com/dup", title="Page Updated", tags=["tag2", "tag3"], ) # Should be the same file assert first == second content = first.read_text(encoding="utf-8") assert "tag1" in content assert "tag2" in content assert "tag3" in content def test_duplicate_url_preserves_existing_tags( self, registry: LinkRegistry, context_path: Path ) -> None: """Existing tags are preserved when merging.""" with patch.object(registry, "_fetch_metadata", return_value=("Page", "")): registry.save_link( "https://example.com/keep", title="Keep", tags=["original"], ) result = registry.save_link( "https://example.com/keep", title="Keep", tags=["new"], ) content = result.read_text(encoding="utf-8") assert "original" in content assert "new" in content def test_duplicate_url_no_change_if_tags_same( self, registry: LinkRegistry, context_path: Path ) -> None: """No rewrite if tags are already the same.""" with patch.object(registry, "_fetch_metadata", return_value=("Page", "")): first = registry.save_link( "https://example.com/same", title="Same", tags=["a", "b"], ) content_before = first.read_text(encoding="utf-8") second = registry.save_link( "https://example.com/same", title="Same", tags=["a", "b"], ) content_after = second.read_text(encoding="utf-8") assert first == second assert content_before == content_after def test_different_urls_create_separate_files( self, registry: LinkRegistry, context_path: Path ) -> None: """Different URLs create separate link artifacts.""" with patch.object(registry, "_fetch_metadata", return_value=("P", "")): first = registry.save_link("https://a.com", title="A") second = registry.save_link("https://b.com", title="B") assert first != second assert first.exists() assert second.exists() # --------------------------------------------------------------------------- # search_links tests # --------------------------------------------------------------------------- class TestSearchLinks: """Tests for search_links. Req 8.6.""" def _create_link( self, registry: LinkRegistry, url: str, title: str, tags: list[str] ) -> Path: """Helper to create a link artifact.""" with patch.object(registry, "_fetch_metadata", return_value=("", "")): return registry.save_link(url, title=title, tags=tags) def test_search_by_url(self, registry: LinkRegistry) -> None: """Req 8.6: Search finds links matching URL.""" self._create_link(registry, "https://python.org", "Python", ["lang"]) self._create_link(registry, "https://rust-lang.org", "Rust", ["lang"]) results = registry.search_links("python") assert len(results) == 1 assert results[0].metadata.title == "Python" def test_search_by_title(self, registry: LinkRegistry) -> None: """Req 8.6: Search finds links matching title.""" self._create_link(registry, "https://a.com", "Django Documentation", ["web"]) self._create_link(registry, "https://b.com", "Flask Tutorial", ["web"]) results = registry.search_links("django") assert len(results) == 1 assert results[0].metadata.title == "Django Documentation" def test_search_by_tag(self, registry: LinkRegistry) -> None: """Req 8.6: Search finds links matching tag.""" self._create_link(registry, "https://a.com", "A", ["python", "web"]) self._create_link(registry, "https://b.com", "B", ["rust", "systems"]) results = registry.search_links("rust") assert len(results) == 1 assert results[0].metadata.title == "B" def test_search_case_insensitive(self, registry: LinkRegistry) -> None: """Search is case-insensitive.""" self._create_link(registry, "https://a.com", "Django Docs", ["Python"]) results = registry.search_links("DJANGO") assert len(results) == 1 results = registry.search_links("python") assert len(results) == 1 def test_search_no_results(self, registry: LinkRegistry) -> None: """Returns empty list when nothing matches.""" self._create_link(registry, "https://a.com", "Something", ["tag"]) results = registry.search_links("nonexistent") assert results == [] def test_search_empty_links_dir(self, registry: LinkRegistry) -> None: """Returns empty list when links directory doesn't exist.""" results = registry.search_links("anything") assert results == [] def test_search_multiple_matches(self, registry: LinkRegistry) -> None: """Returns all matching links.""" self._create_link(registry, "https://a.com", "Python Docs", ["python"]) self._create_link(registry, "https://b.com", "Python Tutorial", ["tutorial"]) results = registry.search_links("python") assert len(results) == 2 # --------------------------------------------------------------------------- # _fetch_metadata tests # --------------------------------------------------------------------------- class TestFetchMetadata: """Tests for _fetch_metadata.""" def test_extracts_title_from_html(self, registry: LinkRegistry) -> None: """Extracts