feat(market-intel): 新增既有資料橋接預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s

This commit is contained in:
OoO
2026-05-18 14:19:43 +08:00
parent c021945047
commit bb6a862dbe
9 changed files with 762 additions and 4 deletions

View File

@@ -393,6 +393,7 @@ def test_market_intel_preview_template_uses_safe_fetch_false_endpoint():
assert "market_intel.market_intel_seed_writer_cli_status" in template
assert "market_intel.market_intel_schema_db_probe" in template
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_migration_blueprint" in template
assert "market_intel.market_intel_write_approval_runbook" in template
assert "market_intel.market_intel_deployment_readiness" in template
@@ -414,6 +415,164 @@ def test_market_intel_schema_metadata_contains_all_market_tables():
assert set(MARKET_INTEL_TABLES) <= metadata_tables
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_27_legacy_source_bridge_preview"
assert bridge["execute_requested"] is False
assert bridge["read_only_query_executed"] is False
assert bridge["database_connection_opened"] is False
assert bridge["database_session_created"] is False
assert bridge["database_write_executed"] is False
assert bridge["database_commit_executed"] is False
assert bridge["external_network_executed"] is False
assert bridge["scheduler_attached"] is False
assert bridge["writes_executed"] is False
assert bridge["would_write_database"] is False
assert bridge["source_count"] == 3
assert bridge["existing_source_count"] == 0
assert "execute_false_planned_only" in bridge["blocked_reasons"]
assert {item["table"] for item in bridge["source_summaries"]} == {
"promo_products",
"competitor_prices",
"competitor_price_history",
}
def test_legacy_source_bridge_read_only_sqlite_counts_sources():
engine = create_engine("sqlite:///:memory:")
with engine.begin() as conn:
conn.execute(
text(
"""
CREATE TABLE promo_products (
id INTEGER PRIMARY KEY,
batch_id TEXT,
crawled_at TEXT,
time_slot TEXT,
activity_time_text TEXT,
session_time_text TEXT,
i_code TEXT,
name TEXT,
price INTEGER,
discount_text TEXT,
url TEXT,
image_url TEXT,
previous_price INTEGER,
remain_qty INTEGER,
status_change TEXT,
page_type TEXT
)
"""
)
)
conn.execute(
text(
"""
CREATE TABLE competitor_prices (
id INTEGER PRIMARY KEY,
sku TEXT,
source TEXT,
price NUMERIC,
original_price NUMERIC,
discount_pct INTEGER,
competitor_product_id TEXT,
competitor_product_name TEXT,
match_score NUMERIC,
crawled_at TEXT
)
"""
)
)
conn.execute(
text(
"""
CREATE TABLE competitor_price_history (
id INTEGER PRIMARY KEY,
sku TEXT,
source TEXT,
momo_product_id INTEGER,
momo_price NUMERIC,
price NUMERIC,
original_price NUMERIC,
discount_pct INTEGER,
competitor_product_id TEXT,
competitor_product_name TEXT,
match_score NUMERIC,
crawled_at TEXT
)
"""
)
)
conn.execute(
text(
"""
INSERT INTO promo_products
(batch_id, crawled_at, time_slot, i_code, name, price, discount_text, url, page_type)
VALUES
('batch-1', '2026-05-18 10:00:00', '10:00', 'A001', 'MOMO 商品 A', 399, '8折', 'https://momo/A001', 'edm'),
('batch-1', '2026-05-18 10:01:00', '10:00', 'A002', 'MOMO 商品 B', 499, '9折', 'https://momo/A002', 'edm')
"""
)
)
conn.execute(
text(
"""
INSERT INTO competitor_prices
(sku, source, price, original_price, competitor_product_id, competitor_product_name, match_score, crawled_at)
VALUES
('A001', 'pchome', 379, 459, 'PC-A001', 'PChome 商品 A', 0.82, '2026-05-18 11:00:00')
"""
)
)
conn.execute(
text(
"""
INSERT INTO competitor_price_history
(sku, source, momo_product_id, momo_price, price, original_price, competitor_product_id, competitor_product_name, match_score, crawled_at)
VALUES
('A001', 'pchome', 1, 399, 379, 459, 'PC-A001', 'PChome 商品 A', 0.82, '2026-05-18 11:00:00')
"""
)
)
bridge = MarketIntelService().build_legacy_source_bridge(
execute_requested=True,
engine=engine,
database_type="sqlite",
)
assert bridge["mode"] == "legacy_source_bridge_read_only"
assert bridge["execute_requested"] is True
assert bridge["read_only_query_executed"] is True
assert bridge["database_connection_opened"] is True
assert bridge["database_session_created"] is False
assert bridge["database_write_executed"] is False
assert bridge["database_commit_executed"] is False
assert bridge["external_network_executed"] is False
assert bridge["scheduler_attached"] is False
assert bridge["writes_executed"] is False
assert bridge["would_write_database"] is False
assert bridge["source_tables_ready"] is True
assert bridge["existing_source_count"] == 3
assert bridge["total_existing_rows"] == 4
assert [item["table"] for item in bridge["source_summaries"]] == [
"promo_products",
"competitor_prices",
"competitor_price_history",
]
assert bridge["source_summaries"][0]["row_count"] == 2
assert bridge["source_summaries"][0]["distinct_entity_count"] == 2
assert bridge["source_summaries"][1]["row_count"] == 1
assert bridge["source_summaries"][2]["row_count"] == 1
assert len(bridge["bridge_operations"]) == 4
assert all(
item["write_status"] != "executed"
for item in bridge["bridge_operations"]
)
def test_market_intel_schema_smoke_checks_platform_columns():
smoke = MarketIntelService().build_schema_smoke()["schema_smoke"]
@@ -661,6 +820,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["checks"]["schema_smoke_passed"] is True
assert readiness["checks"]["schema_db_probe_planned_safe"] is True
assert readiness["checks"]["platform_seed_db_diff_planned_safe"] is True
assert readiness["checks"]["legacy_source_bridge_planned_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"]
@@ -675,6 +835,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert "/health" in readiness["production_smoke_targets"]
assert "/api/market_intel/deployment_readiness" in readiness["production_smoke_targets"]
assert "/api/market_intel/platform_seed_db_diff" in readiness["production_smoke_targets"]
assert "/api/market_intel/legacy_source_bridge" 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
@@ -682,6 +843,7 @@ def test_deployment_readiness_reports_app_only_release_gate():
assert readiness["migration_blueprint"]["file_matches_blueprint"] is True
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
def test_write_approval_runbook_is_read_only_and_blocks_real_write():