feat(market-intel): gate manual fetch behind mcp readiness
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m6s
This commit is contained in:
@@ -17,6 +17,7 @@ from services.market_intel.html_diagnostics import parse_html_diagnostics
|
||||
from services.market_intel.mcp_activation_runbook import build_mcp_activation_runbook_preview
|
||||
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_fetch_gate import build_mcp_fetch_gate_preview
|
||||
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
|
||||
@@ -99,6 +100,41 @@ def test_manual_discovery_fetch_is_blocked_when_flags_are_off():
|
||||
assert result["runs"][0]["status"] == "blocked"
|
||||
assert result["runs"][0]["network_allowed"] is False
|
||||
assert result["runs"][0]["database_write_allowed"] is False
|
||||
assert result["mcp_fetch_gate"]["network_request_allowed"] is False
|
||||
assert result["runs"][0]["network_gate"]["manual_fetch_gate_open"] is False
|
||||
|
||||
|
||||
def test_manual_runner_fetch_can_be_blocked_by_mcp_gate_even_when_flags_allow():
|
||||
class RuntimeStatus:
|
||||
enabled = True
|
||||
crawler_enabled = True
|
||||
|
||||
called = {"count": 0}
|
||||
|
||||
def fake_get(*args, **kwargs):
|
||||
called["count"] += 1
|
||||
raise AssertionError("MCP gate 關閉時不應發 HTTP request")
|
||||
|
||||
gate = {
|
||||
"manual_fetch_gate_open": False,
|
||||
"network_request_allowed": False,
|
||||
"blocked_reasons": ["mcp_router_enabled"],
|
||||
"operator_message": "人工 fetch 仍被 MCP gate 阻擋",
|
||||
}
|
||||
adapter = get_adapter("momo")
|
||||
runner = ManualDiscoveryRunner(
|
||||
runtime_status=RuntimeStatus(),
|
||||
http_get=fake_get,
|
||||
network_allowed_override=False,
|
||||
network_gate=gate,
|
||||
)
|
||||
result = runner.run(adapter, fetch=True).to_dict()
|
||||
|
||||
assert called["count"] == 0
|
||||
assert result["status"] == "blocked"
|
||||
assert result["network_allowed"] is False
|
||||
assert result["network_gate"]["blocked_reasons"] == ["mcp_router_enabled"]
|
||||
assert "MCP gate" in result["error_message"]
|
||||
|
||||
|
||||
def test_manual_runner_fetch_uses_injected_http_get_when_allowed():
|
||||
@@ -299,6 +335,8 @@ def test_candidate_preview_default_is_empty_and_does_not_call_network():
|
||||
assert preview["candidate_count"] == 0
|
||||
assert preview["database_write_allowed"] is False
|
||||
assert preview["scheduler_attached"] is False
|
||||
assert preview["mcp_fetch_gate_open"] is False
|
||||
assert preview["mcp_fetch_gate"]["network_request_allowed"] is False
|
||||
assert preview["run_statuses"][0]["status"] == "planned"
|
||||
|
||||
|
||||
@@ -314,6 +352,8 @@ def test_candidate_preview_fetch_is_blocked_when_flags_are_off():
|
||||
assert called["count"] == 0
|
||||
assert preview["candidate_count"] == 0
|
||||
assert preview["run_statuses"][0]["status"] == "blocked"
|
||||
assert preview["mcp_fetch_gate_open"] is False
|
||||
assert "market_intel_enabled" in preview["mcp_fetch_gate"]["blocked_reasons"]
|
||||
|
||||
|
||||
def test_candidate_preview_aggregates_and_filters_by_band():
|
||||
@@ -399,6 +439,10 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
|
||||
assert "data-market-intel-mcp-activation-stages" in template
|
||||
assert "data-market-intel-mcp-activation-safety" in template
|
||||
assert "data-market-intel-mcp-activation-fallback" in template
|
||||
assert "data-market-intel-mcp-fetch-gate" in template
|
||||
assert "data-market-intel-mcp-fetch-gate-checks" in template
|
||||
assert "data-market-intel-mcp-fetch-gate-sequence" in template
|
||||
assert "data-market-intel-mcp-fetch-gate-readiness" in template
|
||||
assert "data-market-intel-migration" in template
|
||||
assert "data-market-intel-migration-tables" in template
|
||||
assert "data-market-intel-approval" in template
|
||||
@@ -415,6 +459,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
|
||||
assert "market_intel.market_intel_mcp_readiness" in template
|
||||
assert "market_intel.market_intel_mcp_deploy_preflight" in template
|
||||
assert "market_intel.market_intel_mcp_activation_runbook" in template
|
||||
assert "market_intel.market_intel_mcp_fetch_gate" 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
|
||||
@@ -440,7 +485,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_31_mcp_activation_runbook_preview"
|
||||
assert bridge["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert bridge["execute_requested"] is False
|
||||
assert bridge["read_only_query_executed"] is False
|
||||
assert bridge["database_connection_opened"] is False
|
||||
@@ -598,7 +643,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_31_mcp_activation_runbook_preview"
|
||||
assert contract["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert contract["caller"] == "market_intel"
|
||||
assert contract["contract_ready"] is True
|
||||
assert contract["blocked_reasons"] == []
|
||||
@@ -731,7 +776,7 @@ def test_mcp_activation_runbook_route_is_preview_only():
|
||||
|
||||
assert response.status_code == 200
|
||||
assert data["mode"] == "mcp_activation_runbook_preview"
|
||||
assert data["phase"] == "phase_31_mcp_activation_runbook_preview"
|
||||
assert data["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert data["deployment_actions_executed"] is False
|
||||
assert data["docker_command_executed"] is False
|
||||
assert data["ssh_command_executed"] is False
|
||||
@@ -740,6 +785,88 @@ def test_mcp_activation_runbook_route_is_preview_only():
|
||||
assert data["scheduler_attached"] is False
|
||||
|
||||
|
||||
def test_mcp_fetch_gate_default_blocks_external_fetch():
|
||||
gate = MarketIntelService().build_mcp_fetch_gate(fetch_requested=True)
|
||||
|
||||
assert gate["mode"] == "mcp_fetch_gate_planned"
|
||||
assert gate["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert gate["fetch_requested"] is True
|
||||
assert gate["manual_fetch_gate_open"] is False
|
||||
assert gate["network_request_allowed"] is False
|
||||
assert gate["would_use_external_network"] is False
|
||||
assert gate["database_session_created"] is False
|
||||
assert gate["database_write_executed"] is False
|
||||
assert gate["database_commit_executed"] is False
|
||||
assert gate["external_network_executed"] is False
|
||||
assert gate["scheduler_attached"] is False
|
||||
assert gate["writes_executed"] is False
|
||||
assert gate["would_write_database"] is False
|
||||
assert "market_intel_enabled" in gate["blocked_reasons"]
|
||||
assert "market_intel_crawler_enabled" in gate["blocked_reasons"]
|
||||
assert "mcp_readiness_executed" in gate["blocked_reasons"]
|
||||
assert "mcp_router_enabled" in gate["blocked_reasons"]
|
||||
|
||||
|
||||
def test_mcp_fetch_gate_can_open_with_mocked_ready_state():
|
||||
class RuntimeStatus:
|
||||
enabled = True
|
||||
crawler_enabled = True
|
||||
database_write_allowed = False
|
||||
scheduler_attached = False
|
||||
|
||||
readiness = {
|
||||
"mode": "mcp_readiness_read_only",
|
||||
"execute_requested": True,
|
||||
"router_enabled": True,
|
||||
"external_mcp_complete": True,
|
||||
"internal_mcp_complete": True,
|
||||
"market_intel_mcp_integrated": True,
|
||||
"blocked_reasons": [],
|
||||
"readiness_checks": {
|
||||
"market_intel_tool_contract_ready": True,
|
||||
"external_servers_all_healthy": True,
|
||||
},
|
||||
}
|
||||
|
||||
gate = build_mcp_fetch_gate_preview(
|
||||
RuntimeStatus(),
|
||||
fetch_requested=True,
|
||||
execute_readiness=True,
|
||||
readiness=readiness,
|
||||
)
|
||||
|
||||
assert gate["mode"] == "mcp_fetch_gate_read_only"
|
||||
assert gate["manual_fetch_prerequisites_met"] is True
|
||||
assert gate["manual_fetch_gate_open"] is True
|
||||
assert gate["network_request_allowed"] is True
|
||||
assert gate["blocked_reasons"] == []
|
||||
assert gate["database_write_executed"] is False
|
||||
assert gate["scheduler_attached"] is False
|
||||
|
||||
|
||||
def test_mcp_fetch_gate_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_fetch_gate")
|
||||
data = response.get_json()
|
||||
|
||||
assert response.status_code == 200
|
||||
assert data["mode"] == "mcp_fetch_gate_planned"
|
||||
assert data["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert data["fetch_requested"] is False
|
||||
assert data["network_request_allowed"] is False
|
||||
assert data["external_network_executed"] is False
|
||||
assert data["database_write_executed"] is False
|
||||
assert data["scheduler_attached"] is False
|
||||
|
||||
|
||||
def test_mcp_deploy_preflight_blocks_without_required_env():
|
||||
preflight = build_mcp_deploy_preflight_plan(env={})
|
||||
|
||||
@@ -803,7 +930,7 @@ def test_mcp_deploy_preflight_route_is_preview_only():
|
||||
|
||||
assert response.status_code == 200
|
||||
assert data["mode"] == "mcp_external_deploy_preflight_preview"
|
||||
assert data["phase"] == "phase_31_mcp_activation_runbook_preview"
|
||||
assert data["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert data["deployment_actions_executed"] is False
|
||||
assert data["docker_command_executed"] is False
|
||||
assert data["ssh_command_executed"] is False
|
||||
@@ -818,7 +945,7 @@ def test_mcp_readiness_default_is_planned_only(monkeypatch):
|
||||
readiness = MarketIntelService().build_mcp_readiness()
|
||||
|
||||
assert readiness["mode"] == "mcp_readiness_planned"
|
||||
assert readiness["phase"] == "phase_31_mcp_activation_runbook_preview"
|
||||
assert readiness["phase"] == "phase_32_mcp_fetch_gate_preview"
|
||||
assert readiness["execute_requested"] is False
|
||||
assert readiness["router_enabled"] is False
|
||||
assert readiness["external_mcp_complete"] is False
|
||||
@@ -1204,6 +1331,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
|
||||
assert readiness["checks"]["mcp_tool_contract_ready"] is True
|
||||
assert readiness["checks"]["mcp_deploy_preflight_preview_safe"] is True
|
||||
assert readiness["checks"]["mcp_activation_runbook_preview_safe"] is True
|
||||
assert readiness["checks"]["mcp_fetch_gate_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"]
|
||||
@@ -1223,6 +1351,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
|
||||
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 "/api/market_intel/mcp_activation_runbook" in readiness["production_smoke_targets"]
|
||||
assert "/api/market_intel/mcp_fetch_gate" 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
|
||||
@@ -1239,6 +1368,8 @@ def test_deployment_readiness_reports_app_only_release_gate():
|
||||
assert readiness["mcp_deploy_preflight"]["docker_command_executed"] is False
|
||||
assert readiness["mcp_activation_runbook"]["deployment_actions_executed"] is False
|
||||
assert readiness["mcp_activation_runbook"]["docker_command_executed"] is False
|
||||
assert readiness["mcp_fetch_gate"]["network_request_allowed"] is False
|
||||
assert readiness["mcp_fetch_gate"]["external_network_executed"] is False
|
||||
|
||||
|
||||
def test_write_approval_runbook_is_read_only_and_blocks_real_write():
|
||||
|
||||
Reference in New Issue
Block a user