import io import json import re import unittest from pathlib import Path from src.api.server import APIRequestHandler, route_request from src.api.training import ( add_training_annotation, create_training_attempt, get_training_case, get_training_cases, get_training_progress, submit_training_attempt, ) from src.services.training import TrainingCaseService class _FakeTrainingRepository: def __init__(self) -> None: self.list_args = None self.get_args = None self.created_attempt = None self.annotation = None self.submitted_attempt = None self.progress_args = None def list_cases(self, status=None, skill_tag=None, instrument_id=None, limit=100): self.list_args = { "status": status, "skill_tag": skill_tag, "instrument_id": instrument_id, "limit": limit, } return [ { "id": 7, "case_type": "sweep", "skill_tag": "liquidity_pool", "answer_key": {"side": "buy_side"}, } ] def get_case(self, case_id: int, include_answer_key: bool = False): self.get_args = {"case_id": case_id, "include_answer_key": include_answer_key} return { "id": case_id, "case_type": "sweep", "skill_tag": "liquidity_pool", "answer_key": {"side": "buy_side"}, } def create_attempt(self, case_id, user_id="local", status="started", current_frame_index=0, meta=None): self.created_attempt = { "case_id": case_id, "user_id": user_id, "status": status, "current_frame_index": current_frame_index, "meta": meta, } return 101 def add_annotation(self, attempt_id, annotation_type, payload, user_id="local", task_id=None): self.annotation = { "attempt_id": attempt_id, "annotation_type": annotation_type, "payload": payload, "user_id": user_id, "task_id": task_id, } return 202 def submit_attempt(self, attempt_id, total_score, status="submitted"): self.submitted_attempt = {"attempt_id": attempt_id, "total_score": total_score, "status": status} 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 "liquidity_pool", "attempt_count": 1}] def _dependencies(service: TrainingCaseService) -> dict: return { "training_cases": lambda **kwargs: get_training_cases(service=service, **kwargs), "training_case": lambda **kwargs: get_training_case(service=service, **kwargs), "training_create_attempt": lambda body: create_training_attempt(body, service=service), "training_add_annotation": lambda attempt_id, body: add_training_annotation(attempt_id, body, service=service), "training_submit_attempt": lambda attempt_id, body: submit_training_attempt(attempt_id, body, service=service), "training_progress": lambda **kwargs: get_training_progress(service=service, **kwargs), } class _PostHandlerHarness(APIRequestHandler): def __init__(self, path: str, body: bytes, dependencies: dict) -> None: self.path = path self.headers = {"Content-Length": str(len(body))} self.rfile = io.BytesIO(body) self.wfile = io.BytesIO() self.dependencies = dependencies self.status = None self.response_headers = {} 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 json_body(self): return json.loads(self.wfile.getvalue().decode("utf-8")) class TrainingApiM2Tests(unittest.TestCase): def setUp(self) -> None: self.repository = _FakeTrainingRepository() self.service = TrainingCaseService(repository=self.repository) self.dependencies = _dependencies(self.service) def test_get_training_cases_hides_answer_key(self) -> None: status, payload = route_request( "/training/cases", {"status": ["published"], "skill_tag": ["liquidity_pool"], "instrument_id": ["1"], "limit": ["5"]}, dependencies=self.dependencies, ) self.assertEqual(status, 200) self.assertEqual(payload["items"][0]["id"], 7) self.assertEqual(payload["items"][0]["quality"]["rank"], "low") self.assertNotIn("answer_key", payload["items"][0]) self.assertEqual( self.repository.list_args, {"status": "published", "skill_tag": "liquidity_pool", "instrument_id": 1, "limit": 5}, ) def test_get_training_case_hides_answer_key(self) -> None: status, payload = route_request("/training/cases/7", {}, dependencies=self.dependencies) self.assertEqual(status, 200) self.assertEqual(payload["id"], 7) self.assertEqual(payload["quality"]["rank"], "low") self.assertNotIn("answer_key", payload) self.assertEqual(self.repository.get_args, {"case_id": 7, "include_answer_key": False}) def test_get_training_cases_supports_quality_selector_filters_and_sort(self) -> None: class QualityRepository(_FakeTrainingRepository): def list_cases(self, status=None, skill_tag=None, instrument_id=None, limit=100): self.list_args = { "status": status, "skill_tag": skill_tag, "instrument_id": instrument_id, "limit": limit, } return [ { "id": 7, "case_type": "sweep", "skill_tag": "liquidity_pool", "status": "published", "source_event_refs": [{"kind": "sweep_event", "id": 1}], "meta": { "title": "Higher quality case", "skill_tags": ["liquidity_pool", "context_reading"], "rubric": [{"category": "context", "max_score": 5}], "tasks": [{"task_type": "note", "prompt": "Explain context."}], "chapter_ref": "chapter_01", }, "start_ts": "2026-04-01T09:00:00Z", "decision_ts": "2026-04-01T10:00:00Z", "end_ts": "2026-04-01T11:00:00Z", }, { "id": 8, "case_type": "sweep", "skill_tag": "liquidity_pool", "status": "draft", "meta": {"title": "Lower quality case"}, }, ] repository = QualityRepository() service = TrainingCaseService(repository=repository) dependencies = _dependencies(service) status, payload = route_request( "/training/cases", { "quality_rank": ["high"], "min_quality_score": ["80"], "sort_by": ["quality_score_desc"], }, dependencies=dependencies, ) self.assertEqual(status, 200) self.assertEqual([item["id"] for item in payload["items"]], [7]) self.assertEqual(payload["items"][0]["quality"]["rank"], "high") self.assertGreaterEqual(payload["items"][0]["quality"]["score"], 80) def test_post_training_attempt_creates_attempt(self) -> None: status, payload = route_request( "/training/attempts", {}, dependencies=self.dependencies, method="POST", body={"case_id": 7, "user_id": "analyst", "current_frame_index": 3, "meta": {"source": "unit"}}, ) self.assertEqual(status, 201) self.assertEqual(payload, {"id": 101}) self.assertEqual( self.repository.created_attempt, { "case_id": 7, "user_id": "analyst", "status": "started", "current_frame_index": 3, "meta": {"source": "unit"}, }, ) def test_annotation_submit_and_progress_routes(self) -> None: annotation_status, annotation_payload = route_request( "/training/attempts/101/annotations", {}, dependencies=self.dependencies, method="POST", body={ "annotation_type": "liquidity_zone", "payload": {"price_low": 100.0, "price_high": 101.0}, "task_id": 11, "user_id": "analyst", }, ) submit_status, submit_payload = route_request( "/training/attempts/101/submit", {}, dependencies=self.dependencies, method="POST", body={"total_score": 8.5}, ) progress_status, progress_payload = route_request( "/training/progress", {"user_id": ["analyst"], "skill_tag": ["liquidity_pool"], "limit": ["1"]}, dependencies=self.dependencies, ) self.assertEqual(annotation_status, 201) self.assertEqual(annotation_payload, {"id": 202}) self.assertEqual(self.repository.annotation["attempt_id"], 101) self.assertEqual(submit_status, 200) self.assertEqual(submit_payload, {"id": 101, "status": "submitted"}) self.assertEqual(self.repository.submitted_attempt, {"attempt_id": 101, "total_score": 8.5, "status": "submitted"}) self.assertEqual(progress_status, 200) self.assertEqual(progress_payload["items"][0]["user_id"], "analyst") self.assertEqual(self.repository.progress_args, {"user_id": "analyst", "skill_tag": "liquidity_pool", "limit": 1}) def test_post_invalid_json_or_missing_required_field_returns_400(self) -> None: invalid_json_handler = _PostHandlerHarness("/training/attempts", b"{", self.dependencies) invalid_json_handler.do_POST() missing_field_handler = _PostHandlerHarness("/training/attempts", b"{}", self.dependencies) missing_field_handler.do_POST() self.assertEqual(invalid_json_handler.status, 400) self.assertEqual(invalid_json_handler.json_body()["error"], "invalid_json_body") self.assertEqual(missing_field_handler.status, 400) self.assertEqual(missing_field_handler.json_body()["error"], "missing_required_json_field:case_id") def test_training_api_source_does_not_write_trading_chain_tables(self) -> None: paths = [ Path("src/api/training.py"), Path("src/api/server.py"), Path("src/services/training/training_case_service.py"), Path(__file__), ] source = "\n".join(path.read_text(encoding="utf-8") for path in paths if path.exists()).lower() for table_name in ("trade_signals", "trade_executions", "backtest_results"): forbidden = re.compile(rf"\b(insert\s+into|update|delete\s+from|truncate\s+table)\s+{table_name}\b") self.assertIsNone(forbidden.search(source), f"training API writes to {table_name}") if __name__ == "__main__": unittest.main()