A live sync against the real account failed with Meta code 100, subcode
4834011, and the only thing we stored was the literal string "Invalid
parameter". Meta had written an explanation — it puts one in
error_user_title and error_user_msg — and MetaError was discarding it.
Every explanatory field is now kept, describe() prefers the text written
for a person, and as_dict() carries the lot into the API response and the
stored sync_error.
That still left diagnosis needing a terminal, which is no good: the person
who has to attach a payment method or fix a Page id is a client, not a
developer. GET /campaigns/diagnostics runs seven ordered readiness checks
and returns a remedy with each failure. It stops at the first hard failure
— with no token every later check fails for the same reason, and five
identical errors hide the one that matters.
POST /campaigns/diagnostics/smoke-test closes the remaining gap: the unit
suite proves we send what we think we send, not that Meta accepts it. It
builds the real chain, reads back from Graph that every object is PAUSED,
and deletes in a finally. If cleanup fails, the ids come back in the
response rather than being abandoned in a real ad account.
Both routes are declared above /{campaign_id}. /diagnostics is a single
path segment, so the wildcard would otherwise answer it — the same
shadowing /discover and /adopt are already guarded against. Four tests go
through HTTP to pin the ordering, because the rest of this file's tests
call the functions directly and would pass against an unreachable
endpoint.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>