from contextlib import asynccontextmanager from types import SimpleNamespace from unittest.mock import AsyncMock from uuid import uuid4 import pytest from app.domain.domain_errors import DomainError from app.domain.schemas.operational_task import AgentRunCommand from app.infra.db.postgresql.operational_tasks import OperationalTaskRepository from app.services.operational_tasks import OperationalTaskService @pytest.fixture def anyio_backend(): return "asyncio" @pytest.mark.anyio @pytest.mark.parametrize("available,agent_ownership_available,ready", [ (False, False, False), (True, False, False), (True, True, True), ]) async def test_execution_requires_message_and_agent_ownership_storage( available, agent_ownership_available, ready, ): cursor = SimpleNamespace(fetchone=AsyncMock(return_value={ "available": available, "agent_ownership_available": agent_ownership_available, })) connection = SimpleNamespace(execute=AsyncMock(return_value=cursor)) repository = OperationalTaskRepository(connection, uuid4()) if ready: await repository.require_execution_storage() else: with pytest.raises(DomainError) as failure: await repository.require_execution_storage() assert failure.value.status == 503 assert failure.value.code == "DEPENDENCY_UNAVAILABLE" assert "024_agent_messages.sql" in failure.value.message assert "030_operational_run_agent_ownership.sql" in failure.value.message assert "to_regclass('workbench.agent_messages')" in connection.execute.call_args.args[0] @pytest.mark.anyio @pytest.mark.parametrize("available", [False, True]) async def test_run_update_uses_agent_owner_column_only_after_migration(available): availability = SimpleNamespace(fetchone=AsyncMock(return_value={"available": available})) updated = SimpleNamespace(rowcount=1) connection = SimpleNamespace(execute=AsyncMock(side_effect=[availability, updated])) repository = OperationalTaskRepository(connection, uuid4()) run_id = str(uuid4()) await repository.update_run({ "run_id": run_id, "status": "running", "revision": 2, "agent_session_id": "session-1", "agent_instance_id": "agent-a", "lease_expires_at": None, "heartbeat_at": None, "started_at": None, "finished_at": None, "failure_code": None, "failure_detail": None, "dispatch_attempts": 0, "next_dispatch_at": None, "updated_at": None, }, 1) query, params = connection.execute.call_args.args assert ("agent_instance_id=%s" in query) is available assert ("agent-a" in params) is available @pytest.mark.anyio async def test_missing_message_storage_rejects_claim_before_state_changes(): @asynccontextmanager async def transaction(): yield repository = SimpleNamespace( transaction=transaction, require_execution_storage=AsyncMock(side_effect=DomainError( "DEPENDENCY_UNAVAILABLE", "Apply 024_agent_messages.sql", status=503, )), get_request=AsyncMock(return_value=None), lock=AsyncMock(), update_run=AsyncMock(), update_task=AsyncMock(), ) service = OperationalTaskService(repository, SimpleNamespace(), set()) with pytest.raises(DomainError, match="024_agent_messages.sql"): await service.claim_run(uuid4(), uuid4(), AgentRunCommand( request_id=uuid4(), expected_revision=1, source_session_id="test-session", )) repository.get_request.assert_not_awaited() repository.lock.assert_not_awaited() repository.update_run.assert_not_awaited() repository.update_task.assert_not_awaited() @pytest.mark.anyio async def test_presentation_history_storage_filters_project_task_and_committed_cursor(): project_id, task_id = uuid4(), uuid4() connection = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(fetchall=AsyncMock(return_value=[])))) repository = OperationalTaskRepository(connection, project_id) assert await repository.list_presentation_events(str(task_id), 7, 30, 21) == [] query, params = connection.execute.call_args.args assert params == (str(project_id), str(task_id), 7, 30, 21) assert "project_id=%s" in query and "aggregate_id=%s" in query assert "operational.presentation.requested" in query and "event_seq<=%s" in query assert "ORDER BY event_seq LIMIT %s" in query @pytest.mark.anyio async def test_missing_scada_manifest_migration_fails_closed(): connection = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace( fetchone=AsyncMock(return_value={"available": False}), ))) repository = OperationalTaskRepository(connection, uuid4()) with pytest.raises(DomainError) as failure: await repository.get_scada_presentation(str(uuid4()), str(uuid4())) assert failure.value.code == "DEPENDENCY_UNAVAILABLE" assert failure.value.status == 503 assert "035_scada_presentation_coverage.sql" in failure.value.message @pytest.mark.anyio async def test_scada_manifest_is_scoped_to_project_task_and_run_and_preserves_other_run_facts(): from datetime import datetime, timezone project, task, run = map(str, (uuid4(), uuid4(), uuid4())) document = {"version": 1, "targets": ["524529"], "charts": {}} connection = SimpleNamespace(execute=AsyncMock(side_effect=[ SimpleNamespace(fetchone=AsyncMock(return_value={"available": True})), SimpleNamespace(fetchone=AsyncMock(return_value={"document": document})), SimpleNamespace(rowcount=1), ])) repository = OperationalTaskRepository(connection, project) assert await repository.get_scada_presentation(task, run) == document query, params = connection.execute.call_args.args assert "artifact_manifest->'scada_presentation'" in query assert params == (project, task, run) await repository.save_scada_presentation(task, run, document, datetime.now(timezone.utc)) query, params = connection.execute.call_args.args assert "jsonb_set(artifact_manifest,'{scada_presentation}'" in query assert params[1:] == (project, task, run) assert "revision=" not in query and "status=" not in query @pytest.mark.anyio async def test_internal_manifest_does_not_change_public_run_schemas_after_migration(): from datetime import datetime, timezone from app.domain.schemas.operational_task import OperationalTaskRun from tests.support.operational_task_repository import _active_run, _context, _task_document context = _context() task = _task_document(context, datetime.now(timezone.utc)) run = _active_run(task, datetime.now(timezone.utc)) connection = SimpleNamespace(execute=AsyncMock(return_value=SimpleNamespace(fetchall=AsyncMock(return_value=[{ **run, "project_id": context.project_id, "artifact_manifest": {"scada_presentation": {"targets": ["524529"]}}, }])))) repository = OperationalTaskRepository(connection, context.project_id) public = (await repository.list_task_runs(task["task_id"], 10, 0))[0] assert "artifact_manifest" not in public assert OperationalTaskRun.model_validate(public).run_id == OperationalTaskRun.model_validate(run).run_id