feat(M4): System-Rueckbau — Dashboard-Inhalte als MiniApps, Core = reiner Host (#362)
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- system_miniapps.py: audit_activity (audit:read, settings max_items) + system_metrics (settings:read) als Core-Apps in Registry - base.py-Fix: native Manifest-MiniApps reichen component durch (M1-Luecke) - contacts-Manifest: contacts_stats (ContactsStatsWidget, contacts:read, show_companies/show_persons) - Seed-Fix: nur renderbare Apps (component) landen im Dashboard-Layout - Frontend: ContactsStatsWidget, AuditActivityWidget, SystemMetricsWidget; MiniAppHost-Registry +3 - Dashboard.tsx = reiner Host (26 Z.); Page-Tests auf Pure-Host umgeschrieben - Tests: M4 7/7 (TDD rot->gruen), Backend-Regression 46/46, Vitest 22/22, tsc clean, build OK
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""M4 — System-Rückbau tests.
|
||||
|
||||
Core-owned MiniApps (audit_activity, system_metrics) registered by the
|
||||
host; contacts_stats as a native manifest miniapp with component (fixes
|
||||
the base.py gap where native miniapps did not carry their frontend
|
||||
component); the dashboard seed only places renderable apps (component
|
||||
present) — chat interaction apps without a component stay off dashboard
|
||||
layouts.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
from httpx import AsyncClient
|
||||
|
||||
from tests.conftest import ORIGIN_HEADER, login_client, seed_tenant_and_users
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _registry_with_system_apps():
|
||||
"""Fresh registry per test WITH the core system miniapps registered."""
|
||||
from app.plugins.miniapp_registry import (
|
||||
reset_miniapp_registry,
|
||||
)
|
||||
|
||||
reset_miniapp_registry()
|
||||
from app.core.system_miniapps import register_system_miniapps
|
||||
|
||||
register_system_miniapps()
|
||||
yield
|
||||
reset_miniapp_registry()
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Unit: system miniapp definitions
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
class TestSystemMiniAppDefs:
|
||||
def test_audit_activity_definition(self):
|
||||
from app.plugins.miniapp_registry import get_miniapp_registry
|
||||
|
||||
app = get_miniapp_registry().get_app("audit_activity")
|
||||
assert app is not None, "audit_activity must be registered"
|
||||
assert app.plugin_name == "system"
|
||||
assert app.permission == "audit:read"
|
||||
assert app.component == "@/components/dashboard/AuditActivityWidget"
|
||||
assert "dashboard" in app.hosts
|
||||
# settings_schema drives the generic settings form (max_items)
|
||||
fields = app.settings_schema.get("fields", [])
|
||||
assert any(f["name"] == "max_items" for f in fields)
|
||||
|
||||
def test_system_metrics_definition(self):
|
||||
from app.plugins.miniapp_registry import get_miniapp_registry
|
||||
|
||||
app = get_miniapp_registry().get_app("system_metrics")
|
||||
assert app is not None, "system_metrics must be registered"
|
||||
assert app.plugin_name == "system"
|
||||
assert app.permission == "settings:read"
|
||||
assert app.component == "@/components/dashboard/SystemMetricsWidget"
|
||||
assert app.col_span >= 2
|
||||
|
||||
def test_native_manifest_miniapp_carries_component(self):
|
||||
"""contacts_stats lives in the native miniapps manifest section and
|
||||
carries its frontend component (base.py must pass it through)."""
|
||||
from app.plugins.builtins.contacts.plugin import ContactsPlugin
|
||||
|
||||
contributions = ContactsPlugin().manifest.miniapps
|
||||
stats = [m for m in contributions if m.app_id == "contacts_stats"]
|
||||
assert len(stats) == 1
|
||||
assert stats[0].component == "@/components/dashboard/ContactsStatsWidget"
|
||||
assert stats[0].permission == "contacts:read"
|
||||
|
||||
def test_base_plugin_registers_native_miniapp_with_component(self):
|
||||
"""The lifecycle path registers native miniapps INCLUDING component
|
||||
(M4 fix for the M1 gap where only the dashboard_widgets alias
|
||||
passed components through)."""
|
||||
from app.plugins.builtins.contacts.plugin import ContactsPlugin
|
||||
from app.plugins.miniapp_registry import get_miniapp_registry
|
||||
|
||||
plugin = ContactsPlugin()
|
||||
plugin._register_manifest_miniapps()
|
||||
app = get_miniapp_registry().get_app("contacts_stats")
|
||||
assert app is not None
|
||||
assert app.component == "@/components/dashboard/ContactsStatsWidget"
|
||||
# dashboard_widgets alias still works
|
||||
assert get_miniapp_registry().get_app("recent_contacts") is not None
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# API: permission-filtered visibility
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSystemMiniAppsApi:
|
||||
async def test_admin_sees_system_apps(self, client: AsyncClient, db_session):
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
resp = await client.get("/api/v1/miniapps", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
app_ids = {i["app_id"] for i in resp.json()["items"]}
|
||||
assert "audit_activity" in app_ids
|
||||
assert "system_metrics" in app_ids
|
||||
|
||||
async def test_viewer_does_not_see_system_apps(
|
||||
self, client: AsyncClient, db_session
|
||||
):
|
||||
"""Viewer has neither audit:read nor settings:read -> fail-closed."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "viewer@tenanta.com")
|
||||
resp = await client.get("/api/v1/miniapps", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
app_ids = {i["app_id"] for i in resp.json()["items"]}
|
||||
assert "audit_activity" not in app_ids
|
||||
assert "system_metrics" not in app_ids
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Seed: only renderable apps (component) go onto dashboard layouts
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
class TestSeedComponentFilter:
|
||||
async def test_seed_excludes_apps_without_component(
|
||||
self, client: AsyncClient, db_session
|
||||
):
|
||||
"""Chat interaction apps without a frontend component must not be
|
||||
seeded onto dashboard layouts (production measurement 2026-08-30:
|
||||
9 seeded widgets, only 3 renderable)."""
|
||||
await seed_tenant_and_users(db_session)
|
||||
await login_client(client, "admin@tenanta.com")
|
||||
|
||||
from app.plugins.miniapp_registry import get_miniapp_registry
|
||||
|
||||
reg = get_miniapp_registry()
|
||||
reg.register(
|
||||
app_id="renderable_app", name="Renderable", plugin_name="test",
|
||||
permission="", component="@/components/x", order=90,
|
||||
)
|
||||
reg.register(
|
||||
app_id="chat_only_app", name="Chat Only", plugin_name="test",
|
||||
permission="", component="", order=95,
|
||||
)
|
||||
|
||||
resp = await client.get("/api/v1/dashboards", headers=ORIGIN_HEADER)
|
||||
assert resp.status_code == 200
|
||||
items = resp.json()
|
||||
assert len(items) == 1
|
||||
seeded_ids = {
|
||||
w["app_id"]
|
||||
for w in items[0]["layout"]["tabs"][0]["widgets"]
|
||||
}
|
||||
assert "renderable_app" in seeded_ids
|
||||
assert "chat_only_app" not in seeded_ids
|
||||
# system apps are renderable and (for the admin) permitted -> seeded
|
||||
assert "audit_activity" in seeded_ids
|
||||
assert "system_metrics" in seeded_ids
|
||||
Reference in New Issue
Block a user