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:
@@ -0,0 +1,58 @@
|
||||
"""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())
|
||||
Reference in New Issue
Block a user