test: lock training API privacy contract

This commit is contained in:
2026-04-30 22:12:50 -04:00
parent 2705dde8bc
commit 82217a7eb8
@@ -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()