vignette/apps/api/app/test_calibration_transfer_store.py
Yun Chan a0311c5957
Some checks failed
API contract / OpenAPI type drift (push) Failing after 3m27s
회기 무발화 0턴 분리, 자기예측 락 불변식 및 TDD 회귀 검증 완료
2026-09-08 23:28:06 +09:00

872 lines
35 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_append_prediction_revision_normalizes_legacy_instrument_id(self) -> None:
principal = _principal(Role.LEARNER)
learner_id = UUID(principal.user_id)
session_id = uuid4()
history_id = uuid4()
revision_id = uuid4()
conn = AsyncMock()
conn.fetchrow.side_effect = [
{"id": session_id, "learner_id": learner_id, "case_id": uuid4()},
None,
None,
{"prediction_revision_id": str(revision_id), "revision_no": 1},
]
@asynccontextmanager
async def fake_acquire(**_kwargs):
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
res = await calibration_transfer_store.append_prediction_revision(
principal=principal,
submission_id=uuid4(),
prediction_revision_id=revision_id,
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=1,
supersedes_prediction_revision_id=None,
predicted_success_probability=0.7,
confidence=0.8,
recorded_sequence=1,
revision_reason="호환성 검증",
instrument_id="vignette.calibration-self-prediction",
instrument_version="1.0.0",
evidence_turn_ids=(),
)
self.assertEqual(res["revision_no"], 1)
# Verify normalized instrument_id "calibration-mirror-g5" was passed to DB
call_args = conn.fetchrow.call_args_list[-1][0]
self.assertIn("calibration-mirror-g5", call_args)
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"])
self.assertTrue(result["_learner_feedback_snapshot_enabled"])
async def test_read_reports_disabled_historical_session_snapshot(self) -> None:
principal = _principal(Role.LEARNER)
history_id = uuid4()
conn = AsyncMock()
conn.fetch.side_effect = [
[
{
"history_id": history_id,
"session_id": uuid4(),
"source_learner_feedback_enabled": False,
}
],
[],
[],
[],
[],
[],
[],
[],
]
@asynccontextmanager
async def fake_acquire(**_kwargs):
yield conn
with patch.object(calibration_transfer_store.db, "acquire", fake_acquire):
result = await calibration_transfer_store.read_calibration_transfer(
principal=principal
)
self.assertFalse(result["_learner_feedback_snapshot_enabled"])
self.assertNotIn(
"source_learner_feedback_enabled",
result["prediction_histories"][0],
)
history_query = conn.fetch.await_args_list[0].args[0]
self.assertIn("source_session.learner_feedback_enabled", history_query)
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()