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.
84 lines
3.9 KiB
Python
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"}
|