confluence_web/tests/backend/test_runner.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

157 lines
5.3 KiB
Python

"""Unit tests for QueryRunner orchestration, gate, deadlines, and cleanup."""
import asyncio
from pathlib import Path
import pytest
import httpx
from backend.artifacts import ArtifactStore
from backend.confluence import ConfluenceClient
from backend.containers import FakeContainerManager
from backend.dev.fake_peer import ScriptedContainerPeer
from backend.errors import BusyError, ExecutionFailedError, QueryTimeoutError
from backend.model import FakeModelAdapter
from backend.runner import QueryRunner
from backend.settings import Settings
from tests.backend.conftest import make_test_confluence_client_factory
@pytest.mark.asyncio
async def test_runner_standard_scenario(tmp_path: Path):
settings = Settings(
approved_confluence_origins=["https://approved.example.com"],
query_timeout_seconds=30.0,
cleanup_timeout_seconds=5.0,
)
artifact_store = ArtifactStore(tmp_path / "artifacts")
container_mgr = FakeContainerManager(lambda: ScriptedContainerPeer(scenario="standard"))
model_adapter = FakeModelAdapter()
conf_factory = make_test_confluence_client_factory()
runner = QueryRunner(
settings=settings,
container_manager=container_mgr,
artifact_store=artifact_store,
model_adapter=model_adapter,
confluence_client_factory=conf_factory,
)
result = await runner.run(
prompt="How do I deploy service X?",
confluence_url="https://approved.example.com",
confluence_pat="test-pat-12345678901234567890",
session_id="cw_session_123",
)
# Verify query result matches CONTRACTS section 2 & 7
assert result["session_id"]
assert "Deployment Guide" in result["markdown"]
assert len(result["pages_accessed"]) == 1
assert result["pages_accessed"][0]["page_id"] == "847291"
assert result["pages_accessed"][0]["space"] == "OPS"
assert result["pages_accessed"][0]["title"] == "Deployment Guide"
# Authoritative tool history has 2 Confluence calls (search and view)
assert len(result["tool_history"]) == 2
assert result["tool_history"][0]["tool"] == "confluence_search"
assert result["tool_history"][1]["tool"] == "confluence_view"
# Artifacts: 1 artifact committed
assert len(result["artifacts"]) == 1
art = result["artifacts"][0]
assert art["name"] == "checklist.md"
assert art["size_bytes"] == 32
# Container removal confirmed before success
assert len(container_mgr.removed_containers) == 1
# Verify download works from committed store
dl = artifact_store.get_artifact_for_download(art["id"], session_id="cw_session_123")
assert dl is not None
fpath, fname, fsize = dl
assert fsize == 32
assert fpath.read_bytes() == b"# Checklist\n\n- Deploy service X\n"
@pytest.mark.asyncio
async def test_runner_busy_gate(tmp_path: Path):
settings = Settings(
approved_confluence_origins=["https://approved.example.com"],
query_timeout_seconds=30.0,
)
artifact_store = ArtifactStore(tmp_path / "artifacts")
# Timeout scenario hangs so query is active
container_mgr = FakeContainerManager(lambda: ScriptedContainerPeer(scenario="timeout"))
model_adapter = FakeModelAdapter()
conf_factory = make_test_confluence_client_factory()
runner = QueryRunner(
settings=settings,
container_manager=container_mgr,
artifact_store=artifact_store,
model_adapter=model_adapter,
confluence_client_factory=conf_factory,
)
# Start first query in background
task1 = asyncio.create_task(
runner.run(
prompt="Query 1",
confluence_url="https://approved.example.com",
confluence_pat="test-pat-12345678901234567890",
session_id="sess_1",
)
)
await asyncio.sleep(0.05)
# Second query must raise BusyError immediately
with pytest.raises(BusyError, match="in progress"):
await runner.run(
prompt="Query 2",
confluence_url="https://approved.example.com",
confluence_pat="test-pat-12345678901234567890",
session_id="sess_2",
)
# Cancel first task and wait
task1.cancel()
try:
await task1
except asyncio.CancelledError:
pass
@pytest.mark.asyncio
async def test_runner_agent_error_cleanup(tmp_path: Path):
settings = Settings(
approved_confluence_origins=["https://approved.example.com"],
query_timeout_seconds=30.0,
)
artifact_store = ArtifactStore(tmp_path / "artifacts")
container_mgr = FakeContainerManager(lambda: ScriptedContainerPeer(scenario="agent_error"))
model_adapter = FakeModelAdapter()
conf_factory = make_test_confluence_client_factory()
runner = QueryRunner(
settings=settings,
container_manager=container_mgr,
artifact_store=artifact_store,
model_adapter=model_adapter,
confluence_client_factory=conf_factory,
)
with pytest.raises(ExecutionFailedError):
await runner.run(
prompt="Query error",
confluence_url="https://approved.example.com",
confluence_pat="test-pat-12345678901234567890",
session_id="sess_err",
)
# Cleanup confirmed
assert len(container_mgr.removed_containers) == 1
# No retained artifacts
assert len(artifact_store._committed) == 0