Initial monorepo structure

This commit is contained in:
2026-06-30 20:37:40 +02:00
commit 2f2b295531
121 changed files with 39171 additions and 0 deletions
@@ -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"