confluence_web/backend/dev/fake_peer.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

207 lines
6.7 KiB
Python

"""Scripted container peer implementing NDJSON contract revision 1."""
from __future__ import annotations
import asyncio
import base64
import json
from typing import Any, Callable, Dict, List, Optional
class ScriptedContainerPeer:
"""Scripted container peer for deterministic runner tests."""
def __init__(
self,
scenario: str = "standard", # "standard", "no_artifacts", "skip_artifact", "agent_error", "timeout"
custom_steps: Optional[Callable] = None,
):
self.scenario = scenario
self.custom_steps = custom_steps
self.received_messages: List[Dict[str, Any]] = []
async def _read_line(self, reader: asyncio.StreamReader) -> Dict[str, Any]:
line = await reader.readline()
if not line:
raise EOFError("Unexpected EOF from backend")
msg = json.loads(line.decode("utf-8").strip())
self.received_messages.append(msg)
return msg
async def _write_msg(self, writer: asyncio.StreamWriter, msg: Dict[str, Any]) -> None:
raw = json.dumps(msg).encode("utf-8") + b"\n"
writer.write(raw)
await writer.drain()
async def run(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter) -> None:
"""Run the scripted protocol interaction."""
if self.custom_steps:
await self.custom_steps(reader, writer, self)
return
# 1. Receive 'start' frame
start_msg = await self._read_line(reader)
assert start_msg.get("type") == "start"
if self.scenario == "agent_error":
await self._write_msg(writer, {
"v": 1,
"type": "error",
"id": "a_err",
"payload": {"code": "execution_failed", "message": "Agent fatal error"},
})
return
if self.scenario == "timeout":
# Hang until cancelled or timeout
await asyncio.sleep(300)
return
# 2. Tool call: confluence_search
await self._write_msg(writer, {
"v": 1,
"type": "tool_request",
"id": "a_1",
"payload": {
"tool": "confluence_search",
"parameters": {"query": "deploy service X", "limit": 10},
},
})
search_resp = await self._read_line(reader)
assert search_resp.get("type") == "tool_response"
assert search_resp.get("reply_to") == "a_1"
# 3. Tool call: confluence_view
await self._write_msg(writer, {
"v": 1,
"type": "tool_request",
"id": "a_2",
"payload": {
"tool": "confluence_view",
"parameters": {"page_id": "847291"},
},
})
view_resp = await self._read_line(reader)
assert view_resp.get("type") == "tool_response"
assert view_resp.get("reply_to") == "a_2"
# 4. Model call
await self._write_msg(writer, {
"v": 1,
"type": "model_request",
"id": "a_3",
"payload": {
"messages": [{"role": "user", "content": [{"type": "text", "text": "Deploy checklist"}]}],
"tools": [],
},
})
model_resp = await self._read_line(reader)
assert model_resp.get("type") == "model_response"
assert model_resp.get("reply_to") == "a_3"
# 5. Collection start
markdown = (
"# Deployment Guide\n\n"
"Refer to [Deployment Guide](https://approved.example.com/pages/viewpage.action?pageId=847291).\n"
"Exported checklist: `checklist.md`.\n"
)
await self._write_msg(writer, {
"v": 1,
"type": "collection_start",
"id": "a_4",
"payload": {
"markdown": markdown,
"warnings": [],
},
})
coll_ready = await self._read_line(reader)
assert coll_ready.get("type") == "collection_ready"
assert coll_ready.get("reply_to") == "a_4"
# 6. Artifact export
if self.scenario == "no_artifacts":
# Complete with 0 transfers
await self._write_msg(writer, {
"v": 1,
"type": "complete",
"id": "a_comp",
"payload": {"accepted_transfer_count": 0},
})
return
if self.scenario == "skip_artifact":
# Declare oversized file
await self._write_msg(writer, {
"v": 1,
"type": "artifact_begin",
"id": "a_art_beg",
"payload": {
"transfer_id": "t_large",
"name": "large.bin",
"size_bytes": 15 * 1024 * 1024,
},
})
ack = await self._read_line(reader)
assert ack.get("type") == "artifact_ack"
assert ack.get("payload", {}).get("decision") == "skip"
# Complete with 0 accepted transfers
await self._write_msg(writer, {
"v": 1,
"type": "complete",
"id": "a_comp",
"payload": {"accepted_transfer_count": 0},
})
return
# Standard artifact transfer
file_bytes = b"# Checklist\n\n- Deploy service X\n"
b64_data = base64.b64encode(file_bytes).decode("utf-8")
await self._write_msg(writer, {
"v": 1,
"type": "artifact_begin",
"id": "a_5",
"payload": {
"transfer_id": "t_1",
"name": "checklist.md",
"size_bytes": len(file_bytes),
},
})
ack_begin = await self._read_line(reader)
assert ack_begin.get("type") == "artifact_ack"
assert ack_begin.get("payload", {}).get("decision") == "accept"
await self._write_msg(writer, {
"v": 1,
"type": "artifact_chunk",
"id": "a_6",
"payload": {
"transfer_id": "t_1",
"index": 0,
"data_base64": b64_data,
},
})
await self._write_msg(writer, {
"v": 1,
"type": "artifact_end",
"id": "a_7",
"payload": {
"transfer_id": "t_1",
"size_bytes": len(file_bytes),
"chunks": 1,
},
})
ack_end = await self._read_line(reader)
assert ack_end.get("type") == "artifact_ack"
assert ack_end.get("payload", {}).get("decision") == "stored"
# 7. Complete
await self._write_msg(writer, {
"v": 1,
"type": "complete",
"id": "a_8",
"payload": {"accepted_transfer_count": 1},
})