mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 22:35:08 +00:00
Stage 2 of the web plugin catalog, after #688. - The display publishes a plugin runtime snapshot (plugin_runtime.py) to the shared cache: per plugin loaded, lifecycle state, a short redacted error summary, the version it loaded and when, plus published_at / stale_after / running. Written on change (throttled to 10 s; the RUNNING/ENABLED flip of an ordinary update is not a change) and once a minute otherwise; cleanup() publishes running: false. - The web reads it back and restores loaded / state / error_info in /api/v3/plugins/installed (plus loaded_version, loaded_at and data.runtime). Only a live snapshot counts; stale, stopped or missing answers null and says which. - data/plugin_state.json is retired: every reader and writer moved to config + disk (desired) or the snapshot (observed). Nothing in it was non-derivable, so nothing is migrated and an existing file is left unread. The web-side PluginStateManager (state_manager.py) is removed; the display's plugin_state.PluginStateManager is the only state machine. - StateReconciliation compares config + disk with the snapshot, reporting enabled-but-not-loaded and older-version-loaded as no_action findings. - Backups list installed manifests with enabled from config.json. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
3.2 KiB
Python
95 lines
3.2 KiB
Python
"""
|
|
Shared scaffolding for api_v3 blueprint tests.
|
|
|
|
Not a test module (the leading underscore keeps pytest from collecting
|
|
it). It is the pytest-fixture equivalent of ``_make_client()`` in
|
|
test_uninstall_and_reconcile_endpoint.py, which is unittest-style and
|
|
requires ``self.addCleanup``.
|
|
|
|
The api_v3 blueprint keeps its managers as attributes on a module-level
|
|
singleton, not in Flask app state, so replacing them with mocks leaks
|
|
into every later test that imports api_v3 unless the originals are put
|
|
back. ``api_v3_client`` snapshots and restores them around each test.
|
|
"""
|
|
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
from flask import Flask
|
|
|
|
|
|
# Every manager attribute the blueprint reads. Anything missing here keeps
|
|
# whatever a previously-run test left on the singleton.
|
|
API_V3_MANAGER_ATTRS = (
|
|
'config_manager', 'plugin_catalog', 'plugin_store_manager',
|
|
'saved_repositories_manager', 'schema_manager',
|
|
'operation_queue', 'operation_history', 'cache_manager',
|
|
'health_tracker', 'resource_monitor',
|
|
)
|
|
|
|
_SENTINEL = object()
|
|
|
|
|
|
def mock_plugin_catalog():
|
|
"""A MagicMock shaped like PluginCatalog, and only like it.
|
|
|
|
``spec`` makes anything a PluginCatalog lacks raise AttributeError --
|
|
``get_plugin``, ``load_plugin``, ``plugins`` -- so a route that reached
|
|
for a plugin instance in the web process fails the test that drives it
|
|
instead of quietly calling a mock. The instance attributes the spec
|
|
cannot see are set explicitly.
|
|
"""
|
|
from src.plugin_system.plugin_catalog import PluginCatalog
|
|
catalog = MagicMock(spec=PluginCatalog)
|
|
for name in ('plugins_dir', 'config_manager', 'schema_manager',
|
|
'plugin_manifests', 'plugin_directories'):
|
|
setattr(catalog, name, MagicMock())
|
|
return catalog
|
|
|
|
|
|
def build_app(blueprint):
|
|
app = Flask(__name__)
|
|
app.config['TESTING'] = True
|
|
app.config['SECRET_KEY'] = 'test'
|
|
app.register_blueprint(blueprint, url_prefix='/api/v3')
|
|
return app
|
|
|
|
|
|
@pytest.fixture
|
|
def api_v3_module():
|
|
"""The api_v3 module with every manager replaced by a MagicMock.
|
|
|
|
Restores the original attributes afterwards. Tests point individual
|
|
managers at real objects (a ConfigManager over tmp_path, say) or set
|
|
them to None to exercise the not-initialized branches.
|
|
"""
|
|
from web_interface.blueprints import api_v3 as module
|
|
|
|
originals = {
|
|
name: getattr(module.api_v3, name, _SENTINEL)
|
|
for name in API_V3_MANAGER_ATTRS
|
|
}
|
|
for name in API_V3_MANAGER_ATTRS:
|
|
setattr(module.api_v3, name,
|
|
mock_plugin_catalog() if name == 'plugin_catalog' else MagicMock())
|
|
# Default to the direct path; queue tests opt in explicitly.
|
|
module.api_v3.operation_queue = None
|
|
|
|
yield module
|
|
|
|
for name, original in originals.items():
|
|
if original is _SENTINEL:
|
|
if hasattr(module.api_v3, name):
|
|
try:
|
|
delattr(module.api_v3, name)
|
|
except AttributeError:
|
|
pass
|
|
else:
|
|
setattr(module.api_v3, name, original)
|
|
|
|
|
|
@pytest.fixture
|
|
def api_v3_client(api_v3_module):
|
|
"""Flask test client wired to the mocked blueprint."""
|
|
return build_app(api_v3_module.api_v3).test_client()
|