mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 06:15:09 +00:00
The web process built its own PluginManager and loaded plugins into itself: store installs and updates loaded or reloaded a web-side copy, and config saves and enable/disable called on_config_change, on_enable and on_disable on it. None of that reached the panel, and /plugins/installed reported runtime state from those copies. - Add PluginCatalog (src/plugin_system/plugin_catalog.py): manifests, directories, display modes, installed version, schema and config reads, with no way to run a plugin. app.py and both blueprints use it; the plugin_manager blueprint attribute is gone. - Remove every lifecycle call from the web routes. Config changes already reach the display through ConfigService (on_config_change) and the enabled-set reconcile. - Health and metrics readers move to api_v3.health_tracker / resource_monitor. /plugins/installed reports loaded/state/error_info as null (the display does not publish them) and enabled by the display's rule. - Store install, update and uninstall answer restart_required when the running display will not pick the change up by itself (display_restart_required). The restart banner follows the flag via window.noteRestartRequired instead of the /config/main URL heuristic; /config/main now sends restart_required: true. - The one remaining in-process import of plugin code (Starlark helper modules, oauth_flow action scripts) goes through _import_plugin_code_in_web_process() until a web-entry contract. - /plugins/installed reports vegas_participation (from #682) from the user's setting or the manifest, with vegas_participation_source; when only the plugin's code decides it, null with source 'runtime', since the web process no longer has plugin instances to ask. - Check & Update All keeps its restart flags when the final list refresh fails, and asks for a restart when an enabled plugin's first request got no answer and the re-sent one found it up to date. 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',
|
|
'plugin_state_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()
|