from __future__ import annotations from uuid import uuid4 from fastapi.testclient import TestClient from app.core.dependencies import app_state from app.main import app def _create_token(client: TestClient, bridge_id: str | None = None) -> str: response = client.post( "/api/v1/integrations/midi/tokens", json={ "label": f"pytest-{uuid4().hex[:8]}", "bridge_id": bridge_id, "scopes": ["midi:connect", "midi:events", "midi:heartbeat"], }, ) assert response.status_code == 200 return response.json()["token"] def _create_scene(client: TestClient, slug: str) -> int: response = client.post( "/api/v1/scenes", json={ "name": slug, "slug": slug, "priority": 25, "values": [{"channel": 10, "value": 255, "precedence": "htp", "source": "scene"}], "targets": [], }, ) assert response.status_code == 200 return int(response.json()["id"]) def test_midi_heartbeat_updates_bridge_status() -> None: with TestClient(app) as client: token = _create_token(client, bridge_id="bridge-heartbeat") heartbeat = client.post( "/api/v1/integrations/midi/bridge/heartbeat", headers={"Authorization": f"Bearer {token}"}, json={ "type": "heartbeat", "protocol_version": 1, "bridge_id": "bridge-heartbeat", "device": "USB MIDI Test", "timestamp": "2026-07-24T20:30:00+00:00", }, ) assert heartbeat.status_code == 200 bridges = client.get("/api/v1/integrations/midi/bridges") assert bridges.status_code == 200 item = next(entry for entry in bridges.json()["items"] if entry["bridge_id"] == "bridge-heartbeat") assert item["device_name"] == "USB MIDI Test" assert item["online"] is True assert item["last_heartbeat_at"] is not None def test_midi_event_can_activate_scene_through_mapping() -> None: slug = f"midi-scene-{uuid4().hex[:8]}" with TestClient(app) as client: scene_id = _create_scene(client, slug) token = _create_token(client, bridge_id="bridge-scenes") mapping = client.post( "/api/v1/integrations/midi/mappings", json={ "name": "Scene trigger", "enabled": True, "bridge_id": "bridge-scenes", "device_name": "USB*", "message_type": "note_on", "channel": 0, "number": 36, "action": "activate_scene", "target_type": "scene", "target_id": slug, "mode": "trigger", "minimum_value": 1, "maximum_value": 127, }, ) assert mapping.status_code == 200 event = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-scenes", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:30:00+00:00", "message": {"type": "note_on", "channel": 0, "number": 36, "value": 127}, }, ) assert event.status_code == 200 assert event.json()["executed"] == 1 assert f"scene:{slug}" in app_state.engine.layers client.delete(f"/api/v1/integrations/midi/mappings/{mapping.json()['id']}") client.delete(f"/api/v1/scenes/{scene_id}") def test_midi_flash_hold_applies_and_releases_scene_layer() -> None: slug = f"midi-flash-{uuid4().hex[:8]}" with TestClient(app) as client: scene_id = _create_scene(client, slug) token = _create_token(client, bridge_id="bridge-flash") mapping_response = client.post( "/api/v1/integrations/midi/mappings", json={ "name": "Flash scene", "enabled": True, "bridge_id": "bridge-flash", "device_name": "*", "message_type": "note_on", "channel": 0, "number": 40, "action": "flash_scene", "target_type": "scene", "target_id": slug, "mode": "hold", "minimum_value": 1, "maximum_value": 127, }, ) assert mapping_response.status_code == 200 mapping_id = mapping_response.json()["id"] layer_name = f"midi:flash:{mapping_id}:{slug}" note_on = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-flash", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:30:01+00:00", "message": {"type": "note_on", "channel": 0, "number": 40, "value": 127}, }, ) assert note_on.status_code == 200 assert layer_name in app_state.engine.layers note_off = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-flash", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:30:02+00:00", "message": {"type": "note_off", "channel": 0, "number": 40, "value": 0}, }, ) assert note_off.status_code == 200 assert layer_name not in app_state.engine.layers client.delete(f"/api/v1/integrations/midi/mappings/{mapping_id}") client.delete(f"/api/v1/scenes/{scene_id}") def test_midi_continuous_master_and_scene_intensity_are_scaled() -> None: slug = f"midi-intensity-{uuid4().hex[:8]}" with TestClient(app) as client: scene_id = _create_scene(client, slug) token = _create_token(client, bridge_id="bridge-cc") master_mapping = client.post( "/api/v1/integrations/midi/mappings", json={ "name": "Master dimmer", "enabled": True, "bridge_id": "bridge-cc", "device_name": "*", "message_type": "control_change", "channel": 0, "number": 14, "action": "set_master_dimmer", "target_type": "global", "target_id": None, "mode": "continuous", "minimum_value": 0, "maximum_value": 127, }, ) assert master_mapping.status_code == 200 intensity_mapping = client.post( "/api/v1/integrations/midi/mappings", json={ "name": "Scene intensity", "enabled": True, "bridge_id": "bridge-cc", "device_name": "*", "message_type": "control_change", "channel": 0, "number": 15, "action": "set_scene_intensity", "target_type": "scene", "target_id": slug, "mode": "continuous", "minimum_value": 0, "maximum_value": 127, }, ) assert intensity_mapping.status_code == 200 master_event = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-cc", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:31:00+00:00", "message": {"type": "control_change", "channel": 0, "number": 14, "value": 64}, }, ) assert master_event.status_code == 200 assert 120 <= app_state.engine.master <= 132 intensity_event = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-cc", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:31:01+00:00", "message": {"type": "control_change", "channel": 0, "number": 15, "value": 64}, }, ) assert intensity_event.status_code == 200 layer_name = f"midi:intensity:{intensity_mapping.json()['id']}:{slug}" layer = app_state.engine.layers[layer_name] assert 120 <= layer.values_by_universe[1][10] <= 132 client.delete(f"/api/v1/integrations/midi/mappings/{master_mapping.json()['id']}") client.delete(f"/api/v1/integrations/midi/mappings/{intensity_mapping.json()['id']}") client.delete(f"/api/v1/scenes/{scene_id}") def test_midi_learn_captures_next_event_without_triggering_existing_mapping() -> None: slug = f"midi-learn-{uuid4().hex[:8]}" with TestClient(app) as client: scene_id = _create_scene(client, slug) token = _create_token(client, bridge_id="bridge-learn") mapping = client.post( "/api/v1/integrations/midi/mappings", json={ "name": "Learn target", "enabled": True, "bridge_id": "bridge-learn", "device_name": "*", "message_type": "note_on", "channel": 0, "number": 41, "action": "activate_scene", "target_type": "scene", "target_id": slug, "mode": "trigger", "minimum_value": 1, "maximum_value": 127, }, ) assert mapping.status_code == 200 start = client.post( "/api/v1/integrations/midi/learn/start", json={"timeout_seconds": 15, "allow_passthrough": False}, ) assert start.status_code == 200 assert start.json()["active"] is True event = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "bridge-learn", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:32:00+00:00", "message": {"type": "note_on", "channel": 0, "number": 41, "value": 127}, }, ) assert event.status_code == 200 assert event.json()["captured_for_learning"] is True assert f"scene:{slug}" not in app_state.engine.layers state = client.get("/api/v1/integrations/midi/learn") assert state.status_code == 200 assert state.json()["active"] is False assert state.json()["captured_event"]["message"]["number"] == 41 client.delete(f"/api/v1/integrations/midi/mappings/{mapping.json()['id']}") client.delete(f"/api/v1/scenes/{scene_id}") def test_midi_bridge_token_can_be_restricted_to_bridge_id() -> None: with TestClient(app) as client: token = _create_token(client, bridge_id="bridge-locked") response = client.post( "/api/v1/integrations/midi/bridge/events", headers={"Authorization": f"Bearer {token}"}, json={ "type": "midi_event", "protocol_version": 1, "bridge_id": "wrong-bridge", "device": "USB MIDI Controller", "timestamp": "2026-07-24T20:33:00+00:00", "message": {"type": "note_on", "channel": 0, "number": 50, "value": 127}, }, ) assert response.status_code == 401