mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-18 04:41:56 +00:00
test(google-chat): cover relay-declared sender_type honoring
Adds five regression tests for the Format 3 (Cloud Run relay) envelope
path:
- test_relay_flat_honors_declared_sender_type_bot: BOT sender_type
propagates to msg['sender']['type'].
- test_relay_flat_defaults_sender_type_human_when_absent: backward
compat \u2014 missing field still flows as HUMAN.
- test_relay_flat_coerces_unknown_sender_type_to_human: defensive
coercion \u2014 strip+upper normalizes whitespace/case, anything outside
{HUMAN, BOT} falls back to HUMAN.
- test_relay_flat_bot_sender_is_filtered_end_to_end: end-to-end
through _on_pubsub_message \u2014 a relay envelope with sender_type=BOT
is dropped by the BOT self-filter without dispatch.
- test_relay_flat_human_sender_dispatches: end-to-end negative
control \u2014 human relay envelopes still reach the agent loop.
Also clarifies the operator contract in the adapter comment: the
relay must forward upstream sender.type as envelope.sender_type,
otherwise bot replies forwarded as HUMAN cannot be distinguished
from genuine humans by this filter.
This commit is contained in:
parent
c386400040
commit
8578f898cb
2 changed files with 116 additions and 0 deletions
|
|
@ -1018,6 +1018,11 @@ class GoogleChatAdapter(BasePlatformAdapter):
|
|||
# impersonate any allowlisted user without ever being marked
|
||||
# as a bot. Default to "HUMAN" for backward compatibility when
|
||||
# the relay does not provide the field.
|
||||
#
|
||||
# Operator contract: the relay MUST forward sender.type from
|
||||
# the upstream Chat event as ``sender_type``. Relays that
|
||||
# forward bot replies as HUMAN (or omit the field) cannot be
|
||||
# distinguished from genuine humans here.
|
||||
sender_type_raw = (envelope.get("sender_type") or "HUMAN")
|
||||
sender_type = str(sender_type_raw).strip().upper() or "HUMAN"
|
||||
if sender_type not in {"HUMAN", "BOT"}:
|
||||
|
|
|
|||
|
|
@ -485,6 +485,49 @@ class TestOnPubsubMessage:
|
|||
submit.assert_not_called()
|
||||
msg.ack.assert_called_once()
|
||||
|
||||
def test_relay_flat_bot_sender_is_filtered_end_to_end(self, adapter):
|
||||
"""Format 3 end-to-end: a relay envelope declaring sender_type=BOT
|
||||
flows through ``_extract_message_payload`` → ``_on_pubsub_message``
|
||||
and is dropped by the BOT self-filter without dispatch. This is
|
||||
the actual security contract (the unit tests on
|
||||
``_extract_message_payload`` only assert the intermediate dict
|
||||
shape; this test asserts the dispatch is suppressed).
|
||||
"""
|
||||
envelope = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "bot@bots.example.com",
|
||||
"sender_display_name": "HermesBot",
|
||||
"sender_type": "BOT",
|
||||
"text": "reply from bot",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
msg = _make_pubsub_message(envelope)
|
||||
with patch.object(adapter, "_submit_on_loop") as submit:
|
||||
adapter._on_pubsub_message(msg)
|
||||
submit.assert_not_called()
|
||||
msg.ack.assert_called_once()
|
||||
|
||||
def test_relay_flat_human_sender_dispatches(self, adapter):
|
||||
"""Format 3 negative control: an envelope without sender_type
|
||||
(or with sender_type=HUMAN) still dispatches to the agent loop,
|
||||
confirming the BOT-filter doesn't accidentally drop legitimate
|
||||
human messages from a relay.
|
||||
"""
|
||||
envelope = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "alice@example.com",
|
||||
"sender_display_name": "Alice",
|
||||
"text": "hello agent",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
msg = _make_pubsub_message(envelope)
|
||||
with patch.object(adapter, "_submit_on_loop") as submit:
|
||||
adapter._on_pubsub_message(msg)
|
||||
submit.assert_called_once()
|
||||
msg.ack.assert_called_once()
|
||||
|
||||
def test_duplicate_message_dropped(self, adapter):
|
||||
env = _make_chat_envelope(msg_name="spaces/S/messages/DUP.DUP")
|
||||
# Prime dedup
|
||||
|
|
@ -603,6 +646,74 @@ class TestExtractMessagePayload:
|
|||
assert msg["name"] == "spaces/RELAY/messages/M.M"
|
||||
assert space["name"] == "spaces/RELAY"
|
||||
|
||||
def test_relay_flat_honors_declared_sender_type_bot(self):
|
||||
"""Format 3 propagates ``envelope.sender_type`` so the downstream
|
||||
BOT self-filter fires for relay-forwarded bot replies.
|
||||
|
||||
Without this, a relay misconfigured to forward the bot's own
|
||||
replies into the same Pub/Sub topic produced a feedback loop:
|
||||
the adapter would mark the synthesized sender ``HUMAN`` and the
|
||||
``sender.type == "BOT"`` self-filter would never fire.
|
||||
"""
|
||||
envelope = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "bot@bots.example.com",
|
||||
"sender_display_name": "HermesBot",
|
||||
"sender_type": "BOT",
|
||||
"text": "reply from bot",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
result = GoogleChatAdapter._extract_message_payload(envelope)
|
||||
assert result is not None
|
||||
msg, _space, fmt = result
|
||||
assert fmt == "relay_flat"
|
||||
assert msg["sender"]["type"] == "BOT"
|
||||
|
||||
def test_relay_flat_defaults_sender_type_human_when_absent(self):
|
||||
"""Backward compatibility: relays that don't declare sender_type
|
||||
continue to flow as HUMAN exactly as before this change."""
|
||||
envelope = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "alice@example.com",
|
||||
"text": "hi",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
result = GoogleChatAdapter._extract_message_payload(envelope)
|
||||
assert result is not None
|
||||
msg, _space, _fmt = result
|
||||
assert msg["sender"]["type"] == "HUMAN"
|
||||
|
||||
def test_relay_flat_coerces_unknown_sender_type_to_human(self):
|
||||
"""Defensive coercion: only ``HUMAN`` and ``BOT`` are accepted;
|
||||
any other value (including stray casing on those two) is either
|
||||
normalized or falls back to ``HUMAN`` so a malformed relay can't
|
||||
slip an unrecognized type through to the downstream filter."""
|
||||
# Lower / mixed case is normalized to upper.
|
||||
envelope_lower = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "bot@example.com",
|
||||
"sender_type": " bot ",
|
||||
"text": "hi",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
msg, _space, _fmt = GoogleChatAdapter._extract_message_payload(envelope_lower)
|
||||
assert msg["sender"]["type"] == "BOT"
|
||||
|
||||
# Unknown value falls back to HUMAN, not the raw string.
|
||||
envelope_bogus = {
|
||||
"event_type": "MESSAGE",
|
||||
"sender_email": "alice@example.com",
|
||||
"sender_type": "ROBOT",
|
||||
"text": "hi",
|
||||
"space_name": "spaces/RELAY",
|
||||
"message_name": "spaces/RELAY/messages/M.M",
|
||||
}
|
||||
msg, _space, _fmt = GoogleChatAdapter._extract_message_payload(envelope_bogus)
|
||||
assert msg["sender"]["type"] == "HUMAN"
|
||||
|
||||
def test_unrecognized_envelope_returns_none(self):
|
||||
"""Random JSON with no known shape returns None (caller acks)."""
|
||||
envelope = {"foo": "bar", "baz": 123}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue