Add clearer regional scoping for bots. Closes #300.

This commit is contained in:
Jack Kingsman
2026-07-08 17:08:04 -07:00
parent 97b873e991
commit 4385ea5703
6 changed files with 246 additions and 12 deletions
+103
View File
@@ -497,6 +497,109 @@ def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name,
)
assert result == "ok:Hi"
def test_kwargs_bot_receives_scoped(self):
"""Bots using **kwargs receive `scoped` for a region-scoped message (#300)."""
code = """
def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, **kwargs):
return f"scoped={kwargs.get('scoped', 'missing')}"
"""
result = execute_bot_code(
code=code,
sender_name="Someone",
sender_key=None,
message_text="Hi",
is_dm=False,
channel_key="AABBCCDD",
channel_name="#general",
sender_timestamp=None,
path=None,
region="EU",
scoped=True,
)
assert result == "scoped=True"
def test_named_scoped_param_bot_receives_scoped(self):
"""Bots may opt into `scoped` by naming the parameter (with a default)."""
code = """
def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, scoped=False):
return f"scoped={scoped}"
"""
result = execute_bot_code(
code=code,
sender_name="Someone",
sender_key=None,
message_text="Hi",
is_dm=False,
channel_key="AABBCCDD",
channel_name="#general",
sender_timestamp=None,
path=None,
scoped=True,
)
assert result == "scoped=True"
def test_scoped_true_region_none_disambiguates_unknown_region(self):
"""scoped=True with region=None means 'scoped but region unknown' (#300.1)."""
code = """
def bot(**kwargs):
return f"scoped={kwargs.get('scoped')},region={kwargs.get('region')}"
"""
result = execute_bot_code(
code=code,
sender_name="Someone",
sender_key=None,
message_text="Hi",
is_dm=False,
channel_key="AABBCCDD",
channel_name="#general",
sender_timestamp=None,
path=None,
region=None,
scoped=True,
)
assert result == "scoped=True,region=None"
def test_scoped_defaults_false_for_unscoped_message(self):
"""scoped is delivered as False (not absent) for plain/unscoped flood."""
code = """
def bot(**kwargs):
return f"scoped={kwargs.get('scoped', 'missing')}"
"""
result = execute_bot_code(
code=code,
sender_name="Alice",
sender_key="abc123",
message_text="Hi",
is_dm=True,
channel_key=None,
channel_name=None,
sender_timestamp=None,
path=None,
)
assert result == "scoped=False"
def test_legacy_positional_bot_unaffected_by_scoped(self):
"""Historical positional bots keep binding unchanged; scoped is never passed positionally."""
code = """
def bot(sender_name, sender_key, message_text, is_dm, channel_key, channel_name, sender_timestamp, path, is_outgoing):
return f"ok:{message_text}"
"""
result = execute_bot_code(
code=code,
sender_name="Alice",
sender_key="abc123",
message_text="Hi",
is_dm=True,
channel_key=None,
channel_name=None,
sender_timestamp=None,
path=None,
is_outgoing=False,
region="EU",
scoped=True,
)
assert result == "ok:Hi"
def test_dict_return_with_region_produces_bot_reply(self):
"""A {"region", "message"} return becomes a BotReply with a normalized scope (#300)."""
from app.fanout.bot_exec import BotReply
+109
View File
@@ -39,6 +39,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["is_outgoing"] = is_outgoing
captured["is_dm"] = is_dm
@@ -90,6 +91,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["is_outgoing"] = is_outgoing
return None
@@ -138,6 +140,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["path"] = path
captured["path_bytes_per_hop"] = path_bytes_per_hop
@@ -188,6 +191,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["region"] = region
return None
@@ -238,6 +242,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["region"] = region
return None
@@ -264,6 +269,107 @@ class TestBotModuleParameterExtraction:
assert captured["region"] is None
@pytest.mark.asyncio
async def test_scoped_true_when_transport_code_present(self):
"""A message with a transport_code forwards scoped=True (#300)."""
from app.fanout.bot import BotModule
captured = {}
def fake_execute(
code,
sender_name,
sender_key,
message_text,
is_dm,
channel_key,
channel_name,
sender_timestamp,
path,
is_outgoing,
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["scoped"] = scoped
return None
mod = BotModule("test", {"code": "def bot(**k): pass"}, name="Test")
with (
patch("app.fanout.bot_exec.execute_bot_code", side_effect=fake_execute),
patch(
"app.fanout.bot_exec._bot_semaphore",
MagicMock(__aenter__=AsyncMock(), __aexit__=AsyncMock()),
),
patch("app.fanout.bot.asyncio.sleep", new_callable=AsyncMock),
patch("app.repository.ChannelRepository") as mock_chan,
):
mock_chan.get_by_key = AsyncMock(return_value=MagicMock(name="#test"))
await mod._run_for_message(
{
"type": "CHAN",
"conversation_key": "ch1",
"text": "Alice: hello",
"sender_name": "Alice",
"channel_name": "#test",
# Scoped but region unresolved: scoped must still be True.
"transport_code": 6789,
"region": None,
}
)
assert captured["scoped"] is True
@pytest.mark.asyncio
async def test_scoped_false_when_transport_code_absent(self):
"""A message without a transport_code forwards scoped=False (unscoped flood)."""
from app.fanout.bot import BotModule
captured = {}
def fake_execute(
code,
sender_name,
sender_key,
message_text,
is_dm,
channel_key,
channel_name,
sender_timestamp,
path,
is_outgoing,
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["scoped"] = scoped
return None
mod = BotModule("test", {"code": "def bot(**k): pass"}, name="Test")
with (
patch("app.fanout.bot_exec.execute_bot_code", side_effect=fake_execute),
patch(
"app.fanout.bot_exec._bot_semaphore",
MagicMock(__aenter__=AsyncMock(), __aexit__=AsyncMock()),
),
patch("app.fanout.bot.asyncio.sleep", new_callable=AsyncMock),
patch("app.repository.ContactRepository") as mock_contact,
):
mock_contact.get_by_key = AsyncMock(return_value=MagicMock(name="Alice"))
await mod._run_for_message(
{
"type": "PRIV",
"conversation_key": "pk1",
"text": "hello",
}
)
assert captured["scoped"] is False
@pytest.mark.asyncio
async def test_channel_sender_prefix_stripped(self):
"""Channel message text has 'SenderName: ' prefix stripped."""
@@ -285,6 +391,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["message_text"] = message_text
captured["sender_name"] = sender_name
@@ -335,6 +442,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["channel_name"] = channel_name
return None
@@ -384,6 +492,7 @@ class TestBotModuleParameterExtraction:
path_bytes_per_hop,
packet_hash,
region,
scoped,
):
captured["sender_name"] = sender_name
captured["sender_key"] = sender_key