diff --git a/src/adclaw/campaigns/diagnostics.py b/src/adclaw/campaigns/diagnostics.py index dcaedaa..903345a 100644 --- a/src/adclaw/campaigns/diagnostics.py +++ b/src/adclaw/campaigns/diagnostics.py @@ -238,11 +238,32 @@ async def run_diagnostics(meta_factory, ad_account_id: str | None) -> dict[str, return _summarise(checks) +# The checks the smoke test actually depends on. +# +# It creates PAUSED objects and deletes them, so it needs a token, an ad +# account it can write to, and a Page to attach the creative to — but not a +# payment method, and not an operator identity. Gating it on full readiness +# would put it out of reach of the person who most needs it: someone whose +# account has no funding yet, trying to find out whether Meta accepts what +# MaskanX sends at all. +_SMOKE_TEST_CHECKS = frozenset( + {"Meta access token", "Token is valid", "Ad account", "Account status", + "Facebook Page"}, +) + + def _summarise(checks: list[Check]) -> dict[str, Any]: failed = [c for c in checks if c.status == FAILED] + blocked_names = {c.name for c in failed} return { "ready": not failed, "blocking": [c.name for c in failed], + # Separate from `ready`: a launch needs everything, a connection + # test needs only what it touches. + "can_smoke_test": not (blocked_names & _SMOKE_TEST_CHECKS), + "smoke_test_blocking": [ + c.name for c in failed if c.name in _SMOKE_TEST_CHECKS + ], "checks": [asdict(c) for c in checks], } diff --git a/tests/test_campaign_diagnostics.py b/tests/test_campaign_diagnostics.py index ba91493..181e0b5 100644 --- a/tests/test_campaign_diagnostics.py +++ b/tests/test_campaign_diagnostics.py @@ -362,3 +362,59 @@ def test_every_env_var_a_remedy_names_is_offered_in_settings(): f"Environments, but they are not in _KEY_REGISTRY so no field is " f"shown for them." ) + + +# --- what blocks a launch is not what blocks a connection test --- + + +async def test_a_missing_payment_method_does_not_block_the_connection_test( + configured, +): + """The smoke test creates PAUSED objects. Meta does not need a funding + source for that, and someone with an unfunded account is exactly who + needs to find out whether the connection works at all.""" + meta = FakeMeta(account={ + "id": "act_1", "name": "Maskan-MetaAds", "currency": "INR", + "account_status": 1, "funding_source": None, "min_daily_budget": 9709, + }) + + report = await run_diagnostics(lambda: meta, None) + + assert report["ready"] is False + assert "Payment method" in report["blocking"] + assert report["can_smoke_test"] is True + assert report["smoke_test_blocking"] == [] + + +async def test_missing_operator_tokens_do_not_block_the_connection_test( + monkeypatch, +): + monkeypatch.setenv("META_ADS_ACCOUNT_ID", "act_1") + monkeypatch.setenv("META_PAGE_ID", "page_1") + monkeypatch.delenv("MASKANX_OPERATOR_TOKENS", raising=False) + + report = await run_diagnostics(lambda: FakeMeta(), None) + + assert "Operator identity" in report["blocking"] + assert report["can_smoke_test"] is True + + +async def test_an_unreachable_page_does_block_the_connection_test(configured): + """Sync attaches the creative to a Page, so this one it genuinely needs.""" + meta = FakeMeta() + meta.fail_page = True + + report = await run_diagnostics(lambda: meta, None) + + assert report["can_smoke_test"] is False + assert report["smoke_test_blocking"] == ["Facebook Page"] + + +async def test_no_token_blocks_the_connection_test(monkeypatch): + def no_token(): + raise MetaNotConfiguredError("META_ADS_ACCESS_TOKEN is not set.") + + report = await run_diagnostics(no_token, None) + + assert report["can_smoke_test"] is False + assert report["smoke_test_blocking"] == ["Meta access token"]