2 Commits
Author SHA1 Message Date
AFFAANhandClaude Opus 5 4022c8a1f6 fix(meta): refuse an ad set with no audience instead of letting Meta reject it
Graph requires targeting on every ad set. An empty targeting dict reached
Meta and failed opaquely part-way into building the object chain, leaving
a campaign and nothing else. This names the missing field before any
network call, matching how the module already handles a missing objective
and a missing daily budget.

The wizard defaults age and countries, so this only fires on campaigns
built through the API without targeting.

Also fixes the live smoke test, which used Graph's nested geo_locations
shape rather than the flat spec.targeting["countries"] the mapper reads.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:48:34 +05:30
AFFAANhandClaude Opus 5 2fc5f8a837 docs(campaigns): document Phase 2, and add the live smoke test
The README still described Phase 1's "creates nothing in Meta" contract,
which is no longer true. It now covers the lifecycle, that sync creates
only paused objects and why that is enforced in three places, what launch
requires, adopt/delete semantics for imported campaigns, and the
reconciler interval.

Also documents the separate campaign_test database. Without it the
repository integration tests skip and the suite still passes, so the skip
count matters as much as the exit code.

The live smoke test is the only thing that proves Meta accepts what the
client sends: every other test runs against a fake transport. It syncs a
real campaign, reads back from Graph that all three objects are PAUSED,
and deletes them in a finally block. Opt-in via MASKANX_LIVE_TESTS=1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-03 17:39:05 +05:30