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:
kshitijk4poor 2026-05-09 14:44:03 +05:30 committed by kshitij
parent c386400040
commit 8578f898cb
2 changed files with 116 additions and 0 deletions

View file

@ -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"}:

View file

@ -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}