"""M5 — Plugin-MiniApps tests. Each business plugin contributes its MiniApps via the manifest contribution (same pattern as contacts_stats from M4): dms, mail, wiki, graph_rag and automation ship one dashboard app each, wired to the plugin's own permission and frontend component. """ from __future__ import annotations import pytest @pytest.fixture(autouse=True) def _fresh_registry(): from app.plugins.miniapp_registry import reset_miniapp_registry reset_miniapp_registry() yield reset_miniapp_registry() # (plugin_name, app_id, permission, component, order) EXPECTED_APPS: list[tuple[str, str, str, str, int]] = [ ("dms", "dms_folders", "dms:read", "@/components/dashboard/DmsFoldersWidget", 60), ("mail", "mail_unread", "mail:read", "@/components/dashboard/MailUnreadWidget", 70), ("wiki", "wiki_recent", "wiki:read", "@/components/dashboard/WikiRecentWidget", 80), ("graph_rag", "graph_overview", "graph:read", "@/components/dashboard/GraphOverviewWidget", 90), ("automation", "automation_status", "automation:read", "@/components/dashboard/AutomationStatusWidget", 100), ] class TestPluginMiniAppContributions: @pytest.mark.parametrize("plugin_name,app_id,permission,component,order", EXPECTED_APPS) def test_manifest_contains_miniapp( self, plugin_name: str, app_id: str, permission: str, component: str, order: int ): """The plugin manifest declares the MiniApp with the right fields.""" import importlib module = importlib.import_module(f"app.plugins.builtins.{plugin_name}.plugin") plugin_cls = getattr(module, f"{plugin_name.replace('_', '').title().replace('Graphrag', 'GraphRAG')}Plugin") contributions = plugin_cls().manifest.miniapps found = [m for m in contributions if m.app_id == app_id] assert len(found) == 1, f"{plugin_name}: miniapp {app_id} missing" app = found[0] assert app.permission == permission assert app.component == component assert app.order == order assert "dashboard" in app.hosts def test_lifecycle_registers_all_plugin_miniapps(self): """BasePlugin._register_manifest_miniapps carries component (M4 fix) for every contributing plugin — all five apps land in the registry.""" from app.plugins.miniapp_registry import get_miniapp_registry registry = get_miniapp_registry() for plugin_name, app_id, _permission, component, _order in EXPECTED_APPS: import importlib module = importlib.import_module(f"app.plugins.builtins.{plugin_name}.plugin") plugin_cls = getattr( module, f"{plugin_name.replace('_', '').title().replace('Graphrag', 'GraphRAG')}Plugin" ) plugin_cls()._register_manifest_miniapps() app = registry.get_app(app_id) assert app is not None, f"{app_id} not registered" assert app.component == component, f"{app_id}: component lost" def test_settings_schemas_present(self): """List-style widgets expose max_items in their settings_schema so the generic settings form (M3) can render it.""" import importlib for plugin_name, app_id, *_rest in EXPECTED_APPS: module = importlib.import_module(f"app.plugins.builtins.{plugin_name}.plugin") plugin_cls = getattr( module, f"{plugin_name.replace('_', '').title().replace('Graphrag', 'GraphRAG')}Plugin" ) contributions = plugin_cls().manifest.miniapps app = next(m for m in contributions if m.app_id == app_id) fields = app.settings_schema.get("fields", []) assert any(f["name"] == "max_items" for f in fields), ( f"{app_id}: max_items missing in settings_schema" ) def test_automation_legacy_reregistration_removed(): """M5 fix: automation's on_activate used to re-register manifest miniapps with only a subset of fields AFTER super().on_activate(), overwriting the correct M1 registration (component/permission lost — measured live on production 2026-08-30: automation_status comp=no). The legacy block must stay removed; super() owns the registration. """ import inspect from app.plugins.builtins.automation.plugin import AutomationPlugin src = inspect.getsource(AutomationPlugin.on_activate) assert "for miniapp in self.manifest.miniapps" not in src, ( "legacy miniapp re-registration re-introduced" ) assert "kommunikation.contracts import get_miniapp_registry" not in src