test_t3_unit.py file-level

at sha256:f · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 💥 blast risk
sha256:c Add T4 job cancellation retry and validation · · Jun 11, 2026
1 """Unit tier tests — T3 dataset review and job queue state machine."""
2
3 from __future__ import annotations
4
5 import unittest
6
7 from scooling_lab_helpers import valid_payload
8
9 from scooling_lab.contracts import TrainingJobStatus
10 from scooling_lab.dataset_review import (
11 DatasetStatus,
12 DatasetStore,
13 RejectionReasonCode,
14 dataset_transition,
15 default_dataset_shape,
16 require_dataset_id,
17 validate_review_request,
18 )
19 from scooling_lab.errors import ApiError, ErrorCode
20 from scooling_lab.service import TrainingApiService
21 from scooling_lab.store import TrainingJobStore
22
23
24 class T3UnitDatasetStateMachineTests(unittest.TestCase):
25 """Unit tests for the dataset review state machine."""
26
27 def test_unit_t3_registered_to_pending_review(self) -> None:
28 """registered → pending_review is a valid transition."""
29
30 self.assertEqual(
31 dataset_transition(DatasetStatus.REGISTERED, DatasetStatus.PENDING_REVIEW),
32 DatasetStatus.PENDING_REVIEW,
33 )
34
35 def test_unit_t3_pending_to_approved(self) -> None:
36 """pending_review → approved is a valid transition."""
37
38 self.assertEqual(
39 dataset_transition(DatasetStatus.PENDING_REVIEW, DatasetStatus.APPROVED),
40 DatasetStatus.APPROVED,
41 )
42
43 def test_unit_t3_pending_to_rejected(self) -> None:
44 """pending_review → rejected is a valid transition."""
45
46 self.assertEqual(
47 dataset_transition(DatasetStatus.PENDING_REVIEW, DatasetStatus.REJECTED),
48 DatasetStatus.REJECTED,
49 )
50
51 def test_unit_t3_approved_is_terminal(self) -> None:
52 """approved cannot transition to any other state."""
53
54 with self.assertRaises(ApiError) as raised:
55 dataset_transition(DatasetStatus.APPROVED, DatasetStatus.REJECTED)
56 self.assertEqual(raised.exception.code, ErrorCode.INVALID_TRANSITION)
57
58 def test_unit_t3_rejected_is_terminal(self) -> None:
59 """rejected cannot transition to approved."""
60
61 with self.assertRaises(ApiError) as raised:
62 dataset_transition(DatasetStatus.REJECTED, DatasetStatus.APPROVED)
63 self.assertEqual(raised.exception.code, ErrorCode.INVALID_TRANSITION)
64
65 def test_unit_t3_registered_cannot_skip_to_approved(self) -> None:
66 """registered → approved must go through pending_review."""
67
68 with self.assertRaises(ApiError) as raised:
69 dataset_transition(DatasetStatus.REGISTERED, DatasetStatus.APPROVED)
70 self.assertEqual(raised.exception.code, ErrorCode.INVALID_TRANSITION)
71
72 def test_unit_t3_idempotent_same_state_replay(self) -> None:
73 """Same-state replay is allowed for every dataset status."""
74
75 for status in DatasetStatus:
76 with self.subTest(status=status):
77 self.assertEqual(dataset_transition(status, status), status)
78
79
80 class T3UnitRejectionReasonCodeTests(unittest.TestCase):
81 """Unit tests for the rejection reason code validation."""
82
83 def test_unit_t3_all_reason_codes_are_accepted(self) -> None:
84 """Every RejectionReasonCode value passes validation."""
85
86 for code in RejectionReasonCode:
87 with self.subTest(code=code):
88 from scooling_lab.dataset_review import require_rejection_reason
89
90 self.assertEqual(require_rejection_reason(code.value), code)
91
92 def test_unit_t3_free_text_reason_rejected(self) -> None:
93 """Arbitrary strings that are not enum members are rejected."""
94
95 from scooling_lab.dataset_review import require_rejection_reason
96
97 with self.assertRaises(ApiError):
98 require_rejection_reason("because I said so")
99
100 def test_unit_t3_url_shaped_reason_rejected(self) -> None:
101 """URL-shaped strings are not accepted as reason codes."""
102
103 from scooling_lab.dataset_review import require_rejection_reason
104
105 with self.assertRaises(ApiError):
106 require_rejection_reason("https://attacker.invalid/reason")
107
108
109 class T3UnitReviewRequestValidationTests(unittest.TestCase):
110 """Unit tests for the review request payload validator."""
111
112 def test_unit_t3_approve_action_accepted(self) -> None:
113 """action=approve with no reasonCode is valid."""
114
115 action, reason = validate_review_request({"action": "approve"})
116 self.assertEqual(action, "approve")
117 self.assertIsNone(reason)
118
119 def test_unit_t3_reject_action_requires_reason_code(self) -> None:
120 """action=reject without a reasonCode raises VALIDATION_ERROR."""
121
122 with self.assertRaises(ApiError) as raised:
123 validate_review_request({"action": "reject"})
124 self.assertEqual(raised.exception.code, ErrorCode.VALIDATION_ERROR)
125
126 def test_unit_t3_reject_with_valid_reason_accepted(self) -> None:
127 """action=reject with a valid reasonCode returns both values."""
128
129 action, reason = validate_review_request(
130 {"action": "reject", "reasonCode": "POLICY_VIOLATION"}
131 )
132 self.assertEqual(action, "reject")
133 self.assertEqual(reason, RejectionReasonCode.POLICY_VIOLATION)
134
135 def test_unit_t3_unknown_action_rejected(self) -> None:
136 """Unknown action values fail validation."""
137
138 with self.assertRaises(ApiError):
139 validate_review_request({"action": "delete"})
140
141 def test_unit_t3_unknown_keys_rejected(self) -> None:
142 """Extra keys in the review payload are rejected."""
143
144 with self.assertRaises(ApiError):
145 validate_review_request(
146 {"action": "approve", "workerUrl": "http://attacker.invalid"}
147 )
148
149 def test_unit_t3_approve_with_reason_code_rejected(self) -> None:
150 """approve action must not carry a reasonCode."""
151
152 with self.assertRaises(ApiError):
153 validate_review_request(
154 {"action": "approve", "reasonCode": "FORMAT_INVALID"}
155 )
156
157
158 class T3UnitDatasetIdValidationTests(unittest.TestCase):
159 """Unit tests for dataset id format enforcement."""
160
161 def test_unit_t3_safe_dataset_ids_accepted(self) -> None:
162 """Well-formed dataset ids within length bounds are accepted."""
163
164 safe_ids = (
165 "fixture:synthetic-tiny-v1",
166 "ds.001",
167 "dataset-v2",
168 )
169 for dataset_id in safe_ids:
170 with self.subTest(dataset_id=dataset_id):
171 self.assertEqual(require_dataset_id(dataset_id), dataset_id)
172
173 def test_unit_t3_path_traversal_and_url_ids_rejected(self) -> None:
174 """Path-like and URL-like dataset ids are rejected."""
175
176 bad_ids = (
177 "../private/dataset",
178 "https://attacker.invalid/ds",
179 "ds;rm-rf",
180 "d", # too short
181 )
182 for bad in bad_ids:
183 with self.subTest(bad=bad):
184 with self.assertRaises(ApiError):
185 require_dataset_id(bad)
186
187
188 class T3UnitDatasetStoreTests(unittest.TestCase):
189 """Unit tests for the DatasetStore lifecycle operations."""
190
191 def test_unit_t3_fixture_dataset_pre_approved(self) -> None:
192 """The synthetic fixture dataset is pre-approved in a fresh store."""
193
194 store = DatasetStore()
195 self.assertTrue(store.is_approved("fixture:synthetic-tiny-v1"))
196
197 def test_unit_t3_register_new_dataset(self) -> None:
198 """A new dataset starts in registered state."""
199
200 store = DatasetStore()
201 record = store.register("new-dataset-v1")
202 self.assertEqual(record.status, DatasetStatus.REGISTERED)
203
204 def test_unit_t3_register_already_approved_raises_conflict(self) -> None:
205 """Re-registering an approved dataset raises CONFLICT."""
206
207 store = DatasetStore()
208 with self.assertRaises(ApiError) as raised:
209 store.register("fixture:synthetic-tiny-v1")
210 self.assertEqual(raised.exception.code, ErrorCode.CONFLICT)
211
212 def test_unit_t3_full_approval_lifecycle(self) -> None:
213 """Submit-time validation approves the default synthetic shape."""
214
215 store = DatasetStore()
216 store.register("lifecycle-ds-v1")
217 submitted = store.submit_for_review("lifecycle-ds-v1")
218 self.assertEqual(submitted.status, DatasetStatus.APPROVED)
219 record = store.approve("lifecycle-ds-v1")
220 self.assertEqual(record.status, DatasetStatus.APPROVED)
221 self.assertTrue(store.is_approved("lifecycle-ds-v1"))
222
223 def test_unit_t3_full_rejection_lifecycle(self) -> None:
224 """Submit-time validation rejects invalid metadata with the reason code."""
225
226 store = DatasetStore()
227 store.register_shape(
228 "reject-ds-v1",
229 default_dataset_shape(RejectionReasonCode.FORMAT_INVALID),
230 )
231 store.submit_for_review("reject-ds-v1")
232 record = store.reject("reject-ds-v1", RejectionReasonCode.FORMAT_INVALID)
233 self.assertEqual(record.status, DatasetStatus.REJECTED)
234 self.assertEqual(record.rejection_reason, RejectionReasonCode.FORMAT_INVALID)
235 self.assertFalse(store.is_approved("reject-ds-v1"))
236
237 def test_unit_t3_unknown_dataset_is_not_found(self) -> None:
238 """Getting an unregistered dataset raises NOT_FOUND."""
239
240 store = DatasetStore()
241 with self.assertRaises(ApiError) as raised:
242 store.get("totally-unknown-ds")
243 self.assertEqual(raised.exception.code, ErrorCode.NOT_FOUND)
244
245 def test_unit_t3_unapproved_dataset_blocks_job_creation(self) -> None:
246 """Jobs against a registered-but-not-yet-approved dataset are refused."""
247
248 store = DatasetStore()
249 store.register("new-dataset-v1")
250 service = TrainingApiService(TrainingJobStore(), dataset_store=store)
251
252 bad_payload = {
253 "idempotencyKey": "test-unapproved",
254 "datasetId": "fixture:synthetic-tiny-v1",
255 "modelId": "fixture-tiny-llm",
256 "requestedBy": "unit-test",
257 }
258 with self.assertRaises(ApiError) as raised:
259 service.create_training_job({**bad_payload, "datasetId": "new-dataset-v1"})
260 self.assertEqual(raised.exception.code, ErrorCode.DATASET_NOT_APPROVED)
261
262 def test_unit_t3_queue_state_fields_present(self) -> None:
263 """get_queue_state returns all expected fields with correct types."""
264
265 service = TrainingApiService(TrainingJobStore())
266 state = service.get_queue_state()
267 self.assertIn("queuedCount", state)
268 self.assertIn("runningCount", state)
269 self.assertIn("activeCount", state)
270 self.assertIn("maxConcurrentRunning", state)
271 self.assertIn("queueLimit", state)
272 self.assertIsInstance(state["queuedCount"], int)
273 self.assertIsInstance(state["runningCount"], int)
274
275
276 if __name__ == "__main__":
277 unittest.main()