2026-08-31 00:10:31 +02:00
|
|
|
"""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"
|
|
|
|
|
)
|
2026-08-31 00:15:44 +02:00
|
|
|
|
|
|
|
|
|
|
|
|
|
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
|