Skip to content

Ad Network as a first class section - Manual Test Plan โ€‹

FieldValue
Branch / PRfeat/admin-and-merchant-portals (#986)
Environmenthttp://localhost:3012 (hers, the SECOND admin frontend, not 3001). Hits live lantern-app-dev Firebase. 3001 was avoided deliberately: it had been up 26 hours and had demonstrably missed changes on disk, so a pass there can confirm old code.
Build flags / confignone. VITE_MERCHANTS_API_URL=/api and MERCHANTS_API_ORIGIN must be set in apps/admin/.env.local
Build (commit)the working tree at 2026-08-28, after the #981 proxy fix
TesterClaude (headless Playwright, signed in as Agent Probe)
Date2026-08-28

Summary โ€‹

#ScenarioResultNotes
1The section renders with three tabs, each reaching a real surface[x] passAutomated route pass
2Delivery is the old dashboard, mounted body-only[x] passOne page header, not two
3Review is the old queue, mounted body-only, and reads REAL data[x] passThe important one. See below
4Placements shows real inventory and honest blanks[x] passThree surfaces from the shared vocabulary
5Both old paths redirect[x] passAnalytics and Merchants
6Merchants no longer offers Offer review[x] passTwo sub-items remain
7A non-JSON 200 raises instead of becoming an empty list[x] passMeasured before and after
8CONTROL: the sections this work did not touch are unchanged[x] passShared with the Venues plan, 18 sections
9Approving or rejecting an offer still works[ ] NOT EXERCISEDNeeds an offer in pending_review. See below
10The Delivery chart renders with real rollup data[ ] NOT EXERCISEDNeeds the rollup to have run. See below

Scenario 3 is the falsifying one for this plan. Its arms have opposite predictions, and the point is separating them: see its note.

Setup / preconditions โ€‹

  • Her stack: admin portal on 3012, APIs on 8080 to 8086. Do not restart or rebuild them.
  • Sign in with the Agent Probe admin (~/.lantern-agent-probe.env, AGENT_PROBE_PORTAL_PASSWORD, the PORTAL password, not the app passphrase: admin auth and app auth are deliberately separate systems).
  • Re-run the automated route pass with the harness in docs/projects/admin-venues-section/harness/.
  • Budget your logins. /auth/admin/signin rate-limits at 10 per 15 minutes per IP, and the admin and merchant portals SHARE one bucket. Worse, a 429 from the limiter is indistinguishable by status from the five-strike account lockout, so read the response's error code (RATE_LIMITED versus ACCOUNT_LOCKED), never its status. Use a persistent browser profile so the run costs one login rather than one per route.

Scenarios โ€‹

1. The section renders with three tabs, each reaching a real surface โ€‹

  • Steps: open /admin/ad-network; click Delivery, Placements, Review; reload on a non-default tab.
  • Expected: index redirects to Delivery. Three tabs, each with its own URL. A reload lands on the same tab, because the tab is in the URL rather than in state.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    /admin/ad-network redirects to /admin/ad-network/delivery. All three tabs settled on their own selector.
Evidence:  docs/projects/admin-venues-section/screenshots/live/adnetwork-delivery.png, adnetwork-placements.png, adnetwork-review.png
Follow-up:

2. Delivery is the old dashboard, mounted body-only โ€‹

  • Goal: the section owns the header. Mounted un-embedded, the page would draw a SECOND page header under the section's own.
  • Expected: one page header (Ad Network), one tab strip, and the dashboard's refresh controls in the body rather than in a header of their own.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    One header. Reporting window bar sized to content, refresh controls grouped at the end of the row.
Evidence:  screenshots/live/adnetwork-delivery.png, plus an AdminShell test asserting data-embedded="true"
Follow-up:

3. Review is the old queue, mounted body-only, and reads REAL data โ€‹

  • This is the falsifying scenario, and it exists because an empty queue has three possible causes that look identical. State them before running: (a) there genuinely are no offers awaiting review, (b) #981, where vite proxied no /api/admin route so the request was answered by the app shell, or (c) the harness gave up before the surface settled. An empty screenshot cannot tell you which. If the queue renders empty and you cannot separate the three, the correct report is "unknown", not "empty".
  • Steps:
    1. Open /admin/ad-network/review and wait for a CONDITION, not a timeout.
    2. Record the actual network response for /api/admin/offers: status, content-type, and body.
  • Expected: a 200 application/json. An empty list then means (a). A 200 text/html means (b). No response at all means the wait was (c).
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    All three separated. GET /api/admin/offers returned 200 application/json with body {"offers":[],"count":0}.
           So the queue is GENUINELY empty in dev, not silently broken. (b) was ruled out by fixing the proxy and
           re-measuring: an unauthenticated curl went from 200 text/html to 401 application/json, which proves the
           request now reaches merchants-api instead of the page. (c) was ruled out by waiting on a selector.
Evidence:  screenshots/live/adnetwork-review.png; the recorded response above
Follow-up: The 2026-08-24 design survey recorded this queue as empty and reasoned about it as a product state. It
           happens to be empty today, but that survey had no way to know, so its reasoning was unfounded at the time.

4. Placements shows real inventory and honest blanks โ€‹

  • Expected: three surfaces (Hero rail, Inline venue card, Feed insertion) derived from PLACEMENTS in @lantern/shared/ads, each with where it appears and how contended it is. The three per-surface figures render as a dated Scaffold pointing at #998, NOT as zeroes. Unknown is absent, with a note explaining it is a data-quality bucket rather than a fourth surface.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    All three render with contention badges. Scaffold reads "Fill rate, eligible offers and CTR are not
           flowing per surface yet", dated 2026-08-28, linking #998. No zeroes anywhere.
Evidence:  screenshots/live/adnetwork-placements.png
Follow-up:

5. Both old paths redirect โ€‹

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    /admin/analytics/dashboards/ads -> /admin/ad-network/delivery.
           /admin/merchants/offer-review -> /admin/ad-network/review.
Evidence:  screenshots/live/redirect-analytics-ads.png, redirect-merchants-offer-review.png
Follow-up:

6. Merchants no longer offers Offer review โ€‹

  • Goal: the section changed meaning, and no dead nav entry is left behind.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    Merchants expands to All Merchants and Applications only. Both still render.
Evidence:  the control walk covered both Merchants routes
Follow-up:

7. A non-JSON 200 raises instead of becoming an empty list โ€‹

  • Goal: the silent-failure class, not just this instance. parseResponse turned an HTML body into { message: '<html>' }, so result.offers || [] rendered a confident empty queue with no error and no failed request.
  • Verify: curl the proxied path unauthenticated and read the content-type.
Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    before: /api/admin/offers -> 200 text/html (the app shell answering)
           after:  /api/admin/offers -> 401 application/json (merchants-api saying no)
           A 401 is the correct answer to an unauthenticated request and proves it reaches the service.
Evidence:  measured against both 3001 and 3012 after the vite restart
Follow-up: The guard is in merchantsApi only. The same shape exists in every other API client on this portal.

8. CONTROL: the sections this work did not touch are unchanged โ€‹

Result:    [x] pass   [ ] fail   [ ] blocked
Actual:    18 sections walked, all render, none swallowed by the catch-all.
Evidence:  node docs/projects/admin-venues-section/harness/control-walk.e2e.mjs
Follow-up:

9. Approving or rejecting an offer still works โ€‹

  • NOT EXERCISED. The queue is genuinely empty in dev, so there was nothing to approve or reject. This is the highest-value remaining gap: the move changed how the page is mounted, and approve/reject is the only behaviour on it.
  • What would close it: publish an offer as the TEST MERCHANT (not as an admin: an admin publish bypasses the moderation queue by design, and the signature in the offer doc differs, moderation: null for an admin publish versus { autoOk: true, flags: [] } through the gate). Then approve it here and confirm the status write.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    No offers in pending_review in dev.
Evidence:  {"offers":[],"count":0}
Follow-up: Seed one via a merchant publish, then run approve and reject.

10. The Delivery chart renders with real rollup data โ€‹

  • NOT EXERCISED. The dashboard renders and its empty state is honest ("the rollup lands daily via the aggregate-ad-delivery-daily job; a freshly provisioned table stays empty until its first run"), but no chart was drawn because there is no data.
  • What would close it: run against an environment where the daily rollup has produced rows, then confirm the Hero, Inline, Feed and Unknown series all chart.
Result:    [ ] pass   [ ] fail   [x] blocked
Actual:    Page renders, chart area shows its empty state.
Evidence:  screenshots/live/adnetwork-delivery.png
Follow-up:

Notes โ€‹

  • This block did the MOVE, not the Offer review redesign. The operator named that separately. The queue is ported exactly as it stands, and whatever is wrong with it stays wrong until that work.
  • What this run did not do: click anything. It loaded routes and recorded responses. No offer was approved, no tab was clicked by hand, no form was submitted.

Built with VitePress