from __future__ import annotations import json from dataclasses import replace from pathlib import Path from types import SimpleNamespace import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from k1link.observatory import ( OBSERVATORY_CALCULATION_PROFILE_SCHEMA, LaboratorySetupRegistry, LaboratorySetupRegistryError, ) from k1link.sessions import LabReplayCapability, LabSessionBinding, SessionNotFoundError from k1link.sessions.models import SessionSummary from k1link.web.observatory_api import build_observatory_router from k1link.web.session_api import build_session_router REPOSITORY_ROOT = Path(__file__).resolve().parents[1] REGISTRY_PATH = REPOSITORY_ROOT / "config" / "observatory-laboratory-setups.json" RAV00_SESSION_ID = "20260720T065719Z_viewer_live" RAV004_SESSION_ID = "20260828T130511Z_viewer_live" RAV004_RESULT_ID = ( "lab-v1-vegetation-shadow-" "8c8f387599955dd79a16ded2c2d7cfd7f9f308d704f52c42fc26bd39d6da2d60" ) def _registry() -> LaboratorySetupRegistry: return LaboratorySetupRegistry.from_file( REGISTRY_PATH, repository_root=REPOSITORY_ROOT, ) def _source(session_id: str, label: str) -> SessionSummary: return SessionSummary( session_id=session_id, display_name=label, status="ready", started_at_utc="2026-08-28T13:05:16Z", completed_at_utc="2026-08-28T13:18:45Z", duration_seconds=808.0, modalities=("point-cloud", "trajectory", "video"), source_count=3, total_bytes=1, replayable=True, origin="recorded", ) def _rav004_projection() -> SessionSummary: evidence_identity = RAV004_RESULT_ID.removeprefix("lab-v1-vegetation-shadow-") capability = LabReplayCapability( schema_version="missioncore.observation-lab-replay-capability/v1", kind="canonical-recorded-rerun", viewer_profile="recorded-session", timeline="session_time", activation="explicit", commands_enabled=False, ) binding = LabSessionBinding( session_id=RAV004_RESULT_ID, source_session_id=RAV004_SESSION_ID, lab_id="LAB V1", result_kind="recorded-perception-qualification", result_id=RAV004_RESULT_ID, source_result_id=( "lab-v1-vegetation-shadow-" "d179462134967ace1c5ebd6fbdbdd8659905d390484b9c01ea7930f083bb74d1" ), config_sha256=None, run_created_at_utc="2026-08-29T18:05:11.329061Z", published_at_utc="2026-08-30T15:16:27.747Z", replay_capability=capability, provenance={ "schema_version": "missioncore.canonical-recorded-lab-projection/v1", "evidence_identity_sha256": evidence_identity, "result_document_sha256": ( "598a3d859fffcc7426e4a08b7ab4b59cbf09cad634ac0b618ff5845254e82b64" ), "replay_capability": capability.as_dict(), "authority": { "commands_enabled": False, "navigation_or_safety_accepted": False, "actuation_accepted": False, }, "method": { "schema_version": "missioncore.laboratory-method/v1", "completeness": "legacy-partial", "execution_class": "ai-inference", "pipeline_id": ( "ravnoves004tree-full-eomt-ddrnet-recorded-review/v1" ), "components": [ { "kind": "source", "name": "sealed full-route LAB result", "version": "missioncore.lab-v1-vegetation-shadow/v1", "role": "immutable Session catalog projection", "identity_sha256": evidence_identity, } ], }, }, ) return SessionSummary( session_id=RAV004_RESULT_ID, display_name="RAVNOVES004TREE · полный маршрут восприятия", status="ready", started_at_utc=binding.run_created_at_utc, completed_at_utc=binding.run_created_at_utc, duration_seconds=718.0, modalities=(), source_count=0, total_bytes=0, replayable=True, origin="missioncore.lab-instance/v1", lab=binding, ) def test_repository_setup_registry_keeps_real_definition_and_pre_definition_result() -> None: registry = _registry() assert [setup.setup_id for setup in registry.setups] == [ "m49-tgs-full-shadow-v1", "lab-v1-ravnoves004tree-final", ] rav00 = registry.catalog(_source(RAV00_SESSION_ID, "RAVNOVES00")) m49, current = rav00["setups"] assert m49["display_name"] == "M4.9T5 · TRAVEL TGS · CPU-only, без ML" assert m49["origin"] == "archived-definition" assert m49["compatibility"]["compatible"] is True assert m49["preflight"] == { "outcome": "blocked", "action": "open-legacy", "reason": "Точный результат сохранён в legacy LAB.", "submission_allowed": False, "existing_result_ids": [], } assert m49["run_definition"]["definition_sha256"] assert m49["run_definition"]["configuration"] == [ { "role": "primary-profile", "sha256": ( "c2e07010aaee78259d36c057962d6bfb885349251ff7356d867e5813e632881c" ), } ] assert all("path" not in row for row in m49["run_definition"]["configuration"]) assert current["compatibility"]["compatible"] is False rav004 = registry.catalog( _source(RAV004_SESSION_ID, "RAVNOVES004TREE"), available_observatory_result_ids=frozenset({RAV004_RESULT_ID}), ) existing = rav004["setups"][1] assert existing["display_name"] == "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39" assert "полный TGS и независимый YOLOX отсутствуют" in existing["description"] assert existing["origin"] == "existing-result" assert existing["run_definition"] is None assert existing["preflight"]["outcome"] == "existing" assert existing["preflight"]["action"] == "open-existing" assert existing["preflight"]["submission_allowed"] is False def test_registry_attributes_only_the_exact_admitted_preserved_result() -> None: registry = _registry() projection = _rav004_projection() assert registry.observatory_calculation_profile(projection) == { "schema_version": OBSERVATORY_CALCULATION_PROFILE_SCHEMA, "setup_id": "lab-v1-ravnoves004tree-final", "display_name": "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39", "origin": "existing-result", "definition_id": None, "definition_version": None, "definition_sha256": None, } assert registry.observatory_calculation_profile( replace( projection, session_id="lab-v1-vegetation-shadow-" + "0" * 64, ) ) is None assert projection.lab is not None assert registry.observatory_calculation_profile( replace( projection, lab=replace(projection.lab, provenance={}), ) ) is None @pytest.mark.parametrize( ("source", "reason_code"), [ (replace(_source(RAV00_SESSION_ID, "RAVNOVES00"), status="failed"), "source-not-ready"), ( replace(_source(RAV00_SESSION_ID, "RAVNOVES00"), replayable=False), "source-not-replayable", ), ], ) def test_setup_compatibility_requires_a_ready_replayable_source( source: SessionSummary, reason_code: str, ) -> None: setup = _registry().catalog(source)["setups"][0] assert setup["compatibility"]["compatible"] is False assert reason_code in { reason["code"] for reason in setup["compatibility"]["reasons"] } assert setup["preflight"]["outcome"] == "blocked" assert setup["preflight"]["submission_allowed"] is False def test_registry_fails_closed_when_referenced_configuration_digest_changes( tmp_path: Path, ) -> None: document = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) document["setups"][0]["run_definition"]["configuration"][0]["sha256"] = "0" * 64 path = tmp_path / "setups.json" path.write_text(json.dumps(document), encoding="utf-8") with pytest.raises(LaboratorySetupRegistryError, match="digest changed"): LaboratorySetupRegistry.from_file(path, repository_root=REPOSITORY_ROOT) @pytest.mark.parametrize( ("field_path", "invalid"), [ (("setups", 0, "origin"), []), (("setups", 0, "preserved_results", 0, "access"), {}), ], ) def test_registry_normalizes_untrusted_json_types_to_registry_error( tmp_path: Path, field_path: tuple[str | int, ...], invalid: object, ) -> None: document: object = json.loads(REGISTRY_PATH.read_text(encoding="utf-8")) target = document for part in field_path[:-1]: target = target[part] # type: ignore[index] target[field_path[-1]] = invalid # type: ignore[index] path = tmp_path / "setups.json" path.write_text(json.dumps(document), encoding="utf-8") with pytest.raises(LaboratorySetupRegistryError): LaboratorySetupRegistry.from_file(path, repository_root=REPOSITORY_ROOT) class _Store: def __init__(self) -> None: self.sessions = { RAV00_SESSION_ID: _source(RAV00_SESSION_ID, "RAVNOVES00"), RAV004_SESSION_ID: _source(RAV004_SESSION_ID, "RAVNOVES004TREE"), RAV004_RESULT_ID: _rav004_projection(), } def get_session(self, session_id: str): try: summary = self.sessions[session_id] except KeyError as exc: raise SessionNotFoundError(session_id) from exc return SimpleNamespace(summary=summary) def list_recent( self, *, limit: int, cursor: str | None, scope: str, include_capability_projections: bool = False, ): del limit, cursor items = ( (_rav004_projection(),) if scope == "laboratory" and include_capability_projections else () ) return SimpleNamespace(items=items, next_cursor=None) def test_session_catalog_v3_projects_the_exact_preserved_profile() -> None: registry = _registry() app = FastAPI() app.include_router( build_session_router( _Store(), # type: ignore[arg-type] lab_calculation_profile_resolver=registry.observatory_calculation_profile, ) ) client = TestClient(app) v2_lab = client.get( "/api/v1/observation-sessions", params={"scope": "laboratory", "lab_contract": "v2"}, ).json()["items"][0]["lab"] v3_lab = client.get( "/api/v1/observation-sessions", params={"scope": "laboratory", "lab_contract": "v3"}, ).json()["items"][0]["lab"] assert "calculation_profile" not in v2_lab assert v3_lab["calculation_profile"] == { "schema_version": OBSERVATORY_CALCULATION_PROFILE_SCHEMA, "setup_id": "lab-v1-ravnoves004tree-final", "display_name": "LAB V1 · EoMT Cityscapes Large 1024 + DDRNet-39", "origin": "existing-result", "definition_id": None, "definition_version": None, "definition_sha256": None, } def test_observatory_setup_catalog_and_preflight_are_read_only_and_fail_closed() -> None: app = FastAPI() app.include_router(build_observatory_router(_Store(), setup_registry=_registry())) # type: ignore[arg-type] client = TestClient(app) catalog = client.get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV004_SESSION_ID}, ) assert catalog.status_code == 200 existing = catalog.json()["setups"][1] assert existing["preflight"]["outcome"] == "existing" assert existing["run_definition"] is None preflight = client.post( "/api/v1/observatory/run-preflights", json={ "schema_version": "missioncore.observatory-run-preflight-request/v1", "source_session_id": RAV004_SESSION_ID, "setup_id": existing["setup_id"], "definition_sha256": None, }, ) assert preflight.status_code == 200 assert preflight.json()["outcome"] == "existing" assert preflight.json()["submission_allowed"] is False assert preflight.json()["existing_result_ids"] == [RAV004_RESULT_ID] assert next( check for check in preflight.json()["checks"] if check["check_id"] == "executor" )["outcome"] == "not-applicable" m49_catalog = client.get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV00_SESSION_ID}, ).json() m49 = m49_catalog["setups"][0] blocked = client.post( "/api/v1/observatory/run-preflights", json={ "schema_version": "missioncore.observatory-run-preflight-request/v1", "source_session_id": RAV00_SESSION_ID, "setup_id": m49["setup_id"], "definition_sha256": m49["run_definition"]["definition_sha256"], }, ) assert blocked.status_code == 200 assert blocked.json()["outcome"] == "blocked" assert blocked.json()["submission_allowed"] is False assert blocked.json()["executor"]["state"] == "not-installed" stale = client.post( "/api/v1/observatory/run-preflights", json={ "schema_version": "missioncore.observatory-run-preflight-request/v1", "source_session_id": RAV00_SESSION_ID, "setup_id": m49["setup_id"], "definition_sha256": "f" * 64, }, ) assert stale.status_code == 409 assert client.post("/api/v1/observatory/runs", json={}).status_code == 404 def test_existing_result_is_not_openable_when_its_sealed_contract_drifts() -> None: store = _Store() projection = store.sessions[RAV004_RESULT_ID] assert projection.lab is not None store.sessions[RAV004_RESULT_ID] = replace( projection, lab=replace(projection.lab, result_kind="unexpected-result-kind"), ) app = FastAPI() app.include_router(build_observatory_router(store, setup_registry=_registry())) # type: ignore[arg-type] response = TestClient(app).get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV004_SESSION_ID}, ) assert response.status_code == 200 current = response.json()["setups"][1] assert current["preflight"]["outcome"] == "blocked" assert current["preflight"]["action"] == "blocked" assert current["preflight"]["existing_result_ids"] == [] def test_existing_result_is_not_openable_without_canonical_provenance() -> None: store = _Store() projection = store.sessions[RAV004_RESULT_ID] assert projection.lab is not None store.sessions[RAV004_RESULT_ID] = replace( projection, lab=replace(projection.lab, provenance={}), ) app = FastAPI() app.include_router(build_observatory_router(store, setup_registry=_registry())) # type: ignore[arg-type] response = TestClient(app).get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV004_SESSION_ID}, ) assert response.status_code == 200 assert response.json()["setups"][1]["preflight"]["outcome"] == "blocked" def test_existing_result_is_not_openable_outside_capability_owned_origin() -> None: store = _Store() store.sessions[RAV004_RESULT_ID] = replace( store.sessions[RAV004_RESULT_ID], origin="laboratory", ) app = FastAPI() app.include_router(build_observatory_router(store, setup_registry=_registry())) # type: ignore[arg-type] response = TestClient(app).get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV004_SESSION_ID}, ) assert response.status_code == 200 assert response.json()["setups"][1]["preflight"]["outcome"] == "blocked" def test_setup_routes_reject_unsafe_identifiers_and_isolate_registry_failure() -> None: normal = FastAPI() normal.include_router(build_observatory_router(_Store(), setup_registry=_registry())) # type: ignore[arg-type] normal_client = TestClient(normal, raise_server_exceptions=False) assert normal_client.get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": "../bad"}, ).status_code == 422 assert normal_client.post( "/api/v1/observatory/run-preflights", json={ "schema_version": "missioncore.observatory-run-preflight-request/v1", "source_session_id": "../bad", "setup_id": "m49-tgs-full-shadow-v1", "definition_sha256": None, }, ).status_code == 422 unavailable = FastAPI() unavailable.include_router( build_observatory_router( _Store(), # type: ignore[arg-type] setup_registry_error="configuration digest changed", ) ) unavailable_client = TestClient(unavailable) assert unavailable_client.get( "/api/v1/observatory/laboratory-setups", params={"source_session_id": RAV004_SESSION_ID}, ).status_code == 503 assert unavailable_client.post( "/api/v1/observatory/run-preflights", json={ "schema_version": "missioncore.observatory-run-preflight-request/v1", "source_session_id": RAV004_SESSION_ID, "setup_id": "lab-v1-ravnoves004tree-final", "definition_sha256": None, }, ).status_code == 503