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.
59 lines
3.5 KiB
Python
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())
|