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.
This commit is contained in:
2026-10-19 18:46:58 +08:00
parent 2b6fc5aa52
commit be6317eb48
117 changed files with 12703 additions and 638 deletions
@@ -0,0 +1,216 @@
"""Opt-in SQL checks against a disposable PostgreSQL database; no business data."""
import asyncio
import os
from datetime import datetime, timedelta, timezone
from types import SimpleNamespace
from uuid import uuid4
import pytest
from fastapi import HTTPException
from psycopg import AsyncConnection
from psycopg.rows import dict_row
from psycopg.types.json import Jsonb
from app.infra.db.postgresql import read_queries
from app.mcp.models import PeripheralInput
from app.mcp.peripheral import read_peripheral_query
from app.services.content_digest import digest
def test_history_and_evidence_pages_with_real_postgresql():
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN must point to a disposable PostgreSQL database")
async def check():
conn = await AsyncConnection.connect(dsn, row_factory=dict_row)
async with conn, conn.transaction(force_rollback=True):
await conn.execute("CREATE SCHEMA workbench")
await conn.execute("CREATE TABLE workbench.operational_task_evidence (project_id uuid, task_id uuid, evidence_id text, document jsonb)")
await conn.execute("CREATE SCHEMA scada")
await conn.execute("CREATE TABLE scada.measurements (device_id text, time timestamptz, monitored_value double precision, cleaned_value double precision)")
await conn.execute("CREATE SCHEMA analysis")
await conn.execute("CREATE TABLE analysis.node_results (run_id uuid, node_id text, time timestamptz, pressure double precision)")
await conn.execute("CREATE SCHEMA peripheral_sample")
await conn.execute("CREATE TABLE peripheral_sample.demo_datasets (project_id uuid, dataset_id text, document jsonb, digest text)")
project, task, run = uuid4(), uuid4(), uuid4()
start = datetime(2026, 10, 1, tzinfo=timezone.utc)
for i in range(2):
await conn.execute("INSERT INTO scada.measurements VALUES (%s,%s,%s,%s)", ("D1", start+timedelta(minutes=i), i, i+10))
await conn.execute("INSERT INTO analysis.node_results VALUES (%s,%s,%s,%s)", (run, "J1", start+timedelta(minutes=i), i+20))
await conn.execute("INSERT INTO analysis.node_results VALUES (%s,%s,%s,%s)", (uuid4(), "J1", start+timedelta(minutes=i), 999))
descriptors = [
{"source": "scada", "element_type": "junction", "id": "D1", "field": field}
for field in ("monitored_value", "cleaned_value")
] + [{"source": "analysis", "element_type": "junction", "id": "J1", "field": "pressure"}]
seen = []
for offset in (0, 2, 4, 6):
rows = await read_queries.history_rows(conn, descriptors, start=start, end=start+timedelta(hours=1), run_id=run, limit=2, offset=offset)
seen.extend(row["value"] for row in rows)
assert seen == [0, 1, 10, 11, 20, 21]
snapshot = {
"source": "query_scada_readings", "source_ref": "saved:1", "queried_at": start.isoformat(),
"origin": "synthetic", "truncated": False,
"data": {"semantics": "observed", "data": {
"items": [{"value": 1}, {"value": 2, "points": [3, 4]}],
"text": "甲😀乙丙", "none": None,
}},
}
await conn.execute("INSERT INTO workbench.operational_task_evidence VALUES (%s,%s,%s,%s)", (project, task, "E1", Jsonb({"data_snapshot": snapshot})))
async def evidence(path, **overrides):
values = dict(project_id=project, task_id=task, evidence_id="E1", data_path=path, limit=1, offset=0, mcp_sources=["query_scada_readings"])
return await read_queries.evidence_page(conn, **(values | overrides))
page = await evidence(["items"], offset=1)
assert page["total"] == 2 and page["nested"]
assert page["snapshot_metadata"]["origin"] == "synthetic"
assert page["query_metadata"] == {"semantics": "observed"}
assert page["value"] == [{"value": 2, "points": {"collection_type": "array", "count": 2, "data_path": ["items", "1", "points"]}}]
assert (await evidence(["items", "1", "points"], offset=1))["value"] == [4]
assert (await evidence(["text"], offset=1, limit=2))["value"] == "😀乙"
assert (await evidence(["none"]))["value"] is None
assert (await evidence([], full_snapshot=True)) == snapshot
assert (await evidence([]))["value"]["items"]["data_path"] == ["items"]
for overrides, status in (({"project_id": uuid4()}, 404), ({"task_id": uuid4()}, 404), ({"data_path": ["absent"]}, 422)):
with pytest.raises(HTTPException) as caught:
await evidence([], **overrides)
assert caught.value.status_code == status
legacy = {**snapshot, "source": "scada_readings", "data": {"items": [5, 6]}}
await conn.execute("INSERT INTO workbench.operational_task_evidence VALUES (%s,%s,%s,%s)", (project, task, "legacy", Jsonb({"data_snapshot": legacy})))
old_page = await evidence(["items"], evidence_id="legacy", offset=1)
assert old_page["value"] == [6] and old_page["query_metadata"] is None
# A large nested collection is represented by a path, not transferred in full.
snapshot["data"]["data"]["huge"] = ["x" * (read_queries.MAX_QUERY_BYTES + 1)]
await conn.execute("UPDATE workbench.operational_task_evidence SET document=%s", (Jsonb({"data_snapshot": snapshot}),))
assert (await evidence(["huge"], offset=1))["value"] == []
assert (await evidence([], limit=1))["value"]["huge"]["count"] == 1
descriptor = (await evidence(["huge"]))["value"][0]
assert descriptor["collection_type"] == "string" and descriptor["data_path"] == ["huge", "0"]
assert (await evidence(["huge", "0"], offset=7, limit=3))["value"] == "xxx"
with pytest.raises(HTTPException) as caught:
await evidence([], full_snapshot=True)
assert caught.value.status_code == 413
payload = {"project_id": str(project), "points": [{"point_id": "DEMO-1"}], "observations": [
{"point_id": "DEMO-1", "observed_at": (start+timedelta(minutes=i)).isoformat(), "value": i} for i in range(3)
]}
await conn.execute("INSERT INTO peripheral_sample.demo_datasets VALUES (%s,'water-demo-v1',%s,%s)", (project, Jsonb(payload), digest(payload)))
args = PeripheralInput(context_handle="a"*43, source="peripheral_observations", point_id="DEMO-1", start=start, end=start+timedelta(hours=1), limit=1, offset=1)
scope = SimpleNamespace(connection=conn, project=SimpleNamespace(project_id=project))
demo = await read_peripheral_query(scope, args)
assert demo.origin == "synthetic" and demo.truncated and demo.data["items"][0]["value"] == 1
await conn.execute("UPDATE peripheral_sample.demo_datasets SET digest='corrupted'")
with pytest.raises(HTTPException) as caught:
await read_peripheral_query(scope, args)
assert caught.value.status_code == 503
payload["padding"] = "x" * (read_queries.MAX_QUERY_BYTES + 1)
await conn.execute("UPDATE peripheral_sample.demo_datasets SET document=%s", (Jsonb(payload),))
with pytest.raises(HTTPException) as caught:
await read_peripheral_query(scope, args)
assert caught.value.status_code == 413
asyncio.run(check())
def test_task_context_summary_and_sections_with_real_postgresql():
dsn = os.environ.get("MCP_QUERY_TEST_DSN")
if not dsn:
pytest.skip("MCP_QUERY_TEST_DSN must point to a disposable PostgreSQL database")
async def check():
conn = await AsyncConnection.connect(dsn, row_factory=dict_row)
async with conn, conn.transaction(force_rollback=True):
await conn.execute("CREATE SCHEMA workbench")
await conn.execute("""CREATE TABLE workbench.operational_tasks (
project_id uuid,task_id uuid,task_type text,status text,lifecycle_status text,priority text,
revision bigint,execution_generation bigint,current_report_revision bigint,document jsonb)""")
for name, key in (("reports", "revision bigint"), ("plans", "revision bigint"),
("evidence", "evidence_id text"), ("actions", "action_id uuid")):
await conn.execute(f"CREATE TABLE workbench.operational_task_{name} (project_id uuid,task_id uuid,{key},document jsonb)")
await conn.execute("CREATE TABLE workbench.operational_task_runs (project_id uuid,task_id uuid,run_id uuid,generation bigint,status text)")
project, task, run = uuid4(), uuid4(), uuid4()
long_text = "甲😀" * 10000
await conn.execute("INSERT INTO workbench.operational_tasks VALUES (%s,%s,'event_investigation','running','active','normal',1,2,4,%s)",
(project, task, Jsonb({"title": long_text, "source": {"description": long_text}})))
for revision in (3, 4):
await conn.execute("INSERT INTO workbench.operational_task_reports VALUES (%s,%s,%s,%s)",
(project, task, revision, Jsonb({"revision": revision, "report": {"stage": "investigation", "outcome": "pending", "current_conclusion": long_text}})))
await conn.execute("INSERT INTO workbench.operational_task_plans VALUES (%s,%s,1,%s)",
(project, task, Jsonb({"nodes": [{"id": "N1"}]})))
for evidence_id in ("E1", "E2", "E3"):
await conn.execute("INSERT INTO workbench.operational_task_evidence VALUES (%s,%s,%s,%s)",
(project, task, evidence_id, Jsonb({"data_snapshot": {"text": long_text}, "none": None})))
action_id = uuid4()
await conn.execute("INSERT INTO workbench.operational_task_actions VALUES (%s,%s,%s,%s)",
(project, task, action_id, Jsonb({"reason": long_text})))
await conn.execute("INSERT INTO workbench.operational_task_runs VALUES (%s,%s,%s,2,'running')", (project, task, run))
summary = await read_queries.task_context_summary(conn, project_id=project, task_id=task, report_revision=3)
assert summary["selected_report"]["revision"] == 3 and summary["latest_report"]["revision"] == 4
assert summary["task"]["preview_only"] and len(summary["task"]["title_preview"]) == 160
assert len(summary["selected_report"]["current_conclusion_preview"]) == 200
assert "source" not in summary["task"]
for overrides in ({"report_revision": 99}, {"project_id": uuid4()}, {"task_id": uuid4()}):
with pytest.raises(HTTPException) as caught:
await read_queries.task_context_summary(conn, **(dict(project_id=project, task_id=task, report_revision=3) | overrides))
assert caught.value.status_code == 404
async def page(section, path=None, **overrides):
values = dict(project_id=project, task_id=task, section=section, report_revision=None, data_path=path or [], limit=1, offset=0)
return await read_queries.task_context_page(conn, **(values | overrides))
second = await page("evidence", offset=1)
assert second["snapshot_metadata"]["total"] == 3
assert list(second["value"]) == ["E2"]
assert second["value"]["E2"]["data_path"] == ["E2"]
nested = await page("evidence", ["E2"], limit=20)
assert nested["value"]["data_snapshot"]["data_path"] == ["E2", "data_snapshot"]
assert nested["value"]["none"] is None
text = await page("evidence", ["E2", "data_snapshot", "text"], offset=1, limit=2)
assert text["value"] == "😀甲" and text["total"] == len(long_text)
assert (await page("evidence", offset=3))["value"] == {}
assert (await page("evidence", project_id=uuid4()))["value"] == {}
with pytest.raises(HTTPException) as caught:
await page("evidence", ["E2"], task_id=uuid4())
assert caught.value.status_code == 404
report = await page("report", ["report", "current_conclusion"], report_revision=3, offset=2, limit=2)
assert report["value"] == "甲😀"
assert (await page("task"))["nested"]
assert (await page("plan"))["value"]["nodes"]["collection_type"] == "array"
assert (await page("actions"))["value"][str(action_id)]["data_path"] == [str(action_id)]
assert "project_id" not in (await page("run", limit=20))["value"]
for section in ("progress", "access_requests"):
assert (await page(section))["snapshot_metadata"]["available"] is False
assert await read_queries.task_data_scopes(conn, project_id=project, task_id=task, permissions={"scada.view"}) == ["scada_devices", "scada_readings"]
await conn.execute("CREATE TABLE workbench.operational_task_progress (project_id uuid,task_id uuid,run_id uuid,document jsonb)")
await conn.execute("INSERT INTO workbench.operational_task_progress VALUES (%s,%s,%s,%s)", (project, task, run, Jsonb({"content": long_text})))
assert (await page("progress"))["value"]["content"]["collection_type"] == "string"
await conn.execute("CREATE TABLE workbench.operational_task_access_requests (project_id uuid,task_id uuid,access_request_id uuid,status text,document jsonb)")
for grant_project, grant_task, status, scopes in (
(project, task, "approved", ["peripheral_points", "unknown_source"]),
(project, task, "pending", ["scada_readings"]),
(uuid4(), task, "approved", ["peripheral_observations"]),
(project, uuid4(), "approved", ["peripheral_test_data"]),
):
await conn.execute("INSERT INTO workbench.operational_task_access_requests VALUES (%s,%s,%s,%s,%s)",
(grant_project, grant_task, uuid4(), status, Jsonb({"approved_scopes": scopes})))
assert await read_queries.task_data_scopes(conn, project_id=project, task_id=task, permissions=set()) == ["peripheral_points"]
assert (await page("access_requests", limit=20))["snapshot_metadata"]["total"] == 2
# A very wide document must fail before it crosses the DB boundary;
# a smaller page remains readable.
wide = {str(i): "x" * 250 for i in range(200)}
await conn.execute("UPDATE workbench.operational_tasks SET document=%s", (Jsonb(wide),))
with pytest.raises(HTTPException) as caught:
await page("task", limit=200)
assert caught.value.status_code == 413
assert len((await page("task", limit=1))["value"]) == 1
asyncio.run(check())