mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-26 06:01:49 +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
|
# impersonate any allowlisted user without ever being marked
|
||||||
# as a bot. Default to "HUMAN" for backward compatibility when
|
# as a bot. Default to "HUMAN" for backward compatibility when
|
||||||
# the relay does not provide the field.
|
# 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_raw = (envelope.get("sender_type") or "HUMAN")
|
||||||
sender_type = str(sender_type_raw).strip().upper() or "HUMAN"
|
sender_type = str(sender_type_raw).strip().upper() or "HUMAN"
|
||||||
if sender_type not in {"HUMAN", "BOT"}:
|
if sender_type not in {"HUMAN", "BOT"}:
|
||||||
|
|
|
||||||
|
|
@ -485,6 +485,49 @@ class TestOnPubsubMessage:
|
||||||
submit.assert_not_called()
|
submit.assert_not_called()
|
||||||
msg.ack.assert_called_once()
|
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):
|
def test_duplicate_message_dropped(self, adapter):
|
||||||
env = _make_chat_envelope(msg_name="spaces/S/messages/DUP.DUP")
|
env = _make_chat_envelope(msg_name="spaces/S/messages/DUP.DUP")
|
||||||
# Prime dedup
|
# Prime dedup
|
||||||
|
|
@ -603,6 +646,74 @@ class TestExtractMessagePayload:
|
||||||
assert msg["name"] == "spaces/RELAY/messages/M.M"
|
assert msg["name"] == "spaces/RELAY/messages/M.M"
|
||||||
assert space["name"] == "spaces/RELAY"
|
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):
|
def test_unrecognized_envelope_returns_none(self):
|
||||||
"""Random JSON with no known shape returns None (caller acks)."""
|
"""Random JSON with no known shape returns None (caller acks)."""
|
||||||
envelope = {"foo": "bar", "baz": 123}
|
envelope = {"foo": "bar", "baz": 123}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue