Files
hermes-agent/tests/cron/test_cron_origin_synthetic_thread.py
Victor Kyriazakos 58ff0fd302 fix(cron): relay-fronted Slack delivery — synthetic creation-thread capture + preflight fronted-platform blindness
Bug 1: relay-fronted Slack in thread-per-message mode stamps each top-level
message's own id as source.thread_id (session KEYING, native thread_ts
parity). Cron origin capture persisted that stamp as durable routing, so
every delivery landed inside the ephemeral thread spawned around the
creation message instead of the top-level conversation. Fix at the source:
_origin_from_env drops a Slack thread id equal to the creation message's
own id (genuine in-thread creations keep theirs). Fire-time repair for
already-persisted jobs: deliver=origin and the explicit-target Slack
re-attach treat an origin thread as stale when the origin chat is the
configured Slack home chat — top-level (or the home target's configured
thread) wins; non-home working threads are preserved.

Bug 2: _preflight_check_delivery and cron_delivery_targets validated
deliver prefixes against get_connected_platforms(), which only sees
natively configured platforms — a relay-only deployment ({relay}) rejected
'slack:CHAT' with 'no gateway credentials configured' although fire-time
routing (resolve_delivery_transport + RelayAdapter.fronts_platform)
delivers it. New gateway.relay.relay_fronted_platforms() (env-derived from
GATEWAY_RELAY_PLATFORMS — the same source that seeds the live adapter's
identity set, so validation and routing cannot disagree) is unioned into
the connected set when the relay is connected. Native topologies keep the
strict credential check unchanged.
2026-08-13 10:46:13 -07:00

83 lines
3.4 KiB
Python

"""Cron origin capture: Slack per-message session-key threads are not routing.
Bug report (relay-fronted Slack, thread-per-message mode): creating a cron job
from a top-level Slack DM message persisted the creation message's own id as
``origin.thread_id`` — the relay adapter stamps ``source.thread_id = message_id``
on every top-level Slack message purely for SESSION KEYING (native SlackAdapter
parity: ``thread_ts = event.thread_ts or ts``). Every subsequent cron delivery
then landed inside the ephemeral thread spawned around the creation message
instead of the top-level conversation / configured home.
The stamp is recognizable at capture time: a Slack session whose thread id
equals the triggering message's own id is a synthetic per-message key, not a
durable thread. A genuine in-thread creation has thread_id == the parent
thread's id != the triggering message's own id, and must keep its thread.
"""
from unittest.mock import patch
from tools.cronjob_tools import _origin_from_env
def _session_env(env: dict):
"""Patch gateway.session_context.get_session_env with a dict lookup."""
return patch(
"gateway.session_context.get_session_env",
side_effect=lambda name, default="": env.get(name, default),
)
class TestSlackSyntheticThreadCapture:
def test_synthetic_slack_thread_not_captured(self):
"""thread_id == message_id on Slack = per-message session key: drop it."""
env = {
"HERMES_SESSION_PLATFORM": "slack",
"HERMES_SESSION_CHAT_ID": "D0BJTDCSR7C",
"HERMES_SESSION_THREAD_ID": "1755043010.123456",
"HERMES_SESSION_MESSAGE_ID": "1755043010.123456",
}
with _session_env(env):
origin = _origin_from_env()
assert origin is not None
assert origin["platform"] == "slack"
assert origin["chat_id"] == "D0BJTDCSR7C"
assert origin["thread_id"] is None
def test_genuine_slack_thread_preserved(self):
"""A real in-thread creation (thread != own message id) keeps its thread."""
env = {
"HERMES_SESSION_PLATFORM": "slack",
"HERMES_SESSION_CHAT_ID": "C0AGENERAL",
"HERMES_SESSION_THREAD_ID": "1755040000.000100",
"HERMES_SESSION_MESSAGE_ID": "1755043010.123456",
}
with _session_env(env):
origin = _origin_from_env()
assert origin is not None
assert origin["thread_id"] == "1755040000.000100"
def test_non_slack_platform_thread_untouched(self):
"""Telegram forum topics legitimately reuse ids; the rule is Slack-scoped."""
env = {
"HERMES_SESSION_PLATFORM": "telegram",
"HERMES_SESSION_CHAT_ID": "-1003941067111",
"HERMES_SESSION_THREAD_ID": "2203",
"HERMES_SESSION_MESSAGE_ID": "2203",
}
with _session_env(env):
origin = _origin_from_env()
assert origin is not None
assert origin["thread_id"] == "2203"
def test_slack_no_message_id_keeps_thread(self):
"""Without a message id to compare, never guess: keep the thread."""
env = {
"HERMES_SESSION_PLATFORM": "slack",
"HERMES_SESSION_CHAT_ID": "D0BJTDCSR7C",
"HERMES_SESSION_THREAD_ID": "1755040000.000100",
}
with _session_env(env):
origin = _origin_from_env()
assert origin is not None
assert origin["thread_id"] == "1755040000.000100"