Files
hermes-webui/tests/test_cron_delivery_options.py
T
nesquena-hermesandnesquena-hermes f79325498e fix(cron): restore messaging platforms in the delivery picker (#7525)
* fix(cron): restore messaging platforms in the delivery picker

The Agent moved _KNOWN_DELIVERY_PLATFORMS from cron.scheduler to
cron.scheduler_delivery. _handle_cron_delivery_options imported the old
path inside a bare except and fell back to an empty frozenset, so the
import error was swallowed and GET /api/crons/delivery-options silently
returned only local and origin — telegram, discord, slack, feishu and
every other messaging platform disappeared from the cron delivery picker
with no error surfaced anywhere.

Resolve the authority across both module paths (current first, legacy
fallback) so the picker keeps working across Agent versions, and add a
regression test that pins the resolution order and cross-checks the
endpoint output against the resolved authority.

* test(cron): skip the authority-resolution test where the Agent is absent

CI runs agent-free (tests/browser_smoke.py), so neither cron.scheduler_delivery
nor cron.scheduler is importable there and the authority resolves empty. The
new regression test asserted non-empty and therefore failed shard 3 on both
Python versions. Skip instead: with no cron package there is no authority to
resolve and nothing meaningful to assert. The test still fails loudly wherever
the Agent IS installed, which is where a future relocation would bite.

* test(cron): register the new test in _AGENT_DEPENDENT_TESTS

Use the project's existing skip mechanism (tests/conftest.py) rather than an
ad-hoc inline pytest.skip, matching the five sibling delivery-options tests.
CI runs agent-free, so there is no cron package and no authority to resolve
there; with the Agent installed the test still asserts loudly, which is the
environment where a future relocation would actually bite.

Verified both directions: 6/6 pass with the Agent present, and forcing
AGENT_MODULES_AVAILABLE=False skips all 6 (was 5) as CI will.

---------

Co-authored-by: nesquena-hermes <nesquena-hermes@users.noreply.github.com>
2026-09-10 22:30:57 -07:00

108 lines
4.3 KiB
Python

"""Tests for GET /api/crons/delivery-options endpoint.
Verifies the dynamic delivery options API returns a structured list
of known platforms the user can choose as cron job delivery targets.
"""
import json
import urllib.request
import urllib.error
from tests._pytest_port import BASE
def get(path):
with urllib.request.urlopen(BASE + path, timeout=10) as r:
return json.loads(r.read()), r.status
def test_delivery_options_returns_200():
"""Endpoint exists and returns 200."""
result, status = get("/api/crons/delivery-options")
assert status == 200
def test_delivery_options_has_platforms():
"""Response contains a 'platforms' list with at least 'local'."""
result, status = get("/api/crons/delivery-options")
assert status == 200
assert "platforms" in result
platforms = result["platforms"]
assert isinstance(platforms, list)
assert len(platforms) > 0
# 'local' must always be present (it's the built-in default)
values = [p["value"] for p in platforms]
assert "local" in values, f"'local' missing from delivery options: {values}"
def test_delivery_options_structure():
"""Each platform entry has value and label."""
result, status = get("/api/crons/delivery-options")
assert status == 200
for p in result["platforms"]:
assert "value" in p, f"Platform entry missing 'value': {p}"
assert "label" in p, f"Platform entry missing 'label': {p}"
assert isinstance(p["value"], str)
assert isinstance(p["label"], str)
assert p["value"], "Platform value must not be empty"
assert p["label"], "Platform label must not be empty"
def test_delivery_options_includes_common_platforms():
"""Well-known platforms from _KNOWN_DELIVERY_PLATFORMS appear."""
result, status = get("/api/crons/delivery-options")
assert status == 200
values = [p["value"] for p in result["platforms"]]
# These are from the hardcoded _KNOWN_DELIVERY_PLATFORMS in hermes-agent
for expected in ("local", "telegram", "discord", "slack", "feishu"):
assert expected in values, f"Expected platform '{expected}' not found in: {values}"
def test_delivery_options_local_label():
"""'local' entry has a user-friendly label (not just 'Local')."""
result, status = get("/api/crons/delivery-options")
assert status == 200
local_entry = next(p for p in result["platforms"] if p["value"] == "local")
# Label should contain "Local" or be an i18n key — just verify it's non-empty
assert local_entry["label"], "Local platform label is empty"
def test_delivery_options_survives_the_authority_module_move():
"""The platform list must not silently empty when the Agent relocates it.
``_KNOWN_DELIVERY_PLATFORMS`` used to live in ``cron.scheduler`` and now
lives in ``cron.scheduler_delivery``. The endpoint previously imported the
old path inside a bare ``except`` and fell back to an EMPTY frozenset, so
the move silently degraded the cron delivery picker to local/origin only —
every messaging platform (telegram, discord, slack, feishu, ...) vanished
from the UI with no error anywhere. Pin the resolution order so a future
relocation fails loudly instead of silently dropping platforms.
Registered in ``_AGENT_DEPENDENT_TESTS`` (tests/conftest.py) alongside the
other delivery-options tests: CI runs agent-free, so there is no ``cron``
package and no authority to resolve there.
"""
import importlib
resolved = frozenset()
for module_name in ("cron.scheduler_delivery", "cron.scheduler"):
try:
mod = importlib.import_module(module_name)
except Exception:
continue
known = getattr(mod, "_KNOWN_DELIVERY_PLATFORMS", None)
if known:
resolved = frozenset(known)
break
assert resolved, (
"_KNOWN_DELIVERY_PLATFORMS resolved empty from every known module path "
"— the cron delivery picker would silently show only local/origin"
)
# The endpoint's own output must agree with the resolved authority.
result, status = get("/api/crons/delivery-options")
assert status == 200
values = {p["value"] for p in result["platforms"]}
missing = resolved - values
assert not missing, f"platforms resolved but absent from the endpoint: {sorted(missing)}"