mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
* refactor: remove the skin system Skins never rendered with the current scoreboard plugins: the only hook was SportsCore._render_game in src/base_classes, which no plugin builds on, so the UI and store already treated them as unsupported. The owner decided on 2026-09-23 to remove them outright. Removed src/skin_system/ (runtime, base class, fixtures), skins/, scripts/validate_skin.py and their tests; the store's "type": "skin" installer, uninstaller and hide/refuse filters (the official registry lists no skins); SchemaManager.inject_skin_selector; and GET /api/v3/skins. Stored skin/skin_options config values are handled in the next commit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(config): drop retired skin/skin_options keys instead of validating them A config.json written while the skin system existed can carry skin and skin_options in any plugin section, and most plugin schemas set additionalProperties: false. They are no longer core plugin properties; RETIRED_PLUGIN_KEYS in schema_manager lists them and drop_retired_plugin_keys removes them (unless the plugin's own schema declares the name) in prepare_plugin_config, which loading, hot reload, GET /plugins/config and both web saves already share, and in validate_config_against_schema for callers that validate a raw section. POST /plugins/config and /config/main also drop them from the stored section they merge into, so they leave config.json on the next save. Tests cover the load path (real PluginManager.load_plugin: no schema warning, not degraded), raw and prepared validation, validate_all_plugin_configs, and the JSON, form and /config/main saves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * refactor: remove the unused src/base_classes package No scoreboard plugin builds on src.base_classes: the nine monorepo scoreboards ship their own sports.py and share code through src/common (docs/SPORTS_UNIFICATION.md), and none of the third-party registry plugins imports it. The one import anywhere, baseball-scoreboard's rankings_manager.py, is a lazy import of ESPNDataSource in a class nothing instantiates. Removed the package and the eight test files that only tested it (test_api_extractors, test_data_sources, test_sports_base_characterization, test_sports_capabilities, test_sports_core_promotions, test_sports_logo_cache_bounded, test_sports_modes_promotions, test_sports_odds_fanout). test_common_is_hardware_free no longer lists src.base_classes as a forbidden import, and comments in sports_helpers.py and base_odds_manager.py stop pointing at it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * docs: drop the skin system and src/base_classes from the docs Deletes docs/SKIN_SYSTEM.md and docs/CREATING_SKINS.md and every link to them (docs/README.md, README.md, PLUGIN_DEVELOPMENT_GUIDE.md, the /skins section of REST_API_REFERENCE.md), the skin section of CLAUDE.md and the term in PRODUCT.md. SPORTS_UNIFICATION.md now says src/base_classes was removed and shared code lives in src/common, in the Layering section and the view-model-contract rule. Other docs stop pointing at the removed package. CHANGELOG records both removals under Unreleased. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(store): hide and refuse registry entries that aren't plugins The skin filters went with the skin system, but a custom registry can still list "type": "skin" entries, and installing one as a plugin would unpack it into the plugins directory. PluginStoreManager.is_plugin_entry() (a missing type means plugin) now hides non-plugin entries from the store and custom-registry listings, and install refuses them, in the route with a clear 400 and in _install_plugin_impl for any other caller. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
80 lines
3.1 KiB
Python
80 lines
3.1 KiB
Python
"""Registry entries that aren't plugins are hidden and refused.
|
|
|
|
The official registry no longer lists skins, but a custom registry (added
|
|
from the Plugin Store) can still carry ``"type": "skin"`` entries. With the
|
|
skin system removed, installing one as a plugin would unpack it into the
|
|
plugins directory, so the store hides such entries and refuses them at install.
|
|
"""
|
|
|
|
import json
|
|
from unittest.mock import MagicMock
|
|
|
|
import pytest
|
|
|
|
from src.plugin_system.store_manager import PluginStoreManager
|
|
from test._api_v3_test_helpers import api_v3_client, api_v3_module # noqa: F401
|
|
|
|
PLUGIN = {"id": "clock", "name": "Clock", "repo": "https://github.com/x/clock"}
|
|
UNTYPED = {"id": "news", "name": "News", "repo": "https://github.com/x/news"}
|
|
SKIN = {"id": "retro", "name": "Retro", "type": "skin", "repo": "https://github.com/x/retro"}
|
|
|
|
|
|
@pytest.mark.parametrize("entry,expected", [
|
|
(PLUGIN, True),
|
|
({**PLUGIN, "type": "plugin"}, True),
|
|
({**PLUGIN, "type": None}, True),
|
|
(SKIN, False),
|
|
({**PLUGIN, "type": "theme"}, False),
|
|
(None, False),
|
|
("clock", False),
|
|
])
|
|
def test_is_plugin_entry(entry, expected):
|
|
assert PluginStoreManager.is_plugin_entry(entry) is expected
|
|
|
|
|
|
def test_install_refuses_a_non_plugin_entry(tmp_path):
|
|
store = PluginStoreManager(plugins_dir=str(tmp_path / "plugins"))
|
|
store.get_plugin_info = MagicMock(return_value=dict(SKIN))
|
|
store._install_from_monorepo_zip = MagicMock()
|
|
store._install_via_git = MagicMock()
|
|
assert store._install_plugin_impl("retro") is False
|
|
assert not (tmp_path / "plugins" / "retro").exists()
|
|
store._install_from_monorepo_zip.assert_not_called()
|
|
store._install_via_git.assert_not_called()
|
|
|
|
|
|
@pytest.fixture
|
|
def store(api_v3_module):
|
|
mock = MagicMock()
|
|
mock.is_plugin_entry = PluginStoreManager.is_plugin_entry
|
|
api_v3_module.api_v3.plugin_store_manager = mock
|
|
return mock
|
|
|
|
|
|
def test_custom_registry_listing_hides_non_plugins(api_v3_client, store):
|
|
store.fetch_registry_from_url.return_value = {"plugins": [PLUGIN, SKIN, UNTYPED]}
|
|
resp = api_v3_client.post("/api/v3/plugins/registry-from-url",
|
|
data=json.dumps({"repo_url": "https://github.com/x/registry"}),
|
|
content_type="application/json")
|
|
assert resp.status_code == 200
|
|
assert [p["id"] for p in resp.get_json()["plugins"]] == ["clock", "news"]
|
|
|
|
|
|
def test_store_listing_hides_non_plugins(api_v3_client, store):
|
|
store.search_plugins.return_value = [PLUGIN, SKIN, UNTYPED]
|
|
resp = api_v3_client.get("/api/v3/plugins/store/list")
|
|
assert resp.status_code == 200
|
|
body = resp.get_json()
|
|
listed = body.get("data", body).get("plugins", [])
|
|
assert [p["id"] for p in listed] == ["clock", "news"]
|
|
|
|
|
|
def test_install_route_refuses_a_non_plugin_entry(api_v3_client, store):
|
|
store.get_registry_info.return_value = dict(SKIN)
|
|
resp = api_v3_client.post("/api/v3/plugins/install",
|
|
data=json.dumps({"plugin_id": "retro"}),
|
|
content_type="application/json")
|
|
assert resp.status_code == 400
|
|
assert "not a plugin" in resp.get_json()["message"]
|
|
store.install_plugin.assert_not_called()
|