Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions backend/druks/chat/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,12 +31,13 @@ async def request(self, method: str, **values: object) -> dict:
finally:
writer.close()
await writer.wait_closed()
# A docker-sbx gateway accepts a channel to a closed port and then ends it.
if not line:
raise ChatBridgeUnavailable("The Chat bridge is not available.")
try:
response = json.loads(line)
except ValueError as error:
raise ChatBridgeError(
"The Chat bridge closed the request without an answer."
) from error
raise ChatBridgeError("The Chat bridge sent an answer that is not JSON.") from error
if response["ok"]:
return response
raise ChatBridgeError(response["error"])
Expand Down
44 changes: 39 additions & 5 deletions backend/tests/test_chat.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,23 @@
from contextlib import asynccontextmanager
from datetime import timedelta
from types import SimpleNamespace
from unittest.mock import AsyncMock
from unittest.mock import AsyncMock, MagicMock
from uuid import uuid4

import asyncssh
import pytest
from druks.accounts.enums import AccountKind
from druks.accounts.models import Account, PersonalAccessToken
from druks.chat import service, sockets
from druks.chat.bridge import Bridge
from druks.chat.constants import CHAT_KEY_NAME
from druks.chat.enums import ConversationSource, MessageRole, MessageState
from druks.chat.exceptions import ChatBridgeError, ChatHarnessError, ChatSandboxGone
from druks.chat.exceptions import (
ChatBridgeError,
ChatBridgeUnavailable,
ChatHarnessError,
ChatSandboxGone,
)
from druks.chat.models import Conversation, Message
from druks.database import get_session
from druks.files.datastructures import File
Expand Down Expand Up @@ -354,14 +360,42 @@ async def download(**values):
assert host.upload_file.await_count == 0


async def test_bridge_start_uploads_the_bridge_only_when_none_answers(monkeypatch):
def channel(answer: bytes) -> tuple[asyncio.StreamReader, MagicMock]:
"""A loopback channel that sends one answer and ends."""
reader = asyncio.StreamReader()
reader.feed_data(answer)
reader.feed_eof()
return reader, MagicMock(drain=AsyncMock(), wait_closed=AsyncMock())


@pytest.mark.parametrize(
("opened", "error"),
[
(
asyncssh.ChannelOpenError(asyncssh.OPEN_CONNECT_FAILED, "Connection refused"),
ChatBridgeUnavailable,
),
(lambda *address: channel(b""), ChatBridgeUnavailable),
(lambda *address: channel(b"not json\n"), ChatBridgeError),
],
)
async def test_bridge_is_unavailable_only_when_it_sends_no_answer(opened, error):
host = SimpleNamespace(open_tcp_connection=AsyncMock(side_effect=opened))

with pytest.raises(error) as raised:
await Bridge(host).request("ping")

assert raised.type is error


async def test_bridge_start_uploads_the_bridge_only_when_none_answers():
pong = b'{"ok": true}\n'
host = SimpleNamespace(
ssh_username="druks",
open_tcp_connection=AsyncMock(side_effect=[channel(b""), channel(pong), channel(pong)]),
upload_file=AsyncMock(),
exec=AsyncMock(return_value=SimpleNamespace(ok=True)),
)
answers = iter([False, True, True])
monkeypatch.setattr(Bridge, "is_running", AsyncMock(side_effect=lambda: next(answers)))

await Bridge(host).start()
await Bridge(host).start()
Expand Down
Loading