ADR-012 Accepted 2026-05-24

Dev Fixture Personas & Environment Verification

The dev seed produces two named fixture personas — minimal-advertiser and maximal-advertiser — via the real MPP 402 flow, including Stripe webhook confirmation. After seeding, task dev appends a dev-environment assertion test that verifies the known state against the live REST API. advertiser_slug is required on all bids.

Context

The original seed created a single demo-advertiser at $2.00 with a handful of profile fields. Several gaps emerged as the surface area grew:

  • Anonymous bids were allowed but don't make domain sense. An ad without an advertiser is orphaned data — there is no entity to own the X-Api-Key, no profile to enrich, no spend to aggregate.
  • A single fixture can't show contrast. It can't simultaneously demonstrate the minimal viable bid and the full enrichment surface.
  • Webhook settlement was skipped. The seed moved on regardless of whether POST /webhooks/stripe had confirmed payment, leaving bids potentially pending.
  • No assertion that the environment reached expected state. Drift between seed intent and observed API output was only discovered manually.

Decision

1. advertiser_slug is required on all bids

POST /ads/bid requires advertiser_slug in the request body. Anonymous bids are rejected with 422. The OpenAPI spec, JSON schema, and place-bid Lambda all enforce this.

2. Two fixture personas replace demo-advertiser
Propertyminimal-advertisermaximal-advertiser
Bids1 × $1.003 × ($10.00, $5.00, $5.00) — $20.00 total
Ad enrichmentRequired fields onlyAll: logo_url, brand_color, tagline, key_stat, cta_label, docs_url, hero_url
Advertiser PATCHNoneAll fields: name, tagline, logo_url, brand_color, key_stat, cta_label, docs_url
Expected rank21
3. Seed waits for Stripe webhook settlement per bid

After each bid's 200 response, the seed polls GET /ads/{ad_id}/bids until the latest bid shows settled status, or times out after 30 seconds.

4. task dev appends dev-environment assertions

After seed completes, task dev runs task dev:env-test (node tests/dev-env.mjs), asserting the known fixture state against the live API. On failure, the shell pane shows assertion output and exits non-zero.

Endpoints Exercised (seed + assertions)

StepEndpointPurpose
ProbeGET /catalogIdempotency check
Bid + payPOST /ads/bid → 402 → POST /payments/stripe/spt → retryPer bid (4 total: 1 minimal, 3 maximal)
Webhook waitGET /ads/{id}/bids (poll)Confirm settlement per bid
EnrichPATCH /advertisers/maximal-advertiserAll advertiser profile fields
Rank pollGET /ads/{id}/rank-status (MPP 402 flow)Demonstrate gated polling; confirm rank 1
AssertGET /catalog, GET /advertisers, GET /advertisers/{slug} ×2, GET /ads/{id}/bids, GET /ads/{id}/rank-statusVerify known fixture state

Consequences

Positive
  • Every ad is attributable to an advertiser — invariant enforced at API boundary
  • Both enrichment extremes visible in catalog on every cold start
  • All enrichment fields exercised on every seed — field rot caught early
  • Bid history populated (3 bids) so timeline endpoints always have data
  • Dev-env assertions catch drift automatically
Negative
  • Seed slower — webhook wait adds latency per bid
  • Stripe CLI must be running or webhook wait times out
  • Existing anonymous-bid tests in api-smoke.mjs need updating
  • Idempotency detection must recognize both personas

Alternatives Considered

OptionWhy not chosen
Keep single demo-advertiserCan't show minimal vs. maximal contrast simultaneously
Optional advertiser_slug, validate in env tests onlyInvariant becomes soft convention, not enforced contract; orphaned records pollute the model
Skip webhook waitBid status non-deterministic; assertions on settlement would be flaky
New tmux pane for env-test outputDev shell pane already captures task dev output; adding a pane adds complexity without visibility benefit