from __future__ import annotations import unittest from types import SimpleNamespace from unittest.mock import AsyncMock, patch from uuid import uuid4 from fastapi import HTTPException from pydantic import SecretStr from .contracts.supervision_research import ( LearnerAttentionSignal, LedgerEvidencePointer, ) from .routes import supervision_research from .services import supervision_research_store def _pointer() -> LedgerEvidencePointer: return LedgerEvidencePointer( ledger="measurement_event", event_id=str(uuid4()), session_id=str(uuid4()), route_hint="/sessions/{session_id}/measurements", ) def _signal() -> LearnerAttentionSignal: return LearnerAttentionSignal( signal_id="oas-g6-signal-attention-one", learner_ref="learner-alpha", signal_type="deterioration", severity="high", state="active", uncertainty=0.2, observed_sequence=1, evidence=(_pointer(),), ) class SupervisionResearchStoreIdempotencyTests(unittest.IsolatedAsyncioTestCase): async def test_same_submission_returns_stable_snapshot_without_children(self) -> None: submission_id = uuid4() snapshot_id = uuid4() learner_id = uuid4() conn = AsyncMock() conn.fetchrow.return_value = { "snapshot_id": snapshot_id, "content_hash": "a" * 64, } with patch.object( supervision_research_store, "_canonical_hash", return_value="a" * 64 ): result = await supervision_research_store.append_attention_snapshot( conn, submission_id=submission_id, snapshot_id=snapshot_id, cohort_id="cohort-a", signals=[_signal()], learner_ids_by_ref={"learner-alpha": learner_id}, ) self.assertEqual(result["snapshot_id"], snapshot_id) self.assertTrue(result["idempotent_replay"]) self.assertFalse(result["clinical_claim_allowed"]) self.assertEqual(conn.execute.await_count, 1) async def test_changed_submission_is_conflict(self) -> None: conn = AsyncMock() conn.fetchrow.return_value = { "snapshot_id": uuid4(), "content_hash": "b" * 64, } with patch.object( supervision_research_store, "_canonical_hash", return_value="a" * 64 ): with self.assertRaises( supervision_research_store.SupervisionResearchConflictError ): await supervision_research_store.append_attention_snapshot( conn, submission_id=uuid4(), snapshot_id=uuid4(), cohort_id="cohort-a", signals=[_signal()], learner_ids_by_ref={"learner-alpha": uuid4()}, ) class SupervisionResearchInternalAuthenticationTests( unittest.IsolatedAsyncioTestCase ): TOKEN = "g6-supervision-research-token-at-least-32-characters" async def _assert_rejected_before_db( self, dependency, provider_name: str, configured: str | None, presented: str | None, expected_status: int, ) -> None: reached = False async def fake_provider(): nonlocal reached reached = True yield AsyncMock() settings = SimpleNamespace( supervision_research_internal_token=SecretStr(configured or "") ) with ( patch.object(supervision_research, provider_name, fake_provider), ): generator = dependency(settings=settings, presented_token=presented) with self.assertRaises(HTTPException) as captured: await anext(generator) self.assertEqual(captured.exception.status_code, expected_status) self.assertFalse(reached) async def test_supervisor_missing_configuration_is_503_before_db(self) -> None: await self._assert_rejected_before_db( supervision_research.supervision_research_internal_supervisor_db, "_supervisor_db_provider", None, None, 503, ) async def test_research_short_configuration_is_503_before_db(self) -> None: await self._assert_rejected_before_db( supervision_research.supervision_research_internal_research_db, "_research_db_provider", "short", None, 503, ) async def test_missing_header_is_401_before_db(self) -> None: await self._assert_rejected_before_db( supervision_research.supervision_research_internal_supervisor_db, "_supervisor_db_provider", self.TOKEN, None, 401, ) async def test_wrong_header_is_403_before_db(self) -> None: await self._assert_rejected_before_db( supervision_research.supervision_research_internal_research_db, "_research_db_provider", self.TOKEN, "wrong-token", 403, ) async def test_valid_tokens_reach_only_requested_ai_provider(self) -> None: supervisor_conn = AsyncMock() research_conn = AsyncMock() async def fake_supervisor(): yield supervisor_conn async def fake_research(): yield research_conn with ( patch.object( supervision_research, "_supervisor_db_provider", fake_supervisor ), patch.object(supervision_research, "_research_db_provider", fake_research), ): supervisor = ( supervision_research.supervision_research_internal_supervisor_db( settings=SimpleNamespace( supervision_research_internal_token=SecretStr(self.TOKEN) ), presented_token=self.TOKEN ) ) research = supervision_research.supervision_research_internal_research_db( settings=SimpleNamespace( supervision_research_internal_token=SecretStr(self.TOKEN) ), presented_token=self.TOKEN ) self.assertIs(await anext(supervisor), supervisor_conn) self.assertIs(await anext(research), research_conn) await supervisor.aclose() await research.aclose() class SupervisionResearchBoundaryTests(unittest.TestCase): def test_changed_submission_maps_to_http_409(self) -> None: with self.assertRaises(HTTPException) as captured: supervision_research._raise_store_error( supervision_research_store.SupervisionResearchConflictError( "changed content" ) ) self.assertEqual(captured.exception.status_code, 409) def test_write_response_schemas_have_no_aggregate_score_fields(self) -> None: forbidden = {"total", "total_score", "overall_score", "global_score"} models = ( supervision_research.AttentionSnapshotResponse, supervision_research.CurriculumGapResponse, supervision_research.TeacherDisagreementResponse, supervision_research.EvaluationComparisonResponse, supervision_research.Phase3ManifestResponse, ) for model in models: self.assertTrue( forbidden.isdisjoint(model.model_fields), f"forbidden score field in {model.__name__}", ) def test_teacher_request_forbids_transcript_payload(self) -> None: schema_fields = supervision_research.TeacherDisagreementRequest.model_fields self.assertNotIn("transcript", schema_fields) self.assertNotIn("raw_transcript", schema_fields) self.assertNotIn("utterance_text", schema_fields) if __name__ == "__main__": unittest.main()