489 lines
17 KiB
Python
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
|