03dd477899
Check Cross-Plugin Imports / check (push) Has been cancelled
- Scope-Deklarationen: tasks only_mine, kommunikation conversation_ids, wiki category_ids (NEUE contracts.py), report_generator template_ids, automation agent_ids (module_key agents), tags tag_ids, unified_search entity_types dynamisch aus Provider-Registry - Core-Beiträge: navigation default_route (Startseite) + dashboard widget_app_ids (Widget-TYP-Angebot, Layout bleibt Phase M) - Backend-Filter (additive UND): /tasks (only_mine), /comm/conversations, /wiki/articles+/categories (Subtree), /reports/print-templates, /agents, /tags, /search GET+POST (entity_types-Schnitt), /miniapps?host=dashboard - apply_entity_type_scope-Helper (requested ∧ scope) - Frontend: WorkspaceSwitcher default_route-Navigation, Sidebar workspace-menu_order-Sortierung, workspaceStore moduleMenuOrder() - Tests: 18/18 Deklarationen + 11/11 Filter (TDD), Frontend 2/2 + Store 18/18, tsc clean, Build OK - Regression 64 passed (4 Kombi-Failures = Suite-Isolation, solo-bewiesen); Checker 0; Ruff = Vorbestand (Stash-bewiesen)
218 lines
8.3 KiB
Python
218 lines
8.3 KiB
Python
"""N4 — Scope-Deklarationen der restlichen Module (Phase N, letzter Task).
|
|
|
|
Plugins declare workspace_scopes() for: tasks (only_mine), kommunikation
|
|
(conversation_ids), wiki (category_ids subtree), reports (template_ids),
|
|
agents (agent_ids), tags (tag_ids), search (entity_types — dynamic from the
|
|
provider registry). Core contributions add navigation (default_route) and
|
|
dashboard (widget_app_ids — the workspace limits the offered widget TYPES,
|
|
never the personal layout, Phase M boundary).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import pytest
|
|
|
|
from app.schemas.workspace import WorkspaceModuleScopes
|
|
|
|
N4_PLUGINS = (
|
|
"tasks",
|
|
"kommunikation",
|
|
"wiki",
|
|
"report_generator",
|
|
"automation",
|
|
"tags",
|
|
"unified_search",
|
|
)
|
|
N4_MODULES = {
|
|
"tasks",
|
|
"communication",
|
|
"wiki",
|
|
"reports",
|
|
"agents",
|
|
"tags",
|
|
"search",
|
|
"navigation",
|
|
"dashboard",
|
|
}
|
|
|
|
|
|
# ─── Unit: Contract-Deklarationen der N4-Plugins ──────────────
|
|
|
|
|
|
@pytest.mark.parametrize("plugin_name", N4_PLUGINS)
|
|
def test_contract_declares_valid_scopes(plugin_name: str):
|
|
"""Every N4 plugin declares workspace_scopes() with valid contributions."""
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
contract = get_contract(plugin_name)
|
|
assert contract is not None, f"Contract für {plugin_name} fehlt"
|
|
fn = getattr(contract, "workspace_scopes", None)
|
|
assert callable(fn), f"{plugin_name} deklariert workspace_scopes() nicht"
|
|
|
|
contributions = fn() or []
|
|
assert contributions, f"{plugin_name}: mindestens eine Contribution"
|
|
for contribution in contributions:
|
|
WorkspaceModuleScopes.model_validate(contribution)
|
|
|
|
|
|
def _dims_for(plugin_name: str) -> dict[str, dict]:
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
return {
|
|
d["key"]: d
|
|
for c in get_contract(plugin_name).workspace_scopes()
|
|
for d in c["dimensions"]
|
|
}
|
|
|
|
|
|
def test_tasks_declares_only_mine_toggle():
|
|
"""Roadmap N4 tasks: „nur meine" — reiner Toggle ohne Wertequelle."""
|
|
dims = _dims_for("tasks")
|
|
assert "only_mine" in dims
|
|
assert dims["only_mine"]["control"] == "toggle"
|
|
assert dims["only_mine"]["value_source"] is None
|
|
# module_key tasks (Menüpfad /tasks)
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
keys = [c["module_key"] for c in get_contract("tasks").workspace_scopes()]
|
|
assert "tasks" in keys
|
|
|
|
|
|
def test_kommunikation_declares_conversation_ids():
|
|
"""Roadmap N4 Kommunikation: Räume-Teilmengen."""
|
|
dims = _dims_for("kommunikation")
|
|
assert "conversation_ids" in dims
|
|
assert dims["conversation_ids"]["control"] == "multiselect"
|
|
assert dims["conversation_ids"]["value_source"]["endpoint"] == "/api/v1/comm/conversations"
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
keys = [c["module_key"] for c in get_contract("kommunikation").workspace_scopes()]
|
|
assert "communication" in keys # Menüpfad /communication
|
|
|
|
|
|
def test_wiki_declares_category_ids():
|
|
"""Roadmap N4 Wiki: Kategorien-Teilmengen (Subtree wie contacts/dms)."""
|
|
dims = _dims_for("wiki")
|
|
assert "category_ids" in dims
|
|
assert dims["category_ids"]["value_source"]["endpoint"] == "/api/v1/wiki/categories"
|
|
assert dims["category_ids"]["value_source"]["items_path"] == "items"
|
|
|
|
|
|
def test_reports_declares_template_ids():
|
|
"""Roadmap N4 Reports/Dokumente: Vorlagen-Teilmengen."""
|
|
dims = _dims_for("report_generator")
|
|
assert "template_ids" in dims
|
|
assert dims["template_ids"]["value_source"]["endpoint"] == "/api/v1/reports/print-templates"
|
|
assert dims["template_ids"]["value_source"]["items_path"] == "items"
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
keys = [c["module_key"] for c in get_contract("report_generator").workspace_scopes()]
|
|
assert "reports" in keys # Menüpfad /reports
|
|
|
|
|
|
def test_automation_declares_agent_ids():
|
|
"""Roadmap N4 Automation: Agenten-Teilmengen (module_key agents)."""
|
|
dims = _dims_for("automation")
|
|
assert "agent_ids" in dims
|
|
assert dims["agent_ids"]["value_source"]["endpoint"] == "/api/v1/agents"
|
|
assert dims["agent_ids"]["value_source"]["items_path"] == "items"
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
keys = [c["module_key"] for c in get_contract("automation").workspace_scopes()]
|
|
assert "agents" in keys # page route /agents (kein Menüeintrag)
|
|
|
|
|
|
def test_tags_declares_tag_ids():
|
|
"""Roadmap N4 Tags: Tag-Teilmengen."""
|
|
dims = _dims_for("tags")
|
|
assert "tag_ids" in dims
|
|
assert dims["tag_ids"]["value_source"]["endpoint"] == "/api/v1/tags"
|
|
assert dims["tag_ids"]["value_source"]["value_key"] == "id"
|
|
|
|
|
|
def test_search_declares_entity_types_with_current_providers():
|
|
"""Roadmap N4 Suche: Provider-Teilmengen — Optionen dynamisch aus der
|
|
Provider-Registry (Contract liefert sie zur Aufrufzeit)."""
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
contributions = get_contract("unified_search").workspace_scopes()
|
|
keys = [c["module_key"] for c in contributions]
|
|
assert "search" in keys
|
|
dims = {d["key"]: d for c in contributions for d in c["dimensions"]}
|
|
assert "entity_types" in dims
|
|
assert dims["entity_types"]["control"] == "multiselect"
|
|
# Dynamic options: reflect the currently registered providers
|
|
values = {o["value"] for o in dims["entity_types"]["options"]}
|
|
assert {"contact", "task", "mail"} <= values, (
|
|
f"entity_types-Optionen enthalten nicht Core-Provider: {values}"
|
|
)
|
|
|
|
|
|
# ─── Unit: Core-Beiträge (navigation, dashboard) ───────────────
|
|
|
|
|
|
def test_core_contributions_navigation_and_dashboard():
|
|
"""Core-Module navigation + dashboard contribute scope dimensions via the
|
|
aggregator (they are core-owned, not plugin-owned)."""
|
|
from app.services.workspace_scope_service import get_scope_definitions
|
|
|
|
modules = get_scope_definitions()
|
|
assert "navigation" in modules, "navigation-Beitrag fehlt"
|
|
nav_dims = {d["key"]: d for d in modules["navigation"]}
|
|
assert nav_dims["default_route"]["control"] == "select"
|
|
route_values = {o["value"] for o in nav_dims["default_route"]["options"]}
|
|
assert "/" in route_values
|
|
assert "/contacts" in route_values
|
|
|
|
assert "dashboard" in modules, "dashboard-Beitrag fehlt"
|
|
dash_dims = {d["key"]: d for d in modules["dashboard"]}
|
|
assert dash_dims["widget_app_ids"]["control"] == "multiselect"
|
|
source = dash_dims["widget_app_ids"]["value_source"]
|
|
assert source["endpoint"] == "/api/v1/miniapps?host=dashboard"
|
|
assert source["value_key"] == "app_id"
|
|
|
|
|
|
def test_aggregator_covers_all_n4_modules():
|
|
"""The aggregated registry covers every N4 module (plugin + core)."""
|
|
from app.services.workspace_scope_service import get_scope_definitions
|
|
|
|
modules = get_scope_definitions()
|
|
for key in N4_MODULES:
|
|
assert key in modules, f"Modul {key} fehlt in den Scope-Definitionen"
|
|
assert modules[key], f"{key}: Dimensionen leer"
|
|
|
|
|
|
# ─── Unit: deklarierte Value-Endpoints existieren (OpenAPI) ────
|
|
|
|
|
|
async def test_n4_value_endpoints_exist(app):
|
|
"""Every N4 value_source endpoint must exist as a GET route — checked via
|
|
OpenAPI (app.routes carries only _IncludedRouter wrappers, N1 lesson)."""
|
|
from app.plugins.builtins.contracts import get_contract
|
|
|
|
paths = app.openapi().get("paths", {})
|
|
get_paths = {p for p, ops in paths.items() if "get" in ops}
|
|
for plugin_name in N4_PLUGINS:
|
|
contract = get_contract(plugin_name)
|
|
for contribution in contract.workspace_scopes() or []:
|
|
for dim in contribution.get("dimensions", []):
|
|
source = dim.get("value_source")
|
|
if not source:
|
|
continue
|
|
endpoint_path = source["endpoint"].split("?")[0]
|
|
assert endpoint_path in get_paths, (
|
|
f"{plugin_name}: deklarierter Value-Endpoint {endpoint_path} "
|
|
"existiert nicht als GET-Route"
|
|
)
|
|
|
|
|
|
def test_navigation_routes_are_valid_paths():
|
|
"""Navigation default_route options must be real frontend paths."""
|
|
from app.services.workspace_scope_service import get_scope_definitions
|
|
|
|
nav = {d["key"]: d for d in get_scope_definitions()["navigation"]}
|
|
for option in nav["default_route"]["options"]:
|
|
assert option["value"].startswith("/"), (
|
|
f"Route {option['value']} muss mit / beginnen"
|
|
)
|