Files
next-tjwater-server/tests/api/test_operational_task_dynamic_flow.py
jiang 1d061f6099
Generic Container CI/CD / test-build-publish (push) Failing after 6s
Server CI/CD v2 / build-test-publish-and-deploy (push) Failing after 6s
feat: add project-scoped operations
2026-10-22 18:40:20 +08:00

126 lines
6.8 KiB
Python

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()
@pytest.mark.anyio
async def test_report_endpoints_preserve_interactive_blocks_and_version_context(monkeypatch):
from app.domain.schemas.operational_task import AgentArtifactsCommand, InvestigationReportRevision
from tests.unit.test_report_ui import rich_report
report = rich_report()
snapshot = InvestigationReportRevision(
revision=2, execution_generation=1, source_session_id="session-1",
published_at="2026-10-20T08:00:00Z", published_by="agent", report=report,
)
service = SimpleNamespace(report=AsyncMock(return_value=snapshot),
list_reports=AsyncMock(return_value=([snapshot], None)),
publish_artifacts=AsyncMock(return_value={"report_revision": 2}))
monkeypatch.setattr(endpoints, "_service", lambda _ctx, _conn: service)
task_id = uuid4()
command = AgentArtifactsCommand(request_id=uuid4(), source_session_id="session-1", report=report)
await endpoints.publish_operational_task_artifacts(task_id, command, ctx=_service_context(), conn=object())
assert service.publish_artifacts.await_args.args[1].report.ui_blocks[0].block.block_id == "pressure"
result = await endpoints.get_investigation_report(task_id, 2, ctx=_service_context(), conn=object())
restored = InvestigationReportRevision.model_validate_json(result.model_dump_json())
assert restored.report.ui_blocks[0].block.spec.elements["chart"].props["series"][0]["data"] == [0.3, 0.27]
assert restored.revision == 2
schemas = app.openapi()["components"]["schemas"]
assert "ui_blocks" in schemas["InvestigationReportInput"]["properties"]
assert "ui_blocks" in schemas["InvestigationReport"]["properties"]