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/stripehad 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
| Property | minimal-advertiser | maximal-advertiser |
|---|---|---|
| Bids | 1 × $1.00 | 3 × ($10.00, $5.00, $5.00) — $20.00 total |
| Ad enrichment | Required fields only | All: logo_url, brand_color, tagline,
key_stat, cta_label, docs_url, hero_url |
| Advertiser PATCH | None | All fields: name, tagline, logo_url, brand_color, key_stat, cta_label, docs_url |
| Expected rank | 2 | 1 |
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)
| Step | Endpoint | Purpose |
|---|---|---|
| Probe | GET /catalog | Idempotency check |
| Bid + pay | POST /ads/bid → 402 → POST /payments/stripe/spt → retry | Per bid (4 total: 1 minimal, 3 maximal) |
| Webhook wait | GET /ads/{id}/bids (poll) | Confirm settlement per bid |
| Enrich | PATCH /advertisers/maximal-advertiser | All advertiser profile fields |
| Rank poll | GET /ads/{id}/rank-status (MPP 402 flow) | Demonstrate gated polling; confirm rank 1 |
| Assert | GET /catalog, GET /advertisers, GET /advertisers/{slug} ×2, GET /ads/{id}/bids, GET /ads/{id}/rank-status | Verify 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.mjsneed updating - Idempotency detection must recognize both personas
Alternatives Considered
| Option | Why not chosen |
|---|---|
| Keep single demo-advertiser | Can't show minimal vs. maximal contrast simultaneously |
| Optional advertiser_slug, validate in env tests only | Invariant becomes soft convention, not enforced contract; orphaned records pollute the model |
| Skip webhook wait | Bid status non-deterministic; assertions on settlement would be flaky |
| New tmux pane for env-test output | Dev shell pane already captures task dev output; adding a pane adds complexity without visibility benefit |