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
+150
View File
@@ -0,0 +1,150 @@
import asyncio
from contextlib import asynccontextmanager
from types import SimpleNamespace
from unittest.mock import AsyncMock, Mock
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from app.api import health
@pytest.fixture
def probe_app(monkeypatch):
app = FastAPI()
app.include_router(health.router)
app.state.startup_complete = True
app.state.mcp_runtime = SimpleNamespace(ready=True)
database_check = AsyncMock(return_value=True)
monkeypatch.setattr(health, "metadata_database_ready", database_check)
return app, TestClient(app), database_check
@pytest.mark.parametrize("runtime", [None, SimpleNamespace(ready=True)])
def test_ready_accepts_disabled_or_started_mcp(probe_app, runtime):
app, client, database_check = probe_app
app.state.mcp_runtime = runtime
response = client.get("/ready")
assert response.status_code == 200
assert response.json() == {
"status": "ready",
"checks": {
"startup": "ready", "metadata_database": "ready",
"mcp_transport": "disabled" if runtime is None else "ready",
},
}
assert response.headers["cache-control"] == "no-store"
database_check.assert_awaited_once_with()
def test_startup_and_shutdown_are_not_ready_without_touching_database(probe_app):
app, client, database_check = probe_app
app.state.startup_complete = False
response = client.get("/ready")
assert response.status_code == 503
assert response.json()["checks"]["startup"] == "not_ready"
assert response.json()["checks"]["metadata_database"] == "not_checked"
database_check.assert_not_awaited()
@pytest.mark.parametrize("component", ["metadata_database", "mcp_transport"])
def test_dependency_not_ready_returns_503_without_breaking_liveness(probe_app, component):
app, client, database_check = probe_app
if component == "metadata_database":
database_check.return_value = False
else:
app.state.mcp_runtime.ready = False
response = client.get("/ready")
assert response.status_code == 503
assert response.json()["status"] == "not_ready"
assert response.json()["checks"][component] in {"unavailable", "not_ready"}
database_check.reset_mock()
response = client.get("/health")
assert response.status_code == 200
assert response.json() == {"status": "ok"}
database_check.assert_not_awaited()
@pytest.mark.parametrize("outcome", ["ready", "error", "timeout"])
def test_metadata_probe_runs_bounded_select_and_sanitizes_errors(monkeypatch, outcome):
queries = []
closed = []
async def execute(query):
queries.append(str(query))
if outcome == "error":
raise RuntimeError("postgresql://private:secret@internal/database")
if outcome == "timeout":
await asyncio.Event().wait()
return SimpleNamespace(scalar_one=lambda: 1)
@asynccontextmanager
async def connect():
try:
yield SimpleNamespace(execute=execute)
finally:
closed.append(True)
monkeypatch.setattr(health, "engine", SimpleNamespace(connect=connect))
monkeypatch.setattr(health, "METADATA_TIMEOUT_SECONDS", 0.01)
assert asyncio.run(health.metadata_database_ready()) is (outcome == "ready")
assert queries == ["SELECT 1"]
assert closed == [True]
@pytest.mark.parametrize("startup_fails", [False, True])
def test_application_lifecycle_marks_readiness_before_cleanup(monkeypatch, startup_fails):
from app import main
app = FastAPI()
@asynccontextmanager
async def mcp_lifespan():
assert app.state.startup_complete is False
if startup_fails:
raise RuntimeError("MCP startup failed")
try:
yield
finally:
assert app.state.startup_complete is False
monkeypatch.setattr(main, "mcp_runtime", SimpleNamespace(lifespan=mcp_lifespan))
for owner, method in [
(main.workbench_event_hubs, "close_all"),
(main.project_connection_manager, "close_all"),
(main, "close_metadata_engine"),
]:
monkeypatch.setattr(owner, method, AsyncMock())
monkeypatch.setattr(main, "close_all_timescale_pools", Mock())
monkeypatch.setattr(main, "close_all_project_pools", Mock())
async def run():
if startup_fails:
with pytest.raises(RuntimeError, match="MCP startup failed"):
async with main.lifespan(app):
pytest.fail("Failed startup must not yield")
else:
async with main.lifespan(app):
assert app.state.startup_complete is True
assert app.state.startup_complete is False
asyncio.run(run())
def test_mcp_runtime_tracks_its_actual_lifecycle():
from app.mcp.application import McpQueryApplication
from app.mcp.config import McpSettings
runtime = McpQueryApplication(McpSettings(
enabled=True, issuer_uri="http://localhost/realms/water",
resource_uri="http://localhost/mcp", _env_file=None,
))
async def run():
assert runtime.ready is False
async with runtime.lifespan():
assert runtime.ready is True
assert runtime.ready is False
asyncio.run(run())