vignette/apps/api/app/test_calibration_transfer_store.py
Yun Chan 16e791e044 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 산출물은 커밋에서 제외했다.
2026-08-08 01:30:53 +09:00

789 lines
32 KiB
Python

from __future__ import annotations
import unittest
from contextlib import asynccontextmanager
from datetime import UTC, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from unittest.mock import AsyncMock, patch
from uuid import UUID, uuid4
import asyncpg
from fastapi import FastAPI, HTTPException
from pydantic import SecretStr, ValidationError
from .contracts.calibration_transfer import TransferSuiteInput
from .deps import Principal, Role
from .routes import calibration_transfer
from .services import calibration_transfer_store
REPO_ROOT = Path(__file__).resolve().parents[3]
SQL = (REPO_ROOT / "infra" / "db" / "init" / "11_calibration_transfer.sql").read_text(
encoding="utf-8"
)
ACTUAL_SQL_PATH = (
REPO_ROOT / "infra" / "db" / "init" / "16_calibration_transfer_actual_execution.sql"
)
def _principal(role: Role = Role.LEARNER) -> Principal:
return Principal(
user_id=str(uuid4()),
role=role,
cohort_ids=["g5-cohort"],
)
class CalibrationTransferSchemaTests(unittest.TestCase):
def test_schema_owns_append_only_g5_ledgers(self) -> None:
tables = (
"calibration_prediction_history",
"calibration_prediction_revision",
"calibration_prediction_lock",
"calibration_performance_observation",
"calibration_assessment_snapshot",
"calibration_metacognitive_prescription",
"calibration_transfer_suite",
"calibration_transfer_trial",
"calibration_transfer_assessment",
"calibration_subgroup_drift_report",
"calibration_teacher_review_event",
)
for table in tables:
self.assertIn(f"CREATE TABLE IF NOT EXISTS app.{table}", SQL)
self.assertIn(f"'{table}'", SQL)
self.assertIn("audit.reject_measurement_mutation()", SQL)
self.assertNotIn("FOR UPDATE", SQL)
self.assertIn("'calibration-mirror-g5', '1.0.0'", SQL)
self.assertIn("'unseen-transfer-g5', '1.0.0'", SQL)
self.assertIn('"aggregate_total_forbidden":true', SQL)
def test_prediction_revision_is_blocked_after_lock_or_reveal(self) -> None:
self.assertIn(
"self-prediction cannot be revised after lock or external reveal",
SQL,
)
self.assertIn("prediction lock must target the latest revision", SQL)
self.assertIn("revealed_sequence <= lock_sequence", SQL)
self.assertIn("trg_calibration_prediction_revision_contract", SQL)
self.assertIn("trg_calibration_observation_reveal_contract", SQL)
def test_rls_is_learner_self_teacher_cohort_and_admin(self) -> None:
self.assertIn("learner_id = app.current_uid()", SQL)
self.assertIn(
"u.cohort = current_setting(''app.current_cohort'', true)", SQL
)
self.assertIn("app.current_role_name() = 'admin'", SQL)
self.assertIn("current_ai_view'', true) = ''evaluator''", SQL)
def test_schema_rejects_transcript_and_aggregate_score_payloads(self) -> None:
self.assertIn("raw_transcript", SQL)
self.assertIn("total_score", SQL)
self.assertIn(
"calibration payload cannot store transcript text or aggregate score",
SQL,
)
self.assertNotRegex(SQL.lower(), r"\btotal_score\s+(double|numeric|real|int)")
def test_actual_execution_migration_is_append_only_and_server_derived(self) -> None:
actual_sql = ACTUAL_SQL_PATH.read_text(encoding="utf-8")
self.assertIn(
"CREATE TABLE IF NOT EXISTS app.calibration_transfer_execution_event",
actual_sql,
)
self.assertIn("audit.reject_measurement_mutation()", actual_sql)
self.assertIn("trg_calibration_transfer_execution_contract", actual_sql)
self.assertIn("original_transfer_trial_record_id", actual_sql)
self.assertIn("practice_session_id", actual_sql)
self.assertIn("normalized_evaluator_labels", actual_sql)
self.assertIn("model_run_id", actual_sql)
self.assertIn("source_kind", actual_sql)
self.assertIn("perspective", actual_sql)
self.assertIn("instrument_id", actual_sql)
self.assertIn("instrument_version", actual_sql)
self.assertIn("unseen-transfer-g5", actual_sql)
self.assertIn("ADD COLUMN IF NOT EXISTS instrument_id", actual_sql)
self.assertIn("ADD COLUMN IF NOT EXISTS instrument_version", actual_sql)
self.assertIn("ALTER COLUMN instrument_id SET NOT NULL", actual_sql)
self.assertIn("ALTER COLUMN instrument_version SET NOT NULL", actual_sql)
self.assertIn("IF NOT EXISTS (", actual_sql)
self.assertIn("calibration_transfer_execution_instrument_fkey", actual_sql)
self.assertIn("calibration_transfer_execution_instrument_check", actual_sql)
self.assertIn("learner_id = app.current_uid()", actual_sql)
self.assertNotIn("raw_transcript", actual_sql)
self.assertNotIn("text_masked", actual_sql)
class CalibrationTransferOpenAPITests(unittest.TestCase):
@classmethod
def setUpClass(cls) -> None:
app = FastAPI()
app.include_router(calibration_transfer.router)
cls.schema = app.openapi()
def test_standalone_openapi_has_all_role_boundaries(self) -> None:
expected = {
"/calibration/predictions/revisions",
"/calibration/predictions/{history_id}/lock",
"/internal/calibration/performance-observations",
"/internal/sessions/{session_id}/calibration/assessments",
"/internal/sessions/{session_id}/calibration/transfer-suites",
"/calibration/reviews",
"/calibration/learners/me",
"/calibration/learners/{learner_id}",
"/calibration/transfer-executions",
}
self.assertTrue(expected.issubset(self.schema["paths"]))
def test_internal_paths_publish_dedicated_header(self) -> None:
operation = self.schema["paths"][
"/internal/calibration/performance-observations"
]["post"]
headers = {
item["name"]
for item in operation["parameters"]
if item["in"] == "header"
}
self.assertIn(
calibration_transfer.INTERNAL_TOKEN_HEADER,
headers,
)
def test_read_model_has_no_total_score_field(self) -> None:
schema = self.schema["components"]["schemas"][
"CalibrationTransferReadModelResponse"
]
properties = schema["properties"]
self.assertNotIn("total", properties)
self.assertNotIn("score", properties)
self.assertEqual(properties["clinical_claim_allowed"]["const"], False)
class CalibrationTransferRequestTests(unittest.TestCase):
def test_observation_requires_compatible_model_provenance(self) -> None:
with self.assertRaises(ValidationError):
calibration_transfer.PerformanceObservationRequest(
submission_id=uuid4(),
observation_id=uuid4(),
history_id=uuid4(),
status="passed",
source_kind="model_inferred",
perspective="runtime_observation",
instrument_id="g5-performance",
instrument_version="1.0.0",
uncertainty=0.2,
evidence_turn_ids=[uuid4()],
revealed_sequence=3,
)
def test_teacher_correction_payload_rejects_raw_transcript(self) -> None:
with self.assertRaises(ValidationError):
calibration_transfer.TeacherReviewRequest(
submission_id=uuid4(),
review_id=uuid4(),
target_kind="calibration_assessment",
target_id=uuid4(),
disposition="corrected",
correction_payload={"raw_transcript": "do not persist"},
review_reason="근거를 다시 검토했다.",
)
def test_non_correction_review_cannot_smuggle_payload(self) -> None:
with self.assertRaises(ValidationError):
calibration_transfer.TeacherReviewRequest(
submission_id=uuid4(),
review_id=uuid4(),
target_kind="drift_report",
target_id=uuid4(),
disposition="confirmed",
correction_payload={"status": "stable"},
review_reason="합성 subgroup 근거를 확인했다.",
)
def test_actual_execution_request_accepts_only_server_identifiers(self) -> None:
body = calibration_transfer.ActualTransferExecutionRequest(
original_transfer_trial_record_id=uuid4(),
practice_session_id=uuid4(),
)
self.assertEqual(
set(body.model_dump()),
{"original_transfer_trial_record_id", "practice_session_id"},
)
with self.assertRaises(ValidationError):
calibration_transfer.ActualTransferExecutionRequest(
original_transfer_trial_record_id=uuid4(),
practice_session_id=uuid4(),
context_variant="browser-forged-context",
)
class CalibrationTransferStoreTests(unittest.IsolatedAsyncioTestCase):
async def test_transfer_suite_route_preserves_typed_nested_suite(self) -> None:
session_id = uuid4()
body = calibration_transfer.TransferSuiteSubmissionRequest(
submission_id=uuid4(),
transfer_suite_record_id=uuid4(),
suite={
"suite_id": "oas-g5-suite-route-typed",
"training_phrase_family_ids": ["training-route-typed"],
"trials": [
{
"trial_id": "oas-g5-transfer-route-typed",
"competency_id": "competency.empathic_attunement",
"scenario_variant_id": "unseen-route-typed",
"variation": {
"context_variant": "academic-transition",
"relationship_style": "withdrawn",
"difficulty_level": 3,
"expression_variant": "indirect-emotion",
"synthetic_subgroup": "synthetic-route-typed",
"scenario_family_id": "family-academic-transition",
"phrase_family_id": "novel-route-typed",
},
"status": "passed",
"uncertainty": 0.2,
"evidence_refs": [str(uuid4())],
"counterevidence": [],
}
],
},
model_run_id=uuid4(),
instrument_id="unseen-transfer-g5",
instrument_version="1.0.0",
)
mocked = AsyncMock(
return_value={
"submission_id": body.submission_id,
"transfer_suite_record_id": body.transfer_suite_record_id,
"trial_count": 1,
"assessment_count": 1,
"drift_report_count": 1,
"idempotent_replay": False,
}
)
with patch.object(
calibration_transfer.calibration_transfer_store,
"append_transfer_suite",
mocked,
):
response = await calibration_transfer.create_transfer_suite(
session_id=session_id,
body=body,
conn=AsyncMock(),
)
self.assertEqual(response.trial_count, 1)
self.assertIs(mocked.await_args.kwargs["suite"], body.suite)
self.assertIsInstance(mocked.await_args.kwargs["suite"], TransferSuiteInput)
async def test_transfer_suite_passes_json_object_to_registered_codec(self) -> None:
learner_id = uuid4()
session_id = uuid4()
evidence_id = uuid4()
conn = AsyncMock()
conn.fetchrow.side_effect = [
{"id": session_id, "learner_id": learner_id, "case_id": uuid4()},
None,
]
suite = TransferSuiteInput.model_validate(
{
"suite_id": "oas-g5-suite-json-codec",
"training_phrase_family_ids": ["training-json-codec"],
"trials": [
{
"trial_id": "oas-g5-transfer-json-codec",
"competency_id": "competency.empathic_attunement",
"scenario_variant_id": "unseen-json-codec",
"variation": {
"context_variant": "academic-transition",
"relationship_style": "withdrawn",
"difficulty_level": 3,
"expression_variant": "indirect-emotion",
"synthetic_subgroup": "synthetic-json-codec",
"scenario_family_id": "family-academic-transition",
"phrase_family_id": "novel-json-codec",
},
"status": "passed",
"uncertainty": 0.2,
"evidence_refs": [str(evidence_id)],
"counterevidence": [],
}
],
}
)
result = await calibration_transfer_store.append_transfer_suite(
conn=conn,
submission_id=uuid4(),
transfer_suite_record_id=uuid4(),
session_id=session_id,
suite=suite,
model_run_id=uuid4(),
instrument_id="unseen-transfer-g5",
instrument_version="1.0.0",
)
assessment_insert = next(
call
for call in conn.execute.await_args_list
if "INSERT INTO app.calibration_transfer_assessment" in call.args[0]
)
self.assertIsInstance(assessment_insert.args[7], dict)
self.assertEqual(result["trial_count"], 1)
async def test_actual_execution_rejects_before_prediction_lock(self) -> None:
principal = _principal(Role.LEARNER)
now = datetime(2026, 8, 7, tzinfo=UTC)
conn = AsyncMock()
conn.fetchrow.return_value = {
"transfer_trial_record_id": uuid4(),
"transfer_suite_record_id": uuid4(),
"session_id": uuid4(),
"learner_id": UUID(principal.user_id),
"competency_id": "competency.empathic-check",
"created_at": now,
"prediction_locked": False,
}
@asynccontextmanager
async def fake_acquire(**kwargs):
self.assertTrue(kwargs["ai_context"])
self.assertEqual(kwargs["ai_view"], "evaluator")
self.assertEqual(kwargs["user_id"], principal.user_id)
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
with self.assertRaisesRegex(
calibration_transfer_store.CalibrationTransferStateError,
"self-prediction",
):
await calibration_transfer_store.append_actual_transfer_execution(
principal=principal,
original_transfer_trial_record_id=uuid4(),
practice_session_id=uuid4(),
)
self.assertEqual(conn.fetchrow.await_count, 1)
self.assertIn("prediction_lock", conn.fetchrow.await_args.args[0])
async def test_actual_execution_requires_ended_ready_later_session(self) -> None:
principal = _principal(Role.LEARNER)
learner_id = UUID(principal.user_id)
trial_id = uuid4()
original_session_id = uuid4()
practice_session_id = uuid4()
now = datetime(2026, 8, 7, tzinfo=UTC)
conn = AsyncMock()
conn.fetchrow.side_effect = [
{
"transfer_trial_record_id": trial_id,
"transfer_suite_record_id": uuid4(),
"session_id": original_session_id,
"learner_id": learner_id,
"competency_id": "competency.empathic-check",
"created_at": now,
"prediction_locked": True,
},
None,
{
"id": practice_session_id,
"learner_id": learner_id,
"started_at": now + timedelta(minutes=1),
"ended_at": None,
"evaluation_status": "ready",
"evaluation_scope": "session_end",
},
]
@asynccontextmanager
async def fake_acquire(**_kwargs):
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
with self.assertRaisesRegex(
calibration_transfer_store.CalibrationTransferStateError,
"must be ended",
):
await calibration_transfer_store.append_actual_transfer_execution(
principal=principal,
original_transfer_trial_record_id=trial_id,
practice_session_id=practice_session_id,
)
self.assertEqual(conn.fetch.await_count, 0)
async def test_actual_execution_persists_and_reads_g0_instrument_provenance(
self,
) -> None:
principal = _principal(Role.LEARNER)
learner_id = UUID(principal.user_id)
trial_id = uuid4()
suite_id = uuid4()
source_session_id = uuid4()
practice_session_id = uuid4()
counselor_turn_id = uuid4()
client_turn_id = uuid4()
now = datetime(2026, 8, 7, tzinfo=UTC)
original = {
"transfer_trial_record_id": trial_id,
"transfer_suite_record_id": suite_id,
"session_id": source_session_id,
"learner_id": learner_id,
"competency_id": "competency.empathic-check",
"scenario_variant_id": "actual-variant",
"scenario_novelty": "unseen_transfer",
"context_variant": "학업",
"relationship_style": "withdrawn",
"difficulty_level": 3,
"expression_variant": "우회형",
"synthetic_subgroup": "synthetic-a",
"scenario_family_id": "family-school",
"phrase_family_id": "actual-phrase",
"training_phrase_family_ids": ["training-phrase"],
"created_at": now,
"prediction_locked": True,
}
session = {
"id": practice_session_id,
"learner_id": learner_id,
"started_at": now + timedelta(minutes=1),
"ended_at": now + timedelta(minutes=10),
"evaluation_status": "ready",
"evaluation_scope": "session_end",
}
execution_row = {
"execution_event_id": uuid4(),
"original_transfer_trial_record_id": trial_id,
"transfer_suite_record_id": suite_id,
"practice_session_id": practice_session_id,
"learner_id": learner_id,
"competency_id": "competency.empathic-check",
"scenario_variant_id": "actual-variant",
"scenario_novelty": "unseen_transfer",
"context_variant": "학업",
"relationship_style": "withdrawn",
"difficulty_level": 3,
"expression_variant": "우회형",
"synthetic_subgroup": "synthetic-a",
"scenario_family_id": "family-school",
"phrase_family_id": "actual-phrase",
"training_phrase_collision": False,
"status": "passed",
"uncertainty": 0.25,
"evidence_turn_ids": [counselor_turn_id, client_turn_id],
"normalized_evaluator_labels": {
"technique_codes": ["reflection"],
"client_state_codes": ["affect_contact"],
"appropriateness": ["pos"],
"intent_deviation_dimensions": [],
"evaluator_error_count": 0,
},
"counterevidence": [],
"source_kind": "model_inferred",
"perspective": "independent_observer",
"model_run_id": uuid4(),
"instrument_id": "unseen-transfer-g5",
"instrument_version": "1.0.0",
"observer_version": "calibration-actual-transfer-observer-v1",
"created_at": now + timedelta(minutes=10),
}
conn = AsyncMock()
conn.fetchrow.side_effect = [original, None, session, execution_row]
conn.fetch.side_effect = [
[
{
"counselor_turn_id": counselor_turn_id,
"client_turn_id": client_turn_id,
"technique_codes": ["reflection"],
"client_state_codes": ["affect_contact"],
"appropriateness": "pos",
"intent_deviation_dimensions": [],
"evaluator_error": False,
}
],
[execution_row],
]
@asynccontextmanager
async def fake_acquire(**_kwargs):
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
result = await calibration_transfer_store.append_actual_transfer_execution(
principal=principal,
original_transfer_trial_record_id=trial_id,
practice_session_id=practice_session_id,
)
insert_call = conn.fetchrow.await_args_list[3]
self.assertIn("instrument_id", insert_call.args[0])
self.assertIn("instrument_version", insert_call.args[0])
self.assertIsInstance(insert_call.args[19], dict)
model_run_insert = next(
call
for call in conn.execute.await_args_list
if "INSERT INTO audit.model_run" in call.args[0]
)
self.assertIsInstance(model_run_insert.args[6], dict)
self.assertEqual(insert_call.args[22], "model_inferred")
self.assertEqual(insert_call.args[23], "independent_observer")
self.assertEqual(insert_call.args[24], "unseen-transfer-g5")
self.assertEqual(insert_call.args[25], "1.0.0")
self.assertEqual(
insert_call.args[26], "calibration-actual-transfer-observer-v1"
)
self.assertEqual(result["execution"]["source_kind"], "model_inferred")
self.assertEqual(
result["execution"]["perspective"], "independent_observer"
)
self.assertEqual(result["execution"]["instrument_id"], "unseen-transfer-g5")
self.assertEqual(result["execution"]["instrument_version"], "1.0.0")
self.assertEqual(
result["execution"]["observer_version"],
"calibration-actual-transfer-observer-v1",
)
async def test_idempotent_same_content_returns_existing_identifier(self) -> None:
identifier = uuid4()
conn = AsyncMock()
conn.fetchrow.return_value = {
"observation_id": identifier,
"content_hash": "a" * 64,
}
result = await calibration_transfer_store._existing_by_submission(
conn,
table="app.calibration_performance_observation",
submission_id=uuid4(),
content_hash="a" * 64,
)
self.assertEqual(result, identifier)
async def test_idempotent_changed_content_is_conflict(self) -> None:
conn = AsyncMock()
conn.fetchrow.return_value = {
"observation_id": uuid4(),
"content_hash": "a" * 64,
}
with self.assertRaises(
calibration_transfer_store.CalibrationTransferConflictError
):
await calibration_transfer_store._existing_by_submission(
conn,
table="app.calibration_performance_observation",
submission_id=uuid4(),
content_hash="b" * 64,
)
async def test_post_lock_revision_db_guard_maps_to_state_error(self) -> None:
principal = _principal(Role.LEARNER)
learner_id = UUID(principal.user_id)
session_id = uuid4()
history_id = uuid4()
prior_revision_id = uuid4()
conn = AsyncMock()
conn.fetchrow.side_effect = [
{"id": session_id, "learner_id": learner_id, "case_id": uuid4()},
None,
{
"history_id": history_id,
"session_id": session_id,
"learner_id": learner_id,
"competency_id": "competency.empathic-check",
"practice_block_id": "oas-g5-block-one",
"scenario_variant_id": "variant-1",
"phrase_family_id": "phrase-1",
},
asyncpg.ObjectNotInPrerequisiteStateError(
"self-prediction cannot be revised after lock or external reveal"
),
]
@asynccontextmanager
async def fake_acquire(**_kwargs):
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
with self.assertRaises(
calibration_transfer_store.CalibrationTransferStateError
):
await calibration_transfer_store.append_prediction_revision(
principal=principal,
submission_id=uuid4(),
prediction_revision_id=uuid4(),
history_id=history_id,
session_id=session_id,
competency_id="competency.empathic-check",
practice_block_id="oas-g5-block-one",
scenario_variant_id="variant-1",
phrase_family_id="phrase-1",
revision_no=2,
supersedes_prediction_revision_id=prior_revision_id,
predicted_success_probability=0.8,
confidence=0.8,
recorded_sequence=2,
revision_reason="잠금 뒤 수정 차단",
instrument_id="calibration-mirror-g5",
instrument_version="1.0.0",
evidence_turn_ids=(),
)
async def test_evidence_refs_are_uuid_only(self) -> None:
self.assertEqual(
calibration_transfer_store._uuid_evidence(
["30000000-0000-4000-8000-000000000001"], required=True
),
(UUID("30000000-0000-4000-8000-000000000001"),),
)
with self.assertRaises(
calibration_transfer_store.CalibrationTransferStateError
):
calibration_transfer_store._uuid_evidence(
["상담 축어록 본문"], required=True
)
async def test_learner_cannot_append_teacher_review(self) -> None:
with self.assertRaisesRegex(
calibration_transfer_store.CalibrationTransferStateError,
"teacher or admin",
):
await calibration_transfer_store.append_teacher_review(
principal=_principal(Role.LEARNER),
submission_id=uuid4(),
review_id=uuid4(),
target_kind="calibration_assessment",
target_id=uuid4(),
disposition="confirmed",
correction_payload={},
review_reason="확인",
evidence_turn_ids=(),
counterevidence=(),
)
async def test_model_observation_is_appended_with_lock_provenance(self) -> None:
session_id = uuid4()
learner_id = uuid4()
lock_id = uuid4()
history_id = uuid4()
observation_id = uuid4()
conn = AsyncMock()
conn.fetchrow.side_effect = [
{
"history_id": history_id,
"session_id": session_id,
"learner_id": learner_id,
"competency_id": "competency.empathic-check",
"practice_block_id": "oas-g5-block-one",
"scenario_variant_id": "variant-1",
"phrase_family_id": "phrase-1",
"lock_id": lock_id,
},
None,
{"observation_id": observation_id},
]
result = await calibration_transfer_store.append_performance_observation(
conn=conn,
submission_id=uuid4(),
observation_id=observation_id,
history_id=history_id,
status="passed",
source_kind="model_inferred",
perspective="independent_observer",
model_run_id=uuid4(),
instrument_id="g5-performance",
instrument_version="1.0.0",
uncertainty=0.2,
evidence_turn_ids=(uuid4(),),
counterevidence=(),
revealed_sequence=3,
)
self.assertEqual(result["observation_id"], observation_id)
insert = conn.fetchrow.await_args_list[-1]
self.assertIn("prediction_lock_id", insert.args[0])
self.assertEqual(insert.args[5], lock_id)
async def test_empty_read_is_role_safe_for_learner(self) -> None:
principal = _principal(Role.LEARNER)
conn = AsyncMock()
conn.fetch.side_effect = [[], [], [], [], [], [], []]
@asynccontextmanager
async def fake_acquire(**kwargs):
self.assertEqual(kwargs["role"], "learner")
self.assertEqual(kwargs["user_id"], principal.user_id)
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
result = await calibration_transfer_store.read_calibration_transfer(
principal=principal
)
self.assertEqual(result["requested_view"], "learner")
self.assertEqual(result["prediction_histories"], [])
self.assertEqual(result["transfer_suites"], [])
self.assertFalse(result["clinical_claim_allowed"])
class CalibrationTransferInternalAuthenticationTests(
unittest.IsolatedAsyncioTestCase
):
TOKEN = "g5-calibration-transfer-token-at-least-32-characters"
async def _assert_rejected_before_db(
self, configured: str, presented: str | None, expected_status: int
) -> None:
reached = False
async def fake_provider():
nonlocal reached
reached = True
yield AsyncMock()
settings = SimpleNamespace(
calibration_transfer_internal_token=SecretStr(configured)
)
with patch.object(
calibration_transfer, "_evaluator_db_provider", fake_provider
):
dependency = (
calibration_transfer.calibration_transfer_internal_evaluator_db(
settings=settings,
presented_token=presented
)
)
with self.assertRaises(HTTPException) as captured:
await anext(dependency)
self.assertEqual(captured.exception.status_code, expected_status)
self.assertFalse(reached)
async def test_unconfigured_token_fails_closed(self) -> None:
await self._assert_rejected_before_db("", None, 503)
async def test_missing_token_is_401(self) -> None:
await self._assert_rejected_before_db(self.TOKEN, None, 401)
async def test_wrong_token_is_403(self) -> None:
await self._assert_rejected_before_db(self.TOKEN, "wrong-token", 403)
async def test_valid_token_acquires_evaluator_connection(self) -> None:
conn = AsyncMock()
async def fake_provider():
yield conn
settings = SimpleNamespace(
calibration_transfer_internal_token=SecretStr(self.TOKEN)
)
with patch.object(
calibration_transfer, "_evaluator_db_provider", fake_provider
):
dependency = (
calibration_transfer.calibration_transfer_internal_evaluator_db(
settings=settings,
presented_token=self.TOKEN
)
)
self.assertIs(await anext(dependency), conn)
await dependency.aclose()
if __name__ == "__main__":
unittest.main()