From 32cc861f2a85e16ef78dc5d768d7d6756a06f006 Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sun, 16 Aug 2026 20:02:43 +0800 Subject: [PATCH] test(webui): harden mutation replay coverage --- .../websocket/tests/test_websocket_channel.py | 58 ++++++++++++++++++- webui/src/tests/nanobot-client.test.ts | 2 +- 2 files changed, 56 insertions(+), 4 deletions(-) diff --git a/nanobot/channels/websocket/tests/test_websocket_channel.py b/nanobot/channels/websocket/tests/test_websocket_channel.py index b737e5ff3..db8568eef 100644 --- a/nanobot/channels/websocket/tests/test_websocket_channel.py +++ b/nanobot/channels/websocket/tests/test_websocket_channel.py @@ -6,7 +6,7 @@ import time import uuid from pathlib import Path from types import SimpleNamespace -from typing import Any +from typing import Any, cast from unittest.mock import AsyncMock, MagicMock import httpx @@ -1003,10 +1003,13 @@ async def test_webui_mutations_preserve_request_and_response_order(bus: MagicMoc @pytest.mark.asyncio -async def test_webui_request_reuses_inflight_operation_after_reconnect(bus: MagicMock) -> None: +async def test_webui_request_survives_disconnect_and_reuses_inflight_operation( + bus: MagicMock, +) -> None: channel = _ch(bus) first_conn = AsyncMock() retry_conn = AsyncMock() + first_conn.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True) channel._webui_connections.update({first_conn, retry_conn}) started = asyncio.Event() release = asyncio.Event() @@ -1026,6 +1029,8 @@ async def test_webui_request_reuses_inflight_operation_after_reconnect(bus: Magi await channel._dispatch_envelope(first_conn, "webui-client", envelope) await started.wait() + await channel._cleanup_connection(first_conn) + assert first_conn not in channel._webui_connections await channel._dispatch_envelope(retry_conn, "webui-client", envelope) pending = tuple(channel._webui_request_tasks.values()) release.set() @@ -1042,7 +1047,6 @@ async def test_webui_request_reuses_inflight_operation_after_reconnect(bus: Magi "ok": True, "result": {"ran": True}, } - assert json.loads(first_conn.send.await_args.args[0]) == expected assert json.loads(retry_conn.send.await_args.args[0]) == expected @@ -1122,6 +1126,54 @@ async def test_webui_request_rejects_request_id_reuse_with_different_payload( } +def test_webui_request_cache_prunes_expired_completed_but_keeps_pending( + bus: MagicMock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + import nanobot.channels.websocket.runtime as websocket_module + + channel = _ch(bus) + now = 1_000.0 + monkeypatch.setattr(websocket_module, "time", SimpleNamespace(monotonic=lambda: now)) + operations = cast(dict[str, Any], channel._webui_request_operations) + operations["pending"] = SimpleNamespace(completed_at=None) + operations["expired"] = SimpleNamespace( + completed_at=now - websocket_module._WEBUI_REQUEST_CACHE_TTL_S + ) + operations["fresh"] = SimpleNamespace(completed_at=now - 1) + + channel._prune_webui_request_operations() + + assert "pending" in operations + assert "expired" not in operations + assert "fresh" in operations + + +def test_webui_request_cache_prunes_oldest_completed_at_capacity( + bus: MagicMock, + monkeypatch: pytest.MonkeyPatch, +) -> None: + import nanobot.channels.websocket.runtime as websocket_module + + channel = _ch(bus) + now = 1_000.0 + monkeypatch.setattr(websocket_module, "time", SimpleNamespace(monotonic=lambda: now)) + operations = cast(dict[str, Any], channel._webui_request_operations) + operations["pending"] = SimpleNamespace(completed_at=None) + for index in range(websocket_module._WEBUI_REQUEST_CACHE_MAX + 1): + operations[f"completed-{index}"] = SimpleNamespace( + completed_at=now - 1 + index / 1_000 + ) + + channel._prune_webui_request_operations() + + assert "pending" in operations + assert "completed-0" not in operations + assert "completed-1" in operations + completed = [operation for operation in operations.values() if operation.completed_at is not None] + assert len(completed) == websocket_module._WEBUI_REQUEST_CACHE_MAX + + @pytest.mark.asyncio async def test_webui_request_returns_correlated_route_error(bus: MagicMock) -> None: channel = _ch(bus) diff --git a/webui/src/tests/nanobot-client.test.ts b/webui/src/tests/nanobot-client.test.ts index 3d15965bc..8e0019214 100644 --- a/webui/src/tests/nanobot-client.test.ts +++ b/webui/src/tests/nanobot-client.test.ts @@ -172,7 +172,7 @@ describe("NanobotClient", () => { }); }); - it("retries in-flight WebUI mutations with the same request id after reconnect", async () => { + it("replays the exact serialized mutation frame after reconnect", async () => { const client = new NanobotClient({ url: "ws://test", reconnect: true,