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.
207 lines
6.7 KiB
Python
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},
|
|
})
|