rl-hackathon / tests /test_environment.py
Shivacode's picture
Email triage RL
753b5ee
Raw History Blame Contribute Delete
8.09 kB
"""
tests/test_environment.py β€” Unit tests for the Email Triage environment.
Tests:
- reset() returns a valid TriageObservation
- step() returns correct reward type and range
- All 3 task levels complete without errors
- Graders return scores in [0.0, 1.0]
- Perfect action scores >= 0.9
- Wrong action scores <= 0.5
"""
import pytest
import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
from email_triage_rl_hackathon.server.my_environment import EmailTriageEnvironment
from email_triage_rl_hackathon.models import TriageAction
from email_triage_rl_hackathon.server.graders import grade_easy, grade_medium, grade_hard
from email_triage_rl_hackathon.server.email_data import EASY_EMAILS, MEDIUM_EMAILS, HARD_EMAILS
# ─────────────────────────────────────────────
# Helpers
# ─────────────────────────────────────────────
def perfect_action(email) -> TriageAction:
"""Return the ground-truth action for an email."""
return TriageAction(
category=email.gt_category,
priority=email.gt_priority,
department=email.gt_department,
reply=" ".join(email.gt_reply_keywords) + " apologize refund investigate dear",
)
def wrong_action() -> TriageAction:
return TriageAction(
category="spam",
priority="low",
department="support",
reply="",
)
# ─────────────────────────────────────────────
# Environment lifecycle tests
# ─────────────────────────────────────────────
class TestEnvironmentLifecycle:
def test_reset_returns_valid_observation(self):
env = EmailTriageEnvironment(task_level="easy")
obs = env.reset()
assert obs.email_id != "none"
assert len(obs.subject) > 0
assert obs.task_level == "easy"
assert obs.done is False
assert 0.0 <= obs.score <= 1.0
def test_reset_with_seed_is_reproducible(self):
env1 = EmailTriageEnvironment(task_level="easy")
env2 = EmailTriageEnvironment(task_level="easy")
obs1 = env1.reset(seed=42)
obs2 = env2.reset(seed=42)
assert obs1.email_id == obs2.email_id
def test_state_returns_state_object(self):
env = EmailTriageEnvironment(task_level="easy")
env.reset()
state = env.state
assert state.episode_id is not None
assert state.step_count == 0
def test_step_increments_step_count(self):
env = EmailTriageEnvironment(task_level="easy")
obs = env.reset()
action = TriageAction(category="spam", priority="low", department="support", reply="")
env.step(action)
assert env.state.step_count == 1
def test_episode_ends_after_all_emails(self):
for level in ["easy", "medium", "hard"]:
env = EmailTriageEnvironment(task_level=level)
obs = env.reset()
action = wrong_action()
done = False
steps = 0
while not done:
result = env.step(action)
done = result.done
steps += 1
assert steps < 20, "Episode did not terminate"
assert done is True
def test_reward_is_in_valid_range(self):
env = EmailTriageEnvironment(task_level="easy")
obs = env.reset()
while not obs.done:
action = TriageAction(category="billing", priority="medium",
department="finance", reply="")
obs = env.step(action)
assert -1.0 <= obs.reward <= 1.0
# ─────────────────────────────────────────────
# Grader tests
# ─────────────────────────────────────────────
class TestGraders:
def test_easy_perfect_score(self):
email = EASY_EMAILS[1] # billing email
score, _ = grade_easy(email, email.gt_category, email.gt_priority, email.gt_department)
assert score == 1.0
def test_easy_all_wrong_score(self):
email = EASY_EMAILS[0] # spam email
score, _ = grade_easy(email, "technical", "urgent", "engineering")
assert score == 0.0
def test_easy_partial_score(self):
email = EASY_EMAILS[1] # billing/high/finance
score, _ = grade_easy(email, "billing", "low", "support")
assert 0.0 < score < 1.0
assert score == pytest.approx(0.40) # only category correct
def test_medium_no_reply_penalised(self):
email = MEDIUM_EMAILS[0]
score, feedback = grade_medium(
email, email.gt_category, email.gt_priority, email.gt_department, ""
)
assert "too short" in feedback or "missing" in feedback
assert score <= 0.5
def test_medium_good_reply_scores_high(self):
email = MEDIUM_EMAILS[0]
reply = "Dear customer, we sincerely apologize for the double charge. We will investigate and refund within 24-48 hours."
score, _ = grade_medium(
email, email.gt_category, email.gt_priority, email.gt_department, reply
)
assert score >= 0.7
def test_hard_short_reply_penalised(self):
email = HARD_EMAILS[0]
score, feedback = grade_hard(
email, email.gt_category, email.gt_priority, email.gt_department, "ok"
)
# Classification correct = 0.40 max; reply too short adds nothing
assert score <= 0.42 # no reply bonus beyond classification
def test_hard_full_reply_scores_high(self):
email = HARD_EMAILS[0]
reply = (
"Dear customer, we sincerely apologize for the unacceptable experience with "
"account ACC-00421. I am personally escalating this to our senior finance manager "
"right now. We will resolve this within 24 hours and a manager will contact you "
"directly. All previous case numbers have been reviewed."
)
score, _ = grade_hard(
email, email.gt_category, email.gt_priority, email.gt_department, reply
)
assert score >= 0.6
def test_scores_always_between_0_and_1(self):
for email in EASY_EMAILS + MEDIUM_EMAILS + HARD_EMAILS:
if email.task_level == "easy":
s, _ = grade_easy(email, "spam", "low", "support")
elif email.task_level == "medium":
s, _ = grade_medium(email, "spam", "low", "support", "")
else:
s, _ = grade_hard(email, "spam", "low", "support", "")
assert 0.0 <= s <= 1.0, f"Score {s} out of range for {email.email_id}"
# ─────────────────────────────────────────────
# Full episode integration tests
# ─────────────────────────────────────────────
class TestFullEpisode:
def test_perfect_easy_episode_scores_1(self):
env = EmailTriageEnvironment(task_level="easy")
obs = env.reset(seed=0)
total = 0.0
steps = 0
while not obs.done:
# Find current email's ground truth from email_data
from email_triage_rl_hackathon.server.email_data import ALL_EMAILS
email = ALL_EMAILS.get(obs.email_id)
assert email is not None, f"Unknown email_id: {obs.email_id}"
action = perfect_action(email)
obs = env.step(action)
total += obs.reward or 0.0
steps += 1
assert steps > 0
avg = total / steps
assert avg >= 0.9, f"Perfect easy episode should score β‰₯0.9, got {avg:.2f}"