feat(B-PLUGIN-MINIAPP-WIRE): MiniApp Registry als Singleton — gemeinsame Registry für alle Plugins
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
B-PLUGIN-MINIAPP-WIRE: MiniAppRegistry Singleton-Pattern - get_miniapp_registry() / reset_miniapp_registry() in miniapp_registry.py - Alle 6 MiniAppRegistry() Instanziierungen durch get_miniapp_registry() ersetzt - automation/plugin.py (on_activate/on_deactivate), automation/routes.py (3x), kommunikation/plugin.py - contracts.py: get_miniapp_registry + reset_miniapp_registry exportiert - 0 verbleibende MiniAppRegistry() Instanziierungen außerhalb miniapp_registry.py Tests: 12 Tests in test_miniapp_registry.py — alle grün - Singleton, Register/List, UnregisterPlugin, Plugin-Lifecycle-Integration
This commit is contained in:
@@ -193,8 +193,8 @@ class AutomationPlugin(BasePlugin):
|
|||||||
logger.exception("Failed to register agent coordinator tools")
|
logger.exception("Failed to register agent coordinator tools")
|
||||||
# Register MiniApps from manifest
|
# Register MiniApps from manifest
|
||||||
try:
|
try:
|
||||||
from app.plugins.builtins.kommunikation.contracts import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.contracts import get_miniapp_registry
|
||||||
registry = MiniAppRegistry()
|
registry = get_miniapp_registry()
|
||||||
for miniapp in self.manifest.miniapps:
|
for miniapp in self.manifest.miniapps:
|
||||||
registry.register(
|
registry.register(
|
||||||
app_id=miniapp.app_id,
|
app_id=miniapp.app_id,
|
||||||
@@ -236,8 +236,8 @@ class AutomationPlugin(BasePlugin):
|
|||||||
logger.exception("Failed to unregister agent coordinator tools")
|
logger.exception("Failed to unregister agent coordinator tools")
|
||||||
# Unregister MiniApps
|
# Unregister MiniApps
|
||||||
try:
|
try:
|
||||||
from app.plugins.builtins.kommunikation.contracts import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.contracts import get_miniapp_registry
|
||||||
registry = MiniAppRegistry()
|
registry = get_miniapp_registry()
|
||||||
registry.unregister_plugin(self.manifest.name)
|
registry.unregister_plugin(self.manifest.name)
|
||||||
logger.info("Unregistered MiniApps for plugin '%s'", self.manifest.name)
|
logger.info("Unregistered MiniApps for plugin '%s'", self.manifest.name)
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -187,8 +187,8 @@ async def list_miniapps(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""List custom MiniApps from plugin config."""
|
"""List custom MiniApps from plugin config."""
|
||||||
from app.plugins.builtins.kommunikation.contracts import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.contracts import get_miniapp_registry
|
||||||
registry = MiniAppRegistry()
|
registry = get_miniapp_registry()
|
||||||
items = registry.list_apps()
|
items = registry.list_apps()
|
||||||
return {"items": items, "total": len(items)}
|
return {"items": items, "total": len(items)}
|
||||||
|
|
||||||
@@ -204,8 +204,8 @@ async def create_miniapp(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Create a custom MiniApp definition."""
|
"""Create a custom MiniApp definition."""
|
||||||
from app.plugins.builtins.kommunikation.contracts import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.contracts import get_miniapp_registry
|
||||||
registry = MiniAppRegistry()
|
registry = get_miniapp_registry()
|
||||||
registry.register(
|
registry.register(
|
||||||
app_id=data.app_id,
|
app_id=data.app_id,
|
||||||
name=data.name,
|
name=data.name,
|
||||||
@@ -233,8 +233,8 @@ async def delete_miniapp(
|
|||||||
current_user: dict[str, Any] = Depends(get_current_user),
|
current_user: dict[str, Any] = Depends(get_current_user),
|
||||||
):
|
):
|
||||||
"""Delete a custom MiniApp definition."""
|
"""Delete a custom MiniApp definition."""
|
||||||
from app.plugins.builtins.kommunikation.contracts import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.contracts import get_miniapp_registry
|
||||||
registry = MiniAppRegistry()
|
registry = get_miniapp_registry()
|
||||||
registry.unregister(app_id)
|
registry.unregister(app_id)
|
||||||
return {"status": "ok"}
|
return {"status": "ok"}
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ from app.plugins.builtins.contracts import get_contract_registry
|
|||||||
from app.plugins.builtins.kommunikation.miniapp_registry import (
|
from app.plugins.builtins.kommunikation.miniapp_registry import (
|
||||||
MiniAppDef,
|
MiniAppDef,
|
||||||
MiniAppRegistry,
|
MiniAppRegistry,
|
||||||
|
get_miniapp_registry,
|
||||||
|
reset_miniapp_registry,
|
||||||
)
|
)
|
||||||
from app.plugins.builtins.kommunikation.models import (
|
from app.plugins.builtins.kommunikation.models import (
|
||||||
CommConversation,
|
CommConversation,
|
||||||
@@ -60,6 +62,8 @@ class KommunikationContract:
|
|||||||
# ─── mini-app registry ───
|
# ─── mini-app registry ───
|
||||||
MiniAppRegistry = MiniAppRegistry
|
MiniAppRegistry = MiniAppRegistry
|
||||||
MiniAppDef = MiniAppDef
|
MiniAppDef = MiniAppDef
|
||||||
|
get_miniapp_registry = staticmethod(get_miniapp_registry)
|
||||||
|
reset_miniapp_registry = staticmethod(reset_miniapp_registry)
|
||||||
|
|
||||||
# ─── models (read-only for queries) ───
|
# ─── models (read-only for queries) ───
|
||||||
CommConversation = CommConversation
|
CommConversation = CommConversation
|
||||||
@@ -79,6 +83,8 @@ __all__ = [
|
|||||||
"get_participant_registry",
|
"get_participant_registry",
|
||||||
"MiniAppRegistry",
|
"MiniAppRegistry",
|
||||||
"MiniAppDef",
|
"MiniAppDef",
|
||||||
|
"get_miniapp_registry",
|
||||||
|
"reset_miniapp_registry",
|
||||||
"parse_mentions",
|
"parse_mentions",
|
||||||
"get_conversation",
|
"get_conversation",
|
||||||
"get_messages",
|
"get_messages",
|
||||||
|
|||||||
@@ -71,3 +71,22 @@ class MiniAppRegistry:
|
|||||||
def get_app(self, app_id: str) -> MiniAppDef | None:
|
def get_app(self, app_id: str) -> MiniAppDef | None:
|
||||||
"""Get a specific mini-app definition."""
|
"""Get a specific mini-app definition."""
|
||||||
return self._apps.get(app_id)
|
return self._apps.get(app_id)
|
||||||
|
|
||||||
|
|
||||||
|
# ─── Singleton helpers ───
|
||||||
|
|
||||||
|
_registry: MiniAppRegistry | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_miniapp_registry() -> MiniAppRegistry:
|
||||||
|
"""Return the shared singleton MiniAppRegistry instance."""
|
||||||
|
global _registry
|
||||||
|
if _registry is None:
|
||||||
|
_registry = MiniAppRegistry()
|
||||||
|
return _registry
|
||||||
|
|
||||||
|
|
||||||
|
def reset_miniapp_registry() -> None:
|
||||||
|
"""Reset the singleton instance (useful for tests)."""
|
||||||
|
global _registry
|
||||||
|
_registry = None
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ class KommunikationPlugin(BasePlugin):
|
|||||||
service_container.register("comm_websocket", ws_manager)
|
service_container.register("comm_websocket", ws_manager)
|
||||||
|
|
||||||
# Register Mini-App registry as a shared service
|
# Register Mini-App registry as a shared service
|
||||||
from app.plugins.builtins.kommunikation.miniapp_registry import MiniAppRegistry
|
from app.plugins.builtins.kommunikation.miniapp_registry import get_miniapp_registry
|
||||||
miniapp_registry = MiniAppRegistry()
|
miniapp_registry = get_miniapp_registry()
|
||||||
service_container.register("comm_miniapps", miniapp_registry)
|
service_container.register("comm_miniapps", miniapp_registry)
|
||||||
|
|
||||||
# Register built-in mini-apps
|
# Register built-in mini-apps
|
||||||
|
|||||||
@@ -0,0 +1,187 @@
|
|||||||
|
"""Tests for MiniAppRegistry singleton pattern and plugin lifecycle integration."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from app.plugins.builtins.kommunikation.miniapp_registry import (
|
||||||
|
MiniAppRegistry,
|
||||||
|
get_miniapp_registry,
|
||||||
|
reset_miniapp_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def _reset_registry():
|
||||||
|
"""Ensure a clean singleton before and after each test."""
|
||||||
|
reset_miniapp_registry()
|
||||||
|
yield
|
||||||
|
reset_miniapp_registry()
|
||||||
|
|
||||||
|
|
||||||
|
class TestSingletonPattern:
|
||||||
|
def test_get_miniapp_registry_returns_same_instance(self):
|
||||||
|
"""get_miniapp_registry() must return the same object every call."""
|
||||||
|
r1 = get_miniapp_registry()
|
||||||
|
r2 = get_miniapp_registry()
|
||||||
|
assert r1 is r2
|
||||||
|
assert isinstance(r1, MiniAppRegistry)
|
||||||
|
|
||||||
|
def test_reset_miniapp_registry_creates_new_instance(self):
|
||||||
|
"""After reset, a new instance is created."""
|
||||||
|
r1 = get_miniapp_registry()
|
||||||
|
reset_miniapp_registry()
|
||||||
|
r2 = get_miniapp_registry()
|
||||||
|
assert r1 is not r2
|
||||||
|
|
||||||
|
def test_reset_miniapp_registry_sets_none(self):
|
||||||
|
"""reset_miniapp_registry() should set the global to None."""
|
||||||
|
get_miniapp_registry() # ensure singleton exists
|
||||||
|
reset_miniapp_registry()
|
||||||
|
# Access the module-level global directly
|
||||||
|
import app.plugins.builtins.kommunikation.miniapp_registry as mod
|
||||||
|
assert mod._registry is None
|
||||||
|
|
||||||
|
|
||||||
|
class TestRegisterAndList:
|
||||||
|
def test_register_and_list_apps(self):
|
||||||
|
"""register() adds an app that appears in list_apps()."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register(
|
||||||
|
app_id="test_app",
|
||||||
|
name="Test App",
|
||||||
|
icon="🧪",
|
||||||
|
description="A test app",
|
||||||
|
plugin_name="test_plugin",
|
||||||
|
render_schema={"type": "object"},
|
||||||
|
)
|
||||||
|
apps = registry.list_apps()
|
||||||
|
assert len(apps) == 1
|
||||||
|
assert apps[0]["app_id"] == "test_app"
|
||||||
|
assert apps[0]["name"] == "Test App"
|
||||||
|
assert apps[0]["plugin_name"] == "test_plugin"
|
||||||
|
|
||||||
|
def test_register_overwrites_same_app_id(self):
|
||||||
|
"""Registering the same app_id replaces the previous entry."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register(
|
||||||
|
app_id="dup_app",
|
||||||
|
name="First",
|
||||||
|
icon="1",
|
||||||
|
description="",
|
||||||
|
plugin_name="plugin_a",
|
||||||
|
)
|
||||||
|
registry.register(
|
||||||
|
app_id="dup_app",
|
||||||
|
name="Second",
|
||||||
|
icon="2",
|
||||||
|
description="",
|
||||||
|
plugin_name="plugin_b",
|
||||||
|
)
|
||||||
|
apps = registry.list_apps()
|
||||||
|
assert len(apps) == 1
|
||||||
|
assert apps[0]["name"] == "Second"
|
||||||
|
|
||||||
|
def test_list_apps_empty_by_default(self):
|
||||||
|
"""Fresh registry has no apps."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
assert registry.list_apps() == []
|
||||||
|
|
||||||
|
|
||||||
|
class TestUnregisterPlugin:
|
||||||
|
def test_unregister_plugin_removes_all_apps(self):
|
||||||
|
"""unregister_plugin() removes all apps from that plugin."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register("a1", "A1", "x", "", "plugin_x")
|
||||||
|
registry.register("a2", "A2", "x", "", "plugin_x")
|
||||||
|
registry.register("b1", "B1", "x", "", "plugin_y")
|
||||||
|
|
||||||
|
registry.unregister_plugin("plugin_x")
|
||||||
|
|
||||||
|
apps = registry.list_apps()
|
||||||
|
assert len(apps) == 1
|
||||||
|
assert apps[0]["app_id"] == "b1"
|
||||||
|
|
||||||
|
def test_unregister_plugin_noop_for_unknown(self):
|
||||||
|
"""unregister_plugin() for a non-existent plugin is a no-op."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register("a1", "A1", "x", "", "plugin_x")
|
||||||
|
registry.unregister_plugin("nonexistent")
|
||||||
|
assert len(registry.list_apps()) == 1
|
||||||
|
|
||||||
|
def test_unregister_single_app(self):
|
||||||
|
"""unregister() removes a specific app by id."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register("a1", "A1", "x", "", "plugin_x")
|
||||||
|
registry.register("a2", "A2", "x", "", "plugin_x")
|
||||||
|
|
||||||
|
registry.unregister("a1")
|
||||||
|
apps = registry.list_apps()
|
||||||
|
assert len(apps) == 1
|
||||||
|
assert apps[0]["app_id"] == "a2"
|
||||||
|
|
||||||
|
|
||||||
|
class TestPluginLifecycleIntegration:
|
||||||
|
"""Test that plugin activation/deactivation uses the shared singleton."""
|
||||||
|
|
||||||
|
def test_activation_registers_in_shared_registry(self):
|
||||||
|
"""When a plugin registers miniapps, they appear in the shared registry."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
# Simulate what automation/plugin.py on_activate does
|
||||||
|
registry.register(
|
||||||
|
app_id="automation_agent",
|
||||||
|
name="Agent Runner",
|
||||||
|
icon="🤖",
|
||||||
|
description="Run an AI agent",
|
||||||
|
plugin_name="automation",
|
||||||
|
render_schema={"type": "object"},
|
||||||
|
)
|
||||||
|
|
||||||
|
# A different caller (e.g. a route) should see the same apps
|
||||||
|
shared = get_miniapp_registry()
|
||||||
|
assert shared is registry
|
||||||
|
apps = shared.list_apps()
|
||||||
|
assert any(a["app_id"] == "automation_agent" for a in apps)
|
||||||
|
|
||||||
|
def test_deactivation_removes_from_shared_registry(self):
|
||||||
|
"""When a plugin deactivates, its miniapps are removed from the shared registry."""
|
||||||
|
registry = get_miniapp_registry()
|
||||||
|
registry.register(
|
||||||
|
app_id="automation_agent",
|
||||||
|
name="Agent Runner",
|
||||||
|
icon="🤖",
|
||||||
|
description="Run an AI agent",
|
||||||
|
plugin_name="automation",
|
||||||
|
)
|
||||||
|
registry.register(
|
||||||
|
app_id="komm_contact",
|
||||||
|
name="Contact Picker",
|
||||||
|
icon="👤",
|
||||||
|
description="Pick a contact",
|
||||||
|
plugin_name="kommunikation",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Simulate on_deactivate for automation
|
||||||
|
registry.unregister_plugin("automation")
|
||||||
|
|
||||||
|
apps = registry.list_apps()
|
||||||
|
assert len(apps) == 1
|
||||||
|
assert apps[0]["plugin_name"] == "kommunikation"
|
||||||
|
|
||||||
|
def test_cross_plugin_visibility(self):
|
||||||
|
"""Apps registered by plugin A are visible to plugin B via the singleton."""
|
||||||
|
# Plugin A registers
|
||||||
|
registry_a = get_miniapp_registry()
|
||||||
|
registry_a.register(
|
||||||
|
app_id="plugin_a_app",
|
||||||
|
name="A App",
|
||||||
|
icon="x",
|
||||||
|
description="",
|
||||||
|
plugin_name="plugin_a",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Plugin B queries
|
||||||
|
registry_b = get_miniapp_registry()
|
||||||
|
assert registry_a is registry_b
|
||||||
|
apps = registry_b.list_apps()
|
||||||
|
assert any(a["app_id"] == "plugin_a_app" for a in apps)
|
||||||
Reference in New Issue
Block a user