SamSankar commited on
Commit
ebd1bef
·
verified ·
1 Parent(s): 589435c

Upload 8 files

Browse files
tests/__init__.py ADDED
File without changes
tests/__pycache__/__init__.cpython-310.pyc ADDED
Binary file (162 Bytes). View file
 
tests/__pycache__/test_endpoints.cpython-310-pytest-8.0.0.pyc ADDED
Binary file (7.16 kB). View file
 
tests/__pycache__/test_grader.cpython-310-pytest-8.0.0.pyc ADDED
Binary file (16.8 kB). View file
 
tests/__pycache__/test_tasks.cpython-310-pytest-8.0.0.pyc ADDED
Binary file (10.9 kB). View file
 
tests/test_endpoints.py ADDED
@@ -0,0 +1,97 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the FastAPI server endpoints."""
2
+
3
+ import sys, os
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ import pytest
7
+ from fastapi.testclient import TestClient
8
+
9
+
10
+ # Skip entire module if environment can't be created (e.g. missing datasets)
11
+ # This allows CI to run without HF dataset access
12
+ pytestmark = pytest.mark.skipif(
13
+ os.getenv("SKIP_SERVER_TESTS", "1") == "1",
14
+ reason="Set SKIP_SERVER_TESTS=0 to run server tests (requires dataset access)"
15
+ )
16
+
17
+
18
+ class TestHealthEndpoint:
19
+ def test_health_returns_200(self):
20
+ from server.app import app
21
+ client = TestClient(app)
22
+ resp = client.get("/health")
23
+ assert resp.status_code == 200
24
+ data = resp.json()
25
+ assert data["status"] == "healthy"
26
+ assert "version" in data
27
+
28
+
29
+ class TestTasksEndpoint:
30
+ def test_tasks_returns_three_tasks(self):
31
+ from server.app import app
32
+ client = TestClient(app)
33
+ resp = client.get("/tasks")
34
+ assert resp.status_code == 200
35
+ data = resp.json()
36
+ assert len(data["tasks"]) == 3
37
+ task_ids = [t["task_id"] for t in data["tasks"]]
38
+ assert "task_1_factual_grounding" in task_ids
39
+ assert "task_2_multi_hop_synthesis" in task_ids
40
+ assert "task_3_adversarial_resistance" in task_ids
41
+
42
+ def test_tasks_has_action_schema(self):
43
+ from server.app import app
44
+ client = TestClient(app)
45
+ resp = client.get("/tasks")
46
+ data = resp.json()
47
+ assert "action_schema" in data
48
+ assert "answer" in data["action_schema"]["properties"]
49
+
50
+
51
+ class TestGraderEndpoint:
52
+ def test_grader_requires_task_id(self):
53
+ from server.app import app
54
+ client = TestClient(app)
55
+ resp = client.post("/grader", json={"step_rewards": [0.5], "step_infos": [{}]})
56
+ assert resp.status_code == 422
57
+
58
+ def test_grader_invalid_task_id(self):
59
+ from server.app import app
60
+ client = TestClient(app)
61
+ resp = client.post("/grader", json={
62
+ "task_id": "nonexistent",
63
+ "step_rewards": [0.5],
64
+ "step_infos": [{}],
65
+ })
66
+ assert resp.status_code == 404
67
+
68
+ def test_grader_returns_score(self):
69
+ from server.app import app
70
+ client = TestClient(app)
71
+ resp = client.post("/grader", json={
72
+ "task_id": "task_1_factual_grounding",
73
+ "step_rewards": [0.7, 0.5, 0.3],
74
+ "step_infos": [
75
+ {"correctness": 0.7, "grounding": 0.6, "calibration": 0.8,
76
+ "hallucination_score": 0.1, "is_hallucination": False},
77
+ {"correctness": 0.5, "grounding": 0.4, "calibration": 0.7,
78
+ "hallucination_score": 0.2, "is_hallucination": False},
79
+ {"correctness": 0.3, "grounding": 0.3, "calibration": 0.6,
80
+ "hallucination_score": 0.5, "is_hallucination": True},
81
+ ],
82
+ })
83
+ assert resp.status_code == 200
84
+ data = resp.json()
85
+ assert 0.0 <= data["score"] <= 1.0
86
+ assert "breakdown" in data
87
+
88
+
89
+ class TestMetadataEndpoint:
90
+ def test_metadata(self):
91
+ from server.app import app
92
+ client = TestClient(app)
93
+ resp = client.get("/metadata")
94
+ assert resp.status_code == 200
95
+ data = resp.json()
96
+ assert data["name"] == "hallucination-guard-env"
97
+ assert "version" in data
tests/test_grader.py ADDED
@@ -0,0 +1,247 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the 9-component reward system and hallucination detection."""
2
+
3
+ import sys, os
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ import pytest
7
+ from server.grader import (
8
+ calculate_reward,
9
+ detect_hallucination_advanced,
10
+ compute_calibration_error,
11
+ is_refusal_answer,
12
+ normalize_text,
13
+ check_quote_in_context_advanced,
14
+ check_factual_accuracy_advanced,
15
+ compute_rouge,
16
+ compute_bertscore,
17
+ HallucinationType,
18
+ HallucinationSeverity,
19
+ )
20
+
21
+
22
+ class TestRewardRange:
23
+ """Rewards must always be in [0, 1]."""
24
+
25
+ @pytest.mark.parametrize("difficulty", ["beginner", "intermediate", "advanced", "expert"])
26
+ def test_reward_in_range_correct_answer(self, difficulty):
27
+ reward, info = calculate_reward(
28
+ answer="Paris is the capital of France.",
29
+ confidence=0.9,
30
+ source_quote="Paris is the capital of France.",
31
+ context="Paris is the capital of France. It is located in northern France.",
32
+ ground_truth="Paris",
33
+ difficulty_level=difficulty,
34
+ )
35
+ assert 0.0 <= reward <= 1.0, f"Reward {reward} out of range for {difficulty}"
36
+
37
+ def test_reward_in_range_wrong_answer(self):
38
+ reward, info = calculate_reward(
39
+ answer="London is the capital of France.",
40
+ confidence=0.9,
41
+ source_quote="London is the capital of France.",
42
+ context="Paris is the capital of France.",
43
+ ground_truth="Paris",
44
+ )
45
+ assert 0.0 <= reward <= 1.0
46
+
47
+ def test_reward_in_range_empty_answer(self):
48
+ reward, info = calculate_reward(
49
+ answer="",
50
+ confidence=0.5,
51
+ source_quote="",
52
+ context="Some context here.",
53
+ ground_truth="Some answer",
54
+ )
55
+ assert 0.0 <= reward <= 1.0
56
+
57
+ def test_reward_in_range_refusal(self):
58
+ reward, info = calculate_reward(
59
+ answer="I cannot answer from the provided context.",
60
+ confidence=0.3,
61
+ source_quote="",
62
+ context="Some unrelated context.",
63
+ ground_truth="not mentioned in context",
64
+ )
65
+ assert 0.0 <= reward <= 1.0
66
+
67
+
68
+ class TestRefusalHandling:
69
+ """Proper refusals on unanswerable questions should be rewarded."""
70
+
71
+ def test_proper_refusal_rewarded(self):
72
+ reward, info = calculate_reward(
73
+ answer="I cannot answer from the provided context.",
74
+ confidence=0.3,
75
+ source_quote="",
76
+ context="The sky is blue.",
77
+ ground_truth="not mentioned in context",
78
+ )
79
+ assert reward >= 0.5, f"Proper refusal should get reward >= 0.5, got {reward}"
80
+ assert info.get("is_refusal") is True
81
+
82
+ def test_underconfident_refusal_penalized(self):
83
+ """Refusing when the answer IS in context should be penalized."""
84
+ reward, info = calculate_reward(
85
+ answer="I cannot determine the answer from the context.",
86
+ confidence=0.3,
87
+ source_quote="",
88
+ context="The capital of France is Paris.",
89
+ ground_truth="Paris",
90
+ )
91
+ assert reward <= 0.4, f"Underconfident refusal should be penalized, got {reward}"
92
+
93
+ def test_overconfident_refusal(self):
94
+ """High confidence refusal on answerable question should be penalized."""
95
+ reward, info = calculate_reward(
96
+ answer="I don't know the answer.",
97
+ confidence=0.9,
98
+ source_quote="",
99
+ context="The capital of France is Paris.",
100
+ ground_truth="Paris",
101
+ )
102
+ assert reward <= 0.5
103
+
104
+
105
+ class TestHallucinationDetection:
106
+ """Hallucination detection should classify types correctly."""
107
+
108
+ def test_no_hallucination_for_grounded_answer(self):
109
+ score, htype, severity, analysis = detect_hallucination_advanced(
110
+ answer="Paris is the capital of France.",
111
+ context="Paris is the capital of France.",
112
+ ground_truth="Paris",
113
+ confidence=0.9,
114
+ )
115
+ assert score < 0.3, f"Grounded answer should have low hallucination score, got {score}"
116
+
117
+ def test_fabricated_fact_detected(self):
118
+ score, htype, severity, analysis = detect_hallucination_advanced(
119
+ answer="Berlin is the capital of France.",
120
+ context="Paris is the capital of France.",
121
+ ground_truth="Paris",
122
+ confidence=0.9,
123
+ )
124
+ assert score > 0.3, f"Fabricated fact should have high hallucination score, got {score}"
125
+
126
+ def test_numerical_fabrication_detected(self):
127
+ score, htype, severity, analysis = detect_hallucination_advanced(
128
+ answer="The population is 8.7 million.",
129
+ context="The population is 2.1 million people.",
130
+ ground_truth="2.1 million",
131
+ confidence=0.8,
132
+ )
133
+ assert analysis.get("numerical_fabrication", 0) > 0, \
134
+ f"Fabricated number 8.7 should be detected, got {analysis}"
135
+
136
+
137
+ class TestCitationAccuracy:
138
+ """Source quote verification should work correctly."""
139
+
140
+ def test_exact_quote_match(self):
141
+ score, analysis = check_quote_in_context_advanced(
142
+ "Paris is the capital of France.",
143
+ "Paris is the capital of France. It is a beautiful city.",
144
+ )
145
+ assert score == 1.0, f"Exact quote should score 1.0, got {score}"
146
+
147
+ def test_no_quote(self):
148
+ score, analysis = check_quote_in_context_advanced(
149
+ "",
150
+ "Some context here.",
151
+ )
152
+ assert score == 0.0
153
+
154
+ def test_partial_quote(self):
155
+ score, analysis = check_quote_in_context_advanced(
156
+ "capital of France",
157
+ "Paris is the capital of France.",
158
+ )
159
+ assert score > 0.5, f"Partial quote should score > 0.5, got {score}"
160
+
161
+
162
+ class TestCalibrationError:
163
+ """Calibration error should penalize overconfidence."""
164
+
165
+ def test_perfect_calibration(self):
166
+ error = compute_calibration_error(0.9, 0.9)
167
+ assert error == 0.0
168
+
169
+ def test_overconfidence_penalized(self):
170
+ error = compute_calibration_error(0.95, 0.3)
171
+ assert error > 0.5, f"Overconfidence should be heavily penalized, got {error}"
172
+
173
+ def test_underconfidence_safe(self):
174
+ error = compute_calibration_error(0.3, 0.9)
175
+ assert error < compute_calibration_error(0.95, 0.3), \
176
+ "Overconfidence should be penalized more than underconfidence"
177
+
178
+
179
+ class TestBERTScoreEdgeCases:
180
+ """BERTScore should not crash on edge cases."""
181
+
182
+ def test_empty_strings(self):
183
+ result = compute_bertscore("", "")
184
+ assert result["f1"] == 0.0
185
+
186
+ def test_identical_strings(self):
187
+ result = compute_bertscore("The cat sat on the mat.", "The cat sat on the mat.")
188
+ assert result["f1"] > 0.8, f"Identical strings should have high BERTScore, got {result['f1']}"
189
+
190
+ def test_short_strings(self):
191
+ result = compute_bertscore("yes", "no")
192
+ assert "f1" in result # Should not crash
193
+
194
+
195
+ class TestROUGE:
196
+ """ROUGE scores should be computed correctly."""
197
+
198
+ def test_identical_strings(self):
199
+ result = compute_rouge("The cat sat on the mat.", "The cat sat on the mat.")
200
+ assert result["rougeL"] == 1.0
201
+
202
+ def test_completely_different(self):
203
+ result = compute_rouge("The cat sat on the mat.", "Dogs run in the park.")
204
+ assert result["rougeL"] < 0.5
205
+
206
+ def test_empty_strings(self):
207
+ result = compute_rouge("", "")
208
+ assert result["rouge1"] == 0.0
209
+
210
+
211
+ class TestFactualAccuracy:
212
+ """Factual accuracy should handle various answer types."""
213
+
214
+ def test_exact_match(self):
215
+ score, analysis = check_factual_accuracy_advanced(
216
+ "Paris", "Paris", "Paris is the capital of France."
217
+ )
218
+ assert score >= 0.9, f"Exact match should score high, got {score}"
219
+
220
+ def test_wrong_answer(self):
221
+ score, analysis = check_factual_accuracy_advanced(
222
+ "London", "Paris", "Paris is the capital of France."
223
+ )
224
+ assert score < 0.5, f"Wrong answer should score low, got {score}"
225
+
226
+ def test_contains_truth(self):
227
+ score, analysis = check_factual_accuracy_advanced(
228
+ "The capital is Paris, which is in northern France.",
229
+ "Paris",
230
+ "Paris is the capital of France.",
231
+ )
232
+ assert score >= 0.8, f"Answer containing truth should score high, got {score}"
233
+
234
+
235
+ class TestNormalizeText:
236
+ """Text normalization should handle edge cases."""
237
+
238
+ def test_empty_string(self):
239
+ assert normalize_text("") == ""
240
+
241
+ def test_whitespace_normalization(self):
242
+ result = normalize_text(" The cat sat ")
243
+ assert " " not in result
244
+
245
+ def test_case_normalization(self):
246
+ result = normalize_text("PARIS IS THE CAPITAL")
247
+ assert result == result.lower()
tests/test_tasks.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Tests for the OpenEnv task registry and grader."""
2
+
3
+ import sys, os
4
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
5
+
6
+ import pytest
7
+ from server.tasks import (
8
+ ALL_TASKS, TASK_1, TASK_2, TASK_3,
9
+ get_task, task_id_for_difficulty,
10
+ compute_task_score, ACTION_SCHEMA,
11
+ )
12
+
13
+
14
+ class TestTaskRegistry:
15
+ """Task registry should contain all required tasks."""
16
+
17
+ def test_three_tasks_exist(self):
18
+ assert len(ALL_TASKS) == 3
19
+ assert "task_1_factual_grounding" in ALL_TASKS
20
+ assert "task_2_multi_hop_synthesis" in ALL_TASKS
21
+ assert "task_3_adversarial_resistance" in ALL_TASKS
22
+
23
+ def test_task_difficulties(self):
24
+ assert TASK_1.difficulty == "beginner"
25
+ assert TASK_2.difficulty == "intermediate"
26
+ assert TASK_3.difficulty == "advanced"
27
+
28
+ def test_get_task(self):
29
+ assert get_task("task_1_factual_grounding") is TASK_1
30
+ assert get_task("nonexistent") is None
31
+
32
+ def test_difficulty_mapping(self):
33
+ assert task_id_for_difficulty("beginner") == TASK_1.task_id
34
+ assert task_id_for_difficulty("intermediate") == TASK_2.task_id
35
+ assert task_id_for_difficulty("advanced") == TASK_3.task_id
36
+ assert task_id_for_difficulty("expert") == TASK_3.task_id
37
+
38
+ def test_action_schema_has_required_fields(self):
39
+ props = ACTION_SCHEMA["properties"]
40
+ assert "answer" in props
41
+ assert "confidence" in props
42
+ assert "source_quote" in props
43
+ assert ACTION_SCHEMA["required"] == ["answer"]
44
+
45
+
46
+ class TestTaskGrader:
47
+ """Per-episode task grader should produce scores in [0, 1]."""
48
+
49
+ def test_empty_steps_score_zero(self):
50
+ result = compute_task_score(TASK_1, [], [])
51
+ assert result["score"] == 0.0
52
+
53
+ def test_perfect_scores(self):
54
+ step_rewards = [1.0, 1.0, 1.0, 1.0, 1.0]
55
+ step_infos = [
56
+ {"correctness": 1.0, "grounding": 1.0, "calibration": 1.0,
57
+ "hallucination_score": 0.0, "is_hallucination": False}
58
+ for _ in range(5)
59
+ ]
60
+ result = compute_task_score(TASK_1, step_rewards, step_infos)
61
+ assert 0.9 <= result["score"] <= 1.05, f"Perfect answers should score ~1.0, got {result['score']}"
62
+
63
+ def test_zero_rewards_score_low(self):
64
+ step_rewards = [0.0, 0.0, 0.0]
65
+ step_infos = [
66
+ {"correctness": 0.0, "grounding": 0.0, "calibration": 0.0,
67
+ "hallucination_score": 1.0, "is_hallucination": True}
68
+ for _ in range(3)
69
+ ]
70
+ result = compute_task_score(TASK_1, step_rewards, step_infos)
71
+ assert result["score"] <= 0.1, f"All-wrong should score ~0, got {result['score']}"
72
+
73
+ def test_task3_overconfidence_penalty(self):
74
+ """Task 3 should penalize overconfident wrong answers."""
75
+ step_rewards = [0.3, 0.3, 0.3]
76
+ step_infos = [
77
+ {"correctness": 0.2, "grounding": 0.3, "calibration": 0.9,
78
+ "hallucination_score": 0.7, "is_hallucination": True}
79
+ for _ in range(3)
80
+ ]
81
+ result_t3 = compute_task_score(TASK_3, step_rewards, step_infos)
82
+ # Same data on task 1 should score higher than task 3
83
+ result_t1 = compute_task_score(TASK_1, step_rewards, step_infos)
84
+ assert result_t3["score"] <= result_t1["score"], \
85
+ f"Task 3 should penalize overconfidence more than Task 1"
86
+
87
+ def test_completion_bonus(self):
88
+ """5+ steps should get a completion bonus."""
89
+ short_infos = [{"correctness": 0.5, "grounding": 0.5, "calibration": 0.5,
90
+ "hallucination_score": 0.0, "is_hallucination": False}]
91
+ long_infos = [{"correctness": 0.5, "grounding": 0.5, "calibration": 0.5,
92
+ "hallucination_score": 0.0, "is_hallucination": False}
93
+ for _ in range(6)]
94
+ short_result = compute_task_score(TASK_1, [0.5], short_infos)
95
+ long_result = compute_task_score(TASK_1, [0.5] * 6, long_infos)
96
+ assert long_result["score"] > short_result["score"], \
97
+ "Longer episodes should get completion bonus"
98
+
99
+ def test_score_always_in_range(self):
100
+ """Any combination of rewards/infos should produce score in [0, 1]."""
101
+ import random
102
+ random.seed(42)
103
+ for _ in range(100):
104
+ n = random.randint(1, 10)
105
+ rewards = [random.uniform(0, 1) for _ in range(n)]
106
+ infos = [{
107
+ "correctness": random.uniform(0, 1),
108
+ "grounding": random.uniform(0, 1),
109
+ "calibration": random.uniform(0, 1),
110
+ "hallucination_score": random.uniform(0, 1),
111
+ "is_hallucination": random.random() > 0.5,
112
+ } for _ in range(n)]
113
+ for task in [TASK_1, TASK_2, TASK_3]:
114
+ result = compute_task_score(task, rewards, infos)
115
+ assert 0.0 <= result["score"] <= 1.0, \
116
+ f"Score {result['score']} out of range for {task.task_id}"