Files
next-tjwater-server/tests/unit/test_mcp_work_order_skills.py
T
jiang be6317eb48 feat(server): integrate MCP queries and investigation messaging
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.
2026-10-19 18:46:58 +08:00

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)