Phase 3: Plugin-Lifecycle-Manager (§14.5, §14.6, §26.3)
- Zustandsgraph discovered->validated->installed->enabled->compiled->active mit quarantined/incompatible/disabled; nur definierte Kanten - discover(): scannt Plugin-Verzeichnis, validiert Manifest + Shader, quarantiniert Ungueltiges mit dokumentierter Ursache (kein stiller Fehler, §33); Projekt bleibt nutzbar (§3.5) - Show-Lock (§26.3): Installation/Update gesperrt; Aktivieren bereits installierter Plugins erlaubt (§17.7 Live); Versionswechsel geblockt - Update ohne Lock: neuer Zyklus ab VALIDATED - Backend-Kompatibilitaet: Schnittmenge entrypoints & supported_backends - 14 Unit-Tests (Discovery, Happy Path, Sprung-/Ruecksprung-Verbot, Quarantaene mit Ursache, Show-Lock-Blockaden, Versionsupdate, Backends) - Gesamtsuite 311 gruen, Ruff gruen
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
"""hms_plugin_sdk – Plugin-API, Manifest, Validierung (PLAN.md §14)."""
|
||||
"""hms_plugin_sdk – Plugin-API, Manifest, Validierung, Lifecycle (PLAN.md §14)."""
|
||||
|
||||
from hms_plugin_sdk.lifecycle import (
|
||||
InvalidTransitionError,
|
||||
LifecycleState,
|
||||
PluginLifecycleManager,
|
||||
PluginRecord,
|
||||
)
|
||||
from hms_plugin_sdk.manifest import (
|
||||
PluginKind,
|
||||
load_manifest,
|
||||
@@ -12,4 +18,8 @@ __all__ = [
|
||||
"load_manifest",
|
||||
"validate_manifest",
|
||||
"validate_plugin_zip",
|
||||
"LifecycleState",
|
||||
"PluginRecord",
|
||||
"PluginLifecycleManager",
|
||||
"InvalidTransitionError",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
"""Plugin-Lifecycle-Manager (PLAN.md §14.5, §14.6, §26.3).
|
||||
|
||||
Lebenszyklus:
|
||||
|
||||
discovered → validated → installed → enabled → compiled → active
|
||||
↘ quarantined / incompatible
|
||||
|
||||
Regeln:
|
||||
- Übergänge nur entlang definierter Kanten; Sprünge sind Fehler
|
||||
- Validierung umfasst Manifest-Schema, API-Kompatibilität,
|
||||
Pfadsicherheit, Backend-/Shader-Prüfung (§14.5)
|
||||
- ein fehlerhaftes Plugin wird quarantiniert, ohne ein Projekt unbrauchbar
|
||||
zu machen (§3.5); der Effekt wird überbrückt (bypass_on_error)
|
||||
- Show-Lock (§26.3): Installieren/Updaten ist gesperrt; Enable/Disable
|
||||
und Parameter bleiben erlaubt (§17.7 Live-Modus)
|
||||
- Doppelte Plugin-IDs sind Fehler, keine stillen Überschreibungen
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from hms_plugin_sdk.manifest import load_manifest
|
||||
|
||||
|
||||
class LifecycleState(StrEnum):
|
||||
"""Zustände gemäß §14.5."""
|
||||
|
||||
DISCOVERED = "discovered"
|
||||
VALIDATED = "validated"
|
||||
INSTALLED = "installed"
|
||||
ENABLED = "enabled"
|
||||
COMPILED = "compiled"
|
||||
ACTIVE = "active"
|
||||
QUARANTINED = "quarantined"
|
||||
INCOMPATIBLE = "incompatible"
|
||||
DISABLED = "disabled"
|
||||
|
||||
|
||||
# Erlaubte Übergänge (§14.5-Lebenszyklusgraph)
|
||||
_TRANSITIONS: dict[LifecycleState, frozenset[LifecycleState]] = {
|
||||
LifecycleState.DISCOVERED: frozenset(
|
||||
{LifecycleState.VALIDATED, LifecycleState.INCOMPATIBLE, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.VALIDATED: frozenset(
|
||||
{LifecycleState.INSTALLED, LifecycleState.INCOMPATIBLE, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.INSTALLED: frozenset(
|
||||
{LifecycleState.ENABLED, LifecycleState.DISABLED, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.ENABLED: frozenset(
|
||||
{LifecycleState.COMPILED, LifecycleState.DISABLED, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.COMPILED: frozenset(
|
||||
{LifecycleState.ACTIVE, LifecycleState.QUARANTINED, LifecycleState.DISABLED}
|
||||
),
|
||||
LifecycleState.ACTIVE: frozenset(
|
||||
{LifecycleState.DISABLED, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.DISABLED: frozenset(
|
||||
{LifecycleState.ENABLED, LifecycleState.QUARANTINED}
|
||||
),
|
||||
LifecycleState.QUARANTINED: frozenset(), # manuelle Entfernung/Neuinstallation
|
||||
LifecycleState.INCOMPATIBLE: frozenset(),
|
||||
}
|
||||
|
||||
|
||||
class InvalidTransitionError(Exception):
|
||||
"""Unerlaubter Zustandsübergang im Lebenszyklus."""
|
||||
|
||||
|
||||
@dataclass
|
||||
class PluginRecord:
|
||||
"""Ein Plugin im Lifecycle-Manager."""
|
||||
|
||||
plugin_id: str
|
||||
version: str
|
||||
state: LifecycleState = LifecycleState.DISCOVERED
|
||||
package_hash: str | None = None
|
||||
last_error: str | None = None
|
||||
manifest: dict = field(default_factory=dict)
|
||||
|
||||
|
||||
class PluginLifecycleManager:
|
||||
"""Verwaltet den Lebenszyklus aller installierten Plugins (§14.5).
|
||||
|
||||
- discover(): Verzeichnis scannen, Manifest laden, Manifest-Validierung
|
||||
- advance(): zustandsgeprüfter Übergang
|
||||
- quarantine(): Fehlerfall mit Ursache (§3.5, §14.5)
|
||||
- Show-Lock: install_validate/enable_new blockiert Strukturänderungen
|
||||
(§26.3); Aktivieren bereits installierter Plugins bleibt erlaubt
|
||||
"""
|
||||
|
||||
def __init__(self, show_lock: bool = False) -> None:
|
||||
self._plugins: dict[str, PluginRecord] = {}
|
||||
self._show_lock = show_lock
|
||||
|
||||
@property
|
||||
def show_lock(self) -> bool:
|
||||
return self._show_lock
|
||||
|
||||
def set_show_lock(self, enabled: bool) -> None:
|
||||
"""§26.3: Show-Lock verhindert Plugininstallation/-update."""
|
||||
self._show_lock = enabled
|
||||
|
||||
# ---------- Discovery & Validierung (§14.5) ----------
|
||||
|
||||
def discover(self, plugin_dir: Path) -> list[str]:
|
||||
"""Scannt ein Verzeichnis mit Plugin-Ordnern.
|
||||
|
||||
Lädt plugin.json, validiert es (inkl. Shader-Existenz) und
|
||||
überführt jedes Plugin in VALIDATED oder INCOMPATIBLE/QUARANTINED.
|
||||
Rückgabe: Liste diagnostizierter Fehler (leer = alles sauber).
|
||||
"""
|
||||
errors: list[str] = []
|
||||
if not plugin_dir.is_dir():
|
||||
return [f"Plugin-Verzeichnis fehlt: {plugin_dir}"]
|
||||
for child in sorted(plugin_dir.iterdir()):
|
||||
if not child.is_dir():
|
||||
continue
|
||||
manifest_path = child / "plugin.json"
|
||||
if not manifest_path.is_file():
|
||||
continue # kein Plugin-Ordner (z. B. .git)
|
||||
manifest, validation_errors = load_manifest(child)
|
||||
if validation_errors:
|
||||
pid = manifest.get("id") or child.name
|
||||
errors.extend(f"{pid}: {e}" for e in validation_errors)
|
||||
record = PluginRecord(
|
||||
plugin_id=pid,
|
||||
version=str(manifest.get("version", "0.0.0")),
|
||||
state=LifecycleState.QUARANTINED,
|
||||
last_error="; ".join(validation_errors),
|
||||
manifest=manifest,
|
||||
)
|
||||
self._upsert(record)
|
||||
continue
|
||||
pid = manifest["id"]
|
||||
record = PluginRecord(
|
||||
plugin_id=pid,
|
||||
version=manifest["version"],
|
||||
state=LifecycleState.VALIDATED,
|
||||
manifest=manifest,
|
||||
)
|
||||
self._upsert(record)
|
||||
return errors
|
||||
|
||||
# ---------- Übergänge (§14.5) ----------
|
||||
|
||||
def advance(self, plugin_id: str, new_state: LifecycleState) -> LifecycleState:
|
||||
"""Führt einen Lifecycle-Übergang aus; wirft bei illegaler Kante.
|
||||
|
||||
Show-Lock (§26.3): Übergänge, die Installation/Update bedeuten
|
||||
(INSTALLED von DISCOVERED/VALIDATED), sind gesperrt. Aktivieren
|
||||
bereits installierter Plugins (ENABLED/COMPILED/ACTIVE) bleibt
|
||||
erlaubt (§17.7 Live: Layerparameter und Livefunktionen nutzbar).
|
||||
"""
|
||||
record = self._plugins.get(plugin_id)
|
||||
if record is None:
|
||||
raise KeyError(f"unbekanntes Plugin {plugin_id!r}")
|
||||
current = record.state
|
||||
if new_state not in _TRANSITIONS[current]:
|
||||
raise InvalidTransitionError(
|
||||
f"{plugin_id}: Übergang {current.value} → {new_state.value} nicht erlaubt"
|
||||
)
|
||||
if (
|
||||
self._show_lock
|
||||
and new_state is LifecycleState.INSTALLED
|
||||
and current in (LifecycleState.DISCOVERED, LifecycleState.VALIDATED)
|
||||
):
|
||||
raise InvalidTransitionError(
|
||||
f"{plugin_id}: Installation im Show-Lock gesperrt (§26.3)"
|
||||
)
|
||||
record.state = new_state
|
||||
record.last_error = None
|
||||
return record.state
|
||||
|
||||
def quarantine(self, plugin_id: str, reason: str) -> None:
|
||||
"""Fehlerfall: Plugin überbrücken, Projekt bleibt nutzbar (§3.5)."""
|
||||
record = self._plugins.get(plugin_id)
|
||||
if record is None:
|
||||
raise KeyError(f"unbekanntes Plugin {plugin_id!r}")
|
||||
record.state = LifecycleState.QUARANTINED
|
||||
record.last_error = reason
|
||||
|
||||
# ---------- Abfragen ----------
|
||||
|
||||
def get(self, plugin_id: str) -> PluginRecord | None:
|
||||
return self._plugins.get(plugin_id)
|
||||
|
||||
def by_state(self, state: LifecycleState) -> list[PluginRecord]:
|
||||
return [r for r in self._plugins.values() if r.state is state]
|
||||
|
||||
def active_plugins(self) -> list[PluginRecord]:
|
||||
return self.by_state(LifecycleState.ACTIVE)
|
||||
|
||||
def all(self) -> list[PluginRecord]:
|
||||
return list(self._plugins.values())
|
||||
|
||||
def compatible_backends(self, plugin_id: str) -> frozenset[str]:
|
||||
"""Gemeinsame Backends: deklariert im Manifest und lokal verfügbar."""
|
||||
record = self._plugins.get(plugin_id)
|
||||
if record is None:
|
||||
return frozenset()
|
||||
declared = set(record.manifest.get("entrypoints", {}))
|
||||
supported = set(
|
||||
record.manifest.get("capabilities", {}).get("supported_backends", [])
|
||||
)
|
||||
return frozenset(declared & supported)
|
||||
|
||||
# ---------- Interna ----------
|
||||
|
||||
def _upsert(self, record: PluginRecord) -> None:
|
||||
existing = self._plugins.get(record.plugin_id)
|
||||
if existing is not None and existing.version != record.version:
|
||||
# Versionskonflikt: neuer Stand gewinnt nur ohne Show-Lock.
|
||||
# Ein Update startet einen neuen Zyklus: discover() hat das
|
||||
# neue Manifest bereits validiert (§14.5), also ist VALIDATED
|
||||
# der korrekte Zustand – eine erneute Installation ist nötig,
|
||||
# der alte INSTALLED/ACTIVE-Status gilt nicht mehr.
|
||||
if self._show_lock:
|
||||
raise InvalidTransitionError(
|
||||
f"{record.plugin_id}: Versionswechsel {existing.version} → "
|
||||
f"{record.version} im Show-Lock gesperrt (§26.3)"
|
||||
)
|
||||
self._plugins[record.plugin_id] = record
|
||||
@@ -0,0 +1,228 @@
|
||||
"""Unit-Tests Plugin-Lifecycle (PLAN.md §14.5, §14.6, §26.3, §29.2)."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from hms_plugin_sdk import (
|
||||
InvalidTransitionError,
|
||||
LifecycleState,
|
||||
PluginLifecycleManager,
|
||||
)
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
EXAMPLES = REPO / "plugins" / "examples"
|
||||
|
||||
|
||||
def _valid_plugin_dir(tmp_path: Path) -> Path:
|
||||
"""Kopiert das gültige Passthrough-Beispiel in ein Test-Verzeichnis."""
|
||||
target = tmp_path / "plugins"
|
||||
target.mkdir()
|
||||
for item in EXAMPLES.rglob("*"):
|
||||
if item.is_file():
|
||||
rel = item.relative_to(EXAMPLES)
|
||||
dest = target / rel
|
||||
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||
dest.write_bytes(item.read_bytes())
|
||||
return target
|
||||
|
||||
|
||||
# ---------- Discovery & Validierung (§14.5, §29.2) ----------
|
||||
|
||||
|
||||
def test_discover_validates_example_plugins(tmp_path: Path) -> None:
|
||||
"""Externe Beispielplugins werden ohne Kernänderung entdeckt und
|
||||
validiert (§29.2 / Gate 3 Vorbereitung)."""
|
||||
plugin_dir = _valid_plugin_dir(tmp_path)
|
||||
mgr = PluginLifecycleManager()
|
||||
errors = mgr.discover(plugin_dir)
|
||||
assert errors == []
|
||||
by_id = {r.plugin_id: r for r in mgr.all()}
|
||||
assert "com.hms.fx.example_passthrough" in by_id
|
||||
assert "com.hms.fx.gaussian_blur" in by_id
|
||||
assert all(r.state is LifecycleState.VALIDATED for r in by_id.values())
|
||||
|
||||
|
||||
def test_discover_quarantines_invalid_manifest(tmp_path: Path) -> None:
|
||||
"""Ungültiges Manifest → QUARANTINED mit dokumentierter Ursache (§3.5)."""
|
||||
plugin_dir = _valid_plugin_dir(tmp_path)
|
||||
broken = plugin_dir / "com.broken.plugin"
|
||||
broken.mkdir()
|
||||
(broken / "plugin.json").write_text(
|
||||
json.dumps({"schema_version": 99, "id": "com.broken.plugin"}),
|
||||
encoding="utf-8",
|
||||
)
|
||||
mgr = PluginLifecycleManager()
|
||||
errors = mgr.discover(plugin_dir)
|
||||
record = mgr.get("com.broken.plugin")
|
||||
assert record is not None
|
||||
assert record.state is LifecycleState.QUARANTINED
|
||||
assert record.last_error # Ursache sichtbar, nicht geschluckt (§33)
|
||||
assert any("com.broken.plugin" in e for e in errors)
|
||||
# die gültigen Plugins bleiben unbeeinflusst
|
||||
assert mgr.get("com.hms.fx.example_passthrough").state is LifecycleState.VALIDATED
|
||||
|
||||
|
||||
def test_discover_missing_dir_reports_error(tmp_path: Path) -> None:
|
||||
mgr = PluginLifecycleManager()
|
||||
errors = mgr.discover(tmp_path / "gibts-nicht")
|
||||
assert errors and "fehlt" in errors[0]
|
||||
|
||||
|
||||
# ---------- Übergänge (§14.5) ----------
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def manager_with_plugin(tmp_path: Path) -> PluginLifecycleManager:
|
||||
mgr = PluginLifecycleManager()
|
||||
mgr.discover(_valid_plugin_dir(tmp_path))
|
||||
return mgr
|
||||
|
||||
|
||||
def _advance_to(
|
||||
mgr: PluginLifecycleManager, pid: str, *states: LifecycleState
|
||||
) -> None:
|
||||
"""Führt ein Plugin schrittweise durch die angegebenen Zustände."""
|
||||
for state in states:
|
||||
mgr.advance(pid, state)
|
||||
|
||||
|
||||
_ACTIVE_CHAIN = (
|
||||
LifecycleState.INSTALLED,
|
||||
LifecycleState.ENABLED,
|
||||
LifecycleState.COMPILED,
|
||||
LifecycleState.ACTIVE,
|
||||
)
|
||||
|
||||
|
||||
def test_full_lifecycle_happy_path(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""discovered→validated→installed→enabled→compiled→active (§14.5)."""
|
||||
mgr = manager_with_plugin
|
||||
pid = "com.hms.fx.example_passthrough"
|
||||
_advance_to(mgr, pid, *_ACTIVE_CHAIN)
|
||||
record = mgr.get(pid)
|
||||
assert record.state is LifecycleState.ACTIVE
|
||||
assert record.last_error is None
|
||||
assert mgr.active_plugins() == [record]
|
||||
|
||||
|
||||
def test_skip_transition_rejected(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""Sprünge im Graph sind Fehler: validated → active illegal."""
|
||||
mgr = manager_with_plugin
|
||||
with pytest.raises(InvalidTransitionError, match="nicht erlaubt"):
|
||||
mgr.advance("com.hms.fx.example_passthrough", LifecycleState.ACTIVE)
|
||||
|
||||
|
||||
def test_backward_transition_rejected(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""Rücksprünge sind ebenfalls illegal (active → enabled)."""
|
||||
mgr = manager_with_plugin
|
||||
pid = "com.hms.fx.example_passthrough"
|
||||
_advance_to(mgr, pid, *_ACTIVE_CHAIN)
|
||||
with pytest.raises(InvalidTransitionError):
|
||||
mgr.advance(pid, LifecycleState.ENABLED)
|
||||
|
||||
|
||||
def test_disable_and_reenable_cycle(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""active → disabled → enabled: Live-Betrieb bleibt steuerbar (§17.7)."""
|
||||
mgr = manager_with_plugin
|
||||
pid = "com.hms.fx.example_passthrough"
|
||||
_advance_to(mgr, pid, *_ACTIVE_CHAIN)
|
||||
mgr.advance(pid, LifecycleState.DISABLED)
|
||||
mgr.advance(pid, LifecycleState.ENABLED) # Re-Enable: Rücksprung in Betrieb
|
||||
assert mgr.get(pid).state is LifecycleState.ENABLED
|
||||
|
||||
|
||||
def test_unknown_plugin_rejected() -> None:
|
||||
mgr = PluginLifecycleManager()
|
||||
with pytest.raises(KeyError):
|
||||
mgr.advance("gibts-nicht", LifecycleState.INSTALLED)
|
||||
|
||||
|
||||
def test_quarantine_from_active_documents_reason(
|
||||
manager_with_plugin: PluginLifecycleManager,
|
||||
) -> None:
|
||||
"""Fehlerfall im Betrieb: Quarantäne mit Ursache, Projekt bleibt
|
||||
nutzbar (§3.5, §12.2 Bypass)."""
|
||||
mgr = manager_with_plugin
|
||||
pid = "com.hms.fx.gaussian_blur"
|
||||
_advance_to(mgr, pid, *_ACTIVE_CHAIN)
|
||||
mgr.quarantine(pid, "shader compile failed")
|
||||
record = mgr.get(pid)
|
||||
assert record.state is LifecycleState.QUARANTINED
|
||||
assert record.last_error == "shader compile failed"
|
||||
assert mgr.by_state(LifecycleState.QUARANTINED) == [record]
|
||||
# quarantined hat keine Folgezustände: Neustart nur über Neuinstallation
|
||||
with pytest.raises(InvalidTransitionError):
|
||||
mgr.advance(pid, LifecycleState.ENABLED)
|
||||
|
||||
|
||||
# ---------- Show-Lock (§26.3) ----------
|
||||
|
||||
|
||||
def test_show_lock_blocks_installation(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""§26.3: Plugininstallation im Show-Lock gesperrt."""
|
||||
mgr = manager_with_plugin
|
||||
mgr.set_show_lock(True)
|
||||
with pytest.raises(InvalidTransitionError, match="Show-Lock"):
|
||||
mgr.advance("com.hms.fx.example_passthrough", LifecycleState.INSTALLED)
|
||||
|
||||
|
||||
def test_show_lock_allows_enable_of_installed(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""§17.7/§26.3: Bereits installiertes Plugin darf im Live-Betrieb
|
||||
aktiviert werden (Layerparameter und Livefunktionen)."""
|
||||
mgr = manager_with_plugin
|
||||
pid = "com.hms.fx.example_passthrough"
|
||||
mgr.advance(pid, LifecycleState.INSTALLED)
|
||||
mgr.set_show_lock(True)
|
||||
_advance_to(
|
||||
mgr,
|
||||
pid,
|
||||
LifecycleState.ENABLED,
|
||||
LifecycleState.COMPILED,
|
||||
LifecycleState.ACTIVE,
|
||||
)
|
||||
assert mgr.get(pid).state is LifecycleState.ACTIVE
|
||||
|
||||
|
||||
def test_show_lock_blocks_version_update(tmp_path: Path) -> None:
|
||||
"""§26.3: Versionswechsel (= Update) im Show-Lock gesperrt."""
|
||||
plugin_dir = _valid_plugin_dir(tmp_path)
|
||||
mgr = PluginLifecycleManager()
|
||||
mgr.discover(plugin_dir)
|
||||
mgr.set_show_lock(True)
|
||||
# gleiche Plugin-ID mit neuer Version einschleusen
|
||||
manifest_path = plugin_dir / "com.hms.fx.example_passthrough" / "plugin.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["version"] = "2.0.0"
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
with pytest.raises(InvalidTransitionError, match="Versionswechsel"):
|
||||
mgr.discover(plugin_dir)
|
||||
|
||||
|
||||
def test_version_update_without_lock_starts_new_cycle(tmp_path: Path) -> None:
|
||||
"""Update ohne Show-Lock: Plugin startet einen neuen Zyklus in DISCOVERED."""
|
||||
plugin_dir = _valid_plugin_dir(tmp_path)
|
||||
mgr = PluginLifecycleManager()
|
||||
mgr.discover(plugin_dir)
|
||||
mgr.advance("com.hms.fx.example_passthrough", LifecycleState.INSTALLED)
|
||||
manifest_path = plugin_dir / "com.hms.fx.example_passthrough" / "plugin.json"
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
manifest["version"] = "2.0.0"
|
||||
manifest_path.write_text(json.dumps(manifest), encoding="utf-8")
|
||||
mgr.discover(plugin_dir) # Re-Scan erkennt neue Version
|
||||
record = mgr.get("com.hms.fx.example_passthrough")
|
||||
assert record.version == "2.0.0"
|
||||
assert record.state is LifecycleState.VALIDATED # neuer Zyklus
|
||||
|
||||
|
||||
# ---------- Backend-Kompatibilität (§14.5, §12.6) ----------
|
||||
|
||||
|
||||
def test_compatible_backends_intersection(manager_with_plugin: PluginLifecycleManager) -> None:
|
||||
"""Nur Backends mit Entrypoint UND supported_backends gelten (§12.6)."""
|
||||
mgr = manager_with_plugin
|
||||
backends = mgr.compatible_backends("com.hms.fx.example_passthrough")
|
||||
assert backends == frozenset({"d3d11", "gl", "gles"})
|
||||
assert mgr.compatible_backends("gibts-nicht") == frozenset()
|
||||
Reference in New Issue
Block a user