confluence_web/tests/backend/test_transport.py
Artur Mukhamadiev e65fbf4b67 backend: FastAPI backend track handoff (contract revision 1)
FastAPI app, upstream Confluence/model adapters, authoritative history,
rootless container lifecycle, artifact storage and downloads, fake peers
under backend/dev, tests under tests/backend. Root pytest.ini deselects
the live marker by default; requirements gain the backend dependencies.
2026-09-14 21:57:54 +03:00

155 lines
4.9 KiB
Python

"""Unit tests for NDJSON bridge transport and protocol state machine."""
import asyncio
import json
import pytest
from backend.transport import BridgeState, BridgeTransport, NDJSONProtocolError
class MockStream:
def __init__(self):
self._queue = asyncio.Queue()
async def readline(self) -> bytes:
data = await self._queue.get()
return data
def write(self, data: bytes) -> None:
self._queue.put_nowait(data)
async def drain(self) -> None:
pass
@pytest.mark.asyncio
async def test_transport_lifecycle():
to_backend = MockStream()
to_agent = MockStream()
backend_transport = BridgeTransport(reader=to_backend, writer=to_agent)
# 1. Start from INIT
assert backend_transport.state == BridgeState.INIT
await backend_transport.send_start(
prompt="Test prompt",
system_instruction="System",
remaining_ms=180000,
model_descriptor={"id": "test-model", "context_window_tokens": 1000, "max_output_tokens": 500},
)
assert backend_transport.state == BridgeState.RUNNING
# 2. Agent sends tool request
to_backend.write(
json.dumps({
"v": 1,
"type": "tool_request",
"id": "a_1",
"payload": {"tool": "confluence_view", "parameters": {"page_id": "123"}},
}).encode("utf-8") + b"\n"
)
msg = await backend_transport.read_message_bounded()
assert msg["type"] == "tool_request"
assert msg["id"] == "a_1"
assert "a_1" in backend_transport._inflight_calls
# 3. Agent sends collection_start while tool call in flight -> should fail!
to_backend.write(
json.dumps({
"v": 1,
"type": "collection_start",
"id": "a_2",
"payload": {"markdown": "# Done", "warnings": []},
}).encode("utf-8") + b"\n"
)
with pytest.raises(NDJSONProtocolError, match="still in flight"):
await backend_transport.read_message_bounded()
# 4. Backend answers tool request
await backend_transport.send_tool_response(
reply_to="a_1",
result={"page_id": "123", "markdown": "content", "truncated": False},
error=None,
)
assert "a_1" not in backend_transport._inflight_calls
# 5. Now agent sends collection_start -> transition to COLLECTING
to_backend.write(
json.dumps({
"v": 1,
"type": "collection_start",
"id": "a_3",
"payload": {"markdown": "# Done", "warnings": []},
}).encode("utf-8") + b"\n"
)
c_msg = await backend_transport.read_message_bounded()
assert c_msg["type"] == "collection_start"
assert backend_transport.state == BridgeState.COLLECTING
# 6. Backend sends collection_ready
await backend_transport.send_collection_ready(reply_to="a_3")
# 7. Agent sends complete -> transition to COMPLETE
to_backend.write(
json.dumps({
"v": 1,
"type": "complete",
"id": "a_4",
"payload": {"accepted_transfer_count": 0},
}).encode("utf-8") + b"\n"
)
comp_msg = await backend_transport.read_message_bounded()
assert comp_msg["type"] == "complete"
assert backend_transport.state == BridgeState.COMPLETE
@pytest.mark.asyncio
async def test_duplicate_message_ids():
to_backend = MockStream()
to_agent = MockStream()
transport = BridgeTransport(reader=to_backend, writer=to_agent)
await transport.send_start("p", "s", 1000, {"id": "m", "context_window_tokens": 1, "max_output_tokens": 1})
# First message with ID a_1
to_backend.write(
json.dumps({"v": 1, "type": "tool_request", "id": "a_1", "payload": {"tool": "t", "parameters": {}}}).encode()
+ b"\n"
)
await transport.read_message_bounded()
# Duplicate message with same ID a_1
to_backend.write(
json.dumps({"v": 1, "type": "tool_request", "id": "a_1", "payload": {"tool": "t", "parameters": {}}}).encode()
+ b"\n"
)
with pytest.raises(NDJSONProtocolError, match="Duplicate message ID"):
await transport.read_message_bounded()
@pytest.mark.asyncio
async def test_max_inflight_remote_calls():
to_backend = MockStream()
to_agent = MockStream()
transport = BridgeTransport(reader=to_backend, writer=to_agent)
await transport.send_start("p", "s", 1000, {"id": "m", "context_window_tokens": 1, "max_output_tokens": 1})
# Send 4 in-flight requests (allowed)
for i in range(4):
to_backend.write(
json.dumps({"v": 1, "type": "tool_request", "id": f"a_{i}", "payload": {"tool": "t", "parameters": {}}}).encode()
+ b"\n"
)
await transport.read_message_bounded()
# 5th in-flight request -> exceeds 4
to_backend.write(
json.dumps({"v": 1, "type": "tool_request", "id": "a_5", "payload": {"tool": "t", "parameters": {}}}).encode()
+ b"\n"
)
with pytest.raises(NDJSONProtocolError, match="in-flight"):
await transport.read_message_bounded()