6745e25b20
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.
103 lines
3.3 KiB
Python
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 == []
|