"""Unit-Tests für NoteGraphMigrator. Testet die Migration der NoteGraph-Verzeichnisstruktur in den KnowledgeStore: - Directory-Typ-Mapping (decisions→decision, inbox→note, meetings→meeting, etc.) - Frontmatter-Generierung für Dateien ohne Frontmatter - Frontmatter-Anreicherung für Dateien mit bestehendem Frontmatter - Scope-Konfiguration laden - Validierung nach Migration - Fehlerbehandlung Requirements: 3.5, 3.14 """ from __future__ import annotations from pathlib import Path import pytest from monorepo.knowledge.migration import ( NOTEGRAPH_DIR_TYPE_MAP, MigrationResult, NoteGraphMigrator, ValidationResult, load_scope_config, ) from monorepo.knowledge.store import KnowledgeStore from monorepo.models import ScopeConfig # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def scope_config_file(tmp_path: Path) -> Path: """Erstellt eine temporäre scopes.yaml Datei.""" config_path = tmp_path / "scopes.yaml" config_path.write_text( """\ version: "1.0" scopes: - name: privat context: privat description: "Persönlicher Wissensbereich" knowledge_path: shared/knowledge-store/privat/ - name: bahn context: bahn description: "DB InfraGO Wissensbereich" knowledge_path: shared/knowledge-store/bahn/ - name: shared context: shared description: "Kontextübergreifender Wissensbereich" knowledge_path: shared/knowledge-store/shared/ """, encoding="utf-8", ) return config_path @pytest.fixture def knowledge_store(tmp_path: Path) -> KnowledgeStore: """Erstellt einen KnowledgeStore in einem temporären Verzeichnis.""" store_path = tmp_path / "knowledge-store" store_path.mkdir() scope_config = ScopeConfig(scopes={ "privat": {"scope": "privat"}, "bahn": {"scope": "bahn"}, "shared": {"scope": "shared"}, }) return KnowledgeStore(base_path=store_path, scope_config=scope_config) @pytest.fixture def notegraph_dir(tmp_path: Path) -> Path: """Erstellt eine NoteGraph-Verzeichnisstruktur mit Testdaten.""" notegraph = tmp_path / "notegraph" notegraph.mkdir() # decisions/ decisions = notegraph / "decisions" decisions.mkdir() (decisions / "api-design.md").write_text( """\ --- type: decision title: "API-Design Entscheidung" tags: [api, architecture] source_context: bahn --- # API-Design Entscheidung Wir verwenden REST-APIs. """, encoding="utf-8", ) # inbox/ – Datei ohne Frontmatter inbox = notegraph / "inbox" inbox.mkdir() (inbox / "quick-note.md").write_text( """\ # Quick Note Eine schnelle Notiz ohne Frontmatter. """, encoding="utf-8", ) # meetings/ meetings = notegraph / "meetings" meetings.mkdir() (meetings / "sprint-review.md").write_text( """\ --- title: "Sprint Review 42" tags: [sprint, review] --- # Sprint Review 42 Besprechung des aktuellen Sprints. """, encoding="utf-8", ) # people/ people = notegraph / "people" people.mkdir() (people / "max-mustermann.md").write_text( """\ --- type: reference title: "Max Mustermann" tags: [team, developer] source_context: bahn --- # Max Mustermann Backend-Entwickler im Team. """, encoding="utf-8", ) # projects/ projects = notegraph / "projects" projects.mkdir() (projects / "wissensdatenbank.md").write_text( """\ --- type: project title: "DB Wissensdatenbank" tags: [knowledge, etl] source_context: bahn --- # DB Wissensdatenbank ETL-Pipeline für DB-InfraGO. """, encoding="utf-8", ) return notegraph # --------------------------------------------------------------------------- # Tests: load_scope_config # --------------------------------------------------------------------------- class TestLoadScopeConfig: """Tests für das Laden der Scope-Konfiguration.""" def test_load_valid_config(self, scope_config_file: Path) -> None: """Lädt eine gültige scopes.yaml erfolgreich.""" config = load_scope_config(scope_config_file) assert isinstance(config, ScopeConfig) assert "privat" in config.scopes assert "bahn" in config.scopes assert config.scopes["privat"]["scope"] == "privat" def test_load_missing_file_raises(self, tmp_path: Path) -> None: """FileNotFoundError wenn die Datei nicht existiert.""" with pytest.raises(FileNotFoundError): load_scope_config(tmp_path / "missing.yaml") def test_load_invalid_format_raises(self, tmp_path: Path) -> None: """ValueError bei ungültigem Format.""" bad_file = tmp_path / "bad.yaml" bad_file.write_text("just a string", encoding="utf-8") with pytest.raises(ValueError): load_scope_config(bad_file) # --------------------------------------------------------------------------- # Tests: NoteGraphMigrator.migrate # --------------------------------------------------------------------------- class TestNoteGraphMigration: """Tests für die NoteGraph-Migration.""" def test_migrate_full_notegraph( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Migriert eine vollständige NoteGraph-Struktur.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) result = migrator.migrate(source_path=notegraph_dir, context="bahn") assert isinstance(result, MigrationResult) assert result.migrated_count >= 4 # Mindestens 4 neue Artefakte assert result.success def test_migrate_adds_frontmatter_to_plain_files( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Dateien ohne Frontmatter erhalten generiertes Frontmatter.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") # Die inbox/quick-note.md sollte jetzt Frontmatter haben inbox_file = notegraph_dir / "inbox" / "quick-note.md" content = inbox_file.read_text(encoding="utf-8") assert content.startswith("---") assert "type: note" in content assert "source_context: bahn" in content def test_migrate_enriches_existing_frontmatter( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Existierendes Frontmatter wird um fehlende Felder ergänzt.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") # meetings/sprint-review.md hatte keinen type und keinen source_context meeting_file = notegraph_dir / "meetings" / "sprint-review.md" content = meeting_file.read_text(encoding="utf-8") assert "type: meeting" in content assert "source_context: bahn" in content def test_migrate_preserves_existing_metadata( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Vorhandene Metadaten (Tags, Title) bleiben erhalten.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") # decisions/api-design.md hatte bereits vollständige Metadaten decision_file = notegraph_dir / "decisions" / "api-design.md" content = decision_file.read_text(encoding="utf-8") assert "API-Design Entscheidung" in content assert "api" in content def test_migrate_directory_type_mapping( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Verzeichnisse werden korrekt auf Artefakt-Typen gemappt.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") # Index prüfen index_entries = knowledge_store.get_index(scope="bahn") types_found = {entry.type for entry in index_entries} # Mindestens decision, note, meeting, reference, project assert "decision" in types_found assert "note" in types_found assert "meeting" in types_found assert "reference" in types_found assert "project" in types_found def test_migrate_nonexistent_path_returns_error( self, knowledge_store: KnowledgeStore, scope_config_file: Path, tmp_path: Path, ) -> None: """Migration mit nicht-existierendem Pfad gibt Fehler zurück.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) result = migrator.migrate( source_path=tmp_path / "nonexistent", context="bahn" ) assert len(result.errors) > 0 assert result.migrated_count == 0 def test_migrate_empty_notegraph( self, knowledge_store: KnowledgeStore, scope_config_file: Path, tmp_path: Path, ) -> None: """Migration eines leeren NoteGraph-Verzeichnisses.""" empty_dir = tmp_path / "empty-notegraph" empty_dir.mkdir() migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) result = migrator.migrate(source_path=empty_dir, context="bahn") assert result.migrated_count == 0 assert result.skipped_count == 0 assert len(result.errors) == 0 def test_migrate_partial_notegraph( self, knowledge_store: KnowledgeStore, scope_config_file: Path, tmp_path: Path, ) -> None: """Migration mit nur einigen der NoteGraph-Verzeichnisse vorhanden.""" partial = tmp_path / "partial-notegraph" partial.mkdir() # Nur decisions/ anlegen decisions = partial / "decisions" decisions.mkdir() (decisions / "one-decision.md").write_text( """\ --- type: decision title: "Eine Entscheidung" tags: [test] source_context: privat --- # Eine Entscheidung Inhalt der Entscheidung. """, encoding="utf-8", ) migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) result = migrator.migrate(source_path=partial, context="privat") assert result.migrated_count == 1 assert len(result.errors) == 0 # --------------------------------------------------------------------------- # Tests: NoteGraphMigrator.validate # --------------------------------------------------------------------------- class TestNoteGraphValidation: """Tests für die Migrations-Validierung.""" def test_validate_after_successful_migration( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Validierung nach erfolgreicher Migration ist erfolgreich.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") validation = migrator.validate(context="bahn") assert isinstance(validation, ValidationResult) assert validation.valid assert validation.total >= 4 assert validation.found_in_index == validation.total assert len(validation.missing) == 0 def test_validate_empty_context_is_valid( self, knowledge_store: KnowledgeStore, scope_config_file: Path, ) -> None: """Validierung eines leeren Kontexts ist gültig (0 Artefakte).""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) validation = migrator.validate(context="privat") assert validation.valid assert validation.total == 0 def test_validate_returns_details( self, knowledge_store: KnowledgeStore, scope_config_file: Path, notegraph_dir: Path, ) -> None: """Validierung enthält detaillierte Ergebnisse pro Artefakt.""" migrator = NoteGraphMigrator( store=knowledge_store, scope_config_path=scope_config_file ) migrator.migrate(source_path=notegraph_dir, context="bahn") validation = migrator.validate(context="bahn") assert len(validation.details) > 0 for detail in validation.details: assert detail.file_name assert detail.found_in_index # --------------------------------------------------------------------------- # Tests: NOTEGRAPH_DIR_TYPE_MAP # --------------------------------------------------------------------------- class TestDirectoryTypeMapping: """Tests für das Verzeichnis-Typ-Mapping.""" def test_all_standard_directories_mapped(self) -> None: """Alle Standard-NoteGraph-Verzeichnisse sind gemappt.""" assert "decisions" in NOTEGRAPH_DIR_TYPE_MAP assert "inbox" in NOTEGRAPH_DIR_TYPE_MAP assert "meetings" in NOTEGRAPH_DIR_TYPE_MAP assert "people" in NOTEGRAPH_DIR_TYPE_MAP assert "projects" in NOTEGRAPH_DIR_TYPE_MAP def test_correct_type_assignments(self) -> None: """Verzeichnisse sind den korrekten Typen zugeordnet.""" assert NOTEGRAPH_DIR_TYPE_MAP["decisions"] == "decision" assert NOTEGRAPH_DIR_TYPE_MAP["inbox"] == "note" assert NOTEGRAPH_DIR_TYPE_MAP["meetings"] == "meeting" assert NOTEGRAPH_DIR_TYPE_MAP["people"] == "reference" assert NOTEGRAPH_DIR_TYPE_MAP["projects"] == "project"