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.
217 lines
15 KiB
Python
217 lines
15 KiB
Python
"""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())
|