Files
hermes-agent/tests/gateway/test_relay_source_platform_metrics.py
teknium1 8dfdc46067 fix(telemetry): relay-stamped turns are gateway messages; a multi-platform connector's unknown chat stays relay (M5 part, m9)
What:
- M5: `relay` is a gateway surface alias, so an inbound the connector could not stamp is
  entrypoint gateway_message / surface gateway (it counts as engagement) instead of other/none;
  `relay` joins the task/session gateway platform set (it was collapsing to `plugin`).
- m9: RelayAdapter._metrics_platform labels a chat by its inbound platform, and falls back to the
  connector's primary platform only when the socket fronts exactly one identity; _source_platform
  uses it (egress routing keeps _chat_platform unchanged).

Why: the same turn was `other/none` in task/session while delivery/latency said `discord`; an
unknown chat on a multi-platform connector was misattributed to the primary platform.

Tests: the existing unresolvable-chat stub now implements _metrics_platform (renamed hook). New:
task_start_fields({"platform": "relay"}) -> gateway_message/gateway/relay; unstamped chat on a
two-identity connector -> delivery platform `relay`. Both RED on the previous commit
(other/other/none; discord).
Probe (review/engagement/relay/probe_d_coverage.py), before: source.platform='relay' ->
{'entrypoint': 'other', 'execution_surface': 'other', 'platform': 'none'}, engagement surface None.
After: the contract test above.

Not done: resolving the turn-start task platform to the connector's platform (needs the adapter at
agent construction) and M6 (delivery row profile under multiplex); see NOT_COVERED.md.
2026-09-28 12:43:03 -07:00

84 lines
3.9 KiB
Python

"""Replies the relay connector carries are labelled with the platform the conversation lives on."""
import asyncio
from types import SimpleNamespace
import pytest
from gateway.config import Platform, PlatformConfig
from gateway.platforms.event import MessageEvent, MessageType
from gateway.relay.adapter import RelayAdapter
from gateway.session import SessionSource
from gateway.stream_consumer import GatewayStreamConsumer, StreamConsumerConfig
from hermes_cli.observability import shared_metrics_gateway as smg
from tests.gateway.relay.test_relay_adapter import _CaptureTransport, make_desc
from tests.gateway.test_platform_shared_metrics import rows # noqa: F401 - fixture
def _relay(platform="telegram"):
return RelayAdapter(PlatformConfig(), make_desc(platform=platform), transport=_CaptureTransport())
def _inbound(adapter, platform, chat_id):
event = MessageEvent(text="hi", message_type=MessageType.TEXT, message_id=f"in-{chat_id}",
source=SessionSource(platform=platform, chat_id=chat_id, chat_type="dm", user_id="u1"))
adapter._capture_scope(event)
return event
def test_multiplatform_relay_replies_are_labelled_by_the_inbound_platform(rows):
relay = _relay()
discord = _inbound(relay, Platform.DISCORD, "c1")
slack = _inbound(relay, Platform.SLACK, "c2")
smg.start_reply_clock(discord.source)
assert asyncio.run(GatewayStreamConsumer(relay, "c1", StreamConsumerConfig(cursor=""))._send_or_edit("chunk"))
smg.start_reply_clock(slack.source)
asyncio.run(relay.send_final_ledgered(slack, "k", "final answer", {}, reply_to=None))
assert rows("hermes.gateway.reply_latency") == [
{"first_response_bucket": "lt_2s", "platform": "discord"},
{"first_response_bucket": "lt_2s", "platform": "slack"},
]
# Streamed chunks send directly; the final reply is the ledgered, counted delivery.
assert rows("hermes.platform.delivery") == [{"failure_class": "none", "outcome": "sent", "platform": "slack"}]
def test_single_platform_relay_inbound_is_labelled_by_the_connector_platform(rows):
"""An inbound that only says 'relay' resolves to the platform the connector fronts."""
relay = _relay("discord")
event = _inbound(relay, Platform.RELAY, "c9")
smg.start_reply_clock(event.source)
asyncio.run(relay.send_final_ledgered(event, "k", "final answer", {}, reply_to=None))
assert rows("hermes.gateway.reply_latency") == [{"first_response_bucket": "lt_2s", "platform": "discord"}]
assert rows("hermes.platform.delivery") == [{"failure_class": "none", "outcome": "sent", "platform": "discord"}]
@pytest.mark.parametrize("chat_platform", [None, RuntimeError("descriptor gone")])
def test_an_unresolvable_relay_chat_keeps_the_relay_label(rows, chat_platform):
class _Front:
platform = Platform.RELAY
def _metrics_platform(self, chat_id):
if isinstance(chat_platform, BaseException):
raise chat_platform
return chat_platform
smg._record_delivery(_Front(), SimpleNamespace(success=True), chat_id="c1")
assert rows("hermes.platform.delivery") == [{"failure_class": "none", "outcome": "sent", "platform": "relay"}]
def test_an_unstamped_chat_on_a_multiplatform_connector_is_not_labelled_as_its_primary(rows):
relay = _relay("discord")
relay._transport._identities = [("discord", "bot-a"), ("slack", "bot-b")]
event = _inbound(relay, Platform.RELAY, "c7")
smg.start_reply_clock(event.source)
asyncio.run(relay.send_final_ledgered(event, "k", "final answer", {}, reply_to=None))
assert rows("hermes.platform.delivery") == [{"failure_class": "none", "outcome": "sent", "platform": "relay"}]
def test_a_relay_stamped_turn_is_a_gateway_message():
from hermes_cli.observability.shared_metrics_contract import task_start_fields
assert task_start_fields({"platform": "relay"}) == {
"entrypoint": "gateway_message", "execution_surface": "gateway", "platform": "relay"}