Files
NODEDC_MISSION_CORE/tests/test_viewer_diagnostics_api.py
T
DCCONSTRUCTIONS 0ca7316a24 wip(k1): checkpoint connection recovery rewrite
Capture the current unreleased K1 connection, recovery, lifecycle, viewer, and test work as a single known-bad baseline for subsequent fixes.
2026-08-14 14:57:50 +03:00

260 lines
10 KiB
Python

from __future__ import annotations
import json
import logging
import stat
from collections.abc import Callable
from pathlib import Path
from typing import Any
import pytest
from fastapi import APIRouter
from fastapi.routing import APIRoute
from pydantic import ValidationError
from k1link.web.runtime_diagnostics import (
SCANNER_LOGGER_NAME,
configure_scanner_diagnostics,
)
from k1link.web.viewer_diagnostics_api import (
LiveViewerDiagnosticEvent,
build_viewer_diagnostics_router,
)
def _endpoint(router: APIRouter, path: str, method: str) -> Callable[..., Any]:
for route in router.routes:
if (
isinstance(route, APIRoute)
and route.path == path
and route.methods is not None
and method in route.methods
):
return route.endpoint
raise AssertionError(f"{method} {path} route is missing")
def test_private_scanner_diagnostics_are_durable_structured_and_bounded(
tmp_path: Path,
) -> None:
target = configure_scanner_diagnostics(tmp_path / "logs")
logger = logging.getLogger(f"{SCANNER_LOGGER_NAME}.test")
logger.error(
"field control failure",
extra={
"event_code": "k1_application_control_session_failed",
"operation_id": "operation-test-01",
"operation_stage": "ble-provisioning-write",
"connection_mode": "bridge",
"error_category": "device",
"error_code": "BleakGATTProtocolError",
"reason_code": "mqtt_network_loop_failed",
"mqtt_loop_result_code": 7,
"mqtt_loop_result_name": "The connection was lost.",
"mqtt_loop_phase": "post-publish-drain",
"automatic_retry": False,
"side_effect_status": "unknown",
"network_change_attempted": True,
"device_write_attempted": True,
"device_write_confirmed": False,
"ble_att_error_code": 4,
"ble_att_error_name": "INVALID_PDU",
"helper_stage": "compile",
"helper_elapsed_ms": 34720,
"camera_source_id": "sensor.camera.right",
"evidence_session_id": "20260728T163450Z_viewer_live",
"activation_trigger": "application-control-scanning",
"network_change_admissible": True,
"network_change_reconciliation": (
"explicit-network-change-only-after-acknowledged-stop"
),
"lease_generation": 3,
"lease_state": "reachable",
"recovery_strategy": "existing-mqtt-endpoint",
"endpoint_reachable": True,
"address_changed": False,
"device_write_performed": False,
"preferred_port": 9876,
"selected_port": 9877,
"unapproved_secret_field": "must-not-be-written",
},
)
for handler in logging.getLogger(SCANNER_LOGGER_NAME).handlers:
handler.flush()
document = json.loads(target.read_text(encoding="utf-8").splitlines()[-1])
assert stat.S_IMODE(target.parent.stat().st_mode) == 0o700
assert stat.S_IMODE(target.stat().st_mode) == 0o600
assert document["event_code"] == "k1_application_control_session_failed"
assert document["operation_id"] == "operation-test-01"
assert document["operation_stage"] == "ble-provisioning-write"
assert document["connection_mode"] == "bridge"
assert document["error_category"] == "device"
assert document["error_code"] == "BleakGATTProtocolError"
assert document["reason_code"] == "mqtt_network_loop_failed"
assert document["mqtt_loop_result_code"] == 7
assert document["mqtt_loop_phase"] == "post-publish-drain"
assert document["automatic_retry"] is False
assert document["side_effect_status"] == "unknown"
assert document["network_change_attempted"] is True
assert document["device_write_attempted"] is True
assert document["device_write_confirmed"] is False
assert document["ble_att_error_code"] == 4
assert document["ble_att_error_name"] == "INVALID_PDU"
assert document["helper_stage"] == "compile"
assert document["helper_elapsed_ms"] == 34720
assert document["camera_source_id"] == "sensor.camera.right"
assert document["evidence_session_id"] == "20260728T163450Z_viewer_live"
assert document["activation_trigger"] == "application-control-scanning"
assert document["network_change_admissible"] is True
assert document["network_change_reconciliation"] == (
"explicit-network-change-only-after-acknowledged-stop"
)
assert document["lease_generation"] == 3
assert document["lease_state"] == "reachable"
assert document["recovery_strategy"] == "existing-mqtt-endpoint"
assert document["endpoint_reachable"] is True
assert document["address_changed"] is False
assert document["device_write_performed"] is False
assert document["preferred_port"] == 9876
assert document["selected_port"] == 9877
assert "unapproved_secret_field" not in document
parent = logging.getLogger(SCANNER_LOGGER_NAME)
for handler in list(parent.handlers):
if getattr(handler, "baseFilename", None) == str(target):
parent.removeHandler(handler)
handler.close()
def test_live_viewer_diagnostic_endpoint_accepts_only_bounded_events(
caplog: pytest.LogCaptureFixture,
) -> None:
expected_build = "/assets/index-abcdefgh.js"
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: expected_build)
endpoint = _endpoint(router, "/api/v1/viewer/live-diagnostics", "POST")
event = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v2",
event_code="live_receiver_stalled",
ui_build_id=expected_build,
document_instance_id="00000000-0000-4000-8000-000000000001",
viewer_instance_id="00000000-0000-4000-8000-000000000002",
lifecycle_generation=4,
failure_stage="receiver-stalled",
stream_id="acquisition-123",
backend_activity_sequence=8_572,
viewer_range_max_ns=231_000_000_000,
stalled_for_ms=5_500,
recovery_attempt=1,
)
with caplog.at_level(
logging.INFO,
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
):
response = endpoint(event)
assert response.status_code == 204
assert response.headers["x-missioncore-ui-build"] == expected_build
assert "event=live_receiver_stalled" in caplog.text
assert caplog.records[-1].failure_stage == "receiver-stalled"
assert caplog.records[-1].document_instance_id == event.document_instance_id
assert caplog.records[-1].viewer_instance_id == event.viewer_instance_id
assert caplog.records[-1].lifecycle_generation == 4
with pytest.raises(ValidationError):
LiveViewerDiagnosticEvent.model_validate(
{
**event.model_dump(),
"source_url": "http://192.168.56.1/private",
}
)
fallback = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v2",
event_code="live_receiver_active_store_admitted",
ui_build_id=expected_build,
document_instance_id="00000000-0000-4000-8000-000000000001",
viewer_instance_id="00000000-0000-4000-8000-000000000002",
lifecycle_generation=4,
stream_id="acquisition-123",
backend_activity_sequence=8_573,
)
assert fallback.failure_stage is None
def test_live_viewer_diagnostic_rejects_stale_build_before_logging(
caplog: pytest.LogCaptureFixture,
) -> None:
expected_build = "/assets/index-ijklmnop.js"
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: expected_build)
endpoint = _endpoint(router, "/api/v1/viewer/live-diagnostics", "POST")
event = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v2",
event_code="live_receiver_error",
ui_build_id="/assets/index-abcdefgh.js",
document_instance_id="00000000-0000-4000-8000-000000000001",
viewer_instance_id="00000000-0000-4000-8000-000000000002",
lifecycle_generation=1,
)
with caplog.at_level(
logging.INFO,
logger="k1link.device_plugins.xgrids_k1.viewer_receiver",
):
response = endpoint(event)
assert response.status_code == 409
assert response.headers["x-missioncore-ui-build"] == expected_build
assert "Mission Core live Rerun receiver diagnostic" not in caplog.text
def test_live_viewer_client_contract_is_no_store_and_exact_build() -> None:
expected_build = "/assets/index-abcdefgh.js"
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: expected_build)
endpoint = _endpoint(router, "/api/v1/viewer/client-contract", "GET")
response = endpoint()
assert response.status_code == 200
assert response.headers["cache-control"] == "no-store"
assert response.headers["x-missioncore-ui-build"] == expected_build
assert json.loads(response.body) == {
"schema_version": "missioncore.live-viewer-client-contract/v1",
"status": "ready",
"ui_build_id": expected_build,
"diagnostic_schema_version": "missioncore.live-viewer-diagnostic/v2",
}
def test_live_viewer_client_contract_no_dist_is_retryable_without_reload_header() -> None:
router = build_viewer_diagnostics_router(expected_ui_build_id=lambda: None)
endpoint = _endpoint(router, "/api/v1/viewer/client-contract", "GET")
response = endpoint()
assert response.status_code == 503
assert response.headers["cache-control"] == "no-store"
assert "x-missioncore-ui-build" not in response.headers
assert json.loads(response.body) == {
"schema_version": "missioncore.live-viewer-client-contract/v1",
"status": "frontend-build-unavailable",
}
def test_development_viewer_diagnostics_do_not_reload_against_dist_build() -> None:
router = build_viewer_diagnostics_router(
expected_ui_build_id=lambda: "/assets/index-abcdefgh.js",
)
endpoint = _endpoint(router, "/api/v1/viewer/live-diagnostics", "POST")
event = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v2",
event_code="live_receiver_active_store_admitted",
ui_build_id="development",
document_instance_id="00000000-0000-4000-8000-000000000001",
viewer_instance_id="00000000-0000-4000-8000-000000000002",
lifecycle_generation=1,
)
response = endpoint(event)
assert response.status_code == 204