mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-08-31 16:21:50 +03:00
test(webui): harden mutation replay coverage
This commit is contained in:
@@ -6,7 +6,7 @@ import time
|
|||||||
import uuid
|
import uuid
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import SimpleNamespace
|
from types import SimpleNamespace
|
||||||
from typing import Any
|
from typing import Any, cast
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
@@ -1003,10 +1003,13 @@ async def test_webui_mutations_preserve_request_and_response_order(bus: MagicMoc
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@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)
|
channel = _ch(bus)
|
||||||
first_conn = AsyncMock()
|
first_conn = AsyncMock()
|
||||||
retry_conn = AsyncMock()
|
retry_conn = AsyncMock()
|
||||||
|
first_conn.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True)
|
||||||
channel._webui_connections.update({first_conn, retry_conn})
|
channel._webui_connections.update({first_conn, retry_conn})
|
||||||
started = asyncio.Event()
|
started = asyncio.Event()
|
||||||
release = 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 channel._dispatch_envelope(first_conn, "webui-client", envelope)
|
||||||
await started.wait()
|
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)
|
await channel._dispatch_envelope(retry_conn, "webui-client", envelope)
|
||||||
pending = tuple(channel._webui_request_tasks.values())
|
pending = tuple(channel._webui_request_tasks.values())
|
||||||
release.set()
|
release.set()
|
||||||
@@ -1042,7 +1047,6 @@ async def test_webui_request_reuses_inflight_operation_after_reconnect(bus: Magi
|
|||||||
"ok": True,
|
"ok": True,
|
||||||
"result": {"ran": 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
|
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
|
@pytest.mark.asyncio
|
||||||
async def test_webui_request_returns_correlated_route_error(bus: MagicMock) -> None:
|
async def test_webui_request_returns_correlated_route_error(bus: MagicMock) -> None:
|
||||||
channel = _ch(bus)
|
channel = _ch(bus)
|
||||||
|
|||||||
@@ -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({
|
const client = new NanobotClient({
|
||||||
url: "ws://test",
|
url: "ws://test",
|
||||||
reconnect: true,
|
reconnect: true,
|
||||||
|
|||||||
Reference in New Issue
Block a user