feat(market-intel): add external mcp preflight
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
OoO
2026-05-18 14:51:47 +08:00
parent 02339c93e0
commit 6f68178959
8 changed files with 400 additions and 8 deletions

View File

@@ -15,6 +15,7 @@ from services.market_intel.candidate_preview import build_candidate_preview_from
from services.market_intel.discovery_runner import ManualDiscoveryRunner
from services.market_intel.html_diagnostics import parse_html_diagnostics
from services.market_intel.mcp_contract import build_mcp_tool_contract_preview
from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_preflight_plan
from services.market_intel.mcp_readiness import build_mcp_readiness_plan
from services.market_intel.platform_seed_db_diff import build_platform_seed_db_diff_plan
from services.market_intel.schema_db_probe import build_schema_db_probe_plan
@@ -388,6 +389,11 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "data-market-intel-mcp-checks" in template
assert "data-market-intel-mcp-tools" in template
assert "market_intel_contract" in template
assert "data-market-intel-mcp-preflight" in template
assert "data-market-intel-mcp-preflight-env" in template
assert "data-market-intel-mcp-preflight-services" in template
assert "data-market-intel-mcp-preflight-ports" in template
assert "data-market-intel-mcp-preflight-fallback" in template
assert "data-market-intel-migration" in template
assert "data-market-intel-migration-tables" in template
assert "data-market-intel-approval" in template
@@ -402,6 +408,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "market_intel.market_intel_platform_seed_db_diff" in template
assert "market_intel.market_intel_legacy_source_bridge" in template
assert "market_intel.market_intel_mcp_readiness" in template
assert "market_intel.market_intel_mcp_deploy_preflight" in template
assert "market_intel.market_intel_migration_blueprint" in template
assert "market_intel.market_intel_write_approval_runbook" in template
assert "market_intel.market_intel_deployment_readiness" in template
@@ -427,7 +434,7 @@ def test_legacy_source_bridge_default_is_planned_only():
bridge = MarketIntelService().build_legacy_source_bridge()
assert bridge["mode"] == "legacy_source_bridge_planned"
assert bridge["phase"] == "phase_29_internal_mcp_contract_preview"
assert bridge["phase"] == "phase_30_external_mcp_preflight_preview"
assert bridge["execute_requested"] is False
assert bridge["read_only_query_executed"] is False
assert bridge["database_connection_opened"] is False
@@ -585,7 +592,7 @@ def test_mcp_tool_contract_preview_is_read_only_and_whitelisted():
contract = MarketIntelService().build_mcp_tool_contract()
assert contract["mode"] == "mcp_tool_contract_preview"
assert contract["phase"] == "phase_29_internal_mcp_contract_preview"
assert contract["phase"] == "phase_30_external_mcp_preflight_preview"
assert contract["caller"] == "market_intel"
assert contract["contract_ready"] is True
assert contract["blocked_reasons"] == []
@@ -642,13 +649,79 @@ def test_mcp_tool_contract_route_is_preview_only():
assert data["scheduler_attached"] is False
def test_mcp_deploy_preflight_blocks_without_required_env():
preflight = build_mcp_deploy_preflight_plan(env={})
assert preflight["mode"] == "mcp_external_deploy_preflight_preview"
assert preflight["compose_file_present"] is True
assert preflight["ready_to_start_stack"] is False
assert preflight["checks"]["compose_file_present"] is True
assert preflight["checks"]["all_expected_services_declared"] is True
assert preflight["checks"]["all_expected_containers_declared"] is True
assert preflight["checks"]["all_public_mcp_ports_localhost_only"] is True
assert preflight["checks"]["required_env_vars_present"] is False
assert preflight["checks"]["router_flag_still_off_before_health"] is True
assert "required_env_vars_present" in preflight["blocked_reasons"]
assert preflight["operator_command_preview"] == "docker compose -f docker-compose.mcp.yml up -d"
assert preflight["deployment_actions_executed"] is False
assert preflight["docker_command_executed"] is False
assert preflight["ssh_command_executed"] is False
assert preflight["database_session_created"] is False
assert preflight["database_write_executed"] is False
assert preflight["database_commit_executed"] is False
assert preflight["external_network_executed"] is False
assert preflight["scheduler_attached"] is False
assert preflight["would_write_database"] is False
def test_mcp_deploy_preflight_ready_when_env_contract_is_present():
preflight = build_mcp_deploy_preflight_plan(
env={
"MCP_POSTGRES_PASSWORD": "secret",
"TAVILY_API_KEY": "tavily",
"EXA_API_KEY": "exa",
"MCP_ROUTER_ENABLED": "false",
}
)
assert preflight["ready_to_start_stack"] is True
assert preflight["blocked_reasons"] == []
assert preflight["checks"]["required_env_vars_present"] is True
assert preflight["checks"]["router_flag_still_off_before_health"] is True
assert all(item["localhost_only"] for item in preflight["port_statuses"])
def test_mcp_deploy_preflight_route_is_preview_only():
from routes.market_intel_routes import market_intel_bp
app = Flask(__name__)
app.secret_key = "test-secret"
app.register_blueprint(market_intel_bp)
client = app.test_client()
with client.session_transaction() as session:
session["logged_in"] = True
response = client.get("/api/market_intel/mcp_deploy_preflight")
data = response.get_json()
assert response.status_code == 200
assert data["mode"] == "mcp_external_deploy_preflight_preview"
assert data["phase"] == "phase_30_external_mcp_preflight_preview"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
assert data["database_write_executed"] is False
assert data["external_network_executed"] is False
assert data["scheduler_attached"] is False
def test_mcp_readiness_default_is_planned_only(monkeypatch):
monkeypatch.delenv("MCP_ROUTER_ENABLED", raising=False)
readiness = MarketIntelService().build_mcp_readiness()
assert readiness["mode"] == "mcp_readiness_planned"
assert readiness["phase"] == "phase_29_internal_mcp_contract_preview"
assert readiness["phase"] == "phase_30_external_mcp_preflight_preview"
assert readiness["execute_requested"] is False
assert readiness["router_enabled"] is False
assert readiness["external_mcp_complete"] is False
@@ -1032,6 +1105,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["checks"]["legacy_source_bridge_planned_safe"] is True
assert readiness["checks"]["mcp_readiness_planned_safe"] is True
assert readiness["checks"]["mcp_tool_contract_ready"] is True
assert readiness["checks"]["mcp_deploy_preflight_preview_safe"] is True
assert readiness["checks"]["writer_plan_dry_run_only"] is True
assert readiness["writer_plan_summary"]["writes_executed"] is False
assert "readiness_checks_not_all_passed" not in readiness["blocked_reasons"]
@@ -1049,6 +1123,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert "/api/market_intel/legacy_source_bridge" in readiness["production_smoke_targets"]
assert "/api/market_intel/mcp_readiness" in readiness["production_smoke_targets"]
assert "/api/market_intel/mcp_tool_contract" in readiness["production_smoke_targets"]
assert "/api/market_intel/mcp_deploy_preflight" in readiness["production_smoke_targets"]
assert readiness["write_approval_runbook"]["ready_for_real_write"] is False
assert readiness["write_approval_runbook"]["writes_executed"] is False
assert readiness["migration_blueprint"]["migration_executed"] is False
@@ -1061,6 +1136,8 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["mcp_readiness"]["telemetry"]["read_only_query_executed"] is False
assert readiness["mcp_tool_contract"]["contract_ready"] is True
assert readiness["mcp_tool_contract"]["database_write_executed"] is False
assert readiness["mcp_deploy_preflight"]["deployment_actions_executed"] is False
assert readiness["mcp_deploy_preflight"]["docker_command_executed"] is False
def test_write_approval_runbook_is_read_only_and_blocks_real_write():