"""Unit coverage for SPEC-003 injection gates (SAF-01..03).""" import tempfile import unittest from fjerkroa_bot.ai_responder import AIMessage, AIResponder, sanitize_external_text from .test_main import TestBotBase class TestChannelRoutingGate(TestBotBase): async def test_default_allowed_set_from_config_names(self): """SAF-01: default allowed set = channels named in config; everything else refused.""" self.bot.config = dict(self.bot.config) self.bot.config.update( {"chat-channel": "general", "staff-channel": "staff", "welcome-channel": "welcome", "additional-responders": ["games"]} ) self.assertTrue(self.bot.routing_allowed("general")) self.assertTrue(self.bot.routing_allowed("staff")) self.assertTrue(self.bot.routing_allowed("games")) self.assertFalse(self.bot.routing_allowed("random-channel")) self.assertFalse(self.bot.routing_allowed(None)) async def test_explicit_allowlist_wins(self): """SAF-01: configured allowed-channels replaces the default set.""" self.bot.config = dict(self.bot.config) self.bot.config.update({"chat-channel": "general", "allowed-channels": ["announcements"]}) self.assertTrue(self.bot.routing_allowed("announcements")) self.assertFalse(self.bot.routing_allowed("general")) class TestAllowedMentions(TestBotBase): async def test_outbound_pings_disabled(self): """SAF-02: the bot is constructed with allowed_mentions = none.""" mentions = self.bot.allowed_mentions self.assertIsNotNone(mentions) self.assertFalse(mentions.everyone) self.assertFalse(mentions.users) self.assertFalse(mentions.roles) class TestSanitizeExternalText(unittest.TestCase): def test_sanitizer_strips_and_caps(self): """SAF-03: control chars stripped, @everyone/@here neutralized, length capped.""" dirty = "hei\x00\x1b[31m @everyone @here " + "x" * 5000 clean = sanitize_external_text(dirty, max_len=4000) self.assertNotIn("\x00", clean) self.assertNotIn("\x1b", clean) self.assertNotIn("@everyone", clean) self.assertNotIn("@here", clean) self.assertLessEqual(len(clean), 4000) self.assertIn("hei", clean) def test_news_content_sanitized_into_prompt(self): """SAF-03: news file content passes the sanitizer before prompt injection.""" with tempfile.NamedTemporaryFile("w", suffix=".txt", delete=False) as fd: fd.write("Breaking: @everyone \x00 click here") news_path = fd.name config = {"system": "News: {news}", "history-limit": 5, "news": news_path} responder = AIResponder(config, "chat") system = responder.message(AIMessage("alice", "hei"))[0]["content"] self.assertNotIn("@everyone", system) self.assertNotIn("\x00", system) self.assertIn("Breaking:", system)