新增市場情報樣本結果審核預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m4s

This commit is contained in:
OoO
2026-05-19 00:28:20 +08:00
parent 2de9386a40
commit 5908d1bdca
9 changed files with 624 additions and 34 deletions

View File

@@ -20,6 +20,7 @@ from services.market_intel.mcp_deploy_preflight import build_mcp_deploy_prefligh
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.live_db_inventory import build_live_db_inventory_preview
from services.market_intel.manual_sample_review import evaluate_manual_sample_result
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
@@ -510,6 +511,11 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "data-market-intel-sample-acceptance-checks" in template
assert "data-market-intel-sample-acceptance-fields" in template
assert "data-market-intel-sample-acceptance-rules" in template
assert "data-market-intel-sample-review" in template
assert "data-market-intel-sample-review-checks" in template
assert "data-market-intel-sample-review-findings" in template
assert "data-market-intel-sample-review-actions" in template
assert "data-market-intel-sample-review-boundaries" in template
assert "data-market-intel-approval" in template
assert "data-market-intel-approval-gates" in template
assert "data-market-intel-deploy" in template
@@ -527,6 +533,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "market_intel.market_intel_mcp_fetch_gate" in template
assert "market_intel.market_intel_manual_sample_plan" in template
assert "market_intel.market_intel_manual_sample_acceptance" in template
assert "market_intel.market_intel_manual_sample_review" in template
assert "market_intel.market_intel_scheduler_plan" in template
assert "market_intel.market_intel_match_review_plan" in template
assert "market_intel.market_intel_opportunity_plan" in template
@@ -562,7 +569,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_48_manual_sample_acceptance_contract"
assert bridge["phase"] == "phase_49_manual_sample_result_review"
assert bridge["execute_requested"] is False
assert bridge["read_only_query_executed"] is False
assert bridge["database_connection_opened"] is False
@@ -720,7 +727,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_48_manual_sample_acceptance_contract"
assert contract["phase"] == "phase_49_manual_sample_result_review"
assert contract["caller"] == "market_intel"
assert contract["contract_ready"] is True
assert contract["blocked_reasons"] == []
@@ -853,7 +860,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -866,7 +873,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_48_manual_sample_acceptance_contract"
assert gate["phase"] == "phase_49_manual_sample_result_review"
assert gate["fetch_requested"] is True
assert gate["manual_fetch_gate_open"] is False
assert gate["network_request_allowed"] is False
@@ -936,7 +943,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["fetch_requested"] is False
assert data["network_request_allowed"] is False
assert data["external_network_executed"] is False
@@ -948,7 +955,7 @@ def test_manual_sample_plan_preview_blocks_fetch_and_write():
plan = MarketIntelService().build_manual_sample_plan()
assert plan["mode"] == "manual_sample_fetch_plan_preview"
assert plan["phase"] == "phase_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_manual_sample_fetch"] is False
assert plan["sample_fetch_executed"] is False
assert plan["external_network_executed"] is False
@@ -996,7 +1003,7 @@ def test_manual_sample_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "manual_sample_fetch_plan_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["sample_fetch_executed"] is False
assert data["external_network_executed"] is False
assert data["database_write_executed"] is False
@@ -1007,7 +1014,7 @@ def test_manual_sample_acceptance_preview_blocks_candidate_import():
acceptance = MarketIntelService().build_manual_sample_acceptance()
assert acceptance["mode"] == "manual_sample_acceptance_preview"
assert acceptance["phase"] == "phase_48_manual_sample_acceptance_contract"
assert acceptance["phase"] == "phase_49_manual_sample_result_review"
assert acceptance["contract_ready"] is True
assert acceptance["sample_result_loaded"] is False
assert acceptance["sample_result_accepted"] is False
@@ -1049,7 +1056,7 @@ def test_manual_sample_acceptance_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "manual_sample_acceptance_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["sample_result_loaded"] is False
assert data["candidate_import_allowed"] is False
assert data["external_network_executed"] is False
@@ -1057,11 +1064,135 @@ def test_manual_sample_acceptance_route_is_preview_only():
assert data["scheduler_attached"] is False
def test_manual_sample_review_preview_is_planned_until_result_loaded():
review = MarketIntelService().build_manual_sample_review()
assert review["mode"] == "manual_sample_review_preview"
assert review["phase"] == "phase_49_manual_sample_result_review"
assert review["contract_ready"] is True
assert review["sample_result_loaded"] is False
assert review["sample_result_reviewed"] is False
assert review["sample_result_accepted"] is False
assert review["ready_for_candidate_preview"] is False
assert review["candidate_import_allowed"] is False
assert review["external_network_executed"] is False
assert review["database_connection_opened"] is False
assert review["database_session_created"] is False
assert review["database_write_executed"] is False
assert review["database_commit_executed"] is False
assert review["scheduler_attached"] is False
assert review["writes_executed"] is False
assert review["would_write_database"] is False
assert review["review_result"] == "planned_no_sample_result"
assert "sample_result_not_loaded" in review["blocked_reasons"]
assert "candidate_import_still_blocked_until_operator_approval" in review["blocked_reasons"]
assert "do_not_fetch_external_pages_from_review_api" in review["safe_boundaries"]
assert "do_not_write_market_tables_from_review_preview" in review["safe_boundaries"]
def test_manual_sample_review_evaluator_accepts_good_result_without_import():
acceptance = MarketIntelService().build_manual_sample_acceptance()
sample_result = {
"batch_id": "sample-batch-1",
"platform_code": "pchome",
"source_key": "homepage",
"source_url": "https://24h.pchome.com.tw/",
"status": "fetched",
"status_code": 200,
"content_length": 1200,
"page_hash": "a" * 64,
"title": "PChome 活動",
"diagnostics": {
"link_count": 3,
"same_host_link_count": 2,
"campaign_link_candidates": [
{
"confidence_band": "high",
"score": 82,
"url": "https://24h.pchome.com.tw/activity/sample",
"text": "品牌活動",
},
],
},
}
result = evaluate_manual_sample_result(sample_result, acceptance)
assert result["sample_result_loaded"] is True
assert result["sample_result_reviewed"] is True
assert result["sample_result_accepted"] is True
assert result["ready_for_candidate_preview"] is True
assert result["candidate_import_allowed"] is False
assert result["review_result"] == "accepted_for_candidate_preview"
assert all(check["passed"] for check in result["review_checks"])
assert result["review_findings"] == []
assert result["candidate_summary"]["candidate_count"] == 1
assert result["candidate_summary"]["accepted_candidate_count"] == 1
def test_manual_sample_review_evaluator_rejects_bad_result():
acceptance = MarketIntelService().build_manual_sample_acceptance()
sample_result = {
"batch_id": "sample-batch-2",
"platform_code": "shopee",
"source_key": "mall",
"source_url": "https://shopee.tw/mall",
"status": "blocked",
"status_code": 403,
"content_length": 120,
"page_hash": "short",
"title": "",
"diagnostics": {
"link_count": 0,
"same_host_link_count": 0,
"campaign_link_candidates": [],
},
}
result = evaluate_manual_sample_result(sample_result, acceptance)
assert result["sample_result_loaded"] is True
assert result["sample_result_reviewed"] is True
assert result["sample_result_accepted"] is False
assert result["ready_for_candidate_preview"] is False
assert result["candidate_import_allowed"] is False
assert result["review_result"] == "rejected_sample_result"
assert {item["key"] for item in result["review_findings"]} >= {
"http_status_ok",
"content_has_body",
"candidate_quality_reviewed",
}
def test_manual_sample_review_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/manual_sample_review")
data = response.get_json()
assert response.status_code == 200
assert data["mode"] == "manual_sample_review_preview"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["sample_result_loaded"] is False
assert data["sample_result_reviewed"] is False
assert data["candidate_import_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_scheduler_plan_preview_blocks_job_attachment():
plan = MarketIntelService().build_scheduler_plan()
assert plan["mode"] == "scheduler_attach_plan_preview"
assert plan["phase"] == "phase_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_to_attach_scheduler"] is False
assert plan["scheduler_attached"] is False
assert plan["scheduler_registration_executed"] is False
@@ -1099,7 +1230,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["scheduler_registration_executed"] is False
assert data["crawler_job_started"] is False
assert data["external_network_executed"] is False
@@ -1110,7 +1241,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_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_review_queue"] is False
assert plan["review_queue_created"] is False
assert plan["auto_match_executed"] is False
@@ -1146,7 +1277,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["review_queue_created"] is False
assert data["auto_confirm_executed"] is False
assert data["external_network_executed"] is False
@@ -1157,7 +1288,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_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_opportunity_queue"] is False
assert plan["opportunity_queue_created"] is False
assert plan["threat_alert_dispatched"] is False
@@ -1198,7 +1329,7 @@ def test_opportunity_plan_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "opportunity_plan_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["opportunity_queue_created"] is False
assert data["threat_alert_dispatched"] is False
assert data["ai_summary_generated"] is False
@@ -1209,7 +1340,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_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_scoring_job"] is False
assert plan["scoring_job_created"] is False
assert plan["score_calculation_executed"] is False
@@ -1257,7 +1388,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["scoring_job_created"] is False
assert data["score_calculation_executed"] is False
assert data["sample_scores_generated"] is False
@@ -1269,7 +1400,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_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_evidence_bundle"] is False
assert plan["evidence_bundle_created"] is False
assert plan["evidence_query_executed"] is False
@@ -1315,7 +1446,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["evidence_bundle_created"] is False
assert data["evidence_query_executed"] is False
assert data["sample_evidence_generated"] is False
@@ -1328,7 +1459,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_48_manual_sample_acceptance_contract"
assert plan["phase"] == "phase_49_manual_sample_result_review"
assert plan["ready_for_alert_candidates"] is False
assert plan["alert_candidate_created"] is False
assert plan["alert_queue_created"] is False
@@ -1413,7 +1544,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["alert_candidate_created"] is False
assert data["alert_queue_created"] is False
assert data["review_queue_created"] is False
@@ -1491,7 +1622,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_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["deployment_actions_executed"] is False
assert data["docker_command_executed"] is False
assert data["ssh_command_executed"] is False
@@ -1506,7 +1637,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_48_manual_sample_acceptance_contract"
assert readiness["phase"] == "phase_49_manual_sample_result_review"
assert readiness["execute_requested"] is False
assert readiness["router_enabled"] is False
assert readiness["external_mcp_complete"] is False
@@ -1896,6 +2027,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["checks"]["scheduler_plan_preview_safe"] is True
assert readiness["checks"]["manual_sample_plan_preview_safe"] is True
assert readiness["checks"]["manual_sample_acceptance_preview_safe"] is True
assert readiness["checks"]["manual_sample_review_preview_safe"] is True
assert readiness["checks"]["match_review_plan_preview_safe"] is True
assert readiness["checks"]["opportunity_plan_preview_safe"] is True
assert readiness["checks"]["opportunity_scoring_plan_preview_safe"] is True
@@ -1928,6 +2060,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert "/api/market_intel/scheduler_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/manual_sample_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/manual_sample_acceptance" in readiness["production_smoke_targets"]
assert "/api/market_intel/manual_sample_review" in readiness["production_smoke_targets"]
assert "/api/market_intel/match_review_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/opportunity_plan" in readiness["production_smoke_targets"]
assert "/api/market_intel/opportunity_scoring_plan" in readiness["production_smoke_targets"]
@@ -1985,6 +2118,13 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["manual_sample_acceptance"]["external_network_executed"] is False
assert readiness["manual_sample_acceptance"]["database_write_executed"] is False
assert readiness["manual_sample_acceptance"]["scheduler_attached"] is False
assert readiness["manual_sample_review"]["mode"] == "manual_sample_review_preview"
assert readiness["manual_sample_review"]["sample_result_loaded"] is False
assert readiness["manual_sample_review"]["sample_result_reviewed"] is False
assert readiness["manual_sample_review"]["candidate_import_allowed"] is False
assert readiness["manual_sample_review"]["external_network_executed"] is False
assert readiness["manual_sample_review"]["database_write_executed"] is False
assert readiness["manual_sample_review"]["scheduler_attached"] is False
assert readiness["scheduler_plan"]["scheduler_registration_executed"] is False
assert readiness["scheduler_plan"]["crawler_job_started"] is False
assert readiness["scheduler_plan"]["database_write_executed"] is False
@@ -2089,7 +2229,7 @@ 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_48_manual_sample_acceptance_contract"
assert drill["phase"] == "phase_49_manual_sample_result_review"
assert drill["execute_requested"] is False
assert drill["schema_state"] == "planned_no_db_probe"
assert drill["drill_ready_for_operator_review"] is True
@@ -2204,7 +2344,7 @@ def test_migration_apply_drill_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "migration_apply_drill_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["execute_requested"] is False
assert data["migration_executed"] is False
assert data["rollback_executed"] is False
@@ -2216,7 +2356,7 @@ def test_migration_catalog_review_planned_is_safe_and_diagnostic():
review = MarketIntelService().build_migration_catalog_review()
assert review["mode"] == "migration_catalog_review_preview"
assert review["phase"] == "phase_48_manual_sample_acceptance_contract"
assert review["phase"] == "phase_49_manual_sample_result_review"
assert review["execute_requested"] is False
assert review["catalog_state"] == "planned_no_probe"
assert review["seed_state"] == "planned_no_probe"
@@ -2331,7 +2471,7 @@ def test_migration_catalog_review_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "migration_catalog_review_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["execute_requested"] is False
assert data["catalog_state"] == "planned_no_probe"
assert data["migration_executed"] is False
@@ -2344,7 +2484,7 @@ def test_migration_live_smoke_planned_is_preview_only():
smoke = MarketIntelService().build_migration_live_smoke()
assert smoke["mode"] == "migration_live_smoke_preview"
assert smoke["phase"] == "phase_48_manual_sample_acceptance_contract"
assert smoke["phase"] == "phase_49_manual_sample_result_review"
assert smoke["execute_requested"] is False
assert smoke["smoke_result"] == "planned_no_execution"
assert smoke["live_smoke_passed"] is False
@@ -2406,7 +2546,7 @@ def test_migration_live_smoke_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "migration_live_smoke_preview"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["execute_requested"] is False
assert data["smoke_result"] == "planned_no_execution"
assert data["migration_executed"] is False
@@ -2419,7 +2559,7 @@ def test_live_db_inventory_planned_is_preview_only():
inventory = MarketIntelService().build_live_db_inventory()
assert inventory["mode"] == "live_db_inventory_planned"
assert inventory["phase"] == "phase_48_manual_sample_acceptance_contract"
assert inventory["phase"] == "phase_49_manual_sample_result_review"
assert inventory["execute_requested"] is False
assert inventory["read_only_query_executed"] is False
assert inventory["database_connection_opened"] is False
@@ -2563,7 +2703,7 @@ def test_live_db_inventory_route_is_preview_only():
assert response.status_code == 200
assert data["mode"] == "live_db_inventory_planned"
assert data["phase"] == "phase_48_manual_sample_acceptance_contract"
assert data["phase"] == "phase_49_manual_sample_result_review"
assert data["execute_requested"] is False
assert data["read_only_query_executed"] is False
assert data["database_write_executed"] is False