Files
NODEDC_MISSION_CORE/tests/test_observatory_setups.py

489 lines
17 KiB
Python

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