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.
This commit is contained in:
DCCONSTRUCTIONS
2026-08-14 14:57:50 +03:00
parent aff331082f
commit 0ca7316a24
157 changed files with 152962 additions and 4036 deletions
+117
View File
@@ -5,9 +5,39 @@ import json
from typing import Any
import pytest
from fastapi import WebSocketDisconnect
import k1link.web.app as app_module
from k1link.device_plugins.xgrids_k1.facade import XGRIDS_K1_PLUGIN_ID
from k1link.web.app import INVALID_REQUEST_DETAIL, app
from k1link.web.plugin_runtime import PluginExecutionError
class _StateDispatcher:
async def invoke(
self,
plugin_id: str,
action_id: str,
payload: dict[str, Any],
) -> dict[str, Any]:
del plugin_id, action_id, payload
return {"phase": "idle"}
class _FailingSendWebSocket:
def __init__(self, failure: Exception) -> None:
self.failure = failure
self.accepted = False
async def accept(self) -> None:
self.accepted = True
async def send_json(self, payload: dict[str, Any]) -> None:
del payload
raise self.failure
async def close(self, *, code: int, reason: str) -> None:
del code, reason
async def _post_json(path: str, payload: dict[str, Any]) -> tuple[int, str]:
@@ -80,6 +110,21 @@ def test_validation_errors_do_not_echo_sensitive_request_values(
"verification": "live-device-info",
},
}
if wrap_input:
state_status, state_response = asyncio.run(
_post_json(
(
f"/api/v1/device-plugins/{XGRIDS_K1_PLUGIN_ID}/actions/"
"state.read"
),
{"input": {}},
)
)
assert state_status == 200
snapshot_runtime_id = json.loads(state_response)["state"][
"snapshot_runtime_id"
]
action_input["expected_snapshot_runtime_id"] = snapshot_runtime_id
payload = {"input": action_input} if wrap_input else action_input
status_code, response_text = asyncio.run(_post_json(path, payload))
@@ -89,3 +134,75 @@ def test_validation_errors_do_not_echo_sensitive_request_values(
assert sensitive_value not in response_text
assert sensitive_value[:32] not in response_text
assert "input_value" not in response_text
def test_legacy_ble_scan_requires_an_explicit_snapshot_runtime_header() -> None:
status_code, response_text = asyncio.run(
_post_json("/api/ble/scan", {"duration_seconds": 1})
)
assert status_code == 422
assert json.loads(response_text) == {"detail": INVALID_REQUEST_DETAIL}
def test_plugin_expected_state_preserves_its_non_gateway_http_status(
monkeypatch: pytest.MonkeyPatch,
) -> None:
class ExpectedStateDispatcher:
async def invoke(
self,
plugin_id: str,
action_id: str,
payload: dict[str, Any],
) -> dict[str, Any]:
del plugin_id, action_id, payload
raise PluginExecutionError(
"K1 не сообщил адрес общей локальной сети",
http_status_code=409,
reason_code="connection-verify-address-unavailable",
)
monkeypatch.setattr(app_module, "plugin_dispatcher", ExpectedStateDispatcher())
status_code, response_text = asyncio.run(
_post_json(
"/api/v1/device-plugins/test.plugin/actions/connection.verify",
{"input": {}},
)
)
assert status_code == 409
assert json.loads(response_text) == {"detail": "K1 не сообщил адрес общей локальной сети"}
@pytest.mark.parametrize(
"failure",
[
WebSocketDisconnect(code=1001),
RuntimeError("handler is closed"),
RuntimeError(
"unable to perform operation on <TCPTransport closed=True>; "
"the handler is closed"
),
],
)
def test_device_plugin_events_treats_proven_transport_disconnect_as_completion(
monkeypatch: pytest.MonkeyPatch,
failure: Exception,
) -> None:
websocket = _FailingSendWebSocket(failure)
monkeypatch.setattr(app_module, "plugin_dispatcher", _StateDispatcher())
asyncio.run(app_module.device_plugin_events(websocket, "test.plugin"))
assert websocket.accepted is True
def test_device_plugin_events_propagates_arbitrary_send_runtime_error(
monkeypatch: pytest.MonkeyPatch,
) -> None:
websocket = _FailingSendWebSocket(RuntimeError("plugin state serialization failed"))
monkeypatch.setattr(app_module, "plugin_dispatcher", _StateDispatcher())
with pytest.raises(RuntimeError, match="plugin state serialization failed"):
asyncio.run(app_module.device_plugin_events(websocket, "test.plugin"))