feat: implement knowledge management system (spec complete, all 53 tasks done)

This commit is contained in:
2026-07-01 14:14:16 +02:00
parent e4256d3222
commit 8ac664d33d
89 changed files with 19759 additions and 72 deletions
@@ -0,0 +1,319 @@
"""Unit-Tests für monorepo.knowledge.ingestion.config.
Testet SourceConfig, PipelineSettings, Env-Var-Auflösung,
Validierung und sources.yaml-Parsing.
"""
from __future__ import annotations
import os
from pathlib import Path
from textwrap import dedent
import pytest
from monorepo.knowledge.ingestion.config import (
ConfigValidationError,
EnrichmentSettings,
GitSettings,
OCRSettings,
PipelineSettings,
SourceConfig,
load_sources_config,
resolve_env_vars,
)
# ---------------------------------------------------------------------------
# SourceConfig Dataclass
# ---------------------------------------------------------------------------
class TestSourceConfig:
"""Tests für die SourceConfig-Dataclass."""
def test_required_fields(self) -> None:
cfg = SourceConfig(type="confluence", name="Team Wiki")
assert cfg.type == "confluence"
assert cfg.name == "Team Wiki"
def test_defaults(self) -> None:
cfg = SourceConfig(type="jira", name="Sprint Issues")
assert cfg.params == {}
assert cfg.target_folder == ""
assert cfg.sync_frequency == "manual"
assert cfg.enabled is True
def test_full_config(self) -> None:
cfg = SourceConfig(
type="confluence",
name="DB Confluence",
params={"base_url": "https://confluence.bahn.de", "spaces": ["ACV2"]},
target_folder="references",
sync_frequency="daily",
enabled=True,
)
assert cfg.params["base_url"] == "https://confluence.bahn.de"
assert cfg.target_folder == "references"
assert cfg.sync_frequency == "daily"
# ---------------------------------------------------------------------------
# PipelineSettings Dataclass
# ---------------------------------------------------------------------------
class TestPipelineSettings:
"""Tests für PipelineSettings und Unter-Dataclasses."""
def test_defaults(self) -> None:
settings = PipelineSettings()
assert settings.create_tasks is True
assert settings.enrichment.provider == "kiro"
assert settings.enrichment.confidence_threshold == 0.7
assert settings.ocr.provider == "kiro"
assert settings.ocr.languages == "deu+eng"
assert settings.git.auto_commit is True
def test_custom_settings(self) -> None:
settings = PipelineSettings(
create_tasks=False,
enrichment=EnrichmentSettings(provider="litellm", model="gpt-4"),
ocr=OCRSettings(provider="tesseract", languages="deu"),
git=GitSettings(auto_commit=False),
)
assert settings.create_tasks is False
assert settings.enrichment.provider == "litellm"
assert settings.enrichment.model == "gpt-4"
assert settings.ocr.provider == "tesseract"
assert settings.git.auto_commit is False
# ---------------------------------------------------------------------------
# resolve_env_vars
# ---------------------------------------------------------------------------
class TestResolveEnvVars:
"""Tests für die Umgebungsvariablen-Auflösung."""
def test_no_vars(self) -> None:
assert resolve_env_vars("hello world") == "hello world"
def test_single_var(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("MY_TOKEN", "secret123")
assert resolve_env_vars("${MY_TOKEN}") == "secret123"
def test_var_in_string(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOST", "example.com")
assert resolve_env_vars("https://${HOST}/api") == "https://example.com/api"
def test_multiple_vars(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("USER", "admin")
monkeypatch.setenv("PASS", "secret")
result = resolve_env_vars("${USER}:${PASS}")
assert result == "admin:secret"
def test_unset_var_raises(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("NONEXISTENT_VAR_XYZ", raising=False)
with pytest.raises(ConfigValidationError) as exc_info:
resolve_env_vars("${NONEXISTENT_VAR_XYZ}")
assert "NONEXISTENT_VAR_XYZ" in str(exc_info.value)
def test_empty_string(self) -> None:
assert resolve_env_vars("") == ""
def test_var_with_empty_value(self, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("EMPTY_VAR", "")
assert resolve_env_vars("prefix_${EMPTY_VAR}_suffix") == "prefix__suffix"
# ---------------------------------------------------------------------------
# load_sources_config Erfolgsfälle
# ---------------------------------------------------------------------------
class TestLoadSourcesConfig:
"""Tests für load_sources_config."""
def test_basic_config(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("CONF_TOKEN", "tok123")
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "Team Confluence"
type: confluence
enabled: true
sync_frequency: daily
params:
base_url: "https://confluence.bahn.de"
api_key: "${CONF_TOKEN}"
spaces: ["ACV2"]
target_folder: references
settings:
create_tasks: true
enrichment:
provider: "kiro"
confidence_threshold: 0.8
ocr:
provider: "kiro"
git:
auto_commit: false
"""), encoding="utf-8")
sources, settings = load_sources_config(config_file)
assert len(sources) == 1
src = sources[0]
assert src.type == "confluence"
assert src.name == "Team Confluence"
assert src.params["api_key"] == "tok123"
assert src.params["spaces"] == ["ACV2"]
assert src.target_folder == "references"
assert src.sync_frequency == "daily"
assert src.enabled is True
assert settings.create_tasks is True
assert settings.enrichment.provider == "kiro"
assert settings.enrichment.confidence_threshold == 0.8
assert settings.git.auto_commit is False
def test_multiple_sources(self, tmp_path: Path) -> None:
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "Source A"
type: file
params:
path: "/data"
- name: "Source B"
type: jira
enabled: false
params:
jql: "project = X"
"""), encoding="utf-8")
sources, settings = load_sources_config(config_file)
assert len(sources) == 2
assert sources[0].name == "Source A"
assert sources[1].name == "Source B"
assert sources[1].enabled is False
def test_empty_sources_list(self, tmp_path: Path) -> None:
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources: []
"""), encoding="utf-8")
sources, settings = load_sources_config(config_file)
assert sources == []
assert isinstance(settings, PipelineSettings)
def test_no_settings_block(self, tmp_path: Path) -> None:
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "Minimal"
type: file
"""), encoding="utf-8")
sources, settings = load_sources_config(config_file)
assert len(sources) == 1
# Should use defaults
assert settings.create_tasks is True
assert settings.enrichment.provider == "kiro"
def test_file_not_found(self, tmp_path: Path) -> None:
with pytest.raises(FileNotFoundError):
load_sources_config(tmp_path / "nonexistent.yaml")
# ---------------------------------------------------------------------------
# load_sources_config Validierungsfehler
# ---------------------------------------------------------------------------
class TestLoadSourcesConfigValidation:
"""Tests für Validierungsfehler in load_sources_config."""
def test_missing_type(self, tmp_path: Path) -> None:
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "No Type"
params: {}
"""), encoding="utf-8")
with pytest.raises(ConfigValidationError) as exc_info:
load_sources_config(config_file)
assert "type" in str(exc_info.value).lower()
def test_missing_name(self, tmp_path: Path) -> None:
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- type: confluence
params: {}
"""), encoding="utf-8")
with pytest.raises(ConfigValidationError) as exc_info:
load_sources_config(config_file)
assert "name" in str(exc_info.value).lower()
def test_unresolvable_env_var(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("MISSING_SECRET_XYZ", raising=False)
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "Bad Source"
type: confluence
params:
api_key: "${MISSING_SECRET_XYZ}"
"""), encoding="utf-8")
with pytest.raises(ConfigValidationError) as exc_info:
load_sources_config(config_file)
assert "MISSING_SECRET_XYZ" in str(exc_info.value)
def test_multiple_errors_collected(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("TOKEN_A", raising=False)
monkeypatch.delenv("TOKEN_B", raising=False)
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- name: "Source 1"
type: confluence
params:
key_a: "${TOKEN_A}"
key_b: "${TOKEN_B}"
"""), encoding="utf-8")
with pytest.raises(ConfigValidationError) as exc_info:
load_sources_config(config_file)
assert "TOKEN_A" in str(exc_info.value)
assert "TOKEN_B" in str(exc_info.value)
def test_missing_fields_and_env_vars(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.delenv("GHOST_VAR", raising=False)
config_file = tmp_path / "sources.yaml"
config_file.write_text(dedent("""\
version: "1.0"
sources:
- type: jira
params:
token: "${GHOST_VAR}"
"""), encoding="utf-8")
with pytest.raises(ConfigValidationError) as exc_info:
load_sources_config(config_file)
# Both missing name and unresolvable env var
assert "name" in str(exc_info.value).lower()
assert "GHOST_VAR" in str(exc_info.value)