mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-06 15:25:08 +00:00
* fix(vegas): a display duration that is not a number no longer cancels a static pause The Vegas static pause compared plugin.get_display_duration() with the clock. clock-simple, calendar and countdown return their display_duration setting straight from config.json, so a value saved as "20" or null reached that comparison as a string or None. The TypeError went to the pause's broad except, which ended the pause: the plugin flashed up and the scroll went straight on, at every one of its turns. inf held the pause until something interrupted it, and NaN, False, 0 or a negative number ended it at once. The pause now reads the duration the way the rotation has since #739, with the same helper, then the rotation's fallbacks: 30 s for anything that is not a number or a get_display_duration() that raises, 15 s for a number at or below zero. Logged once per plugin. test_vegas_static_mode.py's pauses used 0 to mean "no wait"; they now use 0.01. The helper moves from display_controller (_finite_seconds) to base_plugin (finite_seconds), unchanged: the coordinator cannot import from display_controller, which imports src.vegas_mode at module level, and a new src module would turn ledmatrix-plugins' min-core table check red until it was listed. base_plugin is already loaded whenever either one is. Tests: test/test_vegas_static_pause_duration.py, on a fake clock, including TestSameAsTheRotation, which runs every value through both the pause and the rotation's _get_display_duration/_resolve_durations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(web): a store install asks for a restart by the id it installed as POST /plugins/install decides restart_required from whether config.json already enables the plugin: the display loads a plugin when its enabled flag changes, so one already enabled (a reinstall, or a config carried over) keeps running the copy it loaded until a restart. The route read that flag under the registry id. Weather, Music, Stocks and Leaderboard install under the id their manifests declare (weather -> ledmatrix-weather), which is the config section's id, so reinstalling an enabled one never reported that a restart was needed. Both the queued and the direct path now look up the installed id once (#746's _installed_plugin_id) and use it for the plugin_id they answer with and for the enabled check. Tests: test/test_api_v3_install_restart_installed_id.py, through the Flask test client, both paths. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(vegas): type the static pause fallback on its own (mypy ratchet) _static_pause_duration assigned the fallback to `seconds`, which the except branch typed as float before finite_seconds() reassigned it as float | None. A separate `fallback` keeps both types exact; behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
124 lines
4.6 KiB
Python
124 lines
4.6 KiB
Python
"""POST /plugins/install asks for a restart by the id the plugin installed as.
|
|
|
|
A store install needs a display restart when config.json already enables the
|
|
plugin (a reinstall, or a config carried over): the display loads a plugin
|
|
when its ``enabled`` flag changes, and this flag did not. The route read the
|
|
flag under the registry id it was given. An aliased entry installs under
|
|
another id -- ``weather`` installs a directory whose manifest declares
|
|
``ledmatrix-weather``, and its config section is ``ledmatrix-weather`` -- so
|
|
reinstalling an enabled Weather never reported that a restart was needed,
|
|
and the display kept running the old copy.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401
|
|
|
|
INSTALL = "/api/v3/plugins/install"
|
|
|
|
|
|
@pytest.fixture
|
|
def store(api_v3_module, tmp_path):
|
|
"""The store installs registry entry ``weather`` as ``installed_id``."""
|
|
manager = api_v3_module.api_v3.plugin_store_manager
|
|
manager.install_plugin.return_value = True
|
|
manager.get_registry_info.return_value = None
|
|
manager._find_plugin_path.return_value = None
|
|
|
|
def installs_as(installed_id):
|
|
path = tmp_path / installed_id
|
|
path.mkdir()
|
|
(path / "manifest.json").write_text(json.dumps({"id": installed_id}),
|
|
encoding="utf-8")
|
|
manager._find_plugin_path.side_effect = (
|
|
lambda pid: path if pid == "weather" else None)
|
|
|
|
manager.installs_as = installs_as
|
|
return manager
|
|
|
|
|
|
@pytest.fixture
|
|
def config(api_v3_module):
|
|
"""config.json with an ``enabled`` flag for each plugin id given."""
|
|
def sections(enabled):
|
|
api_v3_module.api_v3.config_manager.load_config.return_value = {
|
|
plugin_id: {"enabled": flag} for plugin_id, flag in enabled.items()}
|
|
return sections
|
|
|
|
|
|
@pytest.fixture
|
|
def queued(api_v3_module):
|
|
queue = MagicMock()
|
|
|
|
def enqueue(operation_type, plugin_id, operation_callback=None):
|
|
queue.callback_result = operation_callback(MagicMock())
|
|
return "op-1"
|
|
|
|
queue.enqueue_operation.side_effect = enqueue
|
|
api_v3_module.api_v3.operation_queue = queue
|
|
return queue
|
|
|
|
|
|
def _direct(client):
|
|
return client.post(INSTALL, json={"plugin_id": "weather"}).get_json()
|
|
|
|
|
|
def _queued(client, queue):
|
|
client.post(INSTALL, json={"plugin_id": "weather"})
|
|
return queue.callback_result
|
|
|
|
|
|
class TestDirectInstall:
|
|
def test_an_aliased_install_enabled_under_its_installed_id_asks_for_a_restart(
|
|
self, api_v3_client, store, config):
|
|
store.installs_as("ledmatrix-weather")
|
|
config({"ledmatrix-weather": True})
|
|
body = _direct(api_v3_client)
|
|
assert body["status"] == "success"
|
|
assert body["restart_required"] is True
|
|
assert body["restart_message"]
|
|
|
|
def test_an_enabled_section_under_the_registry_id_alone_does_not(
|
|
self, api_v3_client, store, config):
|
|
"""The display knows the plugin as ledmatrix-weather; nothing runs
|
|
under a section called weather."""
|
|
store.installs_as("ledmatrix-weather")
|
|
config({"weather": True})
|
|
assert _direct(api_v3_client)["restart_required"] is False
|
|
|
|
def test_an_aliased_install_that_is_not_enabled_needs_no_restart(
|
|
self, api_v3_client, store, config):
|
|
store.installs_as("ledmatrix-weather")
|
|
config({"ledmatrix-weather": False})
|
|
assert _direct(api_v3_client)["restart_required"] is False
|
|
|
|
def test_an_install_under_its_own_id_is_unchanged(self, api_v3_client, store, config):
|
|
store.installs_as("weather")
|
|
config({"weather": True})
|
|
assert _direct(api_v3_client)["restart_required"] is True
|
|
|
|
def test_an_install_that_cannot_be_found_uses_the_requested_id(
|
|
self, api_v3_client, store, config):
|
|
config({"weather": True})
|
|
assert _direct(api_v3_client)["restart_required"] is True
|
|
|
|
|
|
class TestQueuedInstall:
|
|
def test_an_aliased_install_enabled_under_its_installed_id_asks_for_a_restart(
|
|
self, api_v3_client, store, config, queued):
|
|
store.installs_as("ledmatrix-weather")
|
|
config({"ledmatrix-weather": True})
|
|
result = _queued(api_v3_client, queued)
|
|
assert result["success"] is True
|
|
assert result["restart_required"] is True
|
|
assert result["restart_message"]
|
|
|
|
def test_an_enabled_section_under_the_registry_id_alone_does_not(
|
|
self, api_v3_client, store, config, queued):
|
|
store.installs_as("ledmatrix-weather")
|
|
config({"weather": True})
|
|
assert _queued(api_v3_client, queued)["restart_required"] is False
|