Files
NODEDC_MISSION_CORE/tests/test_observatory_setups.py
T

408 lines
15 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 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