fix(setup): register scaffolded target with ProcessorManager + final-review hardening
Final-review blocker: the setup scaffold created the LED output target in the store but never registered it with the ProcessorManager, so the wizard's "Start" step 404'd on a fresh setup (target not found) — the lights never started despite a success screen. Now the scaffold calls target.register_with_manager(manager) right after create (mirroring the canonical POST /output-targets route, same ValueError guard), so start_processing finds the target. Rollback unregisters via manager.remove_target before deleting the store entity, so a post-registration failure leaves no half-registered target. Also from the final review: - solve corner_indices elements now bounded ge=0 (clear 422 instead of silent modulo-wrap). - setup-wizard.ts: reuse tutorials' suppressGettingStartedTour()/TOUR_KEY instead of a duplicated 'tour_completed' literal; drop a duplicate manual-form submit listener. Tests: + adversarial pass over the whole feature (solver/session/scaffold edge cases) and a scaffold->register->startable regression test. Full suite 2149 passed / 2 skipped; tsc clean; build passes; ruff clean.
This commit is contained in:
@@ -21,7 +21,7 @@ from __future__ import annotations
|
||||
import pytest
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
from unittest.mock import patch
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -89,6 +89,14 @@ def event_log():
|
||||
return log
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_manager():
|
||||
"""A MagicMock ProcessorManager that silently accepts all calls."""
|
||||
mgr = MagicMock()
|
||||
mgr.remove_target = MagicMock()
|
||||
return mgr
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def setup_client(
|
||||
tmp_db,
|
||||
@@ -98,6 +106,7 @@ def setup_client(
|
||||
css_store,
|
||||
output_target_store,
|
||||
event_log,
|
||||
mock_manager,
|
||||
):
|
||||
from ledgrab.api.routes.setup import router
|
||||
from ledgrab.api.auth import verify_api_key
|
||||
@@ -111,6 +120,7 @@ def setup_client(
|
||||
app.dependency_overrides[deps.get_picture_source_store] = lambda: picture_source_store
|
||||
app.dependency_overrides[deps.get_color_strip_store] = lambda: css_store
|
||||
app.dependency_overrides[deps.get_output_target_store] = lambda: output_target_store
|
||||
app.dependency_overrides[deps.get_processor_manager] = lambda: mock_manager
|
||||
|
||||
# Capture entity events
|
||||
def _fire(entity_type, action, entity_id):
|
||||
@@ -512,3 +522,79 @@ class TestScaffoldCalibrationIntegration:
|
||||
assert updated.calibration.leds_top == 15
|
||||
assert updated.calibration.leds_right == 9
|
||||
assert updated.calibration.layout == "clockwise"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Regression: scaffold registers the output target with ProcessorManager
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TestScaffoldRegistersWithManager:
|
||||
def test_scaffold_calls_add_target_on_manager(
|
||||
self,
|
||||
setup_client,
|
||||
sample_device,
|
||||
mock_manager,
|
||||
):
|
||||
"""Blocker regression: scaffold must register the created output target
|
||||
with the ProcessorManager so that a subsequent start call can find it.
|
||||
|
||||
WledOutputTarget.register_with_manager calls manager.add_target(...)
|
||||
— we assert that method was invoked with the scaffolded target id.
|
||||
"""
|
||||
resp = _scaffold(setup_client, device_id=sample_device.id)
|
||||
assert resp.status_code == 201, resp.text
|
||||
target_id = resp.json()["output_target_id"]
|
||||
|
||||
# register_with_manager → manager.add_target(target_id=..., ...)
|
||||
mock_manager.add_target.assert_called_once()
|
||||
call_kwargs = mock_manager.add_target.call_args
|
||||
assert call_kwargs.kwargs.get("target_id") == target_id, (
|
||||
f"manager.add_target was not called with target_id={target_id!r}; "
|
||||
f"actual call: {call_kwargs}"
|
||||
)
|
||||
|
||||
def test_scaffold_rollback_calls_remove_target_on_manager(
|
||||
self,
|
||||
setup_client,
|
||||
sample_device,
|
||||
output_target_store,
|
||||
mock_manager,
|
||||
):
|
||||
"""Rollback after a post-target-creation failure must call manager.remove_target
|
||||
so no half-registered target lingers in the ProcessorManager.
|
||||
|
||||
We inject a failure by making register_with_manager raise RuntimeError
|
||||
(bypassing the ValueError-only guard), which puts the outer except branch
|
||||
in play. The target IS already in created_ids at that point, so rollback
|
||||
must call manager.remove_target(target_id).
|
||||
"""
|
||||
created_target_ids: list[str] = []
|
||||
|
||||
original_create = output_target_store.create_wled_target
|
||||
|
||||
def _spy_create(*args, **kwargs):
|
||||
target = original_create(*args, **kwargs)
|
||||
created_target_ids.append(target.id)
|
||||
return target
|
||||
|
||||
output_target_store.create_wled_target = _spy_create
|
||||
try:
|
||||
# Patch register_with_manager on WledOutputTarget to raise RuntimeError —
|
||||
# RuntimeError bypasses the ValueError guard and triggers the outer except,
|
||||
# so rollback fires with the target already in created_ids.
|
||||
with patch(
|
||||
"ledgrab.storage.wled_output_target.WledOutputTarget.register_with_manager",
|
||||
side_effect=RuntimeError("Injected registration failure for rollback test"),
|
||||
):
|
||||
resp = setup_client.post(
|
||||
"/api/v1/setup/scaffold",
|
||||
json={"device_id": sample_device.id, "display_index": 0},
|
||||
)
|
||||
assert resp.status_code == 500, resp.text
|
||||
|
||||
assert len(created_target_ids) == 1, "spy did not record a created target"
|
||||
target_id = created_target_ids[0]
|
||||
mock_manager.remove_target.assert_called_with(target_id)
|
||||
finally:
|
||||
output_target_store.create_wled_target = original_create
|
||||
|
||||
Reference in New Issue
Block a user