Files
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

59 lines
3.5 KiB
Python

"""Opt-in PostGIS checks using transaction-local temporary model tables only."""
import asyncio
import os
import pytest
from psycopg import AsyncConnection, sql
from psycopg.rows import dict_row
from app.infra.db.postgresql import spatial_queries
def test_nearby_model_queries_with_real_postgis(monkeypatch):
dsn = os.environ.get("MCP_SPATIAL_TEST_DSN")
if not dsn:
pytest.skip("MCP_SPATIAL_TEST_DSN must point to a PostGIS database; only TEMP tables are written")
monkeypatch.setitem(spatial_queries._MODEL_RELATIONS, "pipe", sql.Identifier("pg_temp", "mcp_model_pipes"))
monkeypatch.setitem(spatial_queries._MODEL_RELATIONS, "junction", sql.Identifier("pg_temp", "mcp_model_junctions"))
async def check():
conn = await AsyncConnection.connect(dsn, row_factory=dict_row)
async with conn, conn.transaction(force_rollback=True):
await conn.execute("CREATE TEMP TABLE mcp_model_pipes (id text PRIMARY KEY, diameter float, geom geometry(LineString,3857)) ON COMMIT DROP")
await conn.execute("CREATE TEMP TABLE mcp_model_junctions (id text PRIMARY KEY, geom geometry(Point,3857)) ON COMMIT DROP")
await conn.execute("CREATE INDEX ON mcp_model_pipes USING gist (geom)")
# The crossing pipe has distant endpoints, while its middle is at the center.
for pipe_id, wkt in [
("crossing", "LINESTRING(121.49 30.9,121.51 30.9)"),
("near-a", "LINESTRING(121.4999 30.9005,121.5001 30.9005)"),
("near-b", "LINESTRING(121.4999 30.9005,121.5001 30.9005)"),
("outside", "LINESTRING(121.4999 30.91,121.5001 30.91)"),
]:
await conn.execute("INSERT INTO mcp_model_pipes VALUES (%s,300,ST_Transform(ST_GeomFromText(%s,4326),3857))", (pipe_id, wkt))
await conn.execute("INSERT INTO mcp_model_pipes VALUES ('missing',300,NULL),('empty',300,ST_GeomFromText('LINESTRING EMPTY',3857))")
await conn.execute("INSERT INTO mcp_model_junctions VALUES ('junction',ST_Transform(ST_SetSRID(ST_MakePoint(121.5,30.9002),4326),3857))")
async def nearby(kind="pipe", **overrides):
values = dict(center=(121.5, 30.9), radius_m=100, limit=10, offset=0)
return await spatial_queries.nearby_elements(conn, kind, **(values | overrides))
rows = await nearby()
assert [row["element_id"] for row in rows] == ["crossing", "near-a", "near-b"]
assert rows[0]["distance_m"] < 1
assert 54 < rows[1]["distance_m"] < 57
assert rows[1]["properties"]["diameter"] == 300 and "geom" not in rows[1]["properties"]
# 55 m geodesic is ~65 m in Web Mercator here: a 60 m radius must include it.
assert len(await nearby(radius_m=60)) == 3
assert len(await nearby(radius_m=50)) == 1
assert [row["element_id"] for row in await nearby(limit=1, offset=2)] == ["near-b"]
assert await nearby(center=(0, 0)) == []
assert [row["element_id"] for row in await nearby("junction")] == ["junction"]
assert await nearby(center=(0, 90)) == []
# A point west of the dateline must find a model east of it.
await conn.execute("INSERT INTO mcp_model_pipes VALUES ('dateline',300,ST_Transform(ST_GeomFromText('LINESTRING(179.9999 30,179.9999 30.0001)',4326),3857))")
assert [row["element_id"] for row in await nearby(center=(-179.9999, 30), radius_m=100)] == ["dateline"]
asyncio.run(check())