fix(arch): externes Audit — 13 Backend-Fixes (Workspace-Modules, Tenant-Manifeste, Lifecycle, Contracts, Permissions)
Check Cross-Plugin Imports / check (push) Has been cancelled

Verifikation: Alle 17 Audit-Findings gegen den Code geprueft — alle bestaetigt.
Backend-Lifecycle-Fixes umgesetzt; 4 Frontend-Plugin-Architektur-Punkte
als Phase Q in die Roadmap eingeplant.

- P1 list_workspaces: Module + User-Counts gebuendelt laden (Editor-Overwrite-Bug)
- P1 active-manifests: Tenant-Deaktivierung (tenant_plugin_activation) filtern
- P1 uninstall: volle Service-Deactivation VOR registry.uninstall()
- P1 ContractRegistry: DB-Aktivstatus-Guard (Restart-Edge-Case) + Re-Activate
- P1/P2 Field-Definitions: voller Lifecycle (register/unregister) im Service
- P1/P2 Contact-Felddefinitionen (39) ins ContactsPlugin-Manifest verschoben
- P1 12 fehlende Permission-Keys registriert (AST-Scan: 0 fehlend)
- P2 contact_folder -> ContactsPlugin; ENTITY_PLUGIN_OWNERS wird befuellt
- P2 Entity-Permission-Fallback fail-closed statt contacts:read
- P2 forgejo_error_reporter is_core=False; DMS is_core=True (ADR-020)
- P2 Worker: Contacts-Trash-Cleanup ins Plugin (get_job_modules-Discovery)
- P1/P2 DSGVO-Export delegiert an DSAR-Collector (kein Core->Contacts)
- P2 False-green Tests korrigiert (or True, veraltete Route-Count-Assertion)

Verifikation: tests/test_audit_architecture_fixes.py 17/17; Regressionen
gruen (contacts_lifecycle, entity_registry, workspace_scopes, rbac,
lifecycle_service); Combo-Order-Test 35/35; Cross-Plugin-Checker 497/0;
compileall sauber; ruff auf 7-Error-Baseline.

Doku: PROGRESS.md Audit-Section, PLATFORM_ROADMAP.md Phase Q (Q1-Q4),
plugin-development-guide.md Lifecycle, permissions.md Katalog.
This commit is contained in:
Agent Zero
2026-09-13 02:25:01 +02:00
parent 86cea5d6c4
commit 4a25ac1379
25 changed files with 1020 additions and 141 deletions
+36 -10
View File
@@ -46,7 +46,13 @@ class TestContactsPluginLifecycle:
# Before activation: nothing registered
reg = get_restore_registry()
assert not reg.is_registered("contact")
assert "contact" not in ENTITY_MODELS or True # May be in core models
# Audit P2 (false-green): this used to be
# ``assert "contact" not in ENTITY_MODELS or True`` — always true.
# Entity models are registered by the conftest bootstrap for ALL
# discovered plugins (mirroring main.py), so "contact" IS present
# before on_activate; what must NOT be registered yet is the
# restore config (checked above) and the plugin gate:
assert not get_permission_registry().is_plugin_active("contacts")
# Activate
import asyncio
@@ -136,11 +142,26 @@ class TestContactsPluginLifecycle:
assert models["company"] is Contact
def test_app_still_starts_without_contacts_special_case(self):
"""App creates successfully with Contacts as a plugin, not a Core special case."""
"""App creates successfully with Contacts as a plugin, not a Core special case.
Audit P2 (false-green / stale assertion): the old ``> 100 routes``
assertion reflected the PRE-plugin architecture where business
routes were hardcoded in main.py. Since the plugin refactor,
create_app() mounts only CORE routes (~85); plugin routes are
mounted by the registry lifecycle. The real "no special case"
proof is that no contacts route is statically included.
"""
from app.main import create_app
app = create_app()
assert app is not None
assert len(app.routes) > 100 # Should have many routes
# Core routes must exist
assert len(app.routes) > 50
# Contacts is a plugin: its routes must NOT be hardcoded in the
# core app (they are mounted via registry.activate at startup).
static_paths = {getattr(r, "path", "") for r in app.routes}
assert not any(p.startswith("/api/v1/contacts") for p in static_paths), (
"contacts routes must not be statically mounted — Contacts is a plugin"
)
def test_hook_isolation_deactivate_one_plugin_keeps_other(self):
"""Two plugins register on same event; deactivating one keeps the other's handler."""
@@ -180,12 +201,14 @@ class TestContactsPluginLifecycle:
# Cleanup
reg._reset_for_testing()
def test_full_lifecycle_activate_deactivate_via_service(self):
"""Full lifecycle: activate via PluginService → verify registered → deactivate → verify deregistered.
def test_full_lifecycle_activate_deactivate_simulated(self):
"""Full lifecycle: simulate the service activation sequence → verify
registered → simulate deactivation → verify deregistered.
This is the E2E lifecycle test (Phase 5.2): tests that activation/deactivation
through the service layer properly registers/deregisters permissions,
entity models, restore config, and history hooks.
Audit P2 (false-green): renamed — this test drives the plugin hooks
and registry calls MANUALLY (mirroring what PluginService does),
it does NOT go through the real PluginService. The true E2E test
through PluginService lives in test_plugin_lifecycle_service.py.
"""
from app.services.plugin_service import PluginService
from app.core.permission_registry import get_permission_registry, init_permission_registry
@@ -252,8 +275,11 @@ class TestContactsPluginLifecycle:
# Verify everything is deregistered
assert not perm_reg.is_plugin_active(plugin_name), "Plugin should be inactive"
assert not get_restore_registry().is_registered("contact"), "Restore config should be removed"
assert "contact" not in ENTITY_MODELS or "contact" in {"contact", "contacts", "company"}, \
"Entity model may still be in core ENTITY_MODELS"
# Audit P2 (false-green): the old assertion
# ``"contact" not in ENTITY_MODELS or "contact" in {...}`` was
# always true. After unregister_entity_model() the type must be
# GONE from the registry (contacts is plugin-owned, not core).
assert "contact" not in ENTITY_MODELS, "contact must be deregistered after plugin deactivation"
all_hooks = hook_reg._actions if hasattr(hook_reg, '_actions') else {}
assert not any("contact.after_create" in k for k in all_hooks), "History hooks should be removed"