feat: implement knowledge management system (spec complete, all 53 tasks done)

This commit is contained in:
2026-07-01 14:14:16 +02:00
parent e4256d3222
commit 8ac664d33d
89 changed files with 19759 additions and 72 deletions
@@ -0,0 +1,366 @@
"""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