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

457 lines
16 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.
"""Unit-Tests für die WissensdatenbankSource.
Testet den Adapter für bahn/wissensdatenbank/output/ Read-Only-Referenzen
im bahn/knowledge/ YAML-Index ohne Modifikation der Originaldateien.
Requirements: 13.1, 13.2, 13.3, 13.4, 13.5
"""
from __future__ import annotations
from datetime import date
from pathlib import Path
import pytest
from monorepo.knowledge.sources.base import ExtractionResult, SourceConfig
from monorepo.knowledge.sources.wissensdatenbank import (
WissensdatenbankSource,
_compute_file_hash,
)
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def wdb_dir(tmp_path: Path) -> Path:
"""Erstellt ein temporäres Wissensdatenbank-Output-Verzeichnis mit Testdaten."""
output = tmp_path / "wissensdatenbank" / "output"
output.mkdir(parents=True)
# Einige Markdown-Dateien erstellen
(output / "inb-2026-kapitel-1.md").write_text(
"# INB 2026 Kapitel 1\n\nInhalt des ersten Kapitels.",
encoding="utf-8",
)
(output / "inb-2026-kapitel-2.md").write_text(
"# INB 2026 Kapitel 2\n\nInhalt des zweiten Kapitels.",
encoding="utf-8",
)
# Unterverzeichnis mit weiterer Datei
sub = output / "themen"
sub.mkdir()
(sub / "signaltechnik.md").write_text(
"# Signaltechnik\n\nBeschreibung der Signaltechnik.",
encoding="utf-8",
)
# Eine .txt-Datei
(output / "notizen.txt").write_text(
"Allgemeine Notizen zur Wissensdatenbank.",
encoding="utf-8",
)
return output
@pytest.fixture
def source_config(wdb_dir: Path) -> SourceConfig:
"""Erstellt eine SourceConfig für die Wissensdatenbank."""
return SourceConfig(
type="wissensdatenbank",
name="Wissensdatenbank",
params={"output_path": str(wdb_dir)},
target_folder="references",
sync_frequency="manual",
enabled=True,
)
@pytest.fixture
def source() -> WissensdatenbankSource:
"""Erstellt eine WissensdatenbankSource-Instanz."""
return WissensdatenbankSource()
# ---------------------------------------------------------------------------
# Tests: Grundlegende Extraktion (Req 13.1)
# ---------------------------------------------------------------------------
class TestBasicExtraction:
"""Tests für die grundlegende Extraktion von Wissensdatenbank-Artefakten."""
def test_extracts_all_markdown_files(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Alle .md- und .txt-Dateien im Output-Verzeichnis werden extrahiert."""
result = source.extract(source_config, "bahn")
assert isinstance(result, ExtractionResult)
assert len(result.artifacts) == 4 # 2 .md + 1 subdir .md + 1 .txt
assert result.skipped == 0
assert len(result.errors) == 0
def test_extracts_recursively(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Dateien in Unterverzeichnissen werden ebenfalls extrahiert."""
result = source.extract(source_config, "bahn")
file_paths = [str(a.file_path) for a in result.artifacts]
# signaltechnik.md aus themen/ sollte enthalten sein
assert any("signaltechnik" in p for p in file_paths)
def test_ignores_non_supported_extensions(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Dateien mit nicht-unterstützten Endungen werden ignoriert."""
(wdb_dir / "bild.png").write_bytes(b"\x89PNG...")
(wdb_dir / "dokument.pdf").write_bytes(b"%PDF-1.4")
result = source.extract(source_config, "bahn")
# Nur .md und .txt werden verarbeitet
assert len(result.artifacts) == 4
# ---------------------------------------------------------------------------
# Tests: Read-Only-Referenz-Metadaten (Req 13.3)
# ---------------------------------------------------------------------------
class TestReadOnlyMetadata:
"""Tests für korrekte source-Metadaten der Wissensdatenbank-Artefakte."""
def test_source_type_is_wissensdatenbank(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artefakte haben source.type == 'wissensdatenbank'."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert artifact.metadata.source["type"] == "wissensdatenbank"
def test_source_path_references_original(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Artefakte enthalten den Originalpfad in source.path."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert "path" in artifact.metadata.source
# Der Pfad sollte auf eine existierende Datei zeigen
original_path = Path(artifact.metadata.source["path"])
assert original_path.exists()
def test_source_context_is_bahn(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artefakte haben source_context == 'bahn' (übergebener Kontext)."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert artifact.metadata.source_context == "bahn"
def test_artifact_type_is_reference(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artefakte sind vom Typ 'reference'."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert artifact.metadata.type == "reference"
def test_category_is_references(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artefakte haben category == 'references'."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert artifact.metadata.category == "references"
def test_content_hash_is_set(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artefakte haben einen nicht-leeren content_hash."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
assert artifact.metadata.content_hash.startswith("sha256:")
def test_title_derived_from_filename(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Titel wird aus dem Dateinamen abgeleitet."""
result = source.extract(source_config, "bahn")
titles = [a.metadata.title for a in result.artifacts]
assert "Inb 2026 Kapitel 1" in titles
assert "Signaltechnik" in titles
# ---------------------------------------------------------------------------
# Tests: Originaldateien unverändert (Req 13.2, 13.4)
# ---------------------------------------------------------------------------
class TestReadOnlyBehavior:
"""Tests, dass die Originaldateien nicht modifiziert werden."""
def test_original_files_unchanged(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Originaldateien werden nicht verändert gleicher Hash vor/nach Extraktion."""
# Hashes vor Extraktion berechnen
files = list(wdb_dir.rglob("*"))
hashes_before = {
str(f): _compute_file_hash(f) for f in files if f.is_file()
}
# Extraktion ausführen
source.extract(source_config, "bahn")
# Hashes nach Extraktion prüfen
hashes_after = {
str(f): _compute_file_hash(f) for f in files if f.is_file()
}
assert hashes_before == hashes_after
def test_no_new_files_created_in_source_dir(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Keine neuen Dateien werden im Quellverzeichnis erstellt."""
files_before = set(str(f) for f in wdb_dir.rglob("*") if f.is_file())
source.extract(source_config, "bahn")
files_after = set(str(f) for f in wdb_dir.rglob("*") if f.is_file())
assert files_before == files_after
def test_artifact_file_path_points_to_original(
self, source: WissensdatenbankSource, source_config: SourceConfig
) -> None:
"""Artifact.file_path zeigt auf die Originaldatei (Read-Only-Referenz)."""
result = source.extract(source_config, "bahn")
for artifact in result.artifacts:
# file_path zeigt auf die originale Datei
assert artifact.file_path.exists()
assert artifact.file_path.suffix.lower() in {".md", ".txt"}
# ---------------------------------------------------------------------------
# Tests: Inkrementelle Verarbeitung (Req 13.2)
# ---------------------------------------------------------------------------
class TestIncrementalProcessing:
"""Tests für inkrementelle Verarbeitung via Content-Hash."""
def test_supports_incremental(
self, source: WissensdatenbankSource
) -> None:
"""WissensdatenbankSource unterstützt inkrementelle Verarbeitung."""
assert source.supports_incremental() is True
def test_skips_unchanged_files(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Dateien mit bekanntem Hash werden übersprungen."""
# Erste Extraktion alles neu
result1 = source.extract(source_config, "bahn")
assert result1.skipped == 0
assert len(result1.artifacts) == 4
# Hashes der extrahierten Dateien merken
known = {}
for f in wdb_dir.rglob("*"):
if f.is_file() and f.suffix.lower() in {".md", ".txt"}:
known[str(f)] = _compute_file_hash(f)
source.set_known_hashes(known)
# Zweite Extraktion alles unverändert → alles geskippt
result2 = source.extract(source_config, "bahn")
assert result2.skipped == 4
assert len(result2.artifacts) == 0
def test_processes_changed_files(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Geänderte Dateien werden erneut verarbeitet."""
# Erste Extraktion
source.extract(source_config, "bahn")
# Hashes merken
known = {}
for f in wdb_dir.rglob("*"):
if f.is_file() and f.suffix.lower() in {".md", ".txt"}:
known[str(f)] = _compute_file_hash(f)
source.set_known_hashes(known)
# Eine Datei ändern
changed_file = wdb_dir / "inb-2026-kapitel-1.md"
changed_file.write_text(
"# INB 2026 Kapitel 1 (aktualisiert)\n\nNeuer Inhalt.",
encoding="utf-8",
)
# Zweite Extraktion nur die geänderte Datei wird verarbeitet
result = source.extract(source_config, "bahn")
assert len(result.artifacts) == 1
assert result.skipped == 3
assert "inb-2026-kapitel-1" in str(result.artifacts[0].file_path)
# ---------------------------------------------------------------------------
# Tests: Fehlerbehandlung
# ---------------------------------------------------------------------------
class TestErrorHandling:
"""Tests für die Fehlerbehandlung."""
def test_missing_output_path_param(
self, source: WissensdatenbankSource
) -> None:
"""Fehlende output_path-Konfiguration erzeugt einen Fehler."""
config = SourceConfig(
type="wissensdatenbank",
name="Test",
params={},
)
result = source.extract(config, "bahn")
assert len(result.errors) == 1
assert "output_path" in result.errors[0].message
def test_nonexistent_directory(
self, source: WissensdatenbankSource, tmp_path: Path
) -> None:
"""Nicht-existierendes Verzeichnis erzeugt einen Fehler."""
config = SourceConfig(
type="wissensdatenbank",
name="Test",
params={"output_path": str(tmp_path / "nonexistent")},
)
result = source.extract(config, "bahn")
assert len(result.errors) == 1
assert result.errors[0].error_type == "connection"
assert result.errors[0].retry is True
def test_path_is_file_not_directory(
self, source: WissensdatenbankSource, tmp_path: Path
) -> None:
"""Pfad zu einer Datei (statt Verzeichnis) erzeugt einen Fehler."""
file = tmp_path / "file.md"
file.write_text("content", encoding="utf-8")
config = SourceConfig(
type="wissensdatenbank",
name="Test",
params={"output_path": str(file)},
)
result = source.extract(config, "bahn")
assert len(result.errors) == 1
assert result.errors[0].error_type == "parse"
assert result.errors[0].retry is False
def test_empty_directory(
self, source: WissensdatenbankSource, tmp_path: Path
) -> None:
"""Leeres Verzeichnis erzeugt leeres Ergebnis ohne Fehler."""
empty_dir = tmp_path / "empty"
empty_dir.mkdir()
config = SourceConfig(
type="wissensdatenbank",
name="Test",
params={"output_path": str(empty_dir)},
)
result = source.extract(config, "bahn")
assert len(result.artifacts) == 0
assert len(result.errors) == 0
assert result.skipped == 0
# ---------------------------------------------------------------------------
# Tests: Kein OKF-Reformat (Req 13.4)
# ---------------------------------------------------------------------------
class TestNoOkfReformat:
"""Tests, dass kein Google-OKF-Reformat erzwungen wird."""
def test_content_preserved_exactly(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Dateiinhalte werden unverändert als Content übernommen."""
result = source.extract(source_config, "bahn")
# Finde das Artefakt für kapitel-1
kapitel1 = next(
a for a in result.artifacts if "kapitel-1" in str(a.file_path)
)
expected_content = "# INB 2026 Kapitel 1\n\nInhalt des ersten Kapitels."
assert kapitel1.content == expected_content
def test_no_frontmatter_injection_in_original(
self,
source: WissensdatenbankSource,
source_config: SourceConfig,
wdb_dir: Path,
) -> None:
"""Originaldateien erhalten KEIN YAML-Frontmatter."""
source.extract(source_config, "bahn")
# Originaldatei prüfen darf kein '---' am Anfang haben
original = wdb_dir / "inb-2026-kapitel-1.md"
content = original.read_text(encoding="utf-8")
assert not content.startswith("---")
# ---------------------------------------------------------------------------
# Tests: Import von __init__.py
# ---------------------------------------------------------------------------
class TestModuleExport:
"""Tests, dass WissensdatenbankSource aus dem Modul exportiert wird."""
def test_importable_from_sources_package(self) -> None:
"""WissensdatenbankSource ist über das sources-Package importierbar."""
from monorepo.knowledge.sources import WissensdatenbankSource as WDB
assert WDB is WissensdatenbankSource