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:
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user