Files
NODEDC_MISSION_CORE/tests/test_viewer_diagnostics_api.py
T

157 lines
6.0 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,
"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["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:
router = build_viewer_diagnostics_router()
endpoint = _endpoint(router, "/api/v1/viewer/live-diagnostics", "POST")
event = LiveViewerDiagnosticEvent(
schema_version="missioncore.live-viewer-diagnostic/v1",
event_code="live_receiver_stalled",
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 "event=live_receiver_stalled" in caplog.text
assert caplog.records[-1].failure_stage == "receiver-stalled"
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/v1",
event_code="live_receiver_active_store_admitted",
stream_id="acquisition-123",
backend_activity_sequence=8_573,
)
assert fallback.failure_stage is None