mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-08-01 16:58:06 +00:00
Align Vegas API bounds with validate(), fix audit config plumbing
Both from review feedback on #423. The web API's accepted ranges disagreed with VegasModeConfig.validate(), which is what actually gates Vegas starting: scroll_speed 1-100 -> 1-200 (a slider value of 150 returned 400) separator_width 0-500 -> 0-128 target_fps 1-200 -> 30-200 buffer_ahead 1-20 -> 1-5 The three loose ones were the dangerous direction: the value saved with a 200, then VegasModeCoordinator.start() failed validation with only a log line, so the ticker silently never ran. The UI already matched validate() in all four cases, so the API was the odd one out. test_vegas_api_bounds_match_validate parses the numeric_fields map out of api_v3 and asserts every bound against validate(), plus that validate() accepts both endpoints and rejects just outside them, so these cannot drift apart again. That test immediately caught a missing upper bound on min_plugin_width, now added — unbounded it would drop every segment and leave a blank ticker. Separately, vegas_audit.py constructed PluginAdapter without the config, so it fell back to VegasModeConfig() defaults and would report trimming and width-budget behaviour that differed from the user's config.json. It now passes the loaded config exactly as the coordinator does. This is the same class of drift the explicit lead_gap and grouping arguments already guard against. Output is unchanged on a rig whose config matches the defaults. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KEZK1P1Q1fu5pcuVrkrCFZ
This commit is contained in:
@@ -398,6 +398,82 @@ class TestStreamGrouping:
|
||||
assert len(sm.get_all_content_for_composition()) == 5
|
||||
|
||||
|
||||
class TestApiBoundsMatchValidate:
|
||||
"""
|
||||
The web API's accepted range for each Vegas setting must agree with
|
||||
VegasModeConfig.validate(), which is what actually gates Vegas starting.
|
||||
|
||||
A looser API bound saves a value with a 200 and then makes
|
||||
VegasModeCoordinator.start() bail out with only a log line, so the ticker
|
||||
silently never runs. A tighter one rejects a legitimate value with a 400.
|
||||
Both happened before this test existed.
|
||||
"""
|
||||
|
||||
# (config key, min, max) as validate() enforces them.
|
||||
EXPECTED = {
|
||||
'scroll_speed': (1, 200),
|
||||
'separator_width': (0, 128),
|
||||
'intra_plugin_gap': (0, 128),
|
||||
'target_fps': (30, 200),
|
||||
'buffer_ahead': (1, 5),
|
||||
'trim_threshold': (0, 254),
|
||||
'content_padding': (0, 128),
|
||||
'min_plugin_width': (0, 512),
|
||||
'plugins_per_cycle': (1, 50),
|
||||
}
|
||||
|
||||
def _api_numeric_fields(self):
|
||||
"""Extract the numeric_fields map from api_v3 without importing Flask."""
|
||||
import ast
|
||||
import pathlib
|
||||
src = pathlib.Path('web_interface/blueprints/api_v3.py').read_text()
|
||||
tree = ast.parse(src)
|
||||
for node in ast.walk(tree):
|
||||
if not isinstance(node, ast.Assign):
|
||||
continue
|
||||
targets = [t.id for t in node.targets if isinstance(t, ast.Name)]
|
||||
if 'numeric_fields' not in targets:
|
||||
continue
|
||||
if not isinstance(node.value, ast.Dict):
|
||||
continue
|
||||
found = {}
|
||||
for key, value in zip(node.value.keys, node.value.values):
|
||||
if not isinstance(key, ast.Constant):
|
||||
continue
|
||||
if not str(key.value).startswith('vegas_'):
|
||||
break
|
||||
cfg_key, lo, hi = [ast.literal_eval(e) for e in value.elts]
|
||||
found[cfg_key] = (lo, hi)
|
||||
if found:
|
||||
return found
|
||||
raise AssertionError("could not locate the vegas numeric_fields map")
|
||||
|
||||
def test_every_bound_matches_validate(self):
|
||||
api = self._api_numeric_fields()
|
||||
mismatched = {
|
||||
key: (api[key], expected)
|
||||
for key, expected in self.EXPECTED.items()
|
||||
if key in api and api[key] != expected
|
||||
}
|
||||
assert not mismatched, f"API bounds disagree with validate(): {mismatched}"
|
||||
|
||||
@pytest.mark.parametrize('key,bounds', sorted(EXPECTED.items()))
|
||||
def test_validate_accepts_both_endpoints(self, key, bounds):
|
||||
lo, hi = bounds
|
||||
for value in (lo, hi):
|
||||
cfg = VegasModeConfig(**{key: value})
|
||||
errors = [e for e in cfg.validate() if key in e]
|
||||
assert not errors, f"{key}={value} should be valid, got {errors}"
|
||||
|
||||
@pytest.mark.parametrize('key,bounds', sorted(EXPECTED.items()))
|
||||
def test_validate_rejects_just_outside(self, key, bounds):
|
||||
lo, hi = bounds
|
||||
for value in (lo - 1, hi + 1):
|
||||
cfg = VegasModeConfig(**{key: value})
|
||||
errors = [e for e in cfg.validate() if key in e]
|
||||
assert errors, f"{key}={value} should be rejected"
|
||||
|
||||
|
||||
class TestCycleSizing:
|
||||
def test_plugins_per_cycle_defaults_above_buffer_ahead(self):
|
||||
cfg = VegasModeConfig()
|
||||
|
||||
Reference in New Issue
Block a user