"""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