Files
2026-06-30 20:37:40 +02:00

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