test: lock training API privacy contract
This commit is contained in:
@@ -14,6 +14,12 @@ TRAINING_PRODUCT_DOC = ROOT / "docs" / "training_case_engine_m1_m6_product_readm
|
||||
INTERNAL_FIELD = "answer" + "_key"
|
||||
PRIVATE_MARKER = "trainer-only-choice"
|
||||
PRIVATE_RUBRIC_MARKER = "trainer-only-rubric"
|
||||
NESTED_PRIVATE_MARKERS = (
|
||||
"m17-private-choice",
|
||||
"m17-private-close",
|
||||
"m17-private-rubric",
|
||||
)
|
||||
PRIVATE_FIELD_TEXT = "public text mentions " + INTERNAL_FIELD
|
||||
|
||||
DOCUMENTED_TRAINING_ROUTES = (
|
||||
("GET", "/training/cases"),
|
||||
@@ -129,6 +135,70 @@ class _PublicTrainingRepository:
|
||||
}
|
||||
|
||||
|
||||
class _NestedPublicTrainingRepository:
|
||||
def __init__(self) -> None:
|
||||
self.get_case_args = None
|
||||
|
||||
def list_cases(self, status=None, skill_tag=None, instrument_id=None, limit=100):
|
||||
return [self._case_row(17)][:limit]
|
||||
|
||||
def get_case(self, case_id: int, **kwargs):
|
||||
self.get_case_args = {
|
||||
"case_id": case_id,
|
||||
"include_" + INTERNAL_FIELD: kwargs.get("include_" + INTERNAL_FIELD, False),
|
||||
}
|
||||
return self._case_row(case_id)
|
||||
|
||||
def _case_row(self, case_id: int) -> dict:
|
||||
return {
|
||||
"id": case_id,
|
||||
"instrument_id": 1,
|
||||
"timeframe": "15m",
|
||||
"case_type": "sweep",
|
||||
"skill_tag": "sweep",
|
||||
"start_ts": "2026-04-21T09:00:00Z",
|
||||
"decision_ts": "2026-04-21T10:00:00Z",
|
||||
"end_ts": "2026-04-21T11:00:00Z",
|
||||
"difficulty": "easy",
|
||||
"status": "published",
|
||||
"source_event_refs": [{"kind": "sweep_event", "id": 17}],
|
||||
INTERNAL_FIELD: {
|
||||
"answer": {"side": NESTED_PRIVATE_MARKERS[0], "close_back_inside": NESTED_PRIVATE_MARKERS[1]},
|
||||
"rubric": [{"category": NESTED_PRIVATE_MARKERS[2], "max_score": 5}],
|
||||
},
|
||||
"meta": {
|
||||
"title": "Nested public route privacy case",
|
||||
"skill_tags": ["sweep", "context_reading"],
|
||||
"public_note": PRIVATE_FIELD_TEXT,
|
||||
"tasks": [
|
||||
{
|
||||
"task_type": "short_answer",
|
||||
"skill_tag": "sweep",
|
||||
"prompt": "Describe the public confirmation.",
|
||||
"required_annotation_types": ["text"],
|
||||
"max_score": 5,
|
||||
"meta": {
|
||||
"public_hint": "Focus on context.",
|
||||
"public_note": PRIVATE_FIELD_TEXT,
|
||||
"nested": {
|
||||
"answer": NESTED_PRIVATE_MARKERS[1],
|
||||
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[2],
|
||||
},
|
||||
},
|
||||
}
|
||||
],
|
||||
"rubric": [
|
||||
{
|
||||
"category": "context",
|
||||
"max_score": 3,
|
||||
"answer": NESTED_PRIVATE_MARKERS[0],
|
||||
INTERNAL_FIELD: {"rubric": NESTED_PRIVATE_MARKERS[2]},
|
||||
}
|
||||
],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
def _documented_routes() -> tuple:
|
||||
text = TRAINING_PRODUCT_DOC.read_text(encoding="utf-8")
|
||||
routes = []
|
||||
@@ -184,6 +254,21 @@ def _public_import_package() -> dict:
|
||||
}
|
||||
|
||||
|
||||
def _dict_keys(value) -> list[str]:
|
||||
if isinstance(value, dict):
|
||||
keys = []
|
||||
for key, item in value.items():
|
||||
keys.append(str(key))
|
||||
keys.extend(_dict_keys(item))
|
||||
return keys
|
||||
if isinstance(value, list):
|
||||
keys = []
|
||||
for item in value:
|
||||
keys.extend(_dict_keys(item))
|
||||
return keys
|
||||
return []
|
||||
|
||||
|
||||
class TrainingPublicApiContractM8Tests(unittest.TestCase):
|
||||
def test_documented_training_routes_resolve_to_expected_handlers(self) -> None:
|
||||
self.assertEqual(_documented_routes(), DOCUMENTED_TRAINING_ROUTES)
|
||||
@@ -320,6 +405,31 @@ class TrainingPublicApiContractM8Tests(unittest.TestCase):
|
||||
with self.subTest(private_text=private_text):
|
||||
self.assertNotIn(private_text, serialized)
|
||||
|
||||
def test_public_training_list_detail_routes_scrub_nested_solution_material(self) -> None:
|
||||
repository = _NestedPublicTrainingRepository()
|
||||
service = TrainingCaseService(repository=repository)
|
||||
dependencies = _training_dependencies(service)
|
||||
|
||||
list_status, list_payload = server.route_request("/training/cases", {}, dependencies=dependencies)
|
||||
detail_status, detail_payload = server.route_request("/training/cases/17", {}, dependencies=dependencies)
|
||||
|
||||
self.assertEqual([list_status, detail_status], [200, 200])
|
||||
self.assertEqual(repository.get_case_args, {"case_id": 17, "include_" + INTERNAL_FIELD: False})
|
||||
self.assertEqual(list_payload["items"][0]["meta"]["tasks"][0]["meta"]["public_hint"], "Focus on context.")
|
||||
self.assertEqual(detail_payload["meta"]["rubric"], [{"category": "context", "max_score": 3}])
|
||||
|
||||
payload = {"list": list_payload, "detail": detail_payload}
|
||||
serialized = json.dumps(payload, sort_keys=True)
|
||||
|
||||
self.assertNotIn(INTERNAL_FIELD, serialized)
|
||||
for private_text in NESTED_PRIVATE_MARKERS:
|
||||
with self.subTest(private_text=private_text):
|
||||
self.assertNotIn(private_text, serialized)
|
||||
|
||||
keys = _dict_keys(payload)
|
||||
self.assertNotIn("answer", keys)
|
||||
self.assertNotIn(INTERNAL_FIELD, keys)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user