459 lines
14 KiB
Python
459 lines
14 KiB
Python
"""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"
|