"""Unit-Tests für ConfigMerger (Shared Tooling und MCP-Konfiguration). Testet: - MCP-Konfiguration-Merge mit Kontext-Vorrang (Req 8.4, 8.6) - Konflikt-Protokollierung bei Überschreibungen - Shared-Tool-Versionierung (Req 8.2) - Agent-Erweiterungen: Listing und harness-agnostisches Format (Req 8.3, 8.8) - Adapter-Mechanismus für verschiedene Harnesses (Req 8.9) - Provisionierung eines Kontexts Requirements: 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9 """ from __future__ import annotations import os from pathlib import Path import pytest import yaml from monorepo.shared_config import ( AgentExtension, ConfigMerger, HarnessAdapter, MCPServerConfig, MergeConflict, MergedMCPConfig, SharedToolStatus, ToolReference, ) # --------------------------------------------------------------------------- # Fixtures # --------------------------------------------------------------------------- @pytest.fixture def monorepo_root(tmp_path: Path) -> Path: """Erstellt eine Monorepo-Basisstruktur für Tests.""" # Shared-Bereich (tmp_path / "shared" / "mcp-servers").mkdir(parents=True) (tmp_path / "shared" / "tools").mkdir(parents=True) (tmp_path / "shared" / "powers").mkdir(parents=True) (tmp_path / "shared" / "config").mkdir(parents=True) # Kontexte for ctx in ("privat", "dhive", "bahn"): (tmp_path / ctx).mkdir() return tmp_path @pytest.fixture def merger(monorepo_root: Path) -> ConfigMerger: """Erstellt einen ConfigMerger mit der Test-Monorepo-Struktur.""" return ConfigMerger(monorepo_root) # --------------------------------------------------------------------------- # MCP-Konfiguration-Merge Tests # --------------------------------------------------------------------------- class TestMCPConfigMerge: """Tests für MCP-Server-Konfiguration-Merge (Req 8.4, 8.6).""" def test_merge_with_only_shared_config( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Shared-Konfiguration wird vollständig übernommen wenn kein Override.""" shared_config = { "servers": { "db-context": { "command": "npx", "args": ["-y", "@db/mcp-server"], "env": {"DB_TOKEN": "shared-token"}, } } } config_file = monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml" config_file.write_text(yaml.dump(shared_config), encoding="utf-8") result = merger.merge_mcp_config("dhive") assert "db-context" in result.servers assert result.servers["db-context"].command == "npx" assert result.servers["db-context"].args == ["-y", "@db/mcp-server"] assert result.conflicts == [] def test_context_override_takes_precedence( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Kontextspezifische Konfiguration überschreibt shared (Req 8.4).""" # Shared config shared_config = { "servers": { "db-context": { "command": "npx", "args": ["-y", "@db/mcp-server"], "env": {"DB_TOKEN": "shared-token"}, } } } config_file = monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml" config_file.write_text(yaml.dump(shared_config), encoding="utf-8") # Context override ctx_dir = monorepo_root / "dhive" / ".mcp-servers" ctx_dir.mkdir() ctx_config = { "servers": { "db-context": { "command": "node", "args": ["./custom-server.js"], "env": {"DB_TOKEN": "dhive-token"}, } } } (ctx_dir / "mcp-servers.yaml").write_text( yaml.dump(ctx_config), encoding="utf-8" ) result = merger.merge_mcp_config("dhive") # Context values must win assert result.servers["db-context"].command == "node" assert result.servers["db-context"].args == ["./custom-server.js"] assert result.servers["db-context"].env["DB_TOKEN"] == "dhive-token" def test_conflicts_are_logged( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Konflikte werden protokolliert (Req 8.6).""" shared_config = { "servers": { "my-server": { "command": "npx", "env": {"API_KEY": "shared-key"}, } } } (monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml").write_text( yaml.dump(shared_config), encoding="utf-8" ) ctx_dir = monorepo_root / "bahn" / ".mcp-servers" ctx_dir.mkdir() ctx_config = { "servers": { "my-server": { "command": "python", "env": {"API_KEY": "bahn-key"}, } } } (ctx_dir / "mcp-servers.yaml").write_text( yaml.dump(ctx_config), encoding="utf-8" ) result = merger.merge_mcp_config("bahn") assert len(result.conflicts) >= 2 # command + env.API_KEY conflict_keys = [c.key for c in result.conflicts] assert "command" in conflict_keys assert "env.API_KEY" in conflict_keys # Check conflict details cmd_conflict = next(c for c in result.conflicts if c.key == "command") assert cmd_conflict.shared_value == "npx" assert cmd_conflict.context_value == "python" assert cmd_conflict.resolution == "context-wins" assert cmd_conflict.context == "bahn" def test_context_only_servers_are_added( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Server die nur im Kontext existieren werden hinzugefügt.""" shared_config = {"servers": {"shared-only": {"command": "npx"}}} (monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml").write_text( yaml.dump(shared_config), encoding="utf-8" ) ctx_dir = monorepo_root / "privat" / ".mcp-servers" ctx_dir.mkdir() ctx_config = {"servers": {"ctx-only": {"command": "python"}}} (ctx_dir / "mcp-servers.yaml").write_text( yaml.dump(ctx_config), encoding="utf-8" ) result = merger.merge_mcp_config("privat") assert "shared-only" in result.servers assert "ctx-only" in result.servers assert result.servers["ctx-only"].command == "python" def test_no_config_returns_empty(self, merger: ConfigMerger) -> None: """Keine Konfigurationsdateien → leeres Ergebnis.""" result = merger.merge_mcp_config("dhive") assert result.servers == {} assert result.conflicts == [] def test_env_merge_combines_keys( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Env-Variablen aus shared und Kontext werden kombiniert.""" shared_config = { "servers": { "s1": {"command": "x", "env": {"A": "1", "B": "2"}} } } (monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml").write_text( yaml.dump(shared_config), encoding="utf-8" ) ctx_dir = monorepo_root / "dhive" / ".mcp-servers" ctx_dir.mkdir() ctx_config = { "servers": { "s1": {"command": "x", "env": {"B": "override", "C": "3"}} } } (ctx_dir / "mcp-servers.yaml").write_text( yaml.dump(ctx_config), encoding="utf-8" ) result = merger.merge_mcp_config("dhive") env = result.servers["s1"].env assert env["A"] == "1" # from shared assert env["B"] == "override" # overridden by context assert env["C"] == "3" # only in context # --------------------------------------------------------------------------- # Shared-Tool-Versionierung Tests # --------------------------------------------------------------------------- class TestSharedToolVersioning: """Tests für Shared-Tool-Versionierung (Req 8.2).""" def test_ensure_tool_links_creates_symlinks( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Tool-Symlinks werden korrekt erstellt.""" # Create a fake shared tool tool_dir = monorepo_root / "shared" / "tools" / "my-tool" tool_dir.mkdir() (tool_dir / "VERSION").write_text("1.2.3", encoding="utf-8") refs = merger.ensure_tool_links("dhive") assert len(refs) == 1 assert refs[0].name == "my-tool" assert refs[0].version == "1.2.3" # Verify the link/reference exists link_path = monorepo_root / "dhive" / ".tools" / "my-tool" assert link_path.exists() or ( monorepo_root / "dhive" / ".tools" / "my-tool.ref.yaml" ).exists() def test_tool_version_from_pyproject( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Version wird aus pyproject.toml gelesen.""" tool_dir = monorepo_root / "shared" / "tools" / "cli-tool" tool_dir.mkdir() pyproject_content = '[project]\nname = "cli-tool"\nversion = "0.5.0"\n' (tool_dir / "pyproject.toml").write_text(pyproject_content, encoding="utf-8") refs = merger.ensure_tool_links("bahn") assert refs[0].version == "0.5.0" def test_check_tool_status_reports_missing( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Status-Check erkennt fehlende Tool-Links.""" tool_dir = monorepo_root / "shared" / "tools" / "some-tool" tool_dir.mkdir() (tool_dir / "VERSION").write_text("2.0.0", encoding="utf-8") statuses = merger.check_tool_status("privat") assert len(statuses) == 1 assert statuses[0].tool_name == "some-tool" assert statuses[0].is_current is False assert statuses[0].local_version == "missing" def test_ensure_tool_links_is_idempotent( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Wiederholter Aufruf verändert nichts (Links schon aktuell).""" tool_dir = monorepo_root / "shared" / "tools" / "my-tool" tool_dir.mkdir() (tool_dir / "VERSION").write_text("1.0.0", encoding="utf-8") refs1 = merger.ensure_tool_links("dhive") refs2 = merger.ensure_tool_links("dhive") assert len(refs1) == len(refs2) assert refs1[0].name == refs2[0].name def test_no_tools_returns_empty(self, merger: ConfigMerger) -> None: """Keine Shared-Tools → leere Liste.""" refs = merger.ensure_tool_links("privat") assert refs == [] def test_hidden_dirs_are_skipped( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Versteckte Verzeichnisse (mit Punkt) werden übersprungen.""" (monorepo_root / "shared" / "tools" / ".hidden").mkdir() (monorepo_root / "shared" / "tools" / "visible").mkdir() refs = merger.ensure_tool_links("bahn") names = [r.name for r in refs] assert ".hidden" not in names assert "visible" in names # --------------------------------------------------------------------------- # Agent-Erweiterungen Tests # --------------------------------------------------------------------------- class TestAgentExtensions: """Tests für Agent-Erweiterungen (Req 8.3, 8.8, 8.9).""" def test_list_powers_from_shared( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Powers aus shared/powers/ werden korrekt gelistet.""" power_dir = monorepo_root / "shared" / "powers" / "db-dxp-platform" power_dir.mkdir() (power_dir / "POWER.md").write_text( "# DB DXP Platform\nHelps with platform stuff.", encoding="utf-8" ) extensions = merger.list_extensions() assert len(extensions) == 1 assert extensions[0].name == "db-dxp-platform" assert extensions[0].type == "power" assert extensions[0].description == "DB DXP Platform" def test_list_extensions_with_metadata( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Extension mit extension.yaml Metadaten werden korrekt geladen.""" power_dir = monorepo_root / "shared" / "powers" / "my-power" power_dir.mkdir() meta = { "name": "My Custom Power", "type": "power", "description": "A custom power for testing", "tags": ["testing", "custom"], } (power_dir / "extension.yaml").write_text( yaml.dump(meta), encoding="utf-8" ) extensions = merger.list_extensions() assert len(extensions) == 1 assert extensions[0].name == "My Custom Power" assert extensions[0].description == "A custom power for testing" assert extensions[0].tags == ["testing", "custom"] def test_filter_by_type( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Filter nach Extension-Typ funktioniert.""" (monorepo_root / "shared" / "powers" / "power-1").mkdir() ext_dir = monorepo_root / "shared" / "config" / "extensions" ext_dir.mkdir() (ext_dir / "steering.md").write_text("# Steering", encoding="utf-8") powers = merger.list_extensions(ext_type="power") steering = merger.list_extensions(ext_type="steering") assert all(e.type == "power" for e in powers) assert all(e.type == "steering" for e in steering) def test_get_extensions_for_context_includes_shared( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Kontext erhält shared-Extensions als Read-Only-Referenz (Req 8.3).""" power_dir = monorepo_root / "shared" / "powers" / "shared-power" power_dir.mkdir() (power_dir / "POWER.md").write_text("# Shared Power", encoding="utf-8") extensions = merger.get_extensions_for_context("dhive") assert any(e.name == "shared-power" for e in extensions) def test_get_extensions_includes_context_specific( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Kontextspezifische Erweiterungen werden zusätzlich geladen.""" # Shared (monorepo_root / "shared" / "powers" / "shared-power").mkdir() # Context-specific ctx_ext = monorepo_root / "dhive" / ".extensions" ctx_ext.mkdir() (ctx_ext / "local-skill").mkdir() (ctx_ext / "local-skill" / "README.md").write_text( "# Local Skill", encoding="utf-8" ) extensions = merger.get_extensions_for_context("dhive") names = [e.name for e in extensions] assert "shared-power" in names assert "local-skill" in names # --------------------------------------------------------------------------- # Harness-Adapter Tests # --------------------------------------------------------------------------- class TestHarnessAdapter: """Tests für den Adapter-Mechanismus (Req 8.7, 8.9).""" def test_load_adapters_from_config(self, monorepo_root: Path) -> None: """Adapter werden aus harness-adapters.yaml geladen.""" adapters_config = { "adapters": { "kiro": { "config_format": "kiro-powers", "output_path": ".kiro/powers", }, "codex": { "config_format": "codex-agent", "output_path": ".codex/agents", }, } } (monorepo_root / "shared" / "config" / "harness-adapters.yaml").write_text( yaml.dump(adapters_config), encoding="utf-8" ) merger = ConfigMerger(monorepo_root) adapters = merger.list_adapters() assert len(adapters) == 2 names = [a.harness_name for a in adapters] assert "kiro" in names assert "codex" in names def test_get_adapter_returns_none_for_unknown( self, merger: ConfigMerger ) -> None: """Unbekannter Harness → None.""" assert merger.get_adapter("unknown-harness") is None def test_transform_extension_for_codex( self, monorepo_root: Path ) -> None: """Transformation für Codex erzeugt AGENTS.md Referenz.""" adapters_config = { "adapters": { "codex": {"config_format": "codex-agent"}, } } (monorepo_root / "shared" / "config" / "harness-adapters.yaml").write_text( yaml.dump(adapters_config), encoding="utf-8" ) # Create a power to transform power_dir = monorepo_root / "shared" / "powers" / "test-power" power_dir.mkdir() (power_dir / "POWER.md").write_text("# Test Power\nDoes stuff.", encoding="utf-8") merger = ConfigMerger(monorepo_root) ext = AgentExtension( name="test-power", type="power", path=power_dir, description="Test power for testing", ) result = merger.transform_extension_for_harness(ext, "codex") assert result is not None assert result.exists() content = result.read_text(encoding="utf-8") assert "test-power" in content def test_transform_returns_none_without_adapter( self, merger: ConfigMerger, monorepo_root: Path ) -> None: """Transformation ohne konfigurierten Adapter → None.""" ext = AgentExtension( name="test", type="power", path=monorepo_root / "shared" / "powers", ) result = merger.transform_extension_for_harness(ext, "nonexistent") assert result is None # --------------------------------------------------------------------------- # Harness-Konfiguration pro Kontext Tests # --------------------------------------------------------------------------- class TestHarnessConfig: """Tests für Harness-Konfiguration pro Kontext (Req 8.7).""" def test_get_harness_config_returns_context_config( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """Harness-Konfiguration pro Kontext wird korrekt zurückgegeben.""" config = { "contexts": { "dhive": { "harness": "codex", "config": {"model": "o3-pro"}, }, "bahn": { "harness": "kiro", "config": {"powers_path": "shared/powers"}, }, } } (monorepo_root / "shared" / "config" / "harness-config.yaml").write_text( yaml.dump(config), encoding="utf-8" ) dhive_config = merger.get_harness_config("dhive") assert dhive_config["harness"] == "codex" bahn_config = merger.get_harness_config("bahn") assert bahn_config["harness"] == "kiro" def test_get_harness_config_returns_empty_for_unknown( self, merger: ConfigMerger ) -> None: """Unbekannter Kontext → leeres Dict.""" assert merger.get_harness_config("unknown") == {} # --------------------------------------------------------------------------- # Provisionierung Tests # --------------------------------------------------------------------------- class TestProvisionContext: """Tests für die Kontext-Provisionierung.""" def test_provision_context_integrates_all( self, monorepo_root: Path, merger: ConfigMerger ) -> None: """provision_context liefert Tools, MCP-Config und Extensions.""" # Setup shared tool tool_dir = monorepo_root / "shared" / "tools" / "test-tool" tool_dir.mkdir() (tool_dir / "VERSION").write_text("1.0.0", encoding="utf-8") # Setup shared MCP config mcp_config = {"servers": {"s1": {"command": "npx"}}} (monorepo_root / "shared" / "mcp-servers" / "mcp-servers.yaml").write_text( yaml.dump(mcp_config), encoding="utf-8" ) # Setup shared power (monorepo_root / "shared" / "powers" / "test-power").mkdir() result = merger.provision_context("dhive") assert result["context"] == "dhive" assert len(result["tools"]) == 1 assert result["tools"][0]["name"] == "test-tool" assert "s1" in result["mcp_config"]["servers"] assert any(e["name"] == "test-power" for e in result["extensions"])