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()