"""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