"""Exercise the G5 self-prediction lock and reveal boundary over live HTTP/DB.""" from __future__ import annotations import argparse import asyncio import hashlib import json import os import secrets import time import urllib.error import urllib.request from dataclasses import dataclass from http.cookiejar import CookieJar from pathlib import Path from typing import Any from uuid import uuid4 COHORT_ID = "e2e-hanshin" INSTRUMENT_ID = "calibration-mirror-g5" INSTRUMENT_VERSION = "1.0.0" TRANSFER_INSTRUMENT_ID = "unseen-transfer-g5" API_ENV = Path(__file__).resolve().parents[1] / "apps" / "api" / ".env" class SmokeError(RuntimeError): pass @dataclass(frozen=True) class ApiResponse: status: int body: Any class ApiClient: def __init__(self, base_url: str, timeout: float) -> None: self.base_url = base_url.rstrip("/") self.timeout = timeout self._opener = urllib.request.build_opener( urllib.request.HTTPCookieProcessor(CookieJar()) ) def request( self, method: str, path: str, payload: dict[str, Any] | None = None, *, expected: set[int] | None = None, headers: dict[str, str] | None = None, ) -> ApiResponse: data = None request_headers = {"Accept": "application/json", **(headers or {})} if payload is not None: data = json.dumps(payload, ensure_ascii=False).encode("utf-8") request_headers["Content-Type"] = "application/json" request = urllib.request.Request( f"{self.base_url}{path}", data=data, headers=request_headers, method=method, ) try: with self._opener.open(request, timeout=self.timeout) as response: raw = response.read().decode("utf-8") result = ApiResponse(response.status, json.loads(raw) if raw else {}) except urllib.error.HTTPError as exc: raw = exc.read().decode("utf-8", errors="replace") try: body = json.loads(raw) if raw else {} except json.JSONDecodeError: body = {"detail": raw[:500]} result = ApiResponse(exc.code, body) except urllib.error.URLError as exc: raise SmokeError( f"{method} {path} transport failed: {type(exc.reason).__name__}" ) from exc if result.status not in (expected or {200}): detail = result.body.get("detail") if isinstance(result.body, dict) else None raise SmokeError( f"{method} {path} returned HTTP {result.status}; detail={detail!r}" ) return result def _sign_in( client: ApiClient, *, suffix: str, identity: str, role: str, cohort_ids: list[str], ) -> str: client.request( "POST", "/auth/dev-login", { "email": f"dev.e2e.calibration.{identity}.{suffix}@hs.ac.kr", "role": role, "display_name": f"Calibration {identity.title()}", "cohort_ids": cohort_ids, }, ) client.request( "POST", "/users/me/onboarding", { "legal_name": f"Calibration {identity.title()}", "affiliation": "한신대학교", "department": "상담심리학과", "grade_level": "통합검증", "phone": "010-0000-0000", "contact_address": "경기도 오산시 한신대학교", "nickname": f"Calibration {identity.title()}", "self_introduction": "G5 자기보정·전이 API 검증 fixture입니다.", "avatar_url": "", "terms_accepted": True, "privacy_accepted": True, }, ) me = client.request("GET", "/auth/me") user_id = str(me.body.get("user_id") or "") if not user_id: raise SmokeError(f"dev-login omitted user_id for {identity}") return user_id def _choose_distinct_personas(client: ApiClient) -> tuple[str, str]: response = client.request("GET", "/personas") usable = [ item for item in response.body if isinstance(item, dict) and item.get("source") == "database" and not item.get("degraded") and item.get("code") ] codes = list(dict.fromkeys(str(item["code"]) for item in usable)) if len(codes) < 2: raise SmokeError( "persona catalog requires two distinct non-degraded database personas" ) source = "P1" if "P1" in codes else codes[0] practice = next(code for code in codes if code != source) return source, practice def _load_api_env() -> None: if not API_ENV.exists(): return for raw_line in API_ENV.read_text(encoding="utf-8").splitlines(): line = raw_line.strip() if not line or line.startswith("#") or "=" not in line: continue key, value = line.split("=", 1) os.environ.setdefault(key.strip(), value.strip().strip('"').strip("'")) def _wait_for_session_review( client: ApiClient, session_id: str, *, timeout: float, interval: float, ) -> dict[str, Any]: if timeout <= 0 or interval <= 0: raise SmokeError("review poll timeout and interval must be positive") deadline = time.monotonic() + timeout poll_count = 0 while True: poll_count += 1 detail = client.request("GET", f"/sessions/{session_id}") if detail.body.get("review_ready") is True: review = client.request("GET", f"/sessions/{session_id}/review") if review.body.get("reviewReady") is not True: raise SmokeError( "session detail was review-ready but review payload was not ready" ) return {"poll_count": poll_count, "review": review.body} now = time.monotonic() if now >= deadline: raise SmokeError( f"session_end evaluator did not become ready within {timeout:.1f}s " f"for session {session_id}" ) time.sleep(min(interval, deadline - now)) def _durable_turn_ids(review: dict[str, Any]) -> list[str]: ids = [str(item["turn_id"]) for item in review.get("turns", []) if item.get("turn_id")] if len(ids) < 2: raise SmokeError("session review did not expose both durable turn UUIDs") return ids[:2] def _assert_no_aggregate_score(value: Any, path: str = "response") -> None: if isinstance(value, dict): forbidden = {"total", "total_score", "overall_score"} & set(value) if forbidden: raise SmokeError(f"{path} exposed aggregate score keys: {forbidden}") for key, child in value.items(): _assert_no_aggregate_score(child, f"{path}.{key}") elif isinstance(value, list): for index, child in enumerate(value): _assert_no_aggregate_score(child, f"{path}[{index}]") def _assert_no_raw_transcript(value: Any, path: str = "response") -> None: forbidden = { "raw_transcript", "transcript", "text_masked", "utterance_text", "counselor_text", "client_text", } if isinstance(value, dict): contaminated = forbidden & {str(key).lower() for key in value} if contaminated: raise SmokeError(f"{path} exposed raw-text keys: {contaminated}") for key, child in value.items(): _assert_no_raw_transcript(child, f"{path}.{key}") elif isinstance(value, list): for index, child in enumerate(value): _assert_no_raw_transcript(child, f"{path}[{index}]") def _actual_execution_request( *, original_transfer_trial_record_id: str, practice_session_id: str ) -> dict[str, str]: return { "original_transfer_trial_record_id": original_transfer_trial_record_id, "practice_session_id": practice_session_id, } def _build_transfer_suite( *, fixture_suffix: str, evidence_turn_ids: list[str] ) -> dict[str, Any]: if not evidence_turn_ids: raise SmokeError("transfer suite requires durable turn UUID evidence") return { "suite_id": f"oas-g5-suite-live-{fixture_suffix}", "training_phrase_family_ids": [f"training-empathy-{fixture_suffix}"], "trials": [ { "trial_id": f"oas-g5-transfer-live-{fixture_suffix}", "competency_id": "competency.empathic_attunement", "scenario_variant_id": f"unseen-live-{fixture_suffix}", "variation": { "context_variant": f"academic-transition-{fixture_suffix}", "relationship_style": "withdrawn", "difficulty_level": 3, "expression_variant": "indirect-emotion", "synthetic_subgroup": "synthetic-live-a", "scenario_family_id": "family-academic-transition", "phrase_family_id": f"novel-empathy-{fixture_suffix}", }, "status": "passed", "uncertainty": 0.2, "evidence_refs": evidence_turn_ids, "counterevidence": [], } ], } def _actual_execution_read_proof( read_model: dict[str, Any], *, execution_event_id: str, original_transfer_trial_record_id: str, practice_session_id: str, durable_turn_ids: list[str], ) -> dict[str, Any]: execution = next( ( item for item in read_model.get("actual_executions", []) if str(item.get("execution_event_id")) == execution_event_id ), None, ) if execution is None: raise SmokeError("learner read model omitted actual transfer execution") if ( str(execution.get("original_transfer_trial_record_id")) != original_transfer_trial_record_id or str(execution.get("practice_session_id")) != practice_session_id ): raise SmokeError("actual transfer read model changed its server ledger anchors") durable = set(durable_turn_ids) evidence = {str(item) for item in execution.get("evidence_turn_ids") or []} if not evidence or not evidence.issubset(durable): raise SmokeError("actual transfer evidence is not bound to durable practice turns") labels = execution.get("normalized_evaluator_labels") or {} required_labels = { "technique_codes", "client_state_codes", "appropriateness", "intent_deviation_dimensions", "evaluator_error_count", } if not isinstance(labels, dict) or set(labels) != required_labels: raise SmokeError("actual transfer omitted normalized evaluator labels") model_run_id = str(execution.get("model_run_id") or "") if not model_run_id: raise SmokeError("actual transfer omitted evaluator model-run provenance") if ( execution.get("source_kind") != "model_inferred" or execution.get("perspective") != "independent_observer" ): raise SmokeError("actual transfer omitted fixed source provenance") if ( execution.get("instrument_id") != "unseen-transfer-g5" or execution.get("instrument_version") != "1.0.0" or execution.get("observer_version") != "calibration-actual-transfer-observer-v1" ): raise SmokeError("actual transfer omitted fixed G0 instrument provenance") assessment = next( ( item for item in read_model.get("actual_transfer_assessments", []) if execution_event_id in {str(value) for value in item.get("source_execution_event_ids") or []} ), None, ) if assessment is None: raise SmokeError("learner read model omitted actual transfer assessment") if assessment.get("evidence_source") != "actual_practice_execution": raise SmokeError("actual assessment was mixed with synthetic suite evidence") if assessment.get("actual_transfer_status") != "insufficient_evidence": raise SmokeError("one actual execution must remain insufficient evidence") _assert_no_raw_transcript(execution, "actual_execution") _assert_no_raw_transcript(assessment, "actual_transfer_assessment") return { "execution_event_id": execution_event_id, "model_run_id": model_run_id, "source_kind": "model_inferred", "perspective": "independent_observer", "instrument_id": "unseen-transfer-g5", "instrument_version": "1.0.0", "observer_version": "calibration-actual-transfer-observer-v1", "durable_evidence_turn_ids": sorted(evidence), "actual_transfer_status": "insufficient_evidence", "evidence_source": "actual_practice_execution", } def _json_object(value: Any) -> dict[str, Any]: if isinstance(value, dict): return value if isinstance(value, str): parsed = json.loads(value) if isinstance(parsed, dict): return parsed raise SmokeError("Postgres JSON evidence was not an object") async def _create_transfer_suite_model_run( dsn: str, *, learner_id: str, source_session_id: str, evidence_turn_ids: list[str], ) -> str: import asyncpg model_run_id = str(uuid4()) input_payload = { "source": "calibration_transfer_http_smoke", "session_id": source_session_id, "evidence_turn_ids": evidence_turn_ids, } canonical = json.dumps(input_payload, separators=(",", ":"), sort_keys=True) input_hash = hashlib.sha256(canonical.encode("utf-8")).hexdigest() bundle_hash = hashlib.sha256( b"calibration-transfer-http-smoke-v1" ).hexdigest() conn = await asyncpg.connect(dsn) try: async with conn.transaction(): await conn.execute("SELECT set_config('app.ai_context', 'true', true)") await conn.execute( "SELECT set_config('app.current_ai_view', 'evaluator', true)" ) await conn.execute("SELECT set_config('app.current_role', 'admin', true)") await conn.execute( "SELECT set_config('app.current_uid', $1, true)", learner_id ) await conn.execute( "SELECT set_config('app.current_cohort', $1, true)", COHORT_ID ) await conn.execute( """ INSERT INTO audit.model_run ( model_run_id, session_id, turn_id, agent_role, provider, model, prompt_bundle_id, prompt_bundle_version, prompt_bundle_hash, structured_schema_version, input_evidence_hash, status, metadata ) VALUES ( $1::uuid,$2::uuid,$3::uuid,'evaluator','vignette-smoke', 'calibration-transfer-suite-fixture', 'calibration-transfer-http-smoke','1.0.0',$4, 'vignette.calibration-transfer-suite-smoke.v1',$5,'ready',$6::jsonb ) """, model_run_id, source_session_id, evidence_turn_ids[0], bundle_hash, input_hash, json.dumps(input_payload, ensure_ascii=False), ) finally: await conn.close() return model_run_id async def _fetch_actual_transfer_db_proof( dsn: str, *, learner_id: str, execution_event_id: str, original_transfer_trial_record_id: str, practice_session_id: str, durable_turn_ids: list[str], model_run_id: str, ) -> dict[str, Any]: import asyncpg conn = await asyncpg.connect(dsn) try: async with conn.transaction(): await conn.execute("SELECT set_config('app.ai_context', 'true', true)") await conn.execute( "SELECT set_config('app.current_ai_view', 'evaluator', true)" ) await conn.execute("SELECT set_config('app.current_role', 'admin', true)") await conn.execute( "SELECT set_config('app.current_uid', $1, true)", learner_id ) event = await conn.fetchrow( """ SELECT execution_event_id::text, original_transfer_trial_record_id::text, practice_session_id::text, learner_id::text, evidence_turn_ids, normalized_evaluator_labels, model_run_id::text, source_kind, perspective, instrument_id, instrument_version, observer_version FROM app.calibration_transfer_execution_event WHERE execution_event_id = $1::uuid """, execution_event_id, ) model_run = await conn.fetchrow( """ SELECT model_run_id::text, session_id::text, agent_role, provider, model, prompt_bundle_id, prompt_bundle_version, structured_schema_version, input_evidence_hash, status, metadata FROM audit.model_run WHERE model_run_id = $1::uuid """, model_run_id, ) columns = { str(row["column_name"]) for row in await conn.fetch( """ SELECT column_name FROM information_schema.columns WHERE table_schema = 'app' AND table_name = 'calibration_transfer_execution_event' """ ) } finally: await conn.close() if event is None or model_run is None: raise SmokeError("Postgres omitted actual transfer event or model run") if ( str(event["original_transfer_trial_record_id"]) != original_transfer_trial_record_id or str(event["practice_session_id"]) != practice_session_id or str(event["learner_id"]) != learner_id or str(event["model_run_id"]) != model_run_id or event["source_kind"] != "model_inferred" or event["perspective"] != "independent_observer" or event["instrument_id"] != "unseen-transfer-g5" or event["instrument_version"] != "1.0.0" or event["observer_version"] != "calibration-actual-transfer-observer-v1" ): raise SmokeError("Postgres actual transfer anchors or provenance differ") evidence = {str(item) for item in event["evidence_turn_ids"] or []} if not evidence or not evidence.issubset(set(durable_turn_ids)): raise SmokeError("Postgres actual evidence is not durable-turn bound") labels = _json_object(event["normalized_evaluator_labels"]) metadata = _json_object(model_run["metadata"]) _assert_no_raw_transcript(labels, "postgres.normalized_evaluator_labels") _assert_no_raw_transcript(metadata, "postgres.model_run.metadata") forbidden_columns = { "raw_transcript", "transcript", "text", "text_masked", "utterance_text", } if forbidden_columns & columns: raise SmokeError("actual transfer ledger contains a raw-text column") if ( str(model_run["session_id"]) != practice_session_id or model_run["agent_role"] != "evaluator" or model_run["provider"] != "vignette-runtime" or model_run["model"] != "calibration-actual-transfer-observer" or model_run["prompt_bundle_id"] != "calibration-actual-transfer-observer" or model_run["prompt_bundle_version"] != "calibration-actual-transfer-observer-v1" or model_run["structured_schema_version"] != "vignette.calibration-actual-transfer-execution.v1" or model_run["status"] != "ready" or len(str(model_run["input_evidence_hash"])) != 64 ): raise SmokeError("Postgres actual evaluator model-run provenance is incomplete") return { "durable_turn_ids_match_api": True, "model_run_id_matches_api": True, "model_run_schema": "vignette.calibration-actual-transfer-execution.v1", "instrument_id": "unseen-transfer-g5", "instrument_version": "1.0.0", "observer_version": "calibration-actual-transfer-observer-v1", "raw_text_columns": 0, "normalized_evaluator_labels_only": True, } def run(args: argparse.Namespace) -> dict[str, Any]: _load_api_env() dsn = args.database_url or os.environ.get("DATABASE_URL") if not dsn: raise SmokeError("DATABASE_URL is required via --database-url or apps/api/.env") if len(args.internal_token) < 32: raise SmokeError("--internal-token must contain at least 32 characters") health = ApiClient(args.api_base_url, args.request_timeout).request("GET", "/health") if not health.body.get("db") or not health.body.get("engine"): raise SmokeError("API health is not DB+engine ready") suffix = f"{int(time.time())}.{secrets.token_hex(3)}" learner = ApiClient(args.api_base_url, args.request_timeout) teacher = ApiClient(args.api_base_url, args.request_timeout) other_learner = ApiClient(args.api_base_url, args.request_timeout) other_teacher = ApiClient(args.api_base_url, args.request_timeout) learner_id = _sign_in( learner, suffix=suffix, identity="learner", role="learner", cohort_ids=[COHORT_ID], ) _sign_in( teacher, suffix=suffix, identity="teacher", role="teacher", cohort_ids=[COHORT_ID], ) _sign_in( other_learner, suffix=suffix, identity="other-learner", role="learner", cohort_ids=[COHORT_ID], ) _sign_in( other_teacher, suffix=suffix, identity="other-teacher", role="teacher", cohort_ids=["e2e-other-cohort"], ) source_persona, practice_persona = _choose_distinct_personas(learner) started = learner.request( "POST", "/sessions", { "persona_code": source_persona, "theory_mode": "humanistic", "goal_stages": ["라포", "탐색"], }, expected={201}, ) session_id = str(started.body["session_id"]) learner.request( "POST", f"/sessions/{session_id}/turn", {"text": "이번 장면에서 내가 감정을 정확히 반영할 확률을 먼저 예측해볼게요."}, ) learner.request("POST", f"/sessions/{session_id}/end") source_review = _wait_for_session_review( learner, session_id, timeout=args.review_poll_timeout, interval=args.review_poll_interval, ) turn_ids = _durable_turn_ids(source_review["review"]) history_id = str(uuid4()) revision_id = str(uuid4()) revision_submission_id = str(uuid4()) block_suffix = secrets.token_hex(5) revision = { "submission_id": revision_submission_id, "prediction_revision_id": revision_id, "history_id": history_id, "session_id": session_id, "competency_id": "competency.empathic_attunement", "practice_block_id": f"oas-g5-block-live-{block_suffix}", "scenario_variant_id": f"scenario-live-{block_suffix}", "phrase_family_id": f"phrase-live-{block_suffix}", "revision_no": 1, "supersedes_prediction_revision_id": None, "predicted_success_probability": 0.72, "confidence": 0.80, "recorded_sequence": 1, "revision_reason": "외부평가를 보기 전 장면 근거로 성공 가능성을 예측함", "instrument_id": INSTRUMENT_ID, "instrument_version": INSTRUMENT_VERSION, "evidence_turn_ids": turn_ids, } revision_path = "/calibration/predictions/revisions" created = learner.request("POST", revision_path, revision, expected={201}) retried = learner.request("POST", revision_path, revision, expected={201}) if created.body.get("prediction_revision_id") != retried.body.get( "prediction_revision_id" ) or retried.body.get("idempotent_replay") is not True: raise SmokeError("same prediction revision retry was not stable") learner.request( "POST", revision_path, dict(revision, predicted_success_probability=0.73), expected={409}, ) lock_submission_id = str(uuid4()) lock_id = str(uuid4()) lock = { "submission_id": lock_submission_id, "lock_id": lock_id, "prediction_revision_id": revision_id, "locked_sequence": 1, } lock_path = f"/calibration/predictions/{history_id}/lock" locked = learner.request("POST", lock_path, lock, expected={201}) locked_retry = learner.request("POST", lock_path, lock, expected={201}) if locked.body.get("lock_id") != locked_retry.body.get("lock_id") or locked_retry.body.get( "idempotent_replay" ) is not True: raise SmokeError("same prediction lock retry was not stable") post_lock_revision = dict( revision, submission_id=str(uuid4()), prediction_revision_id=str(uuid4()), revision_no=2, supersedes_prediction_revision_id=revision_id, predicted_success_probability=0.78, recorded_sequence=2, revision_reason="잠금 뒤 오염 시도를 검증함", ) learner.request("POST", revision_path, post_lock_revision, expected={422}) internal = ApiClient(args.api_base_url, args.request_timeout) observation_submission_id = str(uuid4()) observation_id = str(uuid4()) observation = { "submission_id": observation_submission_id, "observation_id": observation_id, "history_id": history_id, "status": "passed", "source_kind": "observed_runtime", "perspective": "runtime_observation", "model_run_id": None, "instrument_id": INSTRUMENT_ID, "instrument_version": INSTRUMENT_VERSION, "uncertainty": 0.18, "evidence_turn_ids": turn_ids, "counterevidence": ["single_scene_transfer_not_yet_verified"], "revealed_sequence": 2, } observation_path = "/internal/calibration/performance-observations" token_header = {"X-Vignette-Calibration-Transfer-Token": args.internal_token} observed = internal.request( "POST", observation_path, observation, expected={201}, headers=token_header, ) observed_retry = internal.request( "POST", observation_path, observation, expected={201}, headers=token_header, ) if observed.body.get("observation_id") != observed_retry.body.get( "observation_id" ) or observed_retry.body.get("idempotent_replay") is not True: raise SmokeError("same performance observation retry was not stable") internal.request( "POST", observation_path, dict(observation, uncertainty=0.19), expected={409}, headers=token_header, ) suite_model_run_id = asyncio.run( _create_transfer_suite_model_run( dsn, learner_id=learner_id, source_session_id=session_id, evidence_turn_ids=turn_ids, ) ) transfer_suite_record_id = str(uuid4()) transfer_suite = _build_transfer_suite( fixture_suffix=block_suffix, evidence_turn_ids=turn_ids, ) suite_submission = { "submission_id": str(uuid4()), "transfer_suite_record_id": transfer_suite_record_id, "suite": transfer_suite, "model_run_id": suite_model_run_id, "instrument_id": TRANSFER_INSTRUMENT_ID, "instrument_version": INSTRUMENT_VERSION, } suite_path = f"/internal/sessions/{session_id}/calibration/transfer-suites" suite_created = internal.request( "POST", suite_path, suite_submission, expected={201}, headers=token_header, ) if ( str(suite_created.body.get("transfer_suite_record_id")) != transfer_suite_record_id or suite_created.body.get("trial_count") != 1 ): raise SmokeError("internal transfer suite route omitted its authoritative trial") suite_read = learner.request("GET", "/calibration/learners/me") suite_projection = next( ( item for item in suite_read.body.get("transfer_suites") or [] if str(item.get("transfer_suite_record_id")) == transfer_suite_record_id ), None, ) if suite_projection is None or len(suite_projection.get("trials") or []) != 1: raise SmokeError("learner read model omitted authoritative transfer trial") original_transfer_trial_record_id = str( suite_projection["trials"][0]["transfer_trial_record_id"] ) actual_path = "/calibration/transfer-executions" learner.request( "POST", actual_path, _actual_execution_request( original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=session_id, ), expected={422}, ) pending_started = learner.request( "POST", "/sessions", { "persona_code": practice_persona, "theory_mode": "humanistic", "goal_stages": ["라포", "탐색"], }, expected={201}, ) pending_session_id = str(pending_started.body["session_id"]) learner.request( "POST", actual_path, _actual_execution_request( original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=pending_session_id, ), expected={422}, ) other_learner.request( "POST", actual_path, _actual_execution_request( original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=pending_session_id, ), expected={404}, ) practice_started = learner.request( "POST", "/sessions", { "persona_code": practice_persona, "theory_mode": "humanistic", "goal_stages": ["라포", "탐색"], }, expected={201}, ) practice_session_id = str(practice_started.body["session_id"]) learner.request( "POST", f"/sessions/{practice_session_id}/turn", { "text": ( "그 말을 꺼내기까지 많이 외롭고 조심스러웠던 것 같아요. " "제가 이해한 마음이 맞는지 함께 확인해도 괜찮을까요?" ) }, ) learner.request("POST", f"/sessions/{practice_session_id}/end") practice_review = _wait_for_session_review( learner, practice_session_id, timeout=args.review_poll_timeout, interval=args.review_poll_interval, ) practice_turn_ids = _durable_turn_ids(practice_review["review"]) actual_request = _actual_execution_request( original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=practice_session_id, ) actual_created = learner.request( "POST", actual_path, actual_request, expected={201} ) actual_retried = learner.request( "POST", actual_path, actual_request, expected={201} ) actual_execution = actual_created.body.get("execution") or {} actual_execution_event_id = str( actual_execution.get("execution_event_id") or "" ) actual_model_run_id = str(actual_execution.get("model_run_id") or "") if ( not actual_execution_event_id or not actual_model_run_id or actual_execution_event_id != str( (actual_retried.body.get("execution") or {}).get("execution_event_id") ) or actual_retried.body.get("idempotent_replay") is not True ): raise SmokeError("same actual transfer execution retry was not stable") _assert_no_raw_transcript(actual_created.body, "actual_execution_response") learner_read = learner.request("GET", "/calibration/learners/me") if learner_read.body.get("requested_view") != "learner": raise SmokeError("learner calibration read used the wrong role projection") histories = learner_read.body.get("prediction_histories") or [] target = next((item for item in histories if item.get("history_id") == history_id), None) if not target or not target.get("lock") or not target.get("external_observation"): raise SmokeError("locked prediction and external observation were not hydrated") actual_read_proof = _actual_execution_read_proof( learner_read.body, execution_event_id=actual_execution_event_id, original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=practice_session_id, durable_turn_ids=practice_turn_ids, ) actual_db_proof = asyncio.run( _fetch_actual_transfer_db_proof( dsn, learner_id=learner_id, execution_event_id=actual_execution_event_id, original_transfer_trial_record_id=original_transfer_trial_record_id, practice_session_id=practice_session_id, durable_turn_ids=practice_turn_ids, model_run_id=actual_model_run_id, ) ) teacher_read = teacher.request("GET", f"/calibration/learners/{learner_id}") if teacher_read.body.get("requested_view") != "supervisor": raise SmokeError("teacher calibration read used the wrong role projection") other_learner.request( "GET", f"/calibration/learners/{learner_id}", expected={403} ) other_teacher.request( "GET", f"/calibration/learners/{learner_id}", expected={404} ) _assert_no_aggregate_score(learner_read.body) _assert_no_aggregate_score(teacher_read.body) _assert_no_raw_transcript(learner_read.body, "learner_read") _assert_no_raw_transcript(teacher_read.body, "teacher_read") if learner_read.body.get("clinical_claim_allowed") is not False: raise SmokeError("calibration read omitted the non-clinical boundary") return { "ok": True, "api_base_url": args.api_base_url, "fixture_policy": "retained unique dev:e2e identities; no fixture deletion", "session_id": session_id, "history_id": history_id, "prediction_revision_id": revision_id, "prediction_lock_id": lock_id, "performance_observation_id": observation_id, "transfer_suite_record_id": transfer_suite_record_id, "original_transfer_trial_record_id": original_transfer_trial_record_id, "practice_session_id": practice_session_id, "actual_transfer_execution_event_id": actual_execution_event_id, "proof": { "durable_turn_uuid_count": len(turn_ids), "source_review_poll_count": source_review["poll_count"], "pre_reveal_prediction_recorded": True, "same_prediction_retry_stable": True, "changed_prediction_retry_rejected": True, "prediction_lock_idempotent": True, "post_lock_revision_rejected": True, "authenticated_external_observation": True, "same_observation_retry_stable": True, "changed_observation_retry_rejected": True, "authoritative_transfer_suite_created_by_internal_route": True, "same_source_session_rejected": True, "unended_practice_session_rejected": True, "other_learner_actual_execution_rejected": True, "different_persona_practice_session": practice_persona, "practice_review_poll_count": practice_review["poll_count"], "same_actual_execution_retry_stable": True, "actual_execution_read_model": actual_read_proof, "actual_execution_postgres": actual_db_proof, "no_raw_transcript_evidence": True, "learner_role_projection": "learner", "teacher_role_projection": "supervisor", "other_learner_rejected": True, "cross_cohort_teacher_rejected": True, "no_aggregate_score": True, "clinical_claim_allowed": False, }, } def main() -> None: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--api-base-url", default="http://127.0.0.1:8008") parser.add_argument("--internal-token", required=True) parser.add_argument("--database-url", default="") parser.add_argument("--request-timeout", type=float, default=180.0) parser.add_argument("--review-poll-timeout", type=float, default=180.0) parser.add_argument("--review-poll-interval", type=float, default=0.5) parser.add_argument("--out", default="") args = parser.parse_args() result = run(args) text = json.dumps(result, ensure_ascii=False, indent=2) if args.out: path = Path(args.out) path.parent.mkdir(parents=True, exist_ok=True) path.write_text(text + "\n", encoding="utf-8") print(text) if __name__ == "__main__": main()