test(automations/dispatch): lock _validate_inputs + DispatchError

Cover the input-validation contract dispatch_run relies on:

- no declared schema → inputs pass through unchanged (regression site
  that previously stripped runtime keys like fired_at / last_fired_at
  and broke Jinja templates).
- declared schema, valid inputs → passthrough validated.
- declared schema, invalid inputs → DispatchError (uniform exception
  type, not raw jsonschema.ValidationError).

Plus the DispatchError exception identity (Exception subclass, message
preserved, isinstance-friendly for the dispatch layer's consumers).

4 tests, pure unit.
This commit is contained in:
CREDO23 2026-05-28 19:03:00 +02:00
parent 2a76f43387
commit 18b4800e49
3 changed files with 105 additions and 0 deletions

View file

@ -0,0 +1,28 @@
"""Lock the ``DispatchError`` exception contract.
``DispatchError`` is the uniform exception type the dispatch layer raises
for any "cannot turn this fire request into a run" condition. Other
modules (templates of error envelopes, run records) compare on
``isinstance(exc, DispatchError)``, so the inheritance is the contract.
"""
from __future__ import annotations
import pytest
from app.automations.dispatch.errors import DispatchError
pytestmark = pytest.mark.unit
def test_dispatch_error_is_exception_subclass_and_carries_message() -> None:
"""Lifting a string into ``DispatchError`` preserves the message and
behaves as a regular ``Exception`` for ``isinstance`` / ``raise`` /
``except`` consumers."""
error = DispatchError("missing trigger")
assert isinstance(error, Exception)
assert str(error) == "missing trigger"
with pytest.raises(DispatchError):
raise error

View file

@ -0,0 +1,77 @@
"""Lock the input-validation contract used by ``dispatch_run``.
``_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.
"""
from __future__ import annotations
import pytest
from app.automations.dispatch.errors import DispatchError
from app.automations.dispatch.run 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
pytestmark = pytest.mark.unit
def _minimal_definition(*, inputs: Inputs | None = None) -> AutomationDefinition:
"""One-step definition with an optional declared input schema."""
return AutomationDefinition(
name="test",
inputs=inputs,
plan=[PlanStep(step_id="s1", action="agent_task")],
)
def test_validate_inputs_passes_through_when_no_schema_is_declared() -> None:
"""When the definition declares no input schema, runtime inputs reach
the template context **unchanged**. Regression site: previously this
branch returned ``{}``, which stripped runtime keys like ``fired_at``
and ``last_fired_at`` and made Jinja blow up on ``{{ inputs.* }}``.
"""
definition = _minimal_definition(inputs=None)
runtime_inputs = {
"fired_at": "2026-01-01T00:00:00+00:00",
"last_fired_at": None,
"static_key": "value",
}
assert _validate_inputs(definition, runtime_inputs) == runtime_inputs
def test_validate_inputs_returns_inputs_when_they_match_declared_schema() -> None:
"""With a declared JSON schema, inputs that satisfy it pass through
unchanged (validation succeeds; the function does not coerce or
strip extra fields not mentioned in the schema)."""
schema = {
"type": "object",
"properties": {"topic": {"type": "string"}},
"required": ["topic"],
}
definition = _minimal_definition(inputs=Inputs(schema=schema))
inputs = {"topic": "weekly report"}
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."""
schema = {
"type": "object",
"properties": {"topic": {"type": "string"}},
"required": ["topic"],
}
definition = _minimal_definition(inputs=Inputs(schema=schema))
with pytest.raises(DispatchError):
_validate_inputs(definition, {"topic": 42}) # type violates string