Files
beaver_project/app-instance/backend/tests/unit/test_terminal_websocket_channel.py

244 lines
9.6 KiB
Python

import asyncio
import json
import time
from pathlib import Path
from fastapi.testclient import TestClient
from beaver.foundation.events import InboundMessage, OutboundMessage
from beaver.interfaces.web.app import create_app
from beaver.services.agent_service import AgentService
class TerminalFakeAgentService(AgentService):
def __init__(self, *, config_path: Path, delay_seconds: float = 0.0) -> None:
super().__init__(config_path=config_path)
self.delay_seconds = delay_seconds
self.inbound_calls: list[InboundMessage] = []
async def handle_inbound_message(self, inbound: InboundMessage) -> OutboundMessage:
self.inbound_calls.append(inbound)
if self.delay_seconds:
await asyncio.sleep(self.delay_seconds)
return OutboundMessage(
message_id=inbound.message_id,
channel=inbound.channel,
content=f"echo:{inbound.content}",
session_id=inbound.session_id,
finish_reason="stop",
run_id="run-1",
channel_identity=inbound.channel_identity,
)
def write_terminal_config(tmp_path: Path) -> Path:
workspace = tmp_path / "workspace"
workspace.mkdir()
config_path = tmp_path / "config.json"
config_path.write_text(
json.dumps(
{
"agents": {"defaults": {"workspace": str(workspace), "model": "openai/gpt-5"}},
"providers": {},
"channels": {
"terminal-dev": {
"enabled": True,
"kind": "terminal",
"mode": "websocket",
"accountId": "local",
"displayName": "Terminal Dev",
"config": {"heartbeatSeconds": 30, "maxMessageChars": 20000},
}
},
}
),
encoding="utf-8",
)
return config_path
def test_terminal_websocket_connect_ping_and_message_roundtrip(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json(
{
"type": "connect",
"peer_id": "device-001",
"device_name": "desk-terminal",
"capabilities": ["text"],
}
)
assert websocket.receive_json() == {
"type": "connected",
"channel_id": "terminal-dev",
"session_id": "terminal-dev:local:device-001",
}
websocket.send_json({"type": "ping"})
assert websocket.receive_json() == {"type": "pong"}
websocket.send_json(
{
"type": "message",
"message_id": "device-001-000001",
"text": "hello",
}
)
assert websocket.receive_json() == {
"type": "ack",
"message_id": "device-001-000001",
"session_id": "terminal-dev:local:device-001",
"accepted": True,
}
reply = websocket.receive_json()
service.close()
assert reply == {
"type": "message",
"role": "assistant",
"message_id": "device-001-000001",
"run_id": "run-1",
"text": "echo:hello",
"finish_reason": "stop",
}
assert len(service.inbound_calls) == 1
inbound = service.inbound_calls[0]
assert inbound.channel == "terminal-dev"
assert inbound.content == "hello"
assert inbound.content_type == "text"
assert inbound.session_id == "terminal-dev:local:device-001"
assert inbound.channel_identity is not None
assert inbound.channel_identity.peer_id == "device-001"
assert inbound.channel_identity.peer_type == "terminal"
assert inbound.channel_identity.message_id == "device-001-000001"
def test_terminal_websocket_rejects_message_before_connect(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "message", "message_id": "m1", "text": "hello"})
assert websocket.receive_json() == {
"type": "error",
"error": "connect is required before message",
}
websocket.send_json({"type": "ping"})
assert websocket.receive_json() == {"type": "pong"}
service.close()
assert service.inbound_calls == []
def test_terminal_websocket_unknown_frame_keeps_connection_open(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "example"})
assert websocket.receive_json() == {
"type": "error",
"error": "Unsupported websocket frame type: example",
}
websocket.send_json({"type": "ping"})
assert websocket.receive_json() == {"type": "pong"}
service.close()
def test_terminal_websocket_validates_message_fields(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "connect", "peer_id": "device-001"})
assert websocket.receive_json()["type"] == "connected"
websocket.send_json({"type": "message", "text": "hello"})
assert websocket.receive_json() == {"type": "error", "error": "message_id is required"}
websocket.send_json({"type": "message", "message_id": "m1", "text": " "})
assert websocket.receive_json() == {"type": "error", "error": "text is required"}
service.close()
assert service.inbound_calls == []
def test_terminal_websocket_duplicate_message_returns_cached_reply(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "connect", "peer_id": "device-001"})
assert websocket.receive_json()["type"] == "connected"
frame = {"type": "message", "message_id": "device-001-000001", "text": "hello"}
websocket.send_json(frame)
assert websocket.receive_json()["accepted"] is True
assert websocket.receive_json()["text"] == "echo:hello"
websocket.send_json(frame)
duplicate = websocket.receive_json()
service.close()
assert duplicate["type"] == "ack"
assert duplicate["accepted"] is False
assert duplicate["duplicate"] is True
assert duplicate["pending"] is False
assert duplicate["reply"] == "echo:hello"
assert len(service.inbound_calls) == 1
def test_terminal_websocket_disconnect_before_reply_records_unclaimed(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path, delay_seconds=0.05)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "connect", "peer_id": "device-001"})
assert websocket.receive_json()["type"] == "connected"
websocket.send_json({"type": "message", "message_id": "device-001-000001", "text": "slow"})
assert websocket.receive_json()["accepted"] is True
time.sleep(0.15)
events = client.get("/api/channels/terminal-dev/events").json()
service.close()
kinds = [event["kind"] for event in events]
assert "terminal_disconnected" in kinds
assert "outbound_unclaimed" in kinds
def test_terminal_channel_status_exposes_websocket_url_and_peer_count(tmp_path: Path) -> None:
config_path = write_terminal_config(tmp_path)
service = TerminalFakeAgentService(config_path=config_path)
app = create_app(service=service, manage_service_lifecycle=False)
with TestClient(app) as client:
initial = client.get("/api/status").json()["channels"][0]
assert initial["channel_id"] == "terminal-dev"
assert initial["websocket_url"] == "/api/channels/terminal-dev/ws"
assert initial["connected_peers"] == 0
assert "persistent_connection" in initial["capabilities"]
with client.websocket_connect("/api/channels/terminal-dev/ws") as websocket:
websocket.send_json({"type": "connect", "peer_id": "device-001"})
assert websocket.receive_json()["type"] == "connected"
connected = client.get("/api/status").json()["channels"][0]
assert connected["connected_peers"] == 1
service.close()