mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-13 02:26:37 +00:00
fix(config): use the shared core-key list in the last three private copies
StartupValidator warned "Plugin 'auto_update' is enabled but not found" on every display start with auto-update or a dim schedule on; the reserved plugin-id check missed auto_update, sync, location and the rest; and ConfigManager's (uncalled) orphan cleanup would have deleted display, schedule and auto_update. All three now read src/core_config_keys.py, which also gains CORE_SECRETS_KEYS for the github/youtube secrets sections. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
"""Every "is this top-level key a plugin?" decision uses src/core_config_keys.py.
|
||||
|
||||
#589 moved plugin-state reconciliation onto the shared list, but three private
|
||||
copies stayed behind and did not know about auto_update, dim_schedule, sync,
|
||||
location, target_fps, ...:
|
||||
|
||||
- StartupValidator warned "Plugin 'auto_update' is enabled but not found in
|
||||
plugins directory" on every display start with auto-update or a dim
|
||||
schedule switched on;
|
||||
- SchemaManager.detect_config_key_collisions let a plugin id collide with
|
||||
those sections silently;
|
||||
- ConfigManager.cleanup_orphaned_plugin_configs deleted display, schedule,
|
||||
auto_update, ... as orphans, and validate_all_plugin_configs validated core
|
||||
sections as plugins.
|
||||
"""
|
||||
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.config_manager import ConfigManager
|
||||
from src.core_config_keys import CORE_CONFIG_KEYS, CORE_SECRETS_KEYS
|
||||
from src.plugin_system.schema_manager import SchemaManager
|
||||
from src.startup_validator import StartupValidator
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
class TestStartupValidator:
|
||||
def test_enabled_core_sections_are_not_missing_plugins(self):
|
||||
config = {
|
||||
'display': {'hardware': {}},
|
||||
'auto_update': {'enabled': True},
|
||||
'dim_schedule': {'enabled': True},
|
||||
'schedule': {'enabled': True},
|
||||
'gone-plugin': {'enabled': True},
|
||||
}
|
||||
cm = MagicMock()
|
||||
cm.get_config.return_value = config
|
||||
pm = MagicMock()
|
||||
pm.discover_plugins.return_value = []
|
||||
validator = StartupValidator(cm, plugin_manager=pm, cache_manager=MagicMock())
|
||||
validator.warnings = []
|
||||
validator.errors = []
|
||||
validator._validate_plugins()
|
||||
missing = [w for w in validator.warnings if 'not found in plugins directory' in w]
|
||||
assert missing == ["Plugin 'gone-plugin' is enabled but not found in plugins directory"]
|
||||
|
||||
|
||||
class TestReservedPluginIds:
|
||||
@pytest.mark.parametrize('key', sorted(CORE_CONFIG_KEYS))
|
||||
def test_every_core_key_is_reserved(self, key):
|
||||
collisions = SchemaManager().detect_config_key_collisions([key])
|
||||
assert [c['type'] for c in collisions] == ['reserved_key_collision']
|
||||
|
||||
def test_an_ordinary_plugin_id_is_not(self):
|
||||
assert SchemaManager().detect_config_key_collisions(['clock-simple']) == []
|
||||
|
||||
|
||||
class TestConfigManagerHelpers:
|
||||
@pytest.fixture
|
||||
def manager(self, tmp_path):
|
||||
config = {key: {'enabled': True} for key in ('display', 'schedule', 'auto_update',
|
||||
'dim_schedule', 'sync', 'location')}
|
||||
config.update({'timezone': 'UTC', 'installed': {'enabled': True},
|
||||
'orphan': {'enabled': True}})
|
||||
secrets = {'github': {'token': 't'}, 'youtube': {'api_key': 'k'},
|
||||
'installed': {'api_key': 'a'}, 'orphan': {'api_key': 'o'}}
|
||||
(tmp_path / 'config.json').write_text(json.dumps(config))
|
||||
(tmp_path / 'secrets.json').write_text(json.dumps(secrets))
|
||||
manager = ConfigManager(config_path=str(tmp_path / 'config.json'),
|
||||
secrets_path=str(tmp_path / 'secrets.json'))
|
||||
manager.template_path = str(tmp_path / 'no-template.json')
|
||||
return manager
|
||||
|
||||
def test_cleanup_removes_only_real_orphans(self, manager, tmp_path):
|
||||
removed = manager.cleanup_orphaned_plugin_configs(['installed'])
|
||||
assert removed == ['orphan']
|
||||
config = json.loads((tmp_path / 'config.json').read_text())
|
||||
assert {'display', 'schedule', 'auto_update', 'dim_schedule', 'sync',
|
||||
'location', 'timezone', 'installed'} == set(config)
|
||||
secrets = json.loads((tmp_path / 'secrets.json').read_text())
|
||||
assert set(secrets) == {'github', 'youtube', 'installed'}
|
||||
|
||||
def test_validate_all_skips_core_sections(self, manager):
|
||||
schema_manager = MagicMock()
|
||||
schema_manager.load_schema.return_value = None
|
||||
results = manager.validate_all_plugin_configs(schema_manager)
|
||||
assert set(results) == {'installed', 'orphan'}
|
||||
|
||||
|
||||
def test_core_secrets_keys_are_not_core_config_keys():
|
||||
assert not (CORE_SECRETS_KEYS & CORE_CONFIG_KEYS)
|
||||
|
||||
|
||||
@pytest.mark.parametrize('relpath', [
|
||||
'src/startup_validator.py',
|
||||
'src/config_manager.py',
|
||||
'src/plugin_system/schema_manager.py',
|
||||
])
|
||||
def test_no_private_copy_of_the_list_remains(relpath):
|
||||
"""The old copies all started with this literal; a new copy likely would too."""
|
||||
source = (REPO / relpath).read_text(encoding='utf-8')
|
||||
assert not re.search(r"""\[\s*['"]display['"]\s*,\s*['"]schedule['"]""", source), \
|
||||
f'{relpath} has a private core-key list again; use src/core_config_keys.py'
|
||||
Reference in New Issue
Block a user