新增市場情報 migration 套用演練
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
OoO
2026-05-18 20:05:24 +08:00
parent 48d71c711b
commit 841443f37c
9 changed files with 683 additions and 25 deletions

View File

@@ -484,6 +484,10 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "data-market-intel-opportunity-alert-sequence" in template
assert "data-market-intel-migration" in template
assert "data-market-intel-migration-tables" in template
assert "data-market-intel-migration-drill" in template
assert "data-market-intel-migration-drill-checks" in template
assert "data-market-intel-migration-drill-preapply" in template
assert "data-market-intel-migration-drill-rollback" in template
assert "data-market-intel-approval" in template
assert "data-market-intel-approval-gates" in template
assert "data-market-intel-deploy" in template
@@ -506,6 +510,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "market_intel.market_intel_opportunity_evidence_plan" in template
assert "market_intel.market_intel_opportunity_alert_plan" in template
assert "market_intel.market_intel_migration_blueprint" in template
assert "market_intel.market_intel_migration_apply_drill" in template
assert "market_intel.market_intel_write_approval_runbook" in template
assert "market_intel.market_intel_deployment_readiness" in template
assert "required_manual_steps" in template
@@ -530,7 +535,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_42_alert_review_queue_migration_blueprint"
assert bridge["phase"] == "phase_43_migration_apply_drill_preview"
assert bridge["execute_requested"] is False
assert bridge["read_only_query_executed"] is False
assert bridge["database_connection_opened"] is False
@@ -688,7 +693,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_42_alert_review_queue_migration_blueprint"
assert contract["phase"] == "phase_43_migration_apply_drill_preview"
assert contract["caller"] == "market_intel"
assert contract["contract_ready"] is True
assert contract["blocked_reasons"] == []
@@ -821,7 +826,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_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -834,7 +839,7 @@ 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_42_alert_review_queue_migration_blueprint"
assert gate["phase"] == "phase_43_migration_apply_drill_preview"
assert gate["fetch_requested"] is True
assert gate["manual_fetch_gate_open"] is False
assert gate["network_request_allowed"] is False
@@ -904,7 +909,7 @@ def test_mcp_fetch_gate_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "mcp_fetch_gate_planned"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["fetch_requested"] is False
assert data["network_request_allowed"] is False
assert data["external_network_executed"] is False
@@ -916,7 +921,7 @@ def test_scheduler_plan_preview_blocks_job_attachment():
plan = MarketIntelService().build_scheduler_plan()
assert plan["mode"] == "scheduler_attach_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_to_attach_scheduler"] is False
assert plan["scheduler_attached"] is False
assert plan["scheduler_registration_executed"] is False
@@ -954,7 +959,7 @@ def test_scheduler_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "scheduler_attach_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["scheduler_registration_executed"] is False
assert data["crawler_job_started"] is False
assert data["external_network_executed"] is False
@@ -965,7 +970,7 @@ def test_match_review_plan_preview_blocks_auto_confirm():
plan = MarketIntelService().build_match_review_plan()
assert plan["mode"] == "match_review_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_for_review_queue"] is False
assert plan["review_queue_created"] is False
assert plan["auto_match_executed"] is False
@@ -1001,7 +1006,7 @@ def test_match_review_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "match_review_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["review_queue_created"] is False
assert data["auto_confirm_executed"] is False
assert data["external_network_executed"] is False
@@ -1012,7 +1017,7 @@ def test_opportunity_plan_preview_blocks_alerts_and_ai_summary():
plan = MarketIntelService().build_opportunity_plan()
assert plan["mode"] == "opportunity_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_for_opportunity_queue"] is False
assert plan["opportunity_queue_created"] is False
assert plan["threat_alert_dispatched"] is False
@@ -1053,7 +1058,7 @@ def test_opportunity_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["opportunity_queue_created"] is False
assert data["threat_alert_dispatched"] is False
assert data["ai_summary_generated"] is False
@@ -1064,7 +1069,7 @@ def test_opportunity_scoring_plan_preview_blocks_scoring_and_alerts():
plan = MarketIntelService().build_opportunity_scoring_plan()
assert plan["mode"] == "opportunity_scoring_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_for_scoring_job"] is False
assert plan["scoring_job_created"] is False
assert plan["score_calculation_executed"] is False
@@ -1112,7 +1117,7 @@ def test_opportunity_scoring_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_scoring_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["scoring_job_created"] is False
assert data["score_calculation_executed"] is False
assert data["sample_scores_generated"] is False
@@ -1124,7 +1129,7 @@ def test_opportunity_evidence_plan_preview_blocks_queries_and_alerts():
plan = MarketIntelService().build_opportunity_evidence_plan()
assert plan["mode"] == "opportunity_evidence_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_for_evidence_bundle"] is False
assert plan["evidence_bundle_created"] is False
assert plan["evidence_query_executed"] is False
@@ -1170,7 +1175,7 @@ def test_opportunity_evidence_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_evidence_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["evidence_bundle_created"] is False
assert data["evidence_query_executed"] is False
assert data["sample_evidence_generated"] is False
@@ -1183,7 +1188,7 @@ def test_opportunity_alert_plan_preview_blocks_dispatch_and_llm_calls():
plan = MarketIntelService().build_opportunity_alert_plan()
assert plan["mode"] == "opportunity_alert_plan_preview"
assert plan["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert plan["phase"] == "phase_43_migration_apply_drill_preview"
assert plan["ready_for_alert_candidates"] is False
assert plan["alert_candidate_created"] is False
assert plan["alert_queue_created"] is False
@@ -1268,7 +1273,7 @@ def test_opportunity_alert_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_alert_plan_preview"
assert data["phase"] == "phase_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["alert_candidate_created"] is False
assert data["alert_queue_created"] is False
assert data["review_queue_created"] is False
@@ -1346,7 +1351,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_42_alert_review_queue_migration_blueprint"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -1361,7 +1366,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_42_alert_review_queue_migration_blueprint"
assert readiness["phase"] == "phase_43_migration_apply_drill_preview"
assert readiness["execute_requested"] is False
assert readiness["router_enabled"] is False
assert readiness["external_mcp_complete"] is False
@@ -1754,6 +1759,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["checks"]["opportunity_scoring_plan_preview_safe"] is True
assert readiness["checks"]["opportunity_evidence_plan_preview_safe"] is True
assert readiness["checks"]["opportunity_alert_plan_preview_safe"] is True
assert readiness["checks"]["migration_apply_drill_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"]
@@ -1780,11 +1786,17 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert "/api/market_intel/opportunity_scoring_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/opportunity_evidence_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/opportunity_alert_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/migration_apply_drill" 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
assert readiness["migration_blueprint"]["file_created"] is True
assert readiness["migration_blueprint"]["file_matches_blueprint"] is True
assert readiness["migration_apply_drill"]["mode"] == "migration_apply_drill_preview"
assert readiness["migration_apply_drill"]["migration_executed"] is False
assert readiness["migration_apply_drill"]["rollback_executed"] is False
assert readiness["migration_apply_drill"]["database_write_executed"] is False
assert readiness["migration_apply_drill"]["api_executes_migration"] is False
assert readiness["schema_db_probe"]["read_only_query_executed"] is False
assert readiness["platform_seed_db_diff"]["read_only_query_executed"] is False
assert readiness["legacy_source_bridge"]["read_only_query_executed"] is False
@@ -1898,6 +1910,133 @@ def test_migration_blueprint_is_additive_preview_only():
assert "DROP TABLE IF EXISTS market_alert_review_queue" in blueprint["rollback_sql"]
def test_migration_apply_drill_planned_is_safe_and_manual_only():
drill = MarketIntelService().build_migration_apply_drill()
assert drill["mode"] == "migration_apply_drill_preview"
assert drill["phase"] == "phase_43_migration_apply_drill_preview"
assert drill["execute_requested"] is False
assert drill["schema_state"] == "planned_no_db_probe"
assert drill["drill_ready_for_operator_review"] is True
assert drill["ready_for_manual_apply_review"] is False
assert drill["ready_to_apply_migration"] is False
assert drill["read_only_query_executed"] is False
assert drill["database_connection_opened"] is False
assert drill["database_session_created"] is False
assert drill["database_write_executed"] is False
assert drill["database_commit_executed"] is False
assert drill["migration_executed"] is False
assert drill["rollback_executed"] is False
assert drill["api_executes_migration"] is False
assert drill["api_executes_rollback"] is False
assert drill["checks"]["migration_file_matches_blueprint"] is True
assert drill["checks"]["forward_sql_additive_only"] is True
assert "read_only_db_probe_not_executed" in drill["blocked_reasons"]
assert "api_never_runs_migration" in drill["blocked_reasons"]
assert drill["rollback_drill"]["rollback_executed"] is False
assert drill["rollback_drill"]["requires_manual_approval"] is True
assert "market_alert_review_queue" in drill["rollback_drill"]["rollback_table_order"]
assert len(drill["pre_apply_checklist"]) >= 6
assert len(drill["post_apply_verification"]) >= 4
assert len(drill["risk_register"]) >= 4
def test_migration_apply_drill_sqlite_read_only_reports_already_applied():
service = MarketIntelService()
seed_rows = service.build_platform_seed_plan()["seeds"]
engine = create_engine("sqlite:///:memory:")
with engine.begin() as conn:
conn.execute(
text(
"""
CREATE TABLE market_platforms (
code TEXT PRIMARY KEY,
name TEXT,
base_url TEXT,
enabled BOOLEAN,
crawl_policy_json TEXT
)
"""
)
)
for table_name in MARKET_INTEL_TABLES:
if table_name == "market_platforms":
continue
conn.execute(text(f"CREATE TABLE {table_name} (id INTEGER PRIMARY KEY)"))
for seed in seed_rows:
conn.execute(
text(
"""
INSERT INTO market_platforms
(code, name, base_url, enabled, crawl_policy_json)
VALUES
(:code, :name, :base_url, :enabled, :crawl_policy_json)
"""
),
{
"code": seed["code"],
"name": seed["name"],
"base_url": seed["base_url"],
"enabled": seed["enabled"],
"crawl_policy_json": json.dumps(
seed["crawl_policy_json"],
ensure_ascii=False,
sort_keys=True,
),
},
)
drill = service.build_migration_apply_drill(
execute_requested=True,
schema_engine=engine,
seed_diff_engine=engine,
database_type="sqlite",
)
assert drill["mode"] == "migration_apply_drill_preview"
assert drill["execute_requested"] is True
assert drill["read_only_query_executed"] is True
assert drill["database_connection_opened"] is True
assert drill["schema_state"] == "already_applied"
assert drill["schema_db_probe_summary"]["schema_tables_exist"] is True
assert drill["schema_db_probe_summary"]["missing_tables"] == []
assert drill["platform_seed_db_diff_summary"]["seed_rows_ready"] is True
assert set(drill["platform_seed_db_diff_summary"]["matching_codes"]) == {
"momo",
"pchome",
"coupang",
"shopee",
}
assert drill["database_session_created"] is False
assert drill["database_write_executed"] is False
assert drill["database_commit_executed"] is False
assert drill["migration_executed"] is False
assert "market_schema_already_present_apply_not_required" in drill["blocked_reasons"]
def test_migration_apply_drill_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/migration_apply_drill?execute=false")
data = response.get_json()
assert response.status_code == 200
assert data["mode"] == "migration_apply_drill_preview"
assert data["phase"] == "phase_43_migration_apply_drill_preview"
assert data["execute_requested"] is False
assert data["migration_executed"] is False
assert data["rollback_executed"] is False
assert data["database_write_executed"] is False
assert data["api_executes_migration"] is False
def test_seed_writer_cli_status_blocks_real_write():
status = MarketIntelService().build_seed_writer_cli_status(
platform_code="all",