feat(market-intel): 新增既有資料橋接預覽
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
All checks were successful
CD Pipeline / deploy (push) Successful in 1m2s
This commit is contained in:
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user