howl/tests/test_classifier.py

92 lines
3.2 KiB
Python
Raw Permalink Normal View History

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"