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>
91 lines
3.2 KiB
Python
91 lines
3.2 KiB
Python
"""GET /api/v3/health: the plugin count is real, and a failed check is logged.
|
|
|
|
The plugin check counted ``plugin_manager.get_available_plugins()``, which
|
|
PluginManager does not have; a hasattr guard turned that into a permanent 0.
|
|
Each check that fails answers "see logs for details", so it has to log.
|
|
"""
|
|
|
|
import logging
|
|
import sys
|
|
from pathlib import Path
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
|
|
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401,E402
|
|
|
|
URL = "/api/v3/health"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _no_systemctl(monkeypatch):
|
|
monkeypatch.setattr("web_interface.blueprints.api_v3.misc._get_display_service_status",
|
|
lambda: {"active": True})
|
|
|
|
|
|
def _checks(client):
|
|
response = client.get(URL)
|
|
assert response.status_code == 200, response.get_json()
|
|
return response.get_json()["data"]["checks"]
|
|
|
|
|
|
def test_plugin_count_is_the_number_of_discovered_plugins(api_v3_client, api_v3_module):
|
|
api_v3_module.api_v3.plugin_catalog.plugin_manifests = {
|
|
"clock": {"id": "clock"}, "weather": {"id": "weather"}, "stocks": {"id": "stocks"},
|
|
}
|
|
|
|
check = _checks(api_v3_client)["plugin_system"]
|
|
|
|
assert check == {"status": "operational", "plugin_count": 3}
|
|
|
|
|
|
def test_plugin_count_discovers_when_nothing_is_discovered_yet(api_v3_client, api_v3_module):
|
|
pm = api_v3_module.api_v3.plugin_catalog
|
|
pm.plugin_manifests = {}
|
|
|
|
def discover():
|
|
pm.plugin_manifests = {"clock": {"id": "clock"}}
|
|
pm.discover_plugins.side_effect = discover
|
|
|
|
assert _checks(api_v3_client)["plugin_system"]["plugin_count"] == 1
|
|
|
|
|
|
def test_a_failed_config_check_is_logged(api_v3_client, api_v3_module, caplog):
|
|
api_v3_module.api_v3.config_manager.load_config.side_effect = OSError("disk gone")
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
check = _checks(api_v3_client)["config_file"]
|
|
|
|
assert check["error"] == "see logs for details"
|
|
logged = [r for r in caplog.records if "config file" in r.getMessage()]
|
|
assert logged and logged[0].exc_info and "disk gone" in str(logged[0].exc_info[1])
|
|
|
|
|
|
def test_a_failed_plugin_check_is_logged(api_v3_client, api_v3_module, caplog, monkeypatch):
|
|
def boom():
|
|
raise RuntimeError("manifests unreadable")
|
|
monkeypatch.setattr("web_interface.blueprints.api_v3.misc._discovered_plugin_manifests", boom)
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
check = _checks(api_v3_client)["plugin_system"]
|
|
|
|
assert check["status"] == "error"
|
|
logged = [r for r in caplog.records if "count plugins" in r.getMessage()]
|
|
assert logged and logged[0].exc_info
|
|
|
|
|
|
def test_a_failed_hardware_check_is_logged(api_v3_client, api_v3_module, caplog, monkeypatch):
|
|
def getmtime(_path):
|
|
raise PermissionError("denied")
|
|
fake_os = SimpleNamespace(path=SimpleNamespace(exists=lambda _p: True, getmtime=getmtime))
|
|
monkeypatch.setattr("web_interface.blueprints.api_v3.misc.os", fake_os)
|
|
|
|
with caplog.at_level(logging.WARNING):
|
|
check = _checks(api_v3_client)["hardware"]
|
|
|
|
assert check["status"] == "unknown"
|
|
logged = [r for r in caplog.records if "snapshot" in r.getMessage()]
|
|
assert logged and logged[0].exc_info
|