Add project-bound OAuth MCP queries and controlled analysis with scoped evidence, limits, timeouts and a nearby-model spatial query using EPSG:4326 inputs. Default nearby searches to 200 meters and five results. Integrate investigation messaging/presentation, health/readiness and typed history; remove duplicate web search and fix online simulation and compressed baseline writes. Include project migrations 023-025; keep local credentials and demo seeding excluded. Validation: 319 focused pytest checks; real PostGIS temporary-table regression; read-only Jinqian bridge radius comparison; cross-language MCP discovery/calls.
74 lines
3.4 KiB
Python
74 lines
3.4 KiB
Python
import asyncio
|
|
import json
|
|
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.mcp import query_service
|
|
from app.mcp.catalog import TOOLS
|
|
from app.mcp.models import BoundContext, TaskContextInput
|
|
from app.mcp.work_order_skills import read_work_order_skill
|
|
|
|
|
|
@pytest.mark.parametrize("workflow,name", [
|
|
("burst_report", "burst-report"),
|
|
("water_quality_complaint", "water-quality-complaint"),
|
|
("general", "appointment-follow-up"),
|
|
])
|
|
def test_skill_descriptor_and_body_have_same_version_and_digest(workflow, name):
|
|
descriptor = read_work_order_skill(workflow)
|
|
body = read_work_order_skill(workflow, include_content=True)
|
|
assert descriptor == {key: value for key, value in body.items() if key != "instructions"}
|
|
assert body["name"] == name and body["version"]
|
|
assert len(body["content_sha256"]) == 64 and body["instructions"]
|
|
assert "instructions" not in descriptor
|
|
# MCP task context has no evidence resource fallback: the complete instructions
|
|
# and result envelope must fit its inline response budget.
|
|
assert len(json.dumps(body, ensure_ascii=False).encode()) < 10000
|
|
|
|
|
|
@pytest.mark.parametrize("workflow", [None, "unknown", "../../secrets", "burst-report"])
|
|
def test_unknown_workflow_never_falls_back_or_reads_an_arbitrary_path(workflow):
|
|
assert read_work_order_skill(workflow)["available"] is False
|
|
|
|
|
|
def test_task_skill_uses_persisted_workflow_and_preserves_task_scope(monkeypatch):
|
|
task_id, project_id = uuid4(), uuid4()
|
|
reader = AsyncMock(return_value={"task": {"workflow": "burst_report"}})
|
|
monkeypatch.setattr(query_service.reads, "task_context_summary", reader)
|
|
monkeypatch.setattr(query_service, "permissions_for_context", lambda project: {"operational_task.view"})
|
|
scope = SimpleNamespace(connection=object(), project=object())
|
|
binding = BoundContext(
|
|
context_handle="a" * 43, project_id=project_id, subject="u", client_id="host",
|
|
native_session_id="s", purpose="task_execution", task_id=task_id,
|
|
tool_cap=["get_task_context"], permission_cap=["operational_task.view"],
|
|
expires_at=datetime.now(timezone.utc),
|
|
)
|
|
|
|
def query(section):
|
|
return asyncio.run(query_service.execute_query(scope, binding, TOOLS["get_task_context"],
|
|
TaskContextInput(context_handle=binding.context_handle, section=section)))
|
|
|
|
summary = query("summary")
|
|
body = query("skill")
|
|
assert summary.data["handling_skill"]["content_sha256"] == body.data["skill"]["content_sha256"]
|
|
assert summary.data["display_time_zone"] == "Asia/Shanghai"
|
|
assert body.data["skill"]["workflow"] == "burst_report"
|
|
assert body.data["skill"]["instructions"] and not body.truncated
|
|
assert body.evidence_id is None and body.semantics == "task"
|
|
reader.assert_awaited_with(scope.connection, project_id=project_id, task_id=task_id, report_revision=None)
|
|
binding.permission_cap = []
|
|
with pytest.raises(HTTPException) as caught:
|
|
query("skill")
|
|
assert caught.value.status_code == 403 and reader.await_count == 2
|
|
|
|
|
|
@pytest.mark.parametrize("extra", [{"workflow": "general"}, {"data_path": ["instructions"]}, {"offset": 1}])
|
|
def test_skill_input_cannot_select_another_workflow_or_partial_instructions(extra):
|
|
with pytest.raises(ValueError):
|
|
TaskContextInput(context_handle="a" * 43, section="skill", **extra)
|