""" Unit tests for WhatsApp business-function scoping gate. Tests CHAN-04: Business-function scoping enforces Meta 2026 policy. Two-tier gate: Tier 1: is_clearly_off_topic — keyword overlap check (no LLM call) Tier 2: Borderline messages pass to LLM with scoping in system prompt Covers: - is_clearly_off_topic returns False when message has keyword overlap - is_clearly_off_topic returns True when message has zero keyword overlap - is_clearly_off_topic returns False when allowed_functions is empty (no scoping) - Canned redirect message format includes agent name and allowed topics - Borderline messages (partial overlap) pass through tier 1 - Edge cases: empty text, case insensitivity """ from __future__ import annotations import pytest class TestIsClearlyOffTopic: """Tests for the tier 1 keyword scoping gate.""" def test_matching_keywords_returns_false(self) -> None: """Message with keyword overlap must NOT be considered off-topic.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["order tracking", "customer support", "returns"] text = "I need help with my order" assert is_clearly_off_topic(text, allowed) is False def test_zero_overlap_returns_true(self) -> None: """Message with zero keyword overlap must be considered off-topic.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["order tracking", "customer support", "returns"] text = "Can you write me a poem about cats?" assert is_clearly_off_topic(text, allowed) is True def test_empty_allowed_functions_returns_false(self) -> None: """Empty allowed_functions means no scoping — nothing is off-topic.""" from gateway.channels.whatsapp import is_clearly_off_topic assert is_clearly_off_topic("anything goes", []) is False assert is_clearly_off_topic("write me a poem", []) is False def test_case_insensitive_matching(self) -> None: """Keyword matching must be case-insensitive.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["Order Tracking", "Customer Support"] text = "CUSTOMER needs SUPPORT with their order" assert is_clearly_off_topic(text, allowed) is False def test_empty_message_returns_false(self) -> None: """Empty message should not be considered clearly off-topic.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["customer support"] assert is_clearly_off_topic("", allowed) is False assert is_clearly_off_topic(" ", allowed) is False def test_single_word_overlap_returns_false(self) -> None: """Single keyword match is enough to pass tier 1.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["billing", "payment", "invoice"] text = "I need help with my billing" assert is_clearly_off_topic(text, allowed) is False def test_off_topic_politics_returns_true(self) -> None: """Clearly political message is off-topic for a customer support agent.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["customer support", "order tracking", "returns", "billing"] text = "What do you think about the election?" assert is_clearly_off_topic(text, allowed) is True def test_off_topic_entertainment_returns_true(self) -> None: """Entertainment request is off-topic for a support agent.""" from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["customer support", "order status", "refunds"] text = "Tell me a joke please" assert is_clearly_off_topic(text, allowed) is True def test_borderline_message_passes_through(self) -> None: """ A borderline message with any keyword overlap must pass tier 1. The LLM handles judgment at tier 2 via system prompt. """ from gateway.channels.whatsapp import is_clearly_off_topic allowed = ["customer support", "product questions", "order tracking"] # "product" appears in allowed_functions — passes tier 1 text = "I have a question about your product pricing strategy" assert is_clearly_off_topic(text, allowed) is False class TestOffTopicReply: """Tests for the canned off-topic redirect message.""" def test_reply_contains_agent_name(self) -> None: """Canned redirect must include the agent's name.""" from gateway.channels.whatsapp import build_off_topic_reply reply = build_off_topic_reply("Mara", ["customer support", "order tracking"]) assert "Mara" in reply def test_reply_contains_allowed_functions(self) -> None: """Canned redirect must mention the allowed functions/topics.""" from gateway.channels.whatsapp import build_off_topic_reply allowed = ["customer support", "order tracking"] reply = build_off_topic_reply("Mara", allowed) assert "customer support" in reply assert "order tracking" in reply def test_reply_is_non_empty_string(self) -> None: """Canned redirect must be a non-empty string.""" from gateway.channels.whatsapp import build_off_topic_reply reply = build_off_topic_reply("AI", ["support"]) assert isinstance(reply, str) assert len(reply) > 0 def test_reply_empty_allowed_functions_graceful(self) -> None: """build_off_topic_reply must not crash with empty allowed_functions.""" from gateway.channels.whatsapp import build_off_topic_reply reply = build_off_topic_reply("Mara", []) assert isinstance(reply, str) assert "Mara" in reply def test_reply_multiple_topics_separated(self) -> None: """Multiple topics in canned redirect must be clearly separated.""" from gateway.channels.whatsapp import build_off_topic_reply allowed = ["billing", "invoicing", "payments"] reply = build_off_topic_reply("Alex", allowed) # All topics must appear in the reply for topic in allowed: assert topic in reply