og-arin commited on
Commit
240cde9
Β·
verified Β·
1 Parent(s): 0680904

Update test_grader.py

Browse files
Files changed (1) hide show
  1. test_grader.py +227 -0
test_grader.py CHANGED
@@ -0,0 +1,227 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ test_grader.py – Unit tests for grader.py
3
+ ==========================================
4
+
5
+ Run with: pytest test_grader.py -v
6
+
7
+ These tests cover every branch of grade_action() and the edge-cases of
8
+ calculate_overall_score(). They also act as a regression guard β€” any
9
+ change to a reward constant will immediately fail the assertion that was
10
+ relying on it, forcing an intentional review.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import pytest
16
+
17
+ from grader import (
18
+ HEALTH_DRAIN_THRESHOLD,
19
+ PASS_THRESHOLD,
20
+ R_BREACH,
21
+ R_DISRUPTION,
22
+ R_MALWARE_QUARANTINE,
23
+ R_PARTIAL,
24
+ R_PERFECT,
25
+ R_PHISH_BEC_QUARANTINE,
26
+ R_SPAM_BLOCK,
27
+ R_SPAM_QUARANTINE,
28
+ R_WRONG_PROCEDURE,
29
+ calculate_overall_score,
30
+ grade_action,
31
+ )
32
+
33
+
34
+ # ═════════════════════════════════════════════════════════════════════════════
35
+ # CONSTANT SANITY CHECKS
36
+ # ═════════════════════════════════════════════════════════════════════════════
37
+
38
+ class TestRewardConstants:
39
+ def test_all_rewards_in_open_interval(self):
40
+ for r in [R_PERFECT, R_MALWARE_QUARANTINE, R_PHISH_BEC_QUARANTINE,
41
+ R_SPAM_BLOCK, R_SPAM_QUARANTINE, R_WRONG_PROCEDURE,
42
+ R_DISRUPTION, R_BREACH]:
43
+ assert 0.0 < r < 1.0, f"Reward {r} violates open-interval contract"
44
+
45
+ def test_reward_ordering(self):
46
+ assert R_BREACH < R_DISRUPTION < R_WRONG_PROCEDURE
47
+ assert R_WRONG_PROCEDURE < HEALTH_DRAIN_THRESHOLD
48
+ assert HEALTH_DRAIN_THRESHOLD < R_SPAM_QUARANTINE
49
+ assert R_SPAM_QUARANTINE < R_SPAM_BLOCK
50
+ assert R_SPAM_BLOCK < R_PHISH_BEC_QUARANTINE
51
+ assert R_PHISH_BEC_QUARANTINE < R_MALWARE_QUARANTINE
52
+ assert R_MALWARE_QUARANTINE < R_PERFECT
53
+
54
+ def test_partial_alias(self):
55
+ assert R_PARTIAL == R_MALWARE_QUARANTINE
56
+
57
+ def test_health_drain_covers_breach_disruption_wrong(self):
58
+ assert R_BREACH < HEALTH_DRAIN_THRESHOLD
59
+ assert R_DISRUPTION < HEALTH_DRAIN_THRESHOLD
60
+ assert R_WRONG_PROCEDURE < HEALTH_DRAIN_THRESHOLD
61
+
62
+ def test_cautious_scores_never_drain_health(self):
63
+ assert R_SPAM_QUARANTINE >= HEALTH_DRAIN_THRESHOLD
64
+ assert R_SPAM_BLOCK >= HEALTH_DRAIN_THRESHOLD
65
+ assert R_PHISH_BEC_QUARANTINE >= HEALTH_DRAIN_THRESHOLD
66
+ assert R_MALWARE_QUARANTINE >= HEALTH_DRAIN_THRESHOLD
67
+
68
+
69
+ # ═════════════════════════════════════════════════════════════════════════════
70
+ # GRADE_ACTION β€” PERFECT MATCH
71
+ # ═════════════════════════════════════════════════════════════════════════════
72
+
73
+ class TestPerfectTriage:
74
+ @pytest.mark.parametrize("action,etype", [
75
+ ("MOVE_TO_SPAM", "SPAM"),
76
+ ("BLOCK_DOMAIN", "PHISH"),
77
+ ("MARK_SAFE", "SAFE"),
78
+ ("QUARANTINE", "MALWARE"),
79
+ ("QUARANTINE", "BEC"),
80
+ ("BLOCK_DOMAIN", "BEC"),
81
+ ("QUARANTINE", "PHISH"),
82
+ ])
83
+ def test_perfect_match(self, action, etype):
84
+ reward, msg = grade_action(action, action, etype)
85
+ assert reward == R_PERFECT
86
+ assert "PERFECT_TRIAGE" in msg
87
+
88
+ def test_case_insensitive_action(self):
89
+ reward, _ = grade_action("move_to_spam", "MOVE_TO_SPAM", "SPAM")
90
+ assert reward == R_PERFECT
91
+
92
+ def test_case_insensitive_type(self):
93
+ reward, _ = grade_action("MARK_SAFE", "MARK_SAFE", "safe")
94
+ assert reward == R_PERFECT
95
+
96
+
97
+ # ═════════════════════════════════════════════════════════════════════════════
98
+ # GRADE_ACTION β€” SECURITY BREACH
99
+ # ═════════════════════════════════════════════════════════════════════════════
100
+
101
+ class TestSecurityBreach:
102
+ @pytest.mark.parametrize("etype", ["PHISH", "BEC", "MALWARE", "SPAM"])
103
+ def test_mark_safe_on_any_threat_is_breach(self, etype):
104
+ """BUG FIX coverage: SPAM + MARK_SAFE must return R_BREACH (not R_WRONG_PROCEDURE)."""
105
+ reward, msg = grade_action("MARK_SAFE", "BLOCK_DOMAIN", etype)
106
+ assert reward == R_BREACH
107
+ assert "SECURITY_BREACH" in msg
108
+
109
+ def test_spam_mark_safe_is_breach_not_wrong_procedure(self):
110
+ """Regression: the original code excluded SPAM from _THREAT_TYPES."""
111
+ reward, _ = grade_action("MARK_SAFE", "MOVE_TO_SPAM", "SPAM")
112
+ assert reward == R_BREACH, (
113
+ "SPAM + MARK_SAFE must be R_BREACH β€” SPAM was excluded from "
114
+ "_THREAT_TYPES in the v1.0.2 bug"
115
+ )
116
+ assert reward < HEALTH_DRAIN_THRESHOLD # must drain health
117
+
118
+
119
+ # ═════════════════════════════════════════════════════════════════════════════
120
+ # GRADE_ACTION β€” BUSINESS DISRUPTION
121
+ # ═════════════════════════════════════════════════════════════════════════════
122
+
123
+ class TestBusinessDisruption:
124
+ @pytest.mark.parametrize("blocking_action", ["BLOCK_DOMAIN", "QUARANTINE", "MOVE_TO_SPAM"])
125
+ def test_blocking_safe_email_is_disruption(self, blocking_action):
126
+ reward, msg = grade_action(blocking_action, "MARK_SAFE", "SAFE")
127
+ assert reward == R_DISRUPTION
128
+ assert "BUSINESS_DISRUPTION" in msg
129
+
130
+
131
+ # ═════════════════════════════════════════════════════════════════════════════
132
+ # GRADE_ACTION β€” PARTIAL CREDIT
133
+ # ═════════════════════════════════════════════════════════════════════════════
134
+
135
+ class TestPartialCredit:
136
+ def test_malware_quarantine(self):
137
+ reward, msg = grade_action("QUARANTINE", "BLOCK_DOMAIN", "MALWARE")
138
+ assert reward == R_MALWARE_QUARANTINE
139
+ assert "CAUTIOUS" in msg
140
+
141
+ def test_phish_quarantine(self):
142
+ reward, msg = grade_action("QUARANTINE", "BLOCK_DOMAIN", "PHISH")
143
+ assert reward == R_PHISH_BEC_QUARANTINE
144
+ assert "UNDER_RESPONSE" in msg
145
+
146
+ def test_bec_quarantine(self):
147
+ reward, msg = grade_action("QUARANTINE", "BLOCK_DOMAIN", "BEC")
148
+ assert reward == R_PHISH_BEC_QUARANTINE
149
+
150
+ def test_spam_block_domain(self):
151
+ reward, msg = grade_action("BLOCK_DOMAIN", "MOVE_TO_SPAM", "SPAM")
152
+ assert reward == R_SPAM_BLOCK
153
+ assert "OVER_ESCALATION" in msg
154
+
155
+ def test_spam_quarantine(self):
156
+ reward, msg = grade_action("QUARANTINE", "MOVE_TO_SPAM", "SPAM")
157
+ assert reward == R_SPAM_QUARANTINE
158
+ assert "OVER_ESCALATION" in msg
159
+
160
+ @pytest.mark.parametrize("action", ["QUARANTINE", "BLOCK_DOMAIN", "MOVE_TO_SPAM"])
161
+ def test_partial_credits_never_drain_health(self, action):
162
+ reward, _ = grade_action(action, "MOVE_TO_SPAM", "SPAM")
163
+ assert reward >= HEALTH_DRAIN_THRESHOLD
164
+
165
+
166
+ # ═════════════════════════════════════════════════════════════════════════════
167
+ # GRADE_ACTION β€” WRONG PROCEDURE / INVALID
168
+ # ═════════════════════════════════════════════════════════════════════════════
169
+
170
+ class TestWrongProcedure:
171
+ def test_malware_block_domain_is_wrong_procedure(self):
172
+ """lv4 / lv8: expected QUARANTINE, agent picks BLOCK_DOMAIN."""
173
+ reward, msg = grade_action("BLOCK_DOMAIN", "QUARANTINE", "MALWARE")
174
+ assert reward == R_WRONG_PROCEDURE
175
+ assert "INCORRECT_PROCEDURE" in msg
176
+
177
+ def test_invalid_action_token(self):
178
+ reward, msg = grade_action("DELETE", "QUARANTINE", "MALWARE")
179
+ assert reward == R_WRONG_PROCEDURE
180
+ assert "INVALID_ACTION" in msg
181
+
182
+ def test_invalid_action_drains_health(self):
183
+ reward, _ = grade_action("NONSENSE", "QUARANTINE", "PHISH")
184
+ assert reward < HEALTH_DRAIN_THRESHOLD
185
+
186
+
187
+ # ═════════════════════════════════════════════════════════════════════════════
188
+ # CALCULATE_OVERALL_SCORE
189
+ # ═════════════════════════════════════════════════════════════════════════════
190
+
191
+ class TestCalculateOverallScore:
192
+ def test_empty_list_returns_r_breach(self):
193
+ assert calculate_overall_score([]) == R_BREACH
194
+
195
+ def test_all_perfect_returns_r_perfect(self):
196
+ assert calculate_overall_score([R_PERFECT, R_PERFECT, R_PERFECT]) == R_PERFECT
197
+
198
+ def test_result_never_exceeds_r_perfect(self):
199
+ assert calculate_overall_score([1.0, 1.0]) == R_PERFECT
200
+
201
+ def test_result_never_below_r_breach(self):
202
+ assert calculate_overall_score([0.0, 0.0]) == R_BREACH
203
+
204
+ def test_easy_all_perfect(self):
205
+ scores = [R_PERFECT] * 3
206
+ assert calculate_overall_score(scores) == R_PERFECT
207
+
208
+ def test_medium_mixed(self):
209
+ scores = [R_PERFECT, R_BREACH, R_PERFECT, R_PHISH_BEC_QUARANTINE]
210
+ result = calculate_overall_score(scores)
211
+ expected = round((R_PERFECT + R_BREACH + R_PERFECT + R_PHISH_BEC_QUARANTINE) / 4, 4)
212
+ assert result == expected
213
+
214
+ def test_hard_mostly_bad(self):
215
+ scores = [R_BREACH, R_DISRUPTION, R_PERFECT]
216
+ result = calculate_overall_score(scores)
217
+ assert result < PASS_THRESHOLD
218
+
219
+ def test_returns_four_decimal_places(self):
220
+ result = calculate_overall_score([R_PERFECT, R_BREACH])
221
+ assert result == round(result, 4)
222
+
223
+ def test_single_perfect_step(self):
224
+ assert calculate_overall_score([R_PERFECT]) == R_PERFECT
225
+
226
+ def test_single_breach_step(self):
227
+ assert calculate_overall_score([R_BREACH]) == R_BREACH