Summary
load_config_defaults only reads defaults from top-level schema properties. An object property ("type": "object" with its own properties) has no top-level "default" key, so it is skipped entirely — along with every default nested inside it.
Across the 44 first-party plugins installed here, that drops 2,386 schema defaults across 37 plugins. soccer-scoreboard loses 539 of its 565.
The consequence is that check_plugin.py — the visual regression tool, and what CI runs — renders most plugins with a config that does not resemble a real install, while render_plugin_matrix says it does:
# Start from config_schema.json defaults so the plugin behaves like a real# install; explicit caller config still wins over a schema default.config= {"enabled": True, **load_config_defaults(plugin_dir), **(configor {})}The code
src/plugin_system/testing/loading.py:36-47:
defload_config_defaults(plugin_dir) ->Dict[str, Any]:
"""Extract default values from a plugin's config_schema.json (empty if none)."""
...
defaults: Dict[str, Any] = {}
forkey, propinschema.get('properties', {}).items():
ifisinstance(prop, dict) and'default'inprop:
defaults[key] =prop['default']
returndefaultsNo recursion into prop['properties'].
Demonstration
hockey-scoreboard's schema defines nhl.enabled: true and nhl.display_modes.{live,recent,upcoming}: true. What the harness actually loads:
$ python3 -c "
from src.plugin_system.testing.loading import load_config_defaults
d = load_config_defaults('plugin-repos/hockey-scoreboard')
print('nhl.enabled =', d.get('nhl',{}).get('enabled'))
print('nhl.display_modes =', d.get('nhl',{}).get('display_modes'))"
nhl.enabled = None
nhl.display_modes = None
Same for lacrosse-scoreboard. The plugins then fall back to whatever their internal defaults happen to be, which is what the harness ends up testing.
Scale
Defaults kept (top-level) vs dropped (nested), first-party plugins only:
| plugin | kept | dropped |
|---|
| soccer-scoreboard | 26 | 539 |
| baseball-scoreboard | 9 | 251 |
| basketball-scoreboard | 9 | 246 |
| hockey-scoreboard | 6 | 207 |
| lacrosse-scoreboard | 6 | 155 |
| football-scoreboard | 10 | 152 |
| afl-scoreboard | 34 | 92 |
| nrl-scoreboard | 33 | 92 |
| ufc-scoreboard | 9 | 78 |
| f1-scoreboard | 6 | 64 |
| ledmatrix-leaderboard | 3 | 52 |
| stock-news | 1 | 50 |
| ledmatrix-flights | 51 | 44 |
| masters-tournament | 10 | 41 |
| ledmatrix-stocks | 2 | 39 |
| odds-ticker | 1 | 38 |
| birdnet-go | 2 | 37 |
| news | 1 | 29 |
| cricket-scoreboard | 22 | 28 |
| … 18 more | | |
| total | | 2386 across 37 of 44 plugins |
The plugins that lose the most are exactly the ones whose config is organised by league or by UI section — sports, customization.*, display_options.* — which is to say most of the fleet.
Why it matters beyond tidiness
- Goldens encode the wrong baseline. Committed golden images were captured under this partial config, so they pin behaviour no user will see.
- Whole features go untested. A plugin whose
display_options.* all sit nested is rendered with those options unset at every size, in every CI run. - It produces misleading secondary signals. Comparing each plugin's manifest
display_modes against what it exposes under harness defaults shows six sports plugins "missing" modes — baseball-scoreboard exposes 3 of 9, basketball-scoreboard 3 of 12, hockey-scoreboard 2 of 9. Those look like manifest bugs and are not; the league configs simply never arrived. I nearly filed them as plugin defects.
Suggested fix
Recurse, preserving nesting:
def_defaults(props):
out= {}
forkey, propin (propsor {}).items():
ifnotisinstance(prop, dict):
continueifprop.get('type') =='object'and'properties'inprop:
nested=_defaults(prop['properties'])
ifnested:
out[key] =nestedelif'default'inprop:
out[key] =prop['default']
returnoutwith a merge that lets caller config override at leaf level rather than replacing whole subtrees — otherwise -c '{"nhl": {"enabled": true}}' would wipe the rest of the nhl defaults.
That is a behaviour change: goldens will shift for the 37 affected plugins, and some may start failing for real reasons. That is the point, but it wants a deliberate --update-golden pass and a look at what newly breaks rather than a quiet merge.
A regression test is easy: assert load_config_defaults on a schema with a nested object returns the nested defaults.
Environment
LEDMatrix v3.3.0-4-g0730d952, 256x64 rig, 44 first-party plugins installed. Found while auditing manifest display_modes against the modes plugins actually expose.
Summary
load_config_defaultsonly reads defaults from top-level schema properties. An object property ("type": "object"with its ownproperties) has no top-level"default"key, so it is skipped entirely — along with every default nested inside it.Across the 44 first-party plugins installed here, that drops 2,386 schema defaults across 37 plugins.
soccer-scoreboardloses 539 of its 565.The consequence is that
check_plugin.py— the visual regression tool, and what CI runs — renders most plugins with a config that does not resemble a real install, whilerender_plugin_matrixsays it does:The code
src/plugin_system/testing/loading.py:36-47:No recursion into
prop['properties'].Demonstration
hockey-scoreboard's schema definesnhl.enabled: trueandnhl.display_modes.{live,recent,upcoming}: true. What the harness actually loads:Same for
lacrosse-scoreboard. The plugins then fall back to whatever their internal defaults happen to be, which is what the harness ends up testing.Scale
Defaults kept (top-level) vs dropped (nested), first-party plugins only:
The plugins that lose the most are exactly the ones whose config is organised by league or by UI section — sports,
customization.*,display_options.*— which is to say most of the fleet.Why it matters beyond tidiness
display_options.*all sit nested is rendered with those options unset at every size, in every CI run.display_modesagainst what it exposes under harness defaults shows six sports plugins "missing" modes —baseball-scoreboardexposes 3 of 9,basketball-scoreboard3 of 12,hockey-scoreboard2 of 9. Those look like manifest bugs and are not; the league configs simply never arrived. I nearly filed them as plugin defects.Suggested fix
Recurse, preserving nesting:
with a merge that lets caller config override at leaf level rather than replacing whole subtrees — otherwise
-c '{"nhl": {"enabled": true}}'would wipe the rest of thenhldefaults.That is a behaviour change: goldens will shift for the 37 affected plugins, and some may start failing for real reasons. That is the point, but it wants a deliberate
--update-goldenpass and a look at what newly breaks rather than a quiet merge.A regression test is easy: assert
load_config_defaultson a schema with a nested object returns the nested defaults.Environment
LEDMatrix
v3.3.0-4-g0730d952, 256x64 rig, 44 first-party plugins installed. Found while auditing manifestdisplay_modesagainst the modes plugins actually expose.