G0~G8 성과·동맹 측정 OS 작업 일괄 고정
8월 7일까지 워킹트리에만 남아 있던 미커밋 작업을 커밋한다. 여러 사본 폴더(worktree·clone)에 흩어져 있던 중간 스냅샷을 정리하기 전에 원본을 git 이력으로 고정하는 것이 목적이다. - contracts/routes/services: measurement, outcome_trajectory, rupture_repair, deliberate_practice, calibration_transfer, supervision_research, multimodal_alliance, continuous_improvement 계열 신규 모듈과 테스트 - infra/db/init: 07~16 마이그레이션(측정 기반~calibration transfer 실행) - apps/web: 세션 리뷰 카드·관리 화면·E2E 스펙 추가 - docs/ops: G0~G8 라이브 통합·배포·롤백 증거 문서와 evidence JSON/PNG - scripts: smoke·ledger·릴리스 에이전트·NAS 프리뷰 운영 스크립트 engine.public 로그 .bak과 apps/web/test-results 산출물은 커밋에서 제외했다.
This commit is contained in:
parent
93dd8f82d7
commit
16e791e044
390 changed files with 243188 additions and 499 deletions
226
apps/api/app/test_supervision_research_store.py
Normal file
226
apps/api/app/test_supervision_research_store.py
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue