/ tests / gateway / test_reply_to_injection.py
test_reply_to_injection.py
  1  """Tests for reply-to pointer injection in _prepare_inbound_message_text.
  2  
  3  The `[Replying to: "..."]` prefix is a *disambiguation pointer*, not
  4  deduplication. It must always be injected when the user explicitly replies
  5  to a prior message — even when the quoted text already exists somewhere
  6  in the conversation history. History can contain the same or similar text
  7  multiple times, and without an explicit pointer the agent has to guess
  8  which prior message the user is referencing.
  9  """
 10  import pytest
 11  
 12  from gateway.config import GatewayConfig, Platform, PlatformConfig
 13  from gateway.platforms.base import MessageEvent
 14  from gateway.run import GatewayRunner
 15  from gateway.session import SessionSource
 16  
 17  
 18  def _make_runner() -> GatewayRunner:
 19      runner = object.__new__(GatewayRunner)
 20      runner.config = GatewayConfig(
 21          platforms={Platform.TELEGRAM: PlatformConfig(enabled=True, token="fake")},
 22      )
 23      runner.adapters = {}
 24      runner._model = "openai/gpt-4.1-mini"
 25      runner._base_url = None
 26      return runner
 27  
 28  
 29  def _source() -> SessionSource:
 30      return SessionSource(
 31          platform=Platform.TELEGRAM,
 32          chat_id="123",
 33          chat_name="DM",
 34          chat_type="private",
 35          user_name="Alice",
 36      )
 37  
 38  
 39  @pytest.mark.asyncio
 40  async def test_reply_prefix_injected_when_text_absent_from_history():
 41      runner = _make_runner()
 42      source = _source()
 43      event = MessageEvent(
 44          text="What's the best time to go?",
 45          source=source,
 46          reply_to_message_id="42",
 47          reply_to_text="Japan is great for culture, food, and efficiency.",
 48      )
 49  
 50      result = await runner._prepare_inbound_message_text(
 51          event=event,
 52          source=source,
 53          history=[{"role": "user", "content": "unrelated"}],
 54      )
 55  
 56      assert result is not None
 57      assert result.startswith(
 58          '[Replying to: "Japan is great for culture, food, and efficiency."]'
 59      )
 60      assert result.endswith("What's the best time to go?")
 61  
 62  
 63  @pytest.mark.asyncio
 64  async def test_reply_prefix_still_injected_when_text_in_history():
 65      """Regression test: the pointer must survive even when the quoted text
 66      already appears in history. Previously a `found_in_history` guard
 67      silently dropped the prefix, leaving the agent to guess which prior
 68      message the user was referencing."""
 69      runner = _make_runner()
 70      source = _source()
 71      quoted = "Japan is great for culture, food, and efficiency."
 72      event = MessageEvent(
 73          text="What's the best time to go?",
 74          source=source,
 75          reply_to_message_id="42",
 76          reply_to_text=quoted,
 77      )
 78  
 79      history = [
 80          {"role": "user", "content": "I'm thinking of going to Japan or Italy."},
 81          {
 82              "role": "assistant",
 83              "content": (
 84                  f"{quoted} Italy is better if you prefer a relaxed pace."
 85              ),
 86          },
 87          {"role": "user", "content": "How long should I stay?"},
 88          {"role": "assistant", "content": "For Japan, 10-14 days is ideal."},
 89      ]
 90  
 91      result = await runner._prepare_inbound_message_text(
 92          event=event,
 93          source=source,
 94          history=history,
 95      )
 96  
 97      assert result is not None
 98      assert result.startswith(f'[Replying to: "{quoted}"]')
 99      assert result.endswith("What's the best time to go?")
100  
101  
102  @pytest.mark.asyncio
103  async def test_no_prefix_without_reply_context():
104      runner = _make_runner()
105      source = _source()
106      event = MessageEvent(text="hello", source=source)
107  
108      result = await runner._prepare_inbound_message_text(
109          event=event,
110          source=source,
111          history=[],
112      )
113  
114      assert result == "hello"
115  
116  
117  @pytest.mark.asyncio
118  async def test_no_prefix_when_reply_to_text_is_empty():
119      """reply_to_message_id alone without text (e.g. a reply to a media-only
120      message) should not produce an empty `[Replying to: ""]` prefix."""
121      runner = _make_runner()
122      source = _source()
123      event = MessageEvent(
124          text="hi",
125          source=source,
126          reply_to_message_id="42",
127          reply_to_text=None,
128      )
129  
130      result = await runner._prepare_inbound_message_text(
131          event=event,
132          source=source,
133          history=[],
134      )
135  
136      assert result == "hi"
137  
138  
139  @pytest.mark.asyncio
140  async def test_reply_snippet_truncated_to_500_chars():
141      runner = _make_runner()
142      source = _source()
143      long_text = "x" * 800
144      event = MessageEvent(
145          text="follow-up",
146          source=source,
147          reply_to_message_id="42",
148          reply_to_text=long_text,
149      )
150  
151      result = await runner._prepare_inbound_message_text(
152          event=event,
153          source=source,
154          history=[],
155      )
156  
157      assert result is not None
158      assert result.startswith('[Replying to: "' + "x" * 500 + '"]')
159      assert "x" * 501 not in result