Files
ai-exchange/tests/unit/test_training_public_api_contract_m8.py
T
2026-05-06 22:43:29 +08:00

1423 lines
55 KiB
Python

import ast
import io
import json
import os
import re
import unittest
from pathlib import Path
from unittest import mock
os.environ.setdefault("DATABASE_URL", "postgresql+psycopg://user:pass@localhost:5432/ai_ict_test")
from src.api import server
from src.api import training as training_api
from src.services.training import TrainingCaseService
ROOT = Path(__file__).resolve().parents[2]
TRAINING_PRODUCT_DOC = ROOT / "docs" / "training_console_public_surface_v1.md"
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",
)
M20_PRIVATE_MARKERS = (
"m20-solution-side",
"m20-solution-close",
"m20-solution-rubric",
)
M21_PRIVATE_MARKERS = (
"m21-solution-side",
"m21-solution-close",
"m21-solution-rubric",
)
PRIVATE_FIELD_TEXT = "public text mentions " + INTERNAL_FIELD
M20_FIELD_TEXT = "m20 public text mentions " + INTERNAL_FIELD
M21_FIELD_TEXT = "m21 public text mentions " + INTERNAL_FIELD
DOCUMENTED_TRAINING_ROUTES = (
("GET", "/training/cases"),
("GET", "/training/cases/{case_id}"),
("GET", "/training/cases/export"),
("POST", "/training/cases/import"),
("POST", "/training/attempts"),
("POST", "/training/attempts/{attempt_id}/annotations"),
("POST", "/training/attempts/{attempt_id}/submit"),
("GET", "/training/progress"),
)
PUBLIC_PRIVACY_CONTRACT_TEST_SURFACES = (
"service public export",
"service public list/detail",
"route list/detail",
"route export",
"route import",
"route progress",
"route submit",
"route create-attempt",
"route annotation lifecycle",
)
PUBLIC_VALIDATION_ERROR_CONTRACT_TEST_SURFACES = (
"invalid path identifiers",
"invalid and blank limit query values",
"malformed/non-object JSON bodies",
"attempt request field validation",
"annotation request field validation",
"submit request field validation",
"import request field validation",
"not_found behavior",
"internal_server_error masking",
"route error contract consistency",
)
VALIDATION_ERROR_CONTRACT_TEST_FILE = ROOT / "tests" / "unit" / "test_training_api_error_contract_m9.py"
PUBLIC_PRIVACY_CONTRACT_TEST_INVENTORY = (
{
"surface": "service public export",
"file": ROOT / "tests" / "unit" / "test_training_public_export_privacy_m15.py",
"class_name": "TrainingPublicExportPrivacyM15Tests",
"test_names": ("test_service_public_export_scrubs_nested_solution_material",),
},
{
"surface": "service public list/detail",
"file": ROOT / "tests" / "unit" / "test_training_public_export_privacy_m15.py",
"class_name": "TrainingPublicExportPrivacyM15Tests",
"test_names": ("test_service_public_list_and_detail_scrub_nested_solution_material",),
},
{
"surface": "route list/detail",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_list_detail_routes_scrub_nested_solution_material",),
},
{
"surface": "route export",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_export_route_scrubs_nested_solution_material",),
},
{
"surface": "route import",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_import_route_scrubs_nested_solution_material_from_response",),
},
{
"surface": "route progress",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_progress_route_scrubs_nested_solution_material",),
},
{
"surface": "route submit",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_submit_route_scrubs_unexpected_service_solution_material",),
},
{
"surface": "route create-attempt",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_create_attempt_route_scrubs_unexpected_service_solution_material",),
},
{
"surface": "route annotation lifecycle",
"file": ROOT / "tests" / "unit" / "test_training_public_api_contract_m8.py",
"class_name": "TrainingPublicApiContractM8Tests",
"test_names": ("test_public_training_annotation_route_scrubs_unexpected_service_solution_material",),
},
)
PUBLIC_VALIDATION_ERROR_CONTRACT_TEST_INVENTORY = (
{
"surface": "invalid path identifiers",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_invalid_case_path_identifier_returns_public_400",
"test_non_positive_path_identifiers_are_rejected_before_training_handlers_run",
"test_invalid_post_path_identifiers_are_rejected_before_body_parsing",
"test_missing_and_invalid_annotation_fields_return_public_400",
"test_invalid_submit_fields_return_public_400",
),
},
{
"surface": "invalid and blank limit query values",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_invalid_limits_are_rejected_before_training_handlers_run",
"test_route_request_helper_rejects_blank_training_limits",
),
},
{
"surface": "malformed/non-object JSON bodies",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_malformed_or_non_object_bodies_return_public_400",
"test_error_responses_are_minimal_json_and_privacy_safe",
),
},
{
"surface": "attempt request field validation",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_create_attempt_rejects_invalid_case_identifier_before_side_effects",
"test_missing_and_invalid_attempt_fields_return_public_400",
"test_error_responses_are_minimal_json_and_privacy_safe",
),
},
{
"surface": "annotation request field validation",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": ("test_missing_and_invalid_annotation_fields_return_public_400",),
},
{
"surface": "submit request field validation",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": ("test_invalid_submit_fields_return_public_400",),
},
{
"surface": "import request field validation",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": ("test_missing_and_invalid_import_fields_return_public_400",),
},
{
"surface": "not_found behavior",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_missing_case_returns_public_not_found",
"test_error_responses_are_minimal_json_and_privacy_safe",
),
},
{
"surface": "internal_server_error masking",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": (
"test_handler_internal_error_does_not_echo_raw_detail",
"test_error_responses_are_minimal_json_and_privacy_safe",
),
},
{
"surface": "route error contract consistency",
"file": VALIDATION_ERROR_CONTRACT_TEST_FILE,
"class_name": "TrainingApiErrorContractM9Tests",
"test_names": ("test_documented_training_routes_expose_consistent_public_error_contract",),
},
)
class _TrainingRouteProbe:
def __init__(self) -> None:
self.calls = []
def dependencies(self) -> dict:
return {
"training_cases": self.training_cases,
"training_case": self.training_case,
"training_export_cases": self.training_export_cases,
"training_import_cases": self.training_import_cases,
"training_create_attempt": self.training_create_attempt,
"training_add_annotation": self.training_add_annotation,
"training_submit_attempt": self.training_submit_attempt,
"training_progress": self.training_progress,
}
def training_cases(self, **kwargs):
self.calls.append(("training_cases", kwargs))
return [{"id": 7}]
def training_case(self, **kwargs):
self.calls.append(("training_case", kwargs))
return {"id": kwargs["case_id"]}
def training_export_cases(self, **kwargs):
self.calls.append(("training_export_cases", kwargs))
return {"version": 1, "items": []}
def training_import_cases(self, body):
self.calls.append(("training_import_cases", {"body": body}))
return {"version": 1, "imported": 1, "items": []}
def training_create_attempt(self, body):
self.calls.append(("training_create_attempt", {"body": body}))
return {"id": 101}
def training_add_annotation(self, attempt_id, body):
self.calls.append(("training_add_annotation", {"attempt_id": attempt_id, "body": body}))
return {"id": 202}
def training_submit_attempt(self, attempt_id, body):
self.calls.append(("training_submit_attempt", {"attempt_id": attempt_id, "body": body}))
return {"id": attempt_id, "status": "submitted"}
def training_progress(self, **kwargs):
self.calls.append(("training_progress", kwargs))
return [{"user_id": kwargs["user_id"], "skill_tag": kwargs["skill_tag"]}]
class _HandlerHarness(server.APIRequestHandler):
def __init__(self, path: str, dependencies: dict, method: str = "GET", body: bytes = b"") -> None:
self.path = path
self.dependencies = dependencies
self.headers = {"Content-Length": str(len(body))}
self.rfile = io.BytesIO(body)
self.wfile = io.BytesIO()
self.status = None
self.response_headers = {}
self._method = method
def send_response(self, code, message=None): # noqa: D401
self.status = code
def send_header(self, keyword, value):
self.response_headers[keyword] = value
def end_headers(self):
return None
def log_message(self, format, *args):
return None
def dispatch(self) -> None:
if self._method == "POST":
self.do_POST()
else:
self.do_GET()
def json_body(self) -> dict:
return json.loads(self.wfile.getvalue().decode("utf-8"))
class _ExecutionServiceStub:
def build_execution_dashboard_items(self, instrument_id: int, limit: int = 20) -> list:
return []
class _PublicTrainingRepository:
def __init__(self) -> None:
self.get_case_args = None
self.created_cases = []
self.created_tasks = []
def list_cases(self, status=None, skill_tag=None, instrument_id=None, limit=100):
return [self._case_row(7)]
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 create_case(self, **kwargs):
self.created_cases.append(kwargs)
return 41
def add_task(self, **kwargs):
self.created_tasks.append(kwargs)
return 201
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-01T09:00:00Z",
"decision_ts": "2026-04-01T10:00:00Z",
"end_ts": "2026-04-01T11:00:00Z",
"difficulty": "medium",
"status": "published",
"source_event_refs": [{"kind": "sweep_event", "id": 9}],
INTERNAL_FIELD: {
"answer": {"side": PRIVATE_MARKER},
"rubric": [{"category": PRIVATE_RUBRIC_MARKER, "max_score": 5}],
},
"meta": {
"title": "Public sweep practice",
"skill_tags": ["sweep", "liquidity_pool"],
},
}
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]},
}
],
"metadata_note": {
"public_label": "visible",
"public_note": PRIVATE_FIELD_TEXT,
"answer": NESTED_PRIVATE_MARKERS[0],
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[1],
},
},
}
class _M20ProgressRepository:
def __init__(self) -> None:
self.progress_args = None
def get_progress(self, user_id="local", skill_tag=None, limit=100):
self.progress_args = {"user_id": user_id, "skill_tag": skill_tag, "limit": limit}
return [
{
"user_id": user_id,
"skill_tag": skill_tag or "sweep",
"attempt_count": 3,
"avg_score": 8.5,
"weakness_tags": ["entry_timing"],
"meta": {
"public_summary": "Needs more context reps",
"public_note": M20_FIELD_TEXT,
"answer": {"side": M20_PRIVATE_MARKERS[0]},
INTERNAL_FIELD: {"rubric": M20_PRIVATE_MARKERS[1]},
"nested": {
"visible": "kept",
"answer": M20_PRIVATE_MARKERS[2],
},
},
}
][:limit]
class _M20SubmitService:
def __init__(self) -> None:
self.calls = []
def submit_attempt(self, attempt_id: int, total_score=None) -> dict:
self.calls.append({"attempt_id": attempt_id, "total_score": total_score})
return {
"id": attempt_id,
"status": "submitted",
"summary": {
"public_result": "recorded",
"public_note": M20_FIELD_TEXT,
"answer": {"side": M20_PRIVATE_MARKERS[0]},
INTERNAL_FIELD: {"rubric": M20_PRIVATE_MARKERS[1]},
"nested": {
"visible": "kept",
"answer": M20_PRIVATE_MARKERS[2],
},
},
}
class _M21AttemptLifecycleService:
def __init__(self) -> None:
self.calls = []
def create_attempt(
self,
case_id: int,
user_id: str = "local",
current_frame_index: int = 0,
meta=None,
) -> dict:
self.calls.append(
{
"method": "create_attempt",
"case_id": case_id,
"user_id": user_id,
"current_frame_index": current_frame_index,
"meta": meta,
}
)
return {
"id": 808,
"status": "started",
"summary": {
"public_result": "attempt created",
"public_note": M21_FIELD_TEXT,
"answer": {"side": M21_PRIVATE_MARKERS[0]},
INTERNAL_FIELD: {"rubric": M21_PRIVATE_MARKERS[1]},
"nested": {
"visible": "kept",
"answer": M21_PRIVATE_MARKERS[2],
},
},
}
def add_annotation(
self,
attempt_id: int,
annotation_type: str,
payload: dict,
user_id: str = "local",
task_id=None,
) -> dict:
self.calls.append(
{
"method": "add_annotation",
"attempt_id": attempt_id,
"annotation_type": annotation_type,
"payload": payload,
"user_id": user_id,
"task_id": task_id,
}
)
return {
"id": 909,
"status": "recorded",
"summary": {
"public_result": "annotation added",
"public_note": M21_FIELD_TEXT,
"answer": {"side": M21_PRIVATE_MARKERS[0]},
INTERNAL_FIELD: {"rubric": M21_PRIVATE_MARKERS[1]},
"nested": {
"visible": "kept",
"answer": M21_PRIVATE_MARKERS[2],
},
},
}
class _WorkflowContractRepository:
def __init__(self) -> None:
self.calls = []
def get_case(self, case_id: int, **kwargs):
self.calls.append(
(
"get_case",
{
"case_id": case_id,
"include_" + INTERNAL_FIELD: kwargs.get("include_" + INTERNAL_FIELD, False),
},
)
)
return {
"id": case_id,
"case_type": "sweep",
"skill_tag": "sweep",
"status": "published",
"meta": {
"title": "Workflow contract case",
"public_note": "visible",
"answer": M21_PRIVATE_MARKERS[0],
INTERNAL_FIELD: M21_PRIVATE_MARKERS[1],
},
INTERNAL_FIELD: {"rubric": M21_PRIVATE_MARKERS[2]},
}
def create_attempt(
self,
case_id: int,
user_id: str = "local",
status: str = "started",
current_frame_index: int = 0,
meta=None,
) -> int:
self.calls.append(
(
"create_attempt",
{
"case_id": case_id,
"user_id": user_id,
"status": status,
"current_frame_index": current_frame_index,
"meta": meta,
},
)
)
return 501
def submit_attempt(self, attempt_id: int, total_score=None, status: str = "submitted") -> None:
self.calls.append(
(
"submit_attempt",
{"attempt_id": attempt_id, "total_score": total_score, "status": status},
)
)
def get_progress(self, user_id: str = "local", skill_tag=None, limit: int = 100):
self.calls.append(("get_progress", {"user_id": user_id, "skill_tag": skill_tag, "limit": limit}))
return [
{
"user_id": user_id,
"skill_tag": skill_tag or "sweep",
"attempt_count": 1,
"avg_score": 8.25,
"meta": {
"public_result": "submitted",
"answer": M21_PRIVATE_MARKERS[0],
INTERNAL_FIELD: M21_PRIVATE_MARKERS[1],
},
}
][:limit]
def _documented_routes() -> tuple:
text = TRAINING_PRODUCT_DOC.read_text(encoding="utf-8")
routes = []
for method, path in re.findall(r"\|\s*(GET|POST)\s*\|\s*`([^`]+)`\s*\|", text):
if path.startswith("/training/"):
routes.append((method, path))
return tuple(routes)
def _training_dependencies(service: TrainingCaseService) -> dict:
return {
"training_cases": lambda **kwargs: training_api.get_training_cases(service=service, **kwargs),
"training_case": lambda **kwargs: training_api.get_training_case(service=service, **kwargs),
"training_export_cases": lambda **kwargs: training_api.export_training_case_package(service=service, **kwargs),
"training_import_cases": lambda body: training_api.import_training_case_package(body, service=service),
"training_create_attempt": lambda body: training_api.create_training_attempt(body, service=service),
"training_add_annotation": lambda attempt_id, body: training_api.add_training_annotation(attempt_id, body, service=service),
"training_submit_attempt": lambda attempt_id, body: training_api.submit_training_attempt(attempt_id, body, service=service),
"training_progress": lambda **kwargs: training_api.get_training_progress(service=service, **kwargs),
}
def _public_import_package() -> dict:
return {
"version": 1,
"items": [
{
"title": "Imported sweep practice",
"scenario": {
"instrument_id": 1,
"timeframe": "15m",
"case_type": "sweep",
"start_ts": "2026-04-01T09:00:00Z",
"decision_ts": "2026-04-01T10:00:00Z",
"end_ts": "2026-04-01T11:00:00Z",
"source_event_refs": [{"kind": "sweep_event", "id": 9}],
},
"skill_tags": ["sweep", "liquidity_pool"],
"difficulty": "medium",
"answer": {"side": PRIVATE_MARKER},
"rubric": [{"category": PRIVATE_RUBRIC_MARKER, "max_score": 5}],
"tasks": [
{
"task_type": "multiple_choice",
"skill_tag": "sweep",
"prompt": "Classify the setup.",
"required_annotation_types": ["multiple_choice"],
"max_score": 5,
}
],
}
],
}
def _nested_public_import_package() -> dict:
return {
"version": 1,
"items": [
{
"external_id": "m19-import-ref " + INTERNAL_FIELD,
"title": "Imported response privacy",
"scenario": {
"instrument_id": 1,
"timeframe": "15m",
"case_type": "sweep",
"start_ts": "2026-04-22T09:00:00Z",
"decision_ts": "2026-04-22T10:00:00Z",
"end_ts": "2026-04-22T11:00:00Z",
"source_event_refs": [
{
"kind": "sweep_event",
"id": 19,
"answer": NESTED_PRIVATE_MARKERS[0],
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[1],
},
{"kind": "context", "id": 20, "label": PRIVATE_FIELD_TEXT},
],
},
"skill_tags": ["sweep", "context_reading"],
"difficulty": "medium",
"status": "draft",
"answer": {
"side": NESTED_PRIVATE_MARKERS[0],
"nested": {
"close": NESTED_PRIVATE_MARKERS[1],
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[2],
},
},
"rubric": [
{
"category": NESTED_PRIVATE_MARKERS[2],
"max_score": 5,
"answer": NESTED_PRIVATE_MARKERS[0],
INTERNAL_FIELD: {"detail": NESTED_PRIVATE_MARKERS[1]},
}
],
"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.",
"answer": NESTED_PRIVATE_MARKERS[0],
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[1],
"nested": {"answer": NESTED_PRIVATE_MARKERS[2]},
},
}
],
"metadata": {
"chapter_ref": "chapter_19",
"public_label": "visible",
"answer": NESTED_PRIVATE_MARKERS[0],
INTERNAL_FIELD: NESTED_PRIVATE_MARKERS[1],
"nested": {"answer": NESTED_PRIVATE_MARKERS[2]},
},
}
],
}
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 []
def _test_methods_by_class(path: Path) -> dict[str, set[str]]:
tree = ast.parse(path.read_text(encoding="utf-8"), filename=str(path))
return {
node.name: {
item.name
for item in node.body
if isinstance(item, ast.FunctionDef) and item.name.startswith("test_")
}
for node in tree.body
if isinstance(node, ast.ClassDef)
}
class TrainingPublicApiContractM8Tests(unittest.TestCase):
def test_public_privacy_contract_inventory_names_exist_by_surface(self) -> None:
surfaces = tuple(entry["surface"] for entry in PUBLIC_PRIVACY_CONTRACT_TEST_INVENTORY)
self.assertEqual(surfaces, PUBLIC_PRIVACY_CONTRACT_TEST_SURFACES)
parsed_methods_by_path = {}
seen_test_names = set()
for entry in PUBLIC_PRIVACY_CONTRACT_TEST_INVENTORY:
path = entry["file"]
class_name = entry["class_name"]
test_names = entry["test_names"]
self.assertGreater(len(test_names), 0, entry["surface"])
if path not in parsed_methods_by_path:
self.assertTrue(path.exists(), path)
parsed_methods_by_path[path] = _test_methods_by_class(path)
class_methods = parsed_methods_by_path[path]
self.assertIn(class_name, class_methods)
for test_name in test_names:
seen_key = (path, class_name, test_name)
with self.subTest(surface=entry["surface"], test_name=test_name):
self.assertNotIn(seen_key, seen_test_names)
seen_test_names.add(seen_key)
self.assertIn(test_name, class_methods[class_name])
def test_public_validation_error_contract_inventory_names_exist_by_surface(self) -> None:
surfaces = tuple(entry["surface"] for entry in PUBLIC_VALIDATION_ERROR_CONTRACT_TEST_INVENTORY)
self.assertEqual(surfaces, PUBLIC_VALIDATION_ERROR_CONTRACT_TEST_SURFACES)
parsed_methods_by_path = {}
seen_inventory_entries = set()
for entry in PUBLIC_VALIDATION_ERROR_CONTRACT_TEST_INVENTORY:
path = entry["file"]
class_name = entry["class_name"]
test_names = entry["test_names"]
self.assertGreater(len(test_names), 0, entry["surface"])
if path not in parsed_methods_by_path:
self.assertTrue(path.exists(), path)
parsed_methods_by_path[path] = _test_methods_by_class(path)
class_methods = parsed_methods_by_path[path]
self.assertIn(class_name, class_methods)
for test_name in test_names:
seen_key = (entry["surface"], path, class_name, test_name)
with self.subTest(surface=entry["surface"], test_name=test_name):
self.assertNotIn(seen_key, seen_inventory_entries)
seen_inventory_entries.add(seen_key)
self.assertIn(test_name, class_methods[class_name])
def test_documented_training_routes_resolve_to_expected_handlers(self) -> None:
self.assertEqual(_documented_routes(), DOCUMENTED_TRAINING_ROUTES)
route_cases = (
(
"GET",
"/training/cases",
{"status": ["published"], "skill_tag": ["sweep"], "instrument_id": ["1"], "limit": ["8"]},
None,
200,
("training_cases", {"status": "published", "skill_tag": "sweep", "instrument_id": 1, "limit": 8}),
),
("GET", "/training/cases/7", {}, None, 200, ("training_case", {"case_id": 7})),
(
"GET",
"/training/cases/export",
{"status": ["published"], "skill_tag": ["sweep"], "instrument_id": ["1"], "limit": ["8"]},
None,
200,
("training_export_cases", {"status": "published", "skill_tag": "sweep", "instrument_id": 1, "limit": 8}),
),
(
"POST",
"/training/cases/import",
{},
{"version": 1, "items": [{"title": "stub"}]},
201,
("training_import_cases", {"body": {"version": 1, "items": [{"title": "stub"}]}}),
),
(
"POST",
"/training/attempts",
{},
{"case_id": 7, "user_id": "analyst"},
201,
("training_create_attempt", {"body": {"case_id": 7, "user_id": "analyst"}}),
),
(
"POST",
"/training/attempts/101/annotations",
{},
{"annotation_type": "zone", "payload": {"note": "context"}},
201,
(
"training_add_annotation",
{"attempt_id": 101, "body": {"annotation_type": "zone", "payload": {"note": "context"}}},
),
),
(
"POST",
"/training/attempts/101/submit",
{},
{"total_score": 9},
200,
("training_submit_attempt", {"attempt_id": 101, "body": {"total_score": 9}}),
),
(
"GET",
"/training/progress",
{"user_id": ["analyst"], "skill_tag": ["sweep"], "limit": ["4"]},
None,
200,
("training_progress", {"user_id": "analyst", "skill_tag": "sweep", "limit": 4}),
),
)
self.assertEqual(len(route_cases), len(DOCUMENTED_TRAINING_ROUTES))
for method, path, query, body, expected_status, expected_call in route_cases:
probe = _TrainingRouteProbe()
with self.subTest(method=method, path=path):
status, payload = server.route_request(
path,
query,
dependencies=probe.dependencies(),
method=method,
body=body,
)
self.assertEqual(status, expected_status)
self.assertIsInstance(payload, dict)
self.assertEqual(probe.calls, [expected_call])
def test_training_http_handler_smoke_uses_documented_routes_and_injected_dependencies(self) -> None:
route_cases = (
(
"GET",
"/training/cases?status=published&skill_tag=sweep&instrument_id=1&limit=8",
b"",
200,
{"items": [{"id": 7}]},
("training_cases", {"status": "published", "skill_tag": "sweep", "instrument_id": 1, "limit": 8}),
),
("GET", "/training/cases/7", b"", 200, {"id": 7}, ("training_case", {"case_id": 7})),
(
"GET",
"/training/cases/export?status=published&skill_tag=sweep&instrument_id=1&limit=8",
b"",
200,
{"version": 1, "items": []},
("training_export_cases", {"status": "published", "skill_tag": "sweep", "instrument_id": 1, "limit": 8}),
),
(
"POST",
"/training/cases/import",
json.dumps({"version": 1, "items": [{"title": "stub"}]}).encode("utf-8"),
201,
{"version": 1, "imported": 1, "items": []},
("training_import_cases", {"body": {"version": 1, "items": [{"title": "stub"}]}}),
),
(
"POST",
"/training/attempts",
json.dumps({"case_id": 7, "user_id": "analyst"}).encode("utf-8"),
201,
{"id": 101},
("training_create_attempt", {"body": {"case_id": 7, "user_id": "analyst"}}),
),
(
"POST",
"/training/attempts/101/annotations",
json.dumps({"annotation_type": "zone", "payload": {"note": "context"}}).encode("utf-8"),
201,
{"id": 202},
(
"training_add_annotation",
{"attempt_id": 101, "body": {"annotation_type": "zone", "payload": {"note": "context"}}},
),
),
(
"POST",
"/training/attempts/101/submit",
json.dumps({"total_score": 9}).encode("utf-8"),
200,
{"id": 101, "status": "submitted"},
("training_submit_attempt", {"attempt_id": 101, "body": {"total_score": 9}}),
),
(
"GET",
"/training/progress?user_id=analyst&skill_tag=sweep&limit=4",
b"",
200,
{"items": [{"user_id": "analyst", "skill_tag": "sweep"}]},
("training_progress", {"user_id": "analyst", "skill_tag": "sweep", "limit": 4}),
),
)
self.assertEqual(len(route_cases), len(DOCUMENTED_TRAINING_ROUTES))
for method, path, body, expected_status, expected_payload, expected_call in route_cases:
probe = _TrainingRouteProbe()
with self.subTest(method=method, path=path):
handler = _HandlerHarness(path, probe.dependencies(), method=method, body=body)
handler.dispatch()
self.assertEqual(handler.status, expected_status)
self.assertEqual(handler.json_body(), expected_payload)
self.assertEqual(handler.response_headers.get("Content-Type"), "application/json; charset=utf-8")
self.assertEqual(handler.response_headers.get("Content-Length"), str(len(handler.wfile.getvalue())))
self.assertEqual(probe.calls, [expected_call])
def test_training_case_selector_query_params_route_to_expected_handlers(self) -> None:
probe = _TrainingRouteProbe()
status, payload = server.route_request(
"/training/cases",
{
"quality_rank": ["high"],
"min_quality_score": ["80"],
"sort_by": ["quality_score_desc"],
},
dependencies=probe.dependencies(),
)
self.assertEqual(status, 200)
self.assertEqual(payload, {"items": [{"id": 7}]})
self.assertEqual(
probe.calls,
[
(
"training_cases",
{
"status": None,
"skill_tag": None,
"instrument_id": None,
"limit": 100,
"quality_rank": "high",
"min_quality_score": 80,
"sort_by": "quality_score_desc",
},
)
],
)
def test_training_workflow_contract_case_attempt_submit_progress_is_deterministic(self) -> None:
repository = _WorkflowContractRepository()
service = TrainingCaseService(repository=repository)
dependencies = _training_dependencies(service)
detail_status, detail_payload = server.route_request("/training/cases/17", {}, dependencies=dependencies)
attempt_status, attempt_payload = server.route_request(
"/training/attempts",
{},
dependencies=dependencies,
method="POST",
body={
"case_id": 17,
"user_id": "analyst",
"current_frame_index": 3,
"meta": {"source": "workflow-contract"},
},
)
submit_status, submit_payload = server.route_request(
"/training/attempts/501/submit",
{},
dependencies=dependencies,
method="POST",
body={"total_score": 8.25},
)
progress_status, progress_payload = server.route_request(
"/training/progress",
{"user_id": ["analyst"], "skill_tag": ["sweep"], "limit": ["1"]},
dependencies=dependencies,
)
self.assertEqual([detail_status, attempt_status, submit_status, progress_status], [200, 201, 200, 200])
self.assertEqual(
detail_payload,
{
"id": 17,
"case_type": "sweep",
"skill_tag": "sweep",
"status": "published",
"quality": {"score": 10, "rank": "low", "signals": ["published_case"]},
"meta": {"title": "Workflow contract case", "public_note": "visible"},
},
)
self.assertEqual(attempt_payload, {"id": 501})
self.assertEqual(submit_payload, {"id": 501, "status": "submitted"})
self.assertEqual(
progress_payload,
{
"items": [
{
"user_id": "analyst",
"skill_tag": "sweep",
"attempt_count": 1,
"avg_score": 8.25,
"meta": {"public_result": "submitted"},
}
]
},
)
self.assertEqual(
repository.calls,
[
("get_case", {"case_id": 17, "include_" + INTERNAL_FIELD: False}),
(
"create_attempt",
{
"case_id": 17,
"user_id": "analyst",
"status": "started",
"current_frame_index": 3,
"meta": {"source": "workflow-contract"},
},
),
("submit_attempt", {"attempt_id": 501, "total_score": 8.25, "status": "submitted"}),
("get_progress", {"user_id": "analyst", "skill_tag": "sweep", "limit": 1}),
],
)
serialized = json.dumps(
{
"detail": detail_payload,
"attempt": attempt_payload,
"submit": submit_payload,
"progress": progress_payload,
},
sort_keys=True,
)
self.assertNotIn(INTERNAL_FIELD, serialized)
for private_text in M21_PRIVATE_MARKERS:
with self.subTest(private_text=private_text):
self.assertNotIn(private_text, serialized)
def test_dependency_builder_exposes_training_handler_functions(self) -> None:
expected = {
"training_cases": training_api.get_training_cases,
"training_case": training_api.get_training_case,
"training_export_cases": training_api.export_training_case_package,
"training_import_cases": training_api.import_training_case_package,
"training_create_attempt": training_api.create_training_attempt,
"training_add_annotation": training_api.add_training_annotation,
"training_submit_attempt": training_api.submit_training_attempt,
"training_progress": training_api.get_training_progress,
}
with mock.patch.object(server, "ExecutionService", _ExecutionServiceStub):
dependencies = server.build_api_dependencies()
for name, handler in expected.items():
with self.subTest(name=name):
self.assertIs(dependencies[name], handler)
def test_public_training_serialization_omits_internal_solution_material(self) -> None:
repository = _PublicTrainingRepository()
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/7", {}, dependencies=dependencies)
export_status, export_payload = server.route_request("/training/cases/export", {}, dependencies=dependencies)
import_status, import_payload = server.route_request(
"/training/cases/import",
{},
dependencies=dependencies,
method="POST",
body=_public_import_package(),
)
self.assertEqual([list_status, detail_status, export_status, import_status], [200, 200, 200, 201])
self.assertEqual(repository.get_case_args, {"case_id": 7, "include_" + INTERNAL_FIELD: False})
self.assertEqual(len(repository.created_cases), 1)
self.assertIn(INTERNAL_FIELD, repository.created_cases[0])
serialized = json.dumps(
{
"list": list_payload,
"detail": detail_payload,
"export": export_payload,
"import": import_payload,
},
sort_keys=True,
)
for private_text in (INTERNAL_FIELD, PRIVATE_MARKER, PRIVATE_RUBRIC_MARKER):
with self.subTest(private_text=private_text):
self.assertNotIn(private_text, serialized)
def test_public_training_import_route_scrubs_nested_solution_material_from_response(self) -> None:
repository = _PublicTrainingRepository()
service = TrainingCaseService(repository=repository)
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/cases/import",
{},
dependencies=dependencies,
method="POST",
body=_nested_public_import_package(),
)
self.assertEqual(status, 201)
self.assertEqual(payload["imported"], 1)
self.assertEqual(payload["case_ids"], [41])
item = payload["items"][0]
self.assertEqual(
set(item),
{"id", "title", "scenario", "skill_tags", "difficulty", "status", "external_id"},
)
self.assertEqual(item["id"], 41)
self.assertEqual(item["title"], "Imported response privacy")
self.assertEqual(item["external_id"], "m19-import-ref private solution field")
self.assertEqual(item["skill_tags"], ["sweep", "context_reading"])
self.assertEqual(item["difficulty"], "medium")
self.assertEqual(item["status"], "draft")
self.assertEqual(
item["scenario"],
{
"instrument_id": 1,
"timeframe": "15m",
"case_type": "sweep",
"start_ts": "2026-04-22T09:00:00Z",
"decision_ts": "2026-04-22T10:00:00Z",
"end_ts": "2026-04-22T11:00:00Z",
"source_event_refs": [
{"id": 19, "kind": "sweep_event"},
{"id": 20, "kind": "context", "label": "public text mentions private solution field"},
],
},
)
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)
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)
def test_public_training_export_route_scrubs_nested_solution_material(self) -> None:
repository = _NestedPublicTrainingRepository()
service = TrainingCaseService(repository=repository)
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/cases/export",
{"limit": ["100"]},
dependencies=dependencies,
)
self.assertEqual(status, 200)
self.assertEqual(payload["metadata"]["item_count"], 1)
item = payload["items"][0]
self.assertEqual(item["rubric"], [{"category": "context", "max_score": 3}])
self.assertEqual(item["tasks"][0]["meta"]["public_hint"], "Focus on context.")
self.assertEqual(item["metadata"]["metadata_note"]["public_label"], "visible")
self.assertIn("quality", item)
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)
def test_public_training_progress_route_scrubs_nested_solution_material(self) -> None:
repository = _M20ProgressRepository()
service = TrainingCaseService(repository=repository)
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/progress",
{"user_id": ["analyst"], "skill_tag": ["sweep"], "limit": ["1"]},
dependencies=dependencies,
)
self.assertEqual(status, 200)
self.assertEqual(repository.progress_args, {"user_id": "analyst", "skill_tag": "sweep", "limit": 1})
item = payload["items"][0]
self.assertEqual(item["user_id"], "analyst")
self.assertEqual(item["skill_tag"], "sweep")
self.assertEqual(item["attempt_count"], 3)
self.assertEqual(item["avg_score"], 8.5)
self.assertEqual(item["weakness_tags"], ["entry_timing"])
self.assertEqual(item["meta"]["public_summary"], "Needs more context reps")
self.assertEqual(item["meta"]["public_note"], "m20 public text mentions private solution field")
self.assertEqual(item["meta"]["nested"], {"visible": "kept"})
serialized = json.dumps(payload, sort_keys=True)
self.assertNotIn(INTERNAL_FIELD, serialized)
for private_text in M20_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)
def test_public_training_submit_route_scrubs_unexpected_service_solution_material(self) -> None:
service = _M20SubmitService()
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/attempts/707/submit",
{},
dependencies=dependencies,
method="POST",
body={"total_score": 8.5},
)
self.assertEqual(status, 200)
self.assertEqual(service.calls, [{"attempt_id": 707, "total_score": 8.5}])
self.assertEqual(payload["id"], 707)
self.assertEqual(payload["status"], "submitted")
self.assertEqual(payload["summary"]["public_result"], "recorded")
self.assertEqual(payload["summary"]["public_note"], "m20 public text mentions private solution field")
self.assertEqual(payload["summary"]["nested"], {"visible": "kept"})
serialized = json.dumps(payload, sort_keys=True)
self.assertNotIn(INTERNAL_FIELD, serialized)
for private_text in M20_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)
def test_public_training_create_attempt_route_scrubs_unexpected_service_solution_material(self) -> None:
service = _M21AttemptLifecycleService()
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/attempts",
{},
dependencies=dependencies,
method="POST",
body={"case_id": 17, "user_id": "analyst", "current_frame_index": 2, "meta": {"source": "unit"}},
)
self.assertEqual(status, 201)
self.assertEqual(
service.calls,
[
{
"method": "create_attempt",
"case_id": 17,
"user_id": "analyst",
"current_frame_index": 2,
"meta": {"source": "unit"},
}
],
)
self.assertEqual(payload["id"], 808)
self.assertEqual(payload["status"], "started")
self.assertEqual(payload["summary"]["public_result"], "attempt created")
self.assertEqual(payload["summary"]["public_note"], "m21 public text mentions private solution field")
self.assertEqual(payload["summary"]["nested"], {"visible": "kept"})
serialized = json.dumps(payload, sort_keys=True)
self.assertNotIn(INTERNAL_FIELD, serialized)
for private_text in M21_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)
def test_public_training_annotation_route_scrubs_unexpected_service_solution_material(self) -> None:
service = _M21AttemptLifecycleService()
dependencies = _training_dependencies(service)
status, payload = server.route_request(
"/training/attempts/808/annotations",
{},
dependencies=dependencies,
method="POST",
body={
"annotation_type": "zone",
"payload": {"note": "context"},
"user_id": "analyst",
"task_id": 33,
},
)
self.assertEqual(status, 201)
self.assertEqual(
service.calls,
[
{
"method": "add_annotation",
"attempt_id": 808,
"annotation_type": "zone",
"payload": {"note": "context"},
"user_id": "analyst",
"task_id": 33,
}
],
)
self.assertEqual(payload["id"], 909)
self.assertEqual(payload["status"], "recorded")
self.assertEqual(payload["summary"]["public_result"], "annotation added")
self.assertEqual(payload["summary"]["public_note"], "m21 public text mentions private solution field")
self.assertEqual(payload["summary"]["nested"], {"visible": "kept"})
serialized = json.dumps(payload, sort_keys=True)
self.assertNotIn(INTERNAL_FIELD, serialized)
for private_text in M21_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()