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 LaboratorySetupRegistry, LaboratorySetupRegistryError from k1link.sessions import LabReplayCapability, LabSessionBinding, SessionNotFoundError from k1link.sessions.models import SessionSummary from k1link.web.observatory_api import build_observatory_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 @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 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