feat(growth): automate sales source reconciliation
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
Some checks failed
CD Pipeline / deploy (push) Has been cancelled
This commit is contained in:
@@ -177,12 +177,19 @@ def test_no_candidate_with_missing_freshness_is_blocked_and_receipted(monkeypatc
|
||||
_disable_remote_sources(monkeypatch)
|
||||
monkeypatch.delenv("PCHOME_SALES_LOCAL_DROP_DIR", raising=False)
|
||||
monkeypatch.setattr(import_service, "drive_service", EmptyDrive())
|
||||
monkeypatch.setattr(acquisition, "drive_service", ReadyDrive())
|
||||
|
||||
result = acquisition.PChomeSalesAcquisitionService().run(trigger="test")
|
||||
|
||||
assert result["success"] is False
|
||||
assert result["status"] == "blocked_with_safe_next_action"
|
||||
assert result["safe_next_action"] == "scheduler_retry_authorized_sources"
|
||||
assert result["safe_next_action"] == "scheduled_retry_and_enabled_fallback_probe"
|
||||
assert result["next_retry_at"]
|
||||
assert result["source_reconciliation"]["contract"] == "pchome_sales_source_reconciliation_v1"
|
||||
assert result["source_reconciliation"]["manual_review_required"] is False
|
||||
assert result["providers"][1]["status"] == "disabled"
|
||||
assert result["providers"][2]["status"] == "disabled"
|
||||
assert result["providers"][3]["status"] == "disabled"
|
||||
assert result["receipt_persisted"] is True
|
||||
with import_service.engine.connect() as conn:
|
||||
receipt = conn.execute(text(
|
||||
@@ -303,6 +310,7 @@ def test_auto_import_api_uses_governed_acquisition_service(monkeypatch):
|
||||
def test_scheduler_continues_when_legacy_hitl_pause_is_set(monkeypatch):
|
||||
import scheduler
|
||||
import services.agent_actions as agent_actions
|
||||
import services.notification_manager as notification_manager
|
||||
import services.pchome_sales_acquisition_service as acquisition
|
||||
|
||||
calls = []
|
||||
@@ -322,8 +330,19 @@ def test_scheduler_continues_when_legacy_hitl_pause_is_set(monkeypatch):
|
||||
"data_lag_days": 1,
|
||||
"decision_ready": True,
|
||||
"safe_next_action": "scheduler_retry_authorized_sources",
|
||||
"next_retry_at": "2026-07-14T19:00:00+08:00",
|
||||
"notification_policy": {
|
||||
"send": False,
|
||||
"reason": "duplicate_within_cooldown",
|
||||
"fingerprint": "same-state",
|
||||
},
|
||||
},
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
notification_manager,
|
||||
"NotificationManager",
|
||||
lambda: (_ for _ in ()).throw(AssertionError("suppressed notification must not send")),
|
||||
)
|
||||
monkeypatch.setattr(scheduler, "_save_stats", lambda name, stats: saved.update({"name": name, "stats": stats}))
|
||||
|
||||
scheduler.run_auto_import_task()
|
||||
@@ -332,3 +351,5 @@ def test_scheduler_continues_when_legacy_hitl_pause_is_set(monkeypatch):
|
||||
assert saved["name"] == "auto_import_task"
|
||||
assert saved["stats"]["status"] == "Degraded"
|
||||
assert saved["stats"]["receipt_persisted"] is True
|
||||
assert saved["stats"]["next_retry_at"] == "2026-07-14T19:00:00+08:00"
|
||||
assert saved["stats"]["notification_policy"]["send"] is False
|
||||
|
||||
126
tests/test_pchome_sales_source_reconciliation.py
Normal file
126
tests/test_pchome_sales_source_reconciliation.py
Normal file
@@ -0,0 +1,126 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from services.pchome_sales_source_reconciliation import build_source_reconciliation
|
||||
|
||||
|
||||
NOW = datetime(2026, 7, 14, 18, 30, tzinfo=timezone(timedelta(hours=8)))
|
||||
|
||||
|
||||
def _source(source_id, *, enabled=True, ready=True, status="ready"):
|
||||
return {
|
||||
"id": source_id,
|
||||
"enabled": enabled,
|
||||
"ready": ready,
|
||||
"status": status,
|
||||
}
|
||||
|
||||
|
||||
def _receipt(provider, *, completed_at, reconciliation=None):
|
||||
stages = {"providers": [provider]}
|
||||
if reconciliation:
|
||||
stages["source_reconciliation"] = reconciliation
|
||||
return {
|
||||
"status": "blocked_with_safe_next_action",
|
||||
"completed_at": completed_at,
|
||||
"stages": stages,
|
||||
}
|
||||
|
||||
|
||||
def test_stale_single_source_schedules_retry_and_reports_fallback_gap():
|
||||
payload = build_source_reconciliation(
|
||||
sources=[
|
||||
_source("google_drive"),
|
||||
_source("authorized_https", enabled=False, ready=False, status="disabled"),
|
||||
],
|
||||
provider_attempts=[
|
||||
{"source": "google_drive", "status": "upstream_stale", "file_count": 0},
|
||||
{"source": "authorized_https", "status": "disabled"},
|
||||
],
|
||||
prior_receipts=[],
|
||||
freshness={"latest_sales_date": "2026-06-24", "data_lag_days": 20, "decision_ready": False},
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
assert payload["contract"] == "pchome_sales_source_reconciliation_v1"
|
||||
assert payload["status"] == "degraded"
|
||||
assert payload["fallback_chain"] == ["google_drive"]
|
||||
assert payload["single_source_dependency"] is True
|
||||
assert payload["next_retry_at"] == "2026-07-14T19:00:00+08:00"
|
||||
assert payload["safe_next_action"] == "scheduled_retry_and_enabled_fallback_probe"
|
||||
assert payload["notification_policy"]["severity"] == "critical"
|
||||
assert payload["notification_policy"]["send"] is True
|
||||
assert payload["sources"][0]["consecutive_empty_runs"] == 1
|
||||
assert payload["sources"][1]["runtime_state"] == "disabled"
|
||||
|
||||
|
||||
def test_same_state_within_cooldown_suppresses_duplicate_notification():
|
||||
first = build_source_reconciliation(
|
||||
sources=[_source("google_drive")],
|
||||
provider_attempts=[{"source": "google_drive", "status": "upstream_stale"}],
|
||||
prior_receipts=[],
|
||||
freshness={"data_lag_days": 20, "decision_ready": False},
|
||||
now=NOW - timedelta(minutes=10),
|
||||
)
|
||||
history = [_receipt(
|
||||
{"source": "google_drive", "status": "upstream_stale"},
|
||||
completed_at=(NOW - timedelta(minutes=10)).isoformat(),
|
||||
reconciliation=first,
|
||||
)]
|
||||
|
||||
second = build_source_reconciliation(
|
||||
sources=[_source("google_drive")],
|
||||
provider_attempts=[{"source": "google_drive", "status": "upstream_stale"}],
|
||||
prior_receipts=history,
|
||||
freshness={"data_lag_days": 20, "decision_ready": False},
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
assert second["notification_policy"]["fingerprint"] == first["notification_policy"]["fingerprint"]
|
||||
assert second["notification_policy"]["send"] is False
|
||||
assert second["notification_policy"]["reason"] == "duplicate_within_cooldown"
|
||||
assert second["sources"][0]["consecutive_empty_runs"] == 2
|
||||
|
||||
|
||||
def test_provider_failures_use_bounded_exponential_backoff():
|
||||
failed = {"source": "authorized_https", "status": "failed", "error_kind": "http_fetch_failed"}
|
||||
history = [
|
||||
_receipt(failed, completed_at=(NOW - timedelta(minutes=30)).isoformat()),
|
||||
_receipt(failed, completed_at=(NOW - timedelta(minutes=60)).isoformat()),
|
||||
]
|
||||
|
||||
payload = build_source_reconciliation(
|
||||
sources=[_source("authorized_https")],
|
||||
provider_attempts=[failed],
|
||||
prior_receipts=history,
|
||||
freshness={"data_lag_days": 5, "decision_ready": False},
|
||||
now=NOW,
|
||||
retry_interval_minutes=30,
|
||||
max_failure_backoff_minutes=240,
|
||||
)
|
||||
|
||||
source = payload["sources"][0]
|
||||
assert source["runtime_state"] == "failed"
|
||||
assert source["consecutive_failure_runs"] == 3
|
||||
assert source["retry_minutes"] == 120
|
||||
assert source["next_retry_at"] == "2026-07-14T20:30:00+08:00"
|
||||
assert source["action"] == "retry_with_bounded_backoff_and_probe_fallback"
|
||||
|
||||
|
||||
def test_duplicate_source_remains_on_scheduled_retry_without_writing():
|
||||
payload = build_source_reconciliation(
|
||||
sources=[_source("google_drive")],
|
||||
provider_attempts=[{
|
||||
"source": "google_drive",
|
||||
"status": "duplicate",
|
||||
"duplicate_count": 1,
|
||||
}],
|
||||
prior_receipts=[],
|
||||
freshness={"data_lag_days": 10, "decision_ready": False},
|
||||
now=NOW,
|
||||
)
|
||||
|
||||
source = payload["sources"][0]
|
||||
assert source["runtime_state"] == "duplicate"
|
||||
assert source["retry_minutes"] == 30
|
||||
assert source["next_retry_at"] == "2026-07-14T19:00:00+08:00"
|
||||
assert payload["safe_next_action"] == "scheduled_retry_and_enabled_fallback_probe"
|
||||
Reference in New Issue
Block a user