fix(k1): stabilize repeated acquisition and live viewer recovery
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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",
|
||||
"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,
|
||||
"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["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["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
|
||||
Reference in New Issue
Block a user