mirror of
https://github.com/HKUDS/nanobot.git
synced 2026-06-14 14:54:06 +00:00
refactor: WebSocketChannel accepts injected http_handler, update all tests
This commit is contained in:
parent
22673c2a27
commit
e5eb08e3e5
@ -28,7 +28,6 @@ from websockets.http11 import Response
|
|||||||
from nanobot.bus.events import OUTBOUND_META_AGENT_UI, OutboundMessage
|
from nanobot.bus.events import OUTBOUND_META_AGENT_UI, OutboundMessage
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
from nanobot.channels.base import BaseChannel
|
from nanobot.channels.base import BaseChannel
|
||||||
from nanobot.webui.ws_http import GatewayHTTPHandler
|
|
||||||
from nanobot.config.paths import get_media_dir, get_workspace_path
|
from nanobot.config.paths import get_media_dir, get_workspace_path
|
||||||
from nanobot.config.schema import Base
|
from nanobot.config.schema import Base
|
||||||
from nanobot.security.workspace_access import (
|
from nanobot.security.workspace_access import (
|
||||||
@ -457,12 +456,10 @@ class WebSocketChannel(BaseChannel):
|
|||||||
bus: MessageBus,
|
bus: MessageBus,
|
||||||
*,
|
*,
|
||||||
session_manager: "SessionManager | None" = None,
|
session_manager: "SessionManager | None" = None,
|
||||||
static_dist_path: Path | None = None,
|
http_handler: Any | None = None,
|
||||||
workspace_path: Path | None = None,
|
workspace_path: Path | None = None,
|
||||||
restrict_to_workspace: bool = False,
|
restrict_to_workspace: bool = False,
|
||||||
runtime_model_name: Callable[[], str | None] | None = None,
|
|
||||||
runtime_surface: str = "browser",
|
runtime_surface: str = "browser",
|
||||||
runtime_capabilities_overrides: dict[str, Any] | None = None,
|
|
||||||
):
|
):
|
||||||
if isinstance(config, dict):
|
if isinstance(config, dict):
|
||||||
config = WebSocketConfig.model_validate(config)
|
config = WebSocketConfig.model_validate(config)
|
||||||
@ -476,32 +473,16 @@ class WebSocketChannel(BaseChannel):
|
|||||||
self._conn_default: dict[Any, str] = {}
|
self._conn_default: dict[Any, str] = {}
|
||||||
self._stop_event: asyncio.Event | None = None
|
self._stop_event: asyncio.Event | None = None
|
||||||
self._server_task: asyncio.Task[None] | None = None
|
self._server_task: asyncio.Task[None] | None = None
|
||||||
_resolved_workspace = (
|
|
||||||
Path(workspace_path).expanduser()
|
|
||||||
if workspace_path is not None
|
|
||||||
else get_workspace_path()
|
|
||||||
).resolve(strict=False)
|
|
||||||
self._default_restrict_to_workspace = restrict_to_workspace
|
self._default_restrict_to_workspace = restrict_to_workspace
|
||||||
self._runtime_surface = (
|
self._runtime_surface = (
|
||||||
"native" if runtime_surface in {"native", "desktop"} else "browser"
|
"native" if runtime_surface in {"native", "desktop"} else "browser"
|
||||||
)
|
)
|
||||||
|
|
||||||
# HTTP API handler — owns tokens, sessions, media, settings, static serving
|
# HTTP handler injected from outside (ChannelManager / gateway startup).
|
||||||
self._http = GatewayHTTPHandler(
|
# Owns tokens, sessions, media, settings, static serving.
|
||||||
config=self.config,
|
self._http = http_handler
|
||||||
session_manager=session_manager,
|
# Backwards-compat: workspace controller used in envelope dispatch
|
||||||
static_dist_path=(
|
self._webui_workspaces = http_handler.workspaces if http_handler else None
|
||||||
static_dist_path.resolve() if static_dist_path is not None else None
|
|
||||||
),
|
|
||||||
workspace_path=_resolved_workspace,
|
|
||||||
runtime_model_name=runtime_model_name,
|
|
||||||
runtime_surface=self._runtime_surface,
|
|
||||||
runtime_capabilities_overrides=runtime_capabilities_overrides,
|
|
||||||
bus=self.bus,
|
|
||||||
log=self.logger,
|
|
||||||
)
|
|
||||||
# Backwards-compat aliases for workspace controller used in envelope dispatch
|
|
||||||
self._webui_workspaces = self._http.workspaces
|
|
||||||
|
|
||||||
self._stream_text_buffers: dict[tuple[str, str], list[str]] = {}
|
self._stream_text_buffers: dict[tuple[str, str], list[str]] = {}
|
||||||
|
|
||||||
@ -614,8 +595,6 @@ class WebSocketChannel(BaseChannel):
|
|||||||
def _handle_bootstrap(self, connection, request):
|
def _handle_bootstrap(self, connection, request):
|
||||||
return self._http._handle_bootstrap(connection, request)
|
return self._http._handle_bootstrap(connection, request)
|
||||||
|
|
||||||
_MAX_ISSUED_TOKENS = GatewayHTTPHandler._MAX_ISSUED_TOKENS
|
|
||||||
|
|
||||||
def _handle_sessions_list(self, request):
|
def _handle_sessions_list(self, request):
|
||||||
return self._http._handle_sessions_list(request)
|
return self._http._handle_sessions_list(request)
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import asyncio
|
|||||||
import functools
|
import functools
|
||||||
import json
|
import json
|
||||||
import time
|
import time
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
@ -28,6 +29,7 @@ from nanobot.channels.websocket import (
|
|||||||
_parse_request_path,
|
_parse_request_path,
|
||||||
publish_runtime_model_update,
|
publish_runtime_model_update,
|
||||||
)
|
)
|
||||||
|
from nanobot.webui.ws_http import GatewayHTTPHandler
|
||||||
from nanobot.config.loader import load_config, save_config
|
from nanobot.config.loader import load_config, save_config
|
||||||
from nanobot.config.schema import Config, ModelPresetConfig
|
from nanobot.config.schema import Config, ModelPresetConfig
|
||||||
from nanobot.session import webui_turns as wth
|
from nanobot.session import webui_turns as wth
|
||||||
@ -49,7 +51,36 @@ def _ch(bus: Any, **kw: Any) -> WebSocketChannel:
|
|||||||
"websocketRequiresToken": False,
|
"websocketRequiresToken": False,
|
||||||
}
|
}
|
||||||
cfg.update(kw)
|
cfg.update(kw)
|
||||||
return WebSocketChannel(cfg, bus)
|
parsed = WebSocketConfig.model_validate(cfg)
|
||||||
|
http_handler = GatewayHTTPHandler(
|
||||||
|
config=parsed,
|
||||||
|
session_manager=None,
|
||||||
|
static_dist_path=None,
|
||||||
|
workspace_path=Path.cwd(),
|
||||||
|
runtime_model_name=None,
|
||||||
|
runtime_surface="browser",
|
||||||
|
runtime_capabilities_overrides=None,
|
||||||
|
bus=bus,
|
||||||
|
)
|
||||||
|
return WebSocketChannel(cfg, bus, http_handler=http_handler)
|
||||||
|
|
||||||
|
|
||||||
|
def _basic_handler(bus: Any, **kw: Any) -> GatewayHTTPHandler:
|
||||||
|
cfg = WebSocketConfig.model_validate({
|
||||||
|
"enabled": True, "allowFrom": ["*"],
|
||||||
|
"host": "127.0.0.1", "port": _PORT,
|
||||||
|
"path": "/ws", "websocketRequiresToken": False,
|
||||||
|
})
|
||||||
|
return GatewayHTTPHandler(
|
||||||
|
config=cfg,
|
||||||
|
session_manager=kw.get("session_manager"),
|
||||||
|
static_dist_path=None,
|
||||||
|
workspace_path=kw.get("workspace_path", Path.cwd()),
|
||||||
|
runtime_model_name=None,
|
||||||
|
runtime_surface=kw.get("runtime_surface", "browser"),
|
||||||
|
runtime_capabilities_overrides=kw.get("runtime_capabilities_overrides"),
|
||||||
|
bus=bus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
@ -163,6 +194,7 @@ def test_ssl_context_requires_both_cert_and_key_files() -> None:
|
|||||||
channel = WebSocketChannel(
|
channel = WebSocketChannel(
|
||||||
{"enabled": True, "allowFrom": ["*"], "sslCertfile": "/tmp/c.pem", "sslKeyfile": ""},
|
{"enabled": True, "allowFrom": ["*"], "sslCertfile": "/tmp/c.pem", "sslKeyfile": ""},
|
||||||
bus,
|
bus,
|
||||||
|
http_handler=_basic_handler(bus),
|
||||||
)
|
)
|
||||||
with pytest.raises(ValueError, match="ssl_certfile and ssl_keyfile"):
|
with pytest.raises(ValueError, match="ssl_certfile and ssl_keyfile"):
|
||||||
channel._build_ssl_context()
|
channel._build_ssl_context()
|
||||||
@ -279,8 +311,7 @@ async def test_webui_message_scope_inherits_persisted_session_scope(
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=sessions,
|
session_manager=sessions,
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=sessions, workspace_path=default_workspace),
|
||||||
restrict_to_workspace=True,
|
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("127.0.0.1", 50123)
|
conn.remote_address = ("127.0.0.1", 50123)
|
||||||
@ -327,8 +358,7 @@ async def test_webui_scope_expands_home_project_path(
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=SessionManager(tmp_path / "sessions"),
|
session_manager=SessionManager(tmp_path / "sessions"),
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=SessionManager(tmp_path / "sessions"), workspace_path=default_workspace),
|
||||||
restrict_to_workspace=True,
|
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("127.0.0.1", 50123)
|
conn.remote_address = ("127.0.0.1", 50123)
|
||||||
@ -366,7 +396,7 @@ async def test_webui_scope_rejects_missing_project_path(bus: MagicMock, tmp_path
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=SessionManager(tmp_path / "sessions"),
|
session_manager=SessionManager(tmp_path / "sessions"),
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=SessionManager(tmp_path / "sessions"), workspace_path=default_workspace),
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("127.0.0.1", 50123)
|
conn.remote_address = ("127.0.0.1", 50123)
|
||||||
@ -404,8 +434,7 @@ async def test_webui_scope_rejects_running_scope_change(bus: MagicMock, tmp_path
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=sessions,
|
session_manager=sessions,
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=sessions, workspace_path=default_workspace),
|
||||||
restrict_to_workspace=True,
|
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("127.0.0.1", 50123)
|
conn.remote_address = ("127.0.0.1", 50123)
|
||||||
@ -462,8 +491,7 @@ async def test_webui_set_workspace_scope_rejects_running_chat(bus: MagicMock, tm
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=sessions,
|
session_manager=sessions,
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=sessions, workspace_path=default_workspace),
|
||||||
restrict_to_workspace=True,
|
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("127.0.0.1", 50123)
|
conn.remote_address = ("127.0.0.1", 50123)
|
||||||
@ -523,8 +551,7 @@ async def test_webui_scope_rejects_non_loopback_custom_scope(bus: MagicMock, tmp
|
|||||||
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
{"enabled": True, "allowFrom": ["*"], "host": "127.0.0.1"},
|
||||||
bus,
|
bus,
|
||||||
session_manager=sessions,
|
session_manager=sessions,
|
||||||
workspace_path=default_workspace,
|
http_handler=_basic_handler(bus, session_manager=sessions, workspace_path=default_workspace),
|
||||||
restrict_to_workspace=True,
|
|
||||||
)
|
)
|
||||||
conn = AsyncMock()
|
conn = AsyncMock()
|
||||||
conn.remote_address = ("203.0.113.8", 50123)
|
conn.remote_address = ("203.0.113.8", 50123)
|
||||||
@ -553,7 +580,7 @@ async def test_webui_scope_rejects_non_loopback_custom_scope(bus: MagicMock, tmp
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_delivers_json_message_with_media_and_reply() -> None:
|
async def test_send_delivers_json_message_with_media_and_reply() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -579,7 +606,7 @@ async def test_send_delivers_json_message_with_media_and_reply() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_broadcasts_runtime_model_updates() -> None:
|
async def test_send_broadcasts_runtime_model_updates() -> None:
|
||||||
bus = MessageBus()
|
bus = MessageBus()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -627,7 +654,7 @@ async def test_send_stages_external_media_as_signed_url(monkeypatch, tmp_path) -
|
|||||||
|
|
||||||
monkeypatch.setattr("nanobot.channels.websocket.get_media_dir", fake_media_dir)
|
monkeypatch.setattr("nanobot.channels.websocket.get_media_dir", fake_media_dir)
|
||||||
monkeypatch.setattr("nanobot.webui.ws_http.get_media_dir", fake_media_dir)
|
monkeypatch.setattr("nanobot.webui.ws_http.get_media_dir", fake_media_dir)
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -650,7 +677,7 @@ async def test_send_stages_external_media_as_signed_url(monkeypatch, tmp_path) -
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_missing_connection_is_noop_without_error() -> None:
|
async def test_send_missing_connection_is_noop_without_error() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
msg = OutboundMessage(channel="websocket", chat_id="missing", content="x")
|
msg = OutboundMessage(channel="websocket", chat_id="missing", content="x")
|
||||||
await channel.send(msg)
|
await channel.send(msg)
|
||||||
|
|
||||||
@ -658,7 +685,7 @@ async def test_send_missing_connection_is_noop_without_error() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_removes_connection_on_connection_closed() -> None:
|
async def test_send_removes_connection_on_connection_closed() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
mock_ws.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True)
|
mock_ws.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True)
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
@ -673,7 +700,7 @@ async def test_send_removes_connection_on_connection_closed() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_progress_includes_structured_tool_events() -> None:
|
async def test_send_progress_includes_structured_tool_events() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -721,7 +748,7 @@ async def test_send_progress_includes_structured_tool_events() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_file_edit_progress_uses_file_edit_event() -> None:
|
async def test_send_file_edit_progress_uses_file_edit_event() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -770,7 +797,7 @@ async def test_send_file_edit_progress_uses_file_edit_event() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_progress_includes_agent_ui_blob() -> None:
|
async def test_send_progress_includes_agent_ui_blob() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -794,7 +821,7 @@ async def test_send_progress_includes_agent_ui_blob() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_delta_removes_connection_on_connection_closed() -> None:
|
async def test_send_delta_removes_connection_on_connection_closed() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
mock_ws.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True)
|
mock_ws.send.side_effect = ConnectionClosed(Close(1006, ""), Close(1006, ""), True)
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
@ -808,7 +835,7 @@ async def test_send_delta_removes_connection_on_connection_closed() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_delta_emits_delta_and_stream_end() -> None:
|
async def test_send_delta_emits_delta_and_stream_end() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -845,7 +872,7 @@ async def test_send_delta_stream_end_rewrites_local_markdown_image(monkeypatch,
|
|||||||
channel = WebSocketChannel(
|
channel = WebSocketChannel(
|
||||||
{"enabled": True, "allowFrom": ["*"], "streaming": True},
|
{"enabled": True, "allowFrom": ["*"], "streaming": True},
|
||||||
bus,
|
bus,
|
||||||
workspace_path=workspace,
|
http_handler=_basic_handler(bus, workspace_path=workspace),
|
||||||
)
|
)
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
@ -878,7 +905,7 @@ async def test_send_delta_stream_end_rewrites_inline_final_text(monkeypatch, tmp
|
|||||||
channel = WebSocketChannel(
|
channel = WebSocketChannel(
|
||||||
{"enabled": True, "allowFrom": ["*"], "streaming": True},
|
{"enabled": True, "allowFrom": ["*"], "streaming": True},
|
||||||
bus,
|
bus,
|
||||||
workspace_path=workspace,
|
http_handler=_basic_handler(bus, workspace_path=workspace),
|
||||||
)
|
)
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
@ -898,7 +925,7 @@ async def test_send_delta_stream_end_rewrites_inline_final_text(monkeypatch, tmp
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_reasoning_delta_emits_streaming_frame() -> None:
|
async def test_send_reasoning_delta_emits_streaming_frame() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -919,7 +946,7 @@ async def test_send_reasoning_delta_emits_streaming_frame() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_reasoning_end_emits_close_frame() -> None:
|
async def test_send_reasoning_end_emits_close_frame() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -935,7 +962,7 @@ async def test_send_reasoning_one_shot_expands_to_delta_plus_end() -> None:
|
|||||||
the base implementation must produce one delta and one end so the
|
the base implementation must produce one delta and one end so the
|
||||||
WebUI sees the same shape either way."""
|
WebUI sees the same shape either way."""
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -957,7 +984,7 @@ async def test_send_reasoning_one_shot_expands_to_delta_plus_end() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_reasoning_delta_drops_empty_chunks() -> None:
|
async def test_send_reasoning_delta_drops_empty_chunks() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -969,7 +996,7 @@ async def test_send_reasoning_delta_drops_empty_chunks() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_reasoning_without_subscribers_is_noop() -> None:
|
async def test_send_reasoning_without_subscribers_is_noop() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
|
|
||||||
await channel.send_reasoning_delta("unattached", "thinking", None)
|
await channel.send_reasoning_delta("unattached", "thinking", None)
|
||||||
await channel.send_reasoning_end("unattached", None)
|
await channel.send_reasoning_end("unattached", None)
|
||||||
@ -979,7 +1006,7 @@ async def test_send_reasoning_without_subscribers_is_noop() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_turn_end_emits_turn_end_event() -> None:
|
async def test_send_turn_end_emits_turn_end_event() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -998,7 +1025,7 @@ async def test_send_turn_end_emits_turn_end_event() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_turn_end_includes_latency_ms_when_present() -> None:
|
async def test_send_turn_end_includes_latency_ms_when_present() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1017,7 +1044,7 @@ async def test_send_turn_end_includes_latency_ms_when_present() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_turn_end_includes_goal_state_when_present() -> None:
|
async def test_send_turn_end_includes_goal_state_when_present() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1037,7 +1064,7 @@ async def test_send_turn_end_includes_goal_state_when_present() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_goal_status_running_emits_event_with_started_at() -> None:
|
async def test_send_goal_status_running_emits_event_with_started_at() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1065,7 +1092,7 @@ async def test_send_goal_status_running_emits_event_with_started_at() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_goal_status_idle_omits_started_at() -> None:
|
async def test_send_goal_status_idle_omits_started_at() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1088,7 +1115,7 @@ async def test_send_goal_status_idle_omits_started_at() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_goal_state_emits_blob_per_chat() -> None:
|
async def test_send_goal_state_emits_blob_per_chat() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_a = AsyncMock()
|
mock_a = AsyncMock()
|
||||||
mock_b = AsyncMock()
|
mock_b = AsyncMock()
|
||||||
channel._attach(mock_a, "chat-a")
|
channel._attach(mock_a, "chat-a")
|
||||||
@ -1117,7 +1144,7 @@ async def test_send_goal_state_emits_blob_per_chat() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_push_active_goal_state_noop_without_session_manager() -> None:
|
async def test_maybe_push_active_goal_state_noop_without_session_manager() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
channel._session_manager = None
|
channel._session_manager = None
|
||||||
@ -1128,7 +1155,7 @@ async def test_maybe_push_active_goal_state_noop_without_session_manager() -> No
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_push_active_goal_state_skips_when_no_goal_on_disk() -> None:
|
async def test_maybe_push_active_goal_state_skips_when_no_goal_on_disk() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
sm = MagicMock()
|
sm = MagicMock()
|
||||||
sm.read_session_file.return_value = None
|
sm.read_session_file.return_value = None
|
||||||
channel._session_manager = sm
|
channel._session_manager = sm
|
||||||
@ -1141,7 +1168,7 @@ async def test_maybe_push_active_goal_state_skips_when_no_goal_on_disk() -> None
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_push_active_goal_state_notifies_when_goal_active_on_disk() -> None:
|
async def test_maybe_push_active_goal_state_notifies_when_goal_active_on_disk() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
sm = MagicMock()
|
sm = MagicMock()
|
||||||
sm.read_session_file.return_value = {
|
sm.read_session_file.return_value = {
|
||||||
"metadata": {
|
"metadata": {
|
||||||
@ -1169,7 +1196,7 @@ async def test_maybe_push_active_goal_state_notifies_when_goal_active_on_disk()
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_push_turn_run_wall_clock_skips_when_no_active_turn() -> None:
|
async def test_maybe_push_turn_run_wall_clock_skips_when_no_active_turn() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
from nanobot.session import webui_turns as wth
|
from nanobot.session import webui_turns as wth
|
||||||
@ -1182,7 +1209,7 @@ async def test_maybe_push_turn_run_wall_clock_skips_when_no_active_turn() -> Non
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_maybe_push_turn_run_wall_clock_replays_running() -> None:
|
async def test_maybe_push_turn_run_wall_clock_replays_running() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
from nanobot.session import webui_turns as wth
|
from nanobot.session import webui_turns as wth
|
||||||
@ -1207,7 +1234,7 @@ async def test_maybe_push_turn_run_wall_clock_replays_running() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_session_updated_emits_session_updated_event() -> None:
|
async def test_send_session_updated_emits_session_updated_event() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1226,7 +1253,7 @@ async def test_send_session_updated_emits_session_updated_event() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_session_updated_includes_scope_when_present() -> None:
|
async def test_send_session_updated_includes_scope_when_present() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
|
|
||||||
@ -1245,7 +1272,7 @@ async def test_send_session_updated_includes_scope_when_present() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_non_connection_closed_exception_is_raised() -> None:
|
async def test_send_non_connection_closed_exception_is_raised() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
mock_ws = AsyncMock()
|
mock_ws = AsyncMock()
|
||||||
mock_ws.send.side_effect = RuntimeError("unexpected")
|
mock_ws.send.side_effect = RuntimeError("unexpected")
|
||||||
channel._attach(mock_ws, "chat-1")
|
channel._attach(mock_ws, "chat-1")
|
||||||
@ -1258,7 +1285,7 @@ async def test_send_non_connection_closed_exception_is_raised() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_send_delta_missing_connection_is_noop() -> None:
|
async def test_send_delta_missing_connection_is_noop() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"], "streaming": True}, bus, http_handler=_basic_handler(bus))
|
||||||
# No exception, no error — just a no-op
|
# No exception, no error — just a no-op
|
||||||
await channel.send_delta("nonexistent", "chunk", {"_stream_delta": True, "_stream_id": "s1"})
|
await channel.send_delta("nonexistent", "chunk", {"_stream_delta": True, "_stream_id": "s1"})
|
||||||
|
|
||||||
@ -1266,7 +1293,7 @@ async def test_send_delta_missing_connection_is_noop() -> None:
|
|||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_stop_is_idempotent() -> None:
|
async def test_stop_is_idempotent() -> None:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus)
|
channel = WebSocketChannel({"enabled": True, "allowFrom": ["*"]}, bus, http_handler=_basic_handler(bus))
|
||||||
# stop() before start() should not raise
|
# stop() before start() should not raise
|
||||||
await channel.stop()
|
await channel.stop()
|
||||||
await channel.stop()
|
await channel.stop()
|
||||||
@ -1750,8 +1777,7 @@ async def test_bootstrap_exposes_native_surface(bus: MagicMock) -> None:
|
|||||||
"websocketRequiresToken": True,
|
"websocketRequiresToken": True,
|
||||||
},
|
},
|
||||||
bus,
|
bus,
|
||||||
runtime_surface="native",
|
http_handler=_basic_handler(bus, runtime_surface="native", runtime_capabilities_overrides={"can_pick_folder": True}),
|
||||||
runtime_capabilities_overrides={"can_pick_folder": True},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
server_task = asyncio.create_task(channel.start())
|
server_task = asyncio.create_task(channel.start())
|
||||||
@ -1921,8 +1947,8 @@ async def test_token_issue_rejects_when_at_capacity(bus: MagicMock) -> None:
|
|||||||
|
|
||||||
try:
|
try:
|
||||||
# Fill issued tokens to capacity
|
# Fill issued tokens to capacity
|
||||||
channel._issued_tokens = {
|
channel._http.issued_tokens = {
|
||||||
f"nbwt_fill_{i}": time.monotonic() + 300 for i in range(channel._MAX_ISSUED_TOKENS)
|
f"nbwt_fill_{i}": time.monotonic() + 300 for i in range(channel._http._MAX_ISSUED_TOKENS)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp = await _http_get(
|
resp = await _http_get(
|
||||||
|
|||||||
@ -18,8 +18,10 @@ import pytest
|
|||||||
|
|
||||||
from nanobot.channels.websocket import (
|
from nanobot.channels.websocket import (
|
||||||
WebSocketChannel,
|
WebSocketChannel,
|
||||||
|
WebSocketConfig,
|
||||||
_extract_data_url_mime,
|
_extract_data_url_mime,
|
||||||
)
|
)
|
||||||
|
from nanobot.webui.ws_http import GatewayHTTPHandler
|
||||||
|
|
||||||
|
|
||||||
def _tiny_png_data_url() -> str:
|
def _tiny_png_data_url() -> str:
|
||||||
@ -41,10 +43,19 @@ def _data_url(mime: str, payload: bytes) -> str:
|
|||||||
def _make_channel() -> WebSocketChannel:
|
def _make_channel() -> WebSocketChannel:
|
||||||
bus = MagicMock()
|
bus = MagicMock()
|
||||||
bus.publish_inbound = AsyncMock()
|
bus.publish_inbound = AsyncMock()
|
||||||
channel = WebSocketChannel(
|
cfg = {"enabled": True, "allowFrom": ["*"], "websocketRequiresToken": False}
|
||||||
{"enabled": True, "allowFrom": ["*"], "websocketRequiresToken": False},
|
parsed = WebSocketConfig.model_validate(cfg)
|
||||||
bus,
|
handler = GatewayHTTPHandler(
|
||||||
|
config=parsed,
|
||||||
|
session_manager=None,
|
||||||
|
static_dist_path=None,
|
||||||
|
workspace_path=Path.cwd(),
|
||||||
|
runtime_model_name=None,
|
||||||
|
runtime_surface="browser",
|
||||||
|
runtime_capabilities_overrides=None,
|
||||||
|
bus=bus,
|
||||||
)
|
)
|
||||||
|
channel = WebSocketChannel(cfg, bus, http_handler=handler)
|
||||||
channel._handle_message = AsyncMock() # type: ignore[method-assign]
|
channel._handle_message = AsyncMock() # type: ignore[method-assign]
|
||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
|||||||
@ -11,12 +11,35 @@ from urllib.parse import urlencode
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.channels.websocket import WebSocketChannel
|
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
|
from nanobot.webui.ws_http import GatewayHTTPHandler
|
||||||
|
|
||||||
_PORT = 29900
|
_PORT = 29900
|
||||||
|
|
||||||
|
|
||||||
|
def _make_handler(
|
||||||
|
cfg: dict[str, Any] | WebSocketConfig,
|
||||||
|
bus: Any,
|
||||||
|
*,
|
||||||
|
session_manager: SessionManager | None = None,
|
||||||
|
static_dist_path: Path | None = None,
|
||||||
|
runtime_model_name: Any | None = None,
|
||||||
|
) -> GatewayHTTPHandler:
|
||||||
|
config = WebSocketConfig.model_validate(cfg) if isinstance(cfg, dict) else cfg
|
||||||
|
workspace = Path.cwd()
|
||||||
|
return GatewayHTTPHandler(
|
||||||
|
config=config,
|
||||||
|
session_manager=session_manager,
|
||||||
|
static_dist_path=static_dist_path,
|
||||||
|
workspace_path=workspace,
|
||||||
|
runtime_model_name=runtime_model_name,
|
||||||
|
runtime_surface="browser",
|
||||||
|
runtime_capabilities_overrides=None,
|
||||||
|
bus=bus,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _ch(
|
def _ch(
|
||||||
bus: Any,
|
bus: Any,
|
||||||
*,
|
*,
|
||||||
@ -35,17 +58,13 @@ def _ch(
|
|||||||
"websocketRequiresToken": False,
|
"websocketRequiresToken": False,
|
||||||
}
|
}
|
||||||
cfg.update(extra)
|
cfg.update(extra)
|
||||||
ws_kwargs: dict[str, Any] = {
|
http_handler = _make_handler(
|
||||||
"session_manager": session_manager,
|
cfg, bus,
|
||||||
"static_dist_path": static_dist_path,
|
session_manager=session_manager,
|
||||||
}
|
static_dist_path=static_dist_path,
|
||||||
if runtime_model_name is not None:
|
runtime_model_name=runtime_model_name,
|
||||||
ws_kwargs["runtime_model_name"] = runtime_model_name
|
|
||||||
return WebSocketChannel(
|
|
||||||
cfg,
|
|
||||||
bus,
|
|
||||||
**ws_kwargs,
|
|
||||||
)
|
)
|
||||||
|
return WebSocketChannel(cfg, bus, http_handler=http_handler)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|||||||
@ -8,14 +8,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from unittest.mock import AsyncMock, MagicMock
|
from unittest.mock import AsyncMock, MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
import websockets
|
import websockets
|
||||||
|
|
||||||
from nanobot.channels.websocket import WebSocketChannel
|
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
|
||||||
from nanobot.bus.events import OutboundMessage
|
from nanobot.bus.events import OutboundMessage
|
||||||
|
from nanobot.webui.ws_http import GatewayHTTPHandler
|
||||||
from ws_test_client import WsTestClient, issue_token, issue_token_ok
|
from ws_test_client import WsTestClient, issue_token, issue_token_ok
|
||||||
|
|
||||||
|
|
||||||
@ -29,7 +31,18 @@ def _ch(bus: Any, port: int, **kw: Any) -> WebSocketChannel:
|
|||||||
"websocketRequiresToken": False,
|
"websocketRequiresToken": False,
|
||||||
}
|
}
|
||||||
cfg.update(kw)
|
cfg.update(kw)
|
||||||
return WebSocketChannel(cfg, bus)
|
parsed = WebSocketConfig.model_validate(cfg)
|
||||||
|
handler = GatewayHTTPHandler(
|
||||||
|
config=parsed,
|
||||||
|
session_manager=None,
|
||||||
|
static_dist_path=None,
|
||||||
|
workspace_path=Path.cwd(),
|
||||||
|
runtime_model_name=None,
|
||||||
|
runtime_surface="browser",
|
||||||
|
runtime_capabilities_overrides=None,
|
||||||
|
bus=bus,
|
||||||
|
)
|
||||||
|
return WebSocketChannel(cfg, bus, http_handler=handler)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|||||||
@ -21,12 +21,13 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from nanobot.channels.websocket import WebSocketChannel
|
from nanobot.channels.websocket import WebSocketChannel, WebSocketConfig
|
||||||
from nanobot.webui.media_api import (
|
from nanobot.webui.media_api import (
|
||||||
b64url_decode,
|
b64url_decode,
|
||||||
b64url_encode,
|
b64url_encode,
|
||||||
)
|
)
|
||||||
from nanobot.session.manager import Session, SessionManager
|
from nanobot.session.manager import Session, SessionManager
|
||||||
|
from nanobot.webui.ws_http import GatewayHTTPHandler
|
||||||
|
|
||||||
|
|
||||||
# PNG magic bytes + a couple of sentinel bytes so we can verify byte-for-byte
|
# PNG magic bytes + a couple of sentinel bytes so we can verify byte-for-byte
|
||||||
@ -47,19 +48,26 @@ def _ch(
|
|||||||
workspace_path: Path | None = None,
|
workspace_path: Path | None = None,
|
||||||
port: int,
|
port: int,
|
||||||
) -> WebSocketChannel:
|
) -> WebSocketChannel:
|
||||||
return WebSocketChannel(
|
cfg = {
|
||||||
{
|
"enabled": True,
|
||||||
"enabled": True,
|
"allowFrom": ["*"],
|
||||||
"allowFrom": ["*"],
|
"host": "127.0.0.1",
|
||||||
"host": "127.0.0.1",
|
"port": port,
|
||||||
"port": port,
|
"path": "/",
|
||||||
"path": "/",
|
"websocketRequiresToken": False,
|
||||||
"websocketRequiresToken": False,
|
}
|
||||||
},
|
parsed = WebSocketConfig.model_validate(cfg)
|
||||||
bus,
|
http_handler = GatewayHTTPHandler(
|
||||||
|
config=parsed,
|
||||||
session_manager=session_manager,
|
session_manager=session_manager,
|
||||||
workspace_path=workspace_path,
|
static_dist_path=None,
|
||||||
|
workspace_path=workspace_path or Path.cwd(),
|
||||||
|
runtime_model_name=None,
|
||||||
|
runtime_surface="browser",
|
||||||
|
runtime_capabilities_overrides=None,
|
||||||
|
bus=bus,
|
||||||
)
|
)
|
||||||
|
return WebSocketChannel(cfg, bus, http_handler=http_handler)
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@pytest.fixture()
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user