Initial monorepo structure
This commit is contained in:
@@ -0,0 +1,458 @@
|
||||
"""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"
|
||||
Reference in New Issue
Block a user