Files
ledgrab/server/tests/api/routes/test_automations_routes.py
T
alexei.dolgolyov 6745e25b20 feat: roadmap batch (2026-06-19) — solar/linear-light/dither/nanoleaf + integrations
Eight roadmap features from the 2026-06-19 review, each a full vertical
(backend + tests + frontend + i18n en/ru/zh); ~67 new unit tests:

- automations: SolarRule sunrise/sunset trigger (new utils/solar.py, shared
  with the daylight cycle; window logic mirrors TimeOfDayRule)
- ci: best-effort arm64 multi-arch Docker manifest via QEMU + docker manifest
  (release.yml; amd64 path untouched, continue-on-error)
- game-integration: wire the orphaned LoLPoller via a LoLPollManager + a shared
  runtime_state module (poll lifecycle on enable/CRUD/startup/shutdown)
- ui: color-harmony gradient generator (complementary/analogous/triadic/...)
- effects: audio-reactive palette modulation (new audio_energy_tap; brightness/
  saturation modulation across all 12 procedural effects)
- capture: linear-light blending + spatio-temporal dithering, opt-in per
  calibration (new utils/linear_light.py, utils/dither.py)
- devices: Nanoleaf extControl v2 per-panel UDP streaming (per_panel mode)

Also bundles the pending 2026-06-18 production-review fixes and other
in-progress work already in the working tree (manual-trigger rule, etc.),
since they share files and could not be cleanly separated.

Gate: ruff + tsc clean; pytest 2654 passed / 2 skipped. The single failing
test (automation manual_trigger handler coverage) is a separate in-progress
item owned elsewhere, intentionally left as-is.
2026-06-22 23:21:24 +03:00

103 lines
3.3 KiB
Python

"""Tests for the manual-trigger automation route (POST /automations/{id}/trigger).
The AutomationEngine is replaced with a lightweight fake so the route layer is
tested without driving the real evaluation loop or scene application.
"""
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from ledgrab.api import dependencies as deps
from ledgrab.api.routes.automations import router
from ledgrab.storage.automation import ManualTriggerRule
from ledgrab.storage.automation_store import AutomationStore
class FakeEngine:
"""Stand-in exposing only what the trigger route calls."""
def __init__(self, result=("triggered", [])):
self.result = result
self.calls = []
async def fire_manual_trigger(self, automation):
self.calls.append(automation.id)
return self.result
@pytest.fixture
def _route_db(tmp_path):
from ledgrab.storage.database import Database
db = Database(tmp_path / "test.db")
yield db
db.close()
@pytest.fixture
def automation_store(_route_db) -> AutomationStore:
store = AutomationStore(_route_db)
store.create_automation(
name="Manual one",
enabled=True,
rule_logic="or",
rules=[ManualTriggerRule()],
scene_preset_id=None,
)
return store
@pytest.fixture
def fake_engine():
return FakeEngine()
@pytest.fixture
def client(automation_store, fake_engine):
app = FastAPI()
app.include_router(router)
from ledgrab.api.auth import verify_api_key
app.dependency_overrides[verify_api_key] = lambda: "test-user"
app.dependency_overrides[deps.get_automation_store] = lambda: automation_store
app.dependency_overrides[deps.get_automation_engine] = lambda: fake_engine
# Routes may fire entity events through the processor manager; give it a stub.
deps._deps["processor_manager"] = None
return TestClient(app, raise_server_exceptions=False)
def _first_id(store: AutomationStore) -> str:
return store.get_all_automations()[0].id
class TestTriggerRoute:
def test_trigger_returns_status(self, client, automation_store, fake_engine):
aid = _first_id(automation_store)
resp = client.post(f"/api/v1/automations/{aid}/trigger")
assert resp.status_code == 200
assert resp.json() == {"status": "triggered", "errors": []}
assert fake_engine.calls == [aid]
def test_trigger_skipped(self, client, automation_store, fake_engine):
fake_engine.result = ("skipped", [])
aid = _first_id(automation_store)
resp = client.post(f"/api/v1/automations/{aid}/trigger")
assert resp.status_code == 200
assert resp.json()["status"] == "skipped"
def test_trigger_partial_errors(self, client, automation_store, fake_engine):
fake_engine.result = ("partial", ["dev1: timeout"])
aid = _first_id(automation_store)
resp = client.post(f"/api/v1/automations/{aid}/trigger")
assert resp.status_code == 200
body = resp.json()
assert body["status"] == "partial"
assert body["errors"] == ["dev1: timeout"]
def test_trigger_unknown_id_404(self, client, fake_engine):
resp = client.post("/api/v1/automations/auto_ghost/trigger")
assert resp.status_code == 404
assert fake_engine.calls == []