File size: 6,451 Bytes
0539596
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
from __future__ import annotations

import pytest
from pydantic import ValidationError

from gcmd_classifier.llm.schemas import (
    CandidateDecision,
    TermResponse,
    TopicResponse,
    VariableResponse,
)

CANDIDATE = {
    "candidate_id": "topic-atmosphere",
    "confidence": 0.91,
    "evidence": "The article discusses atmospheric composition.",
    "support_type": "explicit",
    "reason": "Primary subject is atmospheric science.",
}
SECOND_CANDIDATE = {
    "candidate_id": "topic-oceans",
    "confidence": None,
    "evidence": "Ocean observations are also central to the article.",
    "support_type": "mixed",
}


def test_valid_topic_response_with_one_selected_candidate() -> None:
    response = TopicResponse.model_validate(
        {
            "selected": [CANDIDATE],
            "ambiguous_alternatives": [],
            "no_selection_reason": None,
        }
    )

    assert response.selected[0].candidate_id == "topic-atmosphere"
    assert response.selected[0].confidence == 0.91


def test_valid_topic_response_with_multiple_selected_candidates() -> None:
    response = TopicResponse.model_validate(
        {
            "selected": [CANDIDATE, SECOND_CANDIDATE],
            "ambiguous_alternatives": ["topic-cryosphere"],
        }
    )

    assert [candidate.candidate_id for candidate in response.selected] == [
        "topic-atmosphere",
        "topic-oceans",
    ]
    assert response.selected[1].confidence is None


def test_valid_topic_response_with_no_selected_candidates_and_reason() -> None:
    response = TopicResponse.model_validate(
        {
            "selected": [],
            "ambiguous_alternatives": [],
            "no_selection_reason": "No supplied Topic is supported by the title or abstract.",
        }
    )

    assert response.selected == []
    assert response.no_selection_reason is not None


def test_valid_term_response_selecting_candidates() -> None:
    response = TermResponse.model_validate(
        {
            "selected": [CANDIDATE, SECOND_CANDIDATE],
            "stop_at_parent": False,
            "stop_reason": None,
            "ambiguous_alternatives": [],
        }
    )

    assert len(response.selected) == 2
    assert response.stop_at_parent is False


def test_valid_term_response_stopping_at_parent() -> None:
    response = TermResponse.model_validate(
        {
            "selected": [],
            "stop_at_parent": True,
            "stop_reason": "No child Term is adequately supported.",
            "ambiguous_alternatives": ["term-weather-events"],
        }
    )

    assert response.stop_at_parent is True
    assert response.selected == []


@pytest.mark.parametrize(
    "payload",
    [
        {
            "selected": [CANDIDATE],
            "stop_at_parent": True,
            "stop_reason": "Contradictory response.",
            "ambiguous_alternatives": [],
        },
        {
            "selected": [CANDIDATE],
            "stop_at_parent": True,
            "stop_reason": None,
            "ambiguous_alternatives": [],
        },
    ],
)
def test_invalid_term_response_with_stop_and_selected(payload: dict) -> None:
    with pytest.raises(ValidationError):
        TermResponse.model_validate(payload)


@pytest.mark.parametrize("stop_reason", [None, ""])
def test_invalid_term_response_stopping_without_stop_reason(stop_reason: str | None) -> None:
    with pytest.raises(ValidationError):
        TermResponse.model_validate(
            {
                "selected": [],
                "stop_at_parent": True,
                "stop_reason": stop_reason,
                "ambiguous_alternatives": [],
            }
        )


def test_valid_variable_response_selecting_candidates() -> None:
    response = VariableResponse.model_validate(
        {
            "selected": [CANDIDATE],
            "stop_at_parent": False,
            "stop_reason": None,
            "ambiguous_alternatives": [],
        }
    )

    assert response.selected[0].support_type.value == "explicit"


def test_valid_variable_response_stopping_at_parent() -> None:
    response = VariableResponse.model_validate(
        {
            "selected": [],
            "stop_at_parent": True,
            "stop_reason": "Variable children are too specific for the evidence.",
            "ambiguous_alternatives": [],
        }
    )

    assert response.stop_at_parent is True


@pytest.mark.parametrize(
    "payload",
    [
        {
            "selected": [CANDIDATE],
            "stop_at_parent": True,
            "stop_reason": "Contradictory response.",
            "ambiguous_alternatives": [],
        },
        {
            "selected": [CANDIDATE],
            "stop_at_parent": True,
            "stop_reason": None,
            "ambiguous_alternatives": [],
        },
    ],
)
def test_invalid_variable_response_with_stop_and_selected(payload: dict) -> None:
    with pytest.raises(ValidationError):
        VariableResponse.model_validate(payload)


@pytest.mark.parametrize("stop_reason", [None, ""])
def test_invalid_variable_response_stopping_without_stop_reason(stop_reason: str | None) -> None:
    with pytest.raises(ValidationError):
        VariableResponse.model_validate(
            {
                "selected": [],
                "stop_at_parent": True,
                "stop_reason": stop_reason,
                "ambiguous_alternatives": [],
            }
        )


@pytest.mark.parametrize("confidence", [-0.01, 1.01])
def test_invalid_confidence_range(confidence: float) -> None:
    payload = CANDIDATE | {"confidence": confidence}

    with pytest.raises(ValidationError):
        CandidateDecision.model_validate(payload)


def test_valid_missing_confidence() -> None:
    payload = CANDIDATE.copy()
    del payload["confidence"]

    decision = CandidateDecision.model_validate(payload)

    assert decision.confidence is None


def test_unknown_fields_rejected() -> None:
    with pytest.raises(ValidationError):
        TopicResponse.model_validate({"selected": [], "ambiguous_alternatives": [], "extra": "bad"})


def test_malformed_candidate_decision_rejected() -> None:
    payload = CANDIDATE.copy()
    del payload["evidence"]

    with pytest.raises(ValidationError):
        CandidateDecision.model_validate(payload)


def test_empty_candidate_id_rejected() -> None:
    with pytest.raises(ValidationError):
        CandidateDecision.model_validate(CANDIDATE | {"candidate_id": ""})