from datetime import datetime, timezone from types import SimpleNamespace from unittest.mock import AsyncMock from uuid import uuid4 import pytest from fastapi import HTTPException from app.api.v1.endpoints import operational_tasks as endpoints from app.auth.project_dependencies import ProjectContext from app.domain.schemas.operational_data import TaskDataQuery, TaskDataSnapshot from app.domain.schemas.operational_task import EvidenceRecord from app.domain.schemas.operational_task import TaskPresentationEventCommand from app.main import app @pytest.fixture def anyio_backend(): return "asyncio" def _service_context(): return ProjectContext(project_id=uuid4(), user_id=uuid4(), project_code="test", project_role="member", system_role="service") def test_dynamic_investigation_routes_are_in_the_public_contract(): paths = app.openapi()["paths"] assert "post" in paths["/api/v1/operational-tasks/{task_id}/access-requests"] assert "post" in paths["/api/v1/operational-tasks/{task_id}/access-requests/{access_request_id}/decision"] assert "patch" in paths["/api/v1/operational-tasks/{task_id}/actions/{action_id}/draft"] assert "post" in paths["/api/v1/operational-tasks/{task_id}/actions/{action_id}/confirm-dispatch"] assert "get" in paths["/api/v1/operational-tasks/{task_id}/reports"] assert "get" in paths["/api/v1/operational-tasks/{task_id}/reports/{revision}"] assert "post" in paths["/api/v1/operational-tasks/{task_id}/presentation-events"] assert "get" in paths["/api/v1/operational-tasks/{task_id}/presentation-cursor"] context_params = paths["/api/v1/operational-tasks/{task_id}/agent-context"]["get"]["parameters"] assert any(item["name"] == "report_revision" and item["in"] == "query" for item in context_params) @pytest.mark.anyio async def test_presentation_routes_preserve_domain_rejections_and_do_not_create_runs(monkeypatch): from app.domain.domain_errors import DomainError service = SimpleNamespace( publish_presentation_event=AsyncMock(side_effect=DomainError("SESSION_MISMATCH", "stale session", status=409)), presentation_cursor=AsyncMock(return_value={"task_id": str(uuid4()), "execution_generation": 1, "active_run": None, "event_cursor": 17}), ) monkeypatch.setattr(endpoints, "_service", lambda _ctx, _conn: service) with pytest.raises(HTTPException) as error: await endpoints.publish_operational_task_presentation_event( uuid4(), TaskPresentationEventCommand(request_id=uuid4(), run_id=uuid4(), source_session_id="stale", command={"action": "clear_highlights"}), ctx=_service_context(), conn=object(), ) assert error.value.status_code == 409 assert error.value.detail["code"] == "SESSION_MISMATCH" result = await endpoints.get_operational_task_presentation_cursor(uuid4(), ctx=_service_context(), conn=object()) assert result["event_cursor"] == 17 and result["active_run"] is None @pytest.mark.anyio async def test_data_query_requires_task_grant_before_source_request(monkeypatch): service = SimpleNamespace(can_read_source=AsyncMock(return_value=False)) source = AsyncMock() monkeypatch.setattr(endpoints, "_service", lambda _ctx, _conn: service) monkeypatch.setattr(endpoints, "read_task_data", source) with pytest.raises(HTTPException) as error: await endpoints.query_operational_task_data( uuid4(), TaskDataQuery(source="scada_devices"), authorization="Bearer AGENT", ctx=_service_context(), conn=object(), ) assert error.value.status_code == 403 source.assert_not_awaited() @pytest.mark.anyio async def test_preconfigured_service_access_avoids_server_delegation(monkeypatch): now = datetime(2026, 9, 24, 8, tzinfo=timezone.utc) snapshot = TaskDataSnapshot(source="scada_devices", source_ref="scada_devices:sha256:test", queried_at=now, origin="system_data", data={"items": []}, truncated=False) evidence = EvidenceRecord(evidence_id=snapshot.source_ref, kind="system_data", summary="SCADA 设备数据快照", source_label="scada_devices", source_ref=snapshot.source_ref, observed_at=now, recorded_at=now, recorded_by="service", has_snapshot=True) service = SimpleNamespace(can_read_source=AsyncMock(return_value=True), record_data_snapshot=AsyncMock(return_value=(evidence, snapshot))) source = AsyncMock(return_value=snapshot) monkeypatch.setattr(endpoints, "_service", lambda _ctx, _conn: service) monkeypatch.setattr(endpoints, "read_task_data", source) result = await endpoints.query_operational_task_data( uuid4(), TaskDataQuery(source="scada_devices"), authorization="Bearer AGENT", ctx=_service_context(), conn=object(), ) assert source.await_args.kwargs["delegated"] is False assert result.evidence.evidence_id == snapshot.source_ref service.record_data_snapshot.assert_awaited_once()