From 4ba637ea44d9af52a6b9dcc1f9222c6b15bb0bb2 Mon Sep 17 00:00:00 2001 From: CREDO23 Date: Fri, 29 May 2026 17:48:48 +0200 Subject: [PATCH] feat(automations): add event trigger match and inputs --- .../triggers/builtin/event/inputs.py | 17 ++++++++ .../triggers/builtin/event/match.py | 16 ++++++++ .../triggers/builtin/event/test_inputs.py | 26 +++++++++++++ .../triggers/builtin/event/test_match.py | 39 +++++++++++++++++++ 4 files changed, 98 insertions(+) create mode 100644 surfsense_backend/app/automations/triggers/builtin/event/inputs.py create mode 100644 surfsense_backend/app/automations/triggers/builtin/event/match.py create mode 100644 surfsense_backend/tests/unit/automations/triggers/builtin/event/test_inputs.py create mode 100644 surfsense_backend/tests/unit/automations/triggers/builtin/event/test_match.py diff --git a/surfsense_backend/app/automations/triggers/builtin/event/inputs.py b/surfsense_backend/app/automations/triggers/builtin/event/inputs.py new file mode 100644 index 000000000..e597c0b66 --- /dev/null +++ b/surfsense_backend/app/automations/triggers/builtin/event/inputs.py @@ -0,0 +1,17 @@ +"""Build run inputs from a published event.""" + +from __future__ import annotations + +from typing import Any + +from app.event_bus import Event + + +def event_runtime_inputs(event: Event) -> dict[str, Any]: + """Flatten the event payload and stamp event metadata as run inputs.""" + return { + **event.payload, + "event_type": event.event_type, + "event_id": event.event_id, + "occurred_at": event.occurred_at.isoformat(), + } diff --git a/surfsense_backend/app/automations/triggers/builtin/event/match.py b/surfsense_backend/app/automations/triggers/builtin/event/match.py new file mode 100644 index 000000000..b67a3d49a --- /dev/null +++ b/surfsense_backend/app/automations/triggers/builtin/event/match.py @@ -0,0 +1,16 @@ +"""Pure predicate: does an event trigger fire for a given event?""" + +from __future__ import annotations + +from typing import Any + +from app.event_bus import Event + +from .filter import matches + + +def trigger_matches_event(params: dict[str, Any], event: Event) -> bool: + """True when an event trigger configured with ``params`` should fire for ``event``.""" + if params.get("event_type") != event.event_type: + return False + return matches(params.get("filter") or {}, event.payload) diff --git a/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_inputs.py b/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_inputs.py new file mode 100644 index 000000000..e6191d7a7 --- /dev/null +++ b/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_inputs.py @@ -0,0 +1,26 @@ +"""An event hands its payload + metadata to the run as inputs.""" + +from __future__ import annotations + +import pytest + +from app.automations.triggers.builtin.event.inputs import event_runtime_inputs +from app.event_bus import Event + +pytestmark = pytest.mark.unit + + +def test_runtime_inputs_flatten_payload_with_event_metadata() -> None: + event = Event( + event_type="document.indexed", + payload={"document_id": 42, "document_type": "FILE"}, + search_space_id=7, + ) + + inputs = event_runtime_inputs(event) + + assert inputs["document_id"] == 42 + assert inputs["document_type"] == "FILE" + assert inputs["event_type"] == "document.indexed" + assert inputs["event_id"] == event.event_id + assert inputs["occurred_at"] == event.occurred_at.isoformat() diff --git a/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_match.py b/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_match.py new file mode 100644 index 000000000..d83db97a4 --- /dev/null +++ b/surfsense_backend/tests/unit/automations/triggers/builtin/event/test_match.py @@ -0,0 +1,39 @@ +"""Which triggers an event fires: event_type equality + filter match.""" + +from __future__ import annotations + +import pytest + +from app.automations.triggers.builtin.event.match import trigger_matches_event +from app.event_bus import Event + +pytestmark = pytest.mark.unit + + +def _event(event_type: str = "document.indexed", **payload) -> Event: + return Event(event_type=event_type, payload=payload, search_space_id=7) + + +def test_matches_when_event_type_equal_and_filter_passes() -> None: + params = {"event_type": "document.indexed", "filter": {"document_type": "FILE"}} + assert trigger_matches_event(params, _event(document_type="FILE")) is True + + +def test_no_match_when_event_type_differs() -> None: + params = {"event_type": "document.indexed", "filter": {}} + assert trigger_matches_event(params, _event("podcast.generated")) is False + + +def test_no_match_when_filter_rejects_payload() -> None: + params = {"event_type": "document.indexed", "filter": {"document_type": "FILE"}} + assert trigger_matches_event(params, _event(document_type="WEBPAGE")) is False + + +def test_empty_filter_matches_any_payload_of_that_type() -> None: + params = {"event_type": "document.indexed", "filter": {}} + assert trigger_matches_event(params, _event(document_type="ANYTHING")) is True + + +def test_missing_filter_key_is_treated_as_empty() -> None: + params = {"event_type": "document.indexed"} + assert trigger_matches_event(params, _event(document_type="X")) is True