Merge commit '7972901f15' into dev_mod

This commit is contained in:
DESKTOP-RTLN3BA\$punk 2026-05-29 20:28:12 -07:00
commit 80daf46fbf
74 changed files with 1681 additions and 234 deletions

View file

@ -13,7 +13,7 @@ from typing import Any
import pytest
from app.automations.actions.agent_task.auto_decide import build_auto_decisions
from app.automations.actions.builtin.agent_task.auto_decide import build_auto_decisions
pytestmark = pytest.mark.unit

View file

@ -10,7 +10,9 @@ from __future__ import annotations
import pytest
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from app.automations.actions.agent_task.finalize import extract_final_assistant_message
from app.automations.actions.builtin.agent_task.finalize import (
extract_final_assistant_message,
)
pytestmark = pytest.mark.unit

View file

@ -1,10 +1,8 @@
"""Lock the input-validation contract used by ``dispatch_run``.
"""Lock the input-validation contract enforced before a run is enqueued.
``_validate_inputs`` is module-internal by convention (underscore), but it
encodes a real behavior contract the rest of the system depends on, and the
public alternative (``dispatch_run``) requires a real DB session. Tests
target the pure function directly; the contract not the symbol is what's
locked.
``validate_inputs`` is the pure schema check that ``enqueue_run`` runs against
merged inputs. ``enqueue_run`` itself needs a real DB session, so tests target
this pure function directly; the contract not the symbol is what's locked.
"""
from __future__ import annotations
@ -12,7 +10,7 @@ from __future__ import annotations
import pytest
from app.automations.dispatch.errors import DispatchError
from app.automations.dispatch.run import _validate_inputs
from app.automations.dispatch.inputs import validate_inputs
from app.automations.schemas.definition.envelope import AutomationDefinition
from app.automations.schemas.definition.inputs import Inputs
from app.automations.schemas.definition.plan_step import PlanStep
@ -42,7 +40,7 @@ def test_validate_inputs_passes_through_when_no_schema_is_declared() -> None:
"static_key": "value",
}
assert _validate_inputs(definition, runtime_inputs) == runtime_inputs
assert validate_inputs(definition, runtime_inputs) == runtime_inputs
def test_validate_inputs_returns_inputs_when_they_match_declared_schema() -> None:
@ -58,14 +56,13 @@ def test_validate_inputs_returns_inputs_when_they_match_declared_schema() -> Non
inputs = {"topic": "weekly report"}
assert _validate_inputs(definition, inputs) == inputs
assert validate_inputs(definition, inputs) == inputs
def test_validate_inputs_raises_dispatch_error_when_inputs_violate_schema() -> None:
"""Inputs that don't match the declared schema must surface as
``DispatchError`` (not the raw ``jsonschema.ValidationError``), so the
schedule tick and any other caller can handle one dispatch-domain
exception type uniformly."""
``DispatchError`` (not the raw ``jsonschema.ValidationError``), so every
caller can handle one dispatch-domain exception type uniformly."""
schema = {
"type": "object",
"properties": {"topic": {"type": "string"}},
@ -74,4 +71,4 @@ def test_validate_inputs_raises_dispatch_error_when_inputs_violate_schema() -> N
definition = _minimal_definition(inputs=Inputs(schema=schema))
with pytest.raises(DispatchError):
_validate_inputs(definition, {"topic": 42}) # type violates string
validate_inputs(definition, {"topic": 42}) # type violates string

View file

@ -39,7 +39,7 @@ def test_run_status_string_values_are_stable() -> None:
def test_trigger_type_keeps_manual_member_even_though_unregistered() -> None:
"""``MANUAL`` is reserved (mirrors the Postgres enum) but the trigger
store does not register it in v1. The enum must keep both members so
existing DB rows and the schema migration plan stay valid."""
assert {member.value for member in TriggerType} == {"schedule", "manual"}
"""``schedule`` and ``event`` are registered; ``MANUAL`` is reserved
(mirrors the Postgres enum) but the trigger store does not register it.
The enum must keep every member so DB rows and migrations stay valid."""
assert {member.value for member in TriggerType} == {"schedule", "event", "manual"}

View file

@ -0,0 +1,18 @@
"""The ``event`` trigger self-registers on the triggers store at import."""
from __future__ import annotations
import pytest
from app.automations.triggers import get_trigger
from app.automations.triggers.builtin.event.params import EventTriggerParams
pytestmark = pytest.mark.unit
def test_event_trigger_is_registered() -> None:
definition = get_trigger("event")
assert definition is not None
assert definition.type == "event"
assert definition.params_model is EventTriggerParams

View file

@ -0,0 +1,115 @@
"""Behavior tests for the ``matches`` filter grammar."""
from __future__ import annotations
import pytest
from app.automations.triggers.builtin.event.filter import FilterError, matches
pytestmark = pytest.mark.unit
def test_empty_filter_matches_any_payload() -> None:
assert matches({}, {"document_id": 42, "document_type": "FILE"}) is True
assert matches({}, {}) is True
def test_scalar_value_is_implicit_equality() -> None:
flt = {"document_type": "FILE"}
assert matches(flt, {"document_type": "FILE"}) is True
assert matches(flt, {"document_type": "WEBPAGE"}) is False
def test_multiple_fields_are_anded() -> None:
flt = {"document_type": "FILE", "search_space_id": 7}
assert matches(flt, {"document_type": "FILE", "search_space_id": 7}) is True
assert matches(flt, {"document_type": "FILE", "search_space_id": 9}) is False
def test_gt_operator_compares_greater_than() -> None:
flt = {"page_count": {"$gt": 10}}
assert matches(flt, {"page_count": 20}) is True
assert matches(flt, {"page_count": 10}) is False
assert matches(flt, {"page_count": 5}) is False
def test_remaining_comparison_operators() -> None:
assert matches({"n": {"$gte": 10}}, {"n": 10}) is True
assert matches({"n": {"$gte": 10}}, {"n": 9}) is False
assert matches({"n": {"$lt": 10}}, {"n": 9}) is True
assert matches({"n": {"$lt": 10}}, {"n": 10}) is False
assert matches({"n": {"$lte": 10}}, {"n": 10}) is True
assert matches({"n": {"$lte": 10}}, {"n": 11}) is False
assert matches({"s": {"$eq": "FILE"}}, {"s": "FILE"}) is True
assert matches({"s": {"$eq": "FILE"}}, {"s": "WEB"}) is False
assert matches({"s": {"$ne": "FILE"}}, {"s": "WEB"}) is True
assert matches({"s": {"$ne": "FILE"}}, {"s": "FILE"}) is False
def test_multiple_operators_on_one_field_are_anded() -> None:
flt = {"n": {"$gte": 10, "$lt": 20}}
assert matches(flt, {"n": 15}) is True
assert matches(flt, {"n": 10}) is True
assert matches(flt, {"n": 20}) is False
assert matches(flt, {"n": 5}) is False
def test_in_and_nin_membership_operators() -> None:
flt_in = {"document_type": {"$in": ["FILE", "WEBPAGE"]}}
assert matches(flt_in, {"document_type": "FILE"}) is True
assert matches(flt_in, {"document_type": "SLACK"}) is False
flt_nin = {"document_type": {"$nin": ["FILE", "WEBPAGE"]}}
assert matches(flt_nin, {"document_type": "SLACK"}) is True
assert matches(flt_nin, {"document_type": "FILE"}) is False
def test_or_matches_when_any_branch_holds() -> None:
flt = {"$or": [{"document_type": "FILE"}, {"document_type": "WEBPAGE"}]}
assert matches(flt, {"document_type": "WEBPAGE"}) is True
assert matches(flt, {"document_type": "SLACK"}) is False
def test_and_matches_when_every_branch_holds() -> None:
flt = {"$and": [{"n": {"$gt": 5}}, {"n": {"$lt": 10}}]}
assert matches(flt, {"n": 7}) is True
assert matches(flt, {"n": 12}) is False
def test_not_inverts_its_subexpression() -> None:
flt = {"$not": {"document_type": "FILE"}}
assert matches(flt, {"document_type": "WEBPAGE"}) is True
assert matches(flt, {"document_type": "FILE"}) is False
def test_missing_field_never_matches_and_never_raises() -> None:
# Conservative: an absent field fails the constraint, and comparisons must
# not raise on the missing value — including $ne (absence isn't "not equal").
assert matches({"document_type": "FILE"}, {}) is False
assert matches({"page_count": {"$gt": 5}}, {}) is False
assert matches({"document_type": {"$in": ["FILE"]}}, {}) is False
assert matches({"document_type": {"$ne": "FILE"}}, {}) is False
def test_logical_operators_compose_with_fields() -> None:
flt = {
"search_space_id": 7,
"$or": [{"document_type": "FILE"}, {"document_type": "WEBPAGE"}],
}
assert matches(flt, {"search_space_id": 7, "document_type": "FILE"}) is True
assert matches(flt, {"search_space_id": 9, "document_type": "FILE"}) is False
assert matches(flt, {"search_space_id": 7, "document_type": "SLACK"}) is False
def test_unknown_field_operator_raises_filter_error() -> None:
with pytest.raises(FilterError):
matches({"n": {"$regex": "x"}}, {"n": "xyz"})
def test_unknown_logical_operator_raises_filter_error() -> None:
with pytest.raises(FilterError):
matches({"$nor": [{"document_type": "FILE"}]}, {"document_type": "FILE"})

View file

@ -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()

View file

@ -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

View file

@ -0,0 +1,40 @@
"""``EventTriggerParams`` contract: an event_type to listen for + an optional filter."""
from __future__ import annotations
import pytest
from app.automations.triggers.builtin.event.params import EventTriggerParams
pytestmark = pytest.mark.unit
def test_accepts_event_type_and_filter() -> None:
params = EventTriggerParams(
event_type="document.indexed",
filter={"document_type": "FILE"},
)
assert params.event_type == "document.indexed"
assert params.filter == {"document_type": "FILE"}
def test_filter_defaults_to_empty() -> None:
params = EventTriggerParams(event_type="document.indexed")
assert params.filter == {}
def test_event_type_is_required() -> None:
with pytest.raises(ValueError):
EventTriggerParams(filter={"x": 1})
def test_event_type_must_not_be_blank() -> None:
with pytest.raises(ValueError):
EventTriggerParams(event_type="")
def test_extra_keys_are_forbidden() -> None:
with pytest.raises(ValueError):
EventTriggerParams(event_type="document.indexed", typo=True)

View file

@ -6,7 +6,7 @@ from datetime import UTC, datetime
import pytest
from app.automations.triggers.schedule.cron import (
from app.automations.triggers.builtin.schedule.cron import (
InvalidCronError,
compute_next_fire_at,
validate_cron,

View file

@ -5,7 +5,7 @@ from __future__ import annotations
import pytest
from pydantic import ValidationError
from app.automations.triggers.schedule.params import ScheduleTriggerParams
from app.automations.triggers.builtin.schedule.params import ScheduleTriggerParams
pytestmark = pytest.mark.unit