from __future__ import annotations import pytest from howl.db.queries import SenderMatch from howl.llm.schemas import EmailClassification from howl.pipeline import classifier def make_classification(**kwargs) -> EmailClassification: defaults = { "classification": "customer_inquiry", "confidence": 0.90, "action": "move_customer", "reasoning": "Test classification", "priority": "normal", "requires_human_review": False, "tags": [], } defaults.update(kwargs) return EmailClassification.model_validate(defaults) def test_whitelist_sender_not_moved_to_spam(settings, whitelist_match): classification = make_classification(action="move_spam", confidence=0.85) final_action, overridden = classifier.decide(whitelist_match, classification, settings) assert final_action == "inbox_keep" assert overridden is True def test_customer_sender_not_moved_to_spam(settings, customer_match): classification = make_classification(action="move_spam", confidence=0.85) final_action, overridden = classifier.decide(customer_match, classification, settings) assert final_action == "move_review" assert overridden is True def test_requires_human_review_forces_move_review(settings, unknown_match): classification = make_classification( action="inbox_keep", confidence=0.75, requires_human_review=True, ) final_action, overridden = classifier.decide(unknown_match, classification, settings) assert final_action == "move_review" assert overridden is True def test_low_confidence_falls_back_to_inbox_keep(settings, unknown_match): # Settings has llm_confidence_threshold=0.60, this is 0.45 classification = make_classification( action="move_spam", confidence=0.45, requires_human_review=False, ) final_action, overridden = classifier.decide(unknown_match, classification, settings) assert final_action == "inbox_keep" assert overridden is True def test_high_confidence_customer_passes_through(settings, customer_match): classification = make_classification( action="move_customer", confidence=0.95, requires_human_review=False, ) final_action, overridden = classifier.decide(customer_match, classification, settings) assert final_action == "move_customer" assert overridden is False def test_escalate_not_overridden_by_low_confidence(settings, customer_match): """Escalate action should survive even low confidence — it's already going to human review.""" classification = make_classification( action="escalate", confidence=0.40, requires_human_review=True, ) final_action, overridden = classifier.decide(customer_match, classification, settings) assert final_action == "escalate" def test_flag_follow_up_survives_requires_human_review(settings, unknown_match): """flag_follow_up is an acceptable requires_human_review action — should not be overridden.""" classification = make_classification( action="flag_follow_up", confidence=0.80, requires_human_review=True, ) final_action, overridden = classifier.decide(unknown_match, classification, settings) assert final_action == "flag_follow_up"