Files
Orchestrator/shared/tools/monorepo-cli/tests/test_link_registry.py
T

367 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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 <title> from HTML response."""
html = b"<html><head><title>Test Page Title</title></head><body></body></html>"
with patch("urllib.request.urlopen") as mock_urlopen:
mock_response = mock_urlopen.return_value.__enter__.return_value
mock_response.read.return_value = html
mock_response.headers.get_content_charset.return_value = "utf-8"
title, desc = registry._fetch_metadata("https://example.com")
assert title == "Test Page Title"
def test_extracts_meta_description(self, registry: LinkRegistry) -> None:
"""Extracts meta description from HTML."""
html = (
b'<html><head><title>T</title>'
b'<meta name="description" content="A page about testing">'
b"</head></html>"
)
with patch("urllib.request.urlopen") as mock_urlopen:
mock_response = mock_urlopen.return_value.__enter__.return_value
mock_response.read.return_value = html
mock_response.headers.get_content_charset.return_value = "utf-8"
title, desc = registry._fetch_metadata("https://example.com")
assert desc == "A page about testing"
def test_returns_empty_on_network_error(self, registry: LinkRegistry) -> None:
"""Returns empty strings when fetch fails."""
with patch("urllib.request.urlopen", side_effect=Exception("Network error")):
title, desc = registry._fetch_metadata("https://unreachable.test")
assert title == ""
assert desc == ""
# ---------------------------------------------------------------------------
# _find_existing tests
# ---------------------------------------------------------------------------
class TestFindExisting:
"""Tests for _find_existing."""
def test_finds_existing_url(
self, registry: LinkRegistry, context_path: Path
) -> None:
"""Finds an existing link by URL."""
with patch.object(registry, "_fetch_metadata", return_value=("T", "")):
saved = registry.save_link("https://example.com/exists", title="T")
found = registry._find_existing("https://example.com/exists")
assert found == saved
def test_returns_none_for_unknown_url(
self, registry: LinkRegistry, context_path: Path
) -> None:
"""Returns None when URL doesn't exist."""
found = registry._find_existing("https://never-saved.com")
assert found is None
def test_returns_none_for_empty_dir(
self, registry: LinkRegistry, context_path: Path
) -> None:
"""Returns None when links dir doesn't exist."""
found = registry._find_existing("https://example.com")
assert found is None